382 lines
13 KiB
PHP
382 lines
13 KiB
PHP
<?php
|
|
|
|
namespace PHPNative\Ui\Widget;
|
|
|
|
class FileBrowser extends Container
|
|
{
|
|
private Table $fileTable;
|
|
private Label $pathLabel;
|
|
private string $currentPath;
|
|
private $onFileSelect = null;
|
|
private $onEditFile = null;
|
|
private $onRenameFile = null;
|
|
private $onDeleteFile = null;
|
|
private bool $isRemote = false;
|
|
|
|
public function __construct(string $initialPath = '.', bool $isRemote = false, string $style = '')
|
|
{
|
|
parent::__construct('w-full flex flex-col gap-2 ' . $style);
|
|
|
|
$this->currentPath = $initialPath;
|
|
$this->isRemote = $isRemote;
|
|
|
|
// Path display
|
|
$this->pathLabel = new Label($initialPath, 'px-3 py-2 bg-gray-200 text-black rounded text-sm font-mono');
|
|
$this->addComponent($this->pathLabel);
|
|
|
|
// File table with explicit flex-1 for scrolling
|
|
$this->fileTable = new Table('');
|
|
$this->fileTable->setColumns([
|
|
['key' => 'type', 'title' => 'Typ', 'width' => 60],
|
|
['key' => 'name', 'title' => 'Name'],
|
|
['key' => 'size', 'title' => 'Größe', 'width' => 100],
|
|
['key' => 'modified', 'title' => 'Geändert', 'width' => 200],
|
|
['key' => 'actions', 'title' => 'Action', 'width' => 100, 'render' => [$this, 'renderActionsCell']],
|
|
]);
|
|
|
|
$this->addComponent($this->fileTable);
|
|
|
|
// Load initial directory (only if local)
|
|
if (!$isRemote) {
|
|
$this->loadDirectory($initialPath);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Load directory contents (local only)
|
|
*/
|
|
public function loadDirectory(string $path): void
|
|
{
|
|
if ($this->isRemote) {
|
|
return; // Remote loading is handled separately
|
|
}
|
|
|
|
$this->currentPath = realpath($path) ?: $path;
|
|
$this->pathLabel->setText($this->currentPath);
|
|
|
|
$files = [];
|
|
|
|
// Add parent directory entry if not at root
|
|
if ($this->currentPath !== '/') {
|
|
$files[] = [
|
|
'type' => 'DIR',
|
|
'name' => '..',
|
|
'size' => '',
|
|
'modified' => '',
|
|
'path' => dirname($this->currentPath),
|
|
'isDir' => true,
|
|
];
|
|
}
|
|
|
|
// Read directory
|
|
try {
|
|
$entries = scandir($this->currentPath);
|
|
foreach ($entries as $entry) {
|
|
if ($entry === '.' || $entry === '..') {
|
|
continue;
|
|
}
|
|
|
|
$fullPath = $this->currentPath . DIRECTORY_SEPARATOR . $entry;
|
|
$isDir = is_dir($fullPath);
|
|
$mtime = filemtime($fullPath);
|
|
|
|
$files[] = [
|
|
'type' => $isDir ? 'DIR' : 'FILE',
|
|
'name' => $entry,
|
|
'size' => $isDir ? '' : $this->formatSize(filesize($fullPath)),
|
|
'modified' => $mtime ? date('Y-m-d H:i:s', $mtime) : '',
|
|
'path' => $fullPath,
|
|
'isDir' => $isDir,
|
|
];
|
|
}
|
|
} catch (\Exception $e) {
|
|
// Handle permission errors
|
|
$files[] = [
|
|
'type' => 'ERR',
|
|
'name' => 'Fehler: ' . $e->getMessage(),
|
|
'size' => '',
|
|
'modified' => '',
|
|
'path' => '',
|
|
'isDir' => false,
|
|
];
|
|
}
|
|
|
|
$this->fileTable->setData($files);
|
|
|
|
// Handle row selection
|
|
$fileBrowser = $this;
|
|
$this->fileTable->setOnRowSelect(function ($index, $row) use ($fileBrowser) {
|
|
if ($row && isset($row['isDir']) && $row['isDir'] && !empty($row['path'])) {
|
|
// Navigate to directory
|
|
$fileBrowser->loadDirectory($row['path']);
|
|
} elseif ($row && isset($row['path']) && !empty($row['path']) && $fileBrowser->onFileSelect !== null) {
|
|
// File selected
|
|
($fileBrowser->onFileSelect)($row['path'], $row);
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Load remote directory contents via SFTP
|
|
*/
|
|
public function loadRemoteDirectory(string $path, $sftpConnection): void
|
|
{
|
|
if (!$this->isRemote) {
|
|
return;
|
|
}
|
|
|
|
$this->currentPath = $path;
|
|
$this->pathLabel->setText($path);
|
|
|
|
$files = [];
|
|
|
|
// Add parent directory entry if not at root
|
|
if ($path !== '/') {
|
|
$parentPath = dirname($path);
|
|
if ($parentPath === '.') {
|
|
$parentPath = '/';
|
|
}
|
|
$files[] = [
|
|
'type' => 'DIR',
|
|
'name' => '..',
|
|
'size' => '',
|
|
'modified' => '',
|
|
'path' => $parentPath,
|
|
'isDir' => true,
|
|
];
|
|
}
|
|
|
|
// Read remote directory
|
|
try {
|
|
$entries = $sftpConnection->nlist($path);
|
|
if ($entries === false) {
|
|
throw new \Exception('Cannot read directory');
|
|
}
|
|
|
|
foreach ($entries as $entry) {
|
|
if ($entry === '.' || $entry === '..') {
|
|
continue;
|
|
}
|
|
|
|
$fullPath = rtrim($path, '/') . '/' . $entry;
|
|
$stat = $sftpConnection->stat($fullPath);
|
|
$isDir = ($stat['type'] ?? 0) === 2; // NET_SFTP_TYPE_DIRECTORY
|
|
$mtime = $stat['mtime'] ?? 0;
|
|
|
|
$files[] = [
|
|
'type' => $isDir ? 'DIR' : 'FILE',
|
|
'name' => $entry,
|
|
'size' => $isDir ? '' : $this->formatSize($stat['size'] ?? 0),
|
|
'modified' => $mtime ? date('Y-m-d H:i:s', $mtime) : '',
|
|
'path' => $fullPath,
|
|
'isDir' => $isDir,
|
|
];
|
|
}
|
|
} catch (\Exception $e) {
|
|
$files[] = [
|
|
'type' => 'ERR',
|
|
'name' => 'Fehler: ' . $e->getMessage(),
|
|
'size' => '',
|
|
'modified' => '',
|
|
'path' => '',
|
|
'isDir' => false,
|
|
];
|
|
}
|
|
|
|
$this->fileTable->setData($files);
|
|
|
|
// Handle row selection
|
|
$fileBrowser = $this;
|
|
$this->fileTable->setOnRowSelect(function ($index, $row) use ($fileBrowser, $sftpConnection) {
|
|
if ($row && isset($row['isDir']) && $row['isDir'] && !empty($row['path'])) {
|
|
// Navigate to directory
|
|
$fileBrowser->loadRemoteDirectory($row['path'], $sftpConnection);
|
|
} elseif ($row && isset($row['path']) && !empty($row['path']) && $fileBrowser->onFileSelect !== null) {
|
|
// File selected
|
|
($fileBrowser->onFileSelect)($row['path'], $row);
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Format file size
|
|
*/
|
|
private function formatSize(int|float $bytes): string
|
|
{
|
|
$units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
$bytes = max($bytes, 0);
|
|
$pow = floor(($bytes ? log($bytes) : 0) / log(1024));
|
|
$pow = min($pow, count($units) - 1);
|
|
$bytes /= pow(1024, $pow);
|
|
|
|
return round($bytes, 2) . ' ' . $units[$pow];
|
|
}
|
|
|
|
/**
|
|
* Set file select callback
|
|
*/
|
|
public function setOnFileSelect(callable $callback): void
|
|
{
|
|
$this->onFileSelect = $callback;
|
|
}
|
|
|
|
/**
|
|
* Set edit file callback
|
|
*/
|
|
public function setOnEditFile(callable $callback): void
|
|
{
|
|
$this->onEditFile = $callback;
|
|
}
|
|
|
|
/**
|
|
* Set rename file callback
|
|
*/
|
|
public function setOnRenameFile(callable $callback): void
|
|
{
|
|
$this->onRenameFile = $callback;
|
|
}
|
|
|
|
/**
|
|
* Set delete file callback
|
|
*/
|
|
public function setOnDeleteFile(callable $callback): void
|
|
{
|
|
$this->onDeleteFile = $callback;
|
|
}
|
|
|
|
/**
|
|
* Render actions cell with edit, rename, and delete buttons for files
|
|
*/
|
|
public function renderActionsCell(array $rowData, int $rowIndex): Container
|
|
{
|
|
// Match the cell style from Table (100px width for icon buttons)
|
|
$container = new Container(
|
|
'w-25 py-1 border-r border-gray-300 flex flex-row items-center justify-center gap-1',
|
|
);
|
|
|
|
// Only show action buttons for files (not directories)
|
|
if (!($rowData['isDir'] ?? false) && !empty($rowData['path'])) {
|
|
$fileBrowser = $this;
|
|
|
|
// Edit button
|
|
$editButton = new Button('', 'p-1 text-blue-500 hover:text-blue-600 flex items-center justify-center');
|
|
$editIcon = new Icon(\PHPNative\Tailwind\Data\Icon::edit, 16, 'text-blue-500');
|
|
$editButton->setIcon($editIcon);
|
|
$editButton->setOnClick(function () use ($fileBrowser, $rowData) {
|
|
if ($fileBrowser->onEditFile !== null) {
|
|
($fileBrowser->onEditFile)($rowData['path'], $rowData);
|
|
}
|
|
});
|
|
$container->addComponent($editButton);
|
|
|
|
// Rename button
|
|
$renameButton = new Button('', 'p-1 text-amber-500 hover:text-amber-600 flex items-center justify-center');
|
|
$renameIcon = new Icon(\PHPNative\Tailwind\Data\Icon::pen, 16, 'text-amber-500');
|
|
$renameButton->setIcon($renameIcon);
|
|
$renameButton->setOnClick(function () use ($fileBrowser, $rowData) {
|
|
if ($fileBrowser->onRenameFile !== null) {
|
|
($fileBrowser->onRenameFile)($rowData['path'], $rowData);
|
|
}
|
|
});
|
|
$container->addComponent($renameButton);
|
|
|
|
// Delete button
|
|
$deleteButton = new Button('', 'p-1 text-red-500 hover:text-red-600 flex items-center justify-center');
|
|
$deleteIcon = new Icon(\PHPNative\Tailwind\Data\Icon::trash, 16, 'text-red-500');
|
|
$deleteButton->setIcon($deleteIcon);
|
|
$deleteButton->setOnClick(function () use ($fileBrowser, $rowData) {
|
|
if ($fileBrowser->onDeleteFile !== null) {
|
|
($fileBrowser->onDeleteFile)($rowData['path'], $rowData);
|
|
}
|
|
});
|
|
$container->addComponent($deleteButton);
|
|
}
|
|
|
|
return $container;
|
|
}
|
|
|
|
/**
|
|
* Get current path
|
|
*/
|
|
public function getCurrentPath(): string
|
|
{
|
|
return $this->currentPath;
|
|
}
|
|
|
|
/**
|
|
* Set path (updates label only, doesn't reload)
|
|
*/
|
|
public function setPath(string $path): void
|
|
{
|
|
$this->currentPath = $path;
|
|
$this->pathLabel->setText($path);
|
|
}
|
|
|
|
/**
|
|
* Set file data directly (for remote browsers where data comes from async operations)
|
|
*/
|
|
public function setFileData(array $files): void
|
|
{
|
|
$tableData = [];
|
|
|
|
// Add parent directory if not at root
|
|
if ($this->currentPath !== '/') {
|
|
$parentPath = dirname($this->currentPath);
|
|
if ($parentPath === '.') {
|
|
$parentPath = '/';
|
|
}
|
|
$tableData[] = [
|
|
'type' => 'DIR',
|
|
'name' => '..',
|
|
'size' => '',
|
|
'modified' => '',
|
|
'path' => $parentPath,
|
|
'isDir' => true,
|
|
];
|
|
}
|
|
|
|
// Add files
|
|
foreach ($files as $file) {
|
|
$tableData[] = [
|
|
'type' => $file['isDir'] ? 'DIR' : 'FILE',
|
|
'name' => $file['name'],
|
|
'size' => $file['isDir'] ? '' : $this->formatSize($file['size']),
|
|
'modified' => isset($file['mtime']) ? date('Y-m-d H:i:s', $file['mtime']) : '',
|
|
'path' => $file['path'],
|
|
'isDir' => $file['isDir'],
|
|
];
|
|
}
|
|
|
|
$this->fileTable->setData($tableData);
|
|
|
|
// Set up row selection handler AFTER data is set (for remote browsers)
|
|
// This needs to be done every time because setData might reset handlers
|
|
$this->setupRemoteNavigationHandler();
|
|
}
|
|
|
|
/**
|
|
* Setup navigation handler for remote file browser
|
|
*/
|
|
private function setupRemoteNavigationHandler(): void
|
|
{
|
|
if (!$this->isRemote) {
|
|
return;
|
|
}
|
|
|
|
$fileBrowser = $this;
|
|
$this->fileTable->setOnRowSelect(function ($index, $row) use ($fileBrowser) {
|
|
if ($row && isset($row['isDir']) && $row['isDir'] && !empty($row['path'])) {
|
|
// Trigger the external callback for directory navigation
|
|
if ($fileBrowser->onFileSelect !== null) {
|
|
($fileBrowser->onFileSelect)($row['path'], $row);
|
|
}
|
|
} elseif ($row && isset($row['path']) && !empty($row['path']) && !($row['isDir'] ?? false)) {
|
|
// File selected
|
|
if ($fileBrowser->onFileSelect !== null) {
|
|
($fileBrowser->onFileSelect)($row['path'], $row);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|