tab = new Container('flex flex-row p-4 gap-4 bg-gray-50'); $currentPrivateKeyPath = &$privateKeyPath; $currentRemoteStartDir = &$remoteStartDir; // Left side: Local file browser // Lokaler Browser: Spalte, Scrollen übernimmt der FileBrowser selbst $localBrowserContainer = new Container('flex flex-col flex-1 gap-2'); $localBrowserContainer->addComponent(new Label('Lokal', 'text-lg font-bold text-black mb-2')); $this->localFileBrowser = new FileBrowser( getcwd(), false, 'flex-1 bg-white border-2 border-gray-300 rounded p-2', ); $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 // Remote-Browser: Spalte, Scrollen übernimmt der FileBrowser selbst $remoteBrowserContainer = new Container('flex flex-col flex-1 gap-2'); // Header with title and new file button $remoteHeader = new Container('flex flex-row items-center gap-2 mb-2'); $remoteHeader->addComponent(new Label('Remote', 'text-lg font-bold text-black flex-1')); $newFileButton = new Button('', 'px-3 py-2 bg-green-600 text-white rounded hover:bg-green-700 flex items-center justify-center'); $newFileIcon = new Icon(\PHPNative\Tailwind\Data\Icon::plus, 16, 'text-white'); $newFileButton->setIcon($newFileIcon); $remoteHeader->addComponent($newFileButton); $remoteBrowserContainer->addComponent($remoteHeader); $this->remoteFileBrowser = new FileBrowser('/', true, 'flex-1 bg-white border-2 border-gray-300 rounded p-2'); $remoteBrowserContainer->addComponent($this->remoteFileBrowser); // Connection status label $this->connectionStatusLabel = new Label('Nicht verbunden', 'text-sm text-gray-600 italic mb-2'); $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($transferContainer); $this->tab->addComponent($remoteBrowserContainer); // Setup remote navigation handler $this->setupRemoteNavigationHandler($currentPrivateKeyPath, $serverListTab, $statusLabel); // Setup SFTP connection handler $this->setupSftpConnectionHandler($currentPrivateKeyPath, $currentRemoteStartDir, $serverListTab, $tabContainer, $statusLabel); // Create edit modal $this->createEditModal($currentPrivateKeyPath, $serverListTab, $statusLabel); // Create filename input modal $this->createFilenameModal($currentPrivateKeyPath, $serverListTab, $statusLabel); // Create rename modal $this->createRenameModal($currentPrivateKeyPath, $serverListTab, $statusLabel); // Create delete confirmation modal $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 $this->remoteFileBrowser->setOnEditFile(function ($path, $row) use ( &$currentPrivateKeyPath, $serverListTab, $statusLabel, ) { $this->handleFileEdit($path, $row, $currentPrivateKeyPath, $serverListTab, $statusLabel); }); // Setup new file button handler $sftpTab = $this; $newFileButton->setOnClick(function () use ($sftpTab, &$currentPrivateKeyPath, $serverListTab, $statusLabel) { $sftpTab->handleNewFile($currentPrivateKeyPath, $serverListTab, $statusLabel); }); // Setup rename handler $this->remoteFileBrowser->setOnRenameFile(function ($path, $row) use ( $sftpTab, &$currentPrivateKeyPath, $serverListTab, $statusLabel, ) { $sftpTab->handleFileRename($path, $row, $currentPrivateKeyPath, $serverListTab, $statusLabel); }); // Setup delete handler $this->remoteFileBrowser->setOnDeleteFile(function ($path, $row) use ( $sftpTab, &$currentPrivateKeyPath, $serverListTab, $statusLabel, ) { $sftpTab->handleFileDelete($path, $row, $currentPrivateKeyPath, $serverListTab, $statusLabel); }); } private function setupRemoteNavigationHandler( string &$currentPrivateKeyPath, ServerListTab $serverListTab, Label $statusLabel, ): void { $sftpTab = $this; // Create reference to selectedServer array outside of closure $selectedServerRef = &$serverListTab->selectedServer; $this->remoteFileBrowser->setOnFileSelect( function ($path, $row) use ( $sftpTab, &$currentPrivateKeyPath, &$selectedServerRef, $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']) { return; } // Require double click for navigation: // first click selektiert nur, zweiter Klick (innerhalb kurzer Zeit) // öffnet das Verzeichnis. $now = microtime(true); $doubleClickThreshold = 0.4; // Sekunden if ( $sftpTab->lastRemoteClickPath !== $path || ($now - $sftpTab->lastRemoteClickTime) > $doubleClickThreshold ) { $sftpTab->lastRemoteClickPath = $path; $sftpTab->lastRemoteClickTime = $now; return; } // Zweiter Klick innerhalb des Zeitfensters -> als Doppelklick werten $sftpTab->lastRemoteClickPath = null; $sftpTab->lastRemoteClickTime = 0.0; $loadButton = new Button('Load', ''); $loadButton->setOnClickAsync( function () use ($path, &$currentPrivateKeyPath, &$selectedServerRef) { if ($selectedServerRef === null || empty($currentPrivateKeyPath)) { return ['error' => 'Not connected']; } // Copy to local variable for async context $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']; } $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 ($sftpTab, $statusLabel) { if (isset($result['error'])) { $statusLabel->setText('SFTP Fehler: ' . $result['error']); return; } if (isset($result['success'])) { $sftpTab->remoteFileBrowser->setPath($result['path']); $sftpTab->remoteFileBrowser->setFileData($result['files']); } }, function ($error) use ($statusLabel) { $errorMsg = is_string($error) ? $error : 'Unknown error'; $statusLabel->setText('SFTP Error: ' . $errorMsg); }, ); $loadButton->handleMouseClick(0, 0, 0); }, ); } private function setupSftpConnectionHandler( string &$currentPrivateKeyPath, string &$currentRemoteStartDir, ServerListTab $serverListTab, \PHPNative\Ui\Widget\TabContainer $tabContainer, Label $statusLabel, ): void { $sftpTab = $this; // Create a reference to selectedServer that we can update $selectedServerRef = &$serverListTab->selectedServer; // Set up async handler for SFTP connection (tab switch happens in success callback) $serverListTab->getSftpButton()->setOnClickAsync( function () use (&$currentPrivateKeyPath, &$currentRemoteStartDir, &$selectedServerRef) { if ($selectedServerRef === null) { return ['error' => 'Kein Server ausgewählt']; } // Copy the selected server data to a local variable for use in async context $selectedServer = $selectedServerRef; 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']; } $sftp = new \phpseclib3\Net\SFTP($selectedServer['ipv4']); if (!$sftp->login('root', $key)) { return ['error' => 'SFTP Login fehlgeschlagen']; } // Use configured start directory or fallback to root $startDir = !empty($currentRemoteStartDir) ? $currentRemoteStartDir : '/'; // Check if start directory exists, fallback to root if not if (!$sftp->is_dir($startDir)) { $startDir = '/'; } $files = $sftp->nlist($startDir); if ($files === false) { return ['error' => 'Kann Verzeichnis nicht lesen: ' . $startDir]; } $fileList = []; foreach ($files as $file) { if ($file === '.' || $file === '..') { continue; } $fullPath = rtrim($startDir, '/') . '/' . $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, 'server' => $selectedServer, 'files' => $fileList, 'path' => $startDir, ]; } catch (\Exception $e) { return ['error' => 'Verbindung fehlgeschlagen: ' . $e->getMessage()]; } }, function ($result) use ($sftpTab, $tabContainer, $statusLabel) { if (isset($result['error'])) { $statusLabel->setText('SFTP Fehler: ' . $result['error']); return; } if (isset($result['success']) && $result['success']) { // Switch to SFTP tab on successful connection // Tab-Reihenfolge in App.php: // 0 = Server, 1 = Kanban, 2 = SFTP Manager $tabContainer->setActiveTab(2); $sftpTab->connectionStatusLabel->setText( 'Verbunden mit: ' . $result['server']['name'] . ' (' . $result['server']['ipv4'] . ')', ); $statusLabel->setText('SFTP Verbindung erfolgreich zu ' . $result['server']['name']); $sftpTab->remoteFileBrowser->setPath($result['path'] ?? '/'); $sftpTab->remoteFileBrowser->setFileData($result['files']); } }, 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); }, ); } public function getContainer(): Container { return $this->tab; } public function getEditModal(): Modal { return $this->editModal; } public function getFilenameModal(): Modal { return $this->filenameModal; } public function getRenameModal(): Modal { return $this->renameModal; } public function getDeleteConfirmModal(): Modal { return $this->deleteConfirmModal; } private function createEditModal( string &$currentPrivateKeyPath, ServerListTab $serverListTab, Label $statusLabel, ): void { // Create modal content container $modalContent = new Container('flex flex-col bg-white rounded-lg shadow-lg p-4 gap-3 w-[600] h-[400]'); // Disable texture caching for modal content to allow dynamic content updates $modalContent->setUseTextureCache(false); // File path label $this->filePathLabel = new Label('', 'text-sm text-gray-600 font-mono'); $modalContent->addComponent($this->filePathLabel); // Text editor $this->fileEditor = new TextArea( '', 'Lade Datei...', 'flex-1 border-2 border-gray-300 rounded p-2 bg-white text-black font-mono text-sm', ); // Disable texture caching for TextArea to ensure text updates are visible $this->fileEditor->setUseTextureCache(false); $modalContent->addComponent($this->fileEditor); // Button container $buttonContainer = new Container('flex flex-row justify-end gap-2'); // Cancel button $cancelButton = new Button( 'Abbrechen', 'px-4 py-2 bg-gray-300 text-black rounded hover:bg-gray-400', ); $sftpTab = $this; $cancelButton->setOnClick(function () use ($sftpTab) { $sftpTab->editModal->setVisible(false); }); $buttonContainer->addComponent($cancelButton); // Save button $saveButton = new Button('Speichern', 'px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600'); $saveButton->setOnClick(function () use ($sftpTab, &$currentPrivateKeyPath, $serverListTab, $statusLabel) { $sftpTab->handleFileSave($currentPrivateKeyPath, $serverListTab, $statusLabel); }); $buttonContainer->addComponent($saveButton); $modalContent->addComponent($buttonContainer); // Create modal (will be added to main container by App.php) $this->editModal = new Modal($modalContent); } private function handleFileEdit( string $path, array $row, string &$currentPrivateKeyPath, ServerListTab $serverListTab, Label $statusLabel, ): void { $this->currentEditFilePath = $path; $this->filePathLabel->setText('Bearbeite: ' . $path); $this->fileEditor->setValue(''); $this->fileEditor->setFocused(false); $this->editModal->setVisible(true); // Reference to components for async callback $selectedServerRef = &$serverListTab->selectedServer; $sftpTab = $this; // Load file content asynchronously $loadButton = new Button('Load', ''); $loadButton->setOnClickAsync( function () use ($path, &$currentPrivateKeyPath, &$selectedServerRef) { 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']; } $fileContent = $sftp->get($path); if ($fileContent === false) { return ['error' => 'Could not read file']; } return ['success' => true, 'content' => $fileContent]; } catch (\Exception $e) { return ['error' => $e->getMessage()]; } }, function ($result) use ($sftpTab, $statusLabel) { if (isset($result['error'])) { $statusLabel->setText('Fehler beim Laden: ' . $result['error']); $sftpTab->editModal->setVisible(false); return; } if (isset($result['success'])) { $sftpTab->fileEditor->setValue($result['content']); $sftpTab->fileEditor->setFocused(true); $statusLabel->setText('Datei geladen'); } }, function ($error) use ($statusLabel, $sftpTab) { $errorMsg = is_string($error) ? $error : 'Unknown error'; $statusLabel->setText('Fehler: ' . $errorMsg); $sftpTab->editModal->setVisible(false); }, ); $loadButton->handleMouseClick(0, 0, 0); } private function handleFileSave( string &$currentPrivateKeyPath, ServerListTab $serverListTab, Label $statusLabel, ): void { $content = $this->fileEditor->getValue(); $path = $this->currentEditFilePath; if (empty($path)) { $statusLabel->setText('Fehler: Kein Dateipfad'); return; } // Reference to components for async callback $selectedServerRef = &$serverListTab->selectedServer; $sftpTab = $this; // Save file content asynchronously $saveButton = new Button('Save', ''); $saveButton->setOnClickAsync( function () use ($path, $content, &$currentPrivateKeyPath, &$selectedServerRef) { 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($path, $content); if ($result === false) { return ['error' => 'Could not write file']; } 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 Speichern: ' . $result['error']); return; } if (isset($result['success'])) { $statusLabel->setText('Datei erfolgreich gespeichert'); $sftpTab->editModal->setVisible(false); // Reload the current directory to show the new/updated file $sftpTab->reloadCurrentDirectory($currentPrivateKeyPath, $selectedServerRef, $statusLabel); } }, function ($error) use ($statusLabel) { $errorMsg = is_string($error) ? $error : 'Unknown error'; $statusLabel->setText('Fehler: ' . $errorMsg); }, ); $saveButton->handleMouseClick(0, 0, 0); } private function handleNewFile( string &$currentPrivateKeyPath, ServerListTab $serverListTab, Label $statusLabel, ): void { if ($serverListTab->selectedServer === null) { $statusLabel->setText('Kein Server ausgewählt'); return; } // Show filename input modal $this->filenameInput->setValue('neue_datei.txt'); $this->filenameModal->setVisible(true); } private function createFilenameModal( string &$currentPrivateKeyPath, ServerListTab $serverListTab, Label $statusLabel, ): void { // Create modal content $modalContent = new Container('flex flex-col bg-white rounded-lg shadow-lg p-4 gap-3 w-96'); // Title $modalContent->addComponent(new Label('Neue Datei', 'text-lg font-bold text-black')); // Filename input $this->filenameInput = new TextInput( 'Dateiname', 'w-full border-2 border-gray-300 rounded px-3 py-2 bg-white text-black', ); $modalContent->addComponent($this->filenameInput); // Button container $buttonContainer = new Container('flex flex-row justify-end gap-2'); // Cancel button $cancelButton = new Button('Abbrechen', 'px-4 py-2 bg-gray-300 text-black rounded hover:bg-gray-400'); $sftpTab = $this; $cancelButton->setOnClick(function () use ($sftpTab) { $sftpTab->filenameModal->setVisible(false); }); $buttonContainer->addComponent($cancelButton); // Create button $createButton = new Button('Erstellen', 'px-4 py-2 bg-green-600 text-white rounded hover:bg-green-700'); $createButton->setOnClick(function () use ($sftpTab, $statusLabel) { $filename = trim($sftpTab->filenameInput->getValue()); if (empty($filename)) { $statusLabel->setText('Dateiname darf nicht leer sein'); return; } // Close filename modal $sftpTab->filenameModal->setVisible(false); // Create the new file with the given name $currentPath = $sftpTab->remoteFileBrowser->getCurrentPath(); $newFilePath = rtrim($currentPath, '/') . '/' . $filename; $sftpTab->currentEditFilePath = $newFilePath; $sftpTab->filePathLabel->setText('Neue Datei: ' . $newFilePath); $sftpTab->fileEditor->setValue(''); $sftpTab->fileEditor->setFocused(true); $sftpTab->editModal->setVisible(true); $statusLabel->setText('Neue Datei wird erstellt: ' . $filename); }); $buttonContainer->addComponent($createButton); $modalContent->addComponent($buttonContainer); // Create modal $this->filenameModal = new Modal($modalContent); } private function reloadCurrentDirectory( string &$currentPrivateKeyPath, ?array &$selectedServerRef, Label $statusLabel, ): void { $path = $this->remoteFileBrowser->getCurrentPath(); if ($selectedServerRef === null || empty($currentPrivateKeyPath)) { return; } $sftpTab = $this; // Load directory asynchronously $loadButton = new Button('Load', ''); $loadButton->setOnClickAsync( function () use ($path, &$currentPrivateKeyPath, &$selectedServerRef) { 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']; } $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 ($sftpTab, $statusLabel) { if (isset($result['error'])) { $statusLabel->setText('Fehler beim Aktualisieren: ' . $result['error']); return; } if (isset($result['success'])) { $sftpTab->remoteFileBrowser->setPath($result['path']); $sftpTab->remoteFileBrowser->setFileData($result['files']); } }, function ($error) use ($statusLabel) { $errorMsg = is_string($error) ? $error : 'Unknown error'; $statusLabel->setText('Fehler beim Aktualisieren: ' . $errorMsg); }, ); $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( string &$currentPrivateKeyPath, ServerListTab $serverListTab, Label $statusLabel, ): void { // Create modal content $modalContent = new Container('flex flex-col bg-white rounded-lg shadow-lg p-4 gap-3 w-96'); // Title $modalContent->addComponent(new Label('Datei umbenennen', 'text-lg font-bold text-black')); // Filename input $this->renameInput = new TextInput( 'Neuer Dateiname', 'w-full border-2 border-gray-300 rounded px-3 py-2 bg-white text-black', ); $modalContent->addComponent($this->renameInput); // Button container $buttonContainer = new Container('flex flex-row justify-end gap-2'); // Cancel button $cancelButton = new Button('Abbrechen', 'px-4 py-2 bg-gray-300 text-black rounded hover:bg-gray-400'); $sftpTab = $this; $cancelButton->setOnClick(function () use ($sftpTab) { $sftpTab->renameModal->setVisible(false); }); $buttonContainer->addComponent($cancelButton); // Rename button $renameButton = new Button('Umbenennen', 'px-4 py-2 bg-amber-600 text-white rounded hover:bg-amber-700'); $renameButton->setOnClick(function () use ($sftpTab, &$currentPrivateKeyPath, $serverListTab, $statusLabel) { $newFilename = trim($sftpTab->renameInput->getValue()); if (empty($newFilename)) { $statusLabel->setText('Dateiname darf nicht leer sein'); return; } $oldPath = $sftpTab->currentRenameFilePath; $directory = dirname($oldPath); $newPath = $directory . '/' . $newFilename; // Close rename modal $sftpTab->renameModal->setVisible(false); // Perform rename via SFTP $selectedServerRef = &$serverListTab->selectedServer; $renameAsyncButton = new Button('Rename', ''); $renameAsyncButton->setOnClickAsync( function () use ($oldPath, $newPath, &$currentPrivateKeyPath, &$selectedServerRef) { 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->rename($oldPath, $newPath); if ($result === false) { return ['error' => 'Could not rename file']; } 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 Umbenennen: ' . $result['error']); return; } if (isset($result['success'])) { $statusLabel->setText('Datei erfolgreich umbenannt'); $sftpTab->reloadCurrentDirectory($currentPrivateKeyPath, $selectedServerRef, $statusLabel); } }, function ($error) use ($statusLabel) { $errorMsg = is_string($error) ? $error : 'Unknown error'; $statusLabel->setText('Fehler beim Umbenennen: ' . $errorMsg); }, ); $renameAsyncButton->handleMouseClick(0, 0, 0); }); $buttonContainer->addComponent($renameButton); $modalContent->addComponent($buttonContainer); // Create modal $this->renameModal = new Modal($modalContent); } private function createDeleteConfirmModal( string &$currentPrivateKeyPath, ServerListTab $serverListTab, Label $statusLabel, ): void { // Create modal content $modalContent = new Container('flex flex-col bg-white rounded-lg shadow-lg p-4 gap-3 w-96'); // Title $modalContent->addComponent(new Label('Datei löschen', 'text-lg font-bold text-black')); // Confirmation message $this->deleteConfirmLabel = new Label( 'Möchten Sie diese Datei wirklich löschen?', 'text-sm text-gray-700', ); $modalContent->addComponent($this->deleteConfirmLabel); // Button container $buttonContainer = new Container('flex flex-row justify-end gap-2'); // Cancel button $cancelButton = new Button('Abbrechen', 'px-4 py-2 bg-gray-300 text-black rounded hover:bg-gray-400'); $sftpTab = $this; $cancelButton->setOnClick(function () use ($sftpTab) { $sftpTab->deleteConfirmModal->setVisible(false); }); $buttonContainer->addComponent($cancelButton); // Delete button $deleteButton = new Button('Löschen', 'px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700'); $deleteButton->setOnClick(function () use ($sftpTab, &$currentPrivateKeyPath, $serverListTab, $statusLabel) { $filePath = $sftpTab->currentDeleteFilePath; // Close delete modal $sftpTab->deleteConfirmModal->setVisible(false); // Perform delete via SFTP $selectedServerRef = &$serverListTab->selectedServer; $deleteAsyncButton = new Button('Delete', ''); $deleteAsyncButton->setOnClickAsync( function () use ($filePath, &$currentPrivateKeyPath, &$selectedServerRef) { 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->delete($filePath); if ($result === false) { return ['error' => 'Could not delete file']; } 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 Löschen: ' . $result['error']); return; } if (isset($result['success'])) { $statusLabel->setText('Datei erfolgreich gelöscht'); $sftpTab->reloadCurrentDirectory($currentPrivateKeyPath, $selectedServerRef, $statusLabel); } }, function ($error) use ($statusLabel) { $errorMsg = is_string($error) ? $error : 'Unknown error'; $statusLabel->setText('Fehler beim Löschen: ' . $errorMsg); }, ); $deleteAsyncButton->handleMouseClick(0, 0, 0); }); $buttonContainer->addComponent($deleteButton); $modalContent->addComponent($buttonContainer); // Create modal $this->deleteConfirmModal = new Modal($modalContent); } private function handleFileRename( string $path, array $row, string &$currentPrivateKeyPath, ServerListTab $serverListTab, Label $statusLabel, ): void { if ($serverListTab->selectedServer === null) { $statusLabel->setText('Kein Server ausgewählt'); return; } $this->currentRenameFilePath = $path; $filename = basename($path); $this->renameInput->setValue($filename); $this->renameModal->setVisible(true); } private function handleFileDelete( string $path, array $row, string &$currentPrivateKeyPath, ServerListTab $serverListTab, Label $statusLabel, ): void { if ($serverListTab->selectedServer === null) { $statusLabel->setText('Kein Server ausgewählt'); return; } $this->currentDeleteFilePath = $path; $filename = basename($path); $this->deleteConfirmLabel->setText('Möchten Sie die Datei "' . $filename . '" wirklich löschen?'); $this->deleteConfirmModal->setVisible(true); } }