diff --git a/app/Resources/static/themes/material/css/cards.scss b/app/Resources/static/themes/material/css/cards.scss index fe5ae2aaf..918944501 100644 --- a/app/Resources/static/themes/material/css/cards.scss +++ b/app/Resources/static/themes/material/css/cards.scss @@ -287,7 +287,7 @@ a.original:not(.waves-effect) { flex-basis: 5em; align-self: flex-end; float: right; - max-width: 6em; + max-width: 8em; } .tags { diff --git a/app/config/config.yml b/app/config/config.yml index a9afca10b..254344e7c 100644 --- a/app/config/config.yml +++ b/app/config/config.yml @@ -248,6 +248,11 @@ old_sound_rabbit_mq: exchange_options: name: 'wallabag.import.pinboard' type: topic + import_delicious: + connection: default + exchange_options: + name: 'wallabag.import.delicious' + type: topic import_instapaper: connection: default exchange_options: @@ -315,6 +320,15 @@ old_sound_rabbit_mq: name: 'wallabag.import.pinboard' callback: wallabag_import.consumer.amqp.pinboard qos_options: {prefetch_count: "%rabbitmq_prefetch_count%"} + import_delicious: + connection: default + exchange_options: + name: 'wallabag.import.delicious' + type: topic + queue_options: + name: 'wallabag.import.delicious' + callback: wallabag_import.consumer.amqp.delicious + qos_options: {prefetch_count: "%rabbitmq_prefetch_count%"} import_wallabag_v1: connection: default exchange_options: diff --git a/app/config/security.yml b/app/config/security.yml index 5a73440b2..5e74e82fb 100644 --- a/app/config/security.yml +++ b/app/config/security.yml @@ -69,11 +69,11 @@ security: - { path: ^/logout, roles: [IS_AUTHENTICATED_ANONYMOUSLY, IS_AUTHENTICATED_2FA_IN_PROGRESS] } - { path: ^/register, role: IS_AUTHENTICATED_ANONYMOUSLY } - { path: ^/resetting, role: IS_AUTHENTICATED_ANONYMOUSLY } - - { path: /(unread|starred|archive|all).xml$, roles: IS_AUTHENTICATED_ANONYMOUSLY } + - { path: /(unread|starred|archive|annotated|all).xml$, roles: IS_AUTHENTICATED_ANONYMOUSLY } - { path: ^/locale, role: IS_AUTHENTICATED_ANONYMOUSLY } - { path: /tags/(.*).xml$, roles: IS_AUTHENTICATED_ANONYMOUSLY } - { path: ^/feed, roles: IS_AUTHENTICATED_ANONYMOUSLY } - - { path: /(unread|starred|archive).xml$, roles: IS_AUTHENTICATED_ANONYMOUSLY } # For backwards compatibility + - { path: /(unread|starred|archive|annotated).xml$, roles: IS_AUTHENTICATED_ANONYMOUSLY } # For backwards compatibility - { path: ^/share, roles: IS_AUTHENTICATED_ANONYMOUSLY } - { path: ^/settings, roles: ROLE_SUPER_ADMIN } - { path: ^/annotations, roles: ROLE_USER } diff --git a/src/Wallabag/CoreBundle/Controller/EntryController.php b/src/Wallabag/CoreBundle/Controller/EntryController.php index 510dca956..54ee6bf7e 100644 --- a/src/Wallabag/CoreBundle/Controller/EntryController.php +++ b/src/Wallabag/CoreBundle/Controller/EntryController.php @@ -277,12 +277,26 @@ class EntryController extends Controller return $this->showEntries('untagged', $request, $page); } + /** + * Shows entries with annotations for current user. + * + * @param int $page + * + * @Route("/annotated/list/{page}", name="annotated", defaults={"page" = "1"}) + * + * @return \Symfony\Component\HttpFoundation\Response + */ + public function showWithAnnotationsEntriesAction(Request $request, $page) + { + return $this->showEntries('annotated', $request, $page); + } + /** * Shows random entry depending on the given type. * * @param string $type * - * @Route("/{type}/random", name="random_entry", requirements={"type": "unread|starred|archive|untagged|all"}) + * @Route("/{type}/random", name="random_entry", requirements={"type": "unread|starred|archive|untagged|annotated|all"}) * * @return \Symfony\Component\HttpFoundation\RedirectResponse */ @@ -517,6 +531,20 @@ class EntryController extends Controller ); } + /** + * List the entries with the same domain as the current one. + * + * @param int $page + * + * @Route("/domain/{id}/{page}", requirements={"id" = ".+"}, defaults={"page" = 1}, name="same_domain") + * + * @return \Symfony\Component\HttpFoundation\Response + */ + public function getSameDomainEntries(Request $request, $page = 1) + { + return $this->showEntries('same-domain', $request, $page); + } + /** * Global method to retrieve entries depending on the given type * It returns the response to be send. @@ -549,10 +577,16 @@ class EntryController extends Controller $qb = $repository->getBuilderForArchiveByUser($this->getUser()->getId()); $formOptions['filter_archived'] = true; break; + case 'annotated': + $qb = $repository->getBuilderForAnnotationsByUser($this->getUser()->getId()); + break; case 'unread': $qb = $repository->getBuilderForUnreadByUser($this->getUser()->getId()); $formOptions['filter_unread'] = true; break; + case 'same-domain': + $qb = $repository->getBuilderForSameDomainByUser($this->getUser()->getId(), $request->get('id')); + break; case 'all': $qb = $repository->getBuilderForAllByUser($this->getUser()->getId()); break; diff --git a/src/Wallabag/CoreBundle/Controller/ExportController.php b/src/Wallabag/CoreBundle/Controller/ExportController.php index cec8d4999..2db335527 100644 --- a/src/Wallabag/CoreBundle/Controller/ExportController.php +++ b/src/Wallabag/CoreBundle/Controller/ExportController.php @@ -47,7 +47,7 @@ class ExportController extends Controller * * @Route("/export/{category}.{format}", name="export_entries", requirements={ * "format": "epub|mobi|pdf|json|xml|txt|csv", - * "category": "all|unread|starred|archive|tag_entries|untagged|search" + * "category": "all|unread|starred|archive|tag_entries|untagged|search|annotated|same_domain" * }) * * @return \Symfony\Component\HttpFoundation\Response @@ -80,6 +80,13 @@ class ExportController extends Controller ->getResult(); $title = 'Search ' . $searchTerm; + } elseif ('annotated' === $category) { + $entries = $repository->getBuilderForAnnotationsByUser( + $this->getUser()->getId() + )->getQuery() + ->getResult(); + + $title = 'With annotations'; } else { $entries = $repository ->$methodBuilder($this->getUser()->getId()) diff --git a/src/Wallabag/CoreBundle/Form/Type/EntryFilterType.php b/src/Wallabag/CoreBundle/Form/Type/EntryFilterType.php index 176b7ea07..ba2f53662 100644 --- a/src/Wallabag/CoreBundle/Form/Type/EntryFilterType.php +++ b/src/Wallabag/CoreBundle/Form/Type/EntryFilterType.php @@ -93,7 +93,7 @@ class EntryFilterType extends AbstractType 'apply_filter' => function (QueryInterface $filterQuery, $field, $values) { $value = $values['value']; if (\strlen($value) <= 2 || empty($value)) { - return; + return false; } $expression = $filterQuery->getExpr()->like($field, $filterQuery->getExpr()->lower($filterQuery->getExpr()->literal('%' . $value . '%'))); @@ -105,7 +105,7 @@ class EntryFilterType extends AbstractType 'apply_filter' => function (QueryInterface $filterQuery, $field, $values) { $value = $values['value']; if (false === \array_key_exists($value, Response::$statusTexts)) { - return; + return false; } $paramName = sprintf('%s', str_replace('.', '_', $field)); @@ -129,7 +129,7 @@ class EntryFilterType extends AbstractType 'data' => $options['filter_unread'], 'apply_filter' => function (QueryInterface $filterQuery, $field, $values) { if (false === $values['value']) { - return; + return false; } $expression = $filterQuery->getExpr()->eq('e.isArchived', 'false'); @@ -137,10 +137,22 @@ class EntryFilterType extends AbstractType return $filterQuery->createCondition($expression); }, ]) + ->add('isAnnotated', CheckboxFilterType::class, [ + 'label' => 'entry.filters.annotated_label', + 'data' => $options['filter_annotated'], + 'apply_filter' => function (QueryInterface $filterQuery, $field, $values) { + if (false === $values['value']) { + return false; + } + + $qb = $filterQuery->getQueryBuilder(); + $qb->innerJoin('e.annotations', 'a'); + }, + ]) ->add('previewPicture', CheckboxFilterType::class, [ 'apply_filter' => function (QueryInterface $filterQuery, $field, $values) { if (false === $values['value']) { - return; + return false; } $expression = $filterQuery->getExpr()->isNotNull($field); @@ -152,7 +164,7 @@ class EntryFilterType extends AbstractType ->add('isPublic', CheckboxFilterType::class, [ 'apply_filter' => function (QueryInterface $filterQuery, $field, $values) { if (false === $values['value']) { - return; + return false; } // is_public isn't a real field @@ -183,6 +195,7 @@ class EntryFilterType extends AbstractType 'filter_archived' => false, 'filter_starred' => false, 'filter_unread' => false, + 'filter_annotated' => false, ]); } } diff --git a/src/Wallabag/CoreBundle/Repository/EntryRepository.php b/src/Wallabag/CoreBundle/Repository/EntryRepository.php index 14a38d329..7d400b7dc 100644 --- a/src/Wallabag/CoreBundle/Repository/EntryRepository.php +++ b/src/Wallabag/CoreBundle/Repository/EntryRepository.php @@ -39,7 +39,34 @@ class EntryRepository extends EntityRepository return $this ->getSortedQueryBuilderByUser($userId) ->andWhere('e.isArchived = false') - ; + ; + } + + /** + * Retrieves entries with the same domain. + * + * @param int $userId + * @param int $entryId + * + * @return QueryBuilder + */ + public function getBuilderForSameDomainByUser($userId, $entryId) + { + $queryBuilder = $this->createQueryBuilder('e'); + + return $this + ->getSortedQueryBuilderByUser($userId) + ->andWhere('e.id <> :entryId')->setParameter('entryId', $entryId) + ->andWhere( + $queryBuilder->expr()->in( + 'e.domainName', + $this + ->createQueryBuilder('e2') + ->select('e2.domainName') + ->where('e2.id = :entryId')->setParameter('entryId', $entryId) + ->getDQL() + ) + ); } /** @@ -115,6 +142,21 @@ class EntryRepository extends EntityRepository return $this->sortQueryBuilder($this->getRawBuilderForUntaggedByUser($userId)); } + /** + * Retrieve entries with annotations for a user. + * + * @param int $userId + * + * @return QueryBuilder + */ + public function getBuilderForAnnotationsByUser($userId) + { + return $this + ->getSortedQueryBuilderByUser($userId) + ->innerJoin('e.annotations', 'a') + ; + } + /** * Retrieve untagged entries for a user. * @@ -552,6 +594,10 @@ class EntryRepository extends EntityRepository $qb->leftJoin('e.tags', 't'); $qb->andWhere('t.id is null'); break; + case 'annotated': + $qb->leftJoin('e.annotations', 'a'); + $qb->andWhere('a.id is not null'); + break; } $ids = $qb->getQuery()->getArrayResult(); diff --git a/src/Wallabag/CoreBundle/Resources/translations/messages.en.yml b/src/Wallabag/CoreBundle/Resources/translations/messages.en.yml index 298baf8e1..d4d9cf557 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/messages.en.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/messages.en.yml @@ -19,6 +19,7 @@ menu: starred: Starred archive: Archive all_articles: All entries + with_annotations: With annotations config: Config tags: Tags internal_settings: Internal Settings @@ -220,10 +221,12 @@ entry: starred: Starred entries archived: Archived entries filtered: Filtered entries + with_annotations: Entries with annotations filtered_tags: 'Filtered by tags:' filtered_search: 'Filtered by search:' untagged: Untagged entries all: All entries + same_domain: Same domain list: number_on_the_page: '{0} There are no entries.|{1} There is one entry.|]1,Inf[ There are %count% entries.' reading_time: estimated reading time @@ -237,6 +240,7 @@ entry: toogle_as_star: Toggle starred delete: Delete export_title: Export + show_same_domain: Show articles with the same domain assign_search_tag: Assign this search as a tag to each result filters: title: Filters @@ -244,6 +248,7 @@ entry: archived_label: Archived starred_label: Starred unread_label: Unread + annotated_label: Annotated preview_picture_label: Has a preview picture preview_picture_help: Preview picture is_public_label: Has a public link @@ -481,12 +486,12 @@ import: description: Pocket import isn't configured. admin_message: You need to define %keyurls%a pocket_consumer_key%keyurle%. user_message: Your server admin needs to define an API Key for Pocket. - authorize_message: You can import your data from your Pocket account. You just have to click on the below button and authorize the application to connect to getpocket.com. + authorize_message: You can import your data from your Pocket account. You just have to click on the button below and authorize the application to connect to getpocket.com. connect_to_pocket: Connect to Pocket and import data wallabag_v1: page_title: Import > Wallabag v1 description: This importer will import all your wallabag v1 articles. On your config page, click on "JSON export" in the "Export your wallabag data" section. You will have a "wallabag-export-1-xxxx-xx-xx.json" file. - how_to: Please select your wallabag export and click on the below button to upload and import it. + how_to: Please select your wallabag export and click on the button below to upload and import it. wallabag_v2: page_title: Import > Wallabag v2 description: This importer will import all your wallabag v2 articles. Go to All articles, then, on the export sidebar, click on "JSON". You will have a "All articles.json" file. @@ -496,7 +501,7 @@ import: readability: page_title: Import > Readability description: This importer will import all your Readability articles. On the tools (https://www.readability.com/tools/) page, click on "Export your data" in the "Data Export" section. You will received an email to download a json (which does not end with .json in fact). - how_to: Please select your Readability export and click on the below button to upload and import it. + how_to: Please select your Readability export and click on the button below to upload and import it. worker: enabled: 'Import is made asynchronously. Once the import task is started, an external worker will handle jobs one at a time. The current service is:' download_images_warning: You enabled downloading images for your articles. Combined with classic import it can take ages to proceed (or maybe failed). We strongly recommend to enable asynchronous import to avoid errors. @@ -511,11 +516,15 @@ import: instapaper: page_title: Import > Instapaper description: This importer will import all your Instapaper articles. On the settings (https://www.instapaper.com/user) page, click on "Download .CSV file" in the "Export" section. A CSV file will be downloaded (like "instapaper-export.csv"). - how_to: Please select your Instapaper export and click on the below button to upload and import it. + how_to: Please select your Instapaper export and click on the button below to upload and import it. pinboard: page_title: Import > Pinboard description: This importer will import all your Pinboard articles. On the backup (https://pinboard.in/settings/backup) page, click on "JSON" in the "Bookmarks" section. A JSON file will be downloaded (like "pinboard_export"). - how_to: Please select your Pinboard export and click on the below button to upload and import it. + how_to: Please select your Pinboard export and click on the button below to upload and import it. + delicious: + page_title: Import > del.icio.us + description: This importer will import all your Delicious bookmarks. Since 2021, you can export again your data from it using the export page (https://del.icio.us/export). Choose the "JSON" format and download it (like "delicious_export.2021.02.06_21.10.json"). + how_to: Please select your Delicious export and click on the button below to upload and import it. developer: page_title: API clients management welcome_message: Welcome to the wallabag API diff --git a/src/Wallabag/CoreBundle/Resources/translations/messages.fa.yml b/src/Wallabag/CoreBundle/Resources/translations/messages.fa.yml index 44e358cab..efc84762d 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/messages.fa.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/messages.fa.yml @@ -32,6 +32,7 @@ menu: site_credentials: اعتبارنامه‌های وب‌گاه users_management: مدیریت کاربران developer: مدیریت کارخواه‌های API + quickstart: "Quickstart" top: add_new_entry: افزودن مقالهٔ تازه search: جستجو diff --git a/src/Wallabag/CoreBundle/Resources/translations/messages.fr.yml b/src/Wallabag/CoreBundle/Resources/translations/messages.fr.yml index 7a18b833a..6a5188879 100644 --- a/src/Wallabag/CoreBundle/Resources/translations/messages.fr.yml +++ b/src/Wallabag/CoreBundle/Resources/translations/messages.fr.yml @@ -19,6 +19,7 @@ menu: starred: Favoris archive: Lus all_articles: Tous les articles + with_annotations: Avec annotations config: Configuration tags: Étiquettes internal_settings: Configuration interne @@ -220,6 +221,7 @@ entry: starred: Articles favoris archived: Articles lus filtered: Articles filtrés + with_annotations: Articles avec annotations filtered_tags: 'Articles filtrés par étiquettes :' filtered_search: 'Articles filtrés par recherche :' untagged: Article sans étiquette @@ -243,6 +245,7 @@ entry: archived_label: Lus starred_label: Favoris unread_label: Non lus + annotated_label: Annotés preview_picture_label: A une photo preview_picture_help: Photo is_public_label: A un lien public diff --git a/src/Wallabag/CoreBundle/Resources/views/themes/common/Entry/_title.html.twig b/src/Wallabag/CoreBundle/Resources/views/themes/common/Entry/_title.html.twig index 5c17e9f7b..3c5fad1e9 100644 --- a/src/Wallabag/CoreBundle/Resources/views/themes/common/Entry/_title.html.twig +++ b/src/Wallabag/CoreBundle/Resources/views/themes/common/Entry/_title.html.twig @@ -12,6 +12,10 @@ {{ 'entry.page_titles.filtered_tags'|trans }} {{ filter }} {% elseif currentRoute == 'untagged' %} {{ 'entry.page_titles.untagged'|trans }} +{% elseif currentRoute == 'same_domain' %} + {{ 'entry.page_titles.same_domain'|trans }} +{% elseif currentRoute == 'annotated' %} + {{ 'entry.page_titles.with_annotations'|trans }} {% else %} {{ 'entry.page_titles.unread'|trans }} {% endif %} diff --git a/src/Wallabag/CoreBundle/Resources/views/themes/material/Entry/_card_actions.html.twig b/src/Wallabag/CoreBundle/Resources/views/themes/material/Entry/_card_actions.html.twig index 076111244..c2c3fff5e 100644 --- a/src/Wallabag/CoreBundle/Resources/views/themes/material/Entry/_card_actions.html.twig +++ b/src/Wallabag/CoreBundle/Resources/views/themes/material/Entry/_card_actions.html.twig @@ -8,6 +8,9 @@