diff options
| author | 2025-07-15 12:39:51 +0200 | |
|---|---|---|
| committer | 2025-07-15 12:39:51 +0200 | |
| commit | 5f61e426dc90b7b697a46da009af2fc88eed3ad0 (patch) | |
| tree | 079de4957a207ed08a8de181f4f2649067277933 /app | |
| parent | 8a44d1045a49aa04d16af0bd4c8a0d56cadf1be3 (diff) | |
Sort by category title, feed title (#7702)
* Sort by category name, feed name
fix https://github.com/FreshRSS/FreshRSS/issues/7698
Note that sorting is done with the default SQL collation for now, meaning that lower-case vs. upper-case and diacritics are influencing the sorting order. Improvements left for future work.
Watch out that those sorting criteria are slower due to additional joins, additional requests, and poorer indexes.
* i18n:pl
Co-authored-by: Inverle <inverle@proton.me>
* i18n: nl
Co-authored-by: Frans de Jonge <fransdejonge@gmail.com>
* Fix preserve sort
---------
Co-authored-by: Inverle <inverle@proton.me>
Co-authored-by: Frans de Jonge <fransdejonge@gmail.com>
Diffstat (limited to 'app')
32 files changed, 312 insertions, 38 deletions
diff --git a/app/Controllers/indexController.php b/app/Controllers/indexController.php index bcc659d8a..59d4976ba 100644 --- a/app/Controllers/indexController.php +++ b/app/Controllers/indexController.php @@ -283,15 +283,30 @@ class FreshRSS_index_Controller extends FreshRSS_ActionController { $id_min = (time() - (FreshRSS_Context::$sinceHours * 3600)) . '000000'; } - $continuation_value = 0; + $continuation_values = []; if (FreshRSS_Context::$continuation_id !== '0') { - if (in_array(FreshRSS_Context::$sort, ['date', 'link', 'title'], true)) { + if (in_array(FreshRSS_Context::$sort, ['c.name', 'date', 'f.name', 'link', 'title'], true)) { $pagingEntry = $entryDAO->searchById(FreshRSS_Context::$continuation_id); - $continuation_value = $pagingEntry === null ? 0 : match (FreshRSS_Context::$sort) { + + if ($pagingEntry !== null && in_array(FreshRSS_Context::$sort, ['c.name', 'f.name'], true)) { + // We most likely already have the feed object in cache + $feed = FreshRSS_Category::findFeed(FreshRSS_Context::categories(), $pagingEntry->feedId()); + if ($feed !== null) { + $pagingEntry->_feed($feed); + } + } + + $continuation_values[] = $pagingEntry === null ? 0 : match (FreshRSS_Context::$sort) { + 'c.name' => $pagingEntry->feed()?->category()?->name() ?? '', 'date' => $pagingEntry->date(true), + 'f.name' => $pagingEntry->feed()?->name() ?? '', 'link' => $pagingEntry->link(true), 'title' => $pagingEntry->title(), }; + if ($pagingEntry !== null && FreshRSS_Context::$sort === 'c.name') { + // Secondary sort criterion + $continuation_values[] = $pagingEntry->feed()?->name() ?? ''; + } } elseif (FreshRSS_Context::$sort === 'rand') { FreshRSS_Context::$continuation_id = '0'; } @@ -300,7 +315,7 @@ class FreshRSS_index_Controller extends FreshRSS_ActionController { foreach ($entryDAO->listWhere( $type, $id, FreshRSS_Context::$state, FreshRSS_Context::$search, id_min: $id_min, id_max: FreshRSS_Context::$id_max, sort: FreshRSS_Context::$sort, order: FreshRSS_Context::$order, - continuation_id: FreshRSS_Context::$continuation_id, continuation_value: $continuation_value, + continuation_id: FreshRSS_Context::$continuation_id, continuation_values: $continuation_values, limit: $postsPerPage ?? FreshRSS_Context::$number, offset: FreshRSS_Context::$offset) as $entry) { yield $entry; } diff --git a/app/Models/Context.php b/app/Models/Context.php index 749e7e6ff..9a4e0192e 100644 --- a/app/Models/Context.php +++ b/app/Models/Context.php @@ -42,7 +42,7 @@ final class FreshRSS_Context { public static int $state = 0; /** @var 'ASC'|'DESC' */ public static string $order = 'DESC'; - /** @var 'id'|'date'|'link'|'title'|'rand' */ + /** @var 'id'|'c.name'|'date'|'f.name'|'link'|'title'|'rand' */ public static string $sort = 'id'; public static int $number = 0; public static int $offset = 0; @@ -237,7 +237,7 @@ final class FreshRSS_Context { $order = Minz_Request::paramString('order', true) ?: FreshRSS_Context::userConf()->sort_order; self::$order = in_array($order, ['ASC', 'DESC'], true) ? $order : 'DESC'; $sort = Minz_Request::paramString('sort', true) ?: FreshRSS_Context::userConf()->sort; - self::$sort = in_array($sort, ['id', 'date', 'link', 'title', 'rand'], true) ? $sort : 'id'; + self::$sort = in_array($sort, ['id', 'c.name', 'date', 'f.name', 'link', 'title', 'rand'], true) ? $sort : 'id'; self::$number = Minz_Request::paramInt('nb') ?: FreshRSS_Context::userConf()->posts_per_page; if (self::$number > FreshRSS_Context::userConf()->max_posts_per_rss) { self::$number = max( diff --git a/app/Models/EntryDAO.php b/app/Models/EntryDAO.php index aea6e6f65..8713ba930 100644 --- a/app/Models/EntryDAO.php +++ b/app/Models/EntryDAO.php @@ -1184,13 +1184,15 @@ SQL; /** * @param numeric-string $id_min * @param numeric-string $id_max - * @param 'id'|'date'|'link'|'title'|'rand' $sort + * @param 'id'|'c.name'|'date'|'f.name'|'link'|'title'|'rand' $sort * @param 'ASC'|'DESC' $order + * @param numeric-string $continuation_id + * @param list<string|int> $continuation_values * @return array{0:list<int|string>,1:string} */ protected function sqlListEntriesWhere(string $alias = '', int $state = FreshRSS_Entry::STATE_ALL, ?FreshRSS_BooleanSearch $filters = null, string $id_min = '0', string $id_max = '0', string $sort = 'id', string $order = 'DESC', - string $continuation_id = '0', string|int $continuation_value = 0): array { + string $continuation_id = '0', array $continuation_values = []): array { $search = ' '; $values = []; if ($state & FreshRSS_Entry::STATE_ANDS) { @@ -1250,13 +1252,29 @@ SQL; $values[] = $id_min; } - if ($continuation_id !== '0' && in_array($sort, ['date', 'link', 'title'], true)) { + if ($continuation_id !== '0' && in_array($sort, ['c.name', 'date', 'f.name', 'link', 'title'], true)) { $sign = $order === 'ASC' ? '>' : '<'; + $orderBy = match ($sort) { + 'c.name' => 'c.name', + 'f.name' => 'f.name', + default => $alias . $sort, + }; // Keyset pagination (Compatibility syntax due to poor performance of tuple syntax in MySQL https://bugs.mysql.com/bug.php?id=104128) - $search .= "AND ({$alias}{$sort} {$sign} ? OR ({$alias}{$sort} = ? AND {$alias}id {$sign}= ?)) "; - $values[] = $continuation_value; - $values[] = $continuation_value; - $values[] = $continuation_id; + if ($sort === 'c.name') { + // Includes a secondary sort by feed name + $search .= "AND ((c.name {$sign} ?) OR (c.name = ? AND f.name {$sign} ?) OR (c.name = ? AND f.name = ? AND {$alias}id {$sign}= ?)) "; + $values[] = $continuation_values[0]; + $values[] = $continuation_values[0]; + $values[] = $continuation_values[1]; + $values[] = $continuation_values[0]; + $values[] = $continuation_values[1]; + $values[] = $continuation_id; + } else { + $search .= "AND ({$orderBy} {$sign} ? OR ({$orderBy} = ? AND {$alias}id {$sign}= ?)) "; + $values[] = $continuation_values[0]; + $values[] = $continuation_values[0]; + $values[] = $continuation_id; + } } if ($filters !== null && count($filters->searches()) > 0) { @@ -1276,15 +1294,16 @@ SQL; * @param int $id category/feed/tag ID * @param numeric-string $id_min * @param numeric-string $id_max - * @param 'id'|'date'|'link'|'title'|'rand' $sort + * @param 'id'|'c.name'|'date'|'f.name'|'link'|'title'|'rand' $sort * @param 'ASC'|'DESC' $order * @param numeric-string $continuation_id + * @param list<string|int> $continuation_values * @return array{0:list<int|string>,1:string} * @throws FreshRSS_EntriesGetter_Exception */ private function sqlListWhere(string $type = 'a', int $id = 0, int $state = FreshRSS_Entry::STATE_ALL, ?FreshRSS_BooleanSearch $filters = null, string $id_min = '0', string $id_max = '0', string $sort = 'id', string $order = 'DESC', - string $continuation_id = '0', string|int $continuation_value = 0, int $limit = 1, int $offset = 0): array { + string $continuation_id = '0', array $continuation_values = [], int $limit = 1, int $offset = 0): array { if (!$state) { $state = FreshRSS_Entry::STATE_ALL; } @@ -1334,21 +1353,28 @@ SQL; } $order = in_array($order, ['ASC', 'DESC'], true) ? $order : 'DESC'; - $sort = in_array($sort, ['id', 'date', 'link', 'title', 'rand'], true) ? $sort : 'id'; - $orderBy = ($sort === 'rand' ? static::sqlRandom() : 'e.' . $sort); + $sort = in_array($sort, ['id', 'c.name', 'date', 'f.name', 'link', 'title', 'rand'], true) ? $sort : 'id'; + $orderBy = match ($sort) { + 'c.name' => 'c.name', + 'f.name' => 'f.name', + 'rand' => static::sqlRandom(), + default => 'e.' . $sort, + }; [$searchValues, $search] = $this->sqlListEntriesWhere(alias: 'e.', state: $state, filters: $filters, id_min: $id_min, id_max: $id_max, - sort: $sort, order: $order, continuation_id: $continuation_id, continuation_value: $continuation_value); + sort: $sort, order: $order, continuation_id: $continuation_id, continuation_values: $continuation_values); return [array_merge($values, $searchValues), 'SELECT ' . ($type === 'T' ? 'DISTINCT ' : '') . 'e.id' - . ($type === 'T' && $orderBy !== 'e.id' ? ', ' . $orderBy : '') // SELECT DISTINCT, ORDER BY expressions must appear in SELECT + . ($type === 'T' && $sort !== 'id' ? ', ' . $orderBy : '') // SELECT DISTINCT, ORDER BY expressions must appear in SELECT . ' FROM `_entry` e ' - . 'INNER JOIN `_feed` f ON e.id_feed = f.id ' + . 'INNER JOIN `_feed` f ON f.id = e.id_feed ' + . ($sort === 'c.name' ? 'INNER JOIN `_category` c ON c.id = f.category ' : '') . ($type === 't' || $type === 'T' ? 'INNER JOIN `_entrytag` et ON et.id_entry = e.id ' : '') . 'WHERE ' . $where . $search . 'ORDER BY ' . $orderBy . ' ' . $order + . ($sort === 'c.name' ? ', f.name ' . $order : '') // Secondary sort . ($sort === 'id' ? '' : ', e.id ' . $order) // For keyset pagination . ($limit > 0 ? ' LIMIT ' . $limit : '') // http://explainextended.com/2009/10/23/mysql-order-by-limit-performance-late-row-lookups/ . ($offset > 0 ? ' OFFSET ' . $offset : '') @@ -1360,29 +1386,44 @@ SQL; * @param int $id category/feed/tag ID * @param numeric-string $id_min * @param numeric-string $id_max - * @param 'id'|'date'|'link'|'title'|'rand' $sort + * @param 'id'|'c.name'|'date'|'f.name'|'link'|'title'|'rand' $sort * @param 'ASC'|'DESC' $order * @param numeric-string $continuation_id + * @param list<string|int> $continuation_values * @throws FreshRSS_EntriesGetter_Exception */ private function listWhereRaw(string $type = 'a', int $id = 0, int $state = FreshRSS_Entry::STATE_ALL, ?FreshRSS_BooleanSearch $filters = null, string $id_min = '0', string $id_max = '0', string $sort = 'id', string $order = 'DESC', - string $continuation_id = '0', string|int $continuation_value = 0, int $limit = 1, int $offset = 0): PDOStatement|false { + string $continuation_id = '0', array $continuation_values = [], int $limit = 1, int $offset = 0): PDOStatement|false { $order = in_array($order, ['ASC', 'DESC'], true) ? $order : 'DESC'; - $sort = in_array($sort, ['id', 'date', 'link', 'title', 'rand'], true) ? $sort : 'id'; + $sort = in_array($sort, ['id', 'c.name', 'date', 'f.name', 'link', 'title', 'rand'], true) ? $sort : 'id'; [$values, $sql] = $this->sqlListWhere($type, $id, $state, $filters, id_min: $id_min, id_max: $id_max, sort: $sort, order: $order, - continuation_id: $continuation_id, continuation_value: $continuation_value, limit: $limit, offset: $offset); - - $orderBy = ($sort === 'rand' ? static::sqlRandom() : 'e0.' . $sort); + continuation_id: $continuation_id, continuation_values: $continuation_values, limit: $limit, offset: $offset); + + $orderBy = match ($sort) { + 'c.name' => 'c0.name', + 'f.name' => 'f0.name', + 'rand' => static::sqlRandom(), + default => 'e0.' . $sort, + }; $content = static::isCompressed() ? 'UNCOMPRESS(e0.content_bin) AS content' : 'e0.content'; $hash = static::sqlHexEncode('e0.hash'); $sql = <<<SQL SELECT e0.id, e0.guid, e0.title, e0.author, {$content}, e0.link, e0.date, {$hash} AS hash, e0.is_read, e0.is_favorite, e0.id_feed, e0.tags, e0.attributes FROM `_entry` e0 INNER JOIN ({$sql}) e2 ON e2.id=e0.id -ORDER BY {$orderBy} {$order} SQL; + if ($sort === 'f.name' || $sort === 'c.name') { + $sql .= ' INNER JOIN `_feed` f0 ON f0.id = e0.id_feed '; + } + if ($sort === 'c.name') { + $sql .= ' INNER JOIN `_category` c0 ON c0.id = f0.category '; + } + $sql .= ' ORDER BY ' . $orderBy . ' ' . $order; + if ($sort === 'c.name') { + $sql .= ', f0.name ' . $order; // Secondary sort + } if ($sort !== 'id') { // For keyset pagination $sql .= ', e0.id ' . $order; @@ -1395,7 +1436,7 @@ SQL; /** @var array{0:string,1:int,2:string} $info */ if ($this->autoUpdateDb($info)) { return $this->listWhereRaw($type, $id, $state, $filters, id_min: $id_min, id_max: $id_max, sort: $sort, order: $order, - continuation_id: $continuation_id, continuation_value: $continuation_value, limit: $limit, offset: $offset); + continuation_id: $continuation_id, continuation_values: $continuation_values, limit: $limit, offset: $offset); } Minz_Log::error('SQL error ' . __METHOD__ . json_encode($info)); return false; @@ -1407,17 +1448,18 @@ SQL; * @param int $id category/feed/tag ID * @param numeric-string $id_min * @param numeric-string $id_max - * @param 'id'|'date'|'link'|'title'|'rand' $sort + * @param 'id'|'c.name'|'date'|'f.name'|'link'|'title'|'rand' $sort * @param 'ASC'|'DESC' $order * @param numeric-string $continuation_id + * @param list<string|int> $continuation_values * @return Traversable<FreshRSS_Entry> * @throws FreshRSS_EntriesGetter_Exception */ public function listWhere(string $type = 'a', int $id = 0, int $state = FreshRSS_Entry::STATE_ALL, ?FreshRSS_BooleanSearch $filters = null, string $id_min = '0', string $id_max = '0', string $sort = 'id', string $order = 'DESC', - string $continuation_id = '0', string|int $continuation_value = 0, int $limit = 1, int $offset = 0): Traversable { + string $continuation_id = '0', array $continuation_values = [], int $limit = 1, int $offset = 0): Traversable { $stm = $this->listWhereRaw($type, $id, $state, $filters, id_min: $id_min, id_max: $id_max, sort: $sort, order: $order, - continuation_id: $continuation_id, continuation_value: $continuation_value, limit: $limit, offset: $offset); + continuation_id: $continuation_id, continuation_values: $continuation_values, limit: $limit, offset: $offset); if ($stm !== false) { while (is_array($row = $stm->fetch(PDO::FETCH_ASSOC))) { /** @var array{'id':string,'id_feed':int,'guid':string,'title':string,'author':string,'content':string,'link':string,'date':int, @@ -1473,16 +1515,17 @@ SQL; * @param int $id category/feed/tag ID * @param numeric-string $id_min * @param numeric-string $id_max - * @param numeric-string $continuation_id * @param 'ASC'|'DESC' $order + * @param numeric-string $continuation_id + * @param list<string|int> $continuation_values * @return list<numeric-string>|null * @throws FreshRSS_EntriesGetter_Exception */ public function listIdsWhere(string $type = 'a', int $id = 0, int $state = FreshRSS_Entry::STATE_ALL, ?FreshRSS_BooleanSearch $filters = null, string $id_min = '0', string $id_max = '0', string $order = 'DESC', - string $continuation_id = '0', string|int $continuation_value = 0, int $limit = 1, int $offset = 0): ?array { + string $continuation_id = '0', array $continuation_values = [], int $limit = 1, int $offset = 0): ?array { [$values, $sql] = $this->sqlListWhere($type, $id, $state, $filters, id_min: $id_min, id_max: $id_max, order: $order, - continuation_id: $continuation_id, continuation_value: $continuation_value, limit: $limit, offset: $offset); + continuation_id: $continuation_id, continuation_values: $continuation_values, limit: $limit, offset: $offset); $stm = $this->pdo->prepare($sql); if ($stm !== false && $stm->execute($values)) { /** @var list<int|numeric-string> $res */ diff --git a/app/Models/UserConfiguration.php b/app/Models/UserConfiguration.php index 23016c9f2..e53de00d3 100644 --- a/app/Models/UserConfiguration.php +++ b/app/Models/UserConfiguration.php @@ -52,7 +52,7 @@ declare(strict_types=1); * @property bool $show_nav_buttons * @property 'big'|'small'|'none' $mark_read_button * @property 'ASC'|'DESC' $sort_order - * @property 'id'|'date'|'link'|'title'|'rand' $sort + * @property 'id'|'c.name'|'date'|'f.name'|'link'|'title'|'rand' $sort * @property array<int,array<string,string>> $sharing * @property array<string,string> $shortcuts * @property bool $sides_close_article diff --git a/app/i18n/cs/index.php b/app/i18n/cs/index.php index 3516da812..aa0c0df3b 100644 --- a/app/i18n/cs/index.php +++ b/app/i18n/cs/index.php @@ -77,8 +77,16 @@ return array( 'search_short' => 'Hledat', 'sort' => array( '_' => 'Sorting criteria', // TODO + 'c' => array( + 'name_asc' => 'Category, feed titles A→Z', // TODO + 'name_desc' => 'Category, feed titles Z→A', // TODO + ), 'date_asc' => 'Publication date 1→9', // TODO 'date_desc' => 'Publication date 9→1', // TODO + 'f' => array( + 'name_asc' => 'Feed title A→Z', // TODO + 'name_desc' => 'Feed title Z→A', // TODO + ), 'id_asc' => 'Freshly received last', // TODO 'id_desc' => 'Freshly received first', // TODO 'link_asc' => 'Link A→Z', // TODO diff --git a/app/i18n/de/index.php b/app/i18n/de/index.php index 94d871ea7..681915519 100644 --- a/app/i18n/de/index.php +++ b/app/i18n/de/index.php @@ -77,8 +77,16 @@ return array( 'search_short' => 'Suchen', 'sort' => array( '_' => 'Sorting criteria', // TODO + 'c' => array( + 'name_asc' => 'Category, feed titles A→Z', // TODO + 'name_desc' => 'Category, feed titles Z→A', // TODO + ), 'date_asc' => 'Publication date 1→9', // TODO 'date_desc' => 'Publication date 9→1', // TODO + 'f' => array( + 'name_asc' => 'Feed title A→Z', // TODO + 'name_desc' => 'Feed title Z→A', // TODO + ), 'id_asc' => 'Freshly received last', // TODO 'id_desc' => 'Freshly received first', // TODO 'link_asc' => 'Link A→Z', // TODO diff --git a/app/i18n/el/index.php b/app/i18n/el/index.php index 655846518..47b2a958d 100644 --- a/app/i18n/el/index.php +++ b/app/i18n/el/index.php @@ -77,8 +77,16 @@ return array( 'search_short' => 'Search', // TODO 'sort' => array( '_' => 'Sorting criteria', // TODO + 'c' => array( + 'name_asc' => 'Category, feed titles A→Z', // TODO + 'name_desc' => 'Category, feed titles Z→A', // TODO + ), 'date_asc' => 'Publication date 1→9', // TODO 'date_desc' => 'Publication date 9→1', // TODO + 'f' => array( + 'name_asc' => 'Feed title A→Z', // TODO + 'name_desc' => 'Feed title Z→A', // TODO + ), 'id_asc' => 'Freshly received last', // TODO 'id_desc' => 'Freshly received first', // TODO 'link_asc' => 'Link A→Z', // TODO diff --git a/app/i18n/en-us/index.php b/app/i18n/en-us/index.php index a80ccc56b..3bce04e8a 100644 --- a/app/i18n/en-us/index.php +++ b/app/i18n/en-us/index.php @@ -77,8 +77,16 @@ return array( 'search_short' => 'Search', // IGNORE 'sort' => array( '_' => 'Sorting criteria', // IGNORE + 'c' => array( + 'name_asc' => 'Category, feed titles A→Z', // IGNORE + 'name_desc' => 'Category, feed titles Z→A', // IGNORE + ), 'date_asc' => 'Publication date 1→9', // IGNORE 'date_desc' => 'Publication date 9→1', // IGNORE + 'f' => array( + 'name_asc' => 'Feed title A→Z', // IGNORE + 'name_desc' => 'Feed title Z→A', // IGNORE + ), 'id_asc' => 'Freshly received last', // IGNORE 'id_desc' => 'Freshly received first', // IGNORE 'link_asc' => 'Link A→Z', // IGNORE diff --git a/app/i18n/en/index.php b/app/i18n/en/index.php index 61ff073ae..c5404e6ce 100644 --- a/app/i18n/en/index.php +++ b/app/i18n/en/index.php @@ -77,8 +77,16 @@ return array( 'search_short' => 'Search', 'sort' => array( '_' => 'Sorting criteria', + 'c' => array( + 'name_asc' => 'Category, feed titles A→Z', + 'name_desc' => 'Category, feed titles Z→A', + ), 'date_asc' => 'Publication date 1→9', 'date_desc' => 'Publication date 9→1', + 'f' => array( + 'name_asc' => 'Feed title A→Z', + 'name_desc' => 'Feed title Z→A', + ), 'id_asc' => 'Freshly received last', 'id_desc' => 'Freshly received first', 'link_asc' => 'Link A→Z', diff --git a/app/i18n/es/index.php b/app/i18n/es/index.php index b7e407df6..82b63898d 100644 --- a/app/i18n/es/index.php +++ b/app/i18n/es/index.php @@ -77,8 +77,16 @@ return array( 'search_short' => 'Buscar', 'sort' => array( '_' => 'Sorting criteria', // TODO + 'c' => array( + 'name_asc' => 'Category, feed titles A→Z', // TODO + 'name_desc' => 'Category, feed titles Z→A', // TODO + ), 'date_asc' => 'Publication date 1→9', // TODO 'date_desc' => 'Publication date 9→1', // TODO + 'f' => array( + 'name_asc' => 'Feed title A→Z', // TODO + 'name_desc' => 'Feed title Z→A', // TODO + ), 'id_asc' => 'Freshly received last', // TODO 'id_desc' => 'Freshly received first', // TODO 'link_asc' => 'Link A→Z', // TODO diff --git a/app/i18n/fa/index.php b/app/i18n/fa/index.php index 77bb966a0..283bbf805 100644 --- a/app/i18n/fa/index.php +++ b/app/i18n/fa/index.php @@ -77,8 +77,16 @@ return array( 'search_short' => ' جستجو', 'sort' => array( '_' => 'Sorting criteria', // TODO + 'c' => array( + 'name_asc' => 'Category, feed titles A→Z', // TODO + 'name_desc' => 'Category, feed titles Z→A', // TODO + ), 'date_asc' => 'Publication date 1→9', // TODO 'date_desc' => 'Publication date 9→1', // TODO + 'f' => array( + 'name_asc' => 'Feed title A→Z', // TODO + 'name_desc' => 'Feed title Z→A', // TODO + ), 'id_asc' => 'Freshly received last', // TODO 'id_desc' => 'Freshly received first', // TODO 'link_asc' => 'Link A→Z', // TODO diff --git a/app/i18n/fi/index.php b/app/i18n/fi/index.php index 7d8331e1c..f844caa86 100644 --- a/app/i18n/fi/index.php +++ b/app/i18n/fi/index.php @@ -77,8 +77,16 @@ return array( 'search_short' => 'Haku', 'sort' => array( '_' => 'Sorting criteria', // TODO + 'c' => array( + 'name_asc' => 'Category, feed titles A→Z', // TODO + 'name_desc' => 'Category, feed titles Z→A', // TODO + ), 'date_asc' => 'Publication date 1→9', // TODO 'date_desc' => 'Publication date 9→1', // TODO + 'f' => array( + 'name_asc' => 'Feed title A→Z', // TODO + 'name_desc' => 'Feed title Z→A', // TODO + ), 'id_asc' => 'Freshly received last', // TODO 'id_desc' => 'Freshly received first', // TODO 'link_asc' => 'Link A→Z', // TODO diff --git a/app/i18n/fr/index.php b/app/i18n/fr/index.php index b4ebcb335..b6aae67fb 100644 --- a/app/i18n/fr/index.php +++ b/app/i18n/fr/index.php @@ -77,8 +77,16 @@ return array( 'search_short' => 'Rechercher', 'sort' => array( '_' => 'Critère de tri', + 'c' => array( + 'name_asc' => 'Catégorie, flux (titres) A→Z', + 'name_desc' => 'Catégorie, flux (titres) Z→A', + ), 'date_asc' => 'Date de publication 1→9', 'date_desc' => 'Date de publication 9→1', + 'f' => array( + 'name_asc' => 'Flux (titre) A→Z', + 'name_desc' => 'Flux (titre) Z→A', + ), 'id_asc' => 'Reçus récemment en dernier', 'id_desc' => 'Reçus récemment en premier', 'link_asc' => 'Lien A→Z', diff --git a/app/i18n/he/index.php b/app/i18n/he/index.php index ad9b4f514..843b0b843 100644 --- a/app/i18n/he/index.php +++ b/app/i18n/he/index.php @@ -77,8 +77,16 @@ return array( 'search_short' => 'חיפוש', 'sort' => array( '_' => 'Sorting criteria', // TODO + 'c' => array( + 'name_asc' => 'Category, feed titles A→Z', // TODO + 'name_desc' => 'Category, feed titles Z→A', // TODO + ), 'date_asc' => 'Publication date 1→9', // TODO 'date_desc' => 'Publication date 9→1', // TODO + 'f' => array( + 'name_asc' => 'Feed title A→Z', // TODO + 'name_desc' => 'Feed title Z→A', // TODO + ), 'id_asc' => 'Freshly received last', // TODO 'id_desc' => 'Freshly received first', // TODO 'link_asc' => 'Link A→Z', // TODO diff --git a/app/i18n/hu/index.php b/app/i18n/hu/index.php index fe7e74535..51604b9f1 100755 --- a/app/i18n/hu/index.php +++ b/app/i18n/hu/index.php @@ -77,8 +77,16 @@ return array( 'search_short' => 'Keresés', 'sort' => array( '_' => 'Rendezési sorrend', + 'c' => array( + 'name_asc' => 'Category, feed titles A→Z', // TODO + 'name_desc' => 'Category, feed titles Z→A', // TODO + ), 'date_asc' => 'Kiadás dátuma 1→9', 'date_desc' => 'Kiadás dátuma 9→1', + 'f' => array( + 'name_asc' => 'Feed title A→Z', // TODO + 'name_desc' => 'Feed title Z→A', // TODO + ), 'id_asc' => 'Frissen fogadott utoljára', 'id_desc' => 'Frissen fogadott először', 'link_asc' => 'Link A→Z', // TODO diff --git a/app/i18n/id/index.php b/app/i18n/id/index.php index f177c1c91..fe7e515fa 100644 --- a/app/i18n/id/index.php +++ b/app/i18n/id/index.php @@ -77,8 +77,16 @@ return array( 'search_short' => 'Cari', 'sort' => array( '_' => 'Kriteria pengurutan', + 'c' => array( + 'name_asc' => 'Category, feed titles A→Z', // TODO + 'name_desc' => 'Category, feed titles Z→A', // TODO + ), 'date_asc' => 'Tanggal publikasi 1→9', 'date_desc' => 'Tanggal publikasi 9→1', + 'f' => array( + 'name_asc' => 'Feed title A→Z', // TODO + 'name_desc' => 'Feed title Z→A', // TODO + ), 'id_asc' => 'Yang baru diterima terakhir', 'id_desc' => 'Yang baru diterima paling awal', 'link_asc' => 'Tautan A→Z', diff --git a/app/i18n/it/index.php b/app/i18n/it/index.php index 6cbd457ca..a111c55ff 100644 --- a/app/i18n/it/index.php +++ b/app/i18n/it/index.php @@ -77,8 +77,16 @@ return array( 'search_short' => 'Cerca', 'sort' => array( '_' => 'Ordina per', + 'c' => array( + 'name_asc' => 'Category, feed titles A→Z', // TODO + 'name_desc' => 'Category, feed titles Z→A', // TODO + ), 'date_asc' => 'Data di pubblicazione 1→9', 'date_desc' => 'Data di pubblicazione 9→1', + 'f' => array( + 'name_asc' => 'Feed title A→Z', // TODO + 'name_desc' => 'Feed title Z→A', // TODO + ), 'id_asc' => 'Dal meno recente', 'id_desc' => 'Dal più recente', 'link_asc' => 'Link A→Z', // IGNORE diff --git a/app/i18n/ja/index.php b/app/i18n/ja/index.php index 69b5b30db..30543572b 100644 --- a/app/i18n/ja/index.php +++ b/app/i18n/ja/index.php @@ -77,8 +77,16 @@ return array( 'search_short' => '検索', 'sort' => array( '_' => '並べ替え', + 'c' => array( + 'name_asc' => 'Category, feed titles A→Z', // TODO + 'name_desc' => 'Category, feed titles Z→A', // TODO + ), 'date_asc' => '公開日順 1→9', 'date_desc' => '公開日順 9→1', + 'f' => array( + 'name_asc' => 'Feed title A→Z', // TODO + 'name_desc' => 'Feed title Z→A', // TODO + ), 'id_asc' => '新着を最後に並べる', 'id_desc' => '新着を最初に並べる', 'link_asc' => 'リンクURL順 A→Z', diff --git a/app/i18n/ko/index.php b/app/i18n/ko/index.php index 9c1387499..c5190211c 100644 --- a/app/i18n/ko/index.php +++ b/app/i18n/ko/index.php @@ -77,8 +77,16 @@ return array( 'search_short' => '검색', 'sort' => array( '_' => 'Sorting criteria', // TODO + 'c' => array( + 'name_asc' => 'Category, feed titles A→Z', // TODO + 'name_desc' => 'Category, feed titles Z→A', // TODO + ), 'date_asc' => 'Publication date 1→9', // TODO 'date_desc' => 'Publication date 9→1', // TODO + 'f' => array( + 'name_asc' => 'Feed title A→Z', // TODO + 'name_desc' => 'Feed title Z→A', // TODO + ), 'id_asc' => 'Freshly received last', // TODO 'id_desc' => 'Freshly received first', // TODO 'link_asc' => 'Link A→Z', // TODO diff --git a/app/i18n/lv/index.php b/app/i18n/lv/index.php index 72998ae2e..ba66438d8 100644 --- a/app/i18n/lv/index.php +++ b/app/i18n/lv/index.php @@ -77,8 +77,16 @@ return array( 'search_short' => 'Meklēt', 'sort' => array( '_' => 'Sorting criteria', // TODO + 'c' => array( + 'name_asc' => 'Category, feed titles A→Z', // TODO + 'name_desc' => 'Category, feed titles Z→A', // TODO + ), 'date_asc' => 'Publication date 1→9', // TODO 'date_desc' => 'Publication date 9→1', // TODO + 'f' => array( + 'name_asc' => 'Feed title A→Z', // TODO + 'name_desc' => 'Feed title Z→A', // TODO + ), 'id_asc' => 'Freshly received last', // TODO 'id_desc' => 'Freshly received first', // TODO 'link_asc' => 'Link A→Z', // TODO diff --git a/app/i18n/nl/index.php b/app/i18n/nl/index.php index 5b1600370..ebf3c3123 100644 --- a/app/i18n/nl/index.php +++ b/app/i18n/nl/index.php @@ -77,8 +77,16 @@ return array( 'search_short' => 'Zoeken', 'sort' => array( '_' => 'Sorteercriteria', + 'c' => array( + 'name_asc' => 'Categorie, feedtitels A→Z', + 'name_desc' => 'Categorie, feedtitels Z→A', + ), 'date_asc' => 'Publicatiedatum 1→9', 'date_desc' => 'Publicatiedatum 9→1', + 'f' => array( + 'name_asc' => 'Feedtitel A→Z', + 'name_desc' => 'Feedtitel Z→A', + ), 'id_asc' => 'Nieuw ontvangen laatst', 'id_desc' => 'Nieuw ontvangen eerst', 'link_asc' => 'Link A→Z', // IGNORE diff --git a/app/i18n/oc/index.php b/app/i18n/oc/index.php index 0dbaeb16d..35479da95 100644 --- a/app/i18n/oc/index.php +++ b/app/i18n/oc/index.php @@ -77,8 +77,16 @@ return array( 'search_short' => 'Recercar', 'sort' => array( '_' => 'Sorting criteria', // TODO + 'c' => array( + 'name_asc' => 'Category, feed titles A→Z', // TODO + 'name_desc' => 'Category, feed titles Z→A', // TODO + ), 'date_asc' => 'Publication date 1→9', // TODO 'date_desc' => 'Publication date 9→1', // TODO + 'f' => array( + 'name_asc' => 'Feed title A→Z', // TODO + 'name_desc' => 'Feed title Z→A', // TODO + ), 'id_asc' => 'Freshly received last', // TODO 'id_desc' => 'Freshly received first', // TODO 'link_asc' => 'Link A→Z', // TODO diff --git a/app/i18n/pl/index.php b/app/i18n/pl/index.php index b4cab9b90..850294bee 100644 --- a/app/i18n/pl/index.php +++ b/app/i18n/pl/index.php @@ -77,8 +77,16 @@ return array( 'search_short' => 'Szukaj', 'sort' => array( '_' => 'Kryteria sortowania', + 'c' => array( + 'name_asc' => 'Tytuł kategorii i kanału A→Z', + 'name_desc' => 'Tytuł kategorii i kanału Z→A', + ), 'date_asc' => 'Data publikacji 1→9', 'date_desc' => 'Data publikacji 9→1', + 'f' => array( + 'name_asc' => 'Tytuł kanału A→Z', + 'name_desc' => 'Tytuł kanału Z→A', + ), 'id_asc' => 'Najpożniej otrzymane', 'id_desc' => 'Najwcześniej otrzymane', 'link_asc' => 'Odnośnik A→Z', diff --git a/app/i18n/pt-br/index.php b/app/i18n/pt-br/index.php index 17b2aa8d9..8623574c9 100644 --- a/app/i18n/pt-br/index.php +++ b/app/i18n/pt-br/index.php @@ -77,8 +77,16 @@ return array( 'search_short' => 'Buscar', 'sort' => array( '_' => 'Sorting criteria', // TODO + 'c' => array( + 'name_asc' => 'Category, feed titles A→Z', // TODO + 'name_desc' => 'Category, feed titles Z→A', // TODO + ), 'date_asc' => 'Publication date 1→9', // TODO 'date_desc' => 'Publication date 9→1', // TODO + 'f' => array( + 'name_asc' => 'Feed title A→Z', // TODO + 'name_desc' => 'Feed title Z→A', // TODO + ), 'id_asc' => 'Freshly received last', // TODO 'id_desc' => 'Freshly received first', // TODO 'link_asc' => 'Link A→Z', // TODO diff --git a/app/i18n/pt-pt/index.php b/app/i18n/pt-pt/index.php index 914649c83..c1fd4344d 100644 --- a/app/i18n/pt-pt/index.php +++ b/app/i18n/pt-pt/index.php @@ -77,8 +77,16 @@ return array( 'search_short' => 'Pesquisar', 'sort' => array( '_' => 'Sorting criteria', // TODO + 'c' => array( + 'name_asc' => 'Category, feed titles A→Z', // TODO + 'name_desc' => 'Category, feed titles Z→A', // TODO + ), 'date_asc' => 'Publication date 1→9', // TODO 'date_desc' => 'Publication date 9→1', // TODO + 'f' => array( + 'name_asc' => 'Feed title A→Z', // TODO + 'name_desc' => 'Feed title Z→A', // TODO + ), 'id_asc' => 'Freshly received last', // TODO 'id_desc' => 'Freshly received first', // TODO 'link_asc' => 'Link A→Z', // TODO diff --git a/app/i18n/ru/index.php b/app/i18n/ru/index.php index 952d32c49..35fed08ca 100644 --- a/app/i18n/ru/index.php +++ b/app/i18n/ru/index.php @@ -77,8 +77,16 @@ return array( 'search_short' => 'Поиск', 'sort' => array( '_' => 'Sorting criteria', // TODO + 'c' => array( + 'name_asc' => 'Category, feed titles A→Z', // TODO + 'name_desc' => 'Category, feed titles Z→A', // TODO + ), 'date_asc' => 'Publication date 1→9', // TODO 'date_desc' => 'Publication date 9→1', // TODO + 'f' => array( + 'name_asc' => 'Feed title A→Z', // TODO + 'name_desc' => 'Feed title Z→A', // TODO + ), 'id_asc' => 'Freshly received last', // TODO 'id_desc' => 'Freshly received first', // TODO 'link_asc' => 'Link A→Z', // TODO diff --git a/app/i18n/sk/index.php b/app/i18n/sk/index.php index d4392c569..5810af5f8 100644 --- a/app/i18n/sk/index.php +++ b/app/i18n/sk/index.php @@ -77,8 +77,16 @@ return array( 'search_short' => 'Hľadať', 'sort' => array( '_' => 'Sorting criteria', // TODO + 'c' => array( + 'name_asc' => 'Category, feed titles A→Z', // TODO + 'name_desc' => 'Category, feed titles Z→A', // TODO + ), 'date_asc' => 'Publication date 1→9', // TODO 'date_desc' => 'Publication date 9→1', // TODO + 'f' => array( + 'name_asc' => 'Feed title A→Z', // TODO + 'name_desc' => 'Feed title Z→A', // TODO + ), 'id_asc' => 'Freshly received last', // TODO 'id_desc' => 'Freshly received first', // TODO 'link_asc' => 'Link A→Z', // TODO diff --git a/app/i18n/tr/index.php b/app/i18n/tr/index.php index a7ed2eaa7..01ad218ad 100644 --- a/app/i18n/tr/index.php +++ b/app/i18n/tr/index.php @@ -77,8 +77,16 @@ return array( 'search_short' => 'Ara', 'sort' => array( '_' => 'Sıralama kriteri', + 'c' => array( + 'name_asc' => 'Category, feed titles A→Z', // TODO + 'name_desc' => 'Category, feed titles Z→A', // TODO + ), 'date_asc' => 'Yayın tarihi 1→9', 'date_desc' => 'Yayın tarihi 9→1', + 'f' => array( + 'name_asc' => 'Feed title A→Z', // TODO + 'name_desc' => 'Feed title Z→A', // TODO + ), 'id_asc' => 'Son alınanlar en sonda', 'id_desc' => 'Son alınanlar başta', 'link_asc' => 'Bağlantı A→Z', diff --git a/app/i18n/zh-cn/index.php b/app/i18n/zh-cn/index.php index f02326ec9..cd389f8b8 100644 --- a/app/i18n/zh-cn/index.php +++ b/app/i18n/zh-cn/index.php @@ -77,8 +77,16 @@ return array( 'search_short' => '搜索', 'sort' => array( '_' => 'Sorting criteria', // TODO + 'c' => array( + 'name_asc' => 'Category, feed titles A→Z', // TODO + 'name_desc' => 'Category, feed titles Z→A', // TODO + ), 'date_asc' => 'Publication date 1→9', // TODO 'date_desc' => 'Publication date 9→1', // TODO + 'f' => array( + 'name_asc' => 'Feed title A→Z', // TODO + 'name_desc' => 'Feed title Z→A', // TODO + ), 'id_asc' => 'Freshly received last', // TODO 'id_desc' => 'Freshly received first', // TODO 'link_asc' => 'Link A→Z', // TODO diff --git a/app/i18n/zh-tw/index.php b/app/i18n/zh-tw/index.php index 99a4c718b..2122bc4f0 100644 --- a/app/i18n/zh-tw/index.php +++ b/app/i18n/zh-tw/index.php @@ -77,8 +77,16 @@ return array( 'search_short' => '搜尋', 'sort' => array( '_' => 'Sorting criteria', // TODO + 'c' => array( + 'name_asc' => 'Category, feed titles A→Z', // TODO + 'name_desc' => 'Category, feed titles Z→A', // TODO + ), 'date_asc' => 'Publication date 1→9', // TODO 'date_desc' => 'Publication date 9→1', // TODO + 'f' => array( + 'name_asc' => 'Feed title A→Z', // TODO + 'name_desc' => 'Feed title Z→A', // TODO + ), 'id_asc' => 'Freshly received last', // TODO 'id_desc' => 'Freshly received first', // TODO 'link_asc' => 'Link A→Z', // TODO diff --git a/app/layout/aside_feed.phtml b/app/layout/aside_feed.phtml index 3d4103731..33c415c6f 100644 --- a/app/layout/aside_feed.phtml +++ b/app/layout/aside_feed.phtml @@ -13,7 +13,7 @@ if (($s = Minz_Request::paramString('state', plaintext: true)) !== '' && ctype_digit($s)) { $state_filter_manual .= '&state=' . $s; } - if (($s = Minz_Request::paramString('sort', plaintext: true)) !== '' && ctype_alpha($s)) { + if (($s = Minz_Request::paramString('sort', plaintext: true)) !== '' && preg_match('/^[a-z.]+$/', $s)) { $state_filter_manual .= '&sort=' . $s; } if (($s = Minz_Request::paramString('order', plaintext: true)) !== '' && ctype_alpha($s)) { diff --git a/app/layout/nav_menu.phtml b/app/layout/nav_menu.phtml index 944008f04..28f7782df 100644 --- a/app/layout/nav_menu.phtml +++ b/app/layout/nav_menu.phtml @@ -235,6 +235,12 @@ <a href="<?= Minz_Url::display($url_order, amend: ['params' => ['sort' => 'link', 'order' => 'DESC']]) ?>"><?= _t('index.menu.sort.link_desc') ?></a></li> <li class="item" role="radio" aria-checked="<?= FreshRSS_Context::$order === 'DESC' && FreshRSS_Context::$sort === 'title' ? 'true' : 'false' ?>"> <a href="<?= Minz_Url::display($url_order, amend: ['params' => ['sort' => 'title', 'order' => 'DESC']]) ?>"><?= _t('index.menu.sort.title_desc') ?></a></li> + <li class="item" role="radio" aria-checked="<?= FreshRSS_Context::$order === 'DESC' && FreshRSS_Context::$sort === 'f.name' ? 'true' : 'false' ?>"> + <a href="<?= Minz_Url::display($url_order, amend: ['params' => ['sort' => 'f.name', 'order' => 'DESC']]) ?>"><?= _t('index.menu.sort.f.name_desc') ?></a></li> + <li class="item" role="radio" aria-checked="<?= FreshRSS_Context::$order === 'DESC' && FreshRSS_Context::$sort === 'c.name' ? 'true' : 'false' ?>"> + <a href="<?= Minz_Url::display($url_order, amend: ['params' => ['sort' => 'c.name', 'order' => 'DESC']]) ?>"><?= _t('index.menu.sort.c.name_desc') ?></a></li> + <li class="item separator" role="radio" aria-checked="<?= FreshRSS_Context::$sort === 'rand' ? 'true' : 'false' ?>"> + <a href="<?= Minz_Url::display($url_order, amend: ['params' => ['sort' => 'rand', 'order' => null]]) ?>"><?= _t('index.menu.sort.rand') ?></a></li> <li class="item separator" role="radio" aria-checked="<?= FreshRSS_Context::$order === 'ASC' && FreshRSS_Context::$sort === 'id' ? 'true' : 'false' ?>"> <a href="<?= Minz_Url::display($url_order, amend: ['params' => ['sort' => 'id', 'order' => 'ASC']]) ?>"><?= _t('index.menu.sort.id_asc') ?></a></li> <li class="item" role="radio" aria-checked="<?= FreshRSS_Context::$order === 'ASC' && FreshRSS_Context::$sort === 'date' ? 'true' : 'false' ?>"> @@ -243,8 +249,10 @@ <a href="<?= Minz_Url::display($url_order, amend: ['params' => ['sort' => 'link', 'order' => 'ASC']]) ?>"><?= _t('index.menu.sort.link_asc') ?></a></li> <li class="item" role="radio" aria-checked="<?= FreshRSS_Context::$order === 'ASC' && FreshRSS_Context::$sort === 'title' ? 'true' : 'false' ?>"> <a href="<?= Minz_Url::display($url_order, amend: ['params' => ['sort' => 'title', 'order' => 'ASC']]) ?>"><?= _t('index.menu.sort.title_asc') ?></a></li> - <li class="item separator" role="radio" aria-checked="<?= FreshRSS_Context::$sort === 'rand' ? 'true' : 'false' ?>"> - <a href="<?= Minz_Url::display($url_order, amend: ['params' => ['sort' => 'rand', 'order' => null]]) ?>"><?= _t('index.menu.sort.rand') ?></a></li> + <li class="item" role="radio" aria-checked="<?= FreshRSS_Context::$order === 'ASC' && FreshRSS_Context::$sort === 'f.name' ? 'true' : 'false' ?>"> + <a href="<?= Minz_Url::display($url_order, amend: ['params' => ['sort' => 'f.name', 'order' => 'ASC']]) ?>"><?= _t('index.menu.sort.f.name_asc') ?></a></li> + <li class="item" role="radio" aria-checked="<?= FreshRSS_Context::$order === 'ASC' && FreshRSS_Context::$sort === 'c.name' ? 'true' : 'false' ?>"> + <a href="<?= Minz_Url::display($url_order, amend: ['params' => ['sort' => 'c.name', 'order' => 'ASC']]) ?>"><?= _t('index.menu.sort.c.name_asc') ?></a></li> </ul> <a class="dropdown-close" href="#close">❌</a> </div> |
