#12 File tree (bstreeview) implemented instead of file path list. A bit of refactoring. symfony/asset added and used in templates. symfony/flex upgraded due to bugfix.

This commit is contained in:
Alexey Skobkin 2020-12-15 20:44:13 +03:00
parent e06a46508a
commit 18ecb646c0
No known key found for this signature in database
GPG key ID: 5D5CEF6F221278E7
16 changed files with 408 additions and 47 deletions

View file

@ -16,11 +16,13 @@
"ext-ctype": "*", "ext-ctype": "*",
"ext-hash": "*", "ext-hash": "*",
"ext-iconv": "*", "ext-iconv": "*",
"ext-json": "*",
"babdev/pagerfanta-bundle": "^2.4", "babdev/pagerfanta-bundle": "^2.4",
"excelwebzone/recaptcha-bundle": "^1.5", "excelwebzone/recaptcha-bundle": "^1.5",
"sensio/framework-extra-bundle": "^5.1", "sensio/framework-extra-bundle": "^5.1",
"sentry/sentry-symfony": "^3.4", "sentry/sentry-symfony": "^3.4",
"suin/php-rss-writer": "^1.6", "suin/php-rss-writer": "^1.6",
"symfony/asset": "^4.1",
"symfony/console": "^4.1", "symfony/console": "^4.1",
"symfony/dotenv": "^4.1", "symfony/dotenv": "^4.1",
"symfony/expression-language": "^4.1", "symfony/expression-language": "^4.1",

81
composer.lock generated
View file

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "1e62b51a6d3895dcabaf9c263dddca29", "content-hash": "e5b436812dc4f90d4d85a90807e00c18",
"packages": [ "packages": [
{ {
"name": "babdev/pagerfanta-bundle", "name": "babdev/pagerfanta-bundle",
@ -3625,6 +3625,71 @@
], ],
"time": "2017-07-13T10:47:50+00:00" "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", "name": "symfony/cache",
"version": "v5.2.0", "version": "v5.2.0",
@ -4738,16 +4803,16 @@
}, },
{ {
"name": "symfony/flex", "name": "symfony/flex",
"version": "v1.10.0", "version": "v1.11.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/flex.git", "url": "https://github.com/symfony/flex.git",
"reference": "e38520236bdc911c2f219634b485bc328746e980" "reference": "ceb2b4e612bd0b4bb36a4d7fb2e800c861652f48"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/flex/zipball/e38520236bdc911c2f219634b485bc328746e980", "url": "https://api.github.com/repos/symfony/flex/zipball/ceb2b4e612bd0b4bb36a4d7fb2e800c861652f48",
"reference": "e38520236bdc911c2f219634b485bc328746e980", "reference": "ceb2b4e612bd0b4bb36a4d7fb2e800c861652f48",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -4757,6 +4822,7 @@
"require-dev": { "require-dev": {
"composer/composer": "^1.0.2|^2.0", "composer/composer": "^1.0.2|^2.0",
"symfony/dotenv": "^4.4|^5.0", "symfony/dotenv": "^4.4|^5.0",
"symfony/filesystem": "^4.4|^5.0",
"symfony/phpunit-bridge": "^4.4|^5.0", "symfony/phpunit-bridge": "^4.4|^5.0",
"symfony/process": "^3.4|^4.4|^5.0" "symfony/process": "^3.4|^4.4|^5.0"
}, },
@ -4797,7 +4863,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2020-11-05T10:56:45+00:00" "time": "2020-12-03T10:57:35+00:00"
}, },
{ {
"name": "symfony/form", "name": "symfony/form",
@ -8622,7 +8688,8 @@
"php": ">=7.4.0", "php": ">=7.4.0",
"ext-ctype": "*", "ext-ctype": "*",
"ext-hash": "*", "ext-hash": "*",
"ext-iconv": "*" "ext-iconv": "*",
"ext-json": "*"
}, },
"platform-dev": [], "platform-dev": [],
"plugin-api-version": "1.1.0" "plugin-api-version": "1.1.0"

View file

@ -20,7 +20,7 @@ services:
App\: App\:
resource: '../src/*' 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 # Use array in exclude config from Symfony 4.2
#- '../src/Api/V1/{DTO}' #- '../src/Api/V1/{DTO}'
#- '../src/Magnetico/{Entity,Migrations}' #- '../src/Magnetico/{Entity,Migrations}'

View file

