mirror of
https://github.com/FreshRSS/FreshRSS.git
synced 2026-07-16 10:02:59 -04:00
* Add option to keep or reset article sort order when navigating Adds a new per-user reading option, "Keep the current sort order when navigating between categories and feeds" (config key: sticky_sort), under Settings > Reading > "Left navigation: Categories". It defaults to enabled, which reproduces the current behaviour exactly. Background (addresses #8953) ---------------------------- Once a sort/order was active in one view, it was propagated to every sidebar link and to the "mark all as read" redirect. Opening another category then ignored that category's own defaultSort/defaultOrder, because FreshRSS_Context only applies those defaults when sort/order are absent from the request. As a result the current view's sort (whether chosen manually or resolved from that view's default) "bled" into the next category/feed. Behaviour of the new option --------------------------- - ON (default): unchanged. The current sort/order stays sticky while navigating, exactly as before. - OFF: sidebar links and the "mark all as read" redirect no longer carry sort/order, so each target resolves its own order in FreshRSS_Context: feed/category defaultSort/defaultOrder, falling back to the global Reading sort criterion when none is configured. order/sort propagation across the UI (for reference) ---------------------------------------------------- - Categories & feeds (sidebar): now gated by sticky_sort (app/layout/aside_feed.phtml). - "Mark all as read" -> next category, both the top navigation button and the stream-footer button: now gated by sticky_sort (app/Controllers/entryController.php::readAction, non-AJAX redirect). - Custom user queries: unaffected. A query's link is a self-contained URL built from that query's own saved parameters, so switching query A -> B never carried A's sort; no option is needed there. - Stream transition links when sorting by category/feed name: intentionally left unchanged (out of scope; the sort is intrinsic to that grouping). Compatibility ------------- - Not a breaking change: the default (true) reproduces the previous behaviour. - No database migration. The setting is a standard user configuration value with its default declared in config-user.default.php, so existing users keep the previous behaviour transparently. - The read/unread state filter still persists across navigation regardless of this option. i18n ---- English strings authored; en-US marked as IGNORE. Translations for the other 25 languages were produced with an automated (AI) translation service and should be reviewed by native speakers. * Update app/i18n/en/conf.php Co-authored-by: Frans de Jonge <fransdejonge@gmail.com> * Comments preference * Update app/i18n/en/conf.php Co-authored-by: Frans de Jonge <fransdejonge@gmail.com> * i18n updates and comment * Update translations to match reworded sticky_sort strings Re-applies the 24 non-English/French translations of the "sticky_sort" reading option after the English and French wording was updated to "Keep manual sort order during navigation". * improve Dutch * i18n DIRTY * i18n en: custom sort order --------- Co-authored-by: Alexandre Alapetite <alexandre@alapetite.fr> Co-authored-by: Frans de Jonge <fransdejonge@gmail.com>
229 lines
11 KiB
PHTML
229 lines
11 KiB
PHTML
<?php
|
|
declare(strict_types=1);
|
|
/** @var FreshRSS_View $this */
|
|
$actual_view = Minz_Request::actionName();
|
|
$class = '';
|
|
if (FreshRSS_Context::userConf()->hide_read_feeds &&
|
|
(FreshRSS_Context::isStateEnabled(FreshRSS_Entry::STATE_NOT_READ) || FreshRSS_Context::isStateEnabled(FreshRSS_Entry::STATE_OR_NOT_READ)) &&
|
|
!FreshRSS_Context::isStateEnabled(FreshRSS_Entry::STATE_READ)) {
|
|
$class = ' state_unread';
|
|
}
|
|
|
|
$state_filter_manual = '';
|
|
if (($s = Minz_Request::paramString('state', plaintext: true)) !== '' && ctype_digit($s)) {
|
|
$state_filter_manual .= '&state=' . $s;
|
|
}
|
|
if (FreshRSS_Context::userConf()->sticky_sort) {
|
|
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)) {
|
|
$state_filter_manual .= '&order=' . $s;
|
|
}
|
|
}
|
|
|
|
$hideSucGlobal = FreshRSS_Context::userConf()->show_unread_count !== 'all' ? ' data-unread-hide="1"' : '';
|
|
$hideSucImportant = FreshRSS_Context::userConf()->show_unread_count !== 'none' ? '' : ' data-unread-hide="1"';
|
|
?>
|
|
<nav class="nav aside aside_feed<?= $class ?>" id="aside_feed">
|
|
<a class="toggle_aside" href="#close"><?= _i('close') ?></a>
|
|
|
|
<?php if (FreshRSS_Auth::hasAccess()) { ?>
|
|
<div class="stick configure-feeds">
|
|
<a id="btn-subscription" class="btn btn-important" href="<?= _url('subscription', 'index') ?>"><?= _t('index.menu.subscription') ?></a>
|
|
<a id="btn-add" class="btn btn-important" href="<?= _url('subscription', 'add') ?>"><?= _i('add') ?></a>
|
|
</div>
|
|
<?php } elseif (FreshRSS_Auth::accessNeedsLogin()) { ?>
|
|
<a href="<?= _url('index', 'about') ?>" class="about"><?= _t('index.menu.about') ?></a>
|
|
<?php } ?>
|
|
|
|
<form id="mark-read-aside" method="post">
|
|
<input type="hidden" name="_csrf" value="<?= FreshRSS_Auth::csrfToken() ?>" />
|
|
<ul id="sidebar" class="tree scrollbar-thin">
|
|
<li class="tree-folder category all<?= FreshRSS_Context::isCurrentGet('a') || FreshRSS_Context::isCurrentGet('A') || FreshRSS_Context::isCurrentGet('Z') ? ' active' : '' ?>">
|
|
<a class="tree-folder-title" data-unread="<?= format_number(FreshRSS_Context::$total_unread) ?>"<?=
|
|
$hideSucGlobal ?> href="<?= _url('index', $actual_view) . $state_filter_manual ?>">
|
|
<?= _i('all') ?><span class="title" data-unread="<?= format_number(FreshRSS_Context::$total_unread) ?>"<?=
|
|
$hideSucGlobal ?>><?= _t('index.menu.main_stream') ?></span>
|
|
</a>
|
|
</li>
|
|
|
|
<li class="tree-folder category important<?= FreshRSS_Context::isCurrentGet('i') ? ' active' : '' ?>">
|
|
<a class="tree-folder-title" data-unread="<?= format_number(FreshRSS_Context::$total_important_unread) ?>"<?=
|
|
$hideSucImportant ?> href="<?= _url('index', $actual_view, 'get', 'i') . $state_filter_manual ?>">
|
|
<?= _i('important') ?><span class="title" data-unread="<?= format_number(FreshRSS_Context::$total_important_unread) ?>"<?=
|
|
$hideSucImportant ?>><?= _t('index.menu.important') ?></span>
|
|
</a>
|
|
</li>
|
|
|
|
<li class="tree-folder category favorites<?= FreshRSS_Context::isCurrentGet('s') ? ' active' : '' ?>">
|
|
<a class="tree-folder-title" data-unread="<?= format_number(FreshRSS_Context::$total_starred['unread']) ?>"<?=
|
|
$hideSucGlobal ?> href="<?= _url('index', $actual_view, 'get', 's') . $state_filter_manual ?>">
|
|
<?= _i('starred') ?><span class="title" data-unread="<?= format_number(FreshRSS_Context::$total_starred['unread']) ?>"<?=
|
|
$hideSucGlobal ?>><?= _t('index.menu.favorites', format_number(FreshRSS_Context::$total_starred['all'])) ?></span>
|
|
</a>
|
|
</li>
|
|
|
|
<?php
|
|
$t_active = FreshRSS_Context::isCurrentGet('T');
|
|
$t_show = ($t_active && in_array(FreshRSS_Context::userConf()->display_categories, ['active', 'remember'], true)) || FreshRSS_Context::userConf()->display_categories === 'all';
|
|
?>
|
|
<li id="tags" class="tree-folder category tags<?= $t_active ? ' active' : '' ?>" data-unread="<?= format_number($this->nbUnreadTags) ?>"<?= $hideSucGlobal ?>>
|
|
<a href="<?= _url('index', $actual_view, 'get', 'T') . $state_filter_manual ?>" class="tree-folder-title">
|
|
<button class="dropdown-toggle" title="<?= _t('sub.category.expand') ?>"><?= _i($t_show ? 'up' : 'down') ?></button><span class="title" title="<?=
|
|
_t('sub.category.open') ?>" data-unread="<?= format_number($this->nbUnreadTags) ?>"<?= $hideSucGlobal ?>><?= _t('index.menu.mylabels') ?></span>
|
|
</a>
|
|
<ul class="tree-folder-items<?= $t_show ? ' active' : '' ?>">
|
|
<?php
|
|
foreach ($this->tags as $tag):
|
|
?>
|
|
<li id="t_<?= $tag->id() ?>" class="item feed<?= FreshRSS_Context::isCurrentGet('t_' . $tag->id()) ? ' active' : ''
|
|
?>" data-unread="<?= $tag->nbUnread() ?>"<?= $hideSucGlobal ?>>
|
|
<?php if (FreshRSS_Auth::hasAccess()) { ?>
|
|
<div class="dropdown no-mobile">
|
|
<div id="dropdown-t-<?= $tag->id() ?>" class="dropdown-target"></div>
|
|
<a class="dropdown-toggle" href="#dropdown-t-<?= $tag->id() ?>"><?= _i('configure') ?></a>
|
|
<?php /* tag_config_template */ ?>
|
|
</div>
|
|
<?php } else { ?>
|
|
<div class="no-dropdown-toggle"></div>
|
|
<?php } ?>
|
|
<a class="item-title" data-unread="<?= format_number($tag->nbUnread()) ?>"<?= $hideSucGlobal ?> href="<?=
|
|
_url('index', $actual_view, 'get', 't_' . $tag->id()) . $state_filter_manual ?>"><?= _i('label') ?> <?= $tag->name() ?></a>
|
|
</li>
|
|
<?php endforeach; ?>
|
|
</ul>
|
|
</li>
|
|
|
|
<?php
|
|
$nbFeedsTotal = 0;
|
|
foreach ($this->categories as $cat) {
|
|
$nbFeedsTotal += $cat->nbFeeds();
|
|
}
|
|
|
|
foreach ($this->categories as $cat):
|
|
$feeds = $cat->feeds();
|
|
$position = $cat->attributeInt('position');
|
|
if (!empty($feeds)) {
|
|
$c_active = FreshRSS_Context::isCurrentGet('c_' . $cat->id());
|
|
$c_show = ($c_active && in_array(FreshRSS_Context::userConf()->display_categories, ['active', 'remember'], true))
|
|
|| FreshRSS_Context::userConf()->display_categories === 'all';
|
|
$hideSucCat = $cat->showUnreadCount() ? '' : ' data-unread-hide="1"';
|
|
?>
|
|
<li id="c_<?= $cat->id() ?>" class="tree-folder category<?= $c_active ? ' active' : '' ?>"<?=
|
|
null === $position ? '' : " data-position='$position'" ?> data-unread="<?= $cat->nbNotRead() ?>"<?= $hideSucCat ?>>
|
|
<a href="<?= _url('index', $actual_view, 'get', 'c_' . $cat->id()) . $state_filter_manual ?>" class="tree-folder-title">
|
|
<button class="dropdown-toggle" title="<?= _t('sub.category.expand') ?>"><?= _i($c_show ? 'up' : 'down') ?></button><?php
|
|
?><span title="<?= $cat->inError() ? _t('sub.category.error') : _t('sub.category.open') ?>" class="title<?=
|
|
$cat->hasFeedsWithError() || $cat->inError() ? ' error' : ''
|
|
?>" data-unread="<?= format_number($cat->nbNotRead()) ?>"<?= $hideSucCat ?>><?=
|
|
$cat->name() ?><?= $cat->kind() === FreshRSS_Category::KIND_DYNAMIC_OPML ? ' ' . _i('opml-dyn') : ''
|
|
?></span>
|
|
</a>
|
|
|
|
<ul class="tree-folder-items<?= $c_show ? ' active' : '' ?>">
|
|
<?php
|
|
// NB: Reduce whitespace in that loop
|
|
$show_favicon = FreshRSS_Context::userConf()->show_favicons && $nbFeedsTotal < FreshRSS_Context::userConf()->simplify_over_n_feeds;
|
|
|
|
foreach ($feeds as $feed):
|
|
$f_active = FreshRSS_Context::isCurrentGet('f_' . $feed->id());
|
|
if (!$f_active && $feed->priority() < FreshRSS_Feed::PRIORITY_FEED) {
|
|
continue;
|
|
}
|
|
$f_active_class = $f_active ? ' active' : '';
|
|
|
|
$error_class = '';
|
|
$error_title = '';
|
|
if ($feed->inError()) {
|
|
$error_class = ' error';
|
|
$error_title = _t('sub.feed.error');
|
|
}
|
|
|
|
$empty_class = '';
|
|
$empty_title = '';
|
|
if ($feed->nbEntries() <= 0) {
|
|
$empty_class = ' empty';
|
|
$empty_title = _t('sub.feed.empty');
|
|
}
|
|
$mute_class = $feed->mute() ? ' mute' : '';
|
|
$hideSucFeed = $feed->showUnreadCount() ? '' : ' data-unread-hide="1"';
|
|
?>
|
|
<li id="f_<?= $feed->id() ?>" class="item feed<?= $f_active_class, $mute_class, $error_class, $empty_class ?>"
|
|
data-unread="<?= $feed->nbNotRead() ?>"<?= $hideSucFeed ?> data-priority="<?= $feed->priority() ?>"><?php
|
|
if ($f_active || $nbFeedsTotal < FreshRSS_Context::userConf()->simplify_over_n_feeds):
|
|
?><div class="dropdown no-mobile">
|
|
<div id="dropdown-<?= $feed->id() ?>" class="dropdown-target"></div><a href="#dropdown-<?= $feed->id() ?>" class="dropdown-toggle" title="<?=
|
|
_t('gen.action.menu.open') ?>" data-fweb="<?= $feed->website() ?>"><?= _i('configure') ?></a><?php /* feed_config_template */ ?>
|
|
</div><?php
|
|
endif;
|
|
$title = _t('sub.feed.open_feed', $feed->name());
|
|
$title .= $error_title !== '' ? ' ⚠ ' . $error_title : '';
|
|
$title .= $empty_title !== '' ? ' ' . $empty_title : '';
|
|
$title .= $feed->mute() ? ' 🔇 ' . _t('sub.feed.mute.state_is_muted') : '';
|
|
?><a class="item-title" title="<?= $title ?>" data-unread="<?=
|
|
format_number($feed->nbNotRead()) ?>"<?= $hideSucFeed ?> href="<?=
|
|
_url('index', $actual_view, 'get', 'f_' . $feed->id()) . $state_filter_manual ?>">
|
|
<?php
|
|
if ($show_favicon || $f_active) { ?><img class="favicon" src="<?= $feed->favicon() ?>" alt="✇" loading="lazy" /><?php }
|
|
?><span class="title"><?= $feed->name() ?></span></a></li>
|
|
<?php
|
|
endforeach;
|
|
?>
|
|
</ul>
|
|
</li>
|
|
<?php
|
|
}
|
|
endforeach;
|
|
?>
|
|
<li class="tree-bottom"></li>
|
|
</ul>
|
|
</form>
|
|
</nav>
|
|
<a class="close-aside" href="#close">❌</a>
|
|
|
|
<div id="first_load" class="loading"></div>
|
|
<?php flush(); ?>
|
|
|
|
<template id="dynamic_favicon_base">
|
|
<?= file_get_contents(PUBLIC_PATH . '/themes/icons/favicon.svg') ?>
|
|
</template>
|
|
|
|
<template id="tag_config_template">
|
|
<ul class="dropdown-menu">
|
|
<li class="item">
|
|
<a class="configure open-slider" href="<?= _url('tag', 'update', 'id', '------', 'from', Minz_Request::actionName()) ?>"><?= _t('gen.action.manage') ?></a>
|
|
</li>
|
|
</ul>
|
|
<a class="dropdown-close" href="#close">❌</a>
|
|
</template>
|
|
|
|
<template id="feed_config_template">
|
|
<ul class="dropdown-menu">
|
|
<li class="item"><a href="<?= _url('index', $actual_view, 'get', 'f_------') ?>"><?= _t('gen.action.filter') ?></a></li>
|
|
<?php if (FreshRSS_Auth::hasAccess()) { ?>
|
|
<li class="item"><a href="<?= _url('stats', 'repartition', 'id', '------') ?>"><?= _t('index.menu.stats') ?></a></li>
|
|
<?php } ?>
|
|
<li class="item link website"><a target="_blank" rel="noreferrer" href="http://example.net/"><?= _t('gen.action.see_website') ?></a></li>
|
|
<?php if (FreshRSS_Auth::hasAccess()) {
|
|
$get = Minz_Request::paramString('get');
|
|
if ($get === '') {
|
|
$url = _url('subscription', 'feed', 'id', '------', 'from', $actual_view);
|
|
} else {
|
|
$url = _url('subscription', 'feed', 'id', '------', 'get', $get, 'from', $actual_view);
|
|
}
|
|
?>
|
|
<li class="item"><a class="configure open-slider" href="<?= $url ?>"><?= _t('gen.action.manage') ?></a></li>
|
|
<li class="item"><a href="<?= _url('feed', 'actualize', 'id', '------') ?>"><?= _t('gen.action.actualize') ?></a></li>
|
|
<li class="item">
|
|
<?php $confirm = FreshRSS_Context::userConf()->reading_confirm ? 'confirm" disabled="disabled' : ''; ?>
|
|
<button class="read_all as-link <?= $confirm ?>"
|
|
form="mark-read-aside"
|
|
formaction="<?= _url('entry', 'read', 'get', 'f_------') ?>"
|
|
type="submit"><?= _t('index.menu.mark_feed_read') ?></button>
|
|
</li>
|
|
<?php } ?>
|
|
</ul>
|
|
<a class="dropdown-close" href="#close">❌</a>
|
|
</template>
|