From f36ebff4e3b480b5d0d39cd64010869aa3d1b7c9 Mon Sep 17 00:00:00 2001 From: Giuseppe Criscione <18699708+giuscris@users.noreply.github.com> Date: Sat, 31 May 2025 14:04:12 +0200 Subject: [PATCH 1/6] Add access to site files --- formwork/config/system.yaml | 2 ++ formwork/routes.php | 4 +++ formwork/src/Cms/Site.php | 31 ++++++++++++++++++++ formwork/src/Controllers/FilesController.php | 25 ++++++++++++++++ formwork/src/Files/FileUriGenerator.php | 6 ++++ site/files/.gitkeep | 0 6 files changed, 68 insertions(+) create mode 100644 formwork/src/Controllers/FilesController.php create mode 100644 site/files/.gitkeep diff --git a/formwork/config/system.yaml b/formwork/config/system.yaml index 736d5a2a0..37f7623e5 100644 --- a/formwork/config/system.yaml +++ b/formwork/config/system.yaml @@ -41,6 +41,8 @@ fields: files: allowedExtensions: [] metadataExtension: .meta.yaml + paths: + site: '${%ROOT_PATH%}/site/files' uploads: path: '${%ROOT_PATH%}/site/files' baseDestinations: diff --git a/formwork/routes.php b/formwork/routes.php index 4d895b32e..bd5e52d61 100644 --- a/formwork/routes.php +++ b/formwork/routes.php @@ -30,6 +30,10 @@ 'path' => '/site/templates/assets/{file:all}/', 'action' => 'Formwork\Controllers\AssetsController@template', ], + 'files' => [ + 'path' => '/files/{name}/', + 'action' => 'Formwork\Controllers\FilesController@file', + ], 'tag.pagination' => [ 'path' => '/{page:all}/tag/{tagName:slug}/page/{paginationPage:number}/', 'action' => 'Formwork\Controllers\PageController@load', diff --git a/formwork/src/Cms/Site.php b/formwork/src/Cms/Site.php index b4b01ba8c..61f41e9d5 100644 --- a/formwork/src/Cms/Site.php +++ b/formwork/src/Cms/Site.php @@ -3,6 +3,8 @@ namespace Formwork\Cms; use Formwork\Config\Config; +use Formwork\Files\FileCollection; +use Formwork\Files\FileFactory; use Formwork\Languages\Languages; use Formwork\Metadata\MetadataCollection; use Formwork\Model\Model; @@ -20,6 +22,7 @@ use Formwork\Users\Users; use Formwork\Utils\Arr; use Formwork\Utils\FileSystem; +use Formwork\Utils\Str; use Stringable; class Site extends Model implements Stringable @@ -104,6 +107,11 @@ class Site extends Model implements Stringable */ protected array $routeAliases; + /** + * Site files + */ + protected FileCollection $files; + /** * @param array $data */ @@ -468,6 +476,29 @@ public function load(): void $this->loadRouteAliases(); } + public function files(): FileCollection + { + if (isset($this->files)) { + return $this->files; + } + + $files = []; + + $path = $this->config->get('system.files.paths.site'); + + foreach (FileSystem::listFiles($path) as $file) { + $extension = '.' . FileSystem::extension($file); + if (Str::endsWith($file, $this->config->get('system.files.metadataExtension'))) { + continue; + } + if (in_array($extension, $this->config->get('system.files.allowedExtensions'), true)) { + $files[] = $this->app->getService(FileFactory::class)->make(FileSystem::joinPaths($path, $file)); + } + } + + return $this->files = new FileCollection($files); + } + /** * @param array $metadata */ diff --git a/formwork/src/Controllers/FilesController.php b/formwork/src/Controllers/FilesController.php new file mode 100644 index 000000000..a8eb8f112 --- /dev/null +++ b/formwork/src/Controllers/FilesController.php @@ -0,0 +1,25 @@ +config->get('system.files.paths.site'), $routeParams->get('name')); + + if (FileSystem::isFile($path, assertExists: false)) { + return new FileResponse($path); + } + + return $this->forward(PageController::class, 'error'); + } +} diff --git a/formwork/src/Files/FileUriGenerator.php b/formwork/src/Files/FileUriGenerator.php index 9d504b090..a556d5606 100644 --- a/formwork/src/Files/FileUriGenerator.php +++ b/formwork/src/Files/FileUriGenerator.php @@ -29,6 +29,12 @@ public function generate(File $file): string { $path = $file->path(); + if (Str::startsWith($path, FileSystem::normalizePath($this->config->get('system.files.paths.site')))) { + $name = basename($path); + $uriPath = $this->router->generate('files', compact('name')); + return $this->site->uri($uriPath, includeLanguage: false); + } + if (Str::startsWith($path, FileSystem::normalizePath($this->config->get('system.images.processPath')))) { $id = basename(dirname($path)); $name = basename($path); diff --git a/site/files/.gitkeep b/site/files/.gitkeep new file mode 100644 index 000000000..e69de29bb From 4eb081088063a5ee7a9d92b6ce781ec2af3067dd Mon Sep 17 00:00:00 2001 From: Giuseppe Criscione <18699708+giuscris@users.noreply.github.com> Date: Sun, 1 Jun 2025 02:42:28 +0200 Subject: [PATCH 2/6] Allow uploads from nested file inputs --- formwork/src/Fields/FieldCollection.php | 10 +++++----- formwork/src/Http/FilesData.php | 12 +----------- formwork/src/Http/Request.php | 16 +++++++++++----- 3 files changed, 17 insertions(+), 21 deletions(-) diff --git a/formwork/src/Fields/FieldCollection.php b/formwork/src/Fields/FieldCollection.php index 1fa27ea76..a8074b116 100644 --- a/formwork/src/Fields/FieldCollection.php +++ b/formwork/src/Fields/FieldCollection.php @@ -113,10 +113,10 @@ public function setValues(mixed $data, mixed $default = null): static */ public function setValuesFromRequest(Request $request, mixed $default = null): static { - return $this->setValues([ - ...$request->query()->toArray(), - ...$request->input()->toArray(), - ...$request->files()->toArray(), - ], $default); + return $this->setValues(array_merge_recursive( + $request->query()->toArray(), + $request->input()->toArray(), + $request->files()->toArray() + ), $default); } } diff --git a/formwork/src/Http/FilesData.php b/formwork/src/Http/FilesData.php index 85de593bf..3bcfad1ad 100644 --- a/formwork/src/Http/FilesData.php +++ b/formwork/src/Http/FilesData.php @@ -25,16 +25,6 @@ public function isEmpty(): bool */ public function getAll(): array { - $result = []; - foreach ($this->data as $value) { - if (is_array($value)) { - foreach ($value as $v) { - $result[] = $v; - } - } else { - $result[] = $value; - } - } - return $result; + return Arr::flatten($this->data); } } diff --git a/formwork/src/Http/Request.php b/formwork/src/Http/Request.php index 4ba15c0ed..bfb1a9d55 100644 --- a/formwork/src/Http/Request.php +++ b/formwork/src/Http/Request.php @@ -520,17 +520,23 @@ protected function prepareFiles(array $files): FilesData foreach ($files as $fieldName => $data) { if (is_array($data['name'])) { foreach (array_keys($data['name']) as $i) { - /** - * @var array> $result - */ - $result[$fieldName][] = new UploadedFile($fieldName, [ + $props = [ 'name' => $data['name'][$i], 'full_path' => $data['full_path'][$i] ?? '', 'type' => $data['type'][$i] ?? '', 'tmp_name' => $data['tmp_name'][$i] ?? '', 'error' => $data['error'][$i] ?? '', 'size' => $data['size'][$i] ?? '', - ]); + ]; + + if (is_array($data['name'][$i])) { + $result[$fieldName] = $this->prepareFiles([$i => $props])->toArray(); + } else { + /** + * @var array> $result + */ + $result[$fieldName][] = new UploadedFile($fieldName, $props); + } } } else { $result[$fieldName] = new UploadedFile($fieldName, $data); From 66d8d11aee390e87cedd5ca4100cb2fc53ac2fb7 Mon Sep 17 00:00:00 2001 From: Giuseppe Criscione <18699708+giuscris@users.noreply.github.com> Date: Sun, 1 Jun 2025 02:47:16 +0200 Subject: [PATCH 3/6] Add files view --- .../Panel/Controllers/AbstractController.php | 6 + .../src/Panel/Controllers/FilesController.php | 303 +++++++++++++----- .../src/Panel/Controllers/PagesController.php | 139 +------- panel/modals/uploadFile.yaml | 29 ++ panel/routes.php | 72 ++--- panel/src/scss/components/_base.scss | 1 + panel/src/scss/components/_dropdowns.scss | 1 + panel/src/scss/components/_files-list.scss | 110 +++++-- panel/src/scss/components/_users.scss | 2 +- .../scss/components/forms/_forms-image.scss | 8 +- panel/src/ts/components/files.ts | 7 + panel/src/ts/components/fileslist.ts | 292 +++++++++++++++++ .../src/ts/components/inputs/upload-input.ts | 262 ++------------- panel/translations/de.yaml | 71 ++-- panel/translations/en.yaml | 71 ++-- panel/translations/es.yaml | 71 ++-- panel/translations/fr.yaml | 71 ++-- panel/translations/it.yaml | 71 ++-- panel/translations/pl.yaml | 71 ++-- panel/translations/pt.yaml | 71 ++-- panel/translations/ru.yaml | 71 ++-- panel/translations/uk.yaml | 71 ++-- panel/views/fields/partials/filelist.php | 69 ---- panel/views/fields/upload.php | 4 +- panel/views/files/edit.php | 118 +++++++ panel/views/files/index.php | 112 +------ panel/views/modals/modal.php | 8 +- panel/views/partials/files/file/item.php | 44 +++ panel/views/partials/files/file/list.php | 71 ++++ .../views/partials/files/images/info/exif.php | 26 +- .../views/partials/files/images/info/info.php | 24 +- site/users/roles/admin.yaml | 1 + site/users/roles/user.yaml | 1 + 33 files changed, 1367 insertions(+), 982 deletions(-) create mode 100644 panel/modals/uploadFile.yaml create mode 100644 panel/src/ts/components/fileslist.ts delete mode 100644 panel/views/fields/partials/filelist.php create mode 100644 panel/views/files/edit.php create mode 100644 panel/views/partials/files/file/item.php create mode 100644 panel/views/partials/files/file/list.php diff --git a/formwork/src/Panel/Controllers/AbstractController.php b/formwork/src/Panel/Controllers/AbstractController.php index 17999a948..b25d3c6d5 100644 --- a/formwork/src/Panel/Controllers/AbstractController.php +++ b/formwork/src/Panel/Controllers/AbstractController.php @@ -104,6 +104,12 @@ protected function defaults(): array 'permissions' => 'pages', 'badge' => $this->site->descendants()->count(), ], + 'files' => [ + 'label' => $this->translate('panel.files.files'), + 'uri' => '/files/', + 'permissions' => 'files', + 'badge' => null, + ], 'statistics' => [ 'label' => $this->translate('panel.statistics.statistics'), 'uri' => '/statistics/', diff --git a/formwork/src/Panel/Controllers/FilesController.php b/formwork/src/Panel/Controllers/FilesController.php index 6d7855094..9d3bf323e 100644 --- a/formwork/src/Panel/Controllers/FilesController.php +++ b/formwork/src/Panel/Controllers/FilesController.php @@ -2,47 +2,103 @@ namespace Formwork\Panel\Controllers; +use Formwork\Cms\Site; use Formwork\Exceptions\TranslatedException; use Formwork\Fields\FieldCollection; use Formwork\Files\File; use Formwork\Files\FileCollection; +use Formwork\Files\FileFactory; +use Formwork\Http\JsonResponse; use Formwork\Http\RequestMethod; use Formwork\Http\Response; +use Formwork\Http\ResponseStatus; +use Formwork\Images\Image; use Formwork\Pages\Page; use Formwork\Parsers\Yaml; use Formwork\Router\RouteParams; use Formwork\Utils\Arr; +use Formwork\Utils\Date; use Formwork\Utils\FileSystem; +use Formwork\Utils\Path; use Formwork\Utils\Str; +use Formwork\Utils\Uri; +use InvalidArgumentException; final class FilesController extends AbstractController { /** * FilesController@index action */ - public function index(RouteParams $routeParams): Response + public function index(): Response { - if (!$this->hasPermission('pages.file')) { + if (!$this->hasPermission('files.index')) { return $this->forward(ErrorsController::class, 'forbidden'); } - $page = $this->site->findPage($routeParams->get('page')); + if ($this->request->method() === RequestMethod::POST) { + if (!$this->hasPermission('files.upload')) { + return $this->forward(ErrorsController::class, 'forbidden'); + } - $filename = $routeParams->get('filename'); + $fields = $this->modal('uploadFile')->fields(); + + $fields->setValuesFromRequest($this->request, null)->validate(); + + $filesField = $fields->get('files'); + + if (!$filesField->isEmpty()) { + $files = $filesField->isMultiple() ? $filesField->value() : [$filesField->value()]; + + /** @var Page|Site */ + $parent = $fields->get('parent')->return(); + + $destination = $parent instanceof Site + ? $this->config->get('system.files.paths.site') + : $parent->contentPath(); + + foreach ($files as $file) { + $this->fileUploader->upload( + $file, + $destination, + $filesField->filename(), + $filesField->acceptMimeTypes(), + $filesField->overwrite(), + ); + } - if ($page === null) { - $this->panel->notify($this->translate('panel.pages.page.cannotGetFileInfo.pageNotFound'), 'error'); - return $this->redirectToReferer(default: $this->generateRoute('panel.pages'), base: $this->panel->panelRoot()); + $this->panel->notify($this->translate('panel.files.uploaded'), 'success'); + } + + return $this->redirect($this->generateRoute('panel.files.index')); } - if (!$page->files()->has($filename)) { - $this->panel->notify($this->translate('panel.pages.page.cannotGetFileInfo.fileNotFound'), 'error'); - return $this->redirect($this->generateRoute('panel.pages.edit', ['page' => $routeParams->get('page')])); + return new Response($this->view('files.index', [ + 'title' => $this->translate('panel.files.files'), + 'files' => $this->getFiles(), + ])); + } + + /** + * FilesController@edit action + */ + public function edit(RouteParams $routeParams): Response + { + if (!$this->hasPermission('files.edit')) { + return $this->forward(ErrorsController::class, 'forbidden'); } - $files = $page->files(); + $model = $this->getModel($routeParams); + + $filename = $routeParams->get('filename'); + + $files = $model?->files(); + + $file = $files?->get($filename); - $file = $files->get($filename); + if ($model === null || $files === null || $file === null) { + $this->panel->notify($this->translate('panel.files.fileNotFound'), 'error'); + return $this->redirectToReferer(base: $this->panel->panelRoot()); + } switch ($this->request->method()) { case RequestMethod::GET: @@ -59,16 +115,16 @@ public function index(RouteParams $routeParams): Response $this->updateFileMetadata($file, $file->fields()); - $this->updateLastModifiedTime($page); + $this->updateLastModifiedTime($model); $this->panel->notify($this->translate('panel.files.metadata.updated'), 'success'); - return $this->redirect($this->generateRoute('panel.files.index', ['page' => $page->route(), 'filename' => $filename])); + return $this->redirect($this->generateRoute('panel.files.edit', $routeParams->toArray())); } - return new Response($this->view('files.index', [ + return new Response($this->view('files.edit', [ 'title' => $file->name(), - 'page' => $page, + 'model' => $model, 'file' => $file, ...$this->getPreviousAndNextFile($files, $file), ])); @@ -77,129 +133,200 @@ public function index(RouteParams $routeParams): Response /** * FilesController@delete action */ - public function delete(RouteParams $routeParams): Response + public function delete(RouteParams $routeParams): JsonResponse|Response { - if (!$this->hasPermission('pages.deleteFiles')) { + if (!$this->hasPermission('files.delete')) { return $this->forward(ErrorsController::class, 'forbidden'); } - $page = $this->site->findPage($routeParams->get('page')); + $model = $this->getModel($routeParams); - if ($page === null) { - $this->panel->notify($this->translate('panel.files.cannotDelete.pageNotFound'), 'error'); - return $this->redirectToReferer(default: $this->generateRoute('panel.pages'), base: $this->panel->panelRoot()); - } + $file = $model?->files()?->get($routeParams->get('filename')); - if (!$page->files()->has($routeParams->get('filename'))) { + if ($model === null || $file === null) { + if ($this->request->isXmlHttpRequest()) { + return JsonResponse::error($this->translate('panel.files.cannotDelete.fileNotFound'), ResponseStatus::InternalServerError); + } $this->panel->notify($this->translate('panel.files.cannotDelete.fileNotFound'), 'error'); - return $this->redirect($this->generateRoute('panel.pages.edit', ['page' => $routeParams->get('page')])); + return $this->redirectToReferer(base: $this->panel->panelRoot()); } - FileSystem::delete($page->contentPath() . $routeParams->get('filename')); + FileSystem::delete($file->path()); - $this->updateLastModifiedTime($page); + $this->updateLastModifiedTime($model); - $this->panel->notify($this->translate('panel.pages.page.fileDeleted'), 'success'); - return $this->redirect($this->generateRoute('panel.pages.edit', ['page' => $routeParams->get('page')])); + if ($this->request->isXmlHttpRequest()) { + return JsonResponse::success($this->translate('panel.files.deleted')); + } + $this->panel->notify($this->translate('panel.files.deleted'), 'success'); + return $this->redirect($this->generateRoute('panel.files.dashboard')); } /** * FilesController@rename action */ - public function rename(RouteParams $routeParams): Response + public function rename(RouteParams $routeParams, FileFactory $fileFactory): JsonResponse|Response { - if (!$this->hasPermission('pages.renameFiles')) { + if (!$this->hasPermission('files.rename')) { return $this->forward(ErrorsController::class, 'forbidden'); } - $page = $this->site->findPage($routeParams->get('page')); + $model = $this->getModel($routeParams); - $fields = $this->modal('renameFile')->fields(); + $fields = $this->modal($this->request->isXmlHttpRequest() ? 'renameFileItem' : 'renameFile')->fields(); $fields->setValues($this->request->input())->validate(); $data = $fields->everyItem()->value(); - if ($page === null) { - $this->panel->notify($this->translate('panel.pages.page.cannotRenameFile.pageNotFound'), 'error'); - return $this->redirectToReferer(default: $this->generateRoute('panel.pages'), base: $this->panel->panelRoot()); - } + $filename = $routeParams->get('filename'); + + $file = $model?->files()->get($filename); - if (!$page->files()->has($routeParams->get('filename'))) { + if ($model === null || $file === null) { + if ($this->request->isXmlHttpRequest()) { + return JsonResponse::error($this->translate('panel.files.cannotRename.fileNotFound'), ResponseStatus::InternalServerError); + } $this->panel->notify($this->translate('panel.files.cannotRename.fileNotFound'), 'error'); - return $this->redirect($this->generateRoute('panel.pages.edit', ['page' => $routeParams->get('page')])); + return $this->redirect($this->generateRoute('panel.files.edit', $routeParams->toArray())); } $name = Str::slug(FileSystem::name($data->get('filename'))); - $extension = FileSystem::extension($routeParams->get('filename')); + $extension = FileSystem::extension($filename); $newName = $name . '.' . $extension; - $previousName = $routeParams->get('filename'); - - if ($newName !== $previousName) { - if ($page->files()->has($newName)) { - $this->panel->notify($this->translate('panel.pages.page.cannotRenameFile.fileAlreadyExists'), 'error'); + if ($newName !== $filename) { + if ($model->files()->has($newName)) { + $this->panel->notify($this->translate('panel.files.cannotRename.fileAlreadyExists'), 'error'); } else { - FileSystem::move($page->contentPath() . $previousName, $page->contentPath() . $newName); - $this->updateLastModifiedTime($page); - - $this->panel->notify($this->translate('panel.pages.page.fileRenamed'), 'success'); + $dirname = dirname($file->path()); + $destination = FileSystem::joinPaths($dirname, $newName); + + FileSystem::move($file->path(), $destination); + + $file = $fileFactory->make($destination); + + $this->updateLastModifiedTime($model); + + if ($this->request->isXmlHttpRequest()) { + return JsonResponse::success($this->translate('panel.files.renamed'), data: [ + 'filename' => $newName, + 'uri' => $file->uri(), + 'size' => $file->size(), + 'lastModifiedTime' => Date::formatTimestamp( + $file->lastModifiedTime(), + $this->config->get('system.date.datetimeFormat'), + $this->translations->getCurrent() + ), + 'type' => $file->type(), + 'thumbnail' => $this->getThumbnailUri($file), + 'actions' => $this->getActionsUri($file, $model), + ]); + } + $this->panel->notify($this->translate('panel.files.renamed'), 'success'); } } - return $this->redirect($this->generateRoute('panel.files.index', ['page' => $routeParams->get('page'), 'filename' => $newName])); + return $this->redirect($this->generateRoute('panel.files.edit', [...$routeParams->toArray(), 'filename' => $newName])); } /** * FilesController@replace action */ - public function replace(RouteParams $routeParams): Response + public function replace(RouteParams $routeParams): JsonResponse|Response { - if (!$this->hasPermission('pages.replaceFiles')) { + if (!$this->hasPermission('files.replace')) { return $this->forward(ErrorsController::class, 'forbidden'); } - $page = $this->site->findPage($routeParams->get('page')); + $model = $this->getModel($routeParams); $filename = $routeParams->get('filename'); - if ($page === null) { - $this->panel->notify($this->translate('panel.pages.page.cannotReplaceFile.pageNotFound'), 'error'); - return $this->redirectToReferer(default: $this->generateRoute('panel.pages'), base: $this->panel->panelRoot()); - } + $file = $model?->files()->get($filename); - if ($page->contentPath() === null || !$page->files()->has($filename)) { - $this->panel->notify($this->translate('panel.pages.page.cannotReplaceFile.fileNotFound'), 'error'); - return $this->redirectToReferer(default: $this->generateRoute('panel.pages'), base: $this->panel->panelRoot()); + if ($model === null || $file === null) { + if ($this->request->isXmlHttpRequest()) { + return JsonResponse::error($this->translate('panel.files.cannotReplace.fileNotFound'), ResponseStatus::InternalServerError); + } + $this->panel->notify($this->translate('panel.files.cannotReplace.fileNotFound'), 'error'); + return $this->redirectToReferer(base: $this->panel->panelRoot()); } if (!$this->request->files()->isEmpty()) { $files = $this->request->files()->getAll(); if (count($files) > 1) { - $this->panel->notify($this->translate('panel.pages.page.cannotReplaceFile.multipleFiles'), 'error'); - return $this->redirect($this->generateRoute('panel.files.index', ['page' => $routeParams->get('page'), 'filename' => $filename])); + $this->panel->notify($this->translate('panel.files.cannotReplace.multipleFiles'), 'error'); + return $this->redirect($this->generateRoute('panel.files.edit', $routeParams->toArray())); } try { - $this->fileUploader->upload( + $file = $this->fileUploader->upload( $files[0], - $page->contentPath(), + dirname($file->path()), FileSystem::name($filename), - [$page->files()->get($filename)->mimeType()], + [$model->files()->get($filename)->mimeType()], overwrite: true, ); } catch (TranslatedException $e) { + if ($this->request->isXmlHttpRequest()) { + return JsonResponse::error($this->translate('upload.error', $this->translate($e->getLanguageString())), ResponseStatus::InternalServerError); + } $this->panel->notify($this->translate('upload.error', $this->translate($e->getLanguageString())), 'error'); - return $this->redirect($this->generateRoute('panel.files.index', ['page' => $routeParams->get('page'), 'filename' => $filename])); + return $this->redirect($this->generateRoute('panel.files.edit', $routeParams->toArray())); } } - $this->updateLastModifiedTime($page); - + $this->updateLastModifiedTime($model); + + if ($this->request->isXmlHttpRequest()) { + return JsonResponse::success($this->translate('panel.uploader.uploaded'), data: [ + 'filename' => $file->name(), + 'uri' => $file->uri(), + 'size' => $file->size(), + 'lastModifiedTime' => Date::formatTimestamp( + $file->lastModifiedTime(), + $this->config->get('system.date.datetimeFormat'), + $this->translations->getCurrent() + ), + 'type' => $file->type(), + 'thumbnail' => $this->getThumbnailUri($file), + 'actions' => $this->getActionsUri($file, $model), + ]); + } $this->panel->notify($this->translate('panel.uploader.uploaded'), 'success'); - return $this->redirect($this->generateRoute('panel.files.index', ['page' => $routeParams->get('page'), 'filename' => $filename])); + return $this->redirect($this->generateRoute('panel.files.edit', $routeParams->toArray())); + } + + /** + * @return array + */ + private function getFiles(): array + { + $files = []; + + foreach ($this->site->files() as $fileCollectionItem) { + $files[] = [$fileCollectionItem, $this->site]; + } + + foreach ($this->site->descendants() as $pageCollection) { + foreach ($pageCollection->files() as $fileCollectionItem) { + $files[] = [$fileCollectionItem, $pageCollection]; + } + } + + return Arr::sort($files, sortBy: fn(array $a, array $b) => strnatcasecmp($a[0]->name(), $b[0]->name())); + } + + private function getModel(RouteParams $routeParams): Page|Site|null + { + return match ($routeParams->get('model')) { + 'page' => $this->site->findPage($routeParams->get('id')), + 'site' => $this->site, + default => throw new InvalidArgumentException('Invalid model'), + }; } /** @@ -233,12 +360,12 @@ private function updateFileMetadata(File $file, FieldCollection $fieldCollection } /** - * Update last modified time of the given page + * Update last modified time of the given model */ - private function updateLastModifiedTime(Page $page): void + private function updateLastModifiedTime(Page|Site $model): void { - if ($page->contentFile()?->path() !== null) { - FileSystem::touch($page->contentFile()->path()); + if ($model->contentFile()?->path() !== null) { + FileSystem::touch($model->contentFile()->path()); } } @@ -256,4 +383,34 @@ private function getPreviousAndNextFile(FileCollection $fileCollection, File $fi 'nextFile' => $fileCollection->nth($fileIndex + 1), ]; } + + /** + * Generate actions URIs for a file + * + * @return array + */ + private function getActionsUri(File $file, Page|Site $model): array + { + $params = ['model' => $model->getModelIdentifier(), 'id' => $model->route(), 'filename' => $file->name()]; + $actions = [ + 'info' => $this->router->generate('panel.files.edit', $params), + 'rename' => $this->router->generate('panel.files.rename', $params), + 'replace' => $this->router->generate('panel.files.replace', $params), + 'delete' => $this->router->generate('panel.files.delete', $params), + ]; + return Arr::map($actions, fn(string $route): string => Uri::make([], Path::join([$this->request->root(), $route]))); + } + + private function getThumbnailUri(File $file): ?string + { + switch ($file->type()) { + case 'image': + /** @var Image $file */ + return $file->square(300, 'contain')->uri(); + case 'video': + return $file->uri(); + default: + return null; + } + } } diff --git a/formwork/src/Panel/Controllers/PagesController.php b/formwork/src/Panel/Controllers/PagesController.php index 1d96b1974..d5260314a 100644 --- a/formwork/src/Panel/Controllers/PagesController.php +++ b/formwork/src/Panel/Controllers/PagesController.php @@ -19,7 +19,9 @@ use Formwork\Router\RouteParams; use Formwork\Utils\Arr; use Formwork\Utils\Constraint; +use Formwork\Utils\Date; use Formwork\Utils\FileSystem; +use Formwork\Utils\Path; use Formwork\Utils\Str; use Formwork\Utils\Uri; use UnexpectedValueException; @@ -331,7 +333,7 @@ public function delete(RouteParams $routeParams): Response */ public function upload(RouteParams $routeParams): Response|JsonResponse { - if (!$this->hasPermission('pages.uploadFiles')) { + if (!$this->hasPermission('files.upload')) { return $this->forward(ErrorsController::class, 'forbidden'); } @@ -366,8 +368,13 @@ public function upload(RouteParams $routeParams): Response|JsonResponse $this->updateLastModifiedTime($page); return JsonResponse::success($this->translate('panel.uploader.uploaded'), data: Arr::map($uploadedFiles, fn(File $file) => [ - 'name' => $file->name(), - 'size' => $file->size(), + 'name' => $file->name(), + 'size' => $file->size(), + 'lastModifiedTime' => Date::formatTimestamp( + $file->lastModifiedTime(), + $this->config->get('system.date.datetimeFormat'), + $this->translations->getCurrent() + ), 'type' => $file->type(), 'mimeType' => $file->mimeType(), 'hash' => $file->hash(), @@ -377,127 +384,15 @@ public function upload(RouteParams $routeParams): Response|JsonResponse 'video' => $file->uri(), default => null, }, + 'actions' => Arr::map([ + 'info' => $this->router->generate('panel.files.edit', ['model' => $page->getModelIdentifier(), 'id' => $page->route(), 'filename' => $file->name()]), + 'rename' => $this->router->generate('panel.files.rename', ['model' => $page->getModelIdentifier(), 'id' => $page->route(), 'filename' => $file->name()]), + 'replace' => $this->router->generate('panel.files.replace', ['model' => $page->getModelIdentifier(), 'id' => $page->route(), 'filename' => $file->name()]), + 'delete' => $this->router->generate('panel.files.delete', ['model' => $page->getModelIdentifier(), 'id' => $page->route(), 'filename' => $file->name()]), + ], fn(string $route): string => Uri::make([], Path::join([$this->request->root(), $route]))), ]), ); } - /** - * Pages@deleteFile action - */ - public function deleteFile(RouteParams $routeParams): JsonResponse|Response - { - if (!$this->hasPermission('pages.deleteFiles')) { - return $this->forward(ErrorsController::class, 'forbidden'); - } - - $page = $this->site->findPage($routeParams->get('page')); - - $filename = $this->request->input()->get('filename'); - - if ($page === null || !$page->files()->has($filename)) { - return JsonResponse::error($this->translate('panel.files.cannotDelete.fileNotFound'), ResponseStatus::InternalServerError); - } - - FileSystem::delete($page->contentPath() . $this->request->input()->get('filename')); - - $this->updateLastModifiedTime($page); - - return JsonResponse::success($this->translate('panel.files.deleted')); - } - - /** - * Pages@renameFile action - */ - public function renameFile(RouteParams $routeParams): JsonResponse|Response - { - if (!$this->hasPermission('pages.renameFiles')) { - return $this->forward(ErrorsController::class, 'forbidden'); - } - - $previousName = $this->request->input()->get('filename'); - - $page = $this->site->findPage($routeParams->get('page')); - - $fields = $this->modal('renameFileItem')->fields(); - - $fields->setValues($this->request->input())->validate(); - - $data = $fields->everyItem()->value(); - - if ($page === null || !$page->files()->has($previousName)) { - return JsonResponse::error($this->translate('panel.files.cannotRename.fileNotFound'), ResponseStatus::InternalServerError); - } - - $name = Str::slug(FileSystem::name($data->get('filename'))); - $extension = FileSystem::extension($previousName); - - $newName = $name . '.' . $extension; - - if ($newName !== $previousName) { - if ($page->files()->has($newName)) { - $message = $this->translate('panel.files.cannotRename.fileAlreadyExists'); - } else { - FileSystem::move($page->contentPath() . $previousName, $page->contentPath() . $newName); - $this->updateLastModifiedTime($page); - } - } - - return JsonResponse::success($message ?? $this->translate('panel.files.renamed'), data: ['filename' => $newName]); - } - - /** - * Pages@replaceFile action - */ - public function replaceFile(RouteParams $routeParams): Response|JsonResponse - { - if (!$this->hasPermission('pages.replaceFiles')) { - return $this->forward(ErrorsController::class, 'forbidden'); - } - - $page = $this->site->findPage($routeParams->get('page')); - - $filename = $this->request->input()->get('filename'); - - if ($page === null || $page->contentPath() === null || !$page->files()->has($filename)) { - return JsonResponse::error($this->translate('panel.files.cannotReplace.fileNotFound'), ResponseStatus::InternalServerError); - } - - if (!$this->request->files()->isEmpty()) { - $files = $this->request->files()->getAll(); - - if (count($files) > 1) { - return JsonResponse::error($this->translate('panel.files.cannotReplace.multipleFiles'), ResponseStatus::InternalServerError); - } - - try { - $this->fileUploader->upload( - $files[0], - $page->contentPath(), - FileSystem::name($filename), - [$page->files()->get($filename)->mimeType()], - overwrite: true, - ); - } catch (TranslatedException $e) { - return JsonResponse::error($this->translate('upload.error', $this->translate($e->getLanguageString())), ResponseStatus::InternalServerError); - } - } - - $this->updateLastModifiedTime($page); - - $file = $page->files()->get($filename); - - $thumbnail = match ($file->type()) { - 'image' => $file->square(300, 'contain')->uri(), - 'video' => $file->uri(), - default => null, - }; - - return JsonResponse::success($this->translate('panel.uploader.uploaded'), data: [ - 'size' => $file->size(), - 'type' => $file->type(), - 'thumbnail' => $thumbnail, - ]); - } - /** * Create a new page */ @@ -532,7 +427,7 @@ private function updatePage(Page $page, RequestData $requestData, FieldCollectio foreach ($files as $file) { $this->fileUploader->upload( $file, - $field->destination(), + $field->destination() ?? $page->contentPath(), $field->filename(), $field->acceptMimeTypes(), $field->overwrite(), diff --git a/panel/modals/uploadFile.yaml b/panel/modals/uploadFile.yaml new file mode 100644 index 000000000..da4c579aa --- /dev/null +++ b/panel/modals/uploadFile.yaml @@ -0,0 +1,29 @@ +title: '{{panel.files.upload}}' + +size: large + +fields: + files: + type: upload + label: '{{panel.files.files}}' + multiple: true + + parent: + type: page + label: '{{panel.files.parent}}' + collection@: site.descendants + required: true + allowSite: true + +buttons: + dismiss: + action: dismiss + icon: times-circle + label: '{{panel.modal.action.cancel}}' + variant: secondary + + submit: + action: submit + icon: cloud-upload + label: '{{panel.modal.action.upload}}' + align: right diff --git a/panel/routes.php b/panel/routes.php index 6db79bc44..67c64f54e 100644 --- a/panel/routes.php +++ b/panel/routes.php @@ -43,30 +43,6 @@ 'action' => 'Formwork\Panel\Controllers\DashboardController@index', ], - 'panel.files.delete' => [ - 'path' => '/files/pages/{page:all}/{filename}/delete/', - 'action' => 'Formwork\Panel\Controllers\FilesController@delete', - 'methods' => ['POST'], - ], - - 'panel.files.rename' => [ - 'path' => '/files/pages/{page:all}/{filename}/rename/', - 'action' => 'Formwork\Panel\Controllers\FilesController@rename', - 'methods' => ['POST'], - ], - - 'panel.files.replace' => [ - 'path' => '/files/pages/{page:all}/{filename}/replace/', - 'action' => 'Formwork\Panel\Controllers\FilesController@replace', - 'methods' => ['POST'], - ], - - 'panel.files.index' => [ - 'path' => '/files/pages/{page:all}/{filename}/', - 'action' => 'Formwork\Panel\Controllers\FilesController@index', - 'methods' => ['GET', 'POST'], - ], - 'panel.pages' => [ 'path' => '/pages/', 'action' => 'Formwork\Panel\Controllers\PagesController@index', @@ -110,37 +86,49 @@ 'types' => ['XHR'], ], - 'panel.pages.deleteFile' => [ - 'path' => '/pages/{page:all}/file/delete/', - 'action' => 'Formwork\Panel\Controllers\PagesController@deleteFile', + 'panel.pages.delete' => [ + 'path' => '/pages/{page:all}/delete/', + 'action' => 'Formwork\Panel\Controllers\PagesController@delete', 'methods' => ['POST'], - 'types' => ['XHR'], ], - 'panel.pages.renameFile' => [ - 'path' => '/pages/{page:all}/file/rename/', - 'action' => 'Formwork\Panel\Controllers\PagesController@renameFile', + 'panel.pages.delete.lang' => [ + 'path' => '/pages/{page:all}/delete/language/{language:alpha}/', + 'action' => 'Formwork\Panel\Controllers\PagesController@delete', 'methods' => ['POST'], - 'types' => ['XHR'], ], - 'panel.pages.replaceFile' => [ - 'path' => '/pages/{page:all}/file/replace/', - 'action' => 'Formwork\Panel\Controllers\PagesController@replaceFile', + 'panel.files.index' => [ + 'path' => '/files/', + 'action' => 'Formwork\Panel\Controllers\FilesController@index', + 'methods' => ['GET', 'POST'], + ], + + 'panel.files.delete' => [ + 'path' => '/files/{model:page|site}/{id:all}?/{filename}/delete/', + 'action' => 'Formwork\Panel\Controllers\FilesController@delete', 'methods' => ['POST'], - 'types' => ['XHR'], + 'types' => ['HTTP', 'XHR'], ], - 'panel.pages.delete' => [ - 'path' => '/pages/{page:all}/delete/', - 'action' => 'Formwork\Panel\Controllers\PagesController@delete', + 'panel.files.rename' => [ + 'path' => '/files/{model:page|site}/{id:all}?/{filename}/rename/', + 'action' => 'Formwork\Panel\Controllers\FilesController@rename', 'methods' => ['POST'], + 'types' => ['HTTP', 'XHR'], ], - 'panel.pages.delete.lang' => [ - 'path' => '/pages/{page:all}/delete/language/{language:alpha}/', - 'action' => 'Formwork\Panel\Controllers\PagesController@delete', + 'panel.files.replace' => [ + 'path' => '/files/{model:page|site}/{id:all}?/{filename}/replace/', + 'action' => 'Formwork\Panel\Controllers\FilesController@replace', 'methods' => ['POST'], + 'types' => ['HTTP', 'XHR'], + ], + + 'panel.files.edit' => [ + 'path' => '/files/{model:page|site}/{id:all}?/{filename}/', + 'action' => 'Formwork\Panel\Controllers\FilesController@edit', + 'methods' => ['GET', 'POST'], ], 'panel.updates.check' => [ diff --git a/panel/src/scss/components/_base.scss b/panel/src/scss/components/_base.scss index 89c417ec5..e855011fc 100644 --- a/panel/src/scss/components/_base.scss +++ b/panel/src/scss/components/_base.scss @@ -45,6 +45,7 @@ img { } mark { + border-radius: $border-radius; background-color: var(--color-yellow-500); } diff --git a/panel/src/scss/components/_dropdowns.scss b/panel/src/scss/components/_dropdowns.scss index 5ae86f478..194383886 100644 --- a/panel/src/scss/components/_dropdowns.scss +++ b/panel/src/scss/components/_dropdowns.scss @@ -5,6 +5,7 @@ position: relative; display: inline-block; margin-bottom: 0.5rem; + text-align: left; } .dropdown-button { diff --git a/panel/src/scss/components/_files-list.scss b/panel/src/scss/components/_files-list.scss index efaffa327..75a0783ab 100644 --- a/panel/src/scss/components/_files-list.scss +++ b/panel/src/scss/components/_files-list.scss @@ -1,6 +1,8 @@ @use "mixins" as *; @use "variables" as *; +@use "sass:map"; + .files-list { padding: 0; margin: 0; @@ -9,6 +11,21 @@ @include user-select-none; } +.files-list-headers { + display: flex; + align-items: center; + padding: 0.25rem 0; + border-bottom: 1px solid var(--color-base-600); + font-size: $font-size-sm; + font-weight: 600; + @include user-select-none; +} + +.files-headers-cell, +.files-item-cell { + padding: 0.25rem 0.5rem; +} + .is-thumbnails .files-items { display: flex; flex-wrap: wrap; @@ -16,9 +33,10 @@ } .files-item { + position: relative; display: flex; align-items: center; - padding: 0.5rem; + padding: 0.25rem 0; border-bottom: 1px solid var(--color-base-600); font-size: $font-size-sm; @@ -27,35 +45,52 @@ } } -.files-item:first-child { - border-top: 1px solid var(--color-base-600); -} - .files-item .dropdown { margin-bottom: 0; } -.file-item-label { - flex-grow: 1; +.file-icon { + padding-right: 0.25rem; } -.file-icon { - margin-right: 0.5rem; +.file-name { + flex: 1; +} + +.file-parent { + flex: 0 0 25%; +} + +.file-date { + flex: 0 0 15%; } .file-size { - color: var(--color-base-300); + flex: 0 0 10%; +} + +.file-actions { + flex: 0 0 7.5%; + text-align: center; } .is-thumbnails .files-item { - position: relative; - width: 10rem; - height: 10rem; - box-sizing: border-box; + width: calc(33.3% - 0.5rem); + min-width: 5rem; + max-width: 10rem; padding: 0.5rem; border: 1px solid var(--color-base-500); border-radius: $border-radius; margin: 0.25rem; + aspect-ratio: 1; + + @media (min-width: map.get($breakpoints, "xs")) { + width: calc(25% - 0.5rem); + } + + @media (min-width: map.get($breakpoints, "lg")) { + width: calc(16.6% - 0.5rem); + } &:active, &:focus-within { @@ -64,34 +99,50 @@ } } +.is-thumbnails .file-parent, +.is-thumbnails .file-date, +.is-thumbnails .file-size { + display: none; +} + .file-thumbnail { - position: relative; - top: 0; - left: 0; + position: absolute; + left: 0.25rem; + width: 1.5rem; + height: 1.5rem; + border-radius: $border-radius; + background-color: var(--color-base-700); + object-fit: contain; + pointer-events: none; +} + +.is-thumbnails .files-list-headers { display: none; - width: 100%; - height: 100%; - background-origin: content-box; - background-position: center; - background-repeat: no-repeat; - background-size: contain; } .is-thumbnails .file-thumbnail { + top: 0; + left: 0; display: block; + width: 100%; + height: 100%; + padding: 0.25rem; + border-radius: 0; + background-color: transparent; } -.is-thumbnails .file-item-label { +.is-thumbnails .file-name { position: absolute; - right: 0.75rem; bottom: 0.5rem; - left: 0.75rem; + left: 50%; display: block; + max-width: calc(100% - 0.5rem); padding: 0 0.375rem; border-radius: $border-radius; background-color: var(--color-tooltip); font-size: $font-size-xs; text-align: center; + transform: translateX(-50%); @include user-select-none; } @@ -99,6 +150,12 @@ position: absolute; top: 0.5rem; right: 0.5rem; + display: none; +} + +.is-thumbnails .files-item:hover .dropdown, +.is-thumbnails .files-item:focus-within .dropdown { + display: block; } .is-thumbnails .dropdown-button { @@ -117,6 +174,7 @@ position: absolute; top: 50%; left: 50%; + padding: 0; color: var(--color-base-500); font-size: 4rem; line-height: 0; diff --git a/panel/src/scss/components/_users.scss b/panel/src/scss/components/_users.scss index 3c5228558..e3bfce90f 100644 --- a/panel/src/scss/components/_users.scss +++ b/panel/src/scss/components/_users.scss @@ -49,7 +49,7 @@ .users-item { display: flex; align-items: center; - padding: 0.5rem 0; + padding: 0.25rem 0; border-bottom: 1px solid var(--color-base-600); &:hover { diff --git a/panel/src/scss/components/forms/_forms-image.scss b/panel/src/scss/components/forms/_forms-image.scss index 2f552f2b6..53f201a1c 100644 --- a/panel/src/scss/components/forms/_forms-image.scss +++ b/panel/src/scss/components/forms/_forms-image.scss @@ -16,7 +16,7 @@ width: 10em; height: 10rem; box-sizing: border-box; - padding: 0.5rem; + padding: 0.25rem; border: 1px solid var(--color-base-500); border-radius: $border-radius; margin: 0.25rem; @@ -32,16 +32,18 @@ .image-picker-thumbnail::after { position: absolute; - right: 0.75rem; bottom: 0.5rem; - left: 0.75rem; + left: 50%; display: block; + max-width: calc(100% - 0.5rem); padding: 0 0.375rem; border-radius: $border-radius; background-color: var(--color-tooltip); content: attr(data-filename); font-size: $font-size-xs; text-align: center; + transform: translateX(-50%); + @include user-select-none; @include ellipsis; } diff --git a/panel/src/ts/components/files.ts b/panel/src/ts/components/files.ts index 4f2139b29..93449b584 100644 --- a/panel/src/ts/components/files.ts +++ b/panel/src/ts/components/files.ts @@ -1,8 +1,15 @@ import { $ } from "../utils/selectors"; import { app } from "../app"; +import { FilesList } from "./fileslist"; export class Files { constructor() { + const filesView = $("[data-view=files]"); + + if (filesView) { + new FilesList($(".files-list", filesView) as HTMLElement); + } + const fileForm = $("[data-form=file-form]"); if (fileForm) { diff --git a/panel/src/ts/components/fileslist.ts b/panel/src/ts/components/fileslist.ts new file mode 100644 index 000000000..89a9b4b00 --- /dev/null +++ b/panel/src/ts/components/fileslist.ts @@ -0,0 +1,292 @@ +import { $, $$ } from "../utils/selectors"; +import { escapeRegExp, makeDiacriticsRegExp } from "../utils/validation"; +import { app } from "../app"; +import { debounce } from "../utils/events"; +import { Form } from "./form"; +import { Notification } from "./notification"; +import { Request } from "../utils/request"; +import { SelectInput } from "./inputs/select-input"; +import { TagsInput } from "./inputs/tags-input"; + +export class FilesList { + readonly element: HTMLElement; + readonly form?: Form; + + constructor(element: HTMLElement, form?: Form) { + this.element = element; + this.form = form; + + this.initFileList(); + this.initModals(); + } + + sort(selector: string = ".file-name") { + const filesItems = $$(".files-item", this.element); + Array.from(filesItems) + .sort((a: HTMLElement, b: HTMLElement) => { + const keyA = $(selector, a)?.textContent; + const keyB = $(selector, b)?.textContent; + return keyA?.localeCompare(keyB ?? "") ?? 0; + }) + .forEach((element: HTMLElement) => { + element.parentElement?.appendChild(element); + }); + } + + private initFileList() { + const toggle = $(".form-togglegroup.files-list-view-as", this.element); + const searchInput = $(".files-search", this.element); + + if (toggle) { + const fieldName = toggle.dataset.for; + const viewAs = window.localStorage.getItem(`formwork.filesListViewAs[${fieldName}]`); + + if (viewAs) { + $$("input", toggle).forEach((input: HTMLInputElement) => (input.checked = false)); + ($(`input[value=${viewAs}]`, this.element) as HTMLInputElement).checked = true; + this.element.classList.toggle("is-thumbnails", viewAs === "thumbnails"); + } + + $$("input", toggle).forEach((input: HTMLInputElement) => { + input.addEventListener("input", () => { + this.element.classList.toggle("is-thumbnails", input.value === "thumbnails"); + window.localStorage.setItem(`formwork.filesListViewAs[${fieldName}]`, input.value); + }); + }); + } + + document.addEventListener("click", (event) => { + const target = event.target as HTMLElement; + if (!target.closest(".dropdown") && target.closest(".files-item")) { + const item = target.closest(".files-item") as HTMLElement; + if (typeof item.dataset.href === "string") { + location.href = item.dataset.href; + } + } + }); + + this.element.addEventListener("click", (event) => { + const element = (event.target as HTMLElement).closest("[data-command=replaceFile]") as HTMLElement; + if (element) { + const fileInput = document.createElement("input"); + fileInput.type = "file"; + fileInput.accept = element.dataset.mimetype as string; + fileInput.click(); + + fileInput.addEventListener("change", () => { + if (fileInput.files?.length) { + const formData = new FormData(); + formData.append("csrf-token", app.config.csrfToken as string); + formData.append("file", fileInput.files[0]); + + new Request( + { + method: "POST", + url: element.dataset.action as string, + data: formData, + }, + (response) => { + const notification = new Notification(response.message, response.status); + + if (response.status === "success") { + if (element.closest("[data-form=page-file-form]")) { + window.location.reload(); + } else if (response.data.thumbnail) { + const thumbnail = $(".file-thumbnail", element.closest(".files-item") as HTMLElement) as HTMLImageElement | HTMLVideoElement; + thumbnail.src = response.data.thumbnail; + + const fileDate = $(".file-date", element.closest(".files-item") as HTMLElement) as HTMLElement; + fileDate.textContent = response.data.lastModifiedTime; + + const fileSize = $(".file-size", element.closest(".files-item") as HTMLElement) as HTMLElement; + fileSize.textContent = response.data.size; + } + } + + notification.show(); + }, + ); + } + + fileInput.remove(); + }); + } + }); + + if (searchInput) { + const handleSearch = (event: Event) => { + const value = (event.target as HTMLInputElement).value; + ($(".files-item") as HTMLElement).classList.toggle("is-filtered", value.length > 0); + + $$(".files-item").forEach((element) => { + let matches = 0; + + for (const selector of [".file-name", ".file-parent-title"]) { + const item = $(selector, element) as HTMLElement; + + if (!item) { + continue; + } + + const text = item.textContent as string; + + const regexp = value ? new RegExp(`${makeDiacriticsRegExp(escapeRegExp(value))}`, "gi") : null; + + if (regexp && text.match(regexp) !== null) { + item.innerHTML = text.replace(regexp, "$&"); + matches++; + } else { + item.innerHTML = text; + } + } + + if (!value || matches > 0) { + element.style.display = ""; + } else { + element.style.display = "none"; + } + }); + }; + + searchInput.addEventListener("keyup", debounce(handleSearch, 100)); + searchInput.addEventListener("search", handleSearch); + + document.addEventListener("keydown", (event) => { + if (event.ctrlKey || event.metaKey) { + if (event.key === "f" && document.activeElement !== searchInput) { + searchInput.focus(); + event.preventDefault(); + } + } + }); + } + } + + private initModals() { + const renameFileItemModal = app.modals["renameFileItemModal"]; + + if (renameFileItemModal) { + $('[id="renameFileItemModal.filename"]', renameFileItemModal.element)?.addEventListener("keydown", (event) => { + if (event.key === "Enter") { + renameFileItemModal.triggerCommand("rename-file"); + event.preventDefault(); + } + }); + + renameFileItemModal.onOpen((modal, trigger) => { + if (trigger) { + const input = $('[id="renameFileItemModal.filename"]', modal.element) as HTMLInputElement; + input.value = (trigger.closest("[data-filename]") as HTMLElement)?.dataset.filename as string; + input.setSelectionRange(0, input.value.lastIndexOf(".")); + + Object.assign(modal.data, { + action: trigger.dataset.action, + item: trigger.closest(".files-item"), + input, + }); + } + }); + + renameFileItemModal.onCommand("rename-file", (modal) => { + const { action, item, filename, input } = modal.data; + + new Request( + { + method: "POST", + url: action as string, + data: { + filename, + "renameFileItemModal[filename]": (input as HTMLInputElement).value, + "csrf-token": app.config.csrfToken as string, + }, + }, + (response) => { + if (response.status === "success") { + (item as HTMLElement).dataset.filename = response.data.filename; + (item as HTMLElement).dataset.href = response.data.uri; + + ($(".file-name", item as HTMLElement) as HTMLElement).innerHTML = response.data.filename; + + ($("[data-command=infoFile]", item as HTMLElement) as HTMLAnchorElement).href = response.data.actions.info; + ($("[data-command=previewFile]", item as HTMLElement) as HTMLAnchorElement).href = response.data.uri; + ($("[data-command=renameFile]", item as HTMLElement) as HTMLElement).dataset.action = response.data.actions.rename; + ($("[data-command=replaceFile]", item as HTMLElement) as HTMLElement).dataset.action = response.data.actions.replace; + ($("[data-command=deleteFile]", item as HTMLElement) as HTMLElement).dataset.action = response.data.actions.delete; + + if (response.data.thumbnail) { + const thumbnail = $(".file-thumbnail", item as HTMLElement) as HTMLImageElement | HTMLVideoElement; + thumbnail.src = response.data.thumbnail; + } + + this.sort(".file-name"); + } + + const notification = new Notification(response.message, response.status); + notification.show(); + + modal.close(); + }, + ); + + modal.close(); + }); + } + + const deleteFileItemModal = app.modals["deleteFileItemModal"]; + + if (deleteFileItemModal) { + deleteFileItemModal.onOpen((modal, trigger) => { + if (trigger) { + Object.assign(modal.data, { + action: trigger.dataset.action, + item: trigger.closest(".files-item"), + }); + } + }); + + deleteFileItemModal.onCommand("delete-file", (modal) => { + const { action, item, filename } = modal.data; + + new Request( + { + method: "POST", + url: action as string, + data: { + filename, + "csrf-token": app.config.csrfToken as string, + }, + }, + (response) => { + if (response.status === "success") { + (item as HTMLElement).remove(); + + if (this.element.querySelectorAll(".files-item").length === 0) { + this.element.hidden = true; + } + + if (this.form) { + for (const name in this.form.inputs) { + const input = this.form.inputs[name]; + if (input instanceof SelectInput && !input.element.classList.contains("form-file") && !input.element.classList.contains("form-image")) { + input.removeOption(filename as string); + } + + if (input instanceof TagsInput && (input.element.classList.contains("form-files") || input.element.classList.contains("form-images"))) { + input.removeDropdownItem(filename as string); + } + } + } + } + + const notification = new Notification(response.message, response.status); + notification.show(); + + modal.close(); + }, + ); + + modal.close(); + }); + } + } +} diff --git a/panel/src/ts/components/inputs/upload-input.ts b/panel/src/ts/components/inputs/upload-input.ts index f6dc8fc00..770d9efe8 100644 --- a/panel/src/ts/components/inputs/upload-input.ts +++ b/panel/src/ts/components/inputs/upload-input.ts @@ -1,5 +1,6 @@ -import { $, $$ } from "../../utils/selectors"; +import { $ } from "../../utils/selectors"; import { app } from "../../app"; +import { FilesList } from "../fileslist"; import { Form } from "../form"; import { insertIcon } from "../icons"; import { Notification } from "../notification"; @@ -19,7 +20,7 @@ export class UploadInput { private readonly dropTargetLabel: HTMLElement; private readonly defaultDropLabel: string; - private readonly filesList: HTMLElement; + private readonly filesList: FilesList; constructor(element: HTMLInputElement, form: Form) { this.element = element; @@ -34,11 +35,11 @@ export class UploadInput { this.initInput(); - this.filesList = $(`.files-list[data-for="${this.element.id}"]`) as HTMLElement; + const filesList = $(`.files-list[data-for="${this.element.id}"]`) as HTMLElement; - if (this.filesList) { - this.initFileList(); - this.initModals(); + if (filesList) { + this.filesList = new FilesList(filesList, this.form); + this.initFilesList(); } else if (this.element.dataset.autoUpload === "true") { this.element.addEventListener("change", () => { if (!this.form.hasChanged(false)) { @@ -107,37 +108,7 @@ export class UploadInput { }); } - private initFileList() { - const toggle = $(".form-togglegroup.files-list-view-as", this.filesList); - - if (toggle) { - const fieldName = toggle.dataset.for; - const viewAs = window.localStorage.getItem(`formwork.filesListViewAs[${fieldName}]`); - - if (viewAs) { - $$("input", toggle).forEach((input: HTMLInputElement) => (input.checked = false)); - ($(`input[value=${viewAs}]`, this.filesList) as HTMLInputElement).checked = true; - this.filesList.classList.toggle("is-thumbnails", viewAs === "thumbnails"); - } - - $$("input", toggle).forEach((input: HTMLInputElement) => { - input.addEventListener("input", () => { - this.filesList.classList.toggle("is-thumbnails", input.value === "thumbnails"); - window.localStorage.setItem(`formwork.filesListViewAs[${fieldName}]`, input.value); - }); - }); - } - - document.addEventListener("click", (event) => { - const target = event.target as HTMLElement; - if (!target.closest(".dropdown") && target.closest(".files-item")) { - const item = target.closest(".files-item") as HTMLElement; - if (typeof item.dataset.href === "string") { - location.href = item.dataset.href; - } - } - }); - + private initFilesList() { if (this.element.dataset.autoUpload === "true") { this.element.addEventListener("change", () => { if (this.element.files?.length) { @@ -160,7 +131,8 @@ export class UploadInput { const data = response.data[0]; const template = $("template[id=files-item]") as HTMLTemplateElement; this.addFilesItem(data, template); - this.sortFilesList(this.filesList, ".file-name"); + this.filesList.sort(".file-name"); + this.filesList.element.hidden = false; for (const name in this.form.inputs) { const input = this.form.inputs[name]; @@ -196,173 +168,6 @@ export class UploadInput { } }); } - - this.filesList.addEventListener("click", (event) => { - const element = (event.target as HTMLElement).closest("[data-command=replaceFile]") as HTMLElement; - if (element) { - const fileInput = document.createElement("input"); - fileInput.type = "file"; - fileInput.accept = element.dataset.mimetype as string; - fileInput.click(); - - fileInput.addEventListener("change", () => { - if (fileInput.files?.length) { - const formData = new FormData(); - formData.append("filename", (element.closest("[data-filename]") as HTMLElement).dataset.filename as string); - formData.append("csrf-token", app.config.csrfToken as string); - formData.append("file", fileInput.files[0]); - - new Request( - { - method: "POST", - url: element.dataset.action as string, - data: formData, - }, - (response) => { - const notification = new Notification(response.message, response.status); - - if (response.status === "success") { - if (element.closest("[data-form=page-file-form]")) { - window.location.reload(); - } else if (response.data.thumbnail) { - const thumbnail = $(".file-thumbnail", element.closest(".files-item") as HTMLElement) as HTMLImageElement; - const fileSize = $(".file-size", element.closest(".files-item") as HTMLElement) as HTMLImageElement; - - if (response.data.type === "image") { - thumbnail.style.backgroundImage = `url(${response.data.thumbnail})`; - } else if (response.data.type === "video") { - thumbnail.src = response.data.thumbnail; - } - - fileSize.textContent = `(${response.data.size})`; - } - } - - notification.show(); - }, - ); - } - - fileInput.remove(); - }); - } - }); - } - - private initModals() { - const renameFileItemModal = app.modals["renameFileItemModal"]; - - if (renameFileItemModal) { - $('[id="renameFileItemModal.filename"]', renameFileItemModal.element)?.addEventListener("keydown", (event) => { - if (event.key === "Enter") { - renameFileItemModal.triggerCommand("rename-file"); - event.preventDefault(); - } - }); - - renameFileItemModal.onOpen((modal, trigger) => { - if (trigger) { - const input = $('[id="renameFileItemModal.filename"]', modal.element) as HTMLInputElement; - input.value = (trigger.closest("[data-filename]") as HTMLElement)?.dataset.filename as string; - input.setSelectionRange(0, input.value.lastIndexOf(".")); - - Object.assign(modal.data, { - action: trigger.dataset.action, - item: trigger.closest(".files-item"), - filename: (trigger.closest("[data-filename]") as HTMLElement)?.dataset.filename, - input, - }); - } - }); - - renameFileItemModal.onCommand("rename-file", (modal) => { - const { action, item, filename, input } = modal.data; - - new Request( - { - method: "POST", - url: action as string, - data: { - filename, - "renameFileItemModal[filename]": (input as HTMLInputElement).value, - "csrf-token": app.config.csrfToken as string, - }, - }, - (response) => { - if (response.status === "success") { - (item as HTMLElement).dataset.filename = response.data.filename; - - ($(".file-name", item as HTMLElement) as HTMLElement).innerHTML = response.data.filename; - - const template = $("template[id=files-item]") as HTMLTemplateElement; - const uri = ($(".files-item", template.content) as HTMLElement).dataset.href as string; - (item as HTMLElement).dataset.href = `${uri}${response.data.filename}`; - - this.sortFilesList(this.filesList, ".file-name"); - } - - const notification = new Notification(response.message, response.status); - notification.show(); - - modal.close(); - }, - ); - - modal.close(); - }); - } - - const deleteFileItemModal = app.modals["deleteFileItemModal"]; - - if (deleteFileItemModal) { - deleteFileItemModal.onOpen((modal, trigger) => { - if (trigger) { - Object.assign(modal.data, { - action: trigger.dataset.action, - item: trigger.closest(".files-item"), - filename: (trigger.closest("[data-filename]") as HTMLElement)?.dataset.filename, - }); - } - }); - - deleteFileItemModal.onCommand("delete-file", (modal) => { - const { action, item, filename } = modal.data; - - new Request( - { - method: "POST", - url: action as string, - data: { - filename, - "csrf-token": app.config.csrfToken as string, - }, - }, - (response) => { - if (response.status === "success") { - (item as HTMLElement).remove(); - - for (const name in this.form.inputs) { - const input = this.form.inputs[name]; - if (input instanceof SelectInput && !input.element.classList.contains("form-file") && !input.element.classList.contains("form-image")) { - input.removeOption(filename as string); - } - - if (input instanceof TagsInput && (input.element.classList.contains("form-files") || input.element.classList.contains("form-images"))) { - input.removeDropdownItem(filename as string); - } - } - } - - const notification = new Notification(response.message, response.status); - notification.show(); - - modal.close(); - }, - ); - - modal.close(); - }); - } } private formatFileSize(size: number) { @@ -389,28 +194,19 @@ export class UploadInput { } } - private sortFilesList(filesList: HTMLElement, selector: string = ".file-name") { - const filesItems = $$(".files-item", filesList); - Array.from(filesItems) - .sort((a: HTMLElement, b: HTMLElement) => { - const keyA = $(selector, a)?.textContent; - const keyB = $(selector, b)?.textContent; - return keyA?.localeCompare(keyB ?? "") ?? 0; - }) - .forEach((element: HTMLElement) => { - element.parentElement?.appendChild(element); - }); - } - - private addFilesItem(info: { [key: string]: string }, template: HTMLTemplateElement) { + private addFilesItem(info: { [key: string]: any }, template: HTMLTemplateElement) { const node = template.content.cloneNode(true) as HTMLElement; const filesItem = $(".files-item", node) as HTMLElement; filesItem.dataset.filename = info.name; - filesItem.dataset.href += `${info.name}/`; + filesItem.dataset.href = info.actions.info; if (info.type === "image") { - ($(".file-thumbnail", filesItem) as HTMLElement).style.backgroundImage = `url(${info.thumbnail})`; + const img = document.createElement("img"); + img.className = "file-thumbnail"; + img.src = info.thumbnail; + img.loading = "lazy"; + $(".file-thumbnail", filesItem)?.replaceWith(img); } else if (info.type === "video") { const video = document.createElement("video"); video.classList.add("file-thumbnail"); @@ -424,18 +220,30 @@ export class UploadInput { insertIcon(info.type ? `file-${info.type}` : "file", $(".file-icon", filesItem) as HTMLElement); ($(".file-name", filesItem) as HTMLElement).textContent = info.name; - ($(".file-size", filesItem) as HTMLElement).textContent = `(${info.size})`; + ($(".file-date", filesItem) as HTMLElement).textContent = info.lastModifiedTime; + ($(".file-size", filesItem) as HTMLElement).textContent = info.size; ($(".dropdown-button", filesItem) as HTMLElement).dataset.dropdown = `dropdown-${info.hash}`; ($(".dropdown-menu", filesItem) as HTMLElement).id = `dropdown-${info.hash}`; - ($("[data-command=infoFile", filesItem) as HTMLAnchorElement).href += `${info.name}/`; + const infoFileCommand = $("[data-command=infoFile]", filesItem) as HTMLAnchorElement; + const previewFileCommand = $("[data-command=previewFile]", filesItem) as HTMLAnchorElement; + const renameFileCommand = $("[data-command=renameFile]", filesItem) as HTMLElement; + const replaceFileCommand = $("[data-command=replaceFile]", filesItem) as HTMLElement; + const deleteFileCommand = $("[data-command=deleteFile]", filesItem) as HTMLElement; + + infoFileCommand.href = info.actions.info; + + previewFileCommand.href = info.uri; + previewFileCommand.target = `formwork-preview-file-${info.hash}`; + + renameFileCommand.dataset.action = info.actions.rename; - ($("[data-command=previewFile"), filesItem as HTMLAnchorElement).href = info.uri; - ($("[data-command=previewFile"), filesItem as HTMLAnchorElement).target = `formwork-preview-file-${info.hash}`; + replaceFileCommand.dataset.action = info.actions.replace; + replaceFileCommand.dataset.mimetype = `${info.mimeType}`; - ($("[data-command=replaceFile", filesItem) as HTMLElement).dataset.extension = `${info.mimeType}`; + deleteFileCommand.dataset.action = info.actions.delete; - $(".files-items", this.filesList)?.appendChild(node); + $(".files-items", this.filesList.element)?.appendChild(node); } } diff --git a/panel/translations/de.yaml b/panel/translations/de.yaml index c874f1d9d..1bd3aec28 100644 --- a/panel/translations/de.yaml +++ b/panel/translations/de.yaml @@ -54,6 +54,7 @@ panel.errors.error.notFound.description: Die Seite existiert nicht oder die Anfr panel.errors.error.notFound.heading: Oops, Seite nicht gefunden! panel.errors.error.notFound.status: Nicht gefunden panel.files.actions: Aktionen +panel.files.backToPage: Zurück zur Seite panel.files.cannotDelete.fileNotFound: Datei kann nicht gelöscht werden, Datei nicht gefunden panel.files.cannotDelete.pageNotFound: Datei kann nicht gelöscht werden, Datei nicht gefunden panel.files.cannotRename.fileAlreadyExists: Datei kann nicht umbenannt werden, eine Datei mit demselben Namen existiert bereits @@ -64,10 +65,47 @@ panel.files.cannotReplace.multipleFiles: Datei kann nicht ersetzt werden, mehrer panel.files.cannotReplace.pageNotFound: Datei kann nicht ersetzt werden, Seite nicht gefunden panel.files.cannotUpload.pageNotFound: Datei kann nicht hochgeladen werden, Seite nicht gefunden panel.files.deleted: Datei gelöscht +panel.files.exif: EXIF panel.files.fileNotFound: Dateiinfo kann nicht abgerufen werden, Datei nicht gefunden +panel.files.files: Dateien +panel.files.info: Info +panel.files.info.image.colorDepth: Farbtiefe +panel.files.info.image.colorNumber: Anzahl der Farben +panel.files.info.image.colorProfile: Farbprofil +panel.files.info.image.colorSpace: Farbraum +panel.files.info.image.dimensions: Abmessungen +panel.files.info.image.dimensions.widthByHeightPixels: '%d × %d Pixel' +panel.files.info.image.exif.aperture: Blende +panel.files.info.image.exif.camera: Kamera +panel.files.info.image.exif.creationDateAndTime: Erstellungsdatum und -zeit +panel.files.info.image.exif.exposureCompensation: Belichtungskorrektur +panel.files.info.image.exif.exposureProgram: Belichtungsprogramm +panel.files.info.image.exif.exposureTime: Belichtungszeit +panel.files.info.image.exif.flash: Blitz +panel.files.info.image.exif.focalLength: Brennweite +panel.files.info.image.exif.lensModel: Objektivmodell +panel.files.info.image.exif.meteringMode: Belichtungsmessmethode +panel.files.info.image.exif.sensitivity: Empfindlichkeit +panel.files.info.image.exif.whiteBalance: Weißabgleich +panel.files.info.image.frames: '%d Frames' +panel.files.info.image.framesCount: Anzahl der Frames +panel.files.info.image.repeats: '%d Wiederholungen' +panel.files.info.image.repeatsCount: Anzahl der Wiederholungen +panel.files.info.image.resolution: Auflösung +panel.files.info.lastModifiedTime: Letzte Änderungszeit +panel.files.info.mimeType: MIME-Typ +panel.files.info.name: Name +panel.files.info.size: Größe +panel.files.info.uri: URI panel.files.metadata.updated: Dateimetadaten aktualisiert panel.files.pageNotFound: Dateiinfo kann nicht abgerufen werden, Seite nicht gefunden +panel.files.parent: Übergeordnetes Element +panel.files.position: Position +panel.files.preview: Vorschau panel.files.renamed: Datei umbenannt +panel.files.search: Dateien suchen... +panel.files.upload: Dateien hochladen +panel.files.uploaded: Dateien hochgeladen panel.files.viewAsList: Als Liste anzeigen panel.files.viewAsThumbnails: Als Miniaturansichten anzeigen panel.login.attempt.failed: Anmeldeversuch fehlgeschlagen! Versuchen Sie es erneut. @@ -84,6 +122,7 @@ panel.modal.action.continue: Fortfahren panel.modal.action.delete: Löschen panel.modal.action.rename: Umbenennen panel.modal.action.save: Speichern +panel.modal.action.upload: Hochladen panel.modal.action.uploadFile: Datei hochladen panel.modal.images.noImages: Hier sind keine Bilder vorhanden panel.modal.images.noImages.upload: Bitte laden Sie einige Bilder hoch @@ -161,38 +200,6 @@ panel.pages.deleteFile.prompt: Möchten Sie diese Datei wirklich löschen? Diese panel.pages.deletePage: Seite löschen panel.pages.deletePage.prompt: Möchten Sie diese Seite wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden. panel.pages.editPage: Seite bearbeiten %s -panel.pages.file.backToPage: Zurück zur Seite -panel.pages.file.exif: EXIF -panel.pages.file.info: Info -panel.pages.file.info.image.colorDepth: Farbtiefe -panel.pages.file.info.image.colorNumber: Anzahl der Farben -panel.pages.file.info.image.colorProfile: Farbprofil -panel.pages.file.info.image.colorSpace: Farbraum -panel.pages.file.info.image.dimensions: Abmessungen -panel.pages.file.info.image.dimensions.widthByHeightPixels: '%d × %d Pixel' -panel.pages.file.info.image.exif.aperture: Blende -panel.pages.file.info.image.exif.camera: Kamera -panel.pages.file.info.image.exif.creationDateAndTime: Erstellungsdatum und -zeit -panel.pages.file.info.image.exif.exposureCompensation: Belichtungskorrektur -panel.pages.file.info.image.exif.exposureProgram: Belichtungsprogramm -panel.pages.file.info.image.exif.exposureTime: Belichtungszeit -panel.pages.file.info.image.exif.flash: Blitz -panel.pages.file.info.image.exif.focalLength: Brennweite -panel.pages.file.info.image.exif.lensModel: Objektivmodell -panel.pages.file.info.image.exif.meteringMode: Belichtungsmessmethode -panel.pages.file.info.image.exif.sensitivity: Empfindlichkeit -panel.pages.file.info.image.exif.whiteBalance: Weißabgleich -panel.pages.file.info.image.frames: '%d Frames' -panel.pages.file.info.image.framesCount: Anzahl der Frames -panel.pages.file.info.image.repeats: '%d Wiederholungen' -panel.pages.file.info.image.repeatsCount: Anzahl der Wiederholungen -panel.pages.file.info.image.resolution: Auflösung -panel.pages.file.info.lastModifiedTime: Letzte Änderungszeit -panel.pages.file.info.mimeType: MIME-Typ -panel.pages.file.info.size: Größe -panel.pages.file.info.uri: URI -panel.pages.file.position: Position -panel.pages.file.preview: Vorschau panel.pages.history.event.created: Seite erstellt von %s %s. panel.pages.history.event.edited: Seite bearbeitet von %s %s. panel.pages.languages: Sprachen diff --git a/panel/translations/en.yaml b/panel/translations/en.yaml index e5a3f6eab..4fe87bc92 100644 --- a/panel/translations/en.yaml +++ b/panel/translations/en.yaml @@ -54,6 +54,7 @@ panel.errors.error.notFound.description: The page does not exist or the request panel.errors.error.notFound.heading: Oops, page not found! panel.errors.error.notFound.status: Not found panel.files.actions: Actions +panel.files.backToPage: Back to page panel.files.cannotDelete.fileNotFound: Cannot delete file, file not found panel.files.cannotDelete.pageNotFound: Cannot delete file, file not found panel.files.cannotRename.fileAlreadyExists: Cannot rename file, a file with the same name already exists @@ -64,10 +65,47 @@ panel.files.cannotReplace.multipleFiles: Cannot replace file, multiple file give panel.files.cannotReplace.pageNotFound: Cannot replace file, page not found panel.files.cannotUpload.pageNotFound: Cannot upload file, page not found panel.files.deleted: File deleted +panel.files.exif: EXIF panel.files.fileNotFound: Cannot get file info, file not found +panel.files.files: Files +panel.files.info: Info +panel.files.info.image.colorDepth: Color depth +panel.files.info.image.colorNumber: Color number +panel.files.info.image.colorProfile: Color profile +panel.files.info.image.colorSpace: Color space +panel.files.info.image.dimensions: Dimensions +panel.files.info.image.dimensions.widthByHeightPixels: '%d × %d pixels' +panel.files.info.image.exif.aperture: Aperture +panel.files.info.image.exif.camera: Camera +panel.files.info.image.exif.creationDateAndTime: Creation date and time +panel.files.info.image.exif.exposureCompensation: Exposure compensation +panel.files.info.image.exif.exposureProgram: Exposure program +panel.files.info.image.exif.exposureTime: Exposure time +panel.files.info.image.exif.flash: Flash +panel.files.info.image.exif.focalLength: Focal length +panel.files.info.image.exif.lensModel: Lens model +panel.files.info.image.exif.meteringMode: Metering mode +panel.files.info.image.exif.sensitivity: Sensitivity +panel.files.info.image.exif.whiteBalance: White balance +panel.files.info.image.frames: '%d frames' +panel.files.info.image.framesCount: Frames count +panel.files.info.image.repeats: '%d repeats' +panel.files.info.image.repeatsCount: Repeats count +panel.files.info.image.resolution: Resolution +panel.files.info.lastModifiedTime: Last modified time +panel.files.info.mimeType: MIME type +panel.files.info.name: Name +panel.files.info.size: Size +panel.files.info.uri: URI panel.files.metadata.updated: File metadata updated panel.files.pageNotFound: Cannot get file info, page not found +panel.files.parent: Parent +panel.files.position: Position +panel.files.preview: Preview panel.files.renamed: File renamed +panel.files.search: Search files... +panel.files.upload: Upload files +panel.files.uploaded: Files uploaded panel.files.viewAsList: View as list panel.files.viewAsThumbnails: View as thumbnails panel.login.attempt.failed: Login attempt failed! Try again. @@ -84,6 +122,7 @@ panel.modal.action.continue: Continue panel.modal.action.delete: Delete panel.modal.action.rename: Rename panel.modal.action.save: Save +panel.modal.action.upload: Upload panel.modal.action.uploadFile: Upload a file panel.modal.images.noImages: There are no images here panel.modal.images.noImages.upload: Please upload some images @@ -161,38 +200,6 @@ panel.pages.deleteFile.prompt: Are you sure you want to delete this file? This a panel.pages.deletePage: Delete page panel.pages.deletePage.prompt: Are you sure you want to delete this page? This action can’t be undone. panel.pages.editPage: Edit page %s -panel.pages.file.backToPage: Back to page -panel.pages.file.exif: EXIF -panel.pages.file.info: Info -panel.pages.file.info.image.colorDepth: Color depth -panel.pages.file.info.image.colorNumber: Color number -panel.pages.file.info.image.colorProfile: Color profile -panel.pages.file.info.image.colorSpace: Color space -panel.pages.file.info.image.dimensions: Dimensions -panel.pages.file.info.image.dimensions.widthByHeightPixels: '%d × %d pixels' -panel.pages.file.info.image.exif.aperture: Aperture -panel.pages.file.info.image.exif.camera: Camera -panel.pages.file.info.image.exif.creationDateAndTime: Creation date and time -panel.pages.file.info.image.exif.exposureCompensation: Exposure compensation -panel.pages.file.info.image.exif.exposureProgram: Exposure program -panel.pages.file.info.image.exif.exposureTime: Exposure time -panel.pages.file.info.image.exif.flash: Flash -panel.pages.file.info.image.exif.focalLength: Focal length -panel.pages.file.info.image.exif.lensModel: Lens model -panel.pages.file.info.image.exif.meteringMode: Metering mode -panel.pages.file.info.image.exif.sensitivity: Sensitivity -panel.pages.file.info.image.exif.whiteBalance: White balance -panel.pages.file.info.image.frames: '%d frames' -panel.pages.file.info.image.framesCount: Frames count -panel.pages.file.info.image.repeats: '%d repeats' -panel.pages.file.info.image.repeatsCount: Repeats count -panel.pages.file.info.image.resolution: Resolution -panel.pages.file.info.lastModifiedTime: Last modified time -panel.pages.file.info.mimeType: MIME type -panel.pages.file.info.size: Size -panel.pages.file.info.uri: URI -panel.pages.file.position: Position -panel.pages.file.preview: Preview panel.pages.history.event.created: Page created by %s %s. panel.pages.history.event.edited: Page edited by %s %s. panel.pages.languages: Languages diff --git a/panel/translations/es.yaml b/panel/translations/es.yaml index f81669c1b..03a3cb28e 100644 --- a/panel/translations/es.yaml +++ b/panel/translations/es.yaml @@ -54,6 +54,7 @@ panel.errors.error.notFound.description: La página no existe o la solicitud no panel.errors.error.notFound.heading: ¡Ups, página no encontrada! panel.errors.error.notFound.status: No encontrado panel.files.actions: Acciones +panel.files.backToPage: Volver a la página panel.files.cannotDelete.fileNotFound: No se puede borrar el archivo, archivo no encontrado panel.files.cannotDelete.pageNotFound: No se puede borrar el archivo, página no encontrada panel.files.cannotRename.fileAlreadyExists: No se puede renombrar el archivo, ya existe un archivo con el mismo nombre @@ -64,10 +65,47 @@ panel.files.cannotReplace.multipleFiles: No se puede reemplazar el archivo, se p panel.files.cannotReplace.pageNotFound: No se puede reemplazar el archivo, página no encontrada panel.files.cannotUpload.pageNotFound: No se puede subir el archivo, página no encontrada panel.files.deleted: Archivo eliminado +panel.files.exif: EXIF panel.files.fileNotFound: No se pueden obtener las información del archivo, archivo no encontrado +panel.files.files: Archivos +panel.files.info: Información +panel.files.info.image.colorDepth: Profundidad de color +panel.files.info.image.colorNumber: Número de colores +panel.files.info.image.colorProfile: Perfil de color +panel.files.info.image.colorSpace: Espacio de color +panel.files.info.image.dimensions: Dimensiones +panel.files.info.image.dimensions.widthByHeightPixels: '%d × %d píxeles' +panel.files.info.image.exif.aperture: Apertura +panel.files.info.image.exif.camera: Cámara +panel.files.info.image.exif.creationDateAndTime: Fecha y hora de creación +panel.files.info.image.exif.exposureCompensation: Compensación de exposición +panel.files.info.image.exif.exposureProgram: Programa de exposición +panel.files.info.image.exif.exposureTime: Tiempo de exposición +panel.files.info.image.exif.flash: Flash +panel.files.info.image.exif.focalLength: Longitud focal +panel.files.info.image.exif.lensModel: Modelo de lente +panel.files.info.image.exif.meteringMode: Modo de medición +panel.files.info.image.exif.sensitivity: Sensibilidad +panel.files.info.image.exif.whiteBalance: Balance de blancos +panel.files.info.image.frames: '%d frames' +panel.files.info.image.framesCount: Cantidad de frames +panel.files.info.image.repeats: '%d repeticiones' +panel.files.info.image.repeatsCount: Cantidad de repeticiones +panel.files.info.image.resolution: Resolución +panel.files.info.lastModifiedTime: Última modificación +panel.files.info.mimeType: Tipo MIME +panel.files.info.name: Nombre +panel.files.info.size: Tamaño +panel.files.info.uri: URI panel.files.metadata.updated: Metadatos del archivo actualizados panel.files.pageNotFound: No se pueden obtener las información del archivo, página no encontrada +panel.files.parent: Elemento superior +panel.files.position: Posición +panel.files.preview: Vista previa panel.files.renamed: Archivo renombrado +panel.files.search: Buscar archivos... +panel.files.upload: Subir archivos +panel.files.uploaded: Archivos subidos panel.files.viewAsList: Ver como lista panel.files.viewAsThumbnails: Ver como miniaturas panel.login.attempt.failed: ¡Intento de inicio de sesión fallido! Intenta de nuevo. @@ -84,6 +122,7 @@ panel.modal.action.continue: Continuar panel.modal.action.delete: Borrar panel.modal.action.rename: Renombrar panel.modal.action.save: Guardar +panel.modal.action.upload: Subir panel.modal.action.uploadFile: Subir un archivo panel.modal.images.noImages: No hay imágenes aquí panel.modal.images.noImages.upload: Por favor, sube algunas imágenes @@ -161,38 +200,6 @@ panel.pages.deleteFile.prompt: ¿Estás seguro de que quieres borrar este archiv panel.pages.deletePage: Borrar página panel.pages.deletePage.prompt: ¿Estás seguro de que quieres borrar esta página? Esta acción no se puede deshacer. panel.pages.editPage: Editar página %s -panel.pages.file.backToPage: Volver a la página -panel.pages.file.exif: EXIF -panel.pages.file.info: Información -panel.pages.file.info.image.colorDepth: Profundidad de color -panel.pages.file.info.image.colorNumber: Número de colores -panel.pages.file.info.image.colorProfile: Perfil de color -panel.pages.file.info.image.colorSpace: Espacio de color -panel.pages.file.info.image.dimensions: Dimensiones -panel.pages.file.info.image.dimensions.widthByHeightPixels: '%d × %d píxeles' -panel.pages.file.info.image.exif.aperture: Apertura -panel.pages.file.info.image.exif.camera: Cámara -panel.pages.file.info.image.exif.creationDateAndTime: Fecha y hora de creación -panel.pages.file.info.image.exif.exposureCompensation: Compensación de exposición -panel.pages.file.info.image.exif.exposureProgram: Programa de exposición -panel.pages.file.info.image.exif.exposureTime: Tiempo de exposición -panel.pages.file.info.image.exif.flash: Flash -panel.pages.file.info.image.exif.focalLength: Longitud focal -panel.pages.file.info.image.exif.lensModel: Modelo de lente -panel.pages.file.info.image.exif.meteringMode: Modo de medición -panel.pages.file.info.image.exif.sensitivity: Sensibilidad -panel.pages.file.info.image.exif.whiteBalance: Balance de blancos -panel.pages.file.info.image.frames: '%d frames' -panel.pages.file.info.image.framesCount: Cantidad de frames -panel.pages.file.info.image.repeats: '%d repeticiones' -panel.pages.file.info.image.repeatsCount: Cantidad de repeticiones -panel.pages.file.info.image.resolution: Resolución -panel.pages.file.info.lastModifiedTime: Última modificación -panel.pages.file.info.mimeType: Tipo MIME -panel.pages.file.info.size: Tamaño -panel.pages.file.info.uri: URI -panel.pages.file.position: Posición -panel.pages.file.preview: Vista previa panel.pages.history.event.created: Página creada por %s %s. panel.pages.history.event.edited: Página editada por %s %s. panel.pages.languages: Idiomas diff --git a/panel/translations/fr.yaml b/panel/translations/fr.yaml index b4919901d..d959558de 100644 --- a/panel/translations/fr.yaml +++ b/panel/translations/fr.yaml @@ -54,6 +54,7 @@ panel.errors.error.notFound.description: La page n’existe pas ou la demande n panel.errors.error.notFound.heading: Oups, page non trouvée! panel.errors.error.notFound.status: Pas trouvé panel.files.actions: Actions +panel.files.backToPage: Retour à la page panel.files.cannotDelete.fileNotFound: Impossible de supprimer le fichier, fichier introuvable panel.files.cannotDelete.pageNotFound: Impossible de supprimer le fichier, page non trouvée panel.files.cannotRename.fileAlreadyExists: Impossible de renommer le fichier, un fichier portant le même nom existe déjà @@ -64,10 +65,47 @@ panel.files.cannotReplace.multipleFiles: Impossible de remplacer le fichier, plu panel.files.cannotReplace.pageNotFound: Impossible de remplacer le fichier, page non trouvée panel.files.cannotUpload.pageNotFound: Impossible de téléverser le fichier, page introuvable panel.files.deleted: Fichier supprimé +panel.files.exif: EXIF panel.files.fileNotFound: Impossible de récupérer les informations sur le fichier, fichier non trouvé +panel.files.files: Fichiers +panel.files.info: Info +panel.files.info.image.colorDepth: Profondeur de couleur +panel.files.info.image.colorNumber: Nombre de couleurs +panel.files.info.image.colorProfile: Profil de couleur +panel.files.info.image.colorSpace: Espace de couleur +panel.files.info.image.dimensions: Dimensions +panel.files.info.image.dimensions.widthByHeightPixels: '%d × %d pixels' +panel.files.info.image.exif.aperture: Ouverture +panel.files.info.image.exif.camera: Appareil photo +panel.files.info.image.exif.creationDateAndTime: Date et heure de création +panel.files.info.image.exif.exposureCompensation: Compensation d'exposition +panel.files.info.image.exif.exposureProgram: Programme d'exposition +panel.files.info.image.exif.exposureTime: Temps d'exposition +panel.files.info.image.exif.flash: Flash +panel.files.info.image.exif.focalLength: Longueur focale +panel.files.info.image.exif.lensModel: Modèle d'objectif +panel.files.info.image.exif.meteringMode: Mode de mesure +panel.files.info.image.exif.sensitivity: Sensibilité +panel.files.info.image.exif.whiteBalance: Balance des blancs +panel.files.info.image.frames: '%d trames' +panel.files.info.image.framesCount: Nombre de trames +panel.files.info.image.repeats: '%d répétitions' +panel.files.info.image.repeatsCount: Nombre de répétitions +panel.files.info.image.resolution: Résolution +panel.files.info.lastModifiedTime: Dernière modification +panel.files.info.mimeType: Type MIME +panel.files.info.name: Nom +panel.files.info.size: Taille +panel.files.info.uri: URI panel.files.metadata.updated: Métadonnées du fichier mises à jour panel.files.pageNotFound: Impossible de récupérer les informations sur le fichier, page non trouvée +panel.files.parent: Parent +panel.files.position: Position +panel.files.preview: Aperçu panel.files.renamed: Fichier renommé +panel.files.search: Rechercher des fichiers... +panel.files.upload: Téléverser des fichiers +panel.files.uploaded: Fichiers téléversés panel.files.viewAsList: Afficher en liste panel.files.viewAsThumbnails: Afficher en vignettes panel.login.attempt.failed: La tentative de connexion a échoué! Réessayer. @@ -84,6 +122,7 @@ panel.modal.action.continue: Continuer panel.modal.action.delete: Supprimer panel.modal.action.rename: Renommer panel.modal.action.save: Enregistrer +panel.modal.action.upload: Téléverser panel.modal.action.uploadFile: Parcourir… panel.modal.images.noImages: Aucun fichier média n’a été trouvé. panel.modal.images.noImages.upload: Sélectionnez vos fichiers à téléverser. @@ -161,38 +200,6 @@ panel.pages.deleteFile.prompt: ÊtesVous sûr de vouloir supprimer ce fichier ? panel.pages.deletePage: Supprimer la page panel.pages.deletePage.prompt: ÊtesVous sûr de vouloir supprimer cette page ? Cette action est irréversible. panel.pages.editPage: Modifier la page %s -panel.pages.file.backToPage: Retour à la page -panel.pages.file.exif: EXIF -panel.pages.file.info: Info -panel.pages.file.info.image.colorDepth: Profondeur de couleur -panel.pages.file.info.image.colorNumber: Nombre de couleurs -panel.pages.file.info.image.colorProfile: Profil de couleur -panel.pages.file.info.image.colorSpace: Espace de couleur -panel.pages.file.info.image.dimensions: Dimensions -panel.pages.file.info.image.dimensions.widthByHeightPixels: '%d × %d pixels' -panel.pages.file.info.image.exif.aperture: Ouverture -panel.pages.file.info.image.exif.camera: Appareil photo -panel.pages.file.info.image.exif.creationDateAndTime: Date et heure de création -panel.pages.file.info.image.exif.exposureCompensation: Compensation d'exposition -panel.pages.file.info.image.exif.exposureProgram: Programme d'exposition -panel.pages.file.info.image.exif.exposureTime: Temps d'exposition -panel.pages.file.info.image.exif.flash: Flash -panel.pages.file.info.image.exif.focalLength: Longueur focale -panel.pages.file.info.image.exif.lensModel: Modèle d'objectif -panel.pages.file.info.image.exif.meteringMode: Mode de mesure -panel.pages.file.info.image.exif.sensitivity: Sensibilité -panel.pages.file.info.image.exif.whiteBalance: Balance des blancs -panel.pages.file.info.image.frames: '%d trames' -panel.pages.file.info.image.framesCount: Nombre de trames -panel.pages.file.info.image.repeats: '%d répétitions' -panel.pages.file.info.image.repeatsCount: Nombre de répétitions -panel.pages.file.info.image.resolution: Résolution -panel.pages.file.info.lastModifiedTime: Dernière modification -panel.pages.file.info.mimeType: Type MIME -panel.pages.file.info.size: Taille -panel.pages.file.info.uri: URI -panel.pages.file.position: Position -panel.pages.file.preview: Aperçu panel.pages.history.event.created: Page créée par %s %s. panel.pages.history.event.edited: Page modifiée par %s %s. panel.pages.languages: Langues diff --git a/panel/translations/it.yaml b/panel/translations/it.yaml index 3043d6878..c0d305348 100644 --- a/panel/translations/it.yaml +++ b/panel/translations/it.yaml @@ -54,6 +54,7 @@ panel.errors.error.notFound.description: La pagina non esiste o la richiesta non panel.errors.error.notFound.heading: Oops, pagina non trovata! panel.errors.error.notFound.status: Non trovato panel.files.actions: Azioni +panel.files.backToPage: Torna alla pagina panel.files.cannotDelete.fileNotFound: Impossibile eliminare il file, file non trovato panel.files.cannotDelete.pageNotFound: Impossibile eliminare il file, pagina non trovata panel.files.cannotRename.fileAlreadyExists: Impossibile rinominare il file, un file con lo stesso nome esiste già @@ -64,10 +65,47 @@ panel.files.cannotReplace.multipleFiles: Impossibile sostituire il file, forniti panel.files.cannotReplace.pageNotFound: Impossibile sostituire il file, pagina non trovata panel.files.cannotUpload.pageNotFound: Impossibile caricare il file, pagina non trovata panel.files.deleted: File eliminato +panel.files.exif: EXIF panel.files.fileNotFound: Impossibile ottenere le informazioni sul file, file non trovato +panel.files.files: File +panel.files.info: Informazioni +panel.files.info.image.colorDepth: Profondità colore +panel.files.info.image.colorNumber: Numero di colori +panel.files.info.image.colorProfile: Profilo colore +panel.files.info.image.colorSpace: Spazio colore +panel.files.info.image.dimensions: Misure +panel.files.info.image.dimensions.widthByHeightPixels: '%d × %d pixel' +panel.files.info.image.exif.aperture: Apertura +panel.files.info.image.exif.camera: Fotocamera +panel.files.info.image.exif.creationDateAndTime: Data e ora di creazione +panel.files.info.image.exif.exposureCompensation: Compensazione esposizione +panel.files.info.image.exif.exposureProgram: Programma di esposizione +panel.files.info.image.exif.exposureTime: Tempo di esposizione +panel.files.info.image.exif.flash: Flash +panel.files.info.image.exif.focalLength: Lunghezza focale +panel.files.info.image.exif.lensModel: Obiettivo +panel.files.info.image.exif.meteringMode: Modalità di misurazione +panel.files.info.image.exif.sensitivity: Sensibilità +panel.files.info.image.exif.whiteBalance: Bilanciamento del bianco +panel.files.info.image.frames: '%d fotogrammi' +panel.files.info.image.framesCount: Numero di fotogrammi +panel.files.info.image.repeats: '%d ripetizioni' +panel.files.info.image.repeatsCount: Numero di ripetizioni +panel.files.info.image.resolution: Risoluzione +panel.files.info.lastModifiedTime: Ultime modifiche +panel.files.info.mimeType: Tipo MIME +panel.files.info.name: Nome +panel.files.info.size: Dimensioni file +panel.files.info.uri: URI panel.files.metadata.updated: Metadati del file aggiornati panel.files.pageNotFound: Impossibile ottenere le informazioni sul file, pagina non trovata +panel.files.parent: Elemento superiore +panel.files.position: Posizione +panel.files.preview: Anteprima panel.files.renamed: File rinominato +panel.files.search: Cerca file... +panel.files.upload: Carica file +panel.files.uploaded: File caricati panel.files.viewAsList: Visualizza come lista panel.files.viewAsThumbnails: Visualizza come miniature panel.login.attempt.failed: Tentativo di accesso fallito! Riprova. @@ -84,6 +122,7 @@ panel.modal.action.continue: Continua panel.modal.action.delete: Elimina panel.modal.action.rename: Rinomina panel.modal.action.save: Salva +panel.modal.action.upload: Carica panel.modal.action.uploadFile: Carica file panel.modal.images.noImages: Qui non ci sono immagini panel.modal.images.noImages.upload: Carica qualche immagine @@ -161,38 +200,6 @@ panel.pages.deleteFile.prompt: Si è sicuri di eliminare il file? Questa azione panel.pages.deletePage: Elimina pagina panel.pages.deletePage.prompt: Si è sicuri di eliminare la pagina? Questa azione non può essere annullata. panel.pages.editPage: Modifica pagina %s -panel.pages.file.backToPage: Torna alla pagina -panel.pages.file.exif: EXIF -panel.pages.file.info: Informazioni -panel.pages.file.info.image.colorDepth: Profondità colore -panel.pages.file.info.image.colorNumber: Numero di colori -panel.pages.file.info.image.colorProfile: Profilo colore -panel.pages.file.info.image.colorSpace: Spazio colore -panel.pages.file.info.image.dimensions: Misure -panel.pages.file.info.image.dimensions.widthByHeightPixels: '%d × %d pixel' -panel.pages.file.info.image.exif.aperture: Apertura -panel.pages.file.info.image.exif.camera: Fotocamera -panel.pages.file.info.image.exif.creationDateAndTime: Data e ora di creazione -panel.pages.file.info.image.exif.exposureCompensation: Compensazione esposizione -panel.pages.file.info.image.exif.exposureProgram: Programma di esposizione -panel.pages.file.info.image.exif.exposureTime: Tempo di esposizione -panel.pages.file.info.image.exif.flash: Flash -panel.pages.file.info.image.exif.focalLength: Lunghezza focale -panel.pages.file.info.image.exif.lensModel: Obiettivo -panel.pages.file.info.image.exif.meteringMode: Modalità di misurazione -panel.pages.file.info.image.exif.sensitivity: Sensibilità -panel.pages.file.info.image.exif.whiteBalance: Bilanciamento del bianco -panel.pages.file.info.image.frames: '%d fotogrammi' -panel.pages.file.info.image.framesCount: Numero di fotogrammi -panel.pages.file.info.image.repeats: '%d ripetizioni' -panel.pages.file.info.image.repeatsCount: Numero di ripetizioni -panel.pages.file.info.image.resolution: Risoluzione -panel.pages.file.info.lastModifiedTime: Ultime modifiche -panel.pages.file.info.mimeType: Tipo MIME -panel.pages.file.info.size: Dimensioni file -panel.pages.file.info.uri: URI -panel.pages.file.position: Posizione -panel.pages.file.preview: Anteprima panel.pages.history.event.created: Pagina creata da %s %s. panel.pages.history.event.edited: Pagina modificata da %s %s. panel.pages.languages: Lingue diff --git a/panel/translations/pl.yaml b/panel/translations/pl.yaml index 1e3491f77..4f79866ca 100644 --- a/panel/translations/pl.yaml +++ b/panel/translations/pl.yaml @@ -54,6 +54,7 @@ panel.errors.error.notFound.description: Strona nie istnieje lub żądanie jest panel.errors.error.notFound.heading: Ups, nie znaleziono strony! panel.errors.error.notFound.status: Nie znaleziono panel.files.actions: Akcje +panel.files.backToPage: Powrót do strony panel.files.cannotDelete.fileNotFound: Nie można usunąć pliku, plik nie został znaleziony panel.files.cannotDelete.pageNotFound: Nie można usunąć pliku, strona nie została znaleziona panel.files.cannotRename.fileAlreadyExists: Nie można zmienić nazwy pliku, plik o tej samej nazwie już istnieje @@ -64,10 +65,47 @@ panel.files.cannotReplace.multipleFiles: Nie można zamienić pliku, podano wiel panel.files.cannotReplace.pageNotFound: Nie można zamienić pliku, strona nie została znaleziona panel.files.cannotUpload.pageNotFound: Nie można przesłać pliku, strona nie została znaleziona panel.files.deleted: Plik usunięty +panel.files.exif: EXIF panel.files.fileNotFound: Nie można pobrać informacji o pliku, plik nie został znaleziony +panel.files.files: Pliki +panel.files.info: Informacje +panel.files.info.image.colorDepth: Głębokość kolorów +panel.files.info.image.colorNumber: Liczba kolorów +panel.files.info.image.colorProfile: Profil kolorów +panel.files.info.image.colorSpace: Przestrzeń kolorów +panel.files.info.image.dimensions: Wymiary +panel.files.info.image.dimensions.widthByHeightPixels: '%d × %d pikseli' +panel.files.info.image.exif.aperture: Przysłona +panel.files.info.image.exif.camera: Aparat +panel.files.info.image.exif.creationDateAndTime: Data i czas utworzenia +panel.files.info.image.exif.exposureCompensation: Korekcja ekspozycji +panel.files.info.image.exif.exposureProgram: Program ekspozycji +panel.files.info.image.exif.exposureTime: Czas naświetlania +panel.files.info.image.exif.flash: Lampa błyskowa +panel.files.info.image.exif.focalLength: Długość ogniskowa +panel.files.info.image.exif.lensModel: Model obiektywu +panel.files.info.image.exif.meteringMode: Tryb pomiaru +panel.files.info.image.exif.sensitivity: Czułość +panel.files.info.image.exif.whiteBalance: Balans bieli +panel.files.info.image.frames: '%d klatek' +panel.files.info.image.framesCount: Liczba klatek +panel.files.info.image.repeats: '%d powtórzeń' +panel.files.info.image.repeatsCount: Liczba powtórzeń +panel.files.info.image.resolution: Rozdzielczość +panel.files.info.lastModifiedTime: Czas ostatniej modyfikacji +panel.files.info.mimeType: Typ MIME +panel.files.info.name: Nazwa +panel.files.info.size: Rozmiar +panel.files.info.uri: URI panel.files.metadata.updated: Metadane pliku zaktualizowane panel.files.pageNotFound: Nie można pobrać informacji o pliku, strona nie została znaleziona +panel.files.parent: Element nadrzędny +panel.files.position: Pozycja +panel.files.preview: Podgląd panel.files.renamed: Plik zmieniony +panel.files.search: Szukaj plików... +panel.files.upload: Prześlij pliki +panel.files.uploaded: Przesłane pliki panel.files.viewAsList: Wyświetl jako lista panel.files.viewAsThumbnails: Wyświetl jako miniatury panel.login.attempt.failed: Próba logowania nieudana! Spróbuj ponownie. @@ -84,6 +122,7 @@ panel.modal.action.continue: Kontynuuj panel.modal.action.delete: Usuń panel.modal.action.rename: Zmień nazwę panel.modal.action.save: Zapisz +panel.modal.action.upload: Prześlij panel.modal.action.uploadFile: Prześlij plik panel.modal.images.noImages: Nie ma tu żadnych obrazów panel.modal.images.noImages.upload: Prześlij obrazy @@ -161,38 +200,6 @@ panel.pages.deleteFile.prompt: Czy na pewno chcesz usunąć ten plik? Ta akcja j panel.pages.deletePage: Usuń stronę panel.pages.deletePage.prompt: Czy na pewno chcesz usunąć tę stronę? Ta akcja jest nieodwracalna. panel.pages.editPage: Edytuj stronę %s -panel.pages.file.backToPage: Powrót do strony -panel.pages.file.exif: EXIF -panel.pages.file.info: Informacje -panel.pages.file.info.image.colorDepth: Głębokość kolorów -panel.pages.file.info.image.colorNumber: Liczba kolorów -panel.pages.file.info.image.colorProfile: Profil kolorów -panel.pages.file.info.image.colorSpace: Przestrzeń kolorów -panel.pages.file.info.image.dimensions: Wymiary -panel.pages.file.info.image.dimensions.widthByHeightPixels: '%d × %d pikseli' -panel.pages.file.info.image.exif.aperture: Przysłona -panel.pages.file.info.image.exif.camera: Aparat -panel.pages.file.info.image.exif.creationDateAndTime: Data i czas utworzenia -panel.pages.file.info.image.exif.exposureCompensation: Korekcja ekspozycji -panel.pages.file.info.image.exif.exposureProgram: Program ekspozycji -panel.pages.file.info.image.exif.exposureTime: Czas naświetlania -panel.pages.file.info.image.exif.flash: Lampa błyskowa -panel.pages.file.info.image.exif.focalLength: Długość ogniskowa -panel.pages.file.info.image.exif.lensModel: Model obiektywu -panel.pages.file.info.image.exif.meteringMode: Tryb pomiaru -panel.pages.file.info.image.exif.sensitivity: Czułość -panel.pages.file.info.image.exif.whiteBalance: Balans bieli -panel.pages.file.info.image.frames: '%d klatek' -panel.pages.file.info.image.framesCount: Liczba klatek -panel.pages.file.info.image.repeats: '%d powtórzeń' -panel.pages.file.info.image.repeatsCount: Liczba powtórzeń -panel.pages.file.info.image.resolution: Rozdzielczość -panel.pages.file.info.lastModifiedTime: Czas ostatniej modyfikacji -panel.pages.file.info.mimeType: Typ MIME -panel.pages.file.info.size: Rozmiar -panel.pages.file.info.uri: URI -panel.pages.file.position: Pozycja -panel.pages.file.preview: Podgląd panel.pages.history.event.created: Strona utworzona przez %s %s. panel.pages.history.event.edited: Strona edytowana przez %s %s. panel.pages.languages: Języki diff --git a/panel/translations/pt.yaml b/panel/translations/pt.yaml index 72632fd44..da1ff5de4 100644 --- a/panel/translations/pt.yaml +++ b/panel/translations/pt.yaml @@ -54,6 +54,7 @@ panel.errors.error.notFound.description: A página não existe ou a solicitaçã panel.errors.error.notFound.heading: Ops, página não encontrada! panel.errors.error.notFound.status: Não encontrado panel.files.actions: Ações +panel.files.backToPage: Voltar à página panel.files.cannotDelete.fileNotFound: Não é possível excluir o ficheiro, ficheiro não encontrado panel.files.cannotDelete.pageNotFound: Não é possível excluir a página, página não encontrado panel.files.cannotRename.fileAlreadyExists: Não é possível renomear o arquivo, um arquivo com o mesmo nome já existe @@ -64,10 +65,47 @@ panel.files.cannotReplace.multipleFiles: Não é possível substituir o ficheiro panel.files.cannotReplace.pageNotFound: Não é possível substituir o ficheiro, página não encontrada panel.files.cannotUpload.pageNotFound: Não é possível efectuar envio do ficheiro, página não encontrada panel.files.deleted: Ficheiro removido +panel.files.exif: EXIF panel.files.fileNotFound: Não é possível obter informações do arquivo, arquivo não encontrado +panel.files.files: Ficheiros +panel.files.info: Informações +panel.files.info.image.colorDepth: Profundidade de cor +panel.files.info.image.colorNumber: Número de cores +panel.files.info.image.colorProfile: Perfil de cor +panel.files.info.image.colorSpace: Espaço de cor +panel.files.info.image.dimensions: Dimensões +panel.files.info.image.dimensions.widthByHeightPixels: '%d × %d píxeis' +panel.files.info.image.exif.aperture: Abertura +panel.files.info.image.exif.camera: Câmara +panel.files.info.image.exif.creationDateAndTime: Data e hora de criação +panel.files.info.image.exif.exposureCompensation: Compensação de exposição +panel.files.info.image.exif.exposureProgram: Programa de exposição +panel.files.info.image.exif.exposureTime: Tempo de exposição +panel.files.info.image.exif.flash: Flash +panel.files.info.image.exif.focalLength: Distância focal +panel.files.info.image.exif.lensModel: Modelo da lente +panel.files.info.image.exif.meteringMode: Modo de medição +panel.files.info.image.exif.sensitivity: Sensibilidade +panel.files.info.image.exif.whiteBalance: Balanço de branco +panel.files.info.image.frames: '%d frames' +panel.files.info.image.framesCount: Contagem de frames +panel.files.info.image.repeats: '%d repetições' +panel.files.info.image.repeatsCount: Contagem de repetições +panel.files.info.image.resolution: Resolução +panel.files.info.lastModifiedTime: Última modificação +panel.files.info.mimeType: Tipo MIME +panel.files.info.name: Nome +panel.files.info.size: Tamanho +panel.files.info.uri: URI panel.files.metadata.updated: Metadados do ficheiro atualizados panel.files.pageNotFound: Não é possível obter informações do arquivo, página não encontrada +panel.files.parent: Elemento pai +panel.files.position: Posição +panel.files.preview: Pré-visualização panel.files.renamed: Arquivo renomeado +panel.files.search: Procurar ficheiros... +panel.files.upload: Carregar ficheiros +panel.files.uploaded: Ficheiros carregados panel.files.viewAsList: Ver como lista panel.files.viewAsThumbnails: Ver como miniaturas panel.login.attempt.failed: Falha ao tentar efetuar login! Tente novamente. @@ -84,6 +122,7 @@ panel.modal.action.continue: Continuar panel.modal.action.delete: Remover panel.modal.action.rename: Renomear panel.modal.action.save: Guardar +panel.modal.action.upload: Carregar panel.modal.action.uploadFile: Enviar um ficheiro panel.modal.images.noImages: Não há imagens aqui panel.modal.images.noImages.upload: Por favor carregue algumas imagens @@ -161,38 +200,6 @@ panel.pages.deleteFile.prompt: Tem certeza de que deseja remover este ficheiro? panel.pages.deletePage: Remover página panel.pages.deletePage.prompt: Tem certeza que deseja remover esta página? Esta ação não pode ser desfeita. panel.pages.editPage: Editar página %s -panel.pages.file.backToPage: Voltar à página -panel.pages.file.exif: EXIF -panel.pages.file.info: Informações -panel.pages.file.info.image.colorDepth: Profundidade de cor -panel.pages.file.info.image.colorNumber: Número de cores -panel.pages.file.info.image.colorProfile: Perfil de cor -panel.pages.file.info.image.colorSpace: Espaço de cor -panel.pages.file.info.image.dimensions: Dimensões -panel.pages.file.info.image.dimensions.widthByHeightPixels: '%d × %d píxeis' -panel.pages.file.info.image.exif.aperture: Abertura -panel.pages.file.info.image.exif.camera: Câmara -panel.pages.file.info.image.exif.creationDateAndTime: Data e hora de criação -panel.pages.file.info.image.exif.exposureCompensation: Compensação de exposição -panel.pages.file.info.image.exif.exposureProgram: Programa de exposição -panel.pages.file.info.image.exif.exposureTime: Tempo de exposição -panel.pages.file.info.image.exif.flash: Flash -panel.pages.file.info.image.exif.focalLength: Distância focal -panel.pages.file.info.image.exif.lensModel: Modelo da lente -panel.pages.file.info.image.exif.meteringMode: Modo de medição -panel.pages.file.info.image.exif.sensitivity: Sensibilidade -panel.pages.file.info.image.exif.whiteBalance: Balanço de branco -panel.pages.file.info.image.frames: '%d frames' -panel.pages.file.info.image.framesCount: Contagem de frames -panel.pages.file.info.image.repeats: '%d repetições' -panel.pages.file.info.image.repeatsCount: Contagem de repetições -panel.pages.file.info.image.resolution: Resolução -panel.pages.file.info.lastModifiedTime: Última modificação -panel.pages.file.info.mimeType: Tipo MIME -panel.pages.file.info.size: Tamanho -panel.pages.file.info.uri: URI -panel.pages.file.position: Posição -panel.pages.file.preview: Pré-visualização panel.pages.history.event.created: Página criada por %s %s. panel.pages.history.event.edited: Página editada por %s %s. panel.pages.languages: Idiomas diff --git a/panel/translations/ru.yaml b/panel/translations/ru.yaml index 841c050f2..db78cfb6a 100644 --- a/panel/translations/ru.yaml +++ b/panel/translations/ru.yaml @@ -54,6 +54,7 @@ panel.errors.error.notFound.description: Страница не существу panel.errors.error.notFound.heading: К сожалению, страница не найдена! panel.errors.error.notFound.status: Не обнаружена panel.files.actions: Действия +panel.files.backToPage: Назад к странице panel.files.cannotDelete.fileNotFound: Не удается удалить файл, файл не найден panel.files.cannotDelete.pageNotFound: Не удается удалить файл, файл не найден panel.files.cannotRename.fileAlreadyExists: Невозможно переименовать файл, файл с таким именем уже существует @@ -64,10 +65,47 @@ panel.files.cannotReplace.multipleFiles: Невозможно заменить panel.files.cannotReplace.pageNotFound: Невозможно заменить файл, страница не найдена panel.files.cannotUpload.pageNotFound: Невозможно загрузить файл, страница не найдена panel.files.deleted: Файл удален +panel.files.exif: EXIF panel.files.fileNotFound: Невозможно получить информацию о файле, файл не найден +panel.files.files: Файлы +panel.files.info: Информация +panel.files.info.image.colorDepth: Глубина цвета +panel.files.info.image.colorNumber: Количество цветов +panel.files.info.image.colorProfile: Профиль цвета +panel.files.info.image.colorSpace: Цветовое пространство +panel.files.info.image.dimensions: Размеры +panel.files.info.image.dimensions.widthByHeightPixels: '%d × %d пикселей' +panel.files.info.image.exif.aperture: Диафрагма +panel.files.info.image.exif.camera: Камера +panel.files.info.image.exif.creationDateAndTime: Дата и время создания +panel.files.info.image.exif.exposureCompensation: Компенсация экспозиции +panel.files.info.image.exif.exposureProgram: Программа экспозиции +panel.files.info.image.exif.exposureTime: Время экспозиции +panel.files.info.image.exif.flash: Вспышка +panel.files.info.image.exif.focalLength: Фокусное расстояние +panel.files.info.image.exif.lensModel: Модель объектива +panel.files.info.image.exif.meteringMode: Режим измерения +panel.files.info.image.exif.sensitivity: Чувствительность +panel.files.info.image.exif.whiteBalance: Баланс белого +panel.files.info.image.frames: '%d кадров' +panel.files.info.image.framesCount: Количество кадров +panel.files.info.image.repeats: '%d повторов' +panel.files.info.image.repeatsCount: Количество повторений +panel.files.info.image.resolution: Разрешение +panel.files.info.lastModifiedTime: Последнее изменение +panel.files.info.mimeType: Тип MIME +panel.files.info.name: Имя +panel.files.info.size: Размер +panel.files.info.uri: URI panel.files.metadata.updated: Метаданные файла обновлены panel.files.pageNotFound: Невозможно получить информацию о файле, страница не найдена +panel.files.parent: Родительский элемент +panel.files.position: Позиция +panel.files.preview: Просмотр panel.files.renamed: Файл переименован +panel.files.search: Поиск файлов... +panel.files.upload: Загрузить файлы +panel.files.uploaded: Файлы загружены panel.files.viewAsList: Просмотр в виде списка panel.files.viewAsThumbnails: Просмотр в виде миниатюр panel.login.attempt.failed: Войти попытка не удалась! Попробуйте еще раз. @@ -84,6 +122,7 @@ panel.modal.action.continue: Продолжить panel.modal.action.delete: Удалить panel.modal.action.rename: Переименовать panel.modal.action.save: Сохранить +panel.modal.action.upload: Загрузить panel.modal.action.uploadFile: Загрузить файл panel.modal.images.noImages: Там нет изображений здесь panel.modal.images.noImages.upload: Пожалуйста, загрузите некоторые изображения @@ -161,38 +200,6 @@ panel.pages.deleteFile.prompt: Вы уверены, что хотите удал panel.pages.deletePage: Удалить страницу panel.pages.deletePage.prompt: Вы уверены, что хотите удалить эту страницу? Это действие не может быть отменено. panel.pages.editPage: Отредактировать страницу %s -panel.pages.file.backToPage: Назад к странице -panel.pages.file.exif: EXIF -panel.pages.file.info: Информация -panel.pages.file.info.image.colorDepth: Глубина цвета -panel.pages.file.info.image.colorNumber: Количество цветов -panel.pages.file.info.image.colorProfile: Профиль цвета -panel.pages.file.info.image.colorSpace: Цветовое пространство -panel.pages.file.info.image.dimensions: Размеры -panel.pages.file.info.image.dimensions.widthByHeightPixels: '%d × %d пикселей' -panel.pages.file.info.image.exif.aperture: Диафрагма -panel.pages.file.info.image.exif.camera: Камера -panel.pages.file.info.image.exif.creationDateAndTime: Дата и время создания -panel.pages.file.info.image.exif.exposureCompensation: Компенсация экспозиции -panel.pages.file.info.image.exif.exposureProgram: Программа экспозиции -panel.pages.file.info.image.exif.exposureTime: Время экспозиции -panel.pages.file.info.image.exif.flash: Вспышка -panel.pages.file.info.image.exif.focalLength: Фокусное расстояние -panel.pages.file.info.image.exif.lensModel: Модель объектива -panel.pages.file.info.image.exif.meteringMode: Режим измерения -panel.pages.file.info.image.exif.sensitivity: Чувствительность -panel.pages.file.info.image.exif.whiteBalance: Баланс белого -panel.pages.file.info.image.frames: '%d кадров' -panel.pages.file.info.image.framesCount: Количество кадров -panel.pages.file.info.image.repeats: '%d повторов' -panel.pages.file.info.image.repeatsCount: Количество повторений -panel.pages.file.info.image.resolution: Разрешение -panel.pages.file.info.lastModifiedTime: Последнее изменение -panel.pages.file.info.mimeType: Тип MIME -panel.pages.file.info.size: Размер -panel.pages.file.info.uri: URI -panel.pages.file.position: Позиция -panel.pages.file.preview: Просмотр panel.pages.history.event.created: Страница создана %s %s. panel.pages.history.event.edited: Страница изменена %s %s. panel.pages.languages: Языки diff --git a/panel/translations/uk.yaml b/panel/translations/uk.yaml index a4859a257..a8bc47a55 100644 --- a/panel/translations/uk.yaml +++ b/panel/translations/uk.yaml @@ -54,6 +54,7 @@ panel.errors.error.notFound.description: Сторінка не існує або panel.errors.error.notFound.heading: Ой, сторінка не знайдена! panel.errors.error.notFound.status: Не знайдено panel.files.actions: Дії +panel.files.backToPage: Назад до сторінки panel.files.cannotDelete.fileNotFound: Неможливо видалити файл, файл не знайдено panel.files.cannotDelete.pageNotFound: Неможливо видалити файл, сторінка не знайдена panel.files.cannotRename.fileAlreadyExists: Неможливо перейменувати файл, файл з таким іменем вже існує @@ -64,10 +65,47 @@ panel.files.cannotReplace.multipleFiles: Неможливо замінити ф panel.files.cannotReplace.pageNotFound: Неможливо замінити файл, сторінка не знайдена panel.files.cannotUpload.pageNotFound: Неможливо завантажити файл, сторінка не знайдена panel.files.deleted: Файл видалено +panel.files.exif: EXIF panel.files.fileNotFound: Неможливо отримати інформацію про файл, файл не знайдено +panel.files.files: Файли +panel.files.info: Інформація +panel.files.info.image.colorDepth: Глибина кольору +panel.files.info.image.colorNumber: Кількість кольорів +panel.files.info.image.colorProfile: Колірний профіль +panel.files.info.image.colorSpace: Колірний простір +panel.files.info.image.dimensions: Розміри +panel.files.info.image.dimensions.widthByHeightPixels: '%d × %d пікселів' +panel.files.info.image.exif.aperture: Діафрагма +panel.files.info.image.exif.camera: Камера +panel.files.info.image.exif.creationDateAndTime: Дата та час створення +panel.files.info.image.exif.exposureCompensation: Компенсація експозиції +panel.files.info.image.exif.exposureProgram: Програма експозиції +panel.files.info.image.exif.exposureTime: Час експозиції +panel.files.info.image.exif.flash: Спалах +panel.files.info.image.exif.focalLength: Фокусна відстань +panel.files.info.image.exif.lensModel: Модель об'єктива +panel.files.info.image.exif.meteringMode: Режим замірів +panel.files.info.image.exif.sensitivity: Чутливість +panel.files.info.image.exif.whiteBalance: Баланс білого +panel.files.info.image.frames: '%d кадрів' +panel.files.info.image.framesCount: Кількість кадрів +panel.files.info.image.repeats: '%d повторів' +panel.files.info.image.repeatsCount: Кількість повторів +panel.files.info.image.resolution: Роздільна здатність +panel.files.info.lastModifiedTime: Час останнього редагування +panel.files.info.mimeType: MIME тип +panel.files.info.name: Назва +panel.files.info.size: Розмір +panel.files.info.uri: URI panel.files.metadata.updated: Метадані файлу оновлено panel.files.pageNotFound: Неможливо отримати інформацію про файл, сторінка не знайдена +panel.files.parent: Батьківський елемент +panel.files.position: Позиція +panel.files.preview: Попередній перегляд panel.files.renamed: Файл перейменовано +panel.files.search: Пошук файлів... +panel.files.upload: Завантажити файли +panel.files.uploaded: Файли завантажено panel.files.viewAsList: Переглядати як список panel.files.viewAsThumbnails: Переглядати як ескізи panel.login.attempt.failed: Не вдалося увійти! Спробуйте ще раз. @@ -84,6 +122,7 @@ panel.modal.action.continue: Продовжити panel.modal.action.delete: Видалити panel.modal.action.rename: Перейменувати panel.modal.action.save: Зберегти +panel.modal.action.upload: Завантажити panel.modal.action.uploadFile: Завантажити файл panel.modal.images.noImages: Тут немає зображень panel.modal.images.noImages.upload: Будь ласка, завантажте зображення @@ -161,38 +200,6 @@ panel.pages.deleteFile.prompt: Ви впевнені, що хочете вида panel.pages.deletePage: Видалити сторінку panel.pages.deletePage.prompt: Ви впевнені, що хочете видалити цю сторінку? Цю дію не можна скасувати. panel.pages.editPage: Редагувати сторінку %s -panel.pages.file.backToPage: Назад до сторінки -panel.pages.file.exif: EXIF -panel.pages.file.info: Інформація -panel.pages.file.info.image.colorDepth: Глибина кольору -panel.pages.file.info.image.colorNumber: Кількість кольорів -panel.pages.file.info.image.colorProfile: Колірний профіль -panel.pages.file.info.image.colorSpace: Колірний простір -panel.pages.file.info.image.dimensions: Розміри -panel.pages.file.info.image.dimensions.widthByHeightPixels: '%d × %d пікселів' -panel.pages.file.info.image.exif.aperture: Діафрагма -panel.pages.file.info.image.exif.camera: Камера -panel.pages.file.info.image.exif.creationDateAndTime: Дата та час створення -panel.pages.file.info.image.exif.exposureCompensation: Компенсація експозиції -panel.pages.file.info.image.exif.exposureProgram: Програма експозиції -panel.pages.file.info.image.exif.exposureTime: Час експозиції -panel.pages.file.info.image.exif.flash: Спалах -panel.pages.file.info.image.exif.focalLength: Фокусна відстань -panel.pages.file.info.image.exif.lensModel: Модель об'єктива -panel.pages.file.info.image.exif.meteringMode: Режим замірів -panel.pages.file.info.image.exif.sensitivity: Чутливість -panel.pages.file.info.image.exif.whiteBalance: Баланс білого -panel.pages.file.info.image.frames: '%d кадрів' -panel.pages.file.info.image.framesCount: Кількість кадрів -panel.pages.file.info.image.repeats: '%d повторів' -panel.pages.file.info.image.repeatsCount: Кількість повторів -panel.pages.file.info.image.resolution: Роздільна здатність -panel.pages.file.info.lastModifiedTime: Час останнього редагування -panel.pages.file.info.mimeType: MIME тип -panel.pages.file.info.size: Розмір -panel.pages.file.info.uri: URI -panel.pages.file.position: Позиція -panel.pages.file.preview: Попередній перегляд panel.pages.history.event.created: Сторінка створена %s %s. panel.pages.history.event.edited: Сторінка відредагована %s %s. panel.pages.languages: Мови diff --git a/panel/views/fields/partials/filelist.php b/panel/views/fields/partials/filelist.php deleted file mode 100644 index 96c99765c..000000000 --- a/panel/views/fields/partials/filelist.php +++ /dev/null @@ -1,69 +0,0 @@ -modals()->addMultiple(['deleteFileItem', 'renameFileItem']) ?> - -
-
-
- - -
-
- -
- -
- type() === 'image') : ?> -
- - type() === 'video') : ?> - - -
icon(is_null($file->type()) ? 'file' : 'file-' . $file->type()) ?>
-
name() ?> (size() ?>)
- -
- -
-
- - diff --git a/panel/views/fields/upload.php b/panel/views/fields/upload.php index 0a91570d2..6fd3af6e1 100644 --- a/panel/views/fields/upload.php +++ b/panel/views/fields/upload.php @@ -1,5 +1,5 @@ get('listFiles', false) && ($model = $field->parent()?->model())) : ?> - insert('fields.partials.filelist', ['model' => $model, 'files' => $field->collection()]); ?> + insert('partials.files.file.list', ['name' => $field->name(), 'files' => $field->collection()->map(fn($file) => [$file, $model]), 'columns' => ['date', 'size']]); ?> has('label')) : ?> @@ -20,4 +20,4 @@ ]) ?>> icon('cloud-upload') ?> translate('fields.file.uploadLabel') ?> -insert('fields.partials.description') ?> \ No newline at end of file +insert('fields.partials.description') ?> diff --git a/panel/views/files/edit.php b/panel/views/files/edit.php new file mode 100644 index 000000000..d13d5ea07 --- /dev/null +++ b/panel/views/files/edit.php @@ -0,0 +1,118 @@ +layout('panel') ?> + +modals()->addMultiple(['changes', 'renameFile', 'deleteFile']) ?> + +
+
+
+
+
icon(is_null($file->type()) ? 'file' : 'file-' . $file->type()) ?>
+
name() ?>
+
+
+ getModelIdentifier() === 'page'): ?> + icon($model->icon()) ?>escape($model->title()) ?> + + getModelIdentifier() === 'site') : ?> + icon('globe') ?>translate('panel.options.site') ?> + +
+
+
+ href="uri($app->router()->generate('panel.files.edit', ['model' => $model->getModelIdentifier(), 'id' => $model->route(), 'filename' => $previousFile->name()])) ?>" title="translate('panel.pages.previousFile') ?>" aria-label="translate('panel.pages.previousFile') ?>">icon('chevron-left') ?> + href="uri($app->router()->generate('panel.files.edit', ['model' => $model->getModelIdentifier(), 'id' => $model->route(), 'filename' => $nextFile->name()])) ?>" title="translate('panel.pages.nextFile') ?>" aria-label="translate('panel.pages.nextFile') ?>">icon('chevron-right') ?> + user()->permissions()->has('pages.renameFiles')) : ?> + + + user()->permissions()->has('pages.replaceFiles')) : ?> + + + user()->permissions()->has('pages.deleteFiles')) : ?> + + + fields()->isEmpty()): ?> + + +
+
+ type() === 'image') : ?> +
+
+
+ translate('panel.files.preview') ?> +
+
+ +
+
+
+ + type() === 'video') : ?> +
+
+ translate('panel.files.preview') ?> +
+
+ +
+
+ +
+
+ translate('panel.files.info') ?> +
+
+
+
+
translate('panel.files.info.mimeType') ?>:
+ mimeType() ?> +
+
+
translate('panel.files.info.size') ?>:
+ size() ?> +
+
+
translate('panel.files.info.lastModifiedTime') ?>:
+ datetime($file->lastModifiedTime()) ?> +
+
+
translate('panel.files.info.uri') ?>:
+ uri() ?> +
+ type() === 'image') : ?> + insert('_files/images/info/info', ['file' => $file]) ?> + +
+
+
+ type() === 'image') : ?> + hasExifData() && $file->getExifData()->hasPositionData()) : ?> +
+
+ + translate('panel.files.position') ?> +
+
+ insert('_files/images/position/map', ['exif' => $file->getExifData()]) ?> +
+
+ + hasExifData()) : ?> + + + + + fields()->isEmpty()): ?> + insert('fields', ['fields' => $file->fields()]) ?> + +
diff --git a/panel/views/files/index.php b/panel/views/files/index.php index 858feebc1..ffe438bb2 100644 --- a/panel/views/files/index.php +++ b/panel/views/files/index.php @@ -1,111 +1,19 @@ layout('panel') ?> +modals()->add('uploadFile') ?> -modals()->addMultiple(['changes', 'renameFile', 'deleteFile']) ?> - -
+
-
-
-
icon(is_null($file->type()) ? 'file' : 'file-' . $file->type()) ?>
-
name() ?>
-
- -
+
translate('panel.files.files') ?>
- href="uri('/files/pages/' . trim($page->route(), '/') . '/' . ($previousFile->name()) . '/') ?>" title="translate('panel.pages.previousFile') ?>" aria-label="translate('panel.pages.previousFile') ?>">icon('chevron-left') ?> - href="uri('/files/pages/' . trim($page->route(), '/') . '/' . ($nextFile->name()) . '/') ?>" title="translate('panel.pages.nextFile') ?>" aria-label="translate('panel.pages.nextFile') ?>">icon('chevron-right') ?> - user()->permissions()->has('pages.renameFiles')) : ?> - - - user()->permissions()->has('pages.replaceFiles')) : ?> - - - user()->permissions()->has('pages.deleteFiles')) : ?> - - - fields()->isEmpty()): ?> - + user()->permissions()->has('files.upload')) : ?> +
- type() === 'image') : ?> -
-
-
- translate('panel.pages.file.preview') ?> -
-
- -
-
-
- - type() === 'video') : ?> -
-
- translate('panel.pages.file.preview') ?> -
-
- -
-
- -
-
- translate('panel.pages.file.info') ?> -
+ +
-
-
-
translate('panel.pages.file.info.mimeType') ?>:
- mimeType() ?> -
-
-
translate('panel.pages.file.info.size') ?>:
- size() ?> -
-
-
translate('panel.pages.file.info.lastModifiedTime') ?>:
- datetime($file->lastModifiedTime()) ?> -
-
-
translate('panel.pages.file.info.uri') ?>:
- uri() ?> -
- type() === 'image') : ?> - insert('_files/images/info/info', ['file' => $file]) ?> - -
+ insert('partials.files.file.list', ['name' => 'view-files', 'files' => $files, 'columns' => ['parent', 'date', 'size']]) ?>
-
- type() === 'image') : ?> - hasExifData() && $file->getExifData()->hasPositionData()) : ?> -
-
- - translate('panel.pages.file.position') ?> -
-
- insert('_files/images/position/map', ['exif' => $file->getExifData()]) ?> -
-
- - hasExifData()) : ?> - - - - - fields()->isEmpty()): ?> - insert('fields', ['fields' => $file->fields()]) ?> - - +
+ diff --git a/panel/views/modals/modal.php b/panel/views/modals/modal.php index 41f6a6a2c..fad7dc872 100644 --- a/panel/views/modals/modal.php +++ b/panel/views/modals/modal.php @@ -1,7 +1,11 @@ diff --git a/panel/views/partials/files/file/item.php b/panel/views/partials/files/file/item.php new file mode 100644 index 000000000..0992b4c15 --- /dev/null +++ b/panel/views/partials/files/file/item.php @@ -0,0 +1,44 @@ +
+ type() === 'image') : ?> + + + type() === 'video') : ?> + + +
icon(is_null($file->type()) ? 'file' : 'file-' . $file->type()) ?>
+
name() ?>
+ +
+ getModelIdentifier() === 'page'): ?> + icon($model->icon()) ?>escape($model->title()) ?> + + getModelIdentifier() === 'site') : ?> + icon('globe') ?>translate('panel.options.site') ?> + +
+ + +
datetime($file->lastModifiedTime()) ?>
+ + +
size() ?>
+ +
+ +
+
diff --git a/panel/views/partials/files/file/list.php b/panel/views/partials/files/file/list.php new file mode 100644 index 000000000..d9306a31e --- /dev/null +++ b/panel/views/partials/files/file/list.php @@ -0,0 +1,71 @@ +modals()->addMultiple(['deleteFileItem', 'renameFileItem']) ?> + +
attr(['class' => 'files-list', 'data-for' => $name, 'hidden' => count($files) === 0]) ?>> +
+
+
+ icon('search') ?> + +
+
+
+ + +
+
+ +
+
translate('panel.files.info.name') ?>
+ +
translate('panel.files.parent') ?>
+ + +
translate('panel.files.info.lastModifiedTime') ?>
+ + +
translate('panel.files.info.size') ?>
+ +
translate('panel.files.actions') ?>
+
+ +
+ + insert('partials.files.file.item', ['file' => $file, 'model' => $model, 'columns' => $columns]) ?> + +
+
+ + diff --git a/panel/views/partials/files/images/info/exif.php b/panel/views/partials/files/images/info/exif.php index 6b6650b7a..373391d0b 100644 --- a/panel/views/partials/files/images/info/exif.php +++ b/panel/views/partials/files/images/info/exif.php @@ -1,48 +1,48 @@
-
translate('panel.pages.file.info.image.exif.creationDateAndTime') ?>:
+
translate('panel.files.info.image.exif.creationDateAndTime') ?>:
escape($exif->dateTimeOriginal() ?? '-') ?>
-
translate('panel.pages.file.info.image.exif.camera') ?>:
+
translate('panel.files.info.image.exif.camera') ?>:
escape($exif->makeAndModel() ?? '–') ?>
-
translate('panel.pages.file.info.image.exif.lensModel') ?>:
+
translate('panel.files.info.image.exif.lensModel') ?>:
escape($exif->lensModel() ?? '–') ?>
-
translate('panel.pages.file.info.image.exif.focalLength') ?>:
+
translate('panel.files.info.image.exif.focalLength') ?>:
escape($exif->focalLength() ?? '–') ?>
-
translate('panel.pages.file.info.image.exif.exposureTime') ?>:
+
translate('panel.files.info.image.exif.exposureTime') ?>:
escape($exif->exposureTime() ?? '–') ?>
-
translate('panel.pages.file.info.image.exif.aperture') ?>:
+
translate('panel.files.info.image.exif.aperture') ?>:
escape($exif->aperture() ?? '–') ?>
-
translate('panel.pages.file.info.image.exif.sensitivity') ?>:
+
translate('panel.files.info.image.exif.sensitivity') ?>:
escape($exif->photographicSensitivity() ?? '–') ?>
-
translate('panel.pages.file.info.image.exif.exposureCompensation') ?>:
+
translate('panel.files.info.image.exif.exposureCompensation') ?>:
escape($exif->exposureCompensation() ?? '–') ?>
-
translate('panel.pages.file.info.image.exif.exposureProgram') ?>:
+
translate('panel.files.info.image.exif.exposureProgram') ?>:
escape($exif->exposureProgram() ?? '–') ?>
-
translate('panel.pages.file.info.image.exif.whiteBalance') ?>:
+
translate('panel.files.info.image.exif.whiteBalance') ?>:
hasAutoWhiteBalance() === true) : ?>AWB
-
translate('panel.pages.file.info.image.exif.flash') ?>:
+
translate('panel.files.info.image.exif.flash') ?>:
hasFlashFired() !== null) : ?> hasFlashFired() ? $this->icon('camera-flash') : $this->icon('camera-no-flash') ?>
-
translate('panel.pages.file.info.image.exif.meteringMode') ?>:
+
translate('panel.files.info.image.exif.meteringMode') ?>:
meteringMode() !== null) : ?>icon('camera-metering-' . $exif->meteringMode()) ?> -
\ No newline at end of file + diff --git a/panel/views/partials/files/images/info/info.php b/panel/views/partials/files/images/info/info.php index 921963e2f..193a4c7bd 100644 --- a/panel/views/partials/files/images/info/info.php +++ b/panel/views/partials/files/images/info/info.php @@ -1,27 +1,27 @@
-
translate('panel.pages.file.info.image.dimensions') ?>:
- translate('panel.pages.file.info.image.dimensions.widthByHeightPixels', $file->info()->width(), $file->info()->height()) ?> +
translate('panel.files.info.image.dimensions') ?>:
+ translate('panel.files.info.image.dimensions.widthByHeightPixels', $file->info()->width(), $file->info()->height()) ?>
-
translate('panel.pages.file.info.image.resolution') ?>:
+
translate('panel.files.info.image.resolution') ?>:
info()->width() * $file->info()->height() / 1e6, 0.1), 1) ?> MP
-
translate('panel.pages.file.info.image.colorSpace') ?>:
+
translate('panel.files.info.image.colorSpace') ?>:
info()->colorSpace()->value ?>
-
translate('panel.pages.file.info.image.colorDepth') ?>:
+
translate('panel.files.info.image.colorDepth') ?>:
info()->colorDepth() ?> bit
info()->colorSpace()->value === 'PALETTE') : ?>
-
translate('panel.pages.file.info.image.colorNumber') ?>:
+
translate('panel.files.info.image.colorNumber') ?>:
info()->colorNumber() ?>
-
translate('panel.pages.file.info.image.colorProfile') ?>:
+
translate('panel.files.info.image.colorProfile') ?>:
hasColorProfile() && $file->getColorProfile()->name()) : ?> escape($file->getColorProfile()->name()) ?> hasExifData() && $file->getExifData()->colorSpace()) : ?> @@ -32,14 +32,14 @@
info()->isAnimation()) : ?>
-
translate('panel.pages.file.info.image.framesCount') ?>:
- translate('panel.pages.file.info.image.frames', $file->info()->animationFrames()) ?> +
translate('panel.files.info.image.framesCount') ?>:
+ translate('panel.files.info.image.frames', $file->info()->animationFrames()) ?>
-
translate('panel.pages.file.info.image.repeatsCount') ?>:
- info()->animationRepeatCount() > 0) : ?>translate('panel.pages.file.info.image.repeats', $file->info()->animationRepeatCount()) ?> +
translate('panel.files.info.image.repeatsCount') ?>:
+ info()->animationRepeatCount() > 0) : ?>translate('panel.files.info.image.repeats', $file->info()->animationRepeatCount()) ?>
hasExifData()) : ?> insert('_files/images/info/exif', ['exif' => $file->getExifData()]) ?> - \ No newline at end of file + diff --git a/site/users/roles/admin.yaml b/site/users/roles/admin.yaml index 0c73963f4..2245cd60e 100644 --- a/site/users/roles/admin.yaml +++ b/site/users/roles/admin.yaml @@ -5,6 +5,7 @@ permissions: cache: true backup: true pages: true + files: true options: true updates: true users: true diff --git a/site/users/roles/user.yaml b/site/users/roles/user.yaml index 9f4320bc2..518332f2d 100644 --- a/site/users/roles/user.yaml +++ b/site/users/roles/user.yaml @@ -3,3 +3,4 @@ title: '{{user.role.user}}' permissions: dashboard: true pages: true + files: true From e2c4fab07e3d91cbb1368d091f157eac1afe3ad0 Mon Sep 17 00:00:00 2001 From: Giuseppe Criscione <18699708+giuscris@users.noreply.github.com> Date: Sun, 1 Jun 2025 10:22:10 +0200 Subject: [PATCH 4/6] Extract upload action from `FilesController::index()` --- .../src/Panel/Controllers/FilesController.php | 62 ++++++++++--------- panel/modals/uploadFile.yaml | 2 + panel/routes.php | 11 +++- 3 files changed, 43 insertions(+), 32 deletions(-) diff --git a/formwork/src/Panel/Controllers/FilesController.php b/formwork/src/Panel/Controllers/FilesController.php index 9d3bf323e..55f874e4b 100644 --- a/formwork/src/Panel/Controllers/FilesController.php +++ b/formwork/src/Panel/Controllers/FilesController.php @@ -35,47 +35,51 @@ public function index(): Response return $this->forward(ErrorsController::class, 'forbidden'); } - if ($this->request->method() === RequestMethod::POST) { - if (!$this->hasPermission('files.upload')) { - return $this->forward(ErrorsController::class, 'forbidden'); - } + return new Response($this->view('files.index', [ + 'title' => $this->translate('panel.files.files'), + 'files' => $this->getFiles(), + ])); + } - $fields = $this->modal('uploadFile')->fields(); + /** + * FilesController@upload action + */ + public function upload(): Response + { + if (!$this->hasPermission('files.upload')) { + return $this->forward(ErrorsController::class, 'forbidden'); + } - $fields->setValuesFromRequest($this->request, null)->validate(); + $fields = $this->modal('uploadFile')->fields(); - $filesField = $fields->get('files'); + $fields->setValuesFromRequest($this->request, null)->validate(); - if (!$filesField->isEmpty()) { - $files = $filesField->isMultiple() ? $filesField->value() : [$filesField->value()]; + $filesField = $fields->get('files'); - /** @var Page|Site */ - $parent = $fields->get('parent')->return(); + if (!$filesField->isEmpty()) { + $files = $filesField->isMultiple() ? $filesField->value() : [$filesField->value()]; - $destination = $parent instanceof Site - ? $this->config->get('system.files.paths.site') - : $parent->contentPath(); + /** @var Page|Site */ + $parent = $fields->get('parent')->return(); - foreach ($files as $file) { - $this->fileUploader->upload( - $file, - $destination, - $filesField->filename(), - $filesField->acceptMimeTypes(), - $filesField->overwrite(), - ); - } + $destination = $parent instanceof Site + ? $this->config->get('system.files.paths.site') + : $parent->contentPath(); - $this->panel->notify($this->translate('panel.files.uploaded'), 'success'); + foreach ($files as $file) { + $this->fileUploader->upload( + $file, + $destination, + $filesField->filename(), + $filesField->acceptMimeTypes(), + $filesField->overwrite(), + ); } - return $this->redirect($this->generateRoute('panel.files.index')); + $this->panel->notify($this->translate('panel.files.uploaded'), 'success'); } - return new Response($this->view('files.index', [ - 'title' => $this->translate('panel.files.files'), - 'files' => $this->getFiles(), - ])); + return $this->redirect($this->generateRoute('panel.files.index')); } /** diff --git a/panel/modals/uploadFile.yaml b/panel/modals/uploadFile.yaml index da4c579aa..2d8c56502 100644 --- a/panel/modals/uploadFile.yaml +++ b/panel/modals/uploadFile.yaml @@ -2,6 +2,8 @@ title: '{{panel.files.upload}}' size: large +action: /files/upload/ + fields: files: type: upload diff --git a/panel/routes.php b/panel/routes.php index 67c64f54e..2375a775f 100644 --- a/panel/routes.php +++ b/panel/routes.php @@ -99,9 +99,14 @@ ], 'panel.files.index' => [ - 'path' => '/files/', - 'action' => 'Formwork\Panel\Controllers\FilesController@index', - 'methods' => ['GET', 'POST'], + 'path' => '/files/', + 'action' => 'Formwork\Panel\Controllers\FilesController@index', + ], + + 'panel.files.upload' => [ + 'path' => '/files/upload/', + 'action' => 'Formwork\Panel\Controllers\FilesController@upload', + 'methods' => ['POST'], ], 'panel.files.delete' => [ From fb07a67150e144827b366bac6a195bf1b8e1e91c Mon Sep 17 00:00:00 2001 From: Giuseppe Criscione <18699708+giuscris@users.noreply.github.com> Date: Sun, 1 Jun 2025 10:27:19 +0200 Subject: [PATCH 5/6] Add missing phpdoc --- formwork/src/Cms/Site.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/formwork/src/Cms/Site.php b/formwork/src/Cms/Site.php index 61f41e9d5..31da5fa23 100644 --- a/formwork/src/Cms/Site.php +++ b/formwork/src/Cms/Site.php @@ -476,6 +476,9 @@ public function load(): void $this->loadRouteAliases(); } + /** + * Get site files + */ public function files(): FileCollection { if (isset($this->files)) { From 8cc1f00b7180541fef2ec685012aeb7ff1929f65 Mon Sep 17 00:00:00 2001 From: Giuseppe Criscione <18699708+giuscris@users.noreply.github.com> Date: Sun, 1 Jun 2025 10:36:11 +0200 Subject: [PATCH 6/6] Restore styled inline file size --- panel/src/scss/components/forms/_forms-upload.scss | 4 ++++ panel/src/ts/components/inputs/upload-input.ts | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/panel/src/scss/components/forms/_forms-upload.scss b/panel/src/scss/components/forms/_forms-upload.scss index 1ee2e0fab..03a6028d2 100644 --- a/panel/src/scss/components/forms/_forms-upload.scss +++ b/panel/src/scss/components/forms/_forms-upload.scss @@ -36,3 +36,7 @@ .form-upload-drop-target.drag > span { background-color: var(--color-base-700); } + +.file-size-inline { + color: var(--color-base-300); +} diff --git a/panel/src/ts/components/inputs/upload-input.ts b/panel/src/ts/components/inputs/upload-input.ts index 770d9efe8..c54d1a8f1 100644 --- a/panel/src/ts/components/inputs/upload-input.ts +++ b/panel/src/ts/components/inputs/upload-input.ts @@ -180,7 +180,7 @@ export class UploadInput { if (this.element.files && this.element.files.length > 0) { const filenames: string[] = []; for (const file of Array.from(this.element.files)) { - filenames.push(`${file.name} (${this.formatFileSize(file.size)})`); + filenames.push(`${file.name} (${this.formatFileSize(file.size)})`); } this.dropTargetLabel.innerHTML = filenames.join(", ");