@ -0,0 +1,10 @@
/*
@preserve
bstreeview.css
Version: 1.2.0
Authors: Sami CHNITER <sami.chniter@gmail.com>
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}

View file

@ -0,0 +1,10 @@
/*
@preserve
bstreeview.js
Version: 1.2.0
Authors: Sami CHNITER <sami.chniter@gmail.com>
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 = '<div role="treeitem" class="list-group-item" data-toggle="collapse"></div>', d = '<div role="group" class="list-group collapse" id="itemid"></div>', o = '<i class="state-icon"></i>', r = '<i class="item-icon"></i>'; 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);

View file

@ -5,6 +5,7 @@ namespace App\Controller;
use App\Magnetico\Entity\Torrent; use App\Magnetico\Entity\Torrent;
use App\Search\TorrentSearcher; use App\Search\TorrentSearcher;
use App\Pager\PagelessDoctrineORMAdapter; use App\Pager\PagelessDoctrineORMAdapter;
use App\View\Torrent\FileTreeNode;
use Pagerfanta\Pagerfanta; use Pagerfanta\Pagerfanta;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\{Request, Response}; use Symfony\Component\HttpFoundation\{Request, Response};
@ -38,6 +39,7 @@ class TorrentController extends AbstractController
{ {
return $this->render('torrent_show.html.twig', [ return $this->render('torrent_show.html.twig', [
'torrent' => $torrent, 'torrent' => $torrent,
'files' => FileTreeNode::createFromTorrent($torrent),
]); ]);
} }
} }

View file

@ -0,0 +1,67 @@
<?php
namespace App\Helper;
use App\Magnetico\Entity\Torrent;
use App\View\Torrent\FileTreeNode;
class BstreeviewFileTreeBuilder
{
private const DEFAULT_FILE_ICON = 'fas fa-file';
private const DEFAULT_DIR_ICON = 'fas fa-folder';
private FileSizeHumanizer $humanizer;
public function __construct(FileSizeHumanizer $humanizer)
{
$this->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' => '<strong>'.$name.'</strong>',
];
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;
}
}

View file

@ -1,11 +1,8 @@
<?php <?php
namespace App\Twig; namespace App\Helper;
use Twig\Extension\AbstractExtension; class FileSizeHumanizer
use Twig\TwigFilter;
class HumanReadableSizeExtension extends AbstractExtension
{ {
// Can't really exceed 'EB' on 64-bit platform but let it go // Can't really exceed 'EB' on 64-bit platform but let it go
private const SIZE_SUFFIXES = ['B','kB','MB','GB','TB','PB','EB','ZB','YB']; private const SIZE_SUFFIXES = ['B','kB','MB','GB','TB','PB','EB','ZB','YB'];
@ -21,14 +18,7 @@ class HumanReadableSizeExtension extends AbstractExtension
$this->binaryPrefix = $useBinaryPrefix; $this->binaryPrefix = $useBinaryPrefix;
} }
public function getFilters(): array public function humanize(int $bytes, int $decimals = 2, bool $forceBinary = false): string
{
return [
new TwigFilter('readable_size', [$this, 'humanizeSize']),
];
}
public function humanizeSize(int $bytes, int $decimals = 2, bool $forceBinary = false): string
{ {
$bytesString = (string) $bytes; $bytesString = (string) $bytes;

View file

@ -0,0 +1,24 @@
<?php
namespace App\Twig;
use App\Helper\FileSizeHumanizer;
use Twig\Extension\AbstractExtension;
use Twig\TwigFilter;
class FileSizeHumanizerExtension extends AbstractExtension
{
private FileSizeHumanizer $humanizer;
public function __construct(FileSizeHumanizer $humanizer)
{
$this->humanizer = $humanizer;
}
public function getFilters(): array
{
return [
new TwigFilter('humanize_size', [$this->humanizer, 'humanize']),
];
}
}

View file

@ -0,0 +1,25 @@
<?php
namespace App\Twig;
use App\Helper\BstreeviewFileTreeBuilder;
use Twig\Extension\AbstractExtension;
use Twig\{TwigFilter};
class FileTreeExtension extends AbstractExtension
{
private BstreeviewFileTreeBuilder $builder;
public function __construct(BstreeviewFileTreeBuilder $builder)
{
$this->builder = $builder;
}
public function getFilters()
{
return [
new TwigFilter('file_tree', [$this->builder, 'buildFileTreeDataArray']),
new TwigFilter('torrent_file_tree', [$this->builder, 'buildFileTreeDataArrayFromTorrent']),
];
}
}

View file

@ -0,0 +1,146 @@
<?php
namespace App\View\Torrent;
use App\Magnetico\Entity\File;
use App\Magnetico\Entity\Torrent;
class FileTreeNode
{
private ?string $name;
private bool $isDir = true;
private ?int $size;
private ?FileTreeNode $parent;
/** @var FileTreeNode[]|File[] */
private array $children = [];
private function __construct(string $name)
{
$this->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;
}
}

View file

