Skip to content

Commit

Permalink
WIP: tag entries page
Browse files Browse the repository at this point in the history
  • Loading branch information
Romain de Laage authored and rdelaage committed Mar 22, 2024
1 parent 7ee4a73 commit de6e842
Show file tree
Hide file tree
Showing 8 changed files with 285 additions and 2 deletions.
9 changes: 9 additions & 0 deletions internal/storage/entry_pagination_builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
2 changes: 1 addition & 1 deletion internal/storage/entry_query_builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
Expand Down
2 changes: 1 addition & 1 deletion internal/template/templates/views/entry.html
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ <h1 id="page-header-title" dir="auto">
{{ 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 "tagEntries" "tagName" $e }}"><strong>{{ $e }}</strong></a>{{end}}
</div>
{{ end }}
<div class="entry-date">
Expand Down
65 changes: 65 additions & 0 deletions internal/template/templates/views/tag_entries.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
{{ 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>
<nav aria-label="{{ .tagName }} {{ t "menu.title" }}">
<ul>
{{ if .showOnlyUnreadEntries }}
<li>
<a class="page-link" href="{{ route "tagEntriesAll" "tagName" .tagName }}">{{ icon "show-all-entries" }}{{ t "menu.show_all_entries" }}</a>
</li>
{{ else }}
<li>
<a class="page-link" href="{{ route "tagEntries" "tagName" .tagName }}">{{ icon "show-unread-entries" }}{{ t "menu.show_only_unread_entries" }}</a>
</li>
{{ end }}
</ul>
</nav>
</section>
{{ end }}

{{ define "content"}}
{{ if not .entries }}
<p role="alert" class="alert alert-info">{{ t "alert.no_bookmark" }}</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" $.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 }}
86 changes: 86 additions & 0 deletions internal/ui/entry_tag.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// 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"

"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 := request.RouteStringParam(r, "tagName")
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", tagName, "entryID", nextEntry.ID)
}

prevEntryRoute := ""
if prevEntry != nil {
prevEntryRoute = route.Path(h.router, "tagEntry", "tagName", 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("menu", "categories")
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"))
}
59 changes: 59 additions & 0 deletions internal/ui/tag_entries.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// 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"

"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) showTagEntriesPage(w http.ResponseWriter, r *http.Request) {
user, err := h.store.UserByID(request.UserID(r))
if err != nil {
html.ServerError(w, r, err)
return
}

tagName := request.RouteStringParam(r, "tagName")

offset := request.QueryIntParam(r, "offset", 0)
builder := h.store.NewEntryQueryBuilder(user.ID)
builder.WithStatus(model.EntryStatusUnread)
builder.WithTags([]string{tagName})
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, "tagEntries", "tagName", 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", true)

html.OK(w, r, view.Render("tag_entries"))
}
59 changes: 59 additions & 0 deletions internal/ui/tag_entries_all.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// 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"

"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 := request.RouteStringParam(r, "tagName")

offset := request.QueryIntParam(r, "offset", 0)
builder := h.store.NewEntryQueryBuilder(user.ID)
builder.WithoutStatus(model.EntryStatusRemoved)
builder.WithTags([]string{tagName})
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, "tagEntries", "tagName", 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"))
}
5 changes: 5 additions & 0 deletions internal/ui/ui.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,11 @@ 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", handler.showTagEntriesPage).Name("tagEntries").Methods(http.MethodGet)
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)
Expand Down

0 comments on commit de6e842

Please sign in to comment.