Backup
This commit is contained in:
parent
37c2a1db67
commit
312eb2c4bd
@ -53,6 +53,8 @@ class HetznerService
|
|||||||
'status' => ($i % 3) === 0 ? 'stopped' : 'running',
|
'status' => ($i % 3) === 0 ? 'stopped' : 'running',
|
||||||
'type' => 'cx' . (11 + (($i % 4) * 10)),
|
'type' => 'cx' . (11 + (($i % 4) * 10)),
|
||||||
'ipv4' => sprintf('192.168.%d.%d', floor($i / 255), $i % 255),
|
'ipv4' => sprintf('192.168.%d.%d', floor($i / 255), $i % 255),
|
||||||
|
'docker_status' => 'pending',
|
||||||
|
'domains' => [],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
return $testData;
|
return $testData;
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace ServerManager\UI;
|
namespace ServerManager\UI;
|
||||||
|
|
||||||
|
use PHPNative\Async\TaskManager;
|
||||||
use PHPNative\Tailwind\Data\Icon as IconName;
|
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;
|
||||||
@ -30,6 +31,7 @@ class ServerListTab
|
|||||||
private Label $detailStatus;
|
private Label $detailStatus;
|
||||||
private Label $detailType;
|
private Label $detailType;
|
||||||
private Label $detailIpv4;
|
private Label $detailIpv4;
|
||||||
|
private Container $detailDomainsContainer;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
string &$apiKey,
|
string &$apiKey,
|
||||||
@ -66,11 +68,12 @@ class ServerListTab
|
|||||||
// Table
|
// Table
|
||||||
$this->table = new Table(style: ' flex-1');
|
$this->table = new Table(style: ' flex-1');
|
||||||
$this->table->setColumns([
|
$this->table->setColumns([
|
||||||
['key' => 'id', 'title' => 'ID', 'width' => 100],
|
['key' => 'id', 'title' => 'ID', 'width' => 80],
|
||||||
['key' => 'name', 'title' => 'Name', 'width' => 400],
|
['key' => 'name', 'title' => 'Name'],
|
||||||
['key' => 'status', 'title' => 'Status', 'width' => 120],
|
['key' => 'status', 'title' => 'Status', 'width' => 100],
|
||||||
['key' => 'type', 'title' => 'Typ', 'width' => 120],
|
['key' => 'type', 'title' => 'Typ', 'width' => 80],
|
||||||
['key' => 'ipv4', 'title' => 'IPv4'],
|
['key' => 'ipv4', 'title' => 'IPv4', 'width' => 160],
|
||||||
|
['key' => 'docker_status', 'title' => 'Docker', 'width' => 100],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Load initial test data
|
// Load initial test data
|
||||||
@ -101,6 +104,11 @@ class ServerListTab
|
|||||||
$detailPanel->addComponent(new Label('IPv4:', 'text-xs text-gray-500 mt-2'));
|
$detailPanel->addComponent(new Label('IPv4:', 'text-xs text-gray-500 mt-2'));
|
||||||
$detailPanel->addComponent($this->detailIpv4);
|
$detailPanel->addComponent($this->detailIpv4);
|
||||||
|
|
||||||
|
// Domains list
|
||||||
|
$detailPanel->addComponent(new Label('Domains:', 'text-xs text-gray-500 mt-2'));
|
||||||
|
$this->detailDomainsContainer = new Container('flex flex-col gap-1');
|
||||||
|
$detailPanel->addComponent($this->detailDomainsContainer);
|
||||||
|
|
||||||
// SFTP Manager Button (handler will be set by SftpManagerTab)
|
// SFTP Manager Button (handler will be set by SftpManagerTab)
|
||||||
$this->sftpButton = new Button(
|
$this->sftpButton = new Button(
|
||||||
'SFTP Manager öffnen',
|
'SFTP Manager öffnen',
|
||||||
@ -184,6 +192,9 @@ class ServerListTab
|
|||||||
$serverListTab->detailType->setText($row['type']);
|
$serverListTab->detailType->setText($row['type']);
|
||||||
$serverListTab->detailIpv4->setText($row['ipv4']);
|
$serverListTab->detailIpv4->setText($row['ipv4']);
|
||||||
|
|
||||||
|
$domains = $row['domains'] ?? [];
|
||||||
|
$serverListTab->updateDomainDetails(is_array($domains) ? $domains : []);
|
||||||
|
|
||||||
$serverListTab->selectedServer = $row;
|
$serverListTab->selectedServer = $row;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -196,13 +207,18 @@ class ServerListTab
|
|||||||
$serverListTab->table->setData($serverListTab->currentServerData);
|
$serverListTab->table->setData($serverListTab->currentServerData);
|
||||||
} else {
|
} else {
|
||||||
$filteredData = array_filter($serverListTab->currentServerData, function ($row) use ($searchTerm) {
|
$filteredData = array_filter($serverListTab->currentServerData, function ($row) use ($searchTerm) {
|
||||||
return str_contains(strtolower($row['name']), $searchTerm);
|
return (
|
||||||
|
str_contains(strtolower($row['name']), $searchTerm) ||
|
||||||
|
count(array_filter($row['domains'], function ($item) use ($searchTerm) {
|
||||||
|
return str_contains(strtolower($item), $searchTerm);
|
||||||
|
}))
|
||||||
|
);
|
||||||
});
|
});
|
||||||
$serverListTab->table->setData(array_values($filteredData));
|
$serverListTab->table->setData(array_values($filteredData));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Refresh button - use reference to apiKey variable
|
// Refresh button - use reference to apiKey & privateKey variable
|
||||||
$this->refreshButton->setOnClickAsync(
|
$this->refreshButton->setOnClickAsync(
|
||||||
function () use (&$currentApiKey) {
|
function () use (&$currentApiKey) {
|
||||||
try {
|
try {
|
||||||
@ -220,6 +236,8 @@ class ServerListTab
|
|||||||
'status' => $server->status,
|
'status' => $server->status,
|
||||||
'type' => $server->serverType->name,
|
'type' => $server->serverType->name,
|
||||||
'ipv4' => $server->publicNet->ipv4->ip,
|
'ipv4' => $server->publicNet->ipv4->ip,
|
||||||
|
'docker_status' => 'pending',
|
||||||
|
'domains' => [],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -232,32 +250,147 @@ class ServerListTab
|
|||||||
return ['error' => 'Exception: ' . $e->getMessage()];
|
return ['error' => 'Exception: ' . $e->getMessage()];
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
function ($result) use ($serverListTab) {
|
function ($result) use (&$serverListTab, &$currentPrivateKeyPath) {
|
||||||
if (is_array($result)) {
|
if (is_array($result)) {
|
||||||
if (isset($result['error'])) {
|
if (isset($result['error'])) {
|
||||||
$serverListTab->statusLabel->setText('Fehler: ' . $result['error']);
|
$serverListTab->statusLabel->setText('Fehler: ' . $result['error']);
|
||||||
echo "Error: {$result['error']}\n";
|
echo "Error: {$result['error']}\n";
|
||||||
} elseif (isset($result['success'], $result['servers'])) {
|
} elseif (isset($result['success'], $result['servers'])) {
|
||||||
$serverListTab->currentServerData = $result['servers'];
|
// Basisdaten setzen, Docker-Status initial auf "pending"
|
||||||
|
$serverListTab->currentServerData = array_map(static fn($row) => array_merge([
|
||||||
|
'docker_status' => 'pending',
|
||||||
|
'docker' => null,
|
||||||
|
'docker_error' => null,
|
||||||
|
], $row), $result['servers']);
|
||||||
|
|
||||||
$searchTerm = strtolower(trim($serverListTab->searchInput->getValue()));
|
|
||||||
if (empty($searchTerm)) {
|
|
||||||
$serverListTab->table->setData($serverListTab->currentServerData);
|
$serverListTab->table->setData($serverListTab->currentServerData);
|
||||||
} else {
|
|
||||||
$filteredData = array_filter($serverListTab->currentServerData, function ($row) use (
|
|
||||||
$searchTerm,
|
|
||||||
) {
|
|
||||||
return str_contains(strtolower($row['name']), $searchTerm);
|
|
||||||
});
|
|
||||||
$serverListTab->table->setData(array_values($filteredData));
|
|
||||||
}
|
|
||||||
|
|
||||||
$serverListTab->statusLabel->setText('Server geladen: ' . $result['count'] . ' gefunden');
|
$serverListTab->statusLabel->setText('Server geladen: ' . $result['count'] . ' gefunden');
|
||||||
echo "Success: {$result['count']} servers loaded\n";
|
echo "Success: {$result['count']} servers loaded\n";
|
||||||
|
|
||||||
|
// Danach: pro Server asynchron Docker-Infos nachladen
|
||||||
|
foreach ($serverListTab->currentServerData as $index => $row) {
|
||||||
|
$ip = $row['ipv4'] ?? '';
|
||||||
|
|
||||||
|
if (empty($ip) || empty($currentPrivateKeyPath) || !file_exists($currentPrivateKeyPath)) {
|
||||||
|
$serverListTab->currentServerData[$index]['docker_error'] = 'Kein gültiger Private-Key oder IP';
|
||||||
|
$serverListTab->currentServerData[$index]['docker_status'] = 'error';
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$task = TaskManager::getInstance()->runAsync(function () use (
|
||||||
|
$ip,
|
||||||
|
$currentPrivateKeyPath,
|
||||||
|
$index,
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
$ssh = new \phpseclib3\Net\SSH2($ip);
|
||||||
|
$key = \phpseclib3\Crypt\PublicKeyLoader::loadPrivateKey(file_get_contents(
|
||||||
|
$currentPrivateKeyPath,
|
||||||
|
));
|
||||||
|
|
||||||
|
if (!$ssh->login('root', $key)) {
|
||||||
|
return [
|
||||||
|
'index' => $index,
|
||||||
|
'docker' => null,
|
||||||
|
'docker_error' => 'SSH Login fehlgeschlagen',
|
||||||
|
'docker_status' => 'error',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$output = $ssh->exec('docker inspect psc-web-1');
|
||||||
|
|
||||||
|
if (empty($output)) {
|
||||||
|
return [
|
||||||
|
'index' => $index,
|
||||||
|
'docker' => null,
|
||||||
|
'docker_error' => 'Leere Docker-Antwort',
|
||||||
|
'docker_status' => 'error',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$json = json_decode($output, true);
|
||||||
|
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||||
|
return [
|
||||||
|
'index' => $index,
|
||||||
|
'docker' => null,
|
||||||
|
'docker_error' => 'Ungültige Docker-JSON-Antwort',
|
||||||
|
'docker_status' => 'error',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'index' => $index,
|
||||||
|
'docker' => $json,
|
||||||
|
'docker_error' => null,
|
||||||
|
'docker_status' => 'ok',
|
||||||
|
];
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
return [
|
||||||
|
'index' => $index,
|
||||||
|
'docker' => null,
|
||||||
|
'docker_error' => 'SSH-Fehler: ' . $e->getMessage(),
|
||||||
|
'docker_status' => 'error',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$task->onComplete(function ($dockerResult) use (&$serverListTab) {
|
||||||
|
if (!is_array($dockerResult) || !isset($dockerResult['index'])) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$i = $dockerResult['index'];
|
||||||
|
if (!isset($serverListTab->currentServerData[$i])) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (array_key_exists('docker', $dockerResult)) {
|
||||||
|
if (isset($dockerResult['docker'][0]['Config']['Env'])) {
|
||||||
|
$hosts = array_filter(
|
||||||
|
$dockerResult['docker'][0]['Config']['Env'],
|
||||||
|
function ($item) {
|
||||||
|
if (str_starts_with($item, 'LETSENCRYPT_HOST')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
$domains = explode(
|
||||||
|
',',
|
||||||
|
substr(array_first($hosts), strlen('LETSENCRYPT_HOST=')),
|
||||||
|
);
|
||||||
|
|
||||||
|
$serverListTab->currentServerData[$i]['domains'] = $domains;
|
||||||
|
}
|
||||||
|
$serverListTab->currentServerData[$i]['docker'] = $dockerResult['docker'];
|
||||||
|
$serverListTab->table->setData($serverListTab->currentServerData);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (array_key_exists('docker_error', $dockerResult)) {
|
||||||
|
$serverListTab->currentServerData[$i]['docker_error'] =
|
||||||
|
$dockerResult['docker_error'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (array_key_exists('docker_status', $dockerResult)) {
|
||||||
|
$serverListTab->currentServerData[$i]['docker_status'] =
|
||||||
|
$dockerResult['docker_status'];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$task->onError(function ($error) use ($serverListTab, $index) {
|
||||||
|
$errorMsg = is_object($error) && method_exists($error, 'getMessage')
|
||||||
|
? $error->getMessage()
|
||||||
|
: ((string) $error);
|
||||||
|
if (isset($serverListTab->currentServerData[$index])) {
|
||||||
|
$serverListTab->currentServerData[$index]['docker_error'] =
|
||||||
|
'Async Fehler: ' . $errorMsg;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
function ($error) use ($serverListTab) {
|
function ($error) use (&$serverListTab) {
|
||||||
$errorMsg = is_object($error) && method_exists($error, 'getMessage')
|
$errorMsg = is_object($error) && method_exists($error, 'getMessage')
|
||||||
? $error->getMessage()
|
? $error->getMessage()
|
||||||
: ((string) $error);
|
: ((string) $error);
|
||||||
@ -276,4 +409,39 @@ class ServerListTab
|
|||||||
{
|
{
|
||||||
return $this->sftpButton;
|
return $this->sftpButton;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function updateDomainDetails(array $domains): void
|
||||||
|
{
|
||||||
|
$this->detailDomainsContainer->clearChildren();
|
||||||
|
|
||||||
|
if (empty($domains)) {
|
||||||
|
$this->detailDomainsContainer->addComponent(new Label(
|
||||||
|
'Keine Domains gefunden',
|
||||||
|
'text-xs text-gray-500 italic',
|
||||||
|
));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($domains as $domain) {
|
||||||
|
if (!is_string($domain) || trim($domain) === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$domain = trim($domain);
|
||||||
|
$button = new Button($domain, 'text-sm text-blue-600 hover:text-blue-800 underline text-left');
|
||||||
|
|
||||||
|
$button->setOnClick(function () use ($domain) {
|
||||||
|
$url = $domain;
|
||||||
|
if (!str_starts_with($url, 'http://') && !str_starts_with($url, 'https://')) {
|
||||||
|
$url = 'https://' . $url;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Versuche, die Domain im Standardbrowser zu öffnen (Linux-Umgebung)
|
||||||
|
$escapedUrl = escapeshellarg($url);
|
||||||
|
exec("xdg-open {$escapedUrl} > /dev/null 2>&1 &");
|
||||||
|
});
|
||||||
|
|
||||||
|
$this->detailDomainsContainer->addComponent($button);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -29,6 +29,8 @@ class SftpManagerTab
|
|||||||
private Modal $deleteConfirmModal;
|
private Modal $deleteConfirmModal;
|
||||||
private string $currentDeleteFilePath = '';
|
private string $currentDeleteFilePath = '';
|
||||||
private Label $deleteConfirmLabel;
|
private Label $deleteConfirmLabel;
|
||||||
|
private null|array $currentLocalSelection = null;
|
||||||
|
private null|array $currentRemoteSelection = null;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
string &$apiKey,
|
string &$apiKey,
|
||||||
@ -52,6 +54,15 @@ class SftpManagerTab
|
|||||||
);
|
);
|
||||||
$localBrowserContainer->addComponent($this->localFileBrowser);
|
$localBrowserContainer->addComponent($this->localFileBrowser);
|
||||||
|
|
||||||
|
// Track local file selection for uploads
|
||||||
|
$sftpTab = $this;
|
||||||
|
$this->localFileBrowser->setOnFileSelect(function ($path, $row) use ($sftpTab) {
|
||||||
|
$sftpTab->currentLocalSelection = [
|
||||||
|
'path' => $path,
|
||||||
|
'row' => $row,
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
// Right side: Remote file browser
|
// Right side: Remote file browser
|
||||||
$remoteBrowserContainer = new Container('flex flex-col flex-1 gap-2');
|
$remoteBrowserContainer = new Container('flex flex-col flex-1 gap-2');
|
||||||
|
|
||||||
@ -72,7 +83,22 @@ class SftpManagerTab
|
|||||||
$this->connectionStatusLabel = new Label('Nicht verbunden', 'text-sm text-gray-600 italic mb-2');
|
$this->connectionStatusLabel = new Label('Nicht verbunden', 'text-sm text-gray-600 italic mb-2');
|
||||||
$remoteBrowserContainer->addComponent($this->connectionStatusLabel);
|
$remoteBrowserContainer->addComponent($this->connectionStatusLabel);
|
||||||
|
|
||||||
|
// Middle: Transfer buttons (Upload/Download)
|
||||||
|
$transferContainer = new Container('flex flex-col justify-center items-center gap-2');
|
||||||
|
|
||||||
|
$uploadButton = new Button(
|
||||||
|
'Hochladen →',
|
||||||
|
'px-3 py-2 bg-blue-600 text-white rounded hover:bg-blue-700',
|
||||||
|
);
|
||||||
|
$downloadButton = new Button(
|
||||||
|
'← Herunterladen',
|
||||||
|
'px-3 py-2 bg-blue-600 text-white rounded hover:bg-blue-700',
|
||||||
|
);
|
||||||
|
$transferContainer->addComponent($uploadButton);
|
||||||
|
$transferContainer->addComponent($downloadButton);
|
||||||
|
|
||||||
$this->tab->addComponent($localBrowserContainer);
|
$this->tab->addComponent($localBrowserContainer);
|
||||||
|
$this->tab->addComponent($transferContainer);
|
||||||
$this->tab->addComponent($remoteBrowserContainer);
|
$this->tab->addComponent($remoteBrowserContainer);
|
||||||
|
|
||||||
// Setup remote navigation handler
|
// Setup remote navigation handler
|
||||||
@ -93,6 +119,15 @@ class SftpManagerTab
|
|||||||
// Create delete confirmation modal
|
// Create delete confirmation modal
|
||||||
$this->createDeleteConfirmModal($currentPrivateKeyPath, $serverListTab, $statusLabel);
|
$this->createDeleteConfirmModal($currentPrivateKeyPath, $serverListTab, $statusLabel);
|
||||||
|
|
||||||
|
// Setup transfer button handlers
|
||||||
|
$uploadButton->setOnClick(function () use ($sftpTab, &$currentPrivateKeyPath, $serverListTab, $statusLabel) {
|
||||||
|
$sftpTab->handleUpload($currentPrivateKeyPath, $serverListTab, $statusLabel);
|
||||||
|
});
|
||||||
|
|
||||||
|
$downloadButton->setOnClick(function () use ($sftpTab, &$currentPrivateKeyPath, $serverListTab, $statusLabel) {
|
||||||
|
$sftpTab->handleDownload($currentPrivateKeyPath, $serverListTab, $statusLabel);
|
||||||
|
});
|
||||||
|
|
||||||
// Setup file edit handler
|
// Setup file edit handler
|
||||||
$this->remoteFileBrowser->setOnEditFile(function ($path, $row) use (
|
$this->remoteFileBrowser->setOnEditFile(function ($path, $row) use (
|
||||||
&$currentPrivateKeyPath,
|
&$currentPrivateKeyPath,
|
||||||
@ -138,12 +173,20 @@ class SftpManagerTab
|
|||||||
// Create reference to selectedServer array outside of closure
|
// Create reference to selectedServer array outside of closure
|
||||||
$selectedServerRef = &$serverListTab->selectedServer;
|
$selectedServerRef = &$serverListTab->selectedServer;
|
||||||
|
|
||||||
$this->remoteFileBrowser->setOnFileSelect(function ($path, $row) use (
|
$this->remoteFileBrowser->setOnFileSelect(
|
||||||
|
function ($path, $row) use (
|
||||||
$sftpTab,
|
$sftpTab,
|
||||||
&$currentPrivateKeyPath,
|
&$currentPrivateKeyPath,
|
||||||
&$selectedServerRef,
|
&$selectedServerRef,
|
||||||
$statusLabel,
|
$statusLabel,
|
||||||
) {
|
) {
|
||||||
|
// Track remote selection (for downloads)
|
||||||
|
$sftpTab->currentRemoteSelection = [
|
||||||
|
'path' => $path,
|
||||||
|
'row' => $row,
|
||||||
|
];
|
||||||
|
|
||||||
|
// Only navigate when a directory is selected
|
||||||
if (!isset($row['isDir']) || !$row['isDir']) {
|
if (!isset($row['isDir']) || !$row['isDir']) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -210,7 +253,8 @@ class SftpManagerTab
|
|||||||
);
|
);
|
||||||
|
|
||||||
$loadButton->handleMouseClick(0, 0, 0);
|
$loadButton->handleMouseClick(0, 0, 0);
|
||||||
});
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function setupSftpConnectionHandler(
|
private function setupSftpConnectionHandler(
|
||||||
@ -689,6 +733,198 @@ class SftpManagerTab
|
|||||||
$loadButton->handleMouseClick(0, 0, 0);
|
$loadButton->handleMouseClick(0, 0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function handleUpload(
|
||||||
|
string &$currentPrivateKeyPath,
|
||||||
|
ServerListTab $serverListTab,
|
||||||
|
Label $statusLabel,
|
||||||
|
): void {
|
||||||
|
if ($this->currentLocalSelection === null) {
|
||||||
|
$statusLabel->setText('Keine lokale Datei ausgewählt');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$localRow = $this->currentLocalSelection['row'] ?? null;
|
||||||
|
$localPath = $this->currentLocalSelection['path'] ?? null;
|
||||||
|
|
||||||
|
if ($localRow === null || $localPath === null) {
|
||||||
|
$statusLabel->setText('Ungültige lokale Auswahl');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (($localRow['isDir'] ?? false) === true) {
|
||||||
|
$statusLabel->setText('Ordner-Upload wird noch nicht unterstützt');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!is_file($localPath) || !is_readable($localPath)) {
|
||||||
|
$statusLabel->setText('Lokale Datei ist nicht lesbar');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($serverListTab->selectedServer === null) {
|
||||||
|
$statusLabel->setText('Kein Server ausgewählt');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($currentPrivateKeyPath) || !file_exists($currentPrivateKeyPath)) {
|
||||||
|
$statusLabel->setText('Private Key Pfad nicht konfiguriert oder Datei nicht gefunden');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$remoteDir = $this->remoteFileBrowser->getCurrentPath();
|
||||||
|
if ($remoteDir === '') {
|
||||||
|
$remoteDir = '/';
|
||||||
|
}
|
||||||
|
|
||||||
|
$remotePath = rtrim($remoteDir, '/') . '/' . basename($localPath);
|
||||||
|
|
||||||
|
// Use reference to selected server for async operation
|
||||||
|
$selectedServerRef = &$serverListTab->selectedServer;
|
||||||
|
$sftpTab = $this;
|
||||||
|
|
||||||
|
$uploadAsyncButton = new Button('Upload', '');
|
||||||
|
$uploadAsyncButton->setOnClickAsync(
|
||||||
|
function () use (&$currentPrivateKeyPath, &$selectedServerRef, $localPath, $remotePath) {
|
||||||
|
if ($selectedServerRef === null || empty($currentPrivateKeyPath)) {
|
||||||
|
return ['error' => 'Not connected'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$selectedServer = $selectedServerRef;
|
||||||
|
|
||||||
|
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'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = $sftp->put($remotePath, $localPath, \phpseclib3\Net\SFTP::SOURCE_LOCAL_FILE);
|
||||||
|
if ($result === false) {
|
||||||
|
return ['error' => 'Upload fehlgeschlagen'];
|
||||||
|
}
|
||||||
|
|
||||||
|
return ['success' => true];
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return ['error' => $e->getMessage()];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
function ($result) use ($sftpTab, $statusLabel, &$currentPrivateKeyPath, &$selectedServerRef) {
|
||||||
|
if (isset($result['error'])) {
|
||||||
|
$statusLabel->setText('Fehler beim Hochladen: ' . $result['error']);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($result['success'])) {
|
||||||
|
$statusLabel->setText('Datei erfolgreich hochgeladen');
|
||||||
|
$sftpTab->reloadCurrentDirectory($currentPrivateKeyPath, $selectedServerRef, $statusLabel);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
function ($error) use ($statusLabel) {
|
||||||
|
$errorMsg = is_string($error) ? $error : 'Unknown error';
|
||||||
|
$statusLabel->setText('Fehler beim Hochladen: ' . $errorMsg);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
$uploadAsyncButton->handleMouseClick(0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function handleDownload(
|
||||||
|
string &$currentPrivateKeyPath,
|
||||||
|
ServerListTab $serverListTab,
|
||||||
|
Label $statusLabel,
|
||||||
|
): void {
|
||||||
|
if ($this->currentRemoteSelection === null) {
|
||||||
|
$statusLabel->setText('Keine Remote-Datei ausgewählt');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$remoteRow = $this->currentRemoteSelection['row'] ?? null;
|
||||||
|
$remotePath = $this->currentRemoteSelection['path'] ?? null;
|
||||||
|
|
||||||
|
if ($remoteRow === null || $remotePath === null) {
|
||||||
|
$statusLabel->setText('Ungültige Remote-Auswahl');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (($remoteRow['isDir'] ?? false) === true) {
|
||||||
|
$statusLabel->setText('Ordner-Download wird noch nicht unterstützt');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($serverListTab->selectedServer === null) {
|
||||||
|
$statusLabel->setText('Kein Server ausgewählt');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($currentPrivateKeyPath) || !file_exists($currentPrivateKeyPath)) {
|
||||||
|
$statusLabel->setText('Private Key Pfad nicht konfiguriert oder Datei nicht gefunden');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$localDir = $this->localFileBrowser->getCurrentPath();
|
||||||
|
if ($localDir === '') {
|
||||||
|
$localDir = getcwd();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!is_dir($localDir) || !is_writable($localDir)) {
|
||||||
|
$statusLabel->setText('Lokales Verzeichnis ist nicht beschreibbar');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$localPath = rtrim($localDir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . basename($remotePath);
|
||||||
|
|
||||||
|
// Use reference to selected server for async operation
|
||||||
|
$selectedServerRef = &$serverListTab->selectedServer;
|
||||||
|
$sftpTab = $this;
|
||||||
|
|
||||||
|
$downloadAsyncButton = new Button('Download', '');
|
||||||
|
$downloadAsyncButton->setOnClickAsync(
|
||||||
|
function () use (&$currentPrivateKeyPath, &$selectedServerRef, $remotePath, $localPath) {
|
||||||
|
if ($selectedServerRef === null || empty($currentPrivateKeyPath)) {
|
||||||
|
return ['error' => 'Not connected'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$selectedServer = $selectedServerRef;
|
||||||
|
|
||||||
|
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'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = $sftp->get($remotePath, $localPath);
|
||||||
|
if ($result === false) {
|
||||||
|
return ['error' => 'Download fehlgeschlagen'];
|
||||||
|
}
|
||||||
|
|
||||||
|
return ['success' => true];
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return ['error' => $e->getMessage()];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
function ($result) use ($sftpTab, $statusLabel, $localDir) {
|
||||||
|
if (isset($result['error'])) {
|
||||||
|
$statusLabel->setText('Fehler beim Herunterladen: ' . $result['error']);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($result['success'])) {
|
||||||
|
$statusLabel->setText('Datei erfolgreich heruntergeladen');
|
||||||
|
$sftpTab->localFileBrowser->loadDirectory($localDir);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
function ($error) use ($statusLabel) {
|
||||||
|
$errorMsg = is_string($error) ? $error : 'Unknown error';
|
||||||
|
$statusLabel->setText('Fehler beim Herunterladen: ' . $errorMsg);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
$downloadAsyncButton->handleMouseClick(0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
private function createRenameModal(
|
private function createRenameModal(
|
||||||
string &$currentPrivateKeyPath,
|
string &$currentPrivateKeyPath,
|
||||||
ServerListTab $serverListTab,
|
ServerListTab $serverListTab,
|
||||||
@ -922,4 +1158,3 @@ class SftpManagerTab
|
|||||||
$this->deleteConfirmModal->setVisible(true);
|
$this->deleteConfirmModal->setVisible(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -2,13 +2,7 @@
|
|||||||
{
|
{
|
||||||
"id": "board_691661d89de624.46726087",
|
"id": "board_691661d89de624.46726087",
|
||||||
"title": "Backlog",
|
"title": "Backlog",
|
||||||
"tasks": [
|
"tasks": []
|
||||||
{
|
|
||||||
"id": "task_691661d89de735.41071535",
|
|
||||||
"title": "Idee sammeln",
|
|
||||||
"note": ""
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "board_691661d89de806.79123800",
|
"id": "board_691661d89de806.79123800",
|
||||||
@ -18,6 +12,11 @@
|
|||||||
"id": "task_691661d89de858.46479539",
|
"id": "task_691661d89de858.46479539",
|
||||||
"title": "API anbinden",
|
"title": "API anbinden",
|
||||||
"note": ""
|
"note": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "task_691661d89de735.41071535",
|
||||||
|
"title": "Idee sammeln",
|
||||||
|
"note": ""
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@ -75,7 +75,7 @@ class TextRenderer
|
|||||||
|
|
||||||
private function loadFont(int $size): mixed
|
private function loadFont(int $size): mixed
|
||||||
{
|
{
|
||||||
$size = max(1, (int) round($size));
|
$size = max(1, (int) round($size)) * $this->pixelRatio;
|
||||||
|
|
||||||
if (isset($this->fonts[$size])) {
|
if (isset($this->fonts[$size])) {
|
||||||
return $this->fonts[$size];
|
return $this->fonts[$size];
|
||||||
@ -199,6 +199,7 @@ class TextRenderer
|
|||||||
'w' => $textSize['w'],
|
'w' => $textSize['w'],
|
||||||
'h' => $textSize['h'],
|
'h' => $textSize['h'],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Note: Texture and surface are automatically cleaned up by PHP resource destructors
|
// Note: Texture and surface are automatically cleaned up by PHP resource destructors
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -344,19 +344,6 @@ abstract class Component
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Optional: handle drag (mouse move with pressed button).
|
|
||||||
* Default: propagate to children.
|
|
||||||
*/
|
|
||||||
public function handleDrag(float $mouseX, float $mouseY): void
|
|
||||||
{
|
|
||||||
foreach ($this->children as $child) {
|
|
||||||
if (method_exists($child, 'handleDrag')) {
|
|
||||||
$child->handleDrag($mouseX, $mouseY);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public function layout(null|TextRenderer $textRenderer = null): void
|
public function layout(null|TextRenderer $textRenderer = null): void
|
||||||
{
|
{
|
||||||
$this->normalStylesCached = StyleParser::parse($this->style)->getValidStyles(
|
$this->normalStylesCached = StyleParser::parse($this->style)->getValidStyles(
|
||||||
@ -415,11 +402,12 @@ abstract class Component
|
|||||||
Profiler::increment('texture_cache_hit');
|
Profiler::increment('texture_cache_hit');
|
||||||
Profiler::start('render_cached');
|
Profiler::start('render_cached');
|
||||||
// Render cached texture
|
// Render cached texture
|
||||||
|
|
||||||
sdl_render_texture($renderer, $texture, [
|
sdl_render_texture($renderer, $texture, [
|
||||||
'x' => $this->viewport->x * $this->viewport->uiScale,
|
'x' => $this->viewport->x,
|
||||||
'y' => $this->viewport->y * $this->viewport->uiScale,
|
'y' => $this->viewport->y,
|
||||||
'w' => $this->viewport->width * $this->viewport->uiScale,
|
'w' => $this->viewport->width,
|
||||||
'h' => $this->viewport->height * $this->viewport->uiScale,
|
'h' => $this->viewport->height,
|
||||||
]);
|
]);
|
||||||
Profiler::end('render_cached');
|
Profiler::end('render_cached');
|
||||||
Profiler::end('render');
|
Profiler::end('render');
|
||||||
@ -458,12 +446,13 @@ abstract class Component
|
|||||||
// SDL3: sdl_rounded_box_ex uses (x1, y1, x2, y2) instead of (x, y, w, h)
|
// SDL3: sdl_rounded_box_ex uses (x1, y1, x2, y2) instead of (x, y, w, h)
|
||||||
$x2 = $this->viewport->x + $this->viewport->width;
|
$x2 = $this->viewport->x + $this->viewport->width;
|
||||||
$y2 = $this->viewport->y + $this->viewport->height;
|
$y2 = $this->viewport->y + $this->viewport->height;
|
||||||
|
|
||||||
sdl_rounded_box_ex(
|
sdl_rounded_box_ex(
|
||||||
$renderer,
|
$renderer,
|
||||||
((int) $this->viewport->x) * $this->viewport->uiScale,
|
(int) $this->viewport->x,
|
||||||
((int) $this->viewport->y) * $this->viewport->uiScale,
|
(int) $this->viewport->y,
|
||||||
((int) $x2) * $this->viewport->uiScale,
|
(int) $x2,
|
||||||
((int) $y2) * $this->viewport->uiScale,
|
(int) $y2,
|
||||||
$border->roundTopLeft ?? 0,
|
$border->roundTopLeft ?? 0,
|
||||||
$border->roundTopRight ?? 0,
|
$border->roundTopRight ?? 0,
|
||||||
$border->roundBottomRight ?? 0,
|
$border->roundBottomRight ?? 0,
|
||||||
@ -475,20 +464,20 @@ abstract class Component
|
|||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
sdl_render_fill_rect($renderer, [
|
sdl_render_fill_rect($renderer, [
|
||||||
'x' => $this->viewport->x * $this->viewport->uiScale,
|
'x' => $this->viewport->x,
|
||||||
'y' => $this->viewport->y * $this->viewport->uiScale,
|
'y' => $this->viewport->y,
|
||||||
'w' => $this->viewport->width * $this->viewport->uiScale,
|
'w' => $this->viewport->width,
|
||||||
'h' => $this->viewport->height * $this->viewport->uiScale,
|
'h' => $this->viewport->height,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (defined('DEBUG_RENDERING') && DEBUG_RENDERING) {
|
if (defined('DEBUG_RENDERING') && DEBUG_RENDERING) {
|
||||||
sdl_set_render_draw_color($renderer, rand(0, 255), rand(0, 255), rand(0, 255), 10);
|
sdl_set_render_draw_color($renderer, rand(0, 255), rand(0, 255), rand(0, 255), 10);
|
||||||
sdl_render_rect($renderer, [
|
sdl_render_rect($renderer, [
|
||||||
'x' => $this->viewport->x * $this->viewport->uiScale,
|
'x' => $this->viewport->x,
|
||||||
'y' => $this->viewport->y * $this->viewport->uiScale,
|
'y' => $this->viewport->y,
|
||||||
'w' => $this->viewport->width * $this->viewport->uiScale,
|
'w' => $this->viewport->width,
|
||||||
'h' => $this->viewport->height * $this->viewport->uiScale,
|
'h' => $this->viewport->height,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -116,6 +116,7 @@ class Container extends Component
|
|||||||
height: $this->contentViewport->height,
|
height: $this->contentViewport->height,
|
||||||
windowWidth: $this->contentViewport->windowWidth,
|
windowWidth: $this->contentViewport->windowWidth,
|
||||||
windowHeight: $this->contentViewport->windowHeight,
|
windowHeight: $this->contentViewport->windowHeight,
|
||||||
|
uiScale: $this->contentViewport->uiScale,
|
||||||
);
|
);
|
||||||
|
|
||||||
$child->setViewport($childViewport);
|
$child->setViewport($childViewport);
|
||||||
@ -138,6 +139,7 @@ class Container extends Component
|
|||||||
height: $this->contentViewport->windowHeight,
|
height: $this->contentViewport->windowHeight,
|
||||||
windowWidth: $this->contentViewport->windowWidth,
|
windowWidth: $this->contentViewport->windowWidth,
|
||||||
windowHeight: $this->contentViewport->windowHeight,
|
windowHeight: $this->contentViewport->windowHeight,
|
||||||
|
uiScale: $this->contentViewport->uiScale,
|
||||||
);
|
);
|
||||||
$child->setViewport($overlayViewport);
|
$child->setViewport($overlayViewport);
|
||||||
$child->setContentViewport(clone $overlayViewport);
|
$child->setContentViewport(clone $overlayViewport);
|
||||||
@ -272,6 +274,7 @@ class Container extends Component
|
|||||||
height: $isRow ? $this->contentViewport->height : 9999,
|
height: $isRow ? $this->contentViewport->height : 9999,
|
||||||
windowWidth: $this->contentViewport->windowWidth,
|
windowWidth: $this->contentViewport->windowWidth,
|
||||||
windowHeight: $this->contentViewport->windowHeight,
|
windowHeight: $this->contentViewport->windowHeight,
|
||||||
|
uiScale: $this->contentViewport->uiScale,
|
||||||
);
|
);
|
||||||
$child->setViewport($tempViewport);
|
$child->setViewport($tempViewport);
|
||||||
$child->layout($textRenderer);
|
$child->layout($textRenderer);
|
||||||
@ -329,6 +332,7 @@ class Container extends Component
|
|||||||
height: $childHeight,
|
height: $childHeight,
|
||||||
windowWidth: $this->contentViewport->windowWidth,
|
windowWidth: $this->contentViewport->windowWidth,
|
||||||
windowHeight: $this->contentViewport->windowHeight,
|
windowHeight: $this->contentViewport->windowHeight,
|
||||||
|
uiScale: $this->contentViewport->uiScale,
|
||||||
);
|
);
|
||||||
$currentPosition += $size;
|
$currentPosition += $size;
|
||||||
} else {
|
} else {
|
||||||
@ -344,6 +348,7 @@ class Container extends Component
|
|||||||
height: $size,
|
height: $size,
|
||||||
windowWidth: $this->contentViewport->windowWidth,
|
windowWidth: $this->contentViewport->windowWidth,
|
||||||
windowHeight: $this->contentViewport->windowHeight,
|
windowHeight: $this->contentViewport->windowHeight,
|
||||||
|
uiScale: $this->contentViewport->uiScale,
|
||||||
);
|
);
|
||||||
$currentPosition += $size;
|
$currentPosition += $size;
|
||||||
}
|
}
|
||||||
@ -357,7 +362,7 @@ class Container extends Component
|
|||||||
private function calculateSize(Width|Height|Basis $style, float $availableSpace): float
|
private function calculateSize(Width|Height|Basis $style, float $availableSpace): float
|
||||||
{
|
{
|
||||||
return match ($style->unit) {
|
return match ($style->unit) {
|
||||||
Unit::Pixel => (float) $style->value,
|
Unit::Pixel => ((float) $style->value) * $this->contentViewport->uiScale,
|
||||||
Unit::Point => (float) $style->value,
|
Unit::Point => (float) $style->value,
|
||||||
Unit::Percent => ($availableSpace * $style->value) / 100,
|
Unit::Percent => ($availableSpace * $style->value) / 100,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -75,8 +75,8 @@ class Icon extends Component
|
|||||||
if ($font) {
|
if ($font) {
|
||||||
$dimensions = ttf_size_text($font, $this->glyph);
|
$dimensions = ttf_size_text($font, $this->glyph);
|
||||||
if ($dimensions !== false) {
|
if ($dimensions !== false) {
|
||||||
$width = (int) $dimensions['w'];
|
$width = ((int) $dimensions['w']) * $this->viewport->uiScale;
|
||||||
$height = (int) $dimensions['h'];
|
$height = ((int) $dimensions['h']) * $this->viewport->uiScale;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -130,7 +130,7 @@ class Icon extends Component
|
|||||||
{
|
{
|
||||||
$this->clearTexture();
|
$this->clearTexture();
|
||||||
|
|
||||||
$font = IconFontRegistry::getFont($this->size, $this->fontPath);
|
$font = IconFontRegistry::getFont($this->size * $this->viewport->uiScale, $this->fontPath);
|
||||||
if (!$font) {
|
if (!$font) {
|
||||||
$this->renderDirty = false;
|
$this->renderDirty = false;
|
||||||
return;
|
return;
|
||||||
|
|||||||
@ -16,7 +16,6 @@ class Window
|
|||||||
private float $mouseY = 0;
|
private float $mouseY = 0;
|
||||||
private Viewport $viewport;
|
private Viewport $viewport;
|
||||||
private bool $shouldBeReLayouted = true;
|
private bool $shouldBeReLayouted = true;
|
||||||
private float $pixelRatio = 1.0;
|
|
||||||
private float $uiScale = 1.0;
|
private float $uiScale = 1.0;
|
||||||
private bool $shouldClose = false;
|
private bool $shouldClose = false;
|
||||||
private $onResize = null;
|
private $onResize = null;
|
||||||
@ -24,6 +23,7 @@ class Window
|
|||||||
private float $lastFpsUpdate = 0.0;
|
private float $lastFpsUpdate = 0.0;
|
||||||
private int $frameCounter = 0;
|
private int $frameCounter = 0;
|
||||||
private float $currentFps = 0.0;
|
private float $currentFps = 0.0;
|
||||||
|
private bool $leftButtonDown = false;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private string $title,
|
private string $title,
|
||||||
@ -58,12 +58,11 @@ class Window
|
|||||||
// Get window ID for event routing
|
// Get window ID for event routing
|
||||||
$this->windowId = sdl_get_window_id($this->window);
|
$this->windowId = sdl_get_window_id($this->window);
|
||||||
// Use display scale as UI scale (e.g. 2.0 on HiDPI)
|
// Use display scale as UI scale (e.g. 2.0 on HiDPI)
|
||||||
if (function_exists('sdl_get_window_display_scale')) {
|
|
||||||
$scale = sdl_get_window_display_scale($this->window);
|
$scale = sdl_get_window_display_scale($this->window);
|
||||||
|
|
||||||
if ($scale > 0.1 && $scale <= 4.0) {
|
if ($scale > 0.1 && $scale <= 4.0) {
|
||||||
$this->uiScale = (float) $scale;
|
$this->uiScale = (float) $scale;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
// Enable text input for this window
|
// Enable text input for this window
|
||||||
sdl_start_text_input($this->window);
|
sdl_start_text_input($this->window);
|
||||||
|
|
||||||
@ -78,77 +77,21 @@ class Window
|
|||||||
if (!$this->textRenderer->init()) {
|
if (!$this->textRenderer->init()) {
|
||||||
error_log('Warning: Failed to initialize text renderer. Text rendering will not be available.');
|
error_log('Warning: Failed to initialize text renderer. Text rendering will not be available.');
|
||||||
}
|
}
|
||||||
|
$this->textRenderer->setPixelRatio($this->uiScale);
|
||||||
// Get actual window size
|
// Get actual window size
|
||||||
$size = sdl_get_window_size($this->window);
|
$size = sdl_get_window_size($this->window);
|
||||||
$this->width = $size[0];
|
$this->width = $size[0];
|
||||||
$this->height = $size[1];
|
$this->height = $size[1];
|
||||||
$this->viewport = new Viewport(
|
$this->viewport = new Viewport(
|
||||||
windowWidth: $this->width,
|
windowWidth: $this->width * $this->uiScale,
|
||||||
windowHeight: $this->height,
|
windowHeight: $this->height * $this->uiScale,
|
||||||
width: $this->width,
|
width: $this->width * $this->uiScale,
|
||||||
height: $this->height,
|
height: $this->height * $this->uiScale,
|
||||||
uiScale: $this->uiScale,
|
uiScale: $this->uiScale,
|
||||||
);
|
);
|
||||||
$this->updatePixelRatio();
|
|
||||||
|
|
||||||
$this->lastFpsUpdate = microtime(true);
|
$this->lastFpsUpdate = microtime(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function updatePixelRatio(): void
|
|
||||||
{
|
|
||||||
$this->pixelRatio = 1.0;
|
|
||||||
|
|
||||||
// HiDPI‑Scaling ist optional und wird nur aktiviert,
|
|
||||||
// wenn die Umgebungsvariable PHPNATIVE_ENABLE_HIDPI=1 gesetzt ist.
|
|
||||||
$enableHiDpi = getenv('PHPNATIVE_ENABLE_HIDPI');
|
|
||||||
if ($enableHiDpi !== '1') {
|
|
||||||
if ($this->textRenderer) {
|
|
||||||
$this->textRenderer->setPixelRatio($this->pixelRatio);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!function_exists('sdl_get_window_size') || !$this->window) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$windowSize = sdl_get_window_size($this->window);
|
|
||||||
$pixelSize = null;
|
|
||||||
|
|
||||||
if (function_exists('sdl_get_window_size_in_pixels')) {
|
|
||||||
$pixelSize = sdl_get_window_size_in_pixels($this->window);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((!is_array($pixelSize) || count($pixelSize) < 2) && function_exists('sdl_get_renderer_output_size')) {
|
|
||||||
$pixelSize = sdl_get_renderer_output_size($this->renderer);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (is_array($windowSize) && is_array($pixelSize)) {
|
|
||||||
$logicalWidth = max(1, (int) ($windowSize[0] ?? 1));
|
|
||||||
$logicalHeight = max(1, (int) ($windowSize[1] ?? 1));
|
|
||||||
$pixelWidth = max(1, (int) ($pixelSize[0] ?? 1));
|
|
||||||
$pixelHeight = max(1, (int) ($pixelSize[1] ?? 1));
|
|
||||||
|
|
||||||
$ratioX = $pixelWidth / $logicalWidth;
|
|
||||||
$ratioY = $pixelHeight / $logicalHeight;
|
|
||||||
|
|
||||||
$computed = max($ratioX, $ratioY);
|
|
||||||
if ($computed > 0) {
|
|
||||||
$this->pixelRatio = max(1.0, $computed);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($this->textRenderer) {
|
|
||||||
$this->textRenderer->setPixelRatio($this->pixelRatio);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getPixelRatio(): float
|
|
||||||
{
|
|
||||||
return $this->pixelRatio;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getUiScale(): float
|
public function getUiScale(): float
|
||||||
{
|
{
|
||||||
return $this->uiScale;
|
return $this->uiScale;
|
||||||
@ -277,13 +220,12 @@ class Window
|
|||||||
if ($this->textRenderer && $this->textRenderer->isInitialized()) {
|
if ($this->textRenderer && $this->textRenderer->isInitialized()) {
|
||||||
$this->textRenderer->updateFramebuffer($newWidth, $newHeight);
|
$this->textRenderer->updateFramebuffer($newWidth, $newHeight);
|
||||||
}
|
}
|
||||||
$this->updatePixelRatio();
|
|
||||||
$this->viewport->x = 0;
|
$this->viewport->x = 0;
|
||||||
$this->viewport->y = 0;
|
$this->viewport->y = 0;
|
||||||
$this->viewport->windowWidth = $newWidth;
|
$this->viewport->windowWidth = $newWidth * $this->viewport->uiScale;
|
||||||
$this->viewport->width = $newWidth;
|
$this->viewport->width = $newWidth * $this->viewport->uiScale;
|
||||||
$this->viewport->height = $newHeight;
|
$this->viewport->height = $newHeight * $this->viewport->uiScale;
|
||||||
$this->viewport->windowHeight = $newHeight;
|
$this->viewport->windowHeight = $newHeight * $this->viewport->uiScale;
|
||||||
$this->shouldBeReLayouted = true;
|
$this->shouldBeReLayouted = true;
|
||||||
if ($this->onResize) {
|
if ($this->onResize) {
|
||||||
($this->onResize)($this);
|
($this->onResize)($this);
|
||||||
@ -292,8 +234,8 @@ class Window
|
|||||||
|
|
||||||
case SDL_EVENT_MOUSE_MOTION:
|
case SDL_EVENT_MOUSE_MOTION:
|
||||||
// Convert physical pixels to logical coordinates using uiScale
|
// Convert physical pixels to logical coordinates using uiScale
|
||||||
$newMouseX = (float) ($event['x'] ?? 0) / $this->uiScale;
|
$newMouseX = ((float) ($event['x'] ?? 0)) * $this->uiScale;
|
||||||
$newMouseY = (float) ($event['y'] ?? 0) / $this->uiScale;
|
$newMouseY = ((float) ($event['y'] ?? 0)) * $this->uiScale;
|
||||||
|
|
||||||
$this->mouseX = $newMouseX;
|
$this->mouseX = $newMouseX;
|
||||||
$this->mouseY = $newMouseY;
|
$this->mouseY = $newMouseY;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user