From 04fb749af3c5bf6700c6c474ddf9c14e0da48e8a Mon Sep 17 00:00:00 2001 From: Pascal Schneider Date: Fri, 21 Jan 2022 15:34:56 +0100 Subject: [PATCH] init --- .gitignore | 8 + README.md | 11 + composer.json | 29 ++ .../20220121091709_AssetsAddAssetsTable.php | 83 +++++ config/app_assets.php | 13 + config/routes.php | 16 + src/Controller/AppController.php | 18 ++ src/Controller/AssetsController.php | 56 ++++ src/Controller/admin/AssetsController.php | 132 ++++++++ src/Enum/ImageSizes.php | 14 + src/Model/Entity/AssetsAsset.php | 166 ++++++++++ src/Model/Table/AssetsAssetsTable.php | 131 ++++++++ src/Plugin.php | 84 +++++ src/Utilities/ImageAsset.php | 297 ++++++++++++++++++ templates/Admin/Assets/add.php | 32 ++ templates/Admin/Assets/edit.php | 39 +++ templates/Admin/Assets/index.php | 79 +++++ templates/Admin/Assets/view.php | 80 +++++ 18 files changed, 1288 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 composer.json create mode 100644 config/Migrations/20220121091709_AssetsAddAssetsTable.php create mode 100644 config/app_assets.php create mode 100644 config/routes.php create mode 100644 src/Controller/AppController.php create mode 100644 src/Controller/AssetsController.php create mode 100644 src/Controller/admin/AssetsController.php create mode 100644 src/Enum/ImageSizes.php create mode 100644 src/Model/Entity/AssetsAsset.php create mode 100644 src/Model/Table/AssetsAssetsTable.php create mode 100644 src/Plugin.php create mode 100644 src/Utilities/ImageAsset.php create mode 100644 templates/Admin/Assets/add.php create mode 100644 templates/Admin/Assets/edit.php create mode 100644 templates/Admin/Assets/index.php create mode 100644 templates/Admin/Assets/view.php diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..244d127 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +/composer.lock +/composer.phar +/phpunit.xml +/.phpunit.result.cache +/phpunit.phar +/config/Migrations/schema-dump-default.lock +/vendor/ +/.idea/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..bd02178 --- /dev/null +++ b/README.md @@ -0,0 +1,11 @@ +# Assets plugin for CakePHP + +## Installation + +You can install this plugin into your CakePHP application using [composer](https://getcomposer.org). + +The recommended way to install composer packages is: + +``` +composer require passchn/cakephp-assets +``` diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..3fe05a0 --- /dev/null +++ b/composer.json @@ -0,0 +1,29 @@ +{ + "name": "passchn/cakephp-assets", + "description": "Asset management plugin for CakePHP", + "type": "cakephp-plugin", + "license": "MIT", + "require": { + "php": ">=8.0", + "cakephp/cakephp": "^4.3", + "josegonzalez/cakephp-upload": "^6.0", + "league/csv": "^9.8", + "nette/finder": "^2.5", + "nette/utils": "^3.2", + "intervention/image": "^2.7" + }, + "require-dev": { + "phpunit/phpunit": "^8.5 || ^9.3" + }, + "autoload": { + "psr-4": { + "Test\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "Test\\Test\\": "tests/", + "Cake\\Test\\": "vendor/cakephp/cakephp/tests/" + } + } +} diff --git a/config/Migrations/20220121091709_AssetsAddAssetsTable.php b/config/Migrations/20220121091709_AssetsAddAssetsTable.php new file mode 100644 index 0000000..b6b1e2f --- /dev/null +++ b/config/Migrations/20220121091709_AssetsAddAssetsTable.php @@ -0,0 +1,83 @@ +table('assets_assets', ['id' => false, 'primary_key' => ['id']]) + ->addColumn('id', 'uuid', [ + 'default' => '', + 'limit' => null, + 'null' => false, + ]) + ->addColumn('title', 'string', [ + 'default' => '', + 'limit' => 255, + 'null' => false, + ]) + ->addColumn('description', 'text', [ + 'default' => null, + 'limit' => null, + 'null' => true, + ]) + ->addColumn('category', 'string', [ + 'default' => null, + 'limit' => 255, + 'null' => true, + ]) + ->addColumn('filename', 'string', [ + 'default' => '', + 'limit' => 255, + 'null' => false, + ]) + ->addColumn('directory', 'string', [ + 'default' => '', + 'limit' => 255, + 'null' => false, + ]) + ->addColumn('mimetype', 'string', [ + 'default' => '', + 'limit' => 255, + 'null' => false, + ]) + ->addColumn('filesize', 'string', [ + 'default' => '', + 'limit' => 255, + 'null' => false, + ]) + ->addColumn('created', 'timestamp', [ + 'default' => null, + 'limit' => null, + 'null' => true, + ]) + ->addColumn('modified', 'timestamp', [ + 'default' => null, + 'limit' => null, + 'null' => true, + ]) + ->create(); + } + + /** + * Down Method. + * + * More information on this method is available here: + * https://book.cakephp.org/phinx/0/en/migrations.html#the-down-method + * @return void + */ + public function down() + { + + $this->table('assets_assets')->drop()->save(); + } +} diff --git a/config/app_assets.php b/config/app_assets.php new file mode 100644 index 0000000..678b876 --- /dev/null +++ b/config/app_assets.php @@ -0,0 +1,13 @@ + [ + 'AssetsTable' => [ + 'DisplayField' => 'title', + 'Behaviors' => [], + ] + ], +]; diff --git a/config/routes.php b/config/routes.php new file mode 100644 index 0000000..6be492c --- /dev/null +++ b/config/routes.php @@ -0,0 +1,16 @@ +plugin( + 'Assets', + ['path' => '/assets'], + function (RouteBuilder $routes) { + $routes->setRouteClass(DashedRoute::class); + + $routes->fallbacks(DashedRoute::class); + }); +}; + diff --git a/src/Controller/AppController.php b/src/Controller/AppController.php new file mode 100644 index 0000000..d8b1a59 --- /dev/null +++ b/src/Controller/AppController.php @@ -0,0 +1,18 @@ +Assets = $this->fetchTable('Assets.AssetsAssets'); + } +} diff --git a/src/Controller/AssetsController.php b/src/Controller/AssetsController.php new file mode 100644 index 0000000..fadd735 --- /dev/null +++ b/src/Controller/AssetsController.php @@ -0,0 +1,56 @@ +Assets->get($id, [ + 'contain' => [], + ]); + + $this->set(compact('asset')); + } + + /** + * @return void + * + * Add ?download=1 to URL to download instead of view the file. + */ + public function download(string $id) + { + $asset = $this->Assets->get($id); + + $disposition = $this->getRequest()->getQuery('download') ? 'attachment' : 'inline'; + + header("Content-type: " . $asset->mimetype); + header("Content-Disposition: $disposition; filename=\"{$asset->public_filename}\""); + header("Pragma: no-cache"); + header("Expires: 0"); + + try { + echo $asset->read(); + } catch (\Exception $e) { + echo __("Die Datei kann nicht geöffnet werden. "); + } + + exit; + } +} diff --git a/src/Controller/admin/AssetsController.php b/src/Controller/admin/AssetsController.php new file mode 100644 index 0000000..b943747 --- /dev/null +++ b/src/Controller/admin/AssetsController.php @@ -0,0 +1,132 @@ +paginate($this->Assets); + + $this->set(compact('assets')); + } + + /** + * View method + * + * @param string|null $id Asset id. + * @return \Cake\Http\Response|null|void Renders view + * @throws \Cake\Datasource\Exception\RecordNotFoundException When record not found. + */ + public function view($id = null) + { + $asset = $this->Assets->get($id, [ + 'contain' => [], + ]); + + $this->set(compact('asset')); + } + + /** + * Add method + * + * @return \Cake\Http\Response|null|void Redirects on successful add, renders view otherwise. + */ + public function add() + { + $asset = $this->Assets->newEmptyEntity(); + if ($this->request->is('post')) { + $asset = $this->Assets->patchEntity($asset, $this->request->getData()); + if ($this->Assets->save($asset)) { + $this->Flash->success(__('The asset has been saved.')); + + return $this->redirect(['action' => 'index']); + } + $this->Flash->error(__('The asset could not be saved. Please, try again.')); + } + $this->set(compact('asset')); + } + + /** + * Edit method + * + * @param string|null $id Asset id. + * @return \Cake\Http\Response|null|void Redirects on successful edit, renders view otherwise. + * @throws \Cake\Datasource\Exception\RecordNotFoundException When record not found. + */ + public function edit($id = null) + { + $asset = $this->Assets->get($id, [ + 'contain' => [], + ]); + if ($this->request->is(['patch', 'post', 'put'])) { + $asset = $this->Assets->patchEntity($asset, $this->request->getData()); + if ($this->Assets->save($asset)) { + $this->Flash->success(__('The asset has been saved.')); + + return $this->redirect(['action' => 'index']); + } + $this->Flash->error(__('The asset could not be saved. Please, try again.')); + } + $this->set(compact('asset')); + } + + /** + * Delete method + * + * @param string|null $id Asset id. + * @return \Cake\Http\Response|null|void Redirects to index. + * @throws \Cake\Datasource\Exception\RecordNotFoundException When record not found. + */ + public function delete($id = null) + { + $this->request->allowMethod(['post', 'delete']); + $asset = $this->Assets->get($id); + if ($this->Assets->delete($asset)) { + $this->Flash->success(__('The asset has been deleted.')); + } else { + $this->Flash->error(__('The asset could not be deleted. Please, try again.')); + } + + return $this->redirect(['action' => 'index']); + } + + /** + * @return void + * + * Add ?download=1 to URL to download instead of view the file. + */ + public function download(string $id) + { + $asset = $this->Assets->get($id); + + $disposition = $this->getRequest()->getQuery('download') ? 'attachment' : 'inline'; + + header("Content-type: " . $asset->mimetype); + header("Content-Disposition: $disposition; filename=\"{$asset->public_filename}\""); + header("Pragma: no-cache"); + header("Expires: 0"); + + try { + echo $asset->read(); + } catch (\Exception $e) { + echo __("Die Datei kann nicht geöffnet werden. "); + } + + exit; + } +} diff --git a/src/Enum/ImageSizes.php b/src/Enum/ImageSizes.php new file mode 100644 index 0000000..33bdd21 --- /dev/null +++ b/src/Enum/ImageSizes.php @@ -0,0 +1,14 @@ + true, + 'description' => true, + 'category' => true, + 'filename' => true, + 'directory' => true, + 'mimetype' => true, + 'filesize' => true, + 'created' => true, + 'modified' => true, + ]; + + public function exists(): bool + { + return file_exists($this->absolute_path); + } + + protected function _getAbsolutePath(): string + { + return ROOT . DS . $this->directory . DS . $this->filename; + } + + public function read(): string + { + if (!$this->exists()) { + throw new \Exception("The File {$this->filename} for the Asset #{$this->id} ({$this->title}) does not exist in {$this->directory}."); + } + + return FileSystem::read($this->absolute_path); + } + + protected function _getPublicFilename(): string + { + return Strings::webalize($this->title) . '.' . $this->filetype; + } + + protected function _getFiletype(): ?string + { + return Strings::after($this->filename, '.', -1); + } + + public function isViewableInBrowser(): bool + { + return $this->exists() && + match (Strings::before($this->mimetype, '/')) { + 'image', 'video' => true, + default => false, + } || + match (Strings::after($this->mimetype, '/')) { + 'pdf', 'json' => true, + default => false, + }; + } + + public function isImage(): bool + { + return $this->exists() + && Strings::before($this->mimetype, '/') === 'image' + && !Strings::contains((string)Strings::after($this->mimetype, '/'), 'svg'); + } + + public function isPlainText(): bool + { + return $this->exists() + && Strings::before($this->mimetype, '/') === 'text'; + } + + public function getImage(int $quality=90): ImageAsset + { + if (!$this->isImage()) { + throw new \Exception("Cannot call Asset::getImage() on #{$this->id} ($this->title) with MimeType {$this->mimetype}."); + } + + return new ImageAsset($this, $quality); + } + + public function getThumbnail(): ?string + { + if (!$this->exists()) { + return '' . __("Datei nicht gefunden. ") . ''; + } + + if ($this->isImage()) { + return $this->getImage(65)->scaleWidth(ImageSizes::THMB)->setCSS('asset-thumbnail')->toJpg()->getHTML(); + } + + return null; + } + + protected function _getFullTitle(): string + { + return $this->title . ' (' . $this->mimetype . ')'; + } + + public function getFileSizeInfo(): string + { + $filesize = (int)$this->filesize; + + switch ($filesize) { + case $filesize > 500000000: + return round($filesize / 1000000000, 1) . ' GB'; + case $filesize > 500000: + return round($filesize / 1000000, 1) . ' MB'; + case $filesize > 500: + return round($filesize / 1000, 1) . ' kB'; + default: + return $filesize . ' Byte'; + } + } + + /** + * @throws \League\Csv\InvalidArgument + * @throws \League\Csv\Exception + */ + public function getCsvReader(array $options = []): Reader + { + if ($this->filetype !== 'csv') { + throw new \Exception("The Asset {$this->title} is not a csv."); + } + + $reader = Reader::createFromString($this->read()); + $reader->setDelimiter($options['csv_delimiter'] ?? ';'); + $reader->setHeaderOffset($options['csv_header_offset'] ?? 0); + + return $reader; + } +} diff --git a/src/Model/Table/AssetsAssetsTable.php b/src/Model/Table/AssetsAssetsTable.php new file mode 100644 index 0000000..b0325ab --- /dev/null +++ b/src/Model/Table/AssetsAssetsTable.php @@ -0,0 +1,131 @@ +setTable('assets_assets'); + $this->setDisplayField(Configure::read('AssetsPlugin.AssetsTable.DisplayField', 'title')); + $this->setPrimaryKey('id'); + + $this->addBehavior('Timestamp'); + + $this->addBehavior('Josegonzalez/Upload.Upload', [ + 'filename' => [ + 'nameCallback' => function ($table, EntityInterface $entity, UploadedFile $data, $field, $settings + ): string { + + if (method_exists($this, 'fileNameCallback')) { + return $this->fileNameCallback($table, $entity, $data, $field, $settings); + } + + $now = new FrozenTime(); + $pathInfo = pathinfo((string)$data->getClientFilename()); + + return $now->format('ymd') . '-' . $now->format('His') . '_' . $pathInfo['basename']; + }, + 'fields' => [ + 'dir' => 'directory', + 'size' => 'filesize', + 'type' => 'mimetype', + ], + 'path' => self::ASSETS_DIR, + ], + ]); + + foreach (Configure::read('AssetsPlugin.AssetsTable.Behaviors') ?? [] as $behavior) { + $this->addBehavior($behavior); + } + } + + /** + * Default validation rules. + * + * @param \Cake\Validation\Validator $validator Validator instance. + * @return \Cake\Validation\Validator + */ + public function validationDefault(Validator $validator): Validator + { + $validator + ->uuid('id') + ->allowEmptyString('id', null, 'create'); + + $validator + ->scalar('title') + ->maxLength('title', 255) + ->notEmptyString('title'); + + $validator + ->scalar('description') + ->allowEmptyString('description'); + + $validator + ->scalar('category') + ->maxLength('category', 255) + ->allowEmptyString('category'); + + $validator + ->scalar('directory') + ->maxLength('directory', 255) + ->notEmptyString('directory'); + + $validator + ->scalar('mimetype') + ->maxLength('mimetype', 255) + ->notEmptyString('mimetype'); + + $validator + ->scalar('filesize') + ->maxLength('filesize', 255) + ->notEmptyString('filesize'); + + return $validator; + } + + public function beforeFind(EventInterface $e, Query $query, \ArrayObject $options, $primary) + { + return $query->orderDesc('modified'); + } +} diff --git a/src/Plugin.php b/src/Plugin.php new file mode 100644 index 0000000..83b605a --- /dev/null +++ b/src/Plugin.php @@ -0,0 +1,84 @@ +plugin( + 'Assets', + ['path' => '/assets'], + function (RouteBuilder $builder) { + // Add custom routes here + + $builder->fallbacks(); + } + ); + parent::routes($routes); + } + + /** + * Add middleware for the plugin. + * + * @param \Cake\Http\MiddlewareQueue $middleware The middleware queue to update. + * @return \Cake\Http\MiddlewareQueue + */ + public function middleware(MiddlewareQueue $middlewareQueue): MiddlewareQueue + { + // Add your middlewares here + + return $middlewareQueue; + } + + /** + * Add commands for the plugin. + * + * @param \Cake\Console\CommandCollection $commands The command collection to update. + * @return \Cake\Console\CommandCollection + */ + public function console(CommandCollection $commands) : CommandCollection + { + // Add your commands here + + $commands = parent::console($commands); + + return $commands; + } +} diff --git a/src/Utilities/ImageAsset.php b/src/Utilities/ImageAsset.php new file mode 100644 index 0000000..da2f189 --- /dev/null +++ b/src/Utilities/ImageAsset.php @@ -0,0 +1,297 @@ +modifications = []; + $this->image = null; + $this->format = null; + $this->css = "image-asset"; + $this->quality = $quality; + + $this->trackModification('constructor', ['quality' => $quality], true); + } + + /** + * Calls the Intervention API's "widen" method. + * Preserves aspect ratio. + */ + public function scaleWidth(int $width): ImageAsset + { + $this->trackModification('widen', [$width]); + return $this; + } + + public function toWebp(): ImageAsset + { + $this->trackModification('encode', ['webp']); + $this->format = 'webp'; + return $this; + } + + public function toJpg(): ImageAsset + { + $this->trackModification('encode', ['jpg']); + $this->format = 'jpg'; + return $this; + } + + /** + * Set CSS classes for __toString() HTML output + */ + public function setCSS(string $css): ImageAsset + { + $this->css = $css; + return $this; + } + + /** + * e.g. + * ImageAsset::applyFilter(EpaperFilter::class, ['kombi']) + * + * $filer is the className (string) of the Filter. + * $properties will be passed after the ImageManager instance + * when calling the Filter's constructor. + * + * !! Don't pass an ImageManager instance, only string or int properties. + */ + public function applyFilter(string $filter, array $properties = []): ImageAsset + { + $this->trackModification('filter_' . $filter, $properties); + return $this; + } + + /** + * Experimental. + * Wrapper for Intervention API methods. + * + * @link https://image.intervention.io/v2 + */ + public function modify(string $method, string|int ...$params): ImageAsset + { + $this->trackModification($method, $params); + return $this; + } + + /** + * Get the publicly accessible path (from /webroot) + */ + public function getPath(): string + { + $SplFileInfo = $this->getFile(); + if ($SplFileInfo) { + return $this->getRelativePath($SplFileInfo); + } + + if ($this->render()) { + return $this->getRelativePath(); + } + + throw new \Exception("Cannot get path for ImageAsset for Asset #{$this->asset->id}"); + } + + /** + * Renders the ImageAsset as a HTML element. + */ + public function getHTML(): string + { + $image = null; + $path = $this->getPath(); + $html = new HtmlHelper(new AppView()); + + if (!$this->image) { + $manager = new ImageManager(); + $image = $manager->make($this->getAbsolutePath()); + } + + return $html->image($path, [ + 'alt' => $this->asset->description ?: $this->asset->title, + 'width' => $this->image?->width() ?? $image?->width() ?? 255, + 'heigh' => $this->image?->height() ?? $image?->height() ?? 255, + 'loading' => 'lazy', + 'class' => $this->css, + ]); + } + + public function __toString(): string + { + return $this->getHTML(); + } + + /** + * set $noApi to true if the modification should not be called in + * applyModifications(). It will just be relevant for the ModificationHash. + */ + private function trackModification(string $method, string|int|array $params, bool $noApi = false): void + { + if ($noApi) { + $this->modifications['noApi'][$method] = $params; + return; + } + + $this->modifications[$method] = $params; + } + + private function getRelativePath(?\SplFileInfo $file = null): string + { + if ($file) { + + return DS . self::OUTPUT_DIRECTORY . $file->getFilename(); + } + + $file = $this->getFile(); + if ($file) { + + return DS . self::OUTPUT_DIRECTORY . $file->getFilename(); + } + + if ($this->image) { + + $mimeType = $this->image->mime(); + $format = $this->format ?: Strings::after($mimeType, '/'); + + if (!$format) { + throw new \Exception("Cannot read format or mimetype for modified version of Asset #{$this->asset->id}. "); + } + + /** + * Create a new filename. + */ + return DS . self::OUTPUT_DIRECTORY . $this->getAssetIdentifier() . '_' . $this->getModificationHash() . '.' . $format; + } + + throw new \Exception("Cannot get Path for an Image that does not yet exist. The render() method must be called before getRelativePath(). "); + } + + private function getAbsolutePath(): string + { + return WWW_ROOT . ltrim($this->getRelativePath(), DS); + } + + /** + * Returns an md5 sum based on the Asset-ID and the modifications. + */ + private function getModificationHash(): string + { + return md5($this->asset->id . $this->asset->modified?->getTimestamp() . Json::encode($this->modifications)); + } + + private function getAssetIdentifier(): string + { + $identifier = Strings::webalize($this->asset->title); + $identifier = Strings::substring($identifier, 0, 12); + $identifier = $identifier . Strings::substring(md5($this->asset->id), 0, 3); + + return $identifier; + } + + private function getFile(): ?\SplFileInfo + { + if (!is_dir(self::OUTPUT_DIRECTORY)) { + return null; + } + + $files = Finder::findFiles($this->getAssetIdentifier() . '_' . $this->getModificationHash() . '*') + ->in(self::OUTPUT_DIRECTORY); + + foreach ($files as $path => $SplFileInfo) { + + return $SplFileInfo; + } + + return null; + } + + /** + * Renders the Image and returns the + * relative path from OUTPUT_DIRECTORY + */ + private function render(): bool + { + $manager = new ImageManager([ + // TODO (Gong Live) 'driver' => 'imagick', + 'driver' => 'gd' + ]); + + $image = $manager->make($this->asset->absolute_path); + $image = $this->applyModifications($image, $manager); + $this->image = $image; + + FileSystem::createDir(self::OUTPUT_DIRECTORY); + + $image->save($this->getAbsolutePath(), $this->quality, $this->format); + + return true; + } + + private function applyModifications(Image $image, ImageManager $manager): Image + { + $modifications = $this->modifications; + unset($modifications['noApi']); + + foreach ($modifications as $method => $params) { + + if (Strings::contains($method, 'filter_')) { + $filterClassName = Strings::after($method, 'filter_') ?? ''; + if (!class_exists($filterClassName)) { + throw new \Exception("Filter {$filterClassName} does not exist. "); + } + + /** + * @var FilterInterface $filter + */ + $filter = new $filterClassName($manager, ...$params); + $image = $filter->applyFilter($image); + continue; + } + + $params = is_array($params) ? $params : [$params]; + $image->{$method}(...$params); + } + + return $image; + } +} diff --git a/templates/Admin/Assets/add.php b/templates/Admin/Assets/add.php new file mode 100644 index 0000000..d1374ba --- /dev/null +++ b/templates/Admin/Assets/add.php @@ -0,0 +1,32 @@ + +
+ +
+
+ Form->create($asset, ['type' => 'file']) ?> +
+ + Form->control('title'); + echo $this->Form->control('description'); + echo $this->Form->control('category', [ + 'options' => \App\Enum\AssetCategories::getList() + ]); + echo $this->Form->control('filename', ['type' => 'file']); + ?> +
+ Form->button(__('Submit')) ?> + Form->end() ?> +
+
+
diff --git a/templates/Admin/Assets/edit.php b/templates/Admin/Assets/edit.php new file mode 100644 index 0000000..47cc457 --- /dev/null +++ b/templates/Admin/Assets/edit.php @@ -0,0 +1,39 @@ + +
+ +
+
+ Form->create($asset, ['type' => 'file']) ?> +
+ + getThumbnail() ?> + Form->control('title'); + echo $this->Form->control('description'); + echo $this->Form->control('category', [ + 'options' => \App\Enum\AssetCategories::getList() + ]); + echo $this->Form->control('filename', ['type' => 'file']); + ?> + +
+ Form->button(__('Submit')) ?> + Form->end() ?> +
+
+
diff --git a/templates/Admin/Assets/index.php b/templates/Admin/Assets/index.php new file mode 100644 index 0000000..54bbf82 --- /dev/null +++ b/templates/Admin/Assets/index.php @@ -0,0 +1,79 @@ + +
+ Html->link(__('New Asset'), ['action' => 'add'], ['class' => 'button float-right']) ?> +

