Explorar el Código

Add search form

Frédéric Guillot hace 7 años
padre
commit
6d0dc451e4

+ 2 - 0
daemon/routes.go

@@ -95,6 +95,8 @@ func routes(cfg *config.Config, store *storage.Storage, feedHandler *feed.Handle
 	uiRouter.HandleFunc("/unread", uiController.ShowUnreadPage).Name("unread").Methods("GET")
 	uiRouter.HandleFunc("/history", uiController.ShowHistoryPage).Name("history").Methods("GET")
 	uiRouter.HandleFunc("/starred", uiController.ShowStarredPage).Name("starred").Methods("GET")
+	uiRouter.HandleFunc("/search", uiController.ShowSearchEntries).Name("searchEntries").Methods("GET")
+	uiRouter.HandleFunc("/search/entry/{entryID}", uiController.ShowSearchEntry).Name("searchEntry").Methods("GET")
 	uiRouter.HandleFunc("/feed/{feedID}/refresh", uiController.RefreshFeed).Name("refreshFeed").Methods("GET")
 	uiRouter.HandleFunc("/feed/{feedID}/edit", uiController.EditFeed).Name("editFeed").Methods("GET")
 	uiRouter.HandleFunc("/feed/{feedID}/remove", uiController.RemoveFeed).Name("removeFeed").Methods("POST")

+ 8 - 3
locale/translations.go

@@ -1,5 +1,5 @@
 // Code generated by go generate; DO NOT EDIT.
-// 2018-07-04 14:42:27.494264089 -0700 PDT m=+0.053526807
+// 2018-07-04 22:00:22.177727933 -0700 PDT m=+0.039621734
 
 package locale
 
@@ -491,7 +491,12 @@ var translations = map[string]string{
     "Feed Password": "Mot de passe du flux",
     "You are not authorized to access this resource (invalid username/password)": "Vous n'êtes pas autorisé à accéder à cette ressource (nom d'utilisateur / mot de passe incorrect)",
     "Unable to fetch this resource (Status Code = %d)": "Impossible de récupérer cette ressource (code=%d)",
-    "Resource not found (404), this feed doesn't exists anymore, check the feed URL": "Page introuvable (404), cet abonnement n'existe plus, vérifiez l'adresse du flux"
+    "Resource not found (404), this feed doesn't exists anymore, check the feed URL": "Page introuvable (404), cet abonnement n'existe plus, vérifiez l'adresse du flux",
+    "Search Results": "Résultats de la recherche",
+    "There is no result for this search.": "Il n'y a aucun résultat pour cette recherche.",
+    "Search...": "Recherche...",
+    "Set focus on search form": "Mettre le focus sur le champ de recherche",
+    "Search": "Recherche"
 }
 `,
 	"nl_NL": `{
@@ -1166,7 +1171,7 @@ var translations = map[string]string{
 var translationsChecksums = map[string]string{
 	"de_DE": "eddbb2c3224169a6533eed6f917af95b8d9bee58c3b3d61951260face7edd768",
 	"en_US": "6fe95384260941e8a5a3c695a655a932e0a8a6a572c1e45cb2b1ae8baa01b897",
-	"fr_FR": "7a451a1d09e051a847f554937e07a6af24b92f1da7f46da8c0ef2d4cc31bcec6",
+	"fr_FR": "343a148eed375a593023c30597ef7280d18222756c5062e6a85e1006c7b12d14",
 	"nl_NL": "05cca4936bd3b0fa44057c4dab64acdef3aed32fbb682393f254cfe2f686ef1f",
 	"pl_PL": "2295f35a98c8f60cfc6bab241d26b224c06979cc9ca3740bb89c63c7596a0431",
 	"zh_CN": "f5fb0a9b7336c51e74d727a2fb294bab3514e3002376da7fd904e0d7caed1a1c",

+ 6 - 1
locale/translations/fr_FR.json

@@ -235,5 +235,10 @@
     "Feed Password": "Mot de passe du flux",
     "You are not authorized to access this resource (invalid username/password)": "Vous n'êtes pas autorisé à accéder à cette ressource (nom d'utilisateur / mot de passe incorrect)",
     "Unable to fetch this resource (Status Code = %d)": "Impossible de récupérer cette ressource (code=%d)",
-    "Resource not found (404), this feed doesn't exists anymore, check the feed URL": "Page introuvable (404), cet abonnement n'existe plus, vérifiez l'adresse du flux"
+    "Resource not found (404), this feed doesn't exists anymore, check the feed URL": "Page introuvable (404), cet abonnement n'existe plus, vérifiez l'adresse du flux",
+    "Search Results": "Résultats de la recherche",
+    "There is no result for this search.": "Il n'y a aucun résultat pour cette recherche.",
+    "Search...": "Recherche...",
+    "Set focus on search form": "Mettre le focus sur le champ de recherche",
+    "Search": "Recherche"
 }

+ 1 - 1
sql/sql.go

@@ -1,5 +1,5 @@
 // Code generated by go generate; DO NOT EDIT.
-// 2018-07-04 14:42:27.443758746 -0700 PDT m=+0.003021464
+// 2018-07-04 22:00:22.141197828 -0700 PDT m=+0.003091629
 
 package sql
 

+ 8 - 0
storage/entry_pagination_builder.go

@@ -23,6 +23,14 @@ type EntryPaginationBuilder struct {
 	direction  string
 }
 
+// WithSearchQuery adds full-text search query to the condition.
+func (e *EntryPaginationBuilder) WithSearchQuery(query string) {
+	if query != "" {
+		e.conditions = append(e.conditions, fmt.Sprintf("e.document_vectors @@ plainto_tsquery($%d)", len(e.args)+1))
+		e.args = append(e.args, query)
+	}
+}
+
 // WithStarred adds starred to the condition.
 func (e *EntryPaginationBuilder) WithStarred() {
 	e.conditions = append(e.conditions, "e.starred is true")

+ 17 - 8
template/common.go

@@ -1,5 +1,5 @@
 // Code generated by go generate; DO NOT EDIT.
-// 2018-06-21 15:46:05.07724268 +0200 CEST m=+0.035251547
+// 2018-07-04 22:00:22.176755806 -0700 PDT m=+0.038649607
 
 package template
 
@@ -8,7 +8,7 @@ var templateCommonMap = map[string]string{
 <div class="pagination">
     <div class="pagination-prev">
         {{ if .prevEntry }}
-            <a href="{{ .prevEntryRoute }}" title="{{ .prevEntry.Title }}" data-page="previous">{{ t "Previous" }}</a>
+            <a href="{{ .prevEntryRoute }}{{ if .searchQuery }}?q={{ .searchQuery }}{{ end }}" title="{{ .prevEntry.Title }}" data-page="previous">{{ t "Previous" }}</a>
         {{ else }}
             {{ t "Previous" }}
         {{ end }}
@@ -16,7 +16,7 @@ var templateCommonMap = map[string]string{
 
     <div class="pagination-next">
         {{ if .nextEntry }}
-            <a href="{{ .nextEntryRoute }}" title="{{ .nextEntry.Title }}" data-page="next">{{ t "Next" }}</a>
+            <a href="{{ .nextEntryRoute }}{{ if .searchQuery }}?q={{ .searchQuery }}{{ end }}" title="{{ .nextEntry.Title }}" data-page="next">{{ t "Next" }}</a>
         {{ else }}
             {{ t "Next" }}
         {{ end }}
@@ -140,6 +140,14 @@ var templateCommonMap = map[string]string{
                     <a href="{{ route "logout" }}" title="{{ t "Logged as %s" .user.Username }}">{{ t "Logout" }}</a>
                 </li>
             </ul>
+            <div class="search">
+                <div class="search-toggle-switch {{ if $.searchQuery }}has-search-query{{ end }}">
+                    <a href="#" data-action="search">&laquo;&nbsp;{{ t "Search" }}</a>
+                </div>
+                <form action="{{ route "searchEntries" }}" class="search-form {{ if $.searchQuery }}has-search-query{{ end }}">
+                    <input type="search" name="q" id="search-input" placeholder="{{ t "Search..." }}" {{ if $.searchQuery }}value="{{ .searchQuery }}"{{ end }} required>
+                </form>
+            </div>
         </nav>
     </header>
     {{ end }}
@@ -190,6 +198,7 @@ var templateCommonMap = map[string]string{
                     <li>{{ t "Download original content" }} = <strong>d</strong></li>
                     <li>{{ t "Toggle bookmark" }} = <strong>f</strong></li>
                     <li>{{ t "Save article" }} = <strong>s</strong></li>
+                    <li>{{ t "Set focus on search form" }} = <strong>/</strong></li>
                     <li>{{ t "Close modal dialog" }} = <strong>Esc</strong></li>
                 </ul>
             </div>
@@ -203,7 +212,7 @@ var templateCommonMap = map[string]string{
 <div class="pagination">
     <div class="pagination-prev">
         {{ if .ShowPrev }}
-            <a href="{{ .Route }}{{ if gt .PrevOffset 0 }}?offset={{ .PrevOffset }}{{ end }}" data-page="previous">{{ t "Previous" }}</a>
+            <a href="{{ .Route }}{{ if gt .PrevOffset 0 }}?offset={{ .PrevOffset }}{{ if .SearchQuery }}&amp;q={{ .SearchQuery }}{{ end }}{{ else }}{{ if .SearchQuery }}?q={{ .SearchQuery }}{{ end }}{{ end }}" data-page="previous">{{ t "Previous" }}</a>
         {{ else }}
             {{ t "Previous" }}
         {{ end }}
@@ -211,7 +220,7 @@ var templateCommonMap = map[string]string{
 
     <div class="pagination-next">
         {{ if .ShowNext }}
-            <a href="{{ .Route }}?offset={{ .NextOffset }}" data-page="next">{{ t "Next" }}</a>
+            <a href="{{ .Route }}?offset={{ .NextOffset }}{{ if .SearchQuery }}&amp;q={{ .SearchQuery }}{{ end }}" data-page="next">{{ t "Next" }}</a>
         {{ else }}
             {{ t "Next" }}
         {{ end }}
@@ -222,8 +231,8 @@ var templateCommonMap = map[string]string{
 }
 
 var templateCommonMapChecksums = map[string]string{
-	"entry_pagination": "f1465fa70f585ae8043b200ec9de5bf437ffbb0c19fb7aefc015c3555614ee27",
+	"entry_pagination": "756ef122f3ebc73754b5fc4304bf05e59da0ab4af030b2509ff4c9b4a74096ce",
 	"item_meta":        "6cff8ae243f19dac936e523867d2975f70aa749b2a461ae63f6ebbca94cf7419",
-	"layout":           "2cc3abf4d832b8368689d17091856ccae696f8a51b8fc29641107846f5d6661a",
-	"pagination":       "6ff462c2b2a53bc5448b651da017f40a39f1d4f16cef4b2f09784f0797286924",
+	"layout":           "4738561d29c428157e83aa13601c463b5e73bd0e2a5fdee75089f3643d6d4055",
+	"pagination":       "b592d58ea9d6abf2dc0b158621404cbfaeea5413b1c8b8b9818725963096b196",
 }

+ 2 - 2
template/html/common/entry_pagination.html

@@ -2,7 +2,7 @@
 <div class="pagination">
     <div class="pagination-prev">
         {{ if .prevEntry }}
-            <a href="{{ .prevEntryRoute }}" title="{{ .prevEntry.Title }}" data-page="previous">{{ t "Previous" }}</a>
+            <a href="{{ .prevEntryRoute }}{{ if .searchQuery }}?q={{ .searchQuery }}{{ end }}" title="{{ .prevEntry.Title }}" data-page="previous">{{ t "Previous" }}</a>
         {{ else }}
             {{ t "Previous" }}
         {{ end }}
@@ -10,7 +10,7 @@
 
     <div class="pagination-next">
         {{ if .nextEntry }}
-            <a href="{{ .nextEntryRoute }}" title="{{ .nextEntry.Title }}" data-page="next">{{ t "Next" }}</a>
+            <a href="{{ .nextEntryRoute }}{{ if .searchQuery }}?q={{ .searchQuery }}{{ end }}" title="{{ .nextEntry.Title }}" data-page="next">{{ t "Next" }}</a>
         {{ else }}
             {{ t "Next" }}
         {{ end }}

+ 9 - 0
template/html/common/layout.html

@@ -65,6 +65,14 @@
                     <a href="{{ route "logout" }}" title="{{ t "Logged as %s" .user.Username }}">{{ t "Logout" }}</a>
                 </li>
             </ul>
+            <div class="search">
+                <div class="search-toggle-switch {{ if $.searchQuery }}has-search-query{{ end }}">
+                    <a href="#" data-action="search">&laquo;&nbsp;{{ t "Search" }}</a>
+                </div>
+                <form action="{{ route "searchEntries" }}" class="search-form {{ if $.searchQuery }}has-search-query{{ end }}">
+                    <input type="search" name="q" id="search-input" placeholder="{{ t "Search..." }}" {{ if $.searchQuery }}value="{{ .searchQuery }}"{{ end }} required>
+                </form>
+            </div>
         </nav>
     </header>
     {{ end }}
@@ -115,6 +123,7 @@
                     <li>{{ t "Download original content" }} = <strong>d</strong></li>
                     <li>{{ t "Toggle bookmark" }} = <strong>f</strong></li>
                     <li>{{ t "Save article" }} = <strong>s</strong></li>
+                    <li>{{ t "Set focus on search form" }} = <strong>/</strong></li>
                     <li>{{ t "Close modal dialog" }} = <strong>Esc</strong></li>
                 </ul>
             </div>

+ 2 - 2
template/html/common/pagination.html

@@ -2,7 +2,7 @@
 <div class="pagination">
     <div class="pagination-prev">
         {{ if .ShowPrev }}
-            <a href="{{ .Route }}{{ if gt .PrevOffset 0 }}?offset={{ .PrevOffset }}{{ end }}" data-page="previous">{{ t "Previous" }}</a>
+            <a href="{{ .Route }}{{ if gt .PrevOffset 0 }}?offset={{ .PrevOffset }}{{ if .SearchQuery }}&amp;q={{ .SearchQuery }}{{ end }}{{ else }}{{ if .SearchQuery }}?q={{ .SearchQuery }}{{ end }}{{ end }}" data-page="previous">{{ t "Previous" }}</a>
         {{ else }}
             {{ t "Previous" }}
         {{ end }}
@@ -10,7 +10,7 @@
 
     <div class="pagination-next">
         {{ if .ShowNext }}
-            <a href="{{ .Route }}?offset={{ .NextOffset }}" data-page="next">{{ t "Next" }}</a>
+            <a href="{{ .Route }}?offset={{ .NextOffset }}{{ if .SearchQuery }}&amp;q={{ .SearchQuery }}{{ end }}" data-page="next">{{ t "Next" }}</a>
         {{ else }}
             {{ t "Next" }}
         {{ end }}

+ 30 - 0
template/html/search_entries.html

@@ -0,0 +1,30 @@
+{{ define "title"}}{{ t "Search Results" }} ({{ .total }}){{ end }}
+
+{{ define "content"}}
+<section class="page-header">
+    <h1>{{ t "Search Results" }} ({{ .total }})</h1>
+</section>
+
+{{ if not .entries }}
+    <p class="alert alert-info">{{ t "There is no result for this search." }}</p>
+{{ else }}
+    <div class="items">
+        {{ range .entries }}
+        <article class="item touch-item item-status-{{ .Status }}" data-id="{{ .ID }}">
+            <div class="item-header">
+                <span class="item-title">
+                    {{ if ne .Feed.Icon.IconID 0 }}
+                        <img src="{{ route "icon" "iconID" .Feed.Icon.IconID }}" width="16" height="16">
+                    {{ end }}
+                    <a href="{{ route "searchEntry" "entryID" .ID }}?q={{ $.searchQuery }}">{{ .Title }}</a>
+                </span>
+                <span class="category"><a href="{{ route "categoryEntries" "categoryID" .Feed.Category.ID }}">{{ .Feed.Category.Title }}</a></span>
+            </div>
+            {{ template "item_meta" dict "user" $.user "entry" . "hasSaveEntry" $.hasSaveEntry  }}
+        </article>
+        {{ end }}
+    </div>
+    {{ template "pagination" .pagination }}
+{{ end }}
+
+{{ end }}

+ 33 - 1
template/views.go

@@ -1,5 +1,5 @@
 // Code generated by go generate; DO NOT EDIT.
-// 2018-06-30 18:00:36.547092772 -0700 PDT m=+0.023261871
+// 2018-07-04 22:00:22.166425155 -0700 PDT m=+0.028318956
 
 package template
 
@@ -1018,6 +1018,37 @@ var templateViewsMap = map[string]string{
     </div>
     {{ end }}
 </section>
+{{ end }}
+`,
+	"search_entries": `{{ define "title"}}{{ t "Search Results" }} ({{ .total }}){{ end }}
+
+{{ define "content"}}
+<section class="page-header">
+    <h1>{{ t "Search Results" }} ({{ .total }})</h1>
+</section>
+
+{{ if not .entries }}
+    <p class="alert alert-info">{{ t "There is no result for this search." }}</p>
+{{ else }}
+    <div class="items">
+        {{ range .entries }}
+        <article class="item touch-item item-status-{{ .Status }}" data-id="{{ .ID }}">
+            <div class="item-header">
+                <span class="item-title">
+                    {{ if ne .Feed.Icon.IconID 0 }}
+                        <img src="{{ route "icon" "iconID" .Feed.Icon.IconID }}" width="16" height="16">
+                    {{ end }}
+                    <a href="{{ route "searchEntry" "entryID" .ID }}?q={{ $.searchQuery }}">{{ .Title }}</a>
+                </span>
+                <span class="category"><a href="{{ route "categoryEntries" "categoryID" .Feed.Category.ID }}">{{ .Feed.Category.Title }}</a></span>
+            </div>
+            {{ template "item_meta" dict "user" $.user "entry" . "hasSaveEntry" $.hasSaveEntry  }}
+        </article>
+        {{ end }}
+    </div>
+    {{ template "pagination" .pagination }}
+{{ end }}
+
 {{ end }}
 `,
 	"sessions": `{{ define "title"}}{{ t "Sessions" }}{{ end }}
@@ -1285,6 +1316,7 @@ var templateViewsMapChecksums = map[string]string{
 	"import":              "73b5112e20bfd232bf73334544186ea419505936bc237d481517a8622901878f",
 	"integrations":        "20c1c82070b93235d189b10acccd0cda5694cc5684d0b3be23de2ba5ae83e73f",
 	"login":               "7d83c3067c02f1f6aafdd8816c7f97a4eb5a5a4bdaaaa4cc1e2fbb9c17ea65e8",
+	"search_entries":      "2ed1fa914f322ee077bf4a63d29bb2c5bb415bc3245a0d47019ff8077a5d40fc",
 	"sessions":            "3fa79031dd883847eba92fbafe5f535fa3a4e1614bb610f20588b6f8fc8b3624",
 	"settings":            "d435dc37e82896ce9a7a573b3c2aeda1db71eec62349e2472ebbf1d5c3e0bc21",
 	"unread_entries":      "ca3ef1547d7d170b005a2f48fabd4c0a15550884db5e481659c13ffe6a47d19d",

+ 95 - 0
ui/entry_search.go

@@ -0,0 +1,95 @@
+// Copyright 2018 Frédéric Guillot. All rights reserved.
+// Use of this source code is governed by the Apache 2.0
+// license that can be found in the LICENSE file.
+
+package ui
+
+import (
+	"net/http"
+
+	"github.com/miniflux/miniflux/http/context"
+	"github.com/miniflux/miniflux/http/request"
+	"github.com/miniflux/miniflux/http/response/html"
+	"github.com/miniflux/miniflux/http/route"
+	"github.com/miniflux/miniflux/logger"
+	"github.com/miniflux/miniflux/model"
+	"github.com/miniflux/miniflux/storage"
+	"github.com/miniflux/miniflux/ui/session"
+	"github.com/miniflux/miniflux/ui/view"
+)
+
+// ShowSearchEntry shows a single entry in "search" mode.
+func (c *Controller) ShowSearchEntry(w http.ResponseWriter, r *http.Request) {
+	ctx := context.New(r)
+
+	user, err := c.store.UserByID(ctx.UserID())
+	if err != nil {
+		html.ServerError(w, err)
+		return
+	}
+
+	entryID, err := request.IntParam(r, "entryID")
+	if err != nil {
+		html.BadRequest(w, err)
+		return
+	}
+
+	searchQuery := request.QueryParam(r, "q", "")
+	builder := c.store.NewEntryQueryBuilder(user.ID)
+	builder.WithSearchQuery(searchQuery)
+	builder.WithEntryID(entryID)
+	builder.WithoutStatus(model.EntryStatusRemoved)
+
+	entry, err := builder.GetEntry()
+	if err != nil {
+		html.ServerError(w, err)
+		return
+	}
+
+	if entry == nil {
+		html.NotFound(w)
+		return
+	}
+
+	if entry.Status == model.EntryStatusUnread {
+		err = c.store.SetEntriesStatus(user.ID, []int64{entry.ID}, model.EntryStatusRead)
+		if err != nil {
+			logger.Error("[Controller:ShowSearchEntry] %v", err)
+			html.ServerError(w, nil)
+			return
+		}
+	}
+
+	entryPaginationBuilder := storage.NewEntryPaginationBuilder(c.store, user.ID, entry.ID, user.EntryDirection)
+	entryPaginationBuilder.WithSearchQuery(searchQuery)
+	prevEntry, nextEntry, err := entryPaginationBuilder.Entries()
+	if err != nil {
+		html.ServerError(w, err)
+		return
+	}
+
+	nextEntryRoute := ""
+	if nextEntry != nil {
+		nextEntryRoute = route.Path(c.router, "searchEntry", "entryID", nextEntry.ID)
+	}
+
+	prevEntryRoute := ""
+	if prevEntry != nil {
+		prevEntryRoute = route.Path(c.router, "searchEntry", "entryID", prevEntry.ID)
+	}
+
+	sess := session.New(c.store, ctx)
+	view := view.New(c.tpl, ctx, sess)
+	view.Set("searchQuery", searchQuery)
+	view.Set("entry", entry)
+	view.Set("prevEntry", prevEntry)
+	view.Set("nextEntry", nextEntry)
+	view.Set("nextEntryRoute", nextEntryRoute)
+	view.Set("prevEntryRoute", prevEntryRoute)
+	view.Set("menu", "search")
+	view.Set("user", user)
+	view.Set("countUnread", c.store.CountUnreadEntries(user.ID))
+	view.Set("hasSaveEntry", c.store.HasSaveEntry(user.ID))
+
+	html.OK(w, view.Render("entry"))
+}

+ 1 - 0
ui/pagination.go

@@ -17,6 +17,7 @@ type pagination struct {
 	ShowPrev     bool
 	NextOffset   int
 	PrevOffset   int
+	SearchQuery  string
 }
 
 func (c *Controller) getPagination(route string, total, offset int) pagination {

+ 66 - 0
ui/search_entries.go

@@ -0,0 +1,66 @@
+// Copyright 2018 Frédéric Guillot. All rights reserved.
+// Use of this source code is governed by the Apache 2.0
+// license that can be found in the LICENSE file.
+
+package ui
+
+import (
+	"net/http"
+
+	"github.com/miniflux/miniflux/http/context"
+	"github.com/miniflux/miniflux/http/request"
+	"github.com/miniflux/miniflux/http/response/html"
+	"github.com/miniflux/miniflux/http/route"
+	"github.com/miniflux/miniflux/model"
+	"github.com/miniflux/miniflux/ui/session"
+	"github.com/miniflux/miniflux/ui/view"
+)
+
+// ShowSearchEntries shows all entries for the given feed.
+func (c *Controller) ShowSearchEntries(w http.ResponseWriter, r *http.Request) {
+	ctx := context.New(r)
+
+	user, err := c.store.UserByID(ctx.UserID())
+	if err != nil {
+		html.ServerError(w, err)
+		return
+	}
+
+	searchQuery := request.QueryParam(r, "q", "")
+	offset := request.QueryIntParam(r, "offset", 0)
+	builder := c.store.NewEntryQueryBuilder(user.ID)
+	builder.WithSearchQuery(searchQuery)
+	builder.WithoutStatus(model.EntryStatusRemoved)
+	builder.WithOrder(model.DefaultSortingOrder)
+	builder.WithDirection(user.EntryDirection)
+	builder.WithOffset(offset)
+	builder.WithLimit(nbItemsPerPage)
+
+	entries, err := builder.GetEntries()
+	if err != nil {
+		html.ServerError(w, err)
+		return
+	}
+
+	count, err := builder.CountEntries()
+	if err != nil {
+		html.ServerError(w, err)
+		return
+	}
+
+	sess := session.New(c.store, ctx)
+	view := view.New(c.tpl, ctx, sess)
+	pagination := c.getPagination(route.Path(c.router, "searchEntries"), count, offset)
+	pagination.SearchQuery = searchQuery
+
+	view.Set("searchQuery", searchQuery)
+	view.Set("entries", entries)
+	view.Set("total", count)
+	view.Set("pagination", pagination)
+	view.Set("menu", "search")
+	view.Set("user", user)
+	view.Set("countUnread", c.store.CountUnreadEntries(user.ID))
+	view.Set("hasSaveEntry", c.store.HasSaveEntry(user.ID))
+
+	html.OK(w, view.Render("search_entries"))
+}

+ 1 - 1
ui/static/bin.go

@@ -1,5 +1,5 @@
 // Code generated by go generate; DO NOT EDIT.
-// 2018-06-19 22:56:40.300982018 -0700 PDT m=+0.022794138
+// 2018-07-04 22:00:22.158122512 -0700 PDT m=+0.020016313
 
 package static
 

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 1 - 1
ui/static/css.go


+ 5 - 0
ui/static/css/black.css

@@ -17,6 +17,7 @@ a:hover {
     color: #ddd;
 }
 
+/* Header and main menu */
 .header li {
     border-color: #333;
 }
@@ -36,10 +37,12 @@ a:hover {
     color: rgba(82, 168, 236, 0.85);
 }
 
+/* Page header */
 .page-header h1 {
     border-color: #333;
 }
 
+/* Logo */
 .logo a:hover span {
     color: #555;
 }
@@ -61,6 +64,7 @@ tr:hover {
 }
 
 /* Forms */
+input[type="search"],
 input[type="url"],
 input[type="password"],
 input[type="text"] {
@@ -69,6 +73,7 @@ input[type="text"] {
     color: #ccc;
 }
 
+input[type="search"]:focus,
 input[type="url"]:focus,
 input[type="password"]:focus,
 input[type="text"]:focus {

+ 43 - 3
ui/static/css/common.css

@@ -37,9 +37,9 @@ a:hover {
     text-decoration: none;
 }
 
+/* Header and main menu */
 .header {
     margin-top: 10px;
-    margin-bottom: 20px;
 }
 
 .header nav ul {
@@ -74,6 +74,7 @@ a:hover {
     color: #888;
 }
 
+/* Page header */
 .page-header {
     margin-bottom: 25px;
 }
@@ -92,6 +93,7 @@ a:hover {
     line-height: 1.8em;
 }
 
+/* Logo */
 .logo {
     cursor: pointer;
     text-align: center;
@@ -114,6 +116,16 @@ a:hover {
     color: #000;
 }
 
+/* Search form */
+.search {
+    text-align: center;
+    display: none;
+}
+
+.search-toggle-switch {
+    display: none;
+}
+
 @media (min-width: 600px) {
     body {
         margin: auto;
@@ -124,6 +136,7 @@ a:hover {
         text-align: left;
         float: left;
         margin-right: 15px;
+        margin-left: 5px;
     }
 
     .header nav ul {
@@ -147,6 +160,30 @@ a:hover {
         display: inline;
         padding-right: 15px;
     }
+
+    /* Search form */
+    .search {
+        text-align: right;
+        display: block;
+        margin-top: 10px;
+        margin-bottom: 10px;
+    }
+
+    .search-toggle-switch {
+        display: block;
+    }
+
+    .search-form {
+        display: none;
+    }
+
+    .search-toggle-switch.has-search-query {
+        display: none;
+    }
+
+    .search-form.has-search-query {
+        display: block;
+    }
 }
 
 /* Tables */
@@ -217,6 +254,7 @@ select {
     margin-bottom: 15px;
 }
 
+input[type="search"],
 input[type="url"],
 input[type="password"],
 input[type="text"] {
@@ -230,6 +268,7 @@ input[type="text"] {
     -webkit-appearance: none;
 }
 
+input[type="search"]:focus,
 input[type="url"]:focus,
 input[type="password"]:focus,
 input[type="text"]:focus {
@@ -377,7 +416,7 @@ a.button {
     top: 0;
     left: 0;
     bottom: 0;
-    width: 350px;
+    width: 360px;
     overflow: auto;
     background: #f0f0f0;
     box-shadow: 2px 0 5px 0 #ccc;
@@ -387,6 +426,7 @@ a.button {
 
 #modal-left h3 {
     font-weight: 400;
+    margin: 0;
 }
 
 .btn-close-modal {
@@ -577,7 +617,7 @@ article.feed-parsing-error {
 .entry header h1 {
     font-size: 2.0em;
     line-height: 1.25em;
-    margin: 30px 0;
+    margin: 5px 0 30px 0;
 }
 
 .entry header h1 a {

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 9 - 5
ui/static/js.go


+ 38 - 15
ui/static/js/app.js

@@ -168,13 +168,13 @@ class KeyboardHandler {
 
                 if (keys.every((value, index) => value === this.queue[index])) {
                     this.queue = [];
-                    this.shortcuts[combination]();
+                    this.shortcuts[combination](event);
                     return;
                 }
 
                 if (keys.length === 1 && key === keys[0]) {
                     this.queue = [];
-                    this.shortcuts[combination]();
+                    this.shortcuts[combination](event);
                     return;
                 }
             }
@@ -299,20 +299,11 @@ class UnreadCounterHandler {
             let oldValue = parseInt(element.textContent, 10);
             element.innerHTML = callback(oldValue);
         });
-        // The titlebar must be updated only on the "Unread" page.
+
         if (window.location.href.endsWith('/unread')) {
-            // The following 3 lines ensure that the unread count in the titlebar
-            // is updated correctly when users presses "v".
             let oldValue = parseInt(document.title.split('(')[1], 10);
             let newValue = callback(oldValue);
-            // Notes:
-            // - This will only be executed in the /unread page. Therefore, it
-            //   will not affect titles on other pages.
-            // - When there are no unread items, user cannot press "v".
-            //   Therefore, we need not handle the case where title is
-            //   "Unread Items - Miniflux". This applies to other cases as well.
-            //   i.e.: if there are no unread items, user cannot decrement or
-            //   increment anything.
+
             document.title = document.title.replace(
                 /(.*?)\(\d+\)(.*?)/,
                 function (match, prefix, suffix, offset, string) {
@@ -330,8 +321,7 @@ class EntryHandler {
         request.withBody({entry_ids: entryIDs, status: status});
         request.withCallback(callback);
         request.execute();
-        // The following 5 lines ensure that the unread count in the menu is
-        // updated correctly when users presses "v".
+
         if (status === "read") {
             UnreadCounterHandler.decrement(1);
         } else {
@@ -501,6 +491,13 @@ class MenuHandler {
         } else {
             menu.style.display = "block";
         }
+
+        let searchElement = document.querySelector(".header .search");
+        if (DomHelper.isVisible(searchElement)) {
+            searchElement.style.display = "none";
+        } else {
+            searchElement.style.display = "block";
+        }
     }
 }
 
@@ -537,6 +534,27 @@ class ModalHandler {
 }
 
 class NavHandler {
+    setFocusToSearchInput(event) {
+        event.preventDefault();
+        event.stopPropagation();
+
+        let toggleSwitchElement = document.querySelector(".search-toggle-switch");
+        if (toggleSwitchElement) {
+            toggleSwitchElement.style.display = "none";
+        }
+
+        let searchFormElement = document.querySelector(".search-form");
+        if (searchFormElement) {
+            searchFormElement.style.display = "block";
+        }
+
+        let searchInputElement = document.getElementById("search-input");
+        if (searchInputElement) {
+            searchInputElement.focus();
+            searchInputElement.value = "";
+        }
+    }
+
     showKeyboardShortcuts() {
         let template = document.getElementById("keyboard-shortcuts");
         if (template !== null) {
@@ -757,6 +775,7 @@ document.addEventListener("DOMContentLoaded", function() {
     keyboardHandler.on("d", () => navHandler.fetchOriginalContent());
     keyboardHandler.on("f", () => navHandler.toggleBookmark());
     keyboardHandler.on("?", () => navHandler.showKeyboardShortcuts());
+    keyboardHandler.on("/", (e) => navHandler.setFocusToSearchInput(e));
     keyboardHandler.on("Escape", () => ModalHandler.close());
     keyboardHandler.listen();
 
@@ -790,6 +809,10 @@ document.addEventListener("DOMContentLoaded", function() {
         (new ConfirmHandler()).handle(event);
     });
 
+    mouseHandler.onClick("a[data-action=search]", (event) => {
+        navHandler.setFocusToSearchInput(event);
+    });
+
     if (document.documentElement.clientWidth < 600) {
         let menuHandler = new MenuHandler();
         mouseHandler.onClick(".logo", () => menuHandler.toggleMainMenu());

Algunos archivos no se mostraron porque demasiados archivos cambiaron en este cambio