Просмотр исходного кода

Auto-label (#5954)

Add labels automatically to incoming articles
fix https://github.com/FreshRSS/FreshRSS/issues/2380
fix https://github.com/FreshRSS/FreshRSS/issues/2420
fix https://github.com/FreshRSS/FreshRSS/issues/3279
fix https://github.com/FreshRSS/FreshRSS/discussions/4947
fix https://github.com/FreshRSS/FreshRSS/issues/5728
fix https://github.com/FreshRSS/FreshRSS/issues/5599
Alexandre Alapetite 2 лет назад
Родитель
Сommit
70e71b8364
61 измененных файлов с 353 добавлено и 119 удалено
  1. 69 17
      app/Controllers/feedController.php
  2. 45 1
      app/Controllers/tagController.php
  3. 12 1
      app/Models/Entry.php
  4. 11 8
      app/Models/EntryDAO.php
  5. 7 4
      app/Models/FilterActionsTrait.php
  6. 1 1
      app/Models/Tag.php
  7. 29 0
      app/Models/TagDAO.php
  8. 1 0
      app/Models/View.php
  9. 2 0
      app/i18n/cz/feedback.php
  10. 1 0
      app/i18n/cz/sub.php
  11. 2 0
      app/i18n/de/feedback.php
  12. 1 0
      app/i18n/de/sub.php
  13. 2 0
      app/i18n/el/feedback.php
  14. 1 0
      app/i18n/el/sub.php
  15. 2 0
      app/i18n/en-us/feedback.php
  16. 1 0
      app/i18n/en-us/sub.php
  17. 2 0
      app/i18n/en/feedback.php
  18. 1 0
      app/i18n/en/sub.php
  19. 2 0
      app/i18n/es/feedback.php
  20. 1 0
      app/i18n/es/sub.php
  21. 2 0
      app/i18n/fa/feedback.php
  22. 1 0
      app/i18n/fa/sub.php
  23. 3 1
      app/i18n/fr/feedback.php
  24. 1 0
      app/i18n/fr/sub.php
  25. 2 0
      app/i18n/he/feedback.php
  26. 1 0
      app/i18n/he/sub.php
  27. 2 0
      app/i18n/hu/feedback.php
  28. 1 0
      app/i18n/hu/sub.php
  29. 2 0
      app/i18n/id/feedback.php
  30. 1 0
      app/i18n/id/sub.php
  31. 2 0
      app/i18n/it/feedback.php
  32. 1 0
      app/i18n/it/sub.php
  33. 2 0
      app/i18n/ja/feedback.php
  34. 1 0
      app/i18n/ja/sub.php
  35. 2 0
      app/i18n/ko/feedback.php
  36. 1 0
      app/i18n/ko/sub.php
  37. 2 0
      app/i18n/lv/feedback.php
  38. 1 0
      app/i18n/lv/sub.php
  39. 2 0
      app/i18n/nl/feedback.php
  40. 1 0
      app/i18n/nl/sub.php
  41. 2 0
      app/i18n/oc/feedback.php
  42. 1 0
      app/i18n/oc/sub.php
  43. 2 0
      app/i18n/pl/feedback.php
  44. 1 0
      app/i18n/pl/sub.php
  45. 2 0
      app/i18n/pt-br/feedback.php
  46. 1 0
      app/i18n/pt-br/sub.php
  47. 2 0
      app/i18n/ru/feedback.php
  48. 1 0
      app/i18n/ru/sub.php
  49. 2 0
      app/i18n/sk/feedback.php
  50. 1 0
      app/i18n/sk/sub.php
  51. 2 0
      app/i18n/tr/feedback.php
  52. 1 0
      app/i18n/tr/sub.php
  53. 2 0
      app/i18n/zh-cn/feedback.php
  54. 1 0
      app/i18n/zh-cn/sub.php
  55. 2 0
      app/i18n/zh-tw/feedback.php
  56. 1 0
      app/i18n/zh-tw/sub.php
  57. 1 3
      app/layout/aside_feed.phtml
  58. 8 56
      app/views/tag/index.phtml
  59. 69 0
      app/views/tag/update.phtml
  60. 26 26
      docs/fr/users/03_Main_view.md
  61. 1 1
      docs/i18n/freshrss.fr.po

+ 69 - 17
app/Controllers/feedController.php

@@ -679,29 +679,81 @@ class FreshRSS_feed_Controller extends FreshRSS_ActionController {
 		return [$updated_feeds, reset($feeds) ?: null, $nb_new_articles];
 	}
 
-	public static function commitNewEntries(): bool {
-		$entryDAO = FreshRSS_Factory::createEntryDao();
-		if (!$entryDAO->inTransaction()) {
-			$entryDAO->beginTransaction();
+	/**
+	 * @param array<int,int> $newUnreadEntriesPerFeed
+	 * @return int|false The number of articles marked as read, of false if error
+	 */
+	private static function keepMaxUnreads(array $newUnreadEntriesPerFeed) {
+		$affected = 0;
+		$feedDAO = FreshRSS_Factory::createFeedDao();
+		$feeds = $feedDAO->listFeedsOrderUpdate(-1);
+		foreach ($feeds as $feed) {
+			if (!empty($newUnreadEntriesPerFeed[$feed->id()]) && $feed->keepMaxUnread() !== null &&
+				($feed->nbNotRead() + $newUnreadEntriesPerFeed[$feed->id()] > $feed->keepMaxUnread())) {
+				Minz_Log::debug('New unread entries (' . ($feed->nbNotRead() + $newUnreadEntriesPerFeed[$feed->id()]) . ') exceeding max number of ' .
+					$feed->keepMaxUnread() .  ' for [' . $feed->url(false) . ']');
+				$n = $feed->markAsReadMaxUnread();
+				if ($n === false) {
+					$affected = false;
+					break;
+				} else {
+					$affected += $n;
+				}
+			}
+		}
+		if ($feedDAO->updateCachedValues() === false) {
+			$affected = false;
 		}
+		return $affected;
+	}
 
-		$newUnreadEntriesPerFeed = $entryDAO->newUnreadEntriesPerFeed();
-		if ($entryDAO->commitNewEntries()) {
-			$feedDAO = FreshRSS_Factory::createFeedDao();
-			$feeds = $feedDAO->listFeedsOrderUpdate(-1);
-			foreach ($feeds as $feed) {
-				if (!empty($newUnreadEntriesPerFeed[$feed->id()]) && $feed->keepMaxUnread() !== null &&
-					($feed->nbNotRead() + $newUnreadEntriesPerFeed[$feed->id()] > $feed->keepMaxUnread())) {
-					Minz_Log::debug('New unread entries (' . ($feed->nbNotRead() + $newUnreadEntriesPerFeed[$feed->id()]) . ') exceeding max number of ' .
-						$feed->keepMaxUnread() .  ' for [' . $feed->url(false) . ']');
-					$feed->markAsReadMaxUnread();
+	/**
+	 * Auto-add labels to new articles.
+	 * @param int $nbNewEntries The number of top recent entries to process.
+	 * @return int|false The number of new labels added, or false in case of error.
+	 */
+	private static function applyLabelActions(int $nbNewEntries) {
+		$tagDAO = FreshRSS_Factory::createTagDao();
+		$labels = $tagDAO->listTags() ?: [];
+		$labels = array_filter($labels, static function (FreshRSS_Tag $label) {
+			return !empty($label->filtersAction('label'));
+		});
+		if (count($labels) <= 0) {
+			return 0;
+		}
+
+		$entryDAO = FreshRSS_Factory::createEntryDao();
+		/** @var array<array{id_tag:int,id_entry:string}> $applyLabels */
+		$applyLabels = [];
+		foreach (FreshRSS_Entry::fromTraversable($entryDAO->selectAll($nbNewEntries)) as $entry) {
+			foreach ($labels as $label) {
+				$label->applyFilterActions($entry, $applyLabel);
+				if ($applyLabel) {
+					$applyLabels[] = [
+						'id_tag' => $label->id(),
+						'id_entry' => $entry->id(),
+					];
 				}
 			}
-			$feedDAO->updateCachedValues();
 		}
+		return $tagDAO->tagEntries($applyLabels);
+	}
 
-		if ($entryDAO->inTransaction()) {
-			$entryDAO->commit();
+	public static function commitNewEntries(): bool {
+		$entryDAO = FreshRSS_Factory::createEntryDao();
+		$newUnreadEntriesPerFeed = $entryDAO->newUnreadEntriesPerFeed();
+		$nbNewEntries = array_sum($newUnreadEntriesPerFeed);
+		if ($nbNewEntries > 0) {
+			if (!$entryDAO->inTransaction()) {
+				$entryDAO->beginTransaction();
+			}
+			if ($entryDAO->commitNewEntries()) {
+				self::keepMaxUnreads($newUnreadEntriesPerFeed);
+				self::applyLabelActions($nbNewEntries);
+			}
+			if ($entryDAO->inTransaction()) {
+				$entryDAO->commit();
+			}
 		}
 
 		$databaseDAO = FreshRSS_Factory::createDatabaseDAO();

+ 45 - 1
app/Controllers/tagController.php

@@ -21,7 +21,6 @@ class FreshRSS_tag_Controller extends FreshRSS_ActionController {
 		$this->ajax = Minz_Request::paramBoolean('ajax');
 		if ($this->ajax) {
 			$this->view->_layout(null);
-			Minz_Request::_param('ajax');
 		}
 	}
 
@@ -84,6 +83,51 @@ class FreshRSS_tag_Controller extends FreshRSS_ActionController {
 		}
 	}
 
+
+	/**
+	 * This action updates the given tag.
+	 */
+	public function updateAction(): void {
+		if (Minz_Request::paramBoolean('ajax')) {
+			$this->view->_layout(null);
+		}
+
+		$tagDAO = FreshRSS_Factory::createTagDao();
+
+		$id = Minz_Request::paramInt('id');
+		$tag = $tagDAO->searchById($id);
+		if ($id === 0 || $tag === null) {
+			Minz_Error::error(404);
+			return;
+		}
+		$this->view->tag = $tag;
+
+		FreshRSS_View::prependTitle($tag->name() . ' · ' . _t('sub.title') . ' · ');
+
+		if (Minz_Request::isPost()) {
+			invalidateHttpCache();
+			$ok = true;
+
+			if ($tag->name() !== Minz_Request::paramString('name')) {
+				$ok = $tagDAO->updateTagName($tag->id(), Minz_Request::paramString('name')) !== false;
+			}
+
+			if ($ok) {
+				$tag->_filtersAction('label', Minz_Request::paramTextToArray('filteractions_label'));
+				$ok = $tagDAO->updateTagAttributes($tag->id(), $tag->attributes()) !== false;
+			}
+
+			invalidateHttpCache();
+
+			$url_redirect = ['c' => 'tag', 'a' => 'update', 'params' => ['id' => $id]];
+			if ($ok) {
+				Minz_Request::good(_t('feedback.tag.updated'), $url_redirect);
+			} else {
+				Minz_Request::bad(_t('feedback.tag.error'), $url_redirect);
+			}
+		}
+	}
+
 	public function getTagsForEntryAction(): void {
 		if (!FreshRSS_Auth::hasAccess() && !FreshRSS_Context::systemConf()->allow_anonymous) {
 			Minz_Error::error(403);

+ 12 - 1
app/Models/Entry.php

@@ -50,7 +50,7 @@ class FreshRSS_Entry extends Minz_Model {
 	}
 
 	/** @param array{'id'?:string,'id_feed'?:int,'guid'?:string,'title'?:string,'author'?:string,'content'?:string,'link'?:string,'date'?:int|string,'lastSeen'?:int,
-	 *		'hash'?:string,'is_read'?:bool|int,'is_favorite'?:bool|int,'tags'?:string|array<string>,'attributes'?:string,'thumbnail'?:string,'timestamp'?:string} $dao */
+	 *		'hash'?:string,'is_read'?:bool|int,'is_favorite'?:bool|int,'tags'?:string|array<string>,'attributes'?:?string,'thumbnail'?:string,'timestamp'?:string} $dao */
 	public static function fromArray(array $dao): FreshRSS_Entry {
 		FreshRSS_DatabaseDAO::pdoInt($dao, ['id_feed', 'date', 'lastSeen', 'is_read', 'is_favorite']);
 
@@ -98,6 +98,17 @@ class FreshRSS_Entry extends Minz_Model {
 		return $entry;
 	}
 
+	/**
+	 * @param Traversable<array{'id'?:string,'id_feed'?:int,'guid'?:string,'title'?:string,'author'?:string,'content'?:string,'link'?:string,'date'?:int|string,'lastSeen'?:int,
+	 *	'hash'?:string,'is_read'?:bool|int,'is_favorite'?:bool|int,'tags'?:string|array<string>,'attributes'?:?string,'thumbnail'?:string,'timestamp'?:string}> $daos
+	 * @return Traversable<FreshRSS_Entry>
+	 */
+	public static function fromTraversable(Traversable $daos): Traversable {
+		foreach ($daos as $dao) {
+			yield FreshRSS_Entry::fromArray($dao);
+		}
+	}
+
 	public function id(): string {
 		return $this->id;
 	}

+ 11 - 8
app/Models/EntryDAO.php

@@ -95,7 +95,7 @@ SQL;
 	private $addEntryPrepared = false;
 
 	/** @param array{'id':string,'guid':string,'title':string,'author':string,'content':string,'link':string,'date':int,'lastSeen':int,'hash':string,
-	 *		'is_read':bool|int|null,'is_favorite':bool|int|null,'id_feed':int,'tags':string,'attributes':array<string,mixed>} $valuesTmp */
+	 *		'is_read':bool|int|null,'is_favorite':bool|int|null,'id_feed':int,'tags':string,'attributes'?:null|string|array<string,mixed>} $valuesTmp */
 	public function addEntry(array $valuesTmp, bool $useTmpTable = true): bool {
 		if ($this->addEntryPrepared == null) {
 			$sql = static::sqlIgnoreConflict(
@@ -693,19 +693,22 @@ SQL;
 	}
 
 	/** @return Traversable<array{'id':string,'guid':string,'title':string,'author':string,'content':string,'link':string,'date':int,'lastSeen':int,
-	 *		'hash':string,'is_read':?bool,'is_favorite':?bool,'id_feed':int,'tags':string,'attributes':array<string,mixed>}> */
-	public function selectAll(): Traversable {
+	 *		'hash':string,'is_read':bool,'is_favorite':bool,'id_feed':int,'tags':string,'attributes':?string}> */
+	public function selectAll(?int $limit = null): Traversable {
 		$content = static::isCompressed() ? 'UNCOMPRESS(content_bin) AS content' : 'content';
 		$hash = static::sqlHexEncode('hash');
 		$sql = <<<SQL
 SELECT id, guid, title, author, {$content}, link, date, `lastSeen`, {$hash} AS hash, is_read, is_favorite, id_feed, tags, attributes
 FROM `_entry`
 SQL;
+		if (is_int($limit) && $limit >= 0) {
+			$sql .= ' ORDER BY id DESC LIMIT ' . $limit;
+		}
 		$stm = $this->pdo->query($sql);
 		if ($stm != false) {
 			while ($row = $stm->fetch(PDO::FETCH_ASSOC)) {
 				/** @var array{'id':string,'guid':string,'title':string,'author':string,'content':string,'link':string,'date':int,'lastSeen':int,
-				 *	'hash':string,'is_read':?bool,'is_favorite':?bool,'id_feed':int,'tags':string,'attributes':array<string,mixed>} $row */
+				 *	'hash':string,'is_read':bool,'is_favorite':bool,'id_feed':int,'tags':string,'attributes':?string} $row */
 				yield $row;
 			}
 		} else {
@@ -727,7 +730,7 @@ FROM `_entry` WHERE id_feed=:id_feed AND guid=:guid
 SQL;
 		$res = $this->fetchAssoc($sql, [':id_feed' => $id_feed, ':guid' => $guid]);
 		/** @var array<array{'id':string,'id_feed':int,'guid':string,'title':string,'author':string,'content':string,'link':string,'date':int,
-		 *		'is_read':int,'is_favorite':int,'tags':string,'attributes'?:string}> $res */
+		 *		'is_read':int,'is_favorite':int,'tags':string,'attributes':?string}> $res */
 		return isset($res[0]) ? FreshRSS_Entry::fromArray($res[0]) : null;
 	}
 
@@ -740,7 +743,7 @@ FROM `_entry` WHERE id=:id
 SQL;
 		$res = $this->fetchAssoc($sql, [':id' => $id]);
 		/** @var array<array{'id':string,'id_feed':int,'guid':string,'title':string,'author':string,'content':string,'link':string,'date':int,
-		 *		'is_read':int,'is_favorite':int,'tags':string,'attributes'?:string}> $res */
+		 *		'is_read':int,'is_favorite':int,'tags':string,'attributes':?string}> $res */
 		return isset($res[0]) ? FreshRSS_Entry::fromArray($res[0]) : null;
 	}
 
@@ -1167,7 +1170,7 @@ SQL;
 			while ($row = $stm->fetch(PDO::FETCH_ASSOC)) {
 				if (is_array($row)) {
 					/** @var array{'id':string,'id_feed':int,'guid':string,'title':string,'author':string,'content':string,'link':string,'date':int,
-					 *		'hash':string,'is_read':int,'is_favorite':int,'tags':string,'attributes'?:string} $row */
+					 *		'hash':string,'is_read':int,'is_favorite':int,'tags':string,'attributes'?:?string} $row */
 					yield FreshRSS_Entry::fromArray($row);
 				}
 			}
@@ -1212,7 +1215,7 @@ SQL;
 		while ($row = $stm->fetch(PDO::FETCH_ASSOC)) {
 			if (is_array($row)) {
 				/** @var array{'id':string,'id_feed':int,'guid':string,'title':string,'author':string,'content':string,'link':string,'date':int,
-				 *		'hash':string,'is_read':int,'is_favorite':int,'tags':string,'attributes'?:string} $row */
+				 *		'hash':string,'is_read':int,'is_favorite':int,'tags':string,'attributes':?string} $row */
 				yield FreshRSS_Entry::fromArray($row);
 			}
 		}

+ 7 - 4
app/Models/FilterActionsTrait.php

@@ -50,8 +50,7 @@ trait FreshRSS_FilterActionsTrait {
 		$filterActions = $this->filterActions();
 		for ($i = count($filterActions) - 1; $i >= 0; $i--) {
 			$filterAction = $filterActions[$i];
-			if ($filterAction != null && $filterAction->booleanSearch() != null &&
-				$filterAction->actions() != null && in_array($action, $filterAction->actions(), true)) {
+			if (in_array($action, $filterAction->actions(), true)) {
 				$filters[] = $filterAction->booleanSearch();
 			}
 		}
@@ -120,7 +119,11 @@ trait FreshRSS_FilterActionsTrait {
 		$this->_filterActions($filterActions);
 	}
 
-	public function applyFilterActions(FreshRSS_Entry $entry): void {
+	/**
+	 * @param bool $applyLabel Parameter by reference, which will be set to true if the callers needs to apply a label to the article entry.
+	 */
+	public function applyFilterActions(FreshRSS_Entry $entry, ?bool &$applyLabel = null): void {
+		$applyLabel = false;
 		foreach ($this->filterActions() as $filterAction) {
 			if ($entry->matches($filterAction->booleanSearch())) {
 				foreach ($filterAction->actions() as $action) {
@@ -135,7 +138,7 @@ trait FreshRSS_FilterActionsTrait {
 							$entry->_isFavorite(true);
 							break;
 						case 'label':
-							//TODO: Implement more actions
+							$applyLabel = true;
 							break;
 					}
 				}

+ 1 - 1
app/Models/Tag.php

@@ -2,7 +2,7 @@
 declare(strict_types=1);
 
 class FreshRSS_Tag extends Minz_Model {
-	use FreshRSS_AttributesTrait;
+	use FreshRSS_AttributesTrait, FreshRSS_FilterActionsTrait;
 
 	private int $id = 0;
 	private string $name;

+ 29 - 0
app/Models/TagDAO.php

@@ -300,6 +300,35 @@ SQL;
 		return false;
 	}
 
+	/**
+	 * @param array<array{id_tag:int,id_entry:string}> $addLabels Labels to insert as batch
+	 * @return int|false Number of new entries or false in case of error
+	 */
+	public function tagEntries(array $addLabels) {
+		$hasValues = false;
+		$sql = 'INSERT ' . $this->sqlIgnore() . ' INTO `_entrytag`(id_tag, id_entry) VALUES ';
+		foreach ($addLabels as $addLabel) {
+			$id_tag = (int)($addLabel['id_tag'] ?? 0);
+			$id_entry = $addLabel['id_entry'] ?? '';
+			if ($id_tag > 0 && ctype_digit($id_entry)) {
+				$sql .= "({$id_tag},{$id_entry}),";
+				$hasValues = true;
+			}
+		}
+		$sql = rtrim($sql, ',');
+		if (!$hasValues) {
+			return false;
+		}
+
+		$affected = $this->pdo->exec($sql);
+		if ($affected !== false) {
+			return $affected;
+		}
+		$info = $this->pdo->errorInfo();
+		Minz_Log::error('SQL error ' . __METHOD__ . json_encode($info));
+		return false;
+	}
+
 	/**
 	 * @return array<int,array{'id':int,'name':string,'id_entry':string,'checked':bool}>|false
 	 */

+ 1 - 0
app/Models/View.php

@@ -13,6 +13,7 @@ class FreshRSS_View extends Minz_View {
 	/** @var array<FreshRSS_Category> */
 	public array $categories;
 	public ?FreshRSS_Category $category;
+	public ?FreshRSS_Tag $tag;
 	public string $current_user;
 	/** @var iterable<FreshRSS_Entry> */
 	public $entries;

+ 2 - 0
app/i18n/cz/feedback.php

@@ -116,8 +116,10 @@ return array(
 	),
 	'tag' => array(
 		'created' => 'Štítek „%s“ byl vytvořen.',
+		'error' => 'Label could not be updated!',	// TODO
 		'name_exists' => 'Název štítku již existuje.',
 		'renamed' => 'Štítek „%s“ byl přejmenován na „%s“.',
+		'updated' => 'Label has been updated.',	// TODO
 	),
 	'update' => array(
 		'can_apply' => 'FreshRSS bude nyní aktualizováno na <strong>verzi %s</strong>.',

+ 1 - 0
app/i18n/cz/sub.php

@@ -196,6 +196,7 @@ return array(
 		'subscription_tools' => 'Nástroje odběrů',
 	),
 	'tag' => array(
+		'auto_label' => 'Add this label to new articles',	// TODO
 		'name' => 'Název',
 		'new_name' => 'Nový název',
 		'old_name' => 'Starý název',

+ 2 - 0
app/i18n/de/feedback.php

@@ -116,8 +116,10 @@ return array(
 	),
 	'tag' => array(
 		'created' => 'Label „%s“ wurde erstellt.',
+		'error' => 'Label could not be updated!',	// TODO
 		'name_exists' => 'Label-Name existiert bereits.',
 		'renamed' => 'Das Label „%s“ wurde umbenannt in „%s“.',
+		'updated' => 'Label has been updated.',	// TODO
 	),
 	'update' => array(
 		'can_apply' => 'FreshRSS wird nun auf die <strong>Version %s</strong> aktualisiert.',

+ 1 - 0
app/i18n/de/sub.php

@@ -196,6 +196,7 @@ return array(
 		'subscription_tools' => 'Abonnement-Tools',
 	),
 	'tag' => array(
+		'auto_label' => 'Add this label to new articles',	// TODO
 		'name' => 'Name',	// IGNORE
 		'new_name' => 'Neuer Name',
 		'old_name' => 'Alter Name',

+ 2 - 0
app/i18n/el/feedback.php

@@ -116,8 +116,10 @@ return array(
 	),
 	'tag' => array(
 		'created' => 'Label “%s” has been created.',	// TODO
+		'error' => 'Label could not be updated!',	// TODO
 		'name_exists' => 'Label name already exists.',	// TODO
 		'renamed' => 'Label “%s” has been renamed to “%s”.',	// TODO
+		'updated' => 'Label has been updated.',	// TODO
 	),
 	'update' => array(
 		'can_apply' => 'FreshRSS will now be updated to the <strong>version %s</strong>.',	// DIRTY

+ 1 - 0
app/i18n/el/sub.php

@@ -196,6 +196,7 @@ return array(
 		'subscription_tools' => 'Subscription tools',	// TODO
 	),
 	'tag' => array(
+		'auto_label' => 'Add this label to new articles',	// TODO
 		'name' => 'Name',	// TODO
 		'new_name' => 'New name',	// TODO
 		'old_name' => 'Old name',	// TODO

+ 2 - 0
app/i18n/en-us/feedback.php

@@ -116,8 +116,10 @@ return array(
 	),
 	'tag' => array(
 		'created' => 'Label “%s” has been created.',	// IGNORE
+		'error' => 'Label could not be updated!',	// IGNORE
 		'name_exists' => 'Label name already exists.',	// IGNORE
 		'renamed' => 'Label “%s” has been renamed to “%s”.',	// IGNORE
+		'updated' => 'Label has been updated.',	// IGNORE
 	),
 	'update' => array(
 		'can_apply' => 'FreshRSS will now be updated to the <strong>version %s</strong>.',	// IGNORE

+ 1 - 0
app/i18n/en-us/sub.php

@@ -196,6 +196,7 @@ return array(
 		'subscription_tools' => 'Subscription tools',	// IGNORE
 	),
 	'tag' => array(
+		'auto_label' => 'Add this label to new articles',	// IGNORE
 		'name' => 'Name',	// IGNORE
 		'new_name' => 'New name',	// IGNORE
 		'old_name' => 'Old name',	// IGNORE

+ 2 - 0
app/i18n/en/feedback.php

@@ -116,8 +116,10 @@ return array(
 	),
 	'tag' => array(
 		'created' => 'Label “%s” has been created.',
+		'error' => 'Label could not be updated!',
 		'name_exists' => 'Label name already exists.',
 		'renamed' => 'Label “%s” has been renamed to “%s”.',
+		'updated' => 'Label has been updated.',
 	),
 	'update' => array(
 		'can_apply' => 'An update of FreshRSS is available: <strong>Version %s</strong>.',

+ 1 - 0
app/i18n/en/sub.php

@@ -196,6 +196,7 @@ return array(
 		'subscription_tools' => 'Subscription tools',
 	),
 	'tag' => array(
+		'auto_label' => 'Add this label to new articles',
 		'name' => 'Name',
 		'new_name' => 'New name',
 		'old_name' => 'Old name',

+ 2 - 0
app/i18n/es/feedback.php

@@ -116,8 +116,10 @@ return array(
 	),
 	'tag' => array(
 		'created' => 'Se ha creado la etiqueta “%s”.',
+		'error' => 'Label could not be updated!',	// TODO
 		'name_exists' => 'El nombre de la etiqueta ya existe.',
 		'renamed' => 'La etiqueta “%s” ha sido renombrada a “%s”.',
+		'updated' => 'Label has been updated.',	// TODO
 	),
 	'update' => array(
 		'can_apply' => 'FreshRSS se va a actualizar a la <strong>versión %s</strong>.',

+ 1 - 0
app/i18n/es/sub.php

@@ -196,6 +196,7 @@ return array(
 		'subscription_tools' => 'Herramientas de suscripción',
 	),
 	'tag' => array(
+		'auto_label' => 'Add this label to new articles',	// TODO
 		'name' => 'Nombre',
 		'new_name' => 'Nuevo nombre',
 		'old_name' => 'Nombre antiguo',

+ 2 - 0
app/i18n/fa/feedback.php

@@ -116,8 +116,10 @@ return array(
 	),
 	'tag' => array(
 		'created' => ' برچسب "%s" ایجاد شده است.',
+		'error' => 'Label could not be updated!',	// TODO
 		'name_exists' => ' نام برچسب از قبل وجود دارد.',
 		'renamed' => ' برچسب "%s" به "%s" تغییر نام داده است.',
+		'updated' => 'Label has been updated.',	// TODO
 	),
 	'update' => array(
 		'can_apply' => ' به‌روزرسانی FreshRSS موجود است: <strong>نسخه %s</strong>.',

+ 1 - 0
app/i18n/fa/sub.php

@@ -196,6 +196,7 @@ return array(
 		'subscription_tools' => 'ابزارهای اشتراک',
 	),
 	'tag' => array(
+		'auto_label' => 'Add this label to new articles',	// TODO
 		'name' => ' نام',
 		'new_name' => ' نام جدید',
 		'old_name' => ' نام قدیمی',

+ 3 - 1
app/i18n/fr/feedback.php

@@ -116,8 +116,10 @@ return array(
 	),
 	'tag' => array(
 		'created' => 'L’étiquette <em>%s</em> a été créée.',
-		'name_exists' => 'L’étiquette existe déjà.',
+		'error' => 'L’étiquette n’a pas pu être modifiée',
+		'name_exists' => 'L’étiquette existe déjà!',
 		'renamed' => 'L’étiquette <em>%s</em> a été renommée en <em>%s</em>.',
+		'updated' => 'L’étiquette a été mise à jour.',
 	),
 	'update' => array(
 		'can_apply' => 'FreshRSS va maintenant être mis à jour vers la <strong>version %s</strong>.',

+ 1 - 0
app/i18n/fr/sub.php

@@ -196,6 +196,7 @@ return array(
 		'subscription_tools' => 'Outils d’abonnement',
 	),
 	'tag' => array(
+		'auto_label' => 'Ajoute l’étiquette aux nouveaux articles',
 		'name' => 'Nom',
 		'new_name' => 'Nouveau nom',
 		'old_name' => 'Ancien nom',

+ 2 - 0
app/i18n/he/feedback.php

@@ -116,8 +116,10 @@ return array(
 	),
 	'tag' => array(
 		'created' => 'Label “%s” has been created.',	// TODO
+		'error' => 'Label could not be updated!',	// TODO
 		'name_exists' => 'Label name already exists.',	// TODO
 		'renamed' => 'Label “%s” has been renamed to “%s”.',	// TODO
+		'updated' => 'Label has been updated.',	// TODO
 	),
 	'update' => array(
 		'can_apply' => 'FreshRSS will be now updated to the <strong>version %s</strong>.',

+ 1 - 0
app/i18n/he/sub.php

@@ -196,6 +196,7 @@ return array(
 		'subscription_tools' => 'Subscription tools',	// TODO
 	),
 	'tag' => array(
+		'auto_label' => 'Add this label to new articles',	// TODO
 		'name' => 'Name',	// TODO
 		'new_name' => 'New name',	// TODO
 		'old_name' => 'Old name',	// TODO

+ 2 - 0
app/i18n/hu/feedback.php

@@ -116,8 +116,10 @@ return array(
 	),
 	'tag' => array(
 		'created' => 'Címke “%s” létrehozva.',
+		'error' => 'Label could not be updated!',	// TODO
 		'name_exists' => 'Címke név már létezik.',
 		'renamed' => 'Címke “%s” átnevezve “%s”.',
+		'updated' => 'Label has been updated.',	// TODO
 	),
 	'update' => array(
 		'can_apply' => 'Egy FreshRSS frissítés elérhető : <strong>Verzió %s</strong>.',

+ 1 - 0
app/i18n/hu/sub.php

@@ -196,6 +196,7 @@ return array(
 		'subscription_tools' => 'Hírforrás eszközök',
 	),
 	'tag' => array(
+		'auto_label' => 'Add this label to new articles',	// TODO
 		'name' => 'Név',
 		'new_name' => 'Új név',
 		'old_name' => 'Régi név',

+ 2 - 0
app/i18n/id/feedback.php

@@ -116,8 +116,10 @@ return array(
 	),
 	'tag' => array(
 		'created' => 'Label “%s”has been created.',	// DIRTY
+		'error' => 'Label could not be updated!',	// TODO
 		'name_exists' => 'Label name already exists.',	// TODO
 		'renamed' => 'Label “%s”has been renamed to “%s”.',	// DIRTY
+		'updated' => 'Label has been updated.',	// TODO
 	),
 	'update' => array(
 		'can_apply' => 'An update of FreshRSS is available: <strong>Version %s</strong>.',	// TODO

+ 1 - 0
app/i18n/id/sub.php

@@ -196,6 +196,7 @@ return array(
 		'subscription_tools' => 'Subscription tools',	// TODO
 	),
 	'tag' => array(
+		'auto_label' => 'Add this label to new articles',	// TODO
 		'name' => 'Name',	// TODO
 		'new_name' => 'New name',	// TODO
 		'old_name' => 'Old name',	// TODO

+ 2 - 0
app/i18n/it/feedback.php

@@ -116,8 +116,10 @@ return array(
 	),
 	'tag' => array(
 		'created' => 'Il Tag “%s” è stato creato.',	// DIRTY
+		'error' => 'Label could not be updated!',	// TODO
 		'name_exists' => 'Il nome del tag è già presente.',	// DIRTY
 		'renamed' => 'Il Tag “%s” è stato rinominato in “%s”.',	// DIRTY
+		'updated' => 'Label has been updated.',	// TODO
 	),
 	'update' => array(
 		'can_apply' => 'FreshRSS verrà aggiornato alla <strong>versione %s</strong>.',

+ 1 - 0
app/i18n/it/sub.php

@@ -196,6 +196,7 @@ return array(
 		'subscription_tools' => 'Strumenti di sottoscrizione',
 	),
 	'tag' => array(
+		'auto_label' => 'Add this label to new articles',	// TODO
 		'name' => 'Nome',
 		'new_name' => 'Nuovo nome',
 		'old_name' => 'Vecchio nome',

+ 2 - 0
app/i18n/ja/feedback.php

@@ -116,8 +116,10 @@ return array(
 	),
 	'tag' => array(
 		'created' => '“%s” タグが作成されました',
+		'error' => 'Label could not be updated!',	// TODO
 		'name_exists' => 'このタグ名は既に存在します',
 		'renamed' => '“%s”タグは“%s”に改名されました',
+		'updated' => 'Label has been updated.',	// TODO
 	),
 	'update' => array(
 		'can_apply' => 'FreshRSSは<strong>バージョン %s</strong>に更新されます。',

+ 1 - 0
app/i18n/ja/sub.php

@@ -196,6 +196,7 @@ return array(
 		'subscription_tools' => '購読ツール',
 	),
 	'tag' => array(
+		'auto_label' => 'Add this label to new articles',	// TODO
 		'name' => '名前',
 		'new_name' => '新しい名前',
 		'old_name' => '古い名前',

+ 2 - 0
app/i18n/ko/feedback.php

@@ -116,8 +116,10 @@ return array(
 	),
 	'tag' => array(
 		'created' => '“%s” 태그가 생성되었습니다.',
+		'error' => 'Label could not be updated!',	// TODO
 		'name_exists' => '같은 이름의 태그가 이미 존재합니다.',
 		'renamed' => '“%s” 태그가 “%s” (으)로 이름이 변경되었습니다.',
+		'updated' => 'Label has been updated.',	// TODO
 	),
 	'update' => array(
 		'can_apply' => 'FreshRSS가 <strong>%s</strong> 버전으로 업데이트됩니다.',

+ 1 - 0
app/i18n/ko/sub.php

@@ -196,6 +196,7 @@ return array(
 		'subscription_tools' => '구독 도구',
 	),
 	'tag' => array(
+		'auto_label' => 'Add this label to new articles',	// TODO
 		'name' => '이름',
 		'new_name' => '새 이름',
 		'old_name' => '이전 이름',

+ 2 - 0
app/i18n/lv/feedback.php

@@ -116,8 +116,10 @@ return array(
 	),
 	'tag' => array(
 		'created' => 'Birka “%s” tika uztaisīta.',
+		'error' => 'Label could not be updated!',	// TODO
 		'name_exists' => 'Birkas nosaukums jau pastāv.',
 		'renamed' => 'Birka “%s” tika pārdēvēts par “%s”.',
+		'updated' => 'Label has been updated.',	// TODO
 	),
 	'update' => array(
 		'can_apply' => 'FreshRSS tagad būs atjaunots uz <strong>%s versiju</strong>.',

+ 1 - 0
app/i18n/lv/sub.php

@@ -196,6 +196,7 @@ return array(
 		'subscription_tools' => 'Abonamentu rīki',
 	),
 	'tag' => array(
+		'auto_label' => 'Add this label to new articles',	// TODO
 		'name' => 'Vārds',
 		'new_name' => 'Jaunais vārds',
 		'old_name' => 'Vecais vārds',

+ 2 - 0
app/i18n/nl/feedback.php

@@ -116,8 +116,10 @@ return array(
 	),
 	'tag' => array(
 		'created' => 'Label „%s” aangemaakt.',
+		'error' => 'Label could not be updated!',	// TODO
 		'name_exists' => 'Label bestaat al.',
 		'renamed' => 'Label „%s” hernoemd naar „%s”.',
+		'updated' => 'Label has been updated.',	// TODO
 	),
 	'update' => array(
 		'can_apply' => 'FreshRSS word nu bijgewerkt naar <strong>versie %s</strong>.',

+ 1 - 0
app/i18n/nl/sub.php

@@ -196,6 +196,7 @@ return array(
 		'subscription_tools' => 'Hulpmiddelen voor abonnementen',
 	),
 	'tag' => array(
+		'auto_label' => 'Add this label to new articles',	// TODO
 		'name' => 'Naam',
 		'new_name' => 'Nieuwe naam',
 		'old_name' => 'Oude naam',

+ 2 - 0
app/i18n/oc/feedback.php

@@ -116,8 +116,10 @@ return array(
 	),
 	'tag' => array(
 		'created' => 'L’etiqueta « %s » es estada creada.',
+		'error' => 'Label could not be updated!',	// TODO
 		'name_exists' => 'Lo nom de l’etiqueta existís ja.',
 		'renamed' => 'L’etiqueta « %s » es estada renomenada en « %s ».',
+		'updated' => 'Label has been updated.',	// TODO
 	),
 	'update' => array(
 		'can_apply' => 'FreshRSS es per èsser mes a jorn en <strong>version %s</strong>.',

+ 1 - 0
app/i18n/oc/sub.php

@@ -196,6 +196,7 @@ return array(
 		'subscription_tools' => 'Aisinas d’abonament',
 	),
 	'tag' => array(
+		'auto_label' => 'Add this label to new articles',	// TODO
 		'name' => 'Nom',
 		'new_name' => 'Nom novèl',
 		'old_name' => 'Nom ancian',

+ 2 - 0
app/i18n/pl/feedback.php

@@ -116,8 +116,10 @@ return array(
 	),
 	'tag' => array(
 		'created' => 'Etykieta “%s” została stworzona.',
+		'error' => 'Label could not be updated!',	// TODO
 		'name_exists' => 'Etykieta o podanej nazwie już istnieje.',
 		'renamed' => 'Etykieta “%s” została zmieniona na “%s”.',
+		'updated' => 'Label has been updated.',	// TODO
 	),
 	'update' => array(
 		'can_apply' => 'FreshRSS zostanie zaktualizowany do <strong>wersji %s</strong>.',

+ 1 - 0
app/i18n/pl/sub.php

@@ -196,6 +196,7 @@ return array(
 		'subscription_tools' => 'Narzędzia subskrypcji',
 	),
 	'tag' => array(
+		'auto_label' => 'Add this label to new articles',	// TODO
 		'name' => 'Nazwa',
 		'new_name' => 'Nowa nazwa',
 		'old_name' => 'Poprzednia nazwa',

+ 2 - 0
app/i18n/pt-br/feedback.php

@@ -116,8 +116,10 @@ return array(
 	),
 	'tag' => array(
 		'created' => 'A Tag “%s” foi criada.',	// DIRTY
+		'error' => 'Label could not be updated!',	// TODO
 		'name_exists' => 'O nome da tag já existe.',	// DIRTY
 		'renamed' => 'A Tag “%s” foi renomeada para “%s”.',	// DIRTY
+		'updated' => 'Label has been updated.',	// TODO
 	),
 	'update' => array(
 		'can_apply' => 'O FreshRSS será atualizado para a <strong>versão %s</strong>.',

+ 1 - 0
app/i18n/pt-br/sub.php

@@ -196,6 +196,7 @@ return array(
 		'subscription_tools' => 'Ferramentas de inscrição',
 	),
 	'tag' => array(
+		'auto_label' => 'Add this label to new articles',	// TODO
 		'name' => 'Nome',
 		'new_name' => 'Nome novo',
 		'old_name' => 'Nome antigo',

+ 2 - 0
app/i18n/ru/feedback.php

@@ -116,8 +116,10 @@ return array(
 	),
 	'tag' => array(
 		'created' => 'Метка “%s” создана.',
+		'error' => 'Label could not be updated!',	// TODO
 		'name_exists' => 'Метка с таким названием уже существует.',
 		'renamed' => 'Метка “%s” переименована в “%s”.',
+		'updated' => 'Label has been updated.',	// TODO
 	),
 	'update' => array(
 		'can_apply' => 'FreshRSS будет обновлён до <strong>версии %s</strong>.',

+ 1 - 0
app/i18n/ru/sub.php

@@ -196,6 +196,7 @@ return array(
 		'subscription_tools' => 'Инструменты подписки',
 	),
 	'tag' => array(
+		'auto_label' => 'Add this label to new articles',	// TODO
 		'name' => 'Название',
 		'new_name' => 'Новое название',
 		'old_name' => 'Старое название',

+ 2 - 0
app/i18n/sk/feedback.php

@@ -116,8 +116,10 @@ return array(
 	),
 	'tag' => array(
 		'created' => 'Štítok “%s” bol vytvorený.',
+		'error' => 'Label could not be updated!',	// TODO
 		'name_exists' => 'Názov štítku už existuje.',
 		'renamed' => 'Štítok “%s” bol premenovaný na “%s”.',
+		'updated' => 'Label has been updated.',	// TODO
 	),
 	'update' => array(
 		'can_apply' => 'FreshRSS sa teraz aktualizuje <strong>na verziu %s</strong>.',

+ 1 - 0
app/i18n/sk/sub.php

@@ -196,6 +196,7 @@ return array(
 		'subscription_tools' => 'Nástroje na odoberanie kanálov',
 	),
 	'tag' => array(
+		'auto_label' => 'Add this label to new articles',	// TODO
 		'name' => 'Názov',
 		'new_name' => 'Nový názov',
 		'old_name' => 'Starý názov',

+ 2 - 0
app/i18n/tr/feedback.php

@@ -116,8 +116,10 @@ return array(
 	),
 	'tag' => array(
 		'created' => '“%s” etiketi oluşturuldu.',
+		'error' => 'Label could not be updated!',	// TODO
 		'name_exists' => 'Etiket zaten mevcut.',
 		'renamed' => '“%s” isimli etiketin ismi “%s” olarak değiştirildi.',
+		'updated' => 'Label has been updated.',	// TODO
 	),
 	'update' => array(
 		'can_apply' => 'FreshRSS <strong>%s sürümüne</strong> güncellenecek.',

+ 1 - 0
app/i18n/tr/sub.php

@@ -196,6 +196,7 @@ return array(
 		'subscription_tools' => 'Abonelik araçları',
 	),
 	'tag' => array(
+		'auto_label' => 'Add this label to new articles',	// TODO
 		'name' => 'İsim',
 		'new_name' => 'Eski isim',
 		'old_name' => 'Yeni isim',

+ 2 - 0
app/i18n/zh-cn/feedback.php

@@ -116,8 +116,10 @@ return array(
 	),
 	'tag' => array(
 		'created' => '标签 “%s” 已创建。',
+		'error' => 'Label could not be updated!',	// TODO
 		'name_exists' => '标签名已存在。',
 		'renamed' => '标签 “%s” 已被重命名为 “%s”。',
+		'updated' => 'Label has been updated.',	// TODO
 	),
 	'update' => array(
 		'can_apply' => 'FreshRSS 将更新到 <strong>版本 %s</strong>。',

+ 1 - 0
app/i18n/zh-cn/sub.php

@@ -196,6 +196,7 @@ return array(
 		'subscription_tools' => '订阅工具',
 	),
 	'tag' => array(
+		'auto_label' => 'Add this label to new articles',	// TODO
 		'name' => '名称',
 		'new_name' => '新名称',
 		'old_name' => '旧名称',

+ 2 - 0
app/i18n/zh-tw/feedback.php

@@ -116,8 +116,10 @@ return array(
 	),
 	'tag' => array(
 		'created' => '標簽 “%s” 已創建。',
+		'error' => 'Label could not be updated!',	// TODO
 		'name_exists' => '標簽名已存在。',
 		'renamed' => '標簽 “%s” 已被重命名為 “%s”。',
+		'updated' => 'Label has been updated.',	// TODO
 	),
 	'update' => array(
 		'can_apply' => 'FreshRSS 將更新到 <strong>版本 %s</strong>。',

+ 1 - 0
app/i18n/zh-tw/sub.php

@@ -196,6 +196,7 @@ return array(
 		'subscription_tools' => '訂閱工具',
 	),
 	'tag' => array(
+		'auto_label' => 'Add this label to new articles',	// TODO
 		'name' => '名稱',
 		'new_name' => '新名稱',
 		'old_name' => '舊名稱',

+ 1 - 3
app/layout/aside_feed.phtml

@@ -153,9 +153,7 @@
 <script id="tag_config_template" type="text/html">
 	<ul class="dropdown-menu">
 		<li class="item">
-			<button class="as-link confirm" disabled="disabled"
-				form="mark-read-aside" formaction="<?= _url('tag', 'delete', 'id_tag', '------') ?>"
-				type="submit"><?= _t('gen.action.remove') ?></button>
+			<a class="configure open-slider" href="<?= _url('tag', 'update', 'id', '------') ?>"><?= _t('gen.action.manage') ?></a>
 		</li>
 	</ul>
 	<a class="dropdown-close" href="#close">❌</a>

+ 8 - 56
app/views/tag/index.phtml

@@ -27,60 +27,12 @@
 		</div>
 	</form>
 
-	<h2><?= _t('sub.title.rename_label') ?></h2>
-	<?php
-	$disabled = '';
-	if (count($this->tags) < 1) {
-		$disabled = ' disabled="disabled"';
-	} ?>
-	<form id="rename_tag" method="post" action="<?= _url('tag', 'rename') ?>" autocomplete="off">
-		<input type="hidden" name="_csrf" value="<?= FreshRSS_Auth::csrfToken() ?>" />
-		<div class="form-group">
-			<label class="group-name" for="id_tag"><?= _t('sub.tag.old_name') ?></label>
-			<div class="group-controls">
-				<select name="id_tag" id="id_tag"<?= $disabled?>>
-					<?php foreach ($this->tags as $tag): ?>
-						<option value="<?= $tag->id() ?>"><?= $tag->name() ?></option>
-					<?php endforeach; ?>
-				</select>
-			</div>
-		</div>
-		<div class="form-group">
-			<label class="group-name" for="rename_new_name"><?= _t('sub.tag.new_name') ?></label>
-			<div class="group-controls">
-				<input id="rename_new_name" name="name" type="text" autocomplete="off"<?= $disabled?>/>
-			</div>
-		</div>
-
-		<div class="form-group form-actions">
-			<div class="group-controls">
-				<?php if (!$disabled) { ?>
-				<button type="submit" class="btn btn-attention confirm"><?= _t('gen.action.rename') ?></button>
-				<?php } ?>
-			</div>
-		</div>
-	</form>
-
-	<h2><?= _t('sub.title.delete_label') ?></h2>
-	<form id="delete_tag" method="post" action="<?= _url('tag', 'delete') ?>" autocomplete="off">
-		<input type="hidden" name="_csrf" value="<?= FreshRSS_Auth::csrfToken() ?>" />
-		<div class="form-group">
-			<label class="group-name" for="id_tag_delete"><?= _t('sub.tag.name') ?></label>
-			<div class="group-controls">
-				<select name="id_tag" id="id_tag_delete"<?= $disabled?>>
-					<?php foreach ($this->tags as $tag): ?>
-						<option value="<?= $tag->id() ?>"><?= $tag->name() ?></option>
-					<?php endforeach; ?>
-				</select>
-			</div>
-		</div>
-
-		<div class="form-group form-actions">
-			<div class="group-controls">
-				<?php if (!$disabled) { ?>
-				<button type="submit" class="btn btn-attention confirm"><?= _t('gen.action.remove') ?></button>
-				<?php } ?>
-			</div>
-		</div>
-	</form>
+	<?php if (count($this->tags) > 0): ?>
+	<h2><?= _t('gen.action.manage') ?></h2>
+	<ul>
+	<?php foreach ($this->tags as $tag): ?>
+		<li><a href="<?= _url('tag', 'update', 'id', $tag->id()) ?>"><?= $tag->name() ?></a></li>
+	<?php endforeach; ?>
+	</ul>
+	<?php endif; ?>
 </main>

+ 69 - 0
app/views/tag/update.phtml

@@ -0,0 +1,69 @@
+<?php
+	declare(strict_types=1);
+	/** @var FreshRSS_View $this */
+
+	if (!Minz_Request::paramBoolean('ajax')) {
+		$this->partial('aside_subscription');
+	}
+	if ($this->tag === null) {
+		throw new FreshRSS_Context_Exception('Tag not initialised!');
+	}
+?>
+<div class="post">
+	<h2>
+		<?= $this->tag->name() ?>
+	</h2>
+
+	<div>
+		<a href="<?= _url('index', 'index', 'get', 't_' . $this->tag->id()) ?>"><?= _i('link') ?> <?= _t('gen.action.filter') ?></a>
+	</div>
+
+	<form method="post" action="<?= _url('tag', 'update', 'id', $this->tag->id(), '#', 'slider') ?>" autocomplete="off">
+		<input type="hidden" name="_csrf" value="<?= FreshRSS_Auth::csrfToken() ?>" />
+
+		<legend><?= _t('sub.category.information') ?></legend>
+		<div class="form-group">
+			<label class="group-name" for="name"><?= _t('sub.tag.name') ?></label>
+			<div class="group-controls">
+				<input type="text" name="name" id="name" value="<?= $this->tag->name() ?>" />
+			</div>
+		</div>
+
+		<div class="form-group form-actions">
+			<div class="group-controls">
+				<button type="submit" class="btn btn-important"><?= _t('gen.action.submit') ?></button>
+				<button type="reset" class="btn"><?= _t('gen.action.cancel') ?></button>
+			</div>
+		</div>
+
+		<legend><?= _t('sub.feed.filteractions') ?></legend>
+		<div class="form-group">
+			<label class="group-name" for="filteractions_label"><?= _t('sub.tag.auto_label') ?></label>
+			<div class="group-controls">
+				<textarea name="filteractions_label" id="filteractions_label" class="w100"><?php
+					foreach ($this->tag->filtersAction('label') as $filterRead) {
+						echo $filterRead->getRawInput(), PHP_EOL;
+					}
+				?></textarea>
+				<p class="help"><?= _i('help') ?> <?= _t('sub.feed.filteractions.help') ?></p>
+			</div>
+		</div>
+
+		<div class="form-group form-actions">
+			<div class="group-controls">
+				<button type="submit" class="btn btn-important"><?= _t('gen.action.submit') ?></button>
+				<button type="reset" class="btn"><?= _t('gen.action.cancel') ?></button>
+			</div>
+		</div>
+	</form>
+
+	<h2><?= _t('sub.title.delete_label') ?></h2>
+	<form id="delete_tag" method="post" action="<?= _url('tag', 'delete', 'id_tag', $this->tag->id()) ?>">
+		<input type="hidden" name="_csrf" value="<?= FreshRSS_Auth::csrfToken() ?>" />
+		<div class="form-group form-actions">
+			<div class="group-controls">
+				<button type="submit" class="btn btn-attention confirm"><?= _t('gen.action.remove') ?></button>
+			</div>
+		</div>
+	</form>
+</div>

+ 26 - 26
docs/fr/users/03_Main_view.md

@@ -30,7 +30,7 @@ la machine sur laquelle est installée votre instance de FreshRSS.
 
 Le script qui permet de mettre à jour les articles s’appelle
 *actualize_script.php* et se trouve dans le répertoire *app* de votre
-instance de FreshRSS. La syntaxe des tâches planifiées ne sera pas expliqué
+instance de FreshRSS. La syntaxe des tâches planifiées ne sera pas expliquée
 ici, cependant voici [une introduction rapide à
 crontab](http://www.adminschoice.com/crontab-quick-reference/) qui peut vous
 aider.
@@ -205,48 +205,48 @@ the search field.
 Il est possible d’utiliser le champ de recherche pour raffiner les résultats :
 
 * par ID de flux : `f:123` ou plusieurs flux (*ou*) : `f:123,234,345`
-* par auteur : `author:nom` or `author:'nom composé'`
-* par titre : `intitle:mot` or `intitle:'mot composé'`
-* par URL: `inurl:mot` or `inurl:'mot composé'`
-* par tag: `#tag`
-* par texte libre : `mot` or `'mot composé'`
-* by date of discovery, using the [ISO 8601 time interval format](http://en.wikipedia.org/wiki/ISO_8601#Time_intervals): `date:<date-interval>`
-	* From a specific day, or month, or year:
+* par auteur : `author:nom` ou `author:'nom composé'`
+* par titre : `intitle:mot` ou `intitle:'mot composé'`
+* par URL : `inurl:mot` ou `inurl:'mot composé'`
+* par tag : `#tag`
+* par texte libre : `mot` ou `'mot composé'`
+* par date d’ajout, en utilisant le [format ISO 8601 d’intervalle entre deux dates](https://fr.wikipedia.org/wiki/ISO_8601#Intervalle_entre_deux_dates) : `date:<intervalle-de-dates>`
+	* D’un jour spécifique, ou mois, ou année :
 		* `date:2014-03-30`
 		* `date:2014-03` or `date:201403`
 		* `date:2014`
-	* From a specific time of a given day:
+	* D’une heure spécifiée d’un jour donné :
 		* `date:2014-05-30T13`
 		* `date:2014-05-30T13:30`
-	* Between two given dates:
+	* Entre deux dates :
 		* `date:2014-02/2014-04`
 		* `date:2014-02--2014-04`
 		* `date:2014-02/04`
 		* `date:2014-02-03/05`
 		* `date:2014-02-03T22:00/22:15`
 		* `date:2014-02-03T22:00/15`
-	* After a given date:
+	* Après une date donnée :
 		* `date:2014-03/`
-	* Before a given date:
+	* Avant une date donnée :
 		* `date:/2014-03`
-	* For a specific duration after a given date:
+	* Pour une certaine durée après une date donnée :
 		* `date:2014-03/P1W`
-	* For a specific duration before a given date:
+	* Pour une certaine durée avant une date donnée :
 		* `date:P1W/2014-05-25T23:59:59`
-	* For the past duration before now (the trailing slash is optional):
-		* `date:P1Y/` or `date:P1Y` (past year)
-		* `date:P2M/` (past two months)
-		* `date:P3W/` (past three weeks)
-		* `date:P4D/` (past four days)
-		* `date:PT5H/` (past five hours)
-		* `date:PT30M/` (past thirty minutes)
-		* `date:PT90S/` (past ninety seconds)
-		* `date:P1DT1H/` (past one day and one hour)
-* par date de publication, avec la même syntaxe: `pubdate:<date-interval>`
+	* Pour une certaine durée avant maintenant (la barre oblique finale est facultative) :
+		* `date:P1Y/` or `date:P1Y` (depuis un an)
+		* `date:P2M/` (depuis deux mois)
+		* `date:P3W/` (depuis trois semaines)
+		* `date:P4D/` (depuis quatre jours)
+		* `date:PT5H/` (depuis cinq heures)
+		* `date:PT30M/` (depuis trente minutes)
+		* `date:PT90S/` (depuis 90 secondes)
+		* `date:P1DT1H/` (depuis un jour et une heure)
+* par date de publication, avec la même syntaxe : `pubdate:<date-interval>`
 * par ID d’étiquette : `L:12` ou de plusieurs étiquettes : `L:12,13,14` ou avec n’importe quelle étiquette : `L:*`
-* par nom d’étiquette : `label:étiquette`, `label:"mon étiquette"` ou d’une étiquette parmis une liste (*ou*) : `labels:"mon étiquette,mon autre étiquette"`
+* par nom d’étiquette : `label:étiquette`, `label:"mon étiquette"` ou d’une étiquette parmi une liste (*ou*) : `labels:"mon étiquette,mon autre étiquette"`
 * par plusieurs noms d’étiquettes (*et*) : `label:"mon étiquette" label:"mon autre étiquette"`
-* par ID d’article (entrée) : `e:1639310674957894` ou de plusieurs articles (*ou*): `e:1639310674957894,1639310674957893`
+* par ID d’article (entrée) : `e:1639310674957894` ou de plusieurs articles (*ou*) : `e:1639310674957894,1639310674957893`
 * par nom de filtre utilisateur (recherche enregistrée) : `search:maRecherche`, `search:"Ma recherche"` ou par ID de recherche : `S:3`
 	* en interne, ces références sont remplacées par le filtre utilisateur correspondant dans l’expression de recherche
 

+ 1 - 1
docs/i18n/freshrss.fr.po

@@ -8818,7 +8818,7 @@ msgstr ""
 #. type: Bullet: '* '
 #: en/./users/10_filter.md:93
 msgid "by free-text: `keyword` or `'composed keyword'`"
-msgstr "par texte libre : `mot` or `'mot composé'`"
+msgstr "par texte libre : `mot` ou `'mot composé'`"
 
 #. type: Bullet: '* '
 #: en/./users/10_filter.md:93