diff --git a/composer.json b/composer.json index e99201b..8a0691c 100644 --- a/composer.json +++ b/composer.json @@ -16,11 +16,13 @@ "ext-ctype": "*", "ext-hash": "*", "ext-iconv": "*", + "ext-json": "*", "babdev/pagerfanta-bundle": "^2.4", "excelwebzone/recaptcha-bundle": "^1.5", "sensio/framework-extra-bundle": "^5.1", "sentry/sentry-symfony": "^3.4", "suin/php-rss-writer": "^1.6", + "symfony/asset": "^4.1", "symfony/console": "^4.1", "symfony/dotenv": "^4.1", "symfony/expression-language": "^4.1", diff --git a/composer.lock b/composer.lock index 0bebd82..4ba7801 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "1e62b51a6d3895dcabaf9c263dddca29", + "content-hash": "e5b436812dc4f90d4d85a90807e00c18", "packages": [ { "name": "babdev/pagerfanta-bundle", @@ -3625,6 +3625,71 @@ ], "time": "2017-07-13T10:47:50+00:00" }, + { + "name": "symfony/asset", + "version": "v4.4.18", + "source": { + "type": "git", + "url": "https://github.com/symfony/asset.git", + "reference": "7339980f70621f26db6208a75a8ee2a48a7ede5b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/asset/zipball/7339980f70621f26db6208a75a8ee2a48a7ede5b", + "reference": "7339980f70621f26db6208a75a8ee2a48a7ede5b", + "shasum": "" + }, + "require": { + "php": ">=7.1.3" + }, + "require-dev": { + "symfony/http-foundation": "^3.4|^4.0|^5.0", + "symfony/http-kernel": "^3.4|^4.0|^5.0" + }, + "suggest": { + "symfony/http-foundation": "" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Asset\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Asset Component", + "homepage": "https://symfony.com", + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-12-13T22:21:11+00:00" + }, { "name": "symfony/cache", "version": "v5.2.0", @@ -4738,16 +4803,16 @@ }, { "name": "symfony/flex", - "version": "v1.10.0", + "version": "v1.11.0", "source": { "type": "git", "url": "https://github.com/symfony/flex.git", - "reference": "e38520236bdc911c2f219634b485bc328746e980" + "reference": "ceb2b4e612bd0b4bb36a4d7fb2e800c861652f48" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/flex/zipball/e38520236bdc911c2f219634b485bc328746e980", - "reference": "e38520236bdc911c2f219634b485bc328746e980", + "url": "https://api.github.com/repos/symfony/flex/zipball/ceb2b4e612bd0b4bb36a4d7fb2e800c861652f48", + "reference": "ceb2b4e612bd0b4bb36a4d7fb2e800c861652f48", "shasum": "" }, "require": { @@ -4757,6 +4822,7 @@ "require-dev": { "composer/composer": "^1.0.2|^2.0", "symfony/dotenv": "^4.4|^5.0", + "symfony/filesystem": "^4.4|^5.0", "symfony/phpunit-bridge": "^4.4|^5.0", "symfony/process": "^3.4|^4.4|^5.0" }, @@ -4797,7 +4863,7 @@ "type": "tidelift" } ], - "time": "2020-11-05T10:56:45+00:00" + "time": "2020-12-03T10:57:35+00:00" }, { "name": "symfony/form", @@ -8622,7 +8688,8 @@ "php": ">=7.4.0", "ext-ctype": "*", "ext-hash": "*", - "ext-iconv": "*" + "ext-iconv": "*", + "ext-json": "*" }, "platform-dev": [], "plugin-api-version": "1.1.0" diff --git a/config/services.yaml b/config/services.yaml index 4e114fc..4c41af6 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -20,7 +20,7 @@ services: App\: resource: '../src/*' - exclude: '../src/{Api/V1/{DTO},Magnetico/{Entity,Migrations},Entity,FormRequest,Migrations,Tests,Kernel.php}' + exclude: '../src/{Api/V1/{DTO},Magnetico/{Entity,Migrations},Entity,FormRequest,Migrations,Tests,View,Kernel.php}' # Use array in exclude config from Symfony 4.2 #- '../src/Api/V1/{DTO}' #- '../src/Magnetico/{Entity,Migrations}' diff --git a/public/assets/bstreeview/css/bstreeview.min.css b/public/assets/bstreeview/css/bstreeview.min.css new file mode 100644 index 0000000..e5623f9 --- /dev/null +++ b/public/assets/bstreeview/css/bstreeview.min.css @@ -0,0 +1,10 @@ +/* + @preserve + bstreeview.css + Version: 1.2.0 + Authors: Sami CHNITER + Copyright 2020 + License: Apache License 2.0 + Project: https://github.com/chniter/bstreeview +*/ +.bstreeview{position:relative;display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;min-width:0;word-wrap:break-word;background-color:#fff;background-clip:border-box;border:1px solid rgba(0,0,0,.125);border-radius:.25rem;padding:0;overflow:hidden}.bstreeview .list-group{margin-bottom:0}.bstreeview .list-group-item{border-radius:0;border-width:1px 0 0 0;padding-top:.5rem;padding-bottom:.5rem;cursor:pointer}.bstreeview .list-group-item:hover{background-color:#dee2e6}.bstreeview>.list-group-item:first-child{border-top-width:0}.bstreeview .state-icon{margin-right:8px}.bstreeview .item-icon{margin-right:5px} \ No newline at end of file diff --git a/public/assets/bstreeview/js/bstreeview.min.js b/public/assets/bstreeview/js/bstreeview.min.js new file mode 100644 index 0000000..2af8ea1 --- /dev/null +++ b/public/assets/bstreeview/js/bstreeview.min.js @@ -0,0 +1,10 @@ +/* + @preserve + bstreeview.js + Version: 1.2.0 + Authors: Sami CHNITER + Copyright 2020 + License: Apache License 2.0 + Project: https://github.com/chniter/bstreeview +*/ +!function (t, e, i, s) { "use strict"; var n = { expandIcon: "fa fa-angle-down fa-fw", collapseIcon: "fa fa-angle-right fa-fw", indent: 1.25, parentsMarginLeft: "1.25rem", openNodeLinkOnNewTab: !0 }, a = '
', d = '
', o = '', r = ''; function l(e, i) { this.element = e, this.itemIdPrefix = e.id + "-item-", this.settings = t.extend({}, n, i), this.init() } t.extend(l.prototype, { init: function () { this.tree = [], this.nodes = [], this.settings.data && (this.settings.data.isPrototypeOf(String) && (this.settings.data = t.parseJSON(this.settings.data)), this.tree = t.extend(!0, [], this.settings.data), delete this.settings.data), t(this.element).addClass("bstreeview"), this.initData({ nodes: this.tree }); var i = this; this.build(t(this.element), this.tree, 0), t(this.element).on("click", ".list-group-item", function (s) { t(".state-icon", this).toggleClass(i.settings.expandIcon).toggleClass(i.settings.collapseIcon), s.target.hasAttribute("href") && (i.settings.openNodeLinkOnNewTab ? e.open(s.target.getAttribute("href"), "_blank") : e.location = s.target.getAttribute("href")) }) }, initData: function (e) { if (e.nodes) { var i = e, s = this; t.each(e.nodes, function (t, e) { e.nodeId = s.nodes.length, e.parentId = i.nodeId, s.nodes.push(e), e.nodes && s.initData(e) }) } }, build: function (e, i, s) { var n = this, l = n.settings.parentsMarginLeft; s > 0 && (l = (n.settings.indent + s * n.settings.indent).toString() + "rem;"), s += 1, t.each(i, function (i, g) { var h = t(a).attr("data-target", "#" + n.itemIdPrefix + g.nodeId).attr("style", "padding-left:" + l).attr("aria-level", s); if (g.nodes) { var c = t(o).addClass(n.settings.collapseIcon); h.append(c) } if (g.icon) { var f = t(r).addClass(g.icon); h.append(f) } if (h.append(g.text), g.href && h.attr("href", g.href), g.class && h.addClass(g.class), g.id && h.attr("id", g.id), e.append(h), g.nodes) { var p = t(d).attr("id", n.itemIdPrefix + g.nodeId); e.append(p), n.build(p, g.nodes, s) } }) } }), t.fn.bstreeview = function (e) { return this.each(function () { t.data(this, "plugin_bstreeview") || t.data(this, "plugin_bstreeview", new l(this, e)) }) } }(jQuery, window, document); \ No newline at end of file diff --git a/src/Controller/TorrentController.php b/src/Controller/TorrentController.php index 903f0e6..23a9bff 100644 --- a/src/Controller/TorrentController.php +++ b/src/Controller/TorrentController.php @@ -5,6 +5,7 @@ namespace App\Controller; use App\Magnetico\Entity\Torrent; use App\Search\TorrentSearcher; use App\Pager\PagelessDoctrineORMAdapter; +use App\View\Torrent\FileTreeNode; use Pagerfanta\Pagerfanta; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\{Request, Response}; @@ -38,6 +39,7 @@ class TorrentController extends AbstractController { return $this->render('torrent_show.html.twig', [ 'torrent' => $torrent, + 'files' => FileTreeNode::createFromTorrent($torrent), ]); } } \ No newline at end of file diff --git a/src/Helper/BstreeviewFileTreeBuilder.php b/src/Helper/BstreeviewFileTreeBuilder.php new file mode 100644 index 0000000..b9da796 --- /dev/null +++ b/src/Helper/BstreeviewFileTreeBuilder.php @@ -0,0 +1,67 @@ +humanizer = $humanizer; + } + + public function buildFileTreeDataArrayFromTorrent( + Torrent $torrent, + ?string $fileIcon = self::DEFAULT_FILE_ICON, + ?string $dirIcon = self::DEFAULT_DIR_ICON + ): array { + return $this->buildFileTreeDataArray( + FileTreeNode::createFromTorrent($torrent), + $fileIcon, + $dirIcon + ); + } + + public function buildFileTreeDataArray( + FileTreeNode $node, + ?string $fileIcon = self::DEFAULT_FILE_ICON, + ?string $dirIcon = self::DEFAULT_DIR_ICON + ): array { + $data = []; + + foreach ($node->getChildren() as $name => $child) { + $element = [ + 'text' => ''.$name.'', + ]; + + if ($child->isDirectory()) { + $element['nodes'] = $this->buildFileTreeDataArray($child, $fileIcon, $dirIcon); + + if ($dirIcon) { + $element['icon'] = $dirIcon; + } + + // Adding number of chilren + $element['text'] .= ' ['.$child->countChildren().']'; + } else { + if ($fileIcon) { + $element['icon'] = $fileIcon; + } + + // Adding file size. + $element['text'] .= ' ('.$this->humanizer->humanize($child->getSize()).')'; + } + + $data[] = $element; + } + + return $data; + } +} \ No newline at end of file diff --git a/src/Twig/HumanReadableSizeExtension.php b/src/Helper/FileSizeHumanizer.php similarity index 73% rename from src/Twig/HumanReadableSizeExtension.php rename to src/Helper/FileSizeHumanizer.php index 212b418..a2106c4 100644 --- a/src/Twig/HumanReadableSizeExtension.php +++ b/src/Helper/FileSizeHumanizer.php @@ -1,11 +1,8 @@ binaryPrefix = $useBinaryPrefix; } - public function getFilters(): array - { - return [ - new TwigFilter('readable_size', [$this, 'humanizeSize']), - ]; - } - - public function humanizeSize(int $bytes, int $decimals = 2, bool $forceBinary = false): string + public function humanize(int $bytes, int $decimals = 2, bool $forceBinary = false): string { $bytesString = (string) $bytes; @@ -49,4 +39,4 @@ class HumanReadableSizeExtension extends AbstractExtension return sprintf("%.{$decimals}f %s", $bytes / ($sizeDivider ** $suffixIndex), $suffix); } -} \ No newline at end of file +} diff --git a/src/Twig/FileSizeHumanizerExtension.php b/src/Twig/FileSizeHumanizerExtension.php new file mode 100644 index 0000000..ca633e0 --- /dev/null +++ b/src/Twig/FileSizeHumanizerExtension.php @@ -0,0 +1,24 @@ +humanizer = $humanizer; + } + + public function getFilters(): array + { + return [ + new TwigFilter('humanize_size', [$this->humanizer, 'humanize']), + ]; + } +} \ No newline at end of file diff --git a/src/Twig/FileTreeExtension.php b/src/Twig/FileTreeExtension.php new file mode 100644 index 0000000..9a8ba89 --- /dev/null +++ b/src/Twig/FileTreeExtension.php @@ -0,0 +1,25 @@ +builder = $builder; + } + + public function getFilters() + { + return [ + new TwigFilter('file_tree', [$this->builder, 'buildFileTreeDataArray']), + new TwigFilter('torrent_file_tree', [$this->builder, 'buildFileTreeDataArrayFromTorrent']), + ]; + } +} diff --git a/src/View/Torrent/FileTreeNode.php b/src/View/Torrent/FileTreeNode.php new file mode 100644 index 0000000..8b412a9 --- /dev/null +++ b/src/View/Torrent/FileTreeNode.php @@ -0,0 +1,146 @@ +name = $name; + } + + public static function createFromTorrent(Torrent $torrent): FileTreeNode + { + $node = new static($torrent->getName()); + + foreach ($torrent->getFiles() as $file) { + $node->addFileToPath($file->getPath(), $file); + } + + return $node; + } + + public static function createFromFile(string $name, File $file, ?FileTreeNode $parent): FileTreeNode + { + $node = new static($name); + $node->isDir = false; + $node->size = $file->getSize(); + $node->parent = $parent; + + return $node; + } + + public function addFileToPath(string $path, File $file): void + { + $path = ltrim($path, '/'); + + $pathParts = explode('/', $path); + + // If we have file only file and not a tree. + if (1 === count($pathParts)) { + $this->addChild($path, FileTreeNode::createFromFile($path, $file, $this)); + + return; + } + + // If we have a file in a tree. + $childNodeName = array_shift($pathParts); + $childNodeChildPath = implode('/', $pathParts); + + if (!$this->hasChild($childNodeName)) { + $childNode = new static($childNodeName); + $childNode->parent = $this; + $this->addChild($childNodeName, $childNode); + } else { + $childNode = $this->getChild($childNodeName); + } + + $childNode->addFileToPath($childNodeChildPath, $file); + } + + public function addChild(string $name, FileTreeNode $element, bool $overwriteDuplicates = false): void + { + if (!$overwriteDuplicates && array_key_exists($name, $this->children)) { + throw new \RuntimeException(sprintf( + 'Child \'%s\' already exist.', + $name + )); + } + + $this->children[$name] = $element; + } + + public function hasChild(string $name): bool + { + return array_key_exists($name, $this->children); + } + + /** + * @param string $name + * + * @return FileTreeNode|File + */ + public function getChild(string $name) + { + if (!array_key_exists($name, $this->children)) { + throw new \InvalidArgumentException(sprintf( + 'Node has no \'%s\' child.', + $name + )); + } + + return $this->children[$name]; + } + + /** + * @return File[]|FileTreeNode[] + */ + public function getChildren(bool $dirsFirst = true): array + { + if (!$dirsFirst) { + return $this->children; + } + + $dirs = []; + $files = []; + + foreach ($this->children as $name => $child) { + if ($child instanceof FileTreeNode) { + $dirs[$name] = $child; + } elseif ($child instanceof File) { + $files[] = $child; + } + } + + return array_merge($dirs, $files); + } + + public function isDirectory(): bool + { + return $this->isDir; + } + + public function countChildren(): int + { + return count($this->children); + } + + public function getSize(): ?int + { + return $this->size; + } +} diff --git a/symfony.lock b/symfony.lock index defd236..e79008e 100644 --- a/symfony.lock +++ b/symfony.lock @@ -5,6 +5,9 @@ "clue/stream-filter": { "version": "v1.4.1" }, + "composer/package-versions-deprecated": { + "version": "1.11.99.1" + }, "doctrine/annotations": { "version": "1.0", "recipe": { @@ -111,6 +114,12 @@ "jean85/pretty-package-versions": { "version": "1.2" }, + "laminas/laminas-code": { + "version": "3.5.0" + }, + "laminas/laminas-eventmanager": { + "version": "3.3.0" + }, "laminas/laminas-zendframework-bridge": { "version": "1.0.1" }, @@ -207,6 +216,9 @@ "suin/php-rss-writer": { "version": "1.6.0" }, + "symfony/asset": { + "version": "v4.4.18" + }, "symfony/cache": { "version": "v4.1.0" }, diff --git a/templates/base.html.twig b/templates/base.html.twig index d06bf77..440c2fe 100644 --- a/templates/base.html.twig +++ b/templates/base.html.twig @@ -12,11 +12,11 @@ {% block css %} - + - + - + {% endblock %} @@ -81,9 +81,9 @@ {% block javascript %} - - - + + + {% endblock %} diff --git a/templates/torrent_files.html.twig b/templates/torrent_files.html.twig new file mode 100644 index 0000000..c9fa274 --- /dev/null +++ b/templates/torrent_files.html.twig @@ -0,0 +1,10 @@ +
+ +{# @var \App\Magnetico\Entity\Torrent torrent #} + \ No newline at end of file diff --git a/templates/torrent_list.html.twig b/templates/torrent_list.html.twig index 6a6fe01..44fe31e 100644 --- a/templates/torrent_list.html.twig +++ b/templates/torrent_list.html.twig @@ -21,7 +21,7 @@ 🔗 {{ torrent.name }} - {{ torrent.totalSize | readable_size }} + {{ torrent.totalSize | humanize_size }} {{ torrent.discoveredOn | date('Y-m-d H:i:s')}} {% endfor %} diff --git a/templates/torrent_show.html.twig b/templates/torrent_show.html.twig index 8f7f6d4..41f9586 100644 --- a/templates/torrent_show.html.twig +++ b/templates/torrent_show.html.twig @@ -1,5 +1,17 @@ {% extends 'base.html.twig' %} +{% block css %} + {{ parent() }} + + +{% endblock %} + +{% block javascript %} + {{ parent() }} + + +{% endblock %} + {% block content %} {# @var torrent \App\Magnetico\Entity\Torrent #} @@ -16,7 +28,7 @@ - + @@ -24,21 +36,5 @@
Size{{ torrent.totalSize | readable_size }}{{ torrent.totalSize | humanize_size }}
Discovered
- - - - - - - - - {# @var file \App\Magnetico\Entity\File #} - {% for file in torrent.files | sort %} - - - - - {% endfor %} - -
FileSize
{{ file.path }}{{ file.size | readable_size }}
+ {% include 'torrent_files.html.twig' with {'torrent': torrent} only %} {% endblock %} \ No newline at end of file