diff --git a/components/Collection/Collection.php b/components/Collection/Collection.php deleted file mode 100644 index e642fb6872..0000000000 --- a/components/Collection/Collection.php +++ /dev/null @@ -1,11 +0,0 @@ - $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; - } } diff --git a/plugins/AttachmentCollections/Controller/Controller.php b/plugins/AttachmentCollections/Controller/Controller.php index 573b642604..bef3c8d870 100644 --- a/plugins/AttachmentCollections/Controller/Controller.php +++ b/plugins/AttachmentCollections/Controller/Controller.php @@ -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]); + } } diff --git a/components/Collection/Entity/Collection.php b/plugins/AttachmentCollections/Entity/AttachmentCollection.php similarity index 94% rename from components/Collection/Entity/Collection.php rename to plugins/AttachmentCollections/Entity/AttachmentCollection.php index 444cae4cd3..2c1f976bcd 100644 --- a/components/Collection/Entity/Collection.php +++ b/plugins/AttachmentCollections/Entity/AttachmentCollection.php @@ -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 diff --git a/plugins/AttachmentCollections/templates/AttachmentCollections/collection.html.twig b/plugins/AttachmentCollections/templates/AttachmentCollections/collection.html.twig index 1e8ac4d579..acefeb8b90 100644 --- a/plugins/AttachmentCollections/templates/AttachmentCollections/collection.html.twig +++ b/plugins/AttachmentCollections/templates/AttachmentCollections/collection.html.twig @@ -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() }} - -{% endblock stylesheets %} - -{% block body %} -

{{ page_title | trans }}

- {# Backwards compatibility with hAtom 0.1 #} -
-
- {% for key, attachment in attachments %} -
- {% include '/cards/attachments/view.html.twig' with {'attachment': attachment, 'note': bare_notes[key], 'title': attachment.getBestTitle(bare_notes[key])} only %} - {{ 'Download link' | trans }} -
- {% else %} -

{% trans %}No attachments here.{% endtrans %}

- {% endfor %} -
-
-{% endblock body %} +{% block collection_items %} + {% for key, attachment in attachments %} +
+ {% include '/cards/attachments/view.html.twig' with {'attachment': attachment, 'note': bare_notes[key], 'title': attachment.getBestTitle(bare_notes[key])} only %} + {{ 'Download link' | trans }} +
+ {% else %} +

{% trans %}No attachments here.{% endtrans %}

+ {% endfor %} +{% endblock collection_items %} diff --git a/public/components/Collection/assets/css/pages.css b/public/components/Collection/assets/css/pages.css new file mode 100644 index 0000000000..c1db52eb8d --- /dev/null +++ b/public/components/Collection/assets/css/pages.css @@ -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; +} diff --git a/public/components/Collection/assets/css/widget.css b/public/components/Collection/assets/css/widget.css new file mode 100644 index 0000000000..a0e5135fb6 --- /dev/null +++ b/public/components/Collection/assets/css/widget.css @@ -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; +} diff --git a/src/Core/Controller/CollectionController.php b/src/Core/Controller/CollectionController.php new file mode 100644 index 0000000000..9bed76323f --- /dev/null +++ b/src/Core/Controller/CollectionController.php @@ -0,0 +1,216 @@ +. +// }}} +/** + * Collections Controller for GNU social + * + * @package GNUsocial + * @category Plugin + * + * @author Phablulo + * @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); + } +} diff --git a/src/Core/Modules/Collection.php b/src/Core/Modules/Collection.php new file mode 100644 index 0000000000..47ba9e0999 --- /dev/null +++ b/src/Core/Modules/Collection.php @@ -0,0 +1,157 @@ +. +// }}} +/** + * Collections for GNU social + * + * @package GNUsocial + * @category Plugin + * + * @author Phablulo + * @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; + } +} diff --git a/templates/collections/collection.html.twig b/templates/collections/collection.html.twig new file mode 100644 index 0000000000..bc218848a8 --- /dev/null +++ b/templates/collections/collection.html.twig @@ -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() }} + +{% endblock stylesheets %} + +{% block body %} +

{{ page_title | trans }}

+ {# Backwards compatibility with hAtom 0.1 #} +
+
+ {% block collection_items %} + {% endblock collection_items %} +
+
+{% endblock body %} diff --git a/plugins/AttachmentCollections/templates/AttachmentCollections/collections.html.twig b/templates/collections/collections.html.twig similarity index 86% rename from plugins/AttachmentCollections/templates/AttachmentCollections/collections.html.twig rename to templates/collections/collections.html.twig index edf42966df..7dd761f3aa 100644 --- a/plugins/AttachmentCollections/templates/AttachmentCollections/collections.html.twig +++ b/templates/collections/collections.html.twig @@ -9,19 +9,19 @@ {% endblock stylesheets %} {% block body %} -

{{ 'Attachment Collections' | trans }}

+

{{ page_title | trans }}

{# Backwards compatibility with hAtom 0.1 #}
{% if add_collection %} -
+
{{ form(add_collection) }}
{% endif %} -
+

{{ 'Your collections' | trans }}

{% for col in collections %} -
+
{{ col.name }}
diff --git a/plugins/AttachmentCollections/templates/AttachmentCollections/widget.html.twig b/templates/collections/widget.html.twig similarity index 90% rename from plugins/AttachmentCollections/templates/AttachmentCollections/widget.html.twig rename to templates/collections/widget.html.twig index 25fd7f9693..9b33f30229 100644 --- a/plugins/AttachmentCollections/templates/AttachmentCollections/widget.html.twig +++ b/templates/collections/widget.html.twig @@ -1,7 +1,7 @@ -
+
-

{% trans %}Add to collection{% endtrans %}

+

{{ctitle}}

{{ icon('arrow-down', 'icon icon-details-open') | raw }}
{% if has_collections %}