summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGravatar Romain de Laage <romain.delaage@rdelaage.ovh> 2024-04-05 12:09:29 +0200
committerGravatar Frédéric Guillot <f@miniflux.net> 2024-04-14 20:08:38 -0700
commit647c66e70af50cf7ca786eba51d4fdb67b89fecc (patch)
tree0b83098620a1be897f8f2081da82c575ee71e1d8
parentb205b5aad075dc89040231f87c79bec2a7ea60c7 (diff)
downloadv2-647c66e70af50cf7ca786eba51d4fdb67b89fecc.tar.gz
v2-647c66e70af50cf7ca786eba51d4fdb67b89fecc.tar.zst
v2-647c66e70af50cf7ca786eba51d4fdb67b89fecc.zip
ui: add tag entries page
Diffstat (limited to '')
-rw-r--r--internal/locale/translations/de_DE.json1
-rw-r--r--internal/locale/translations/el_EL.json1
-rw-r--r--internal/locale/translations/en_US.json1
-rw-r--r--internal/locale/translations/es_ES.json1
-rw-r--r--internal/locale/translations/fi_FI.json1
-rw-r--r--internal/locale/translations/fr_FR.json1
-rw-r--r--internal/locale/translations/hi_IN.json1
-rw-r--r--internal/locale/translations/id_ID.json1
-rw-r--r--internal/locale/translations/it_IT.json1
-rw-r--r--internal/locale/translations/ja_JP.json1
-rw-r--r--internal/locale/translations/nl_NL.json1
-rw-r--r--internal/locale/translations/pl_PL.json1
-rw-r--r--internal/locale/translations/pt_BR.json1
-rw-r--r--internal/locale/translations/ru_RU.json1
-rw-r--r--internal/locale/translations/tr_TR.json1
-rw-r--r--internal/locale/translations/uk_UA.json1
-rw-r--r--internal/locale/translations/zh_CN.json1
-rw-r--r--internal/locale/translations/zh_TW.json1
-rw-r--r--internal/storage/entry_pagination_builder.go9
-rw-r--r--internal/storage/entry_query_builder.go2
-rw-r--r--internal/template/functions.go6
-rw-r--r--internal/template/templates/views/entry.html2
-rw-r--r--internal/template/templates/views/tag_entries.html52
-rw-r--r--internal/ui/entry_tag.go90
-rw-r--r--internal/ui/tag_entries_all.go65
-rw-r--r--internal/ui/ui.go4
26 files changed, 244 insertions, 4 deletions
diff --git a/internal/locale/translations/de_DE.json b/internal/locale/translations/de_DE.json
index cc60035a..c49a27f6 100644
--- a/internal/locale/translations/de_DE.json
+++ b/internal/locale/translations/de_DE.json
@@ -258,6 +258,7 @@
"alert.no_bookmark": "Es existiert derzeit kein Lesezeichen.",
"alert.no_category": "Es ist keine Kategorie vorhanden.",
"alert.no_category_entry": "Es befindet sich kein Artikel in dieser Kategorie.",
+ "alert.no_tag_entry": "Es gibt keine Artikel, die diesem Tag entsprechen.",
"alert.no_feed_entry": "Es existiert kein Artikel für dieses Abonnement.",
"alert.no_feed": "Es sind keine Abonnements vorhanden.",
"alert.no_feed_in_category": "Für diese Kategorie gibt es kein Abonnement.",
diff --git a/internal/locale/translations/el_EL.json b/internal/locale/translations/el_EL.json
index 82282163..94d74149 100644
--- a/internal/locale/translations/el_EL.json
+++ b/internal/locale/translations/el_EL.json
@@ -258,6 +258,7 @@
"alert.no_bookmark": "Δεν υπάρχει σελιδοδείκτης αυτή τη στιγμή.",
"alert.no_category": "Δεν υπάρχει κατηγορία.",
"alert.no_category_entry": "Δεν υπάρχουν άρθρα σε αυτήν την κατηγορία.",
+ "alert.no_tag_entry": "Δεν υπάρχουν αντικείμενα που να ταιριάζουν με αυτή την ετικέτα.",
"alert.no_feed_entry": "Δεν υπάρχουν άρθρα για αυτήν τη ροή.",
"alert.no_feed": "Δεν έχετε συνδρομές.",
"alert.no_feed_in_category": "Δεν υπάρχει συνδρομή για αυτήν την κατηγορία.",
diff --git a/internal/locale/translations/en_US.json b/internal/locale/translations/en_US.json
index 97e58fe5..77b73778 100644
--- a/internal/locale/translations/en_US.json
+++ b/internal/locale/translations/en_US.json
@@ -258,6 +258,7 @@
"alert.no_bookmark": "There are no starred entries.",
"alert.no_category": "There is no category.",
"alert.no_category_entry": "There are no entries in this category.",
+ "alert.no_tag_entry": "There are no entries matching this tag.",
"alert.no_feed_entry": "There are no entries for this feed.",
"alert.no_feed": "You don’t have any feeds.",
"alert.no_feed_in_category": "There is no feed for this category.",
diff --git a/internal/locale/translations/es_ES.json b/internal/locale/translations/es_ES.json
index 914d8994..d4669b39 100644
--- a/internal/locale/translations/es_ES.json
+++ b/internal/locale/translations/es_ES.json
@@ -258,6 +258,7 @@
"alert.no_bookmark": "No hay marcador en este momento.",
"alert.no_category": "No hay categoría.",
"alert.no_category_entry": "No hay artículos en esta categoría.",
+ "alert.no_tag_entry": "No hay artículos con esta etiqueta.",
"alert.no_feed_entry": "No hay artículos para esta fuente.",
"alert.no_feed": "No tienes fuentes.",
"alert.no_feed_in_category": "No hay fuentes para esta categoría.",
diff --git a/internal/locale/translations/fi_FI.json b/internal/locale/translations/fi_FI.json
index 41bd2e50..6b2f2fca 100644
--- a/internal/locale/translations/fi_FI.json
+++ b/internal/locale/translations/fi_FI.json
@@ -258,6 +258,7 @@
"alert.no_bookmark": "Tällä hetkellä ei ole kirjanmerkkiä.",
"alert.no_category": "Ei ole kategoriaa.",
"alert.no_category_entry": "Tässä kategoriassa ei ole artikkeleita.",
+ "alert.no_tag_entry": "Tätä tunnistetta vastaavia merkintöjä ei ole.",
"alert.no_feed_entry": "Tässä syötteessä ei ole artikkeleita.",
"alert.no_feed": "Sinulla ei ole tilauksia.",
"alert.no_feed_in_category": "Tälle kategorialle ei ole tilausta.",
diff --git a/internal/locale/translations/fr_FR.json b/internal/locale/translations/fr_FR.json
index d616eb3e..fb2982fa 100644
--- a/internal/locale/translations/fr_FR.json
+++ b/internal/locale/translations/fr_FR.json
@@ -258,6 +258,7 @@
"alert.no_bookmark": "Il n'y a aucun favoris pour le moment.",
"alert.no_category": "Il n'y a aucune catégorie.",
"alert.no_category_entry": "Il n'y a aucun article dans cette catégorie.",
+ "alert.no_tag_entry": "Il n'y a aucun article correspondant à ce tag.",
"alert.no_feed_entry": "Il n'y a aucun article pour cet abonnement.",
"alert.no_feed": "Vous n'avez aucun abonnement.",
"alert.no_feed_in_category": "Il n'y a pas d'abonnement pour cette catégorie.",
diff --git a/internal/locale/translations/hi_IN.json b/internal/locale/translations/hi_IN.json
index 199c66af..ef7c0c04 100644
--- a/internal/locale/translations/hi_IN.json
+++ b/internal/locale/translations/hi_IN.json
@@ -258,6 +258,7 @@
"alert.no_bookmark": "इस समय कोई बुकमार्क नहीं है",
"alert.no_category": "कोई श्रेणी नहीं है।",
"alert.no_category_entry": "इस श्रेणी में कोई विषय-वस्तु नहीं है।",
+ "alert.no_tag_entry": "इस टैग से मेल खाती कोई प्रविष्टियाँ नहीं हैं।",
"alert.no_feed_entry": "इस फ़ीड के लिए कोई विषय-वस्तु नहीं है।",
"alert.no_feed": "आपके पास कोई सदस्यता नहीं है।",
"alert.no_feed_in_category": "इस श्रेणी के लिए कोई सदस्यता नहीं है।",
diff --git a/internal/locale/translations/id_ID.json b/internal/locale/translations/id_ID.json
index ee06ac61..3f1e3cd3 100644
--- a/internal/locale/translations/id_ID.json
+++ b/internal/locale/translations/id_ID.json
@@ -248,6 +248,7 @@
"alert.no_bookmark": "Tidak ada markah.",
"alert.no_category": "Tidak ada kategori.",
"alert.no_category_entry": "Tidak ada artikel di kategori ini.",
+ "alert.no_tag_entry": "Tidak ada entri yang cocok dengan tag ini.",
"alert.no_feed_entry": "Tidak ada artikel di umpan ini.",
"alert.no_feed": "Anda tidak memiliki langganan.",
"alert.no_feed_in_category": "Tidak ada langganan untuk kategori ini.",
diff --git a/internal/locale/translations/it_IT.json b/internal/locale/translations/it_IT.json
index 6e808a02..e9385c7c 100644
--- a/internal/locale/translations/it_IT.json
+++ b/internal/locale/translations/it_IT.json
@@ -258,6 +258,7 @@
"alert.no_bookmark": "Nessun preferito disponibile.",
"alert.no_category": "Nessuna categoria disponibile.",
"alert.no_category_entry": "Questa categoria non contiene alcun articolo.",
+ "alert.no_tag_entry": "Non ci sono voci corrispondenti a questo tag.",
"alert.no_feed_entry": "Questo feed non contiene alcun articolo.",
"alert.no_feed": "Nessun feed disponibile.",
"alert.no_feed_in_category": "Non esiste un abbonamento per questa categoria.",
diff --git a/internal/locale/translations/ja_JP.json b/internal/locale/translations/ja_JP.json
index 8c767b55..efd2d37d 100644
--- a/internal/locale/translations/ja_JP.json
+++ b/internal/locale/translations/ja_JP.json
@@ -248,6 +248,7 @@
"alert.no_bookmark": "現在星付きはありません。",
"alert.no_category": "カテゴリが存在しません。",
"alert.no_category_entry": "このカテゴリには記事がありません。",
+ "alert.no_tag_entry": "このタグに一致するエントリーはありません。",
"alert.no_feed_entry": "このフィードには記事がありません。",
"alert.no_feed": "何も購読していません。",
"alert.no_feed_in_category": "このカテゴリには購読中のフィードがありません。",
diff --git a/internal/locale/translations/nl_NL.json b/internal/locale/translations/nl_NL.json
index d47510e9..d9141819 100644
--- a/internal/locale/translations/nl_NL.json
+++ b/internal/locale/translations/nl_NL.json
@@ -258,6 +258,7 @@
"alert.no_bookmark": "Er zijn op dit moment geen favorieten.",
"alert.no_category": "Er zijn geen categorieën.",
"alert.no_category_entry": "Deze categorie bevat geen feeds.",
+ "alert.no_tag_entry": "Er zijn geen items die overeenkomen met deze tag.",
"alert.no_feed_entry": "Er zijn geen artikelen in deze feed.",
"alert.no_feed": "Je hebt nog geen feeds geabboneerd staan.",
"alert.no_feed_in_category": "Er is geen abonnement voor deze categorie.",
diff --git a/internal/locale/translations/pl_PL.json b/internal/locale/translations/pl_PL.json
index 32f3e4f8..3b39301d 100644
--- a/internal/locale/translations/pl_PL.json
+++ b/internal/locale/translations/pl_PL.json
@@ -268,6 +268,7 @@
"alert.no_bookmark": "Obecnie nie ma żadnych zakładek.",
"alert.no_category": "Nie ma żadnej kategorii!",
"alert.no_category_entry": "W tej kategorii nie ma żadnych artykułów",
+ "alert.no_tag_entry": "Nie ma wpisów pasujących do tego tagu.",
"alert.no_feed_entry": "Nie ma artykułu dla tego kanału.",
"alert.no_feed": "Nie masz żadnej subskrypcji.",
"alert.no_feed_in_category": "Nie ma subskrypcji dla tej kategorii.",
diff --git a/internal/locale/translations/pt_BR.json b/internal/locale/translations/pt_BR.json
index 56861a3a..7c639f5b 100644
--- a/internal/locale/translations/pt_BR.json
+++ b/internal/locale/translations/pt_BR.json
@@ -258,6 +258,7 @@
"alert.no_bookmark": "Não há favorito neste momento.",
"alert.no_category": "Não há categoria.",
"alert.no_category_entry": "Não há itens nesta categoria.",
+ "alert.no_tag_entry": "Não há itens que correspondam a esta etiqueta.",
"alert.no_feed_entry": "Não há itens nessa fonte.",
"alert.no_feed": "Não há inscrições.",
"alert.no_feed_in_category": "Não há inscrições nessa categoria.",
diff --git a/internal/locale/translations/ru_RU.json b/internal/locale/translations/ru_RU.json
index 69c139f0..6327c09f 100644
--- a/internal/locale/translations/ru_RU.json
+++ b/internal/locale/translations/ru_RU.json
@@ -268,6 +268,7 @@
"alert.no_bookmark": "Избранное отсутствует.",
"alert.no_category": "Категории отсутствуют.",
"alert.no_category_entry": "В этой категории нет статей.",
+ "alert.no_tag_entry": "Нет записей, соответствующих этому тегу.",
"alert.no_feed_entry": "В этой подписке отсутствуют статьи.",
"alert.no_feed": "У вас нет ни одной подписки.",
"alert.no_feed_in_category": "Для этой категории нет подписки.",
diff --git a/internal/locale/translations/tr_TR.json b/internal/locale/translations/tr_TR.json
index 15fe4c8c..4fc999a2 100644
--- a/internal/locale/translations/tr_TR.json
+++ b/internal/locale/translations/tr_TR.json
@@ -18,6 +18,7 @@
"alert.no_bookmark": "Yıldızlanmış makale yok.",
"alert.no_category": "Hiç kategori yok.",
"alert.no_category_entry": "Bu kategoride hiç makele yok.",
+ "alert.no_tag_entry": "Bu etiketle eşleşen hiçbir giriş yok.",
"alert.no_feed": "Hiç beslemeniz yok.",
"alert.no_feed_entry": "Bu besleme için makele yok.",
"alert.no_feed_in_category": "Bu kategori için besleme yok.",
diff --git a/internal/locale/translations/uk_UA.json b/internal/locale/translations/uk_UA.json
index eeb305d7..832a1471 100644
--- a/internal/locale/translations/uk_UA.json
+++ b/internal/locale/translations/uk_UA.json
@@ -268,6 +268,7 @@
"alert.no_bookmark": "Наразі закладки відсутні.",
"alert.no_category": "Немає категорії.",
"alert.no_category_entry": "У цій категорії немає записів.",
+ "alert.no_tag_entry": "Немає записів, що відповідають цьому тегу.",
"alert.no_feed_entry": "У цій стрічці немає записів.",
"alert.no_feed": "У вас немає підписок.",
"alert.no_feed_in_category": "У цій категорії немає підписок.",
diff --git a/internal/locale/translations/zh_CN.json b/internal/locale/translations/zh_CN.json
index dc1079c4..b32a270a 100644
--- a/internal/locale/translations/zh_CN.json
+++ b/internal/locale/translations/zh_CN.json
@@ -248,6 +248,7 @@
"alert.no_bookmark": "目前没有收藏",
"alert.no_category": "目前没有分类",
"alert.no_category_entry": "该分类下没有文章",
+ "alert.no_tag_entry": "没有与此标签匹配的条目。",
"alert.no_feed_entry": "该源中没有文章",
"alert.no_feed": "目前没有源",
"alert.no_history": "目前没有历史",
diff --git a/internal/locale/translations/zh_TW.json b/internal/locale/translations/zh_TW.json
index 4b4316f3..39504b73 100644
--- a/internal/locale/translations/zh_TW.json
+++ b/internal/locale/translations/zh_TW.json
@@ -248,6 +248,7 @@
"alert.no_bookmark": "目前沒有收藏",
"alert.no_category": "目前沒有分類",
"alert.no_category_entry": "該分類下沒有文章",
+ "alert.no_tag_entry": "沒有與此標籤相符的條目。",
"alert.no_feed_entry": "該Feed中沒有文章",
"alert.no_feed": "目前沒有Feed",
"alert.no_history": "目前沒有歷史",
diff --git a/internal/storage/entry_pagination_builder.go b/internal/storage/entry_pagination_builder.go
index bab478d3..9779f245 100644
--- a/internal/storage/entry_pagination_builder.go
+++ b/internal/storage/entry_pagination_builder.go
@@ -58,6 +58,15 @@ func (e *EntryPaginationBuilder) WithStatus(status string) {
}
}
+func (e *EntryPaginationBuilder) WithTags(tags []string) {
+ if len(tags) > 0 {
+ for _, tag := range tags {
+ e.conditions = append(e.conditions, fmt.Sprintf("LOWER($%d) = ANY(LOWER(e.tags::text)::text[])", len(e.args)+1))
+ e.args = append(e.args, tag)
+ }
+ }
+}
+
// WithGloballyVisible adds global visibility to the condition.
func (e *EntryPaginationBuilder) WithGloballyVisible() {
e.conditions = append(e.conditions, "not c.hide_globally")
diff --git a/internal/storage/entry_query_builder.go b/internal/storage/entry_query_builder.go
index 680fbedb..9ab26738 100644
--- a/internal/storage/entry_query_builder.go
+++ b/internal/storage/entry_query_builder.go
@@ -160,7 +160,7 @@ func (e *EntryQueryBuilder) WithStatuses(statuses []string) *EntryQueryBuilder {
func (e *EntryQueryBuilder) WithTags(tags []string) *EntryQueryBuilder {
if len(tags) > 0 {
for _, cat := range tags {
- e.conditions = append(e.conditions, fmt.Sprintf("$%d = ANY(e.tags)", len(e.args)+1))
+ e.conditions = append(e.conditions, fmt.Sprintf("LOWER($%d) = ANY(LOWER(e.tags::text)::text[])", len(e.args)+1))
e.args = append(e.args, cat)
}
}
diff --git a/internal/template/functions.go b/internal/template/functions.go
index 54e787cf..cfbfc53d 100644
--- a/internal/template/functions.go
+++ b/internal/template/functions.go
@@ -8,6 +8,7 @@ import (
"html/template"
"math"
"net/mail"
+ "net/url"
"slices"
"strings"
"time"
@@ -91,8 +92,9 @@ func (f *funcMap) Map() template.FuncMap {
"nonce": func() string {
return crypto.GenerateRandomStringHex(16)
},
- "deRef": func(i *int) int { return *i },
- "duration": duration,
+ "deRef": func(i *int) int { return *i },
+ "duration": duration,
+ "urlEncode": url.PathEscape,
// These functions are overrode at runtime after the parsing.
"elapsed": func(timezone string, t time.Time) string {
diff --git a/internal/template/templates/views/entry.html b/internal/template/templates/views/entry.html
index 4284f097..48f3c5fe 100644
--- a/internal/template/templates/views/entry.html
+++ b/internal/template/templates/views/entry.html
@@ -135,7 +135,7 @@
{{ if .entry.Tags }}
<div class="entry-tags">
{{ t "entry.tags.label" }}
- {{range $i, $e := .entry.Tags}}{{if $i}}, {{end}}<strong>{{ $e }}</strong>{{end}}
+ {{range $i, $e := .entry.Tags}}{{if $i}}, {{end}}<a href="{{ route "tagEntriesAll" "tagName" (urlEncode $e) }}"><strong>{{ $e }}</strong></a>{{end}}
</div>
{{ end }}
<div class="entry-date">
diff --git a/internal/template/templates/views/tag_entries.html b/internal/template/templates/views/tag_entries.html
new file mode 100644
index 00000000..86d1c203
--- /dev/null
+++ b/internal/template/templates/views/tag_entries.html
@@ -0,0 +1,52 @@
+{{ define "title"}}{{ .tagName }} ({{ .total }}){{ end }}
+
+{{ define "page_header"}}
+<section class="page-header" aria-labelledby="page-header-title page-header-title-count">
+ <h1 id="page-header-title" dir="auto">
+ {{ .tagName }}
+ <span aria-hidden="true"> ({{ .total }})</span>
+ </h1>
+ <span id="page-header-title-count" class="sr-only">{{ plural "page.tag_entry_count" .total .total }}</span>
+</section>
+{{ end }}
+
+{{ define "content"}}
+{{ if not .entries }}
+ <p role="alert" class="alert alert-info">{{ t "alert.no_tag_entry" }}</p>
+{{ else }}
+ <div class="pagination-top">
+ {{ template "pagination" .pagination }}
+ </div>
+ <div class="items">
+ {{ range .entries }}
+ <article
+ class="item entry-item {{ if $.user.EntrySwipe }}entry-swipe{{ end }} item-status-{{ .Status }}"
+ data-id="{{ .ID }}"
+ aria-labelledby="entry-title-{{ .ID }}"
+ tabindex="-1"
+ >
+ <header class="item-header" dir="auto">
+ <h2 id="entry-title-{{ .ID }}" class="item-title">
+ <a href="{{ route "tagEntry" "entryID" .ID "tagName" (urlEncode $.tagName) }}">
+ {{ if ne .Feed.Icon.IconID 0 }}
+ <img src="{{ route "icon" "iconID" .Feed.Icon.IconID }}" width="16" height="16" loading="lazy" alt="">
+ {{ end }}
+ {{ .Title }}
+ </a>
+ </h2>
+ <span class="category">
+ <a href="{{ route "categoryEntries" "categoryID" .Feed.Category.ID }}">
+ {{ .Feed.Category.Title }}
+ </a>
+ </span>
+ </header>
+ {{ template "item_meta" dict "user" $.user "entry" . "hasSaveEntry" $.hasSaveEntry }}
+ </article>
+ {{ end }}
+ </div>
+ <div class="pagination-bottom">
+ {{ template "pagination" .pagination }}
+ </div>
+{{ end }}
+
+{{ end }}
diff --git a/internal/ui/entry_tag.go b/internal/ui/entry_tag.go
new file mode 100644
index 00000000..cf153a8b
--- /dev/null
+++ b/internal/ui/entry_tag.go
@@ -0,0 +1,90 @@
+// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package ui // import "miniflux.app/v2/internal/ui"
+
+import (
+ "net/http"
+ "net/url"
+
+ "miniflux.app/v2/internal/http/request"
+ "miniflux.app/v2/internal/http/response/html"
+ "miniflux.app/v2/internal/http/route"
+ "miniflux.app/v2/internal/model"
+ "miniflux.app/v2/internal/storage"
+ "miniflux.app/v2/internal/ui/session"
+ "miniflux.app/v2/internal/ui/view"
+)
+
+func (h *handler) showTagEntryPage(w http.ResponseWriter, r *http.Request) {
+ user, err := h.store.UserByID(request.UserID(r))
+ if err != nil {
+ html.ServerError(w, r, err)
+ return
+ }
+
+ tagName, err := url.PathUnescape(request.RouteStringParam(r, "tagName"))
+ if err != nil {
+ html.ServerError(w, r, err)
+ return
+ }
+ entryID := request.RouteInt64Param(r, "entryID")
+
+ builder := h.store.NewEntryQueryBuilder(user.ID)
+ builder.WithTags([]string{tagName})
+ builder.WithEntryID(entryID)
+ builder.WithoutStatus(model.EntryStatusRemoved)
+
+ entry, err := builder.GetEntry()
+ if err != nil {
+ html.ServerError(w, r, err)
+ return
+ }
+
+ if entry == nil {
+ html.NotFound(w, r)
+ return
+ }
+
+ if user.MarkReadOnView && entry.Status == model.EntryStatusUnread {
+ err = h.store.SetEntriesStatus(user.ID, []int64{entry.ID}, model.EntryStatusRead)
+ if err != nil {
+ html.ServerError(w, r, err)
+ return
+ }
+
+ entry.Status = model.EntryStatusRead
+ }
+
+ entryPaginationBuilder := storage.NewEntryPaginationBuilder(h.store, user.ID, entry.ID, user.EntryOrder, user.EntryDirection)
+ entryPaginationBuilder.WithTags([]string{tagName})
+ prevEntry, nextEntry, err := entryPaginationBuilder.Entries()
+ if err != nil {
+ html.ServerError(w, r, err)
+ return
+ }
+
+ nextEntryRoute := ""
+ if nextEntry != nil {
+ nextEntryRoute = route.Path(h.router, "tagEntry", "tagName", url.PathEscape(tagName), "entryID", nextEntry.ID)
+ }
+
+ prevEntryRoute := ""
+ if prevEntry != nil {
+ prevEntryRoute = route.Path(h.router, "tagEntry", "tagName", url.PathEscape(tagName), "entryID", prevEntry.ID)
+ }
+
+ sess := session.New(h.store, request.SessionID(r))
+ view := view.New(h.tpl, r, sess)
+ view.Set("entry", entry)
+ view.Set("prevEntry", prevEntry)
+ view.Set("nextEntry", nextEntry)
+ view.Set("nextEntryRoute", nextEntryRoute)
+ view.Set("prevEntryRoute", prevEntryRoute)
+ view.Set("user", user)
+ view.Set("countUnread", h.store.CountUnreadEntries(user.ID))
+ view.Set("countErrorFeeds", h.store.CountUserFeedsWithErrors(user.ID))
+ view.Set("hasSaveEntry", h.store.HasSaveEntry(user.ID))
+
+ html.OK(w, r, view.Render("entry"))
+}
diff --git a/internal/ui/tag_entries_all.go b/internal/ui/tag_entries_all.go
new file mode 100644
index 00000000..a7f6fb02
--- /dev/null
+++ b/internal/ui/tag_entries_all.go
@@ -0,0 +1,65 @@
+// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package ui // import "miniflux.app/v2/internal/ui"
+
+import (
+ "net/http"
+ "net/url"
+
+ "miniflux.app/v2/internal/http/request"
+ "miniflux.app/v2/internal/http/response/html"
+ "miniflux.app/v2/internal/http/route"
+ "miniflux.app/v2/internal/model"
+ "miniflux.app/v2/internal/ui/session"
+ "miniflux.app/v2/internal/ui/view"
+)
+
+func (h *handler) showTagEntriesAllPage(w http.ResponseWriter, r *http.Request) {
+ user, err := h.store.UserByID(request.UserID(r))
+ if err != nil {
+ html.ServerError(w, r, err)
+ return
+ }
+
+ tagName, err := url.PathUnescape(request.RouteStringParam(r, "tagName"))
+ if err != nil {
+ html.ServerError(w, r, err)
+ return
+ }
+
+ offset := request.QueryIntParam(r, "offset", 0)
+ builder := h.store.NewEntryQueryBuilder(user.ID)
+ builder.WithoutStatus(model.EntryStatusRemoved)
+ builder.WithTags([]string{tagName})
+ builder.WithSorting("status", "asc")
+ builder.WithSorting(user.EntryOrder, user.EntryDirection)
+ builder.WithOffset(offset)
+ builder.WithLimit(user.EntriesPerPage)
+
+ entries, err := builder.GetEntries()
+ if err != nil {
+ html.ServerError(w, r, err)
+ return
+ }
+
+ count, err := builder.CountEntries()
+ if err != nil {
+ html.ServerError(w, r, err)
+ return
+ }
+
+ sess := session.New(h.store, request.SessionID(r))
+ view := view.New(h.tpl, r, sess)
+ view.Set("tagName", tagName)
+ view.Set("total", count)
+ view.Set("entries", entries)
+ view.Set("pagination", getPagination(route.Path(h.router, "tagEntriesAll", "tagName", url.PathEscape(tagName)), count, offset, user.EntriesPerPage))
+ view.Set("user", user)
+ view.Set("countUnread", h.store.CountUnreadEntries(user.ID))
+ view.Set("countErrorFeeds", h.store.CountUserFeedsWithErrors(user.ID))
+ view.Set("hasSaveEntry", h.store.HasSaveEntry(user.ID))
+ view.Set("showOnlyUnreadEntries", false)
+
+ html.OK(w, r, view.Render("tag_entries"))
+}
diff --git a/internal/ui/ui.go b/internal/ui/ui.go
index 6d8e729c..d6641c01 100644
--- a/internal/ui/ui.go
+++ b/internal/ui/ui.go
@@ -93,6 +93,10 @@ func Serve(router *mux.Router, store *storage.Storage, pool *worker.Pool) {
uiRouter.HandleFunc("/category/{categoryID}/remove", handler.removeCategory).Name("removeCategory").Methods(http.MethodPost)
uiRouter.HandleFunc("/category/{categoryID}/mark-all-as-read", handler.markCategoryAsRead).Name("markCategoryAsRead").Methods(http.MethodPost)
+ // Tag pages.
+ uiRouter.HandleFunc("/tags/{tagName}/entries/all", handler.showTagEntriesAllPage).Name("tagEntriesAll").Methods(http.MethodGet)
+ uiRouter.HandleFunc("/tags/{tagName}/entry/{entryID}", handler.showTagEntryPage).Name("tagEntry").Methods(http.MethodGet)
+
// Entry pages.
uiRouter.HandleFunc("/entry/status", handler.updateEntriesStatus).Name("updateEntriesStatus").Methods(http.MethodPost)
uiRouter.HandleFunc("/entry/save/{entryID}", handler.saveEntry).Name("saveEntry").Methods(http.MethodPost)