diff options
| author | 2025-10-10 19:43:38 -0300 | |
|---|---|---|
| committer | 2025-10-11 00:43:38 +0200 | |
| commit | 673067a52d44cbfc14327d226f4f1c4ce66f737a (patch) | |
| tree | 649f9617a56593b2d95bd45498b1d69738fe4985 | |
| parent | ec1f5ee61bfeb9e8ed6a3c1e069b82d9f26f64e6 (diff) | |
Last user modified (#7886)
* feat: Add user modified functionality
Closes https://github.com/FreshRSS/FreshRSS/issues/7862
Changes proposed in this pull request:
This is an implementation of the proposed feature. It allows entries to have a new field that will be updated whenever an item is marked as read/unread or bookmark/removed from bookmarks. And a new sort criteria to sort by it.
How to test the feature manually:
1. Mark items from a feed as read/unread
2. Mark items from a feed as bookmark / remove bookmark
3. Sort by the new criteria
* feat: Add sort functionality
* feat: Add sort nav button
* fix: Use correct migrations
* fix: Add internationalization
* fix: Linter errors
* chore: PR comments
* Update app/i18n/fr/index.php
Co-authored-by: Alexandre Alapetite <alexandre@alapetite.fr>
* Update app/i18n/pl/index.php
Co-authored-by: Inverle <inverle@proton.me>
* Update app/i18n/nl/index.php
Co-authored-by: Frans de Jonge <fransdejonge@gmail.com>
* make fix-all
* Fixes
* More fixes sort
* Fix wrong index
* Fix unneeded column
* Fix auto-create indexes
* Some copilot suggestions
* One more fix
Co-authored-by: Alexandre Alapetite <alexandre@alapetite.fr>
---------
Co-authored-by: Alexandre Alapetite <alexandre@alapetite.fr>
Co-authored-by: Inverle <inverle@proton.me>
Co-authored-by: Frans de Jonge <fransdejonge@gmail.com>
39 files changed, 175 insertions, 54 deletions
diff --git a/README.fr.md b/README.fr.md index 3661d8062..782e205bd 100644 --- a/README.fr.md +++ b/README.fr.md @@ -234,11 +234,11 @@ Voir le [dépôt dédié à ces extensions](https://github.com/FreshRSS/Extensio | English (United States) (en-US) | ■■■■■■■■■■ 100% | [contribuer](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fen-US+%2F%28TODO%7CDIRTY%29%24%2F) | | Español (es) | ■■■■■■■■■・ 91% | [contribuer](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fes+%2F%28TODO%7CDIRTY%29%24%2F) | | فارسی (fa) | ■■■■■■■■■・ 97% | [contribuer](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Ffa+%2F%28TODO%7CDIRTY%29%24%2F) | -| Suomi (fi) | ■■■■■■■■■■ 100% | [contribuer](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Ffi+%2F%28TODO%7CDIRTY%29%24%2F) | +| Suomi (fi) | ■■■■■■■■■・ 99% | [contribuer](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Ffi+%2F%28TODO%7CDIRTY%29%24%2F) | | Français (fr) | ■■■■■■■■■■ 100% | [contribuer](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Ffr+%2F%28TODO%7CDIRTY%29%24%2F) | | עברית (he) | ■■■■・・・・・・ 45% | [contribuer](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fhe+%2F%28TODO%7CDIRTY%29%24%2F) | | Magyar (hu) | ■■■■■■■■■・ 99% | [contribuer](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fhu+%2F%28TODO%7CDIRTY%29%24%2F) | -| Bahasa Indonesia (id) | ■■■■■■■■■・ 97% | [contribuer](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fid+%2F%28TODO%7CDIRTY%29%24%2F) | +| Bahasa Indonesia (id) | ■■■■■■■■■・ 96% | [contribuer](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fid+%2F%28TODO%7CDIRTY%29%24%2F) | | Italiano (it) | ■■■■■■■■■・ 96% | [contribuer](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fit+%2F%28TODO%7CDIRTY%29%24%2F) | | 日本語 (ja) | ■■■■■■■■■・ 95% | [contribuer](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fja+%2F%28TODO%7CDIRTY%29%24%2F) | | 한국어 (ko) | ■■■■■■■■・・ 88% | [contribuer](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fko+%2F%28TODO%7CDIRTY%29%24%2F) | @@ -247,7 +247,7 @@ Voir le [dépôt dédié à ces extensions](https://github.com/FreshRSS/Extensio | Occitan (oc) | ■■■■■■■■・・ 81% | [contribuer](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Foc+%2F%28TODO%7CDIRTY%29%24%2F) | | Polski (pl) | ■■■■■■■■■■ 100% | [contribuer](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fpl+%2F%28TODO%7CDIRTY%29%24%2F) | | Português (Brasil) (pt-BR) | ■■■■■■■■・・ 88% | [contribuer](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fpt-BR+%2F%28TODO%7CDIRTY%29%24%2F) | -| Português (Portugal) (pt-PT) | ■■■■■■■■・・ 88% | [contribuer](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fpt-PT+%2F%28TODO%7CDIRTY%29%24%2F) | +| Português (Portugal) (pt-PT) | ■■■■■■■■・・ 87% | [contribuer](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fpt-PT+%2F%28TODO%7CDIRTY%29%24%2F) | | Русский (ru) | ■■■■■■■■・・ 88% | [contribuer](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fru+%2F%28TODO%7CDIRTY%29%24%2F) | | Slovenčina (sk) | ■■■■■■■■・・ 88% | [contribuer](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fsk+%2F%28TODO%7CDIRTY%29%24%2F) | | Türkçe (tr) | ■■■■■■■■■・ 96% | [contribuer](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Ftr+%2F%28TODO%7CDIRTY%29%24%2F) | @@ -132,11 +132,11 @@ See the [repository dedicated to those extensions](https://github.com/FreshRSS/E | English (United States) (en-US) | ■■■■■■■■■■ 100% | [contribute](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fen-US+%2F%28TODO%7CDIRTY%29%24%2F) | | Español (es) | ■■■■■■■■■・ 91% | [contribute](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fes+%2F%28TODO%7CDIRTY%29%24%2F) | | فارسی (fa) | ■■■■■■■■■・ 97% | [contribute](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Ffa+%2F%28TODO%7CDIRTY%29%24%2F) | -| Suomi (fi) | ■■■■■■■■■■ 100% | [contribute](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Ffi+%2F%28TODO%7CDIRTY%29%24%2F) | +| Suomi (fi) | ■■■■■■■■■・ 99% | [contribute](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Ffi+%2F%28TODO%7CDIRTY%29%24%2F) | | Français (fr) | ■■■■■■■■■■ 100% | [contribute](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Ffr+%2F%28TODO%7CDIRTY%29%24%2F) | | עברית (he) | ■■■■・・・・・・ 45% | [contribute](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fhe+%2F%28TODO%7CDIRTY%29%24%2F) | | Magyar (hu) | ■■■■■■■■■・ 99% | [contribute](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fhu+%2F%28TODO%7CDIRTY%29%24%2F) | -| Bahasa Indonesia (id) | ■■■■■■■■■・ 97% | [contribute](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fid+%2F%28TODO%7CDIRTY%29%24%2F) | +| Bahasa Indonesia (id) | ■■■■■■■■■・ 96% | [contribute](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fid+%2F%28TODO%7CDIRTY%29%24%2F) | | Italiano (it) | ■■■■■■■■■・ 96% | [contribute](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fit+%2F%28TODO%7CDIRTY%29%24%2F) | | 日本語 (ja) | ■■■■■■■■■・ 95% | [contribute](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fja+%2F%28TODO%7CDIRTY%29%24%2F) | | 한국어 (ko) | ■■■■■■■■・・ 88% | [contribute](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fko+%2F%28TODO%7CDIRTY%29%24%2F) | @@ -145,7 +145,7 @@ See the [repository dedicated to those extensions](https://github.com/FreshRSS/E | Occitan (oc) | ■■■■■■■■・・ 81% | [contribute](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Foc+%2F%28TODO%7CDIRTY%29%24%2F) | | Polski (pl) | ■■■■■■■■■■ 100% | [contribute](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fpl+%2F%28TODO%7CDIRTY%29%24%2F) | | Português (Brasil) (pt-BR) | ■■■■■■■■・・ 88% | [contribute](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fpt-BR+%2F%28TODO%7CDIRTY%29%24%2F) | -| Português (Portugal) (pt-PT) | ■■■■■■■■・・ 88% | [contribute](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fpt-PT+%2F%28TODO%7CDIRTY%29%24%2F) | +| Português (Portugal) (pt-PT) | ■■■■■■■■・・ 87% | [contribute](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fpt-PT+%2F%28TODO%7CDIRTY%29%24%2F) | | Русский (ru) | ■■■■■■■■・・ 88% | [contribute](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fru+%2F%28TODO%7CDIRTY%29%24%2F) | | Slovenčina (sk) | ■■■■■■■■・・ 88% | [contribute](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fsk+%2F%28TODO%7CDIRTY%29%24%2F) | | Türkçe (tr) | ■■■■■■■■■・ 96% | [contribute](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Ftr+%2F%28TODO%7CDIRTY%29%24%2F) | diff --git a/app/Controllers/indexController.php b/app/Controllers/indexController.php index 7c66d308a..405c7c5a8 100644 --- a/app/Controllers/indexController.php +++ b/app/Controllers/indexController.php @@ -285,7 +285,7 @@ class FreshRSS_index_Controller extends FreshRSS_ActionController { $continuation_values = []; if (FreshRSS_Context::$continuation_id !== '0') { - if (in_array(FreshRSS_Context::$sort, ['c.name', 'date', 'f.name', 'link', 'title'], true)) { + if (in_array(FreshRSS_Context::$sort, ['c.name', 'date', 'f.name', 'link', 'title', 'lastUserModified'], true)) { $pagingEntry = $entryDAO->searchById(FreshRSS_Context::$continuation_id); if ($pagingEntry !== null && in_array(FreshRSS_Context::$sort, ['c.name', 'f.name'], true)) { @@ -302,6 +302,7 @@ class FreshRSS_index_Controller extends FreshRSS_ActionController { 'f.name' => $pagingEntry->feed()?->name() ?? '', 'link' => $pagingEntry->link(true), 'title' => $pagingEntry->title(), + 'lastUserModified' => $pagingEntry->lastUserModified(), }; if ($pagingEntry !== null && FreshRSS_Context::$sort === 'c.name') { // Secondary sort criterion diff --git a/app/Models/Context.php b/app/Models/Context.php index d6942fbcd..c0e25dffd 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'|'c.name'|'date'|'f.name'|'link'|'title'|'rand' */ + /** @var 'id'|'c.name'|'date'|'f.name'|'link'|'title'|'rand'|'lastUserModified' */ public static string $sort = 'id'; public static int $number = 0; public static int $offset = 0; @@ -252,7 +252,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', 'c.name', 'date', 'f.name', 'link', 'title', 'rand'], true) ? $sort : 'id'; + self::$sort = in_array($sort, ['id', 'c.name', 'date', 'f.name', 'link', 'title', 'rand', 'lastUserModified'], 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/Entry.php b/app/Models/Entry.php index 54516ca58..1f99a8345 100644 --- a/app/Models/Entry.php +++ b/app/Models/Entry.php @@ -24,6 +24,7 @@ class FreshRSS_Entry extends Minz_Model { private string $link; private int $date; private int $lastSeen = 0; + private int $lastUserModified = 0; /** In microseconds */ private string $date_added = '0'; private string $hash = ''; @@ -53,7 +54,8 @@ class FreshRSS_Entry extends Minz_Model { $this->_guid($guid); } - /** @param array{id?:string,id_feed?:int,guid?:string,title?:string,author?:string,content?:string,link?:string,date?:int|string,lastSeen?:int, + /** @param array{id?:string,id_feed?:int,guid?:string,title?:string,author?:string,content?:string,link?:string, + * date?:int|string,lastSeen?:int,lastUserModified?:int, * 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 { if (empty($dao['content']) || !is_string($dao['content'])) { @@ -97,6 +99,9 @@ class FreshRSS_Entry extends Minz_Model { if (isset($dao['lastSeen'])) { $entry->_lastSeen($dao['lastSeen']); } + if (isset($dao['lastUserModified'])) { + $entry->_lastUserModified($dao['lastUserModified']); + } if (!empty($dao['attributes'])) { $entry->_attributes($dao['attributes']); } @@ -107,8 +112,11 @@ class FreshRSS_Entry extends Minz_Model { } /** - * @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 + * @param Traversable<array{id?:string,id_feed?:int,guid?:string, + * title?:string,author?:string,content?:string,link?:string, + * date?:int|string,lastSeen?:int,lastUserModified?: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 { @@ -421,6 +429,10 @@ HTML; return $this->lastSeen; } + public function lastUserModified(): int { + return $this->lastUserModified; + } + /** * @phpstan-return ($raw is false ? string : ($microsecond is true ? string : int)) */ @@ -556,6 +568,11 @@ HTML; $this->lastSeen = $value > 0 ? $value : 0; } + public function _lastUserModified(int|string $value): void { + $value = (int)$value; + $this->lastUserModified = $value > 0 ? $value : 0; + } + /** @param int|numeric-string $value */ public function _dateAdded(int|string $value, bool $microsecond = false): void { if ($microsecond) { @@ -1046,8 +1063,9 @@ HTML; } /** - * @return 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>} + * @return array{id:string,guid:string,title:string,author:string,content:string,link:string,date:int, + * lastSeen:int,lastUserModified:int, + * hash:string,is_read:?bool,is_favorite:?bool,id_feed:int,tags:string,attributes:array<string,mixed>} */ public function toArray(): array { return [ @@ -1059,6 +1077,7 @@ HTML; 'link' => $this->link(raw: true), 'date' => $this->date(true), 'lastSeen' => $this->lastSeen(), + 'lastUserModified' => $this->lastUserModified(), 'hash' => $this->hash(), 'is_read' => $this->isRead(), 'is_favorite' => $this->isFavorite(), diff --git a/app/Models/EntryDAO.php b/app/Models/EntryDAO.php index d7a9a2cbc..707002875 100644 --- a/app/Models/EntryDAO.php +++ b/app/Models/EntryDAO.php @@ -119,6 +119,7 @@ SQL; $this->pdo->commit(); } Minz_Log::warning(__METHOD__ . ': ' . $name); + require APP_PATH . '/SQL/install.sql.' . $this->pdo->dbType() . '.php'; try { if ($name === 'attributes') { //v1.20.0 $sql = <<<'SQL' @@ -127,6 +128,13 @@ ALTER TABLE `_entrytmp` ADD COLUMN attributes TEXT; SQL; return $this->pdo->exec($sql) !== false; } + if ($name === 'lastUserModified') { //v1.28.0 + $sql = $GLOBALS['ALTER_TABLE_ENTRY_LAST_USER_MODIFIED']; + if (!is_string($sql)) { + throw new Exception('ALTER_TABLE_ENTRY_LAST_USER_MODIFIED is not a string!'); + } + return $this->pdo->exec($sql) !== false; + } } catch (Exception $e) { Minz_Log::error(__METHOD__ . ' error: ' . $e->getMessage()); } @@ -261,8 +269,11 @@ SQL; private PDOStatement|null|false $updateEntryPrepared = null; - /** @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 */ + /** + * @param array{id:string,guid:string,title:string,author:string,content:string,link:string, + * date:int,lastSeen:int,lastUserModified?:int,hash:string, + * is_read:bool|int|null,is_favorite:bool|int|null,id_feed:int,tags:string,attributes:array<string,mixed>} $valuesTmp + */ public function updateEntry(array $valuesTmp): bool { if (!isset($valuesTmp['is_read'])) { $valuesTmp['is_read'] = null; @@ -270,12 +281,16 @@ SQL; if (!isset($valuesTmp['is_favorite'])) { $valuesTmp['is_favorite'] = null; } + if (empty($valuesTmp['lastUserModified'])) { + $valuesTmp['lastUserModified'] = 0; + } if ($this->updateEntryPrepared == null) { $sql = 'UPDATE `_entry` ' . 'SET title=:title, author=:author, ' . (static::isCompressed() ? 'content_bin=COMPRESS(:content)' : 'content=:content') . ', link=:link, date=:date, `lastSeen`=:last_seen' + . ', `lastUserModified`=MAX(:last_user_modified, `lastUserModified`)' . ', hash=' . static::sqlHexDecode(':hash') . ', is_read=COALESCE(:is_read, is_read)' . ', is_favorite=COALESCE(:is_favorite, is_favorite)' @@ -300,6 +315,7 @@ SQL; $this->updateEntryPrepared->bindParam(':link', $valuesTmp['link']); $this->updateEntryPrepared->bindParam(':date', $valuesTmp['date'], PDO::PARAM_INT); $this->updateEntryPrepared->bindParam(':last_seen', $valuesTmp['lastSeen'], PDO::PARAM_INT); + $this->updateEntryPrepared->bindParam(':last_user_modified', $valuesTmp['lastUserModified'], PDO::PARAM_INT); if ($valuesTmp['is_read'] === null) { $this->updateEntryPrepared->bindValue(':is_read', null, PDO::PARAM_NULL); } else { @@ -332,7 +348,8 @@ SQL; return true; } else { $info = $this->updateEntryPrepared == false ? $this->pdo->errorInfo() : $this->updateEntryPrepared->errorInfo(); - /** @var array{id:string,guid:string,title:string,author:string,content:string,link:string,date:int,lastSeen:int,hash:string, + /** @var array{id:string,guid:string,title:string,author:string,content:string,link:string, + * date:int,lastSeen:int,lastUserModified:int,hash:string, * is_read:bool|int|null,is_favorite:bool|int|null,id_feed:int,tags:string,attributes:array<string,mixed>} $valuesTmp */ /** @var array{0:string,1:int,2:string} $info */ if ($this->autoUpdateDb($info)) { @@ -381,9 +398,10 @@ SQL; return $affected; } $sql = 'UPDATE `_entry` ' - . 'SET is_favorite=? ' + . 'SET is_favorite=?, `lastUserModified`=? ' . 'WHERE id IN (' . str_repeat('?,', count($ids) - 1) . '?)'; $values = [$is_favorite ? 1 : 0]; + $values[] = time(); $values = array_merge($values, $ids); $stm = $this->pdo->prepare($sql); if ($stm !== false && $stm->execute($values)) { @@ -462,9 +480,9 @@ SQL; FreshRSS_UserDAO::touch(); $sql = 'UPDATE `_entry` ' - . 'SET is_read=? ' - . 'WHERE id IN (' . str_repeat('?,', count($ids) - 1) . '?)'; - $values = [$is_read ? 1 : 0]; + . 'SET is_read=?, `lastUserModified`=? ' + . 'WHERE is_read<>? AND id IN (' . str_repeat('?,', count($ids) - 1) . '?)'; + $values = [$is_read ? 1 : 0, time(), $is_read ? 1 : 0]; $values = array_merge($values, $ids); $stm = $this->pdo->prepare($sql); if ($stm === false || !$stm->execute($values)) { @@ -480,10 +498,10 @@ SQL; } else { FreshRSS_UserDAO::touch(); $sql = 'UPDATE `_entry` e INNER JOIN `_feed` f ON e.id_feed=f.id ' - . 'SET e.is_read=?,' + . 'SET e.is_read=?,`lastUserModified`=?,' . 'f.`cache_nbUnreads`=f.`cache_nbUnreads`' . ($is_read ? '-' : '+') . '1 ' . 'WHERE e.id=? AND e.is_read=?'; - $values = [$is_read ? 1 : 0, $ids, $is_read ? 0 : 1]; + $values = [$is_read ? 1 : 0, time(), $ids, $is_read ? 0 : 1]; $stm = $this->pdo->prepare($sql); if ($stm !== false && $stm->execute($values)) { return $stm->rowCount(); @@ -516,8 +534,8 @@ SQL; Minz_Log::debug('Calling markReadEntries(0) is deprecated!'); } - $sql = 'UPDATE `_entry` SET is_read = ? WHERE is_read <> ? AND id <= ?'; - $values = [$is_read ? 1 : 0, $is_read ? 1 : 0, $idMax]; + $sql = 'UPDATE `_entry` SET is_read = ?, `lastUserModified`=? WHERE is_read <> ? AND id <= ?'; + $values = [$is_read ? 1 : 0, time(), $is_read ? 1 : 0, $idMax]; if ($onlyFavorites) { $sql .= ' AND is_favorite=1'; } @@ -569,11 +587,11 @@ SQL; $sql = <<<'SQL' UPDATE `_entry` -SET is_read = ? +SET is_read = ?, `lastUserModified` = ? WHERE is_read <> ? AND id <= ? AND id_feed IN (SELECT f.id FROM `_feed` f WHERE f.category=? AND f.priority >= ? AND f.priority < ?) SQL; - $values = [$is_read ? 1 : 0, $is_read ? 1 : 0, $idMax, $id, FreshRSS_Feed::PRIORITY_CATEGORY, FreshRSS_Feed::PRIORITY_IMPORTANT]; + $values = [$is_read ? 1 : 0, time(), $is_read ? 1 : 0, $idMax, $id, FreshRSS_Feed::PRIORITY_CATEGORY, FreshRSS_Feed::PRIORITY_IMPORTANT]; [$searchValues, $search] = $this->sqlListEntriesWhere(alias: '', state: $state, filters: $filters); @@ -613,9 +631,9 @@ SQL; } $sql = 'UPDATE `_entry` ' - . 'SET is_read=? ' + . 'SET is_read=?, `lastUserModified`=? ' . 'WHERE id_feed=? AND is_read <> ? AND id <= ?'; - $values = [$is_read ? 1 : 0, $id_feed, $is_read ? 1 : 0, $idMax]; + $values = [$is_read ? 1 : 0, time(), $id_feed, $is_read ? 1 : 0, $idMax]; [$searchValues, $search] = $this->sqlListEntriesWhere(alias: '', state: $state, filters: $filters); @@ -664,11 +682,11 @@ SQL; } $sql = 'UPDATE `_entry` e INNER JOIN `_entrytag` et ON et.id_entry = e.id ' - . 'SET e.is_read = ? ' + . 'SET e.is_read = ?, `lastUserModified` = ? ' . 'WHERE ' . ($id == 0 ? '' : 'et.id_tag = ? AND ') . 'e.is_read <> ? AND e.id <= ?'; - $values = [$is_read ? 1 : 0]; + $values = [$is_read ? 1 : 0, time()]; if ($id != 0) { $values[] = $id; } @@ -756,8 +774,9 @@ SQL; /** * @param 'ASC'|'DESC' $order - * @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:?string}> + * @return Traversable<array{id:string,guid:string,title:string,author:string,content:string,link:string, + * date:int,lastSeen:int,lastUserModified:int, + * hash:string,is_read:bool,is_favorite:bool,id_feed:int,tags:string,attributes:?string}> */ public function selectAll(string $order = 'ASC', int $limit = -1, int $offset = 0): Traversable { $content = static::isCompressed() ? 'UNCOMPRESS(content_bin) AS content' : 'content'; @@ -765,14 +784,14 @@ SQL; $order = in_array($order, ['ASC', 'DESC'], true) ? $order : 'ASC'; $sqlLimit = static::sqlLimit($limit, $offset); $sql = <<<SQL -SELECT id, guid, title, author, {$content}, link, date, `lastSeen`, {$hash} AS hash, is_read, is_favorite, id_feed, tags, attributes +SELECT id, guid, title, author, {$content}, link, date, `lastSeen`, `lastUserModified`, {$hash} AS hash, is_read, is_favorite, id_feed, tags, attributes FROM `_entry` ORDER BY id {$order} {$sqlLimit} SQL; $stm = $this->pdo->query($sql); if ($stm !== false) { while (is_array($row = $stm->fetch(PDO::FETCH_ASSOC))) { - /** @var array{id:string,guid:string,title:string,author:string,content:string,link:string,date:int,lastSeen:int, + /** @var array{id:string,guid:string,title:string,author:string,content:string,link:string,date:int,lastSeen:int,lastUserModified:int, * hash:string,is_read:bool,is_favorite:bool,id_feed:int,tags:string,attributes:?string} $row */ yield $row; } @@ -791,7 +810,7 @@ SQL; $content = static::isCompressed() ? 'UNCOMPRESS(content_bin) AS content' : 'content'; $hash = static::sqlHexEncode('hash'); $sql = <<<SQL -SELECT id, guid, title, author, link, date, is_read, is_favorite, {$hash} AS hash, id_feed, tags, attributes, {$content} +SELECT id, guid, title, author, {$content}, link, date, `lastSeen`, `lastUserModified`, {$hash} AS hash, is_read, is_favorite, id_feed, tags, attributes FROM `_entry` WHERE id_feed=:id_feed AND guid=:guid SQL; $res = $this->fetchAssoc($sql, [':id_feed' => $id_feed, ':guid' => $guid]); @@ -804,7 +823,7 @@ SQL; $content = static::isCompressed() ? 'UNCOMPRESS(content_bin) AS content' : 'content'; $hash = static::sqlHexEncode('hash'); $sql = <<<SQL -SELECT id, guid, title, author, link, date, is_read, is_favorite, {$hash} AS hash, id_feed, tags, attributes, {$content} +SELECT id, guid, title, author, {$content}, link, date, `lastSeen`, `lastUserModified`, {$hash} AS hash, is_read, is_favorite, id_feed, tags, attributes FROM `_entry` WHERE id=:id SQL; $res = $this->fetchAssoc($sql, [':id' => $id]); @@ -1210,7 +1229,7 @@ SQL; /** * @param numeric-string $id_min * @param numeric-string $id_max - * @param 'id'|'c.name'|'date'|'f.name'|'link'|'title'|'rand' $sort + * @param 'id'|'c.name'|'date'|'f.name'|'link'|'title'|'rand'|'lastUserModified' $sort * @param 'ASC'|'DESC' $order * @param numeric-string $continuation_id * @param list<string|int> $continuation_values @@ -1278,11 +1297,12 @@ SQL; $values[] = $id_min; } - if ($continuation_id !== '0' && in_array($sort, ['c.name', 'date', 'f.name', 'link', 'title'], true)) { + if ($continuation_id !== '0' && in_array($sort, ['c.name', 'date', 'f.name', 'link', 'title', 'lastUserModified'], true)) { $sign = $order === 'ASC' ? '>' : '<'; $orderBy = match ($sort) { 'c.name' => 'c.name', 'f.name' => 'f.name', + 'lastUserModified' => $alias . '`lastUserModified`', default => $alias . $sort, }; // Keyset pagination (Compatibility syntax due to poor performance of tuple syntax in MySQL https://bugs.mysql.com/bug.php?id=104128) @@ -1320,7 +1340,7 @@ SQL; * @param int $id category/feed/tag ID * @param numeric-string $id_min * @param numeric-string $id_max - * @param 'id'|'c.name'|'date'|'f.name'|'link'|'title'|'rand' $sort + * @param 'id'|'c.name'|'date'|'f.name'|'link'|'title'|'rand'|'lastUserModified' $sort * @param 'ASC'|'DESC' $order * @param numeric-string $continuation_id * @param list<string|int> $continuation_values @@ -1379,10 +1399,11 @@ SQL; } $order = in_array($order, ['ASC', 'DESC'], true) ? $order : 'DESC'; - $sort = in_array($sort, ['id', 'c.name', 'date', 'f.name', 'link', 'title', 'rand'], true) ? $sort : 'id'; + $sort = in_array($sort, ['id', 'c.name', 'date', 'f.name', 'link', 'title', 'rand', 'lastUserModified'], true) ? $sort : 'id'; $orderBy = match ($sort) { 'c.name' => 'c.name', 'f.name' => 'f.name', + 'lastUserModified' => 'e.`lastUserModified`', 'rand' => static::sqlRandom(), default => 'e.' . $sort, }; @@ -1412,7 +1433,7 @@ SQL; * @param int $id category/feed/tag ID * @param numeric-string $id_min * @param numeric-string $id_max - * @param 'id'|'c.name'|'date'|'f.name'|'link'|'title'|'rand' $sort + * @param 'id'|'c.name'|'date'|'f.name'|'link'|'title'|'rand'|'lastUserModified' $sort * @param 'ASC'|'DESC' $order * @param numeric-string $continuation_id * @param list<string|int> $continuation_values @@ -1422,7 +1443,7 @@ SQL; string $id_min = '0', string $id_max = '0', string $sort = 'id', string $order = 'DESC', 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', 'c.name', 'date', 'f.name', 'link', 'title', 'rand'], true) ? $sort : 'id'; + $sort = in_array($sort, ['id', 'c.name', 'date', 'f.name', 'link', 'title', 'rand', 'lastUserModified'], 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_values: $continuation_values, limit: $limit, offset: $offset); @@ -1430,15 +1451,16 @@ SQL; $orderBy = match ($sort) { 'c.name' => 'c0.name', 'f.name' => 'f0.name', + 'lastUserModified' => 'e0.`lastUserModified`', '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 +SELECT e0.id, e0.guid, e0.title, e0.author, {$content}, e0.link, + e0.date, e0.`lastSeen`, e0.`lastUserModified`, {$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 SQL; if ($sort === 'f.name' || $sort === 'c.name') { $sql .= ' INNER JOIN `_feed` f0 ON f0.id = e0.id_feed '; @@ -1474,7 +1496,7 @@ SQL; * @param int $id category/feed/tag ID * @param numeric-string $id_min * @param numeric-string $id_max - * @param 'id'|'c.name'|'date'|'f.name'|'link'|'title'|'rand' $sort + * @param 'id'|'c.name'|'date'|'f.name'|'link'|'title'|'rand'|'lastUserModified' $sort * @param 'ASC'|'DESC' $order * @param numeric-string $continuation_id * @param list<string|int> $continuation_values @@ -1520,7 +1542,7 @@ SQL; $hash = static::sqlHexEncode('hash'); $repeats = str_repeat('?,', count($ids) - 1) . '?'; $sql = <<<SQL -SELECT id, guid, title, author, link, date, {$hash} AS hash, is_read, is_favorite, id_feed, tags, attributes, {$content} +SELECT id, guid, title, author, link, date, {$hash} AS hash, is_read, is_favorite, id_feed, tags, attributes, {$content}, lastUserModified FROM `_entry` WHERE id IN ({$repeats}) ORDER BY id {$order} diff --git a/app/Models/EntryDAOPGSQL.php b/app/Models/EntryDAOPGSQL.php index 68b52d071..72bdd7f3e 100644 --- a/app/Models/EntryDAOPGSQL.php +++ b/app/Models/EntryDAOPGSQL.php @@ -66,7 +66,7 @@ class FreshRSS_EntryDAOPGSQL extends FreshRSS_EntryDAOSQLite { if (isset($errorInfo[0])) { if ($errorInfo[0] === FreshRSS_DatabaseDAO::ER_BAD_FIELD_ERROR || $errorInfo[0] === FreshRSS_DatabaseDAOPGSQL::UNDEFINED_COLUMN) { $errorLines = explode("\n", $errorInfo[2], 2); // The relevant column name is on the first line, other lines are noise - foreach (['attributes'] as $column) { + foreach (['attributes', 'lastUserModified'] as $column) { if (str_contains($errorLines[0], $column)) { return $this->addColumn($column); } diff --git a/app/Models/EntryDAOSQLite.php b/app/Models/EntryDAOSQLite.php index d46b48f27..8bf0e2209 100644 --- a/app/Models/EntryDAOSQLite.php +++ b/app/Models/EntryDAOSQLite.php @@ -64,7 +64,7 @@ class FreshRSS_EntryDAOSQLite extends FreshRSS_EntryDAO { #[\Override] protected function autoUpdateDb(array $errorInfo): bool { if (($tableInfo = $this->pdo->query("PRAGMA table_info('entry')")) !== false && ($columns = $tableInfo->fetchAll(PDO::FETCH_COLUMN, 1)) !== false) { - foreach (['attributes'] as $column) { + foreach (['attributes', 'lastUserModified'] as $column) { if (!in_array($column, $columns, true)) { return $this->addColumn($column); } @@ -124,8 +124,8 @@ SQL; } else { FreshRSS_UserDAO::touch(); $this->pdo->beginTransaction(); - $sql = 'UPDATE `_entry` SET is_read=? WHERE id=? AND is_read=?'; - $values = [$is_read ? 1 : 0, $ids, $is_read ? 0 : 1]; + $sql = 'UPDATE `_entry` SET is_read=?, `lastUserModified` = ? WHERE id=? AND is_read=?'; + $values = [$is_read ? 1 : 0, time(), $ids, $is_read ? 0 : 1]; $stm = $this->pdo->prepare($sql); if ($stm === false || !$stm->execute($values)) { $info = $stm === false ? $this->pdo->errorInfo() : $stm->errorInfo(); @@ -165,11 +165,11 @@ SQL; Minz_Log::debug('Calling markReadTag(0) is deprecated!'); } - $sql = 'UPDATE `_entry` SET is_read = ? WHERE is_read <> ? AND id <= ? AND ' + $sql = 'UPDATE `_entry` SET is_read = ?, `lastUserModified` = ? WHERE is_read <> ? AND id <= ? AND ' . 'id IN (SELECT et.id_entry FROM `_entrytag` et ' . ($id == 0 ? '' : 'WHERE et.id_tag = ?') . ')'; - $values = [$is_read ? 1 : 0, $is_read ? 1 : 0, $idMax]; + $values = [$is_read ? 1 : 0, time(), $is_read ? 1 : 0, $idMax]; if ($id != 0) { $values[] = $id; } diff --git a/app/SQL/install.sql.mysql.php b/app/SQL/install.sql.mysql.php index 40553608a..5e8d092c5 100644 --- a/app/SQL/install.sql.mysql.php +++ b/app/SQL/install.sql.mysql.php @@ -49,6 +49,7 @@ CREATE TABLE IF NOT EXISTS `_entry` ( `link` VARCHAR(16383) CHARACTER SET latin1 COLLATE latin1_bin NOT NULL, `date` BIGINT, `lastSeen` BIGINT DEFAULT 0, + `lastUserModified` BIGINT DEFAULT 0, -- v1.28.0 `hash` BINARY(16), -- v1.1.1 `is_read` BOOLEAN NOT NULL DEFAULT 0, `is_favorite` BOOLEAN NOT NULL DEFAULT 0, @@ -61,6 +62,7 @@ CREATE TABLE IF NOT EXISTS `_entry` ( INDEX (`is_favorite`), -- v0.7 INDEX (`is_read`), -- v0.7 INDEX `entry_lastSeen_index` (`lastSeen`), -- v1.1.1 + INDEX `entry_last_user_modified_index` (`lastUserModified`), -- v1.28.0 INDEX `entry_feed_read_index` (`id_feed`,`is_read`) -- v1.7 ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = INNODB; @@ -109,6 +111,11 @@ CREATE TABLE IF NOT EXISTS `_entrytag` ( -- v1.12 ENGINE = INNODB; SQL; +$GLOBALS['ALTER_TABLE_ENTRY_LAST_USER_MODIFIED'] = <<<'SQL' +ALTER TABLE `_entry` ADD `lastUserModified` BIGINT DEFAULT 0; -- 1.28.0 +CREATE INDEX IF NOT EXISTS `entry_last_user_modified_index` ON `_entry` (`lastUserModified`); -- //v1.28.0 +SQL; + $GLOBALS['SQL_DROP_TABLES'] = <<<'SQL' DROP TABLE IF EXISTS `_entrytag`, `_tag`, `_entrytmp`, `_entry`, `_feed`, `_category`; SQL; diff --git a/app/SQL/install.sql.pgsql.php b/app/SQL/install.sql.pgsql.php index 5ea59b828..557a42a34 100644 --- a/app/SQL/install.sql.pgsql.php +++ b/app/SQL/install.sql.pgsql.php @@ -44,6 +44,7 @@ CREATE TABLE IF NOT EXISTS `_entry` ( "link" VARCHAR(16383) NOT NULL, "date" BIGINT, "lastSeen" BIGINT DEFAULT 0, + "lastUserModified" BIGINT DEFAULT 0, "hash" BYTEA, "is_read" SMALLINT NOT NULL DEFAULT 0, "is_favorite" SMALLINT NOT NULL DEFAULT 0, @@ -57,6 +58,7 @@ CREATE INDEX IF NOT EXISTS `_is_favorite_index` ON `_entry` ("is_favorite"); CREATE INDEX IF NOT EXISTS `_is_read_index` ON `_entry` ("is_read"); CREATE INDEX IF NOT EXISTS `_entry_lastSeen_index` ON `_entry` ("lastSeen"); CREATE INDEX IF NOT EXISTS `_entry_feed_read_index` ON `_entry` ("id_feed","is_read"); -- v1.7 +CREATE INDEX IF NOT EXISTS `_entry_last_user_modified_index` ON `_entry` ("lastUserModified"); -- v1.28.0 INSERT INTO `_category` (id, name) SELECT 1, 'Uncategorized' @@ -98,6 +100,11 @@ CREATE TABLE IF NOT EXISTS `_entrytag` ( CREATE INDEX IF NOT EXISTS `_entrytag_id_entry_index` ON `_entrytag` ("id_entry"); SQL; +$GLOBALS['ALTER_TABLE_ENTRY_LAST_USER_MODIFIED'] = <<<'SQL' +ALTER TABLE `_entry` ADD `lastUserModified` BIGINT DEFAULT 0; -- 1.28.0 +CREATE INDEX IF NOT EXISTS `_entry_last_user_modified_index` ON `_entry` (`lastUserModified`); +SQL; + $GLOBALS['SQL_DROP_TABLES'] = <<<'SQL' DROP TABLE IF EXISTS `_entrytag`, `_tag`, `_entrytmp`, `_entry`, `_feed`, `_category`; SQL; diff --git a/app/SQL/install.sql.sqlite.php b/app/SQL/install.sql.sqlite.php index 1deb627d3..55de33f71 100644 --- a/app/SQL/install.sql.sqlite.php +++ b/app/SQL/install.sql.sqlite.php @@ -45,6 +45,7 @@ CREATE TABLE IF NOT EXISTS `entry` ( `link` VARCHAR(16383) NOT NULL, `date` BIGINT, `lastSeen` BIGINT DEFAULT 0, + `lastUserModified` BIGINT DEFAULT 0, -- v1.28.0 `hash` BINARY(16), -- v1.1.1 `is_read` BOOLEAN NOT NULL DEFAULT 0, `is_favorite` BOOLEAN NOT NULL DEFAULT 0, @@ -59,6 +60,7 @@ CREATE INDEX IF NOT EXISTS entry_is_favorite_index ON `entry`(`is_favorite`); CREATE INDEX IF NOT EXISTS entry_is_read_index ON `entry`(`is_read`); CREATE INDEX IF NOT EXISTS entry_lastSeen_index ON `entry`(`lastSeen`); -- //v1.1.1 CREATE INDEX IF NOT EXISTS entry_feed_read_index ON `entry`(`id_feed`,`is_read`); -- v1.7 +CREATE INDEX IF NOT EXISTS entry_last_user_modified_index ON `entry` (`lastUserModified`); -- //v1.28.0 INSERT OR IGNORE INTO `category` (id, name) VALUES(1, 'Uncategorized'); @@ -99,6 +101,11 @@ CREATE TABLE IF NOT EXISTS `entrytag` ( CREATE INDEX IF NOT EXISTS entrytag_id_entry_index ON `entrytag` (`id_entry`); SQL; +$GLOBALS['ALTER_TABLE_ENTRY_LAST_USER_MODIFIED'] = <<<'SQL' +ALTER TABLE `entry` ADD `lastUserModified` BIGINT DEFAULT 0; -- 1.28.0 +CREATE INDEX IF NOT EXISTS entry_last_user_modified_index ON `entry` (`lastUserModified`); +SQL; + $GLOBALS['SQL_DROP_TABLES'] = <<<'SQL' DROP TABLE IF EXISTS `entrytag`; DROP TABLE IF EXISTS `tag`; diff --git a/app/i18n/cs/index.php b/app/i18n/cs/index.php index c0f968c50..255f0b18b 100644 --- a/app/i18n/cs/index.php +++ b/app/i18n/cs/index.php @@ -94,6 +94,8 @@ return array( 'rand' => 'Random order', // TODO 'title_asc' => 'Title A→Z', // TODO 'title_desc' => 'Title Z→A', // TODO + 'user_modified_asc' => 'User modified 1→9', // TODO + 'user_modified_desc' => 'User modified 9→1', // TODO ), 'starred' => 'Zobrazit oblíbené', 'stats' => 'Statistika', diff --git a/app/i18n/de/index.php b/app/i18n/de/index.php index a8493459c..f2d3346e3 100644 --- a/app/i18n/de/index.php +++ b/app/i18n/de/index.php @@ -94,6 +94,8 @@ return array( 'rand' => 'Zufällige Reihenfolge', 'title_asc' => 'Titel A→Z', 'title_desc' => 'Titel Z→A', + 'user_modified_asc' => 'User modified 1→9', // TODO + 'user_modified_desc' => 'User modified 9→1', // TODO ), 'starred' => 'Nur Favoriten zeigen', 'stats' => 'Statistiken', diff --git a/app/i18n/el/index.php b/app/i18n/el/index.php index c33036c5c..8ac8a4927 100644 --- a/app/i18n/el/index.php +++ b/app/i18n/el/index.php @@ -94,6 +94,8 @@ return array( 'rand' => 'Random order', // TODO 'title_asc' => 'Title A→Z', // TODO 'title_desc' => 'Title Z→A', // TODO + 'user_modified_asc' => 'User modified 1→9', // TODO + 'user_modified_desc' => 'User modified 9→1', // TODO ), 'starred' => 'Show favourites', // TODO 'stats' => 'Statistics', // TODO diff --git a/app/i18n/en-US/index.php b/app/i18n/en-US/index.php index 7a019bce0..a65b752f7 100644 --- a/app/i18n/en-US/index.php +++ b/app/i18n/en-US/index.php @@ -94,6 +94,8 @@ return array( 'rand' => 'Random order', // IGNORE 'title_asc' => 'Title A→Z', // IGNORE 'title_desc' => 'Title Z→A', // IGNORE + 'user_modified_asc' => 'User modified 1→9', // IGNORE + 'user_modified_desc' => 'User modified 9→1', // IGNORE ), 'starred' => 'Show favorites', 'stats' => 'Statistics', // IGNORE diff --git a/app/i18n/en/index.php b/app/i18n/en/index.php index 4e566c4af..47e89f148 100644 --- a/app/i18n/en/index.php +++ b/app/i18n/en/index.php @@ -94,6 +94,8 @@ return array( 'rand' => 'Random order', 'title_asc' => 'Title A→Z', 'title_desc' => 'Title Z→A', + 'user_modified_asc' => 'User modified 1→9', + 'user_modified_desc' => 'User modified 9→1', ), 'starred' => 'Show favourites', 'stats' => 'Statistics', diff --git a/app/i18n/es/index.php b/app/i18n/es/index.php index e24ad7178..6ace58d64 100644 --- a/app/i18n/es/index.php +++ b/app/i18n/es/index.php @@ -94,6 +94,8 @@ return array( 'rand' => 'Random order', // TODO 'title_asc' => 'Title A→Z', // TODO 'title_desc' => 'Title Z→A', // TODO + 'user_modified_asc' => 'User modified 1→9', // TODO + 'user_modified_desc' => 'User modified 9→1', // TODO ), 'starred' => 'Mostrar solo los favoritos', 'stats' => 'Estadísticas', diff --git a/app/i18n/fa/index.php b/app/i18n/fa/index.php index 5db7e8b28..588bfba4b 100644 --- a/app/i18n/fa/index.php +++ b/app/i18n/fa/index.php @@ -94,6 +94,8 @@ return array( 'rand' => 'ترتیب تصادفی', 'title_asc' => 'عنوانA→Z', 'title_desc' => 'عنوان Z→A', + 'user_modified_asc' => 'User modified 1→9', // TODO + 'user_modified_desc' => 'User modified 9→1', // TODO ), 'starred' => ' نمایش موارد دلخواه', 'stats' => ' آمار', diff --git a/app/i18n/fi/index.php b/app/i18n/fi/index.php index edbb2300d..ccd6454fd 100644 --- a/app/i18n/fi/index.php +++ b/app/i18n/fi/index.php @@ -94,6 +94,8 @@ return array( 'rand' => 'Satunnainen järjestys', 'title_asc' => 'Otsikko A→Ö', 'title_desc' => 'Otsikko Ö→A', + 'user_modified_asc' => 'User modified 1→9', // TODO + 'user_modified_desc' => 'User modified 9→1', // TODO ), 'starred' => 'Näytä suosikit', 'stats' => 'Tilastot', diff --git a/app/i18n/fr/index.php b/app/i18n/fr/index.php index 1707100c6..ab7bc08cb 100644 --- a/app/i18n/fr/index.php +++ b/app/i18n/fr/index.php @@ -94,6 +94,8 @@ return array( 'rand' => 'Ordre aléatoire', 'title_asc' => 'Titre A→Z', 'title_desc' => 'Titre Z→A', + 'user_modified_asc' => 'Modifié par l’utilisateur 1→9', + 'user_modified_desc' => 'Modifié par l’utilisateur 9→1', ), 'starred' => 'Afficher les favoris', 'stats' => 'Statistiques', diff --git a/app/i18n/he/index.php b/app/i18n/he/index.php index 4b39ab225..16037eba3 100644 --- a/app/i18n/he/index.php +++ b/app/i18n/he/index.php @@ -94,6 +94,8 @@ return array( 'rand' => 'Random order', // TODO 'title_asc' => 'Title A→Z', // TODO 'title_desc' => 'Title Z→A', // TODO + 'user_modified_asc' => 'User modified 1→9', // TODO + 'user_modified_desc' => 'User modified 9→1', // TODO ), 'starred' => 'הצגת מועדפים בלבד', 'stats' => 'סטטיסטיקות', diff --git a/app/i18n/hu/index.php b/app/i18n/hu/index.php index 664a41592..36c945f78 100644 --- a/app/i18n/hu/index.php +++ b/app/i18n/hu/index.php @@ -94,6 +94,8 @@ return array( 'rand' => 'Véletlen sorrend', 'title_asc' => 'Cím A→Z', 'title_desc' => 'Cím Z→A', + 'user_modified_asc' => 'User modified 1→9', // TODO + 'user_modified_desc' => 'User modified 9→1', // TODO ), 'starred' => 'Kedvencek megjelenítése', 'stats' => 'Statisztika', diff --git a/app/i18n/id/index.php b/app/i18n/id/index.php index de0ddbc3f..7b14a9ee8 100644 --- a/app/i18n/id/index.php +++ b/app/i18n/id/index.php @@ -94,6 +94,8 @@ return array( 'rand' => 'Acak', 'title_asc' => 'Judul A→Z', 'title_desc' => 'Judul Z→A', + 'user_modified_asc' => 'User modified 1→9', // TODO + 'user_modified_desc' => 'User modified 9→1', // TODO ), 'starred' => 'Tampilkan yang difavoritkan', 'stats' => 'Statistik', diff --git a/app/i18n/it/index.php b/app/i18n/it/index.php index 7ca367401..cc6c2999a 100644 --- a/app/i18n/it/index.php +++ b/app/i18n/it/index.php @@ -94,6 +94,8 @@ return array( 'rand' => 'Ordine casuale', 'title_asc' => 'Titolo A→Z', 'title_desc' => 'Titolo Z→A', + 'user_modified_asc' => 'User modified 1→9', // TODO + 'user_modified_desc' => 'User modified 9→1', // TODO ), 'starred' => 'Mostra solo preferiti', 'stats' => 'Statistiche', diff --git a/app/i18n/ja/index.php b/app/i18n/ja/index.php index f4eb7584b..831029974 100644 --- a/app/i18n/ja/index.php +++ b/app/i18n/ja/index.php @@ -94,6 +94,8 @@ return array( 'rand' => 'ランダムに並べる', 'title_asc' => 'タイトル順 A→Z', 'title_desc' => 'タイトル順 Z→A', + 'user_modified_asc' => 'User modified 1→9', // TODO + 'user_modified_desc' => 'User modified 9→1', // TODO ), 'starred' => 'お気に入りを表示する', 'stats' => '統計', diff --git a/app/i18n/ko/index.php b/app/i18n/ko/index.php index 82a7b1655..c54bbe7c2 100644 --- a/app/i18n/ko/index.php +++ b/app/i18n/ko/index.php @@ -94,6 +94,8 @@ return array( 'rand' => 'Random order', // TODO 'title_asc' => 'Title A→Z', // TODO 'title_desc' => 'Title Z→A', // TODO + 'user_modified_asc' => 'User modified 1→9', // TODO + 'user_modified_desc' => 'User modified 9→1', // TODO ), 'starred' => '즐겨찾기만 표시', 'stats' => '통계', diff --git a/app/i18n/lv/index.php b/app/i18n/lv/index.php index c84041843..f985e67c3 100644 --- a/app/i18n/lv/index.php +++ b/app/i18n/lv/index.php @@ -94,6 +94,8 @@ return array( 'rand' => 'Random order', // TODO 'title_asc' => 'Title A→Z', // TODO 'title_desc' => 'Title Z→A', // TODO + 'user_modified_asc' => 'User modified 1→9', // TODO + 'user_modified_desc' => 'User modified 9→1', // TODO ), 'starred' => 'Rādīt mīļākos', 'stats' => 'Statistika', diff --git a/app/i18n/nl/index.php b/app/i18n/nl/index.php index 331059838..ea7582845 100644 --- a/app/i18n/nl/index.php +++ b/app/i18n/nl/index.php @@ -94,6 +94,8 @@ return array( 'rand' => 'Willekeurige volgorde', 'title_asc' => 'Titel A→Z', 'title_desc' => 'Titel Z→A', + 'user_modified_asc' => 'Aangepast door gebruiker 1→9', + 'user_modified_desc' => 'Aangepast door gebruiker 9→1', ), 'starred' => 'Laat alleen favorieten zien', 'stats' => 'Statistieken', diff --git a/app/i18n/oc/index.php b/app/i18n/oc/index.php index 4dd0c90b1..dce2e1ac6 100644 --- a/app/i18n/oc/index.php +++ b/app/i18n/oc/index.php @@ -94,6 +94,8 @@ return array( 'rand' => 'Random order', // TODO 'title_asc' => 'Title A→Z', // TODO 'title_desc' => 'Title Z→A', // TODO + 'user_modified_asc' => 'User modified 1→9', // TODO + 'user_modified_desc' => 'User modified 9→1', // TODO ), 'starred' => 'Mostrar los favorits', 'stats' => 'Estatisticas', diff --git a/app/i18n/pl/index.php b/app/i18n/pl/index.php index 75d938b81..8cf9d999b 100644 --- a/app/i18n/pl/index.php +++ b/app/i18n/pl/index.php @@ -94,6 +94,8 @@ return array( 'rand' => 'Losowa kolejność', 'title_asc' => 'Tytuł A→Z', 'title_desc' => 'Tytuł Z→A', + 'user_modified_asc' => 'Zmodyfikowane przez użytkownika 1→9', + 'user_modified_desc' => 'Zmodyfikowane przez użytkownika 9→1', ), 'starred' => 'Pokaż ulubione', 'stats' => 'Statystyki', diff --git a/app/i18n/pt-BR/index.php b/app/i18n/pt-BR/index.php index 9a52c27cd..777e5f895 100644 --- a/app/i18n/pt-BR/index.php +++ b/app/i18n/pt-BR/index.php @@ -94,6 +94,8 @@ return array( 'rand' => 'Random order', // TODO 'title_asc' => 'Title A→Z', // TODO 'title_desc' => 'Title Z→A', // TODO + 'user_modified_asc' => 'User modified 1→9', // TODO + 'user_modified_desc' => 'User modified 9→1', // TODO ), 'starred' => 'Mostrar apenas os favoritos', 'stats' => 'Estatísticas', diff --git a/app/i18n/pt-PT/index.php b/app/i18n/pt-PT/index.php index 14a9acacc..c57d6907f 100644 --- a/app/i18n/pt-PT/index.php +++ b/app/i18n/pt-PT/index.php @@ -94,6 +94,8 @@ return array( 'rand' => 'Random order', // TODO 'title_asc' => 'Title A→Z', // TODO 'title_desc' => 'Title Z→A', // TODO + 'user_modified_asc' => 'User modified 1→9', // TODO + 'user_modified_desc' => 'User modified 9→1', // TODO ), 'starred' => 'Mostrar apenas os favoritos', 'stats' => 'Estatísticas', diff --git a/app/i18n/ru/index.php b/app/i18n/ru/index.php index 801780f9d..d341d5712 100644 --- a/app/i18n/ru/index.php +++ b/app/i18n/ru/index.php @@ -94,6 +94,8 @@ return array( 'rand' => 'Random order', // TODO 'title_asc' => 'Title A→Z', // TODO 'title_desc' => 'Title Z→A', // TODO + 'user_modified_asc' => 'User modified 1→9', // TODO + 'user_modified_desc' => 'User modified 9→1', // TODO ), 'starred' => 'Показать избранное', 'stats' => 'Статистика', diff --git a/app/i18n/sk/index.php b/app/i18n/sk/index.php index 20f1dfd93..87a352449 100644 --- a/app/i18n/sk/index.php +++ b/app/i18n/sk/index.php @@ -94,6 +94,8 @@ return array( 'rand' => 'Random order', // TODO 'title_asc' => 'Title A→Z', // TODO 'title_desc' => 'Title Z→A', // TODO + 'user_modified_asc' => 'User modified 1→9', // TODO + 'user_modified_desc' => 'User modified 9→1', // TODO ), 'starred' => 'Zobraziť obľúbené', 'stats' => 'Štatistiky', diff --git a/app/i18n/tr/index.php b/app/i18n/tr/index.php index 794a65a11..38fc548a9 100644 --- a/app/i18n/tr/index.php +++ b/app/i18n/tr/index.php @@ -94,6 +94,8 @@ return array( 'rand' => 'Rastgele sıralama', 'title_asc' => 'Başlık A→Z', 'title_desc' => 'Başlık Z→A', + 'user_modified_asc' => 'User modified 1→9', // TODO + 'user_modified_desc' => 'User modified 9→1', // TODO ), 'starred' => 'Favorileri göster', 'stats' => 'İstatistikler', diff --git a/app/i18n/uk/index.php b/app/i18n/uk/index.php index 73f57571c..533b51b80 100644 --- a/app/i18n/uk/index.php +++ b/app/i18n/uk/index.php @@ -94,6 +94,8 @@ return array( 'rand' => 'Довільний порядок', 'title_asc' => 'Заголовок А→Я', 'title_desc' => 'Заголовок Я→А', + 'user_modified_asc' => 'User modified 1→9', // TODO + 'user_modified_desc' => 'User modified 9→1', // TODO ), 'starred' => 'Показати вподобані', 'stats' => 'Статистика', diff --git a/app/i18n/zh-CN/index.php b/app/i18n/zh-CN/index.php index 33c9998b0..87aaa8399 100644 --- a/app/i18n/zh-CN/index.php +++ b/app/i18n/zh-CN/index.php @@ -94,6 +94,8 @@ return array( 'rand' => 'Random order', // TODO 'title_asc' => 'Title A→Z', // TODO 'title_desc' => 'Title Z→A', // TODO + 'user_modified_asc' => 'User modified 1→9', // TODO + 'user_modified_desc' => 'User modified 9→1', // TODO ), 'starred' => '显示收藏', 'stats' => '统计', diff --git a/app/i18n/zh-TW/index.php b/app/i18n/zh-TW/index.php index 4fcae7478..1a1b52ba4 100644 --- a/app/i18n/zh-TW/index.php +++ b/app/i18n/zh-TW/index.php @@ -94,6 +94,8 @@ return array( 'rand' => 'Random order', // TODO 'title_asc' => 'Title A→Z', // TODO 'title_desc' => 'Title Z→A', // TODO + 'user_modified_asc' => 'User modified 1→9', // TODO + 'user_modified_desc' => 'User modified 9→1', // TODO ), 'starred' => '顯示收藏', 'stats' => '統計', diff --git a/app/layout/nav_menu.phtml b/app/layout/nav_menu.phtml index d477bff37..0c6fc7dd2 100644 --- a/app/layout/nav_menu.phtml +++ b/app/layout/nav_menu.phtml @@ -233,6 +233,8 @@ <a href="<?= Minz_Url::display($url_order, amend: ['params' => ['sort' => 'id', 'order' => 'DESC']]) ?>"><?= _t('index.menu.sort.id_desc') ?></a></li> <li class="item" role="radio" aria-checked="<?= FreshRSS_Context::$order === 'DESC' && FreshRSS_Context::$sort === 'date' ? 'true' : 'false' ?>"> <a href="<?= Minz_Url::display($url_order, amend: ['params' => ['sort' => 'date', 'order' => 'DESC']]) ?>"><?= _t('index.menu.sort.date_desc') ?></a></li> + <li class="item" role="radio" aria-checked="<?= FreshRSS_Context::$order === 'DESC' && FreshRSS_Context::$sort === 'lastUserModified' ? 'true' : 'false' ?>"> + <a href="<?= Minz_Url::display($url_order, amend: ['params' => ['sort' => 'lastUserModified', 'order' => 'DESC']]) ?>"><?= _t('index.menu.sort.user_modified_desc') ?></a></li> <li class="item" role="radio" aria-checked="<?= FreshRSS_Context::$order === 'DESC' && FreshRSS_Context::$sort === 'link' ? 'true' : 'false' ?>"> <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' ?>"> @@ -247,6 +249,8 @@ <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' ?>"> <a href="<?= Minz_Url::display($url_order, amend: ['params' => ['sort' => 'date', 'order' => 'ASC']]) ?>"><?= _t('index.menu.sort.date_asc') ?></a></li> + <li class="item" role="radio" aria-checked="<?= FreshRSS_Context::$order === 'ASC' && FreshRSS_Context::$sort === 'lastUserModified' ? 'true' : 'false' ?>"> + <a href="<?= Minz_Url::display($url_order, amend: ['params' => ['sort' => 'lastUserModified', 'order' => 'ASC']]) ?>"><?= _t('index.menu.sort.user_modified_asc') ?></a></li> <li class="item" role="radio" aria-checked="<?= FreshRSS_Context::$order === 'ASC' && FreshRSS_Context::$sort === 'link' ? 'true' : 'false' ?>"> <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' ?>"> |