@ -5,6 +5,9 @@
"clue/stream-filter": { "clue/stream-filter": {
"version": "v1.4.1" "version": "v1.4.1"
}, },
"composer/package-versions-deprecated": {
"version": "1.11.99.1"
},
"doctrine/annotations": { "doctrine/annotations": {
"version": "1.0", "version": "1.0",
"recipe": { "recipe": {
@ -111,6 +114,12 @@
"jean85/pretty-package-versions": { "jean85/pretty-package-versions": {
"version": "1.2" "version": "1.2"
}, },
"laminas/laminas-code": {
"version": "3.5.0"
},
"laminas/laminas-eventmanager": {
"version": "3.3.0"
},
"laminas/laminas-zendframework-bridge": { "laminas/laminas-zendframework-bridge": {
"version": "1.0.1" "version": "1.0.1"
}, },
@ -207,6 +216,9 @@
"suin/php-rss-writer": { "suin/php-rss-writer": {
"version": "1.6.0" "version": "1.6.0"
}, },
"symfony/asset": {
"version": "v4.4.18"
},
"symfony/cache": { "symfony/cache": {
"version": "v4.1.0" "version": "v4.1.0"
}, },

View file

@ -12,11 +12,11 @@
{% block css %} {% block css %}
<!-- Bootstrap core CSS --> <!-- Bootstrap core CSS -->
<link href="/assets/bootstrap/css/bootstrap.min.css" rel="stylesheet"> <link href="{{ asset('assets/bootstrap/css/bootstrap.min.css') }}" rel="stylesheet">
<!-- Font Awesome --> <!-- Font Awesome -->
<link href="/assets/fontawesome/css/all.css" rel="stylesheet"> <link href="{{ asset('assets/fontawesome/css/all.css') }}" rel="stylesheet">
<!-- SIte style --> <!-- SIte style -->
<link href="/assets/magnetico-web/css/style.css" rel="stylesheet"> <link href="{{ asset('assets/magnetico-web/css/style.css') }}" rel="stylesheet">
{% endblock %} {% endblock %}
</head> </head>
@ -81,9 +81,9 @@
</main><!-- /.container --> </main><!-- /.container -->
{% block javascript %} {% block javascript %}
<script src="/assets/jquery/js/jquery-3.5.1.slim.min.js"></script> <script src="{{ asset('assets/jquery/js/jquery-3.5.1.slim.min.js') }}"></script>
<script src="/assets/popper/js/popper.min.js"></script> <script src="{{ asset('assets/popper/js/popper.min.js') }}"></script>
<script src="/assets/bootstrap/js/bootstrap.min.js"></script> <script src="{{ asset('assets/bootstrap/js/bootstrap.min.js') }}"></script>
{% endblock %} {% endblock %}
</body> </body>
</html> </html>

View file

@ -0,0 +1,10 @@
<div class="file-tree"></div>
{# @var \App\Magnetico\Entity\Torrent torrent #}
<script type="text/javascript">
document.addEventListener('DOMContentLoaded', function(){
let tree = {{ torrent | torrent_file_tree | json_encode | raw }};
$('.file-tree').bstreeview({data: tree});
});
</script>

View file

@ -21,7 +21,7 @@
<tr> <tr>
<td><a href="{{ magnet(torrent.infoHash, torrent.name) }}">&#128279;</a></td> <td><a href="{{ magnet(torrent.infoHash, torrent.name) }}">&#128279;</a></td>
<td><a href="{{ path('torrents_show', {'id': torrent.id}) }}">{{ torrent.name }}</a></td> <td><a href="{{ path('torrents_show', {'id': torrent.id}) }}">{{ torrent.name }}</a></td>
<td>{{ torrent.totalSize | readable_size }}</td> <td>{{ torrent.totalSize | humanize_size }}</td>
<td>{{ torrent.discoveredOn | date('Y-m-d H:i:s')}}</td> <td>{{ torrent.discoveredOn | date('Y-m-d H:i:s')}}</td>
</tr> </tr>
{% endfor %} {% endfor %}

View file

@ -1,5 +1,17 @@
{% extends 'base.html.twig' %} {% extends 'base.html.twig' %}
{% block css %}
{{ parent() }}
<!-- BSTreeView plugin -->
<link href="{{ asset('assets/bstreeview/css/bstreeview.min.css') }}" rel="stylesheet">
{% endblock %}
{% block javascript %}
{{ parent() }}
<!-- BSTreeView plugin -->
<script src="{{ asset('assets/bstreeview/js/bstreeview.min.js') }}"></script>
{% endblock %}
{% block content %} {% block content %}
{# @var torrent \App\Magnetico\Entity\Torrent #} {# @var torrent \App\Magnetico\Entity\Torrent #}
<table class="table"> <table class="table">
@ -16,7 +28,7 @@
</tr> </tr>
<tr> <tr>
<td>Size</td> <td>Size</td>
<td><abbr title="{{ torrent.totalSize }} bytes">{{ torrent.totalSize | readable_size }}</abbr></td> <td><abbr title="{{ torrent.totalSize }} bytes">{{ torrent.totalSize | humanize_size }}</abbr></td>
</tr> </tr>
<tr> <tr>
<td>Discovered</td> <td>Discovered</td>
@ -24,21 +36,5 @@
</tr> </tr>
</table> </table>
<table class="table"> {% include 'torrent_files.html.twig' with {'torrent': torrent} only %}
<thead>
<tr>
<th scope="col">File</th>
<th scope="col">Size</th>
</tr>
</thead>
<tbody>
{# @var file \App\Magnetico\Entity\File #}
{% for file in torrent.files | sort %}
<tr>
<td>{{ file.path }}</td>
<td>{{ file.size | readable_size }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %} {% endblock %}