[MODULES][Collection] Abstracting Collections
This commit is contained in:
		| @@ -1,11 +0,0 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types = 1); | ||||
|  | ||||
| namespace Component\Collection; | ||||
|  | ||||
| use App\Core\Modules\Component; | ||||
|  | ||||
| class Collection extends Component | ||||
| { | ||||
| } | ||||
| @@ -33,27 +33,95 @@ namespace Plugin\AttachmentCollections; | ||||
|  | ||||
| use App\Core\DB\DB; | ||||
| use App\Core\Event; | ||||
| use App\Core\Form; | ||||
| use function App\Core\I18n\_m; | ||||
| use App\Core\Modules\Plugin; | ||||
| use App\Core\Modules\Collection; | ||||
| use App\Core\Router\RouteLoader; | ||||
| use App\Core\Router\Router; | ||||
| use App\Entity\Actor; | ||||
| use App\Entity\Feed; | ||||
| use App\Entity\LocalUser; | ||||
| use App\Util\Common; | ||||
| use App\Util\Exception\RedirectException; | ||||
| use App\Util\Formatting; | ||||
| use App\Util\Nickname; | ||||
| use Component\Collection\Entity\Collection; | ||||
| use Plugin\AttachmentCollections\Controller as C; | ||||
| use Plugin\AttachmentCollections\Entity\AttachmentCollection; | ||||
| use Plugin\AttachmentCollections\Entity\AttachmentCollectionEntry; | ||||
| use Symfony\Component\Form\Extension\Core\Type\ChoiceType; | ||||
| use Symfony\Component\Form\Extension\Core\Type\SubmitType; | ||||
| use Symfony\Component\Form\Extension\Core\Type\TextType; | ||||
| use Symfony\Component\HttpFoundation\Request; | ||||
|  | ||||
| class AttachmentCollections extends Plugin | ||||
| class AttachmentCollections extends Collection | ||||
| { | ||||
|     protected function createCollection(Actor $owner, array $vars, string $name) | ||||
|     { | ||||
|         $col = AttachmentCollection::create([ | ||||
|             'name'     => $name, | ||||
|             'actor_id' => $owner->getId(), | ||||
|         ]); | ||||
|         DB::persist($col); | ||||
|         DB::persist(AttachmentCollectionEntry::create([ | ||||
|             'attachment_id' => $vars['vars']['attachment_id'], | ||||
|             'note_id'       => $vars['vars']['note_id'], | ||||
|             'collection_id' => $col->getId(), | ||||
|         ])); | ||||
|     } | ||||
|     protected function removeItems(Actor $owner, array $vars, $items, array $collections) | ||||
|     { | ||||
|         return DB::dql(<<<'EOF' | ||||
|             DELETE FROM \Plugin\AttachmentCollections\Entity\AttachmentCollectionEntry AS entry | ||||
|             WHERE entry.attachment_id = :attach_id AND entry.note_id = :note_id | ||||
|             AND entry.collection_id IN ( | ||||
|                 SELECT album.id FROM \Plugin\AttachmentCollections\Entity\AttachmentCollection AS album | ||||
|                 WHERE album.actor_id = :user_id | ||||
|                   AND album.id IN (:ids) | ||||
|             ) | ||||
|             EOF, [ | ||||
|             'attach_id' => $vars['vars']['attachment_id'], | ||||
|             'note_id'   => $vars['vars']['note_id'], | ||||
|             'user_id'   => $owner->getId(), | ||||
|             'ids'       => $items, | ||||
|         ]); | ||||
|     } | ||||
|  | ||||
|     protected function addItems(Actor $owner, array $vars, $items, array $collections) | ||||
|     { | ||||
|         foreach ($items as $id) { | ||||
|             // prevent user from putting something in a collection (s)he doesn't own: | ||||
|             if (\in_array($id, $collections)) { | ||||
|                 DB::persist(AttachmentCollectionEntry::create([ | ||||
|                     'attachment_id' => $vars['vars']['attachment_id'], | ||||
|                     'note_id'       => $vars['vars']['note_id'], | ||||
|                     'collection_id' => $id, | ||||
|                 ])); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     protected function shouldAddToRightPanel(Actor $user, $vars, Request $request): bool | ||||
|     { | ||||
|         return $vars['path'] === 'note_attachment_show'; | ||||
|     } | ||||
|     protected function getCollectionsBy(Actor $owner, ?array $vars = null, bool $ids_only = false): array | ||||
|     { | ||||
|         if (\is_null($vars)) { | ||||
|             $res = DB::findBy(AttachmentCollection::class, ['actor_id' => $owner->getId()]); | ||||
|         } else { | ||||
|             $res = DB::dql( | ||||
|                 <<<'EOF' | ||||
|                     SELECT entry.collection_id FROM \Plugin\AttachmentCollections\Entity\AttachmentCollectionEntry AS entry | ||||
|                     INNER JOIN \Plugin\AttachmentCollections\Entity\AttachmentCollection AS attachment_collection | ||||
|                     WITH attachment_collection.id = entry.collection_id | ||||
|                     WHERE entry.attachment_id = :attach_id AND entry.note_id = :note_id AND attachment_collection.actor_id = :id | ||||
|                     EOF, | ||||
|                 [ | ||||
|                     'id'        => $owner->getId(), | ||||
|                     'note_id'   => $vars['vars']['note_id'], | ||||
|                     'attach_id' => $vars['vars']['attachment_id'], | ||||
|                 ], | ||||
|             ); | ||||
|         } | ||||
|         if (!$ids_only) { | ||||
|             return $res; | ||||
|         } | ||||
|         return array_map(fn ($x) => $x['collection_id'], $res); | ||||
|     } | ||||
|  | ||||
|     public function onAddRoute(RouteLoader $r): bool | ||||
|     { | ||||
|         // View all collections by actor id and nickname | ||||
| @@ -92,146 +160,4 @@ class AttachmentCollections extends Plugin | ||||
|         ])); | ||||
|         return Event::next; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Append Attachment Collections widget to the right panel. | ||||
|      * It's compose of two forms: one to select collections to add | ||||
|      * the current attachment to, and another to create a new collection. | ||||
|      */ | ||||
|     public function onAppendRightPanelBlock($vars, Request $request, &$res): bool | ||||
|     { | ||||
|         if ($vars['path'] !== 'note_attachment_show') { | ||||
|             return Event::next; | ||||
|         } | ||||
|         $user = Common::user(); | ||||
|         if (\is_null($user)) { | ||||
|             return Event::next; | ||||
|         } | ||||
|  | ||||
|         $collections = DB::findBy(Collection::class, ['actor_id' => $user->getId()]); | ||||
|  | ||||
|         // add to collection form | ||||
|         $attachment_id = $vars['vars']['attachment_id']; | ||||
|         $note_id       = $vars['vars']['note_id']; | ||||
|         $choices       = []; | ||||
|         foreach ($collections as $col) { | ||||
|             $choices[$col->getName()] = $col->getId(); | ||||
|         } | ||||
|         $already_selected = DB::dql( | ||||
|             <<<'EOF' | ||||
|                 SELECT entry.collection_id FROM \Plugin\AttachmentCollections\Entity\AttachmentCollectionEntry AS entry | ||||
|                 INNER JOIN \Component\Collection\Entity\Collection AS attachment_collection | ||||
|                 WITH attachment_collection.id = entry.collection_id | ||||
|                 WHERE entry.attachment_id = :attach_id AND entry.note_id = :note_id AND attachment_collection.actor_id = :id | ||||
|                 EOF, | ||||
|             ['attach_id' => $attachment_id, 'note_id' => $note_id, 'id' => $user->getId()], | ||||
|         ); | ||||
|         $already_selected = array_map(fn ($x) => $x['collection_id'], $already_selected); | ||||
|         $add_form         = Form::create([ | ||||
|             ['collections', ChoiceType::class, [ | ||||
|                 'choices'     => $choices, | ||||
|                 'multiple'    => true, | ||||
|                 'required'    => false, | ||||
|                 'choice_attr' => function ($id) use ($already_selected) { | ||||
|                     if (\in_array($id, $already_selected)) { | ||||
|                         return ['selected' => 'selected']; | ||||
|                     } | ||||
|                     return []; | ||||
|                 }, | ||||
|             ]], | ||||
|             ['add', SubmitType::class, [ | ||||
|                 'label' => _m('Add to collections'), | ||||
|                 'attr'  => [ | ||||
|                     'title' => _m('Add to collection'), | ||||
|                 ], | ||||
|             ]], | ||||
|         ]); | ||||
|  | ||||
|         $add_form->handleRequest($request); | ||||
|         if ($add_form->isSubmitted() && $add_form->isValid()) { | ||||
|             $collections = $add_form->getData()['collections']; | ||||
|             $removed     = array_filter($already_selected, fn ($x) => !\in_array($x, $collections)); | ||||
|             $added       = array_filter($collections, fn ($x) => !\in_array($x, $already_selected)); | ||||
|             if (\count($removed) > 0) { | ||||
|                 DB::dql(<<<'EOF' | ||||
|                         DELETE FROM \Plugin\AttachmentCollections\Entity\AttachmentCollectionEntry AS entry | ||||
|                         WHERE entry.attachment_id = :attach_id AND entry.note_id = :note_id | ||||
|                         AND entry.collection_id IN ( | ||||
|                             SELECT album.id FROM \Component\Collection\Entity\Collection AS album | ||||
|                             WHERE album.actor_id = :user_id | ||||
|                               AND album.id IN (:ids) | ||||
|                         ) | ||||
|                     EOF, ['attach_id' => $attachment_id, 'note_id' => $note_id, 'user_id' => $user->getId(), 'ids' => $removed]); | ||||
|             } | ||||
|             foreach ($added as $cid) { | ||||
|                 // prevent user from putting something in a collection (s)he doesn't own: | ||||
|                 if (\in_array($cid, $collections)) { | ||||
|                     DB::persist(AttachmentCollectionEntry::create([ | ||||
|                         'attachment_id' => $attachment_id, | ||||
|                         'note_id'       => $note_id, | ||||
|                         'collection_id' => $cid, | ||||
|                     ])); | ||||
|                 } | ||||
|             } | ||||
|             DB::flush(); | ||||
|             throw new RedirectException(); | ||||
|         } | ||||
|         // add to new collection form | ||||
|         $create_form = Form::create([ | ||||
|             ['name', TextType::class, [ | ||||
|                 'label' => _m('Add to a new collection'), | ||||
|                 'attr'  => [ | ||||
|                     'placeholder' => _m('New collection name'), | ||||
|                     'required'    => 'required', | ||||
|                 ], | ||||
|                 'data' => '', | ||||
|             ]], | ||||
|             ['create', SubmitType::class, [ | ||||
|                 'label' => _m('Create a new collection'), | ||||
|                 'attr'  => [ | ||||
|                     'title' => _m('Create a new collection'), | ||||
|                 ], | ||||
|             ]], | ||||
|         ]); | ||||
|         $create_form->handleRequest($request); | ||||
|         if ($create_form->isSubmitted() && $create_form->isValid()) { | ||||
|             $name = $create_form->getData()['name']; | ||||
|             $coll = Collection::create([ | ||||
|                 'name'     => $name, | ||||
|                 'actor_id' => $user->getId(), | ||||
|             ]); | ||||
|             DB::persist($coll); | ||||
|             DB::persist(AttachmentCollectionEntry::create([ | ||||
|                 'attachment_id' => $attachment_id, | ||||
|                 'note_id'       => $note_id, | ||||
|                 'collection_id' => $coll->getId(), | ||||
|             ])); | ||||
|             DB::flush(); | ||||
|             throw new RedirectException(); | ||||
|         } | ||||
|  | ||||
|         $res[] = Formatting::twigRenderFile( | ||||
|             'AttachmentCollections/widget.html.twig', | ||||
|             [ | ||||
|                 'has_collections' => $collections, | ||||
|                 'add_form'        => $add_form->createView(), | ||||
|                 'create_form'     => $create_form->createView(), | ||||
|             ], | ||||
|         ); | ||||
|         return Event::next; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Output our dedicated stylesheet | ||||
|      * | ||||
|      * @param array $styles stylesheets path | ||||
|      * | ||||
|      * @return bool hook value; true means continue processing, false means stop | ||||
|      */ | ||||
|     public function onEndShowStyles(array &$styles, string $route): bool | ||||
|     { | ||||
|         $styles[] = 'plugins/AttachmentCollections/assets/css/widget.css'; | ||||
|         $styles[] = 'plugins/AttachmentCollections/assets/css/pages.css'; | ||||
|         return Event::next; | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -23,181 +23,35 @@ declare(strict_types = 1); | ||||
|  | ||||
| namespace Plugin\AttachmentCollections\Controller; | ||||
|  | ||||
| use App\Core\Controller\CollectionController; | ||||
| use App\Core\DB\DB; | ||||
| use App\Core\Form; | ||||
| use function App\Core\I18n\_m; | ||||
| use App\Core\Router\Router; | ||||
| use App\Entity\LocalUser; | ||||
| use App\Util\Common; | ||||
| use App\Util\Exception\RedirectException; | ||||
| use Component\Collection\Entity\Collection; | ||||
| use Component\Feed\Util\FeedController; | ||||
| use Symfony\Component\Form\Extension\Core\Type\SubmitType; | ||||
| use Symfony\Component\Form\Extension\Core\Type\TextType; | ||||
| use Symfony\Component\HttpFoundation\Request; | ||||
| use Plugin\AttachmentCollections\Entity\AttachmentCollection; | ||||
|  | ||||
| class Controller extends FeedController | ||||
| class Controller extends CollectionController | ||||
| { | ||||
|     public function collectionsByActorNickname(Request $request, string $nickname): array | ||||
|     public function createCollection(int $owner_id, string $name) | ||||
|     { | ||||
|         $user = DB::findOneBy(LocalUser::class, ['nickname' => $nickname]); | ||||
|         return self::collectionsView($request, $user->getId(), $nickname); | ||||
|         DB::persist(AttachmentCollection::create([ | ||||
|             'name'     => $name, | ||||
|             'actor_id' => $owner_id, | ||||
|         ])); | ||||
|     } | ||||
|  | ||||
|     public function collectionsViewByActorId(Request $request, int $id): array | ||||
|     public function getCollectionUrl(int $owner_id, ?string $owner_nickname, int $collection_id): string | ||||
|     { | ||||
|         return self::collectionsView($request, $id, null); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Generate Collections page | ||||
|      * | ||||
|      * @param int     $id       actor id | ||||
|      * @param ?string $nickname actor nickname | ||||
|      * | ||||
|      * @return array twig template options | ||||
|      */ | ||||
|     public function collectionsView(Request $request, int $id, ?string $nickname): array | ||||
|     { | ||||
|         $collections = DB::findBy(Collection::class, ['actor_id' => $id]); | ||||
|  | ||||
|         // create collection form | ||||
|         $create = null; | ||||
|         if (Common::user()?->getId() === $id) { | ||||
|             $create = Form::create([ | ||||
|                 ['name', TextType::class, [ | ||||
|                     'label' => _m('Create collection'), | ||||
|                     'attr'  => [ | ||||
|                         'placeholder' => _m('Name'), | ||||
|                         'required'    => 'required', | ||||
|                     ], | ||||
|                     'data' => '', | ||||
|                 ]], | ||||
|                 ['add_collection', SubmitType::class, [ | ||||
|                     'label' => _m('Create collection'), | ||||
|                     'attr'  => [ | ||||
|                         'title' => _m('Create collection'), | ||||
|                     ], | ||||
|                 ]], | ||||
|             ]); | ||||
|             $create->handleRequest($request); | ||||
|             if ($create->isSubmitted() && $create->isValid()) { | ||||
|                 DB::persist(Collection::create([ | ||||
|                     'name'     => $create->getData()['name'], | ||||
|                     'actor_id' => $id, | ||||
|                 ])); | ||||
|                 DB::flush(); | ||||
|                 throw new RedirectException(); | ||||
|             } | ||||
|         if (\is_null($owner_nickname)) { | ||||
|             return Router::url( | ||||
|                 'collection_notes_view_by_actor_id', | ||||
|                 ['id' => $owner_id, 'cid' => $collection_id], | ||||
|             ); | ||||
|         } | ||||
|  | ||||
|         // We need to inject some functions in twig, | ||||
|         // but I don't want to create an environment for this | ||||
|         // as twig docs suggest in https://twig.symfony.com/doc/2.x/advanced.html#functions. | ||||
|         // | ||||
|         // Instead, I'm using an anonymous class to encapsulate | ||||
|         // the functions and passing that class to the template. | ||||
|         // This is suggested at https://stackoverflow.com/a/50364502. | ||||
|         $fn = new class($id, $nickname, $request) { | ||||
|             private $id; | ||||
|             private $nick; | ||||
|             private $request; | ||||
|  | ||||
|             public function __construct($id, $nickname, $request) | ||||
|             { | ||||
|                 $this->id      = $id; | ||||
|                 $this->nick    = $nickname; | ||||
|                 $this->request = $request; | ||||
|             } | ||||
|             // there's already a injected function called path, | ||||
|             // that maps to Router::url(name, args), but since | ||||
|             // I want to preserve nicknames, I think it's better | ||||
|             // to use that getUrl function | ||||
|             public function getUrl($cid) | ||||
|             { | ||||
|                 if (\is_null($this->nick)) { | ||||
|                     return Router::url( | ||||
|                         'collection_notes_view_by_actor_id', | ||||
|                         ['id' => $this->id, 'cid' => $cid], | ||||
|                     ); | ||||
|                 } | ||||
|                 return Router::url( | ||||
|                     'collection_notes_view_by_nickname', | ||||
|                     ['nickname' => $this->nick, 'cid' => $cid], | ||||
|                 ); | ||||
|             } | ||||
|             // There are many collections in this page and we need two | ||||
|             // forms for each one of them: one form to edit the collection's | ||||
|             // name and another to remove the collection. | ||||
|  | ||||
|             // creating the edit form | ||||
|             public function editForm($collection) | ||||
|             { | ||||
|                 $edit = Form::create([ | ||||
|                     ['name', TextType::class, [ | ||||
|                         'attr' => [ | ||||
|                             'placeholder' => 'New name', | ||||
|                             'required'    => 'required', | ||||
|                         ], | ||||
|                         'data' => '', | ||||
|                     ]], | ||||
|                     ['update_' . $collection->getId(), SubmitType::class, [ | ||||
|                         'label' => _m('Save'), | ||||
|                         'attr'  => [ | ||||
|                             'title' => _m('Save'), | ||||
|                         ], | ||||
|                     ]], | ||||
|                 ]); | ||||
|                 $edit->handleRequest($this->request); | ||||
|                 if ($edit->isSubmitted() && $edit->isValid()) { | ||||
|                     $collection->setName($edit->getData()['name']); | ||||
|                     DB::persist($collection); | ||||
|                     DB::flush(); | ||||
|                     throw new RedirectException(); | ||||
|                 } | ||||
|                 return $edit->createView(); | ||||
|             } | ||||
|  | ||||
|             // creating the remove form | ||||
|             public function rmForm($collection) | ||||
|             { | ||||
|                 $rm = Form::create([ | ||||
|                     ['remove_' . $collection->getId(), SubmitType::class, [ | ||||
|                         'label' => _m('Delete collection'), | ||||
|                         'attr'  => [ | ||||
|                             'title' => _m('Delete collection'), | ||||
|                             'class' => 'danger', | ||||
|                         ], | ||||
|                     ]], | ||||
|                 ]); | ||||
|                 $rm->handleRequest($this->request); | ||||
|                 if ($rm->isSubmitted()) { | ||||
|                     DB::remove($collection); | ||||
|                     DB::flush(); | ||||
|                     throw new RedirectException(); | ||||
|                 } | ||||
|                 return $rm->createView(); | ||||
|             } | ||||
|         }; | ||||
|  | ||||
|         return [ | ||||
|             '_template'      => 'AttachmentCollections/collections.html.twig', | ||||
|             'page_title'     => 'Attachment Collections list', | ||||
|             'add_collection' => $create?->createView(), | ||||
|             'fn'             => $fn, | ||||
|             'collections'    => $collections, | ||||
|         ]; | ||||
|         return Router::url( | ||||
|             'collection_notes_view_by_nickname', | ||||
|             ['nickname' => $owner_nickname, 'cid' => $collection_id], | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     public function collectionNotesByNickname(Request $request, string $nickname, int $cid): array | ||||
|     public function getCollectionItems(int $owner_id, $collection_id): array | ||||
|     { | ||||
|         $user = DB::findOneBy(LocalUser::class, ['nickname' => $nickname]); | ||||
|         return self::collectionNotesByActorId($request, $user->getId(), $cid); | ||||
|     } | ||||
|  | ||||
|     public function collectionNotesByActorId(Request $request, int $id, int $cid): array | ||||
|     { | ||||
|         $collection        = DB::findOneBy(Collection::class, ['id' => $cid]); | ||||
|         [$attachs, $notes] = DB::dql( | ||||
|             <<<'EOF' | ||||
|                 SELECT attach, notice FROM \Plugin\AttachmentCollections\Entity\AttachmentCollectionEntry AS entry | ||||
| @@ -207,13 +61,20 @@ class Controller extends FeedController | ||||
|                     WITH entry.note_id = notice.id | ||||
|                 WHERE entry.collection_id = :cid | ||||
|                 EOF, | ||||
|             ['cid' => $cid], | ||||
|             ['cid' => $collection_id], | ||||
|         ); | ||||
|         return [ | ||||
|             '_template'   => 'AttachmentCollections/collection.html.twig', | ||||
|             'page_title'  => $collection->getName(), | ||||
|             'attachments' => array_values($attachs), | ||||
|             'bare_notes'  => array_values($notes), | ||||
|         ]; | ||||
|     } | ||||
|     public function getCollectionsBy(int $owner_id): array | ||||
|     { | ||||
|         return DB::findBy(AttachmentCollection::class, ['actor_id' => $owner_id], order_by: ['id' => 'desc']); | ||||
|     } | ||||
|     public function getCollectionBy(int $owner_id, int $collection_id): AttachmentCollection | ||||
|     { | ||||
|         return DB::findOneBy(AttachmentCollection::class, ['id' => $collection_id]); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -2,12 +2,12 @@ | ||||
| 
 | ||||
| declare(strict_types = 1); | ||||
| 
 | ||||
| namespace Component\Collection\Entity; | ||||
| namespace Plugin\AttachmentCollections\Entity; | ||||
| 
 | ||||
| use App\Core\Entity; | ||||
| use function mb_substr; | ||||
| 
 | ||||
| class Collection extends Entity | ||||
| class AttachmentCollection extends Entity | ||||
| { | ||||
|     // These tags are meant to be literally included and will be populated with the appropriate fields, setters and getters by `bin/generate_entity_fields`
 | ||||
|     // {{{ Autocode
 | ||||
| @@ -1,27 +1,13 @@ | ||||
| {% extends 'stdgrid.html.twig' %} | ||||
| {% import '/cards/note/view.html.twig' as noteView %} | ||||
| {% extends 'collections/collection.html.twig' %} | ||||
|  | ||||
| {% block title %}{{ page_title | trans }}{% endblock %} | ||||
|  | ||||
| {% block stylesheets %} | ||||
|     {{ parent() }} | ||||
|     <link rel="stylesheet" href="{{ asset('assets/default_theme/css/pages/feeds.css') }}" type="text/css"> | ||||
| {% endblock stylesheets %} | ||||
|  | ||||
| {% block body %} | ||||
|     <h1>{{ page_title | trans }}</h1> | ||||
|     {# Backwards compatibility with hAtom 0.1 #} | ||||
|     <main class="feed" tabindex="0" role="feed"> | ||||
|         <div class="h-feed hfeed notes"> | ||||
|             {% for key, attachment in attachments %} | ||||
|                 <section class="section-widget section-padding"> | ||||
|                     {% include '/cards/attachments/view.html.twig' with {'attachment': attachment, 'note': bare_notes[key], 'title': attachment.getBestTitle(bare_notes[key])} only %} | ||||
|                     <a class="section-widget-button-like" | ||||
|                        href="{{ attachment.getDownloadUrl(bare_notes[key]) }}"> {{ 'Download link' | trans }}</a> | ||||
|                 </section> | ||||
|             {% else %} | ||||
|                 <div id="empty-notes"><h1>{% trans %}No attachments here.{% endtrans %}</h1></div> | ||||
|             {% endfor %} | ||||
|         </div> | ||||
|     </main> | ||||
| {% endblock body %} | ||||
| {% block collection_items %} | ||||
|     {% for key, attachment in attachments %} | ||||
|         <section class="section-widget section-padding"> | ||||
|             {% include '/cards/attachments/view.html.twig' with {'attachment': attachment, 'note': bare_notes[key], 'title': attachment.getBestTitle(bare_notes[key])} only %} | ||||
|             <a class="section-widget-button-like" | ||||
|                href="{{ attachment.getDownloadUrl(bare_notes[key]) }}"> {{ 'Download link' | trans }}</a> | ||||
|         </section> | ||||
|     {% else %} | ||||
|         <div id="empty-notes"><h1>{% trans %}No attachments here.{% endtrans %}</h1></div> | ||||
|     {% endfor %} | ||||
| {% endblock collection_items %} | ||||
|   | ||||
							
								
								
									
										106
									
								
								public/components/Collection/assets/css/pages.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										106
									
								
								public/components/Collection/assets/css/pages.css
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,106 @@ | ||||
| .collection-add, .collections-list { | ||||
|     padding: 10px 12px; | ||||
| } | ||||
| .collection-add > form > div { | ||||
|     display: flex; | ||||
|     align-items: flex-end; | ||||
| } | ||||
| .collection-add > form > div .mb-6 { | ||||
|     margin-right: 12px; | ||||
| } | ||||
| .collection-add > form > div button { | ||||
|     margin-top: 0px; | ||||
|     margin-bottom: var(--s); | ||||
| } | ||||
| @media only screen and (max-width:465px) { | ||||
|     .collection-add > form, .collection-add > form > div .mb-6 { | ||||
|         width: 100%; | ||||
|         margin: 0px; | ||||
|     } | ||||
|     .collection-add > form > div { | ||||
|         flex-direction: column; | ||||
|     } | ||||
|     .collection-add > form > div button { | ||||
|         margin-top: var(--s);; | ||||
|         margin-bottom: 0px; | ||||
|     } | ||||
| } | ||||
|  | ||||
| :root { | ||||
|     --collections-list-quantity: 3; | ||||
| } | ||||
| @media only screen and (min:1281px) { | ||||
|   :root { | ||||
|       --collections-list-quantity: 3; | ||||
|   } | ||||
| } | ||||
| @media only screen and (max-width:1280px) { | ||||
|   :root { | ||||
|       --collections-list-quantity: 4; | ||||
|   } | ||||
| } | ||||
| @media only screen and (max-width:900px) { | ||||
|   :root { | ||||
|       --collections-list-quantity: 3; | ||||
|   } | ||||
| } | ||||
| @media only screen and (max-width:700px) { | ||||
|   :root { | ||||
|       --collections-list-quantity: 2; | ||||
|   } | ||||
| } | ||||
| @media only screen and (max-width:465px) { | ||||
|   :root { | ||||
|       --collections-list-quantity: 1; | ||||
|   } | ||||
| } | ||||
|  | ||||
|  | ||||
| .collections-list { | ||||
|     display: grid !important; | ||||
|     grid-gap: 12px; | ||||
|     grid-template-columns: repeat(var(--collections-list-quantity), 1fr); | ||||
| } | ||||
| .collections-list h3, .collections-list h2, .collections-list h1 { | ||||
|     grid-column-start: 1; | ||||
|     grid-column-end: calc(var(--collections-list-quantity) + 1); | ||||
| } | ||||
| .collections-list .collection-item { | ||||
|     border: 2px solid var(--border); | ||||
|     border-radius: var(--s); | ||||
|     padding: 10px 20px; | ||||
|     display: flex; | ||||
|     flex-direction: column; | ||||
|     position: relative; | ||||
| } | ||||
| .collections-list .collection-item .name { | ||||
|     margin-right: auto; | ||||
|     flex-grow: 1; | ||||
|     flex-shrink: 1; | ||||
|     word-break: break-word; | ||||
|     margin-right: 60px; | ||||
| } | ||||
| .collections-list .collection-item summary { | ||||
|     position: absolute; | ||||
|     top: 10px; | ||||
|     right: 50px; | ||||
|     width: 16px; | ||||
| } | ||||
| .collections-list .collection-item details + details > summary { | ||||
|     right: 20px; | ||||
| } | ||||
| .collections-list .collection-item details { | ||||
|     margin-top: 12px; | ||||
| } | ||||
| .collections-list .collection-item svg { | ||||
|     fill: var(--foreground); | ||||
| } | ||||
| .collections-list .collection-item svg:hover { | ||||
|     fill: var(--accent); | ||||
| } | ||||
| .collections-list .collection-item details label { | ||||
|     display: none; | ||||
| } | ||||
| .collections-list .collection-item details .danger { | ||||
|     color: #cb2d2d; | ||||
| } | ||||
							
								
								
									
										17
									
								
								public/components/Collection/assets/css/widget.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								public/components/Collection/assets/css/widget.css
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | ||||
| .collections { | ||||
|     background-color: red; | ||||
| } | ||||
| .collections .collections-selections-options { | ||||
|     display: flex; | ||||
|     margin-top: 12px; | ||||
|     align-items: center; | ||||
|     justify-content: space-between; | ||||
| } | ||||
| .acollections .collections-selections-options button { | ||||
|     margin: 0 !important; | ||||
| } | ||||
| #add_collections { | ||||
|     height: auto !important; | ||||
|     max-height: 20rem !important; | ||||
|     overflow-y: auto !important; | ||||
| } | ||||
							
								
								
									
										216
									
								
								src/Core/Controller/CollectionController.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										216
									
								
								src/Core/Controller/CollectionController.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,216 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types = 1); | ||||
|  | ||||
| // {{{ License | ||||
| // This file is part of GNU social - https://www.gnu.org/software/social | ||||
| // | ||||
| // GNU social is free software: you can redistribute it and/or modify | ||||
| // it under the terms of the GNU Affero General Public License as published by | ||||
| // the Free Software Foundation, either version 3 of the License, or | ||||
| // (at your option) any later version. | ||||
| // | ||||
| // GNU social is distributed in the hope that it will be useful, | ||||
| // but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
| // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
| // GNU Affero General Public License for more details. | ||||
| // | ||||
| // You should have received a copy of the GNU Affero General Public License | ||||
| // along with GNU social.  If not, see <http://www.gnu.org/licenses/>. | ||||
| // }}} | ||||
| /** | ||||
|  * Collections Controller for GNU social | ||||
|  * | ||||
|  * @package   GNUsocial | ||||
|  * @category  Plugin | ||||
|  * | ||||
|  * @author    Phablulo <phablulo@gmail.com> | ||||
|  * @copyright 2018-2019, 2021 Free Software Foundation, Inc http://www.fsf.org | ||||
|  * @license   https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later | ||||
|  */ | ||||
|  | ||||
| namespace App\Core\Controller; | ||||
|  | ||||
| use App\Core\DB\DB; | ||||
| use App\Core\Form; | ||||
| use function App\Core\I18n\_m; | ||||
| use App\Core\Router\Router; | ||||
| use App\Entity\LocalUser; | ||||
| use App\Util\Common; | ||||
| use App\Util\Exception\RedirectException; | ||||
| use Component\Feed\Util\FeedController; | ||||
| use Symfony\Component\Form\Extension\Core\Type\SubmitType; | ||||
| use Symfony\Component\Form\Extension\Core\Type\TextType; | ||||
| use Symfony\Component\HttpFoundation\Request; | ||||
|  | ||||
| abstract class CollectionController extends FeedController | ||||
| { | ||||
|     protected string $slug        = 'collection'; | ||||
|     protected string $plural_slug = 'collections'; | ||||
|     protected string $page_title  = 'collections'; | ||||
|  | ||||
|     abstract public function getCollectionUrl(int $owner_id, string $owner_nickname, int $collection_id): string; | ||||
|     abstract public function getCollectionItems(int $owner_id, $collection_id): array; | ||||
|     abstract public function getCollectionsBy(int $owner_id): array; | ||||
|     abstract public function getCollectionBy(int $owner_id, int $collection_id); | ||||
|     abstract public function createCollection(int $owner_id, string $name); | ||||
|  | ||||
|     public function collectionsByActorNickname(Request $request, string $nickname): array | ||||
|     { | ||||
|         $user = DB::findOneBy(LocalUser::class, ['nickname' => $nickname]); | ||||
|         return self::collectionsView($request, $user->getId(), $nickname); | ||||
|     } | ||||
|  | ||||
|     public function collectionsViewByActorId(Request $request, int $id): array | ||||
|     { | ||||
|         return self::collectionsView($request, $id, null); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Generate Collections page | ||||
|      * | ||||
|      * @param int     $id       actor id | ||||
|      * @param ?string $nickname actor nickname | ||||
|      * | ||||
|      * @return array twig template options | ||||
|      */ | ||||
|     public function collectionsView(Request $request, int $id, ?string $nickname): array | ||||
|     { | ||||
|         $collections = $this->getCollectionsBy($id); | ||||
|  | ||||
|         // create collection form | ||||
|         $create = null; | ||||
|         if (Common::user()?->getId() === $id) { | ||||
|             $create = Form::create([ | ||||
|                 ['name', TextType::class, [ | ||||
|                     'label' => _m('Create ' . $this->slug), | ||||
|                     'attr'  => [ | ||||
|                         'placeholder' => _m('Name'), | ||||
|                         'required'    => 'required', | ||||
|                     ], | ||||
|                     'data' => '', | ||||
|                 ]], | ||||
|                 ['add_collection', SubmitType::class, [ | ||||
|                     'label' => _m('Create ' . $this->slug), | ||||
|                     'attr'  => [ | ||||
|                         'title' => _m('Create ' . $this->slug), | ||||
|                     ], | ||||
|                 ]], | ||||
|             ]); | ||||
|             $create->handleRequest($request); | ||||
|             if ($create->isSubmitted() && $create->isValid()) { | ||||
|                 $this->createCollection($id, $create->getData()['name']); | ||||
|                 DB::flush(); | ||||
|                 throw new RedirectException(); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         // We need to inject some functions in twig, | ||||
|         // but I don't want to create an environment for this | ||||
|         // as twig docs suggest in https://twig.symfony.com/doc/2.x/advanced.html#functions. | ||||
|         // | ||||
|         // Instead, I'm using an anonymous class to encapsulate | ||||
|         // the functions and passing that class to the template. | ||||
|         // This is suggested at https://stackoverflow.com/a/50364502. | ||||
|         $fn = new class($id, $nickname, $request, $this, $this->slug) { | ||||
|             private $id; | ||||
|             private $nick; | ||||
|             private $request; | ||||
|             private $parent; | ||||
|             private $slug; | ||||
|  | ||||
|             public function __construct($id, $nickname, $request, $parent, $slug) | ||||
|             { | ||||
|                 $this->id      = $id; | ||||
|                 $this->nick    = $nickname; | ||||
|                 $this->request = $request; | ||||
|                 $this->parent  = $parent; | ||||
|                 $this->slug    = $slug; | ||||
|             } | ||||
|             // there's already a injected function called path, | ||||
|             // that maps to Router::url(name, args), but since | ||||
|             // I want to preserve nicknames, I think it's better | ||||
|             // to use that getUrl function | ||||
|             public function getUrl($cid) | ||||
|             { | ||||
|                 return $this->parent->getCollectionUrl($this->id, $this->nick, $cid); | ||||
|             } | ||||
|             // There are many collections in this page and we need two | ||||
|             // forms for each one of them: one form to edit the collection's | ||||
|             // name and another to remove the collection. | ||||
|  | ||||
|             // creating the edit form | ||||
|             public function editForm($collection) | ||||
|             { | ||||
|                 $edit = Form::create([ | ||||
|                     ['name', TextType::class, [ | ||||
|                         'attr' => [ | ||||
|                             'placeholder' => 'New name', | ||||
|                             'required'    => 'required', | ||||
|                         ], | ||||
|                         'data' => '', | ||||
|                     ]], | ||||
|                     ['update_' . $collection->getId(), SubmitType::class, [ | ||||
|                         'label' => _m('Save'), | ||||
|                         'attr'  => [ | ||||
|                             'title' => _m('Save'), | ||||
|                         ], | ||||
|                     ]], | ||||
|                 ]); | ||||
|                 $edit->handleRequest($this->request); | ||||
|                 if ($edit->isSubmitted() && $edit->isValid()) { | ||||
|                     $collection->setName($edit->getData()['name']); | ||||
|                     DB::persist($collection); | ||||
|                     DB::flush(); | ||||
|                     throw new RedirectException(); | ||||
|                 } | ||||
|                 return $edit->createView(); | ||||
|             } | ||||
|  | ||||
|             // creating the remove form | ||||
|             public function rmForm($collection) | ||||
|             { | ||||
|                 $rm = Form::create([ | ||||
|                     ['remove_' . $collection->getId(), SubmitType::class, [ | ||||
|                         'label' => _m('Delete ' . $this->slug), | ||||
|                         'attr'  => [ | ||||
|                             'title' => _m('Delete ' . $this->slug), | ||||
|                             'class' => 'danger', | ||||
|                         ], | ||||
|                     ]], | ||||
|                 ]); | ||||
|                 $rm->handleRequest($this->request); | ||||
|                 if ($rm->isSubmitted()) { | ||||
|                     DB::remove($collection); | ||||
|                     DB::flush(); | ||||
|                     throw new RedirectException(); | ||||
|                 } | ||||
|                 return $rm->createView(); | ||||
|             } | ||||
|         }; | ||||
|  | ||||
|         return [ | ||||
|             '_template'      => 'collections/collections.html.twig', | ||||
|             'page_title'     => $this->page_title, | ||||
|             'add_collection' => $create?->createView(), | ||||
|             'fn'             => $fn, | ||||
|             'collections'    => $collections, | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
|     public function collectionNotesByNickname(Request $request, string $nickname, int $cid): array | ||||
|     { | ||||
|         $user = DB::findOneBy(LocalUser::class, ['nickname' => $nickname]); | ||||
|         return self::collectionNotesByActorId($request, $user->getId(), $cid); | ||||
|     } | ||||
|  | ||||
|     public function collectionNotesByActorId(Request $request, int $id, int $cid): array | ||||
|     { | ||||
|         $collection = $this->getCollectionBy($id, $cid); | ||||
|         $vars       = $this->getCollectionItems($id, $cid); | ||||
|         return array_merge([ | ||||
|             '_template'  => 'collections/collection.html.twig', | ||||
|             'page_title' => $collection->getName(), | ||||
|         ], $vars); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										157
									
								
								src/Core/Modules/Collection.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										157
									
								
								src/Core/Modules/Collection.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,157 @@ | ||||
| <?php | ||||
|  | ||||
| declare(strict_types = 1); | ||||
|  | ||||
| // {{{ License | ||||
| // This file is part of GNU social - https://www.gnu.org/software/social | ||||
| // | ||||
| // GNU social is free software: you can redistribute it and/or modify | ||||
| // it under the terms of the GNU Affero General Public License as published by | ||||
| // the Free Software Foundation, either version 3 of the License, or | ||||
| // (at your option) any later version. | ||||
| // | ||||
| // GNU social is distributed in the hope that it will be useful, | ||||
| // but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
| // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
| // GNU Affero General Public License for more details. | ||||
| // | ||||
| // You should have received a copy of the GNU Affero General Public License | ||||
| // along with GNU social.  If not, see <http://www.gnu.org/licenses/>. | ||||
| // }}} | ||||
| /** | ||||
|  * Collections for GNU social | ||||
|  * | ||||
|  * @package   GNUsocial | ||||
|  * @category  Plugin | ||||
|  * | ||||
|  * @author    Phablulo <phablulo@gmail.com> | ||||
|  * @copyright 2018-2019, 2021 Free Software Foundation, Inc http://www.fsf.org | ||||
|  * @license   https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later | ||||
|  */ | ||||
|  | ||||
| namespace App\Core\Modules; | ||||
|  | ||||
| use App\Core\DB\DB; | ||||
| use App\Core\Event; | ||||
| use App\Core\Form; | ||||
| use function App\Core\I18n\_m; | ||||
| use App\Entity\Actor; | ||||
| use App\Util\Common; | ||||
| use App\Util\Exception\RedirectException; | ||||
| use App\Util\Formatting; | ||||
| use Symfony\Component\Form\Extension\Core\Type\ChoiceType; | ||||
| use Symfony\Component\Form\Extension\Core\Type\SubmitType; | ||||
| use Symfony\Component\Form\Extension\Core\Type\TextType; | ||||
| use Symfony\Component\HttpFoundation\Request; | ||||
|  | ||||
| abstract class Collection extends Plugin | ||||
| { | ||||
|     protected string $slug        = 'collection'; | ||||
|     protected string $plural_slug = 'collections'; | ||||
|  | ||||
|     abstract protected function createCollection(Actor $owner, array $vars, string $name); | ||||
|     abstract protected function removeItems(Actor $owner, array $vars, $items, array $collections); | ||||
|     abstract protected function addItems(Actor $owner, array $vars, $items, array $collections); | ||||
|  | ||||
|     abstract protected function shouldAddToRightPanel(Actor $user, $vars, Request $request): bool; | ||||
|     abstract protected function getCollectionsBy(Actor $owner, ?array $vars = null, bool $ids_only = false): array; | ||||
|     /** | ||||
|      * Append Collections widget to the right panel. | ||||
|      * It's compose of two forms: one to select collections to add | ||||
|      * the current item to, and another to create a new collection. | ||||
|      */ | ||||
|     public function onAppendRightPanelBlock($vars, Request $request, &$res): bool | ||||
|     { | ||||
|         $user = Common::actor(); | ||||
|         if (\is_null($user)) { | ||||
|             return Event::next; | ||||
|         } | ||||
|         if (!$this->shouldAddToRightPanel($user, $vars, $request)) { | ||||
|             return Event::next; | ||||
|         } | ||||
|         $collections = $this->getCollectionsBy($user); | ||||
|  | ||||
|         // form: add to collection | ||||
|         $choices = []; | ||||
|         foreach ($collections as $col) { | ||||
|             $choices[$col->getName()] = $col->getId(); | ||||
|         } | ||||
|         $already_selected = $this->getCollectionsBy($user, $vars, true); | ||||
|         $add_form         = Form::create([ | ||||
|             ['collections', ChoiceType::class, [ | ||||
|                 'choices'     => $choices, | ||||
|                 'multiple'    => true, | ||||
|                 'required'    => false, | ||||
|                 'choice_attr' => function ($id) use ($already_selected) { | ||||
|                     if (\in_array($id, $already_selected)) { | ||||
|                         return ['selected' => 'selected']; | ||||
|                     } | ||||
|                     return []; | ||||
|                 }, | ||||
|             ]], | ||||
|             ['add', SubmitType::class, [ | ||||
|                 'label' => _m('Add to ' . $this->plural_slug), | ||||
|                 'attr'  => [ | ||||
|                     'title' => _m('Add to ' . $this->plural_slug), | ||||
|                 ], | ||||
|             ]], | ||||
|         ]); | ||||
|         $add_form->handleRequest($request); | ||||
|         if ($add_form->isSubmitted() && $add_form->isValid()) { | ||||
|             $collections = $add_form->getData()['collections']; | ||||
|             $removed     = array_filter($already_selected, fn ($x) => !\in_array($x, $collections)); | ||||
|             $added       = array_filter($collections, fn ($x) => !\in_array($x, $already_selected)); | ||||
|             if (\count($removed) > 0) { | ||||
|                 $this->removeItems($user, $vars, $removed, $collections); | ||||
|             } | ||||
|             if (\count($added) > 0) { | ||||
|                 $this->addItems($user, $vars, $added, $collections); | ||||
|             } | ||||
|             DB::flush(); | ||||
|             throw new RedirectException(); | ||||
|         } | ||||
|  | ||||
|         // form: add to new collection | ||||
|         $create_form = Form::create([ | ||||
|             ['name', TextType::class, [ | ||||
|                 'label' => _m('Add to a new ' . $this->slug), | ||||
|                 'attr'  => [ | ||||
|                     'placeholder' => _m('New ' . $this->slug . ' name'), | ||||
|                     'required'    => 'required', | ||||
|                 ], | ||||
|                 'data' => '', | ||||
|             ]], | ||||
|             ['create', SubmitType::class, [ | ||||
|                 'label' => _m('Create a new ' . $this->slug), | ||||
|                 'attr'  => [ | ||||
|                     'title' => _m('Create a new ' . $this->slug), | ||||
|                 ], | ||||
|             ]], | ||||
|         ]); | ||||
|         $create_form->handleRequest($request); | ||||
|         if ($create_form->isSubmitted() && $create_form->isValid()) { | ||||
|             $name = $create_form->getData()['name']; | ||||
|             $this->createCollection($user, $vars, $name); | ||||
|             DB::flush(); | ||||
|             throw new RedirectException(); | ||||
|         } | ||||
|  | ||||
|         $res[] = Formatting::twigRenderFile( | ||||
|             'collections/widget.html.twig', | ||||
|             [ | ||||
|                 'ctitle'          => _m('Add to ' . $this->plural_slug), | ||||
|                 'user'            => $user, | ||||
|                 'has_collections' => \count($collections) > 0, | ||||
|                 'add_form'        => $add_form->createView(), | ||||
|                 'create_form'     => $create_form->createView(), | ||||
|             ], | ||||
|         ); | ||||
|         return Event::next; | ||||
|     } | ||||
|     public function onEndShowStyles(array &$styles, string $route): bool | ||||
|     { | ||||
|         $styles[] = 'components/Collection/assets/css/widget.css'; | ||||
|         $styles[] = 'components/Collection/assets/css/pages.css'; | ||||
|         return Event::next; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										20
									
								
								templates/collections/collection.html.twig
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								templates/collections/collection.html.twig
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,20 @@ | ||||
| {% extends 'stdgrid.html.twig' %} | ||||
| {% import '/cards/note/view.html.twig' as noteView %} | ||||
|  | ||||
| {% block title %}{{ page_title | trans }}{% endblock %} | ||||
|  | ||||
| {% block stylesheets %} | ||||
|     {{ parent() }} | ||||
|     <link rel="stylesheet" href="{{ asset('assets/default_theme/css/pages/feeds.css') }}" type="text/css"> | ||||
| {% endblock stylesheets %} | ||||
|  | ||||
| {% block body %} | ||||
|     <h1>{{ page_title | trans }}</h1> | ||||
|     {# Backwards compatibility with hAtom 0.1 #} | ||||
|     <main class="feed" tabindex="0" role="feed"> | ||||
|         <div class="h-feed hfeed notes"> | ||||
|             {% block collection_items %} | ||||
|             {% endblock collection_items %} | ||||
|         </div> | ||||
|     </main> | ||||
| {% endblock body %} | ||||
| @@ -9,19 +9,19 @@ | ||||
| {% endblock stylesheets %} | ||||
| 
 | ||||
| {% block body %} | ||||
|     <h1>{{ 'Attachment Collections' | trans }}</h1> | ||||
|     <h1>{{ page_title | trans }}</h1> | ||||
|     {# Backwards compatibility with hAtom 0.1 #} | ||||
|     <main class="feed" tabindex="0" role="feed"> | ||||
|         <div class="h-feed hfeed notes"> | ||||
|             {% if add_collection %} | ||||
|                 <div class="h-entry hentry note attachment-collection-add"> | ||||
|                 <div class="h-entry hentry note collection-add"> | ||||
|                     {{ form(add_collection) }} | ||||
|                 </div> | ||||
|             {% endif %} | ||||
|             <div class="h-entry hentry note attachment-collections-list"> | ||||
|             <div class="h-entry hentry note collections-list"> | ||||
|                 <h3>{{ 'Your collections' | trans }}</h3> | ||||
|                 {% for col in collections %} | ||||
|                     <div class="attachment-collection-item"> | ||||
|                         <div class="collection-item"> | ||||
|                         <a class="name" href="{{ fn.getUrl(col.id) }}">{{ col.name }}</a> | ||||
|                         <details title="Expand if you want to edit the collection's name"> | ||||
|                             <summary> | ||||
| @@ -1,7 +1,7 @@ | ||||
| <section class="section-widget attachment-collections"> | ||||
| <section class="section-widget collections"> | ||||
|     <details class="section-widget-title-details" title="Expand if you want to access more options."> | ||||
|         <summary class="section-title-summary"> | ||||
|             <h2>{% trans %}Add to collection{% endtrans %}</h2> | ||||
|             <h2>{{ctitle}}</h2> | ||||
|             {{ icon('arrow-down', 'icon icon-details-open') | raw }} | ||||
|         </summary> | ||||
|         {% if has_collections %} | ||||
		Reference in New Issue
	
	Block a user