+
+ + + + + + + + + + + + + + + + + + + + + +
+ Paginator->sort('title', 'Titel') ?> / + Paginator->sort('filesize', 'Größe') ?> + + Paginator->sort('category', 'Kat.') ?> / + Paginator->sort('filename', 'Datei') ?> + + Paginator->sort('created', 'Erstellt') ?> / + Paginator->sort('modified', 'Geändert') ?> + Vorschau
+ title) ?> +
+ + getFileSizeInfo() ?> + +
+

+ category) ?> +

+

+ filename) ?> +

+
+

+ created) ?> +

+

+ modified) ?> +

+
+ Html->link($asset->getThumbnail() ?: 'Öffnen', ['action' => 'download', $asset->id, '?' => ['download' => $asset->isViewableInBrowser() ? 0 : 1]], ['escape' => false]) ?> + + Html->link(__('View'), ['action' => 'view', $asset->id]) ?> + Html->link(__('Edit'), ['action' => 'edit', $asset->id]) ?> + Form->postLink(__('Delete'), ['action' => 'delete', $asset->id], ['confirm' => __('Are you sure you want to delete # {0}?', $asset->id)]) ?> +
+
+
+
    + Paginator->first('<< ' . __('first')) ?> + Paginator->prev('< ' . __('previous')) ?> + Paginator->numbers() ?> + Paginator->next(__('next') . ' >') ?> + Paginator->last(__('last') . ' >>') ?> +
+

Paginator->counter(__('Page {{page}} of {{pages}}, showing {{current}} record(s) out of {{count}} total')) ?>

+
+
diff --git a/templates/Admin/Assets/view.php b/templates/Admin/Assets/view.php new file mode 100644 index 0000000..0d827dd --- /dev/null +++ b/templates/Admin/Assets/view.php @@ -0,0 +1,80 @@ + +
+ +
+
+

title) ?>

+ isImage()): ?> +

+ getImage(65)->setCSS("my-class randcom-class")->scaleWidth(\Assets\Enum\ImageSizes::SM)->toJpg() ?> +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
id) ?>
title) ?>
category) ?>
filename) ?>
directory) ?>
mimetype) ?>
getFileSizeInfo()) ?>
created) ?>
modified) ?>
+
+ +
+ description); ?> +
+
+ isPlainText()): ?> +
+ +
+ AssetContent->plainTextPreview($asset) ?> +
+
+ +
+
+