diff options
author | Frédéric Guillot <fred@miniflux.net> | 2018-07-04 22:05:19 -0700 |
---|---|---|
committer | Frédéric Guillot <fred@miniflux.net> | 2018-07-04 22:05:19 -0700 |
commit | 6d0dc451e45effc8cbb6953a766b111036d893ce (patch) | |
tree | 3c7be86a036dfc77534fe89abb7d88413e5fdb52 | |
parent | af15412954b9b36365f3fd723bf91b3c7c2f88bd (diff) |
Add search form
-rw-r--r-- | daemon/routes.go | 2 | ||||
-rwxr-xr-x | locale/translations.go | 11 | ||||
-rw-r--r-- | locale/translations/fr_FR.json | 7 | ||||
-rw-r--r-- | sql/sql.go | 2 | ||||
-rw-r--r-- | storage/entry_pagination_builder.go | 8 | ||||
-rw-r--r-- | template/common.go | 25 | ||||
-rw-r--r-- | template/html/common/entry_pagination.html | 4 | ||||
-rw-r--r-- | template/html/common/layout.html | 9 | ||||
-rw-r--r-- | template/html/common/pagination.html | 4 | ||||
-rw-r--r-- | template/html/search_entries.html | 30 | ||||
-rw-r--r-- | template/views.go | 34 | ||||
-rw-r--r-- | ui/entry_search.go | 95 | ||||
-rw-r--r-- | ui/pagination.go | 1 | ||||
-rw-r--r-- | ui/search_entries.go | 66 | ||||
-rw-r--r-- | ui/static/bin.go | 2 | ||||
-rw-r--r-- | ui/static/css.go | 10 | ||||
-rw-r--r-- | ui/static/css/black.css | 5 | ||||
-rw-r--r-- | ui/static/css/common.css | 46 | ||||
-rw-r--r-- | ui/static/js.go | 18 | ||||
-rw-r--r-- | ui/static/js/app.js | 53 |
20 files changed, 383 insertions, 49 deletions
diff --git a/daemon/routes.go b/daemon/routes.go index 6f52437..56ee6b9 100644 --- a/daemon/routes.go +++ b/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") diff --git a/locale/translations.go b/locale/translations.go index aff5b7c..a14f70e 100755 --- a/locale/translations.go +++ b/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", diff --git a/locale/translations/fr_FR.json b/locale/translations/fr_FR.json index 5270b91..534432c 100644 --- a/locale/translations/fr_FR.json +++ b/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,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 diff --git a/storage/entry_pagination_builder.go b/storage/entry_pagination_builder.go index 72fae8d..e443b00 100644 --- a/storage/entry_pagination_builder.go +++ b/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") diff --git a/template/common.go b/template/common.go index 1c0f5e2..be12839 100644 --- a/template/common.go +++ b/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">« {{ 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 }}&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 }}&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", } diff --git a/template/html/common/entry_pagination.html b/template/html/common/entry_pagination.html index 6c9f29c..bb2b84f 100644 --- a/template/html/common/entry_pagination.html +++ b/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 }} diff --git a/template/html/common/layout.html b/template/html/common/layout.html index d98de9d..59a3b23 100644 --- a/template/html/common/layout.html +++ b/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">« {{ 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> diff --git a/template/html/common/pagination.html b/template/html/common/pagination.html index 4c6766a..3ea32fb 100644 --- a/template/html/common/pagination.html +++ b/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 }}&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 }}&q={{ .SearchQuery }}{{ end }}" data-page="next">{{ t "Next" }}</a> {{ else }} {{ t "Next" }} {{ end }} diff --git a/template/html/search_entries.html b/template/html/search_entries.html new file mode 100644 index 0000000..85aa7c2 --- /dev/null +++ b/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 }} diff --git a/template/views.go b/template/views.go index a875ef2..c0c031d 100644 --- a/template/views.go +++ b/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 @@ -1020,6 +1020,37 @@ var templateViewsMap = map[string]string{ </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 }} {{ define "content"}} @@ -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", diff --git a/ui/entry_search.go b/ui/entry_search.go new file mode 100644 index 0000000..b97bcea --- /dev/null +++ b/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")) +} diff --git a/ui/pagination.go b/ui/pagination.go index 751ba8a..aaf9d12 100644 --- a/ui/pagination.go +++ b/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 { diff --git a/ui/search_entries.go b/ui/search_entries.go new file mode 100644 index 0000000..cb02df9 --- /dev/null +++ b/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")) +} diff --git a/ui/static/bin.go b/ui/static/bin.go index 3cb9157..7a29018 100644 --- a/ui/static/bin.go +++ b/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 diff --git a/ui/static/css.go b/ui/static/css.go index 3dbc5f2..0152544 100644 --- a/ui/static/css.go +++ b/ui/static/css.go @@ -1,16 +1,16 @@ // Code generated by go generate; DO NOT EDIT. -// 2018-07-02 01:25:09.162618385 -0400 EDT m=+0.008090707 +// 2018-07-04 22:00:22.16263119 -0700 PDT m=+0.024524991 package static var Stylesheets = map[string]string{ - "black": `body{background:#222;color:#efefef}h1,h2,h3{color:#aaa}a{color:#aaa}a:focus,a:hover{color:#ddd}.header li{border-color:#333}.header a{color:#ddd;font-weight:400}.header .active a{font-weight:400;color:#9b9494}.header a:focus,.header a:hover{color:rgba(82,168,236,.85)}.page-header h1{border-color:#333}.logo a:hover span{color:#555}table,th,td{border:1px solid #555}th{background:#333;color:#aaa;font-weight:400}tr:hover{background-color:#333;color:#aaa}input[type=url],input[type=password],input[type=text]{border:1px solid #555;background:#333;color:#ccc}input[type=url]:focus,input[type=password]:focus,input[type=text]:focus{color:#efefef;border-color:rgba(82,168,236,.8);box-shadow:0 0 8px rgba(82,168,236,.6)}.button-primary{border-color:#444;background:#333;color:#efefef}.button-primary:hover,.button-primary:focus{border-color:#888;background:#555}.alert,.alert-success,.alert-error,.alert-info,.alert-normal{color:#efefef;background-color:#333;border-color:#444}.panel{background:#333;border-color:#555;color:#9b9b9b}#modal-left{background:#333;color:#efefef;box-shadow:0 0 10px rgba(82,168,236,.6)}.keyboard-shortcuts li{color:#9b9b9b}.unread-counter-wrapper{color:#bbb}.category{color:#efefef;background-color:#333;border-color:#444}.category a{color:#999}.category a:hover,.category a:focus{color:#aaa}.pagination a{color:#aaa}.pagination-bottom{border-color:#333}.item{border-color:#666;padding:4px}.item.current-item{border-width:2px;border-color:rgba(82,168,236,.8);box-shadow:0 0 8px rgba(82,168,236,.6)}.item-title a{font-weight:400}.item-status-read .item-title a{color:#666}.item-status-read .item-title a:focus,.item-status-read .item-title a:hover{color:rgba(82,168,236,.6)}.item-meta a:hover,.item-meta a:focus{color:#aaa}.item-meta li:after{color:#ddd}article.feed-parsing-error{background-color:#343434}.parsing-error{color:#eee}.entry header{border-color:#333}.entry header h1 a{color:#bbb}.entry-content,.entry-content p,ul{color:#999}.entry-content pre,.entry-content code{color:#fff;background:#555;border-color:#888}.entry-content q{color:#777}.entry-enclosure{border-color:#333}`, - "common": `*{margin:0;padding:0;box-sizing:border-box}html{-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}body{font-family:helvetica neue,Helvetica,Arial,sans-serif;text-rendering:optimizeLegibility}main{padding-left:5px;padding-right:5px;margin-bottom:30px}a{color:#36c}a:focus{outline:0;color:red;text-decoration:none;border:1px dotted #aaa}a:hover{color:#333;text-decoration:none}.header{margin-top:10px;margin-bottom:20px}.header nav ul{display:none}.header li{cursor:pointer;padding-left:10px;line-height:2.1em;font-size:1.2em;border-bottom:1px dotted #ddd}.header li:hover a{color:#888}.header a{font-size:.9em;color:#444;text-decoration:none;border:0}.header .active a{font-weight:600}.header a:hover,.header a:focus{color:#888}.page-header{margin-bottom:25px}.page-header h1{font-weight:500;border-bottom:1px dotted #ddd}.page-header ul{margin-left:25px}.page-header li{list-style-type:circle;line-height:1.8em}.logo{cursor:pointer;text-align:center}.logo a{color:#000;letter-spacing:1px}.logo a:hover{color:#396}.logo a span{color:#396}.logo a:hover span{color:#000}@media(min-width:600px){body{margin:auto;max-width:750px}.logo{text-align:left;float:left;margin-right:15px}.header nav ul{display:block}.header li{display:inline;padding:0;padding-right:15px;line-height:normal;border:0;font-size:1em}.page-header ul{margin-left:0}.page-header li{display:inline;padding-right:15px}}table{width:100%;border-collapse:collapse}table,th,td{border:1px solid #ddd}th,td{padding:5px;text-align:left}td{vertical-align:top}th{background:#fcfcfc}tr:hover{background-color:#f9f9f9}.column-40{width:40%}.column-25{width:25%}.column-20{width:20%}fieldset{border:1px solid #ddd;padding:8px}legend{font-weight:500;padding-left:3px;padding-right:3px}label{cursor:pointer;display:block}.radio-group{line-height:1.9em}div.radio-group label{display:inline-block}select{margin-bottom:15px}input[type=url],input[type=password],input[type=text]{border:1px solid #ccc;padding:3px;line-height:20px;width:250px;font-size:99%;margin-bottom:10px;margin-top:5px;-webkit-appearance:none}input[type=url]:focus,input[type=password]:focus,input[type=text]:focus{color:#000;border-color:rgba(82,168,236,.8);outline:0;box-shadow:0 0 8px rgba(82,168,236,.6)}input[type=checkbox]{margin-bottom:15px}::-moz-placeholder,::-ms-input-placeholder,::-webkit-input-placeholder{color:#ddd;padding-top:2px}.form-help{font-size:.9em;color:brown;margin-bottom:15px}.form-section{border-left:2px dotted #ddd;padding-left:20px;margin-left:10px}a.button{text-decoration:none}.button{display:inline-block;-webkit-appearance:none;-moz-appearance:none;font-size:1.1em;cursor:pointer;padding:3px 10px;border:1px solid;border-radius:unset}.button-primary{border-color:#3079ed;background:#4d90fe;color:#fff}.button-primary:hover,.button-primary:focus{border-color:#2f5bb7;background:#357ae8}.button-danger{border-color:#b0281a;background:#d14836;color:#fff}.button-danger:hover,.button-danger:focus{color:#fff;background:#c53727}.button:disabled{color:#ccc;background:#f7f7f7;border-color:#ccc}.buttons{margin-top:10px;margin-bottom:20px}.alert{padding:8px 35px 8px 14px;margin-bottom:20px;color:#c09853;background-color:#fcf8e3;border:1px solid #fbeed5;border-radius:4px;overflow:auto}.alert h3{margin-top:0;margin-bottom:15px}.alert-success{color:#468847;background-color:#dff0d8;border-color:#d6e9c6}.alert-error{color:#b94a48;background-color:#f2dede;border-color:#eed3d7}.alert-error a{color:#b94a48}.alert-info{color:#3a87ad;background-color:#d9edf7;border-color:#bce8f1}.panel{color:#333;background-color:#fcfcfc;border:1px solid #ddd;border-radius:5px;padding:10px;margin-bottom:15px}.panel h3{font-weight:500;margin-top:0;margin-bottom:20px}.panel ul{margin-left:30px}#modal-left{position:fixed;top:0;left:0;bottom:0;width:350px;overflow:auto;background:#f0f0f0;box-shadow:2px 0 5px 0 #ccc;padding:5px;padding-top:30px}#modal-left h3{font-weight:400}.btn-close-modal{position:absolute;top:0;right:0;font-size:1.7em;color:#ccc;padding:0 .2em;margin:10px;text-decoration:none}.btn-close-modal:hover{color:#999}.keyboard-shortcuts li{margin-left:25px;list-style-type:square;color:#333;font-size:.95em;line-height:1.45em}.keyboard-shortcuts p{line-height:1.9em}.login-form{margin:50px auto 0;max-width:280px}.unread-counter-wrapper{font-size:.9em;font-weight:300;color:#666}.category{font-size:.75em;background-color:#fffcd7;border:1px solid #d5d458;border-radius:5px;margin-left:.25em;padding:1px .4em;white-space:nowrap}.category a{color:#555;text-decoration:none}.category a:hover,.category a:focus{color:#000}.pagination{font-size:1.1em;display:flex;align-items:center;padding-top:8px}.pagination-bottom{border-top:1px dotted #ddd;margin-bottom:15px;margin-top:50px}.pagination>div{flex:1}.pagination-next{text-align:right}.pagination-prev:before{content:"« "}.pagination-next:after{content:" »"}.pagination a{color:#333}.pagination a:hover,.pagination a:focus{text-decoration:none}.item{border:1px dotted #ddd;margin-bottom:20px;padding:5px;overflow:hidden}.item.current-item{border:3px solid #bce;padding:3px}.item-title a{text-decoration:none;font-weight:600}.item-status-read .item-title a{color:#777}.item-meta{color:#777;font-size:.8em}.item-meta a{color:#777;text-decoration:none}.item-meta a:hover,.item-meta a:focus{color:#333}.item-meta ul{margin-top:5px}.item-meta li{display:inline}.item-meta li:after{content:"|";color:#aaa}.item-meta li:last-child:after{content:""}.items{overflow-x:hidden}.hide-read-items .item-status-read{display:none}article.feed-parsing-error{background-color:#fcf8e3;border-color:#aaa}.parsing-error{font-size:.85em;margin-top:2px;color:#333}.parsing-error-count{cursor:pointer}.entry header{padding-bottom:5px;border-bottom:1px dotted #ddd}.entry header h1{font-size:2em;line-height:1.25em;margin:30px 0}.entry header h1 a{text-decoration:none;color:#333}.entry header h1 a:hover,.entry header h1 a:focus{color:#666}.entry-actions{margin-bottom:20px}.entry-actions a{text-decoration:none}.entry-actions li{display:inline}.entry-actions li:not(:last-child):after{content:"|"}.entry-meta{font-size:.95em;margin:0 0 20px;color:#666;overflow-wrap:break-word}.entry-website img{vertical-align:top}.entry-website a{color:#666;vertical-align:top;text-decoration:none}.entry-website a:hover,.entry-website a:focus{text-decoration:underline}.entry-date{font-size:.65em;font-style:italic;color:#555}.entry-content{padding-top:15px;font-size:1.2em;font-weight:300;font-family:Georgia,times new roman,Times,serif;color:#555;line-height:1.4em;overflow-wrap:break-word}.entry-content h1,h2,h3,h4,h5,h6{margin-top:15px;margin-bottom:10px}.entry-content iframe,.entry-content video,.entry-content img{max-width:100%}.entry-content figure{margin-top:15px;margin-bottom:15px}.entry-content figure img{border:1px solid #000}.entry-content figcaption{font-size:.75em;text-transform:uppercase;color:#777}.entry-content p{margin-top:10px;margin-bottom:15px}.entry-content a{overflow-wrap:break-word}.entry-content a:visited{color:purple}.entry-content dt{font-weight:500;margin-top:15px;color:#555}.entry-content dd{margin-left:15px;margin-top:5px;padding-left:20px;border-left:3px solid #ddd;color:#777;font-weight:300;line-height:1.4em}.entry-content blockquote{border-left:4px solid #ddd;padding-left:25px;margin-left:20px;margin-top:20px;margin-bottom:20px;color:#888;line-height:1.4em;font-family:Georgia,serif}.entry-content q{color:purple;font-family:Georgia,serif;font-style:italic}.entry-content q:before{content:"“"}.entry-content q:after{content:"”"}.entry-content pre{padding:5px;background:#f0f0f0;border:1px solid #ddd;overflow:scroll;overflow-wrap:initial}.entry-content table{table-layout:fixed;max-width:100%}.entry-content ul,.entry-content ol{margin-left:30px}.entry-content ul{list-style-type:square}.entry-enclosures h3{font-weight:500}.entry-enclosure{border:1px dotted #ddd;padding:5px;margin-top:10px;max-width:100%}.entry-enclosure-download{font-size:.85em;overflow-wrap:break-word}.enclosure-video video,.enclosure-image img{max-width:100%}.confirm{font-weight:500;color:#ed2d04}.confirm a{color:#ed2d04}.loading{font-style:italic}.bookmarklet{border:1px dashed #ccc;border-radius:5px;padding:15px;margin:15px;text-align:center}.bookmarklet a{font-weight:600;text-decoration:none;font-size:1.2em}`, + "black": `body{background:#222;color:#efefef}h1,h2,h3{color:#aaa}a{color:#aaa}a:focus,a:hover{color:#ddd}.header li{border-color:#333}.header a{color:#ddd;font-weight:400}.header .active a{font-weight:400;color:#9b9494}.header a:focus,.header a:hover{color:rgba(82,168,236,.85)}.page-header h1{border-color:#333}.logo a:hover span{color:#555}table,th,td{border:1px solid #555}th{background:#333;color:#aaa;font-weight:400}tr:hover{background-color:#333;color:#aaa}input[type=search],input[type=url],input[type=password],input[type=text]{border:1px solid #555;background:#333;color:#ccc}input[type=search]:focus,input[type=url]:focus,input[type=password]:focus,input[type=text]:focus{color:#efefef;border-color:rgba(82,168,236,.8);box-shadow:0 0 8px rgba(82,168,236,.6)}.button-primary{border-color:#444;background:#333;color:#efefef}.button-primary:hover,.button-primary:focus{border-color:#888;background:#555}.alert,.alert-success,.alert-error,.alert-info,.alert-normal{color:#efefef;background-color:#333;border-color:#444}.panel{background:#333;border-color:#555;color:#9b9b9b}#modal-left{background:#333;color:#efefef;box-shadow:0 0 10px rgba(82,168,236,.6)}.keyboard-shortcuts li{color:#9b9b9b}.unread-counter-wrapper{color:#bbb}.category{color:#efefef;background-color:#333;border-color:#444}.category a{color:#999}.category a:hover,.category a:focus{color:#aaa}.pagination a{color:#aaa}.pagination-bottom{border-color:#333}.item{border-color:#666;padding:4px}.item.current-item{border-width:2px;border-color:rgba(82,168,236,.8);box-shadow:0 0 8px rgba(82,168,236,.6)}.item-title a{font-weight:400}.item-status-read .item-title a{color:#666}.item-status-read .item-title a:focus,.item-status-read .item-title a:hover{color:rgba(82,168,236,.6)}.item-meta a:hover,.item-meta a:focus{color:#aaa}.item-meta li:after{color:#ddd}article.feed-parsing-error{background-color:#343434}.parsing-error{color:#eee}.entry header{border-color:#333}.entry header h1 a{color:#bbb}.entry-content,.entry-content p,ul{color:#999}.entry-content pre,.entry-content code{color:#fff;background:#555;border-color:#888}.entry-content q{color:#777}.entry-enclosure{border-color:#333}`, + "common": `*{margin:0;padding:0;box-sizing:border-box}html{-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}body{font-family:helvetica neue,Helvetica,Arial,sans-serif;text-rendering:optimizeLegibility}main{padding-left:5px;padding-right:5px;margin-bottom:30px}a{color:#36c}a:focus{outline:0;color:red;text-decoration:none;border:1px dotted #aaa}a:hover{color:#333;text-decoration:none}.header{margin-top:10px}.header nav ul{display:none}.header li{cursor:pointer;padding-left:10px;line-height:2.1em;font-size:1.2em;border-bottom:1px dotted #ddd}.header li:hover a{color:#888}.header a{font-size:.9em;color:#444;text-decoration:none;border:0}.header .active a{font-weight:600}.header a:hover,.header a:focus{color:#888}.page-header{margin-bottom:25px}.page-header h1{font-weight:500;border-bottom:1px dotted #ddd}.page-header ul{margin-left:25px}.page-header li{list-style-type:circle;line-height:1.8em}.logo{cursor:pointer;text-align:center}.logo a{color:#000;letter-spacing:1px}.logo a:hover{color:#396}.logo a span{color:#396}.logo a:hover span{color:#000}.search{text-align:center;display:none}.search-toggle-switch{display:none}@media(min-width:600px){body{margin:auto;max-width:750px}.logo{text-align:left;float:left;margin-right:15px;margin-left:5px}.header nav ul{display:block}.header li{display:inline;padding:0;padding-right:15px;line-height:normal;border:0;font-size:1em}.page-header ul{margin-left:0}.page-header li{display:inline;padding-right:15px}.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}}table{width:100%;border-collapse:collapse}table,th,td{border:1px solid #ddd}th,td{padding:5px;text-align:left}td{vertical-align:top}th{background:#fcfcfc}tr:hover{background-color:#f9f9f9}.column-40{width:40%}.column-25{width:25%}.column-20{width:20%}fieldset{border:1px solid #ddd;padding:8px}legend{font-weight:500;padding-left:3px;padding-right:3px}label{cursor:pointer;display:block}.radio-group{line-height:1.9em}div.radio-group label{display:inline-block}select{margin-bottom:15px}input[type=search],input[type=url],input[type=password],input[type=text]{border:1px solid #ccc;padding:3px;line-height:20px;width:250px;font-size:99%;margin-bottom:10px;margin-top:5px;-webkit-appearance:none}input[type=search]:focus,input[type=url]:focus,input[type=password]:focus,input[type=text]:focus{color:#000;border-color:rgba(82,168,236,.8);outline:0;box-shadow:0 0 8px rgba(82,168,236,.6)}input[type=checkbox]{margin-bottom:15px}::-moz-placeholder,::-ms-input-placeholder,::-webkit-input-placeholder{color:#ddd;padding-top:2px}.form-help{font-size:.9em;color:brown;margin-bottom:15px}.form-section{border-left:2px dotted #ddd;padding-left:20px;margin-left:10px}a.button{text-decoration:none}.button{display:inline-block;-webkit-appearance:none;-moz-appearance:none;font-size:1.1em;cursor:pointer;padding:3px 10px;border:1px solid;border-radius:unset}.button-primary{border-color:#3079ed;background:#4d90fe;color:#fff}.button-primary:hover,.button-primary:focus{border-color:#2f5bb7;background:#357ae8}.button-danger{border-color:#b0281a;background:#d14836;color:#fff}.button-danger:hover,.button-danger:focus{color:#fff;background:#c53727}.button:disabled{color:#ccc;background:#f7f7f7;border-color:#ccc}.buttons{margin-top:10px;margin-bottom:20px}.alert{padding:8px 35px 8px 14px;margin-bottom:20px;color:#c09853;background-color:#fcf8e3;border:1px solid #fbeed5;border-radius:4px;overflow:auto}.alert h3{margin-top:0;margin-bottom:15px}.alert-success{color:#468847;background-color:#dff0d8;border-color:#d6e9c6}.alert-error{color:#b94a48;background-color:#f2dede;border-color:#eed3d7}.alert-error a{color:#b94a48}.alert-info{color:#3a87ad;background-color:#d9edf7;border-color:#bce8f1}.panel{color:#333;background-color:#fcfcfc;border:1px solid #ddd;border-radius:5px;padding:10px;margin-bottom:15px}.panel h3{font-weight:500;margin-top:0;margin-bottom:20px}.panel ul{margin-left:30px}#modal-left{position:fixed;top:0;left:0;bottom:0;width:360px;overflow:auto;background:#f0f0f0;box-shadow:2px 0 5px 0 #ccc;padding:5px;padding-top:30px}#modal-left h3{font-weight:400;margin:0}.btn-close-modal{position:absolute;top:0;right:0;font-size:1.7em;color:#ccc;padding:0 .2em;margin:10px;text-decoration:none}.btn-close-modal:hover{color:#999}.keyboard-shortcuts li{margin-left:25px;list-style-type:square;color:#333;font-size:.95em;line-height:1.45em}.keyboard-shortcuts p{line-height:1.9em}.login-form{margin:50px auto 0;max-width:280px}.unread-counter-wrapper{font-size:.9em;font-weight:300;color:#666}.category{font-size:.75em;background-color:#fffcd7;border:1px solid #d5d458;border-radius:5px;margin-left:.25em;padding:1px .4em;white-space:nowrap}.category a{color:#555;text-decoration:none}.category a:hover,.category a:focus{color:#000}.pagination{font-size:1.1em;display:flex;align-items:center;padding-top:8px}.pagination-bottom{border-top:1px dotted #ddd;margin-bottom:15px;margin-top:50px}.pagination>div{flex:1}.pagination-next{text-align:right}.pagination-prev:before{content:"« "}.pagination-next:after{content:" »"}.pagination a{color:#333}.pagination a:hover,.pagination a:focus{text-decoration:none}.item{border:1px dotted #ddd;margin-bottom:20px;padding:5px;overflow:hidden}.item.current-item{border:3px solid #bce;padding:3px}.item-title a{text-decoration:none;font-weight:600}.item-status-read .item-title a{color:#777}.item-meta{color:#777;font-size:.8em}.item-meta a{color:#777;text-decoration:none}.item-meta a:hover,.item-meta a:focus{color:#333}.item-meta ul{margin-top:5px}.item-meta li{display:inline}.item-meta li:after{content:"|";color:#aaa}.item-meta li:last-child:after{content:""}.items{overflow-x:hidden}.hide-read-items .item-status-read{display:none}article.feed-parsing-error{background-color:#fcf8e3;border-color:#aaa}.parsing-error{font-size:.85em;margin-top:2px;color:#333}.parsing-error-count{cursor:pointer}.entry header{padding-bottom:5px;border-bottom:1px dotted #ddd}.entry header h1{font-size:2em;line-height:1.25em;margin:5px 0 30px}.entry header h1 a{text-decoration:none;color:#333}.entry header h1 a:hover,.entry header h1 a:focus{color:#666}.entry-actions{margin-bottom:20px}.entry-actions a{text-decoration:none}.entry-actions li{display:inline}.entry-actions li:not(:last-child):after{content:"|"}.entry-meta{font-size:.95em;margin:0 0 20px;color:#666;overflow-wrap:break-word}.entry-website img{vertical-align:top}.entry-website a{color:#666;vertical-align:top;text-decoration:none}.entry-website a:hover,.entry-website a:focus{text-decoration:underline}.entry-date{font-size:.65em;font-style:italic;color:#555}.entry-content{padding-top:15px;font-size:1.2em;font-weight:300;font-family:Georgia,times new roman,Times,serif;color:#555;line-height:1.4em;overflow-wrap:break-word}.entry-content h1,h2,h3,h4,h5,h6{margin-top:15px;margin-bottom:10px}.entry-content iframe,.entry-content video,.entry-content img{max-width:100%}.entry-content figure{margin-top:15px;margin-bottom:15px}.entry-content figure img{border:1px solid #000}.entry-content figcaption{font-size:.75em;text-transform:uppercase;color:#777}.entry-content p{margin-top:10px;margin-bottom:15px}.entry-content a{overflow-wrap:break-word}.entry-content a:visited{color:purple}.entry-content dt{font-weight:500;margin-top:15px;color:#555}.entry-content dd{margin-left:15px;margin-top:5px;padding-left:20px;border-left:3px solid #ddd;color:#777;font-weight:300;line-height:1.4em}.entry-content blockquote{border-left:4px solid #ddd;padding-left:25px;margin-left:20px;margin-top:20px;margin-bottom:20px;color:#888;line-height:1.4em;font-family:Georgia,serif}.entry-content q{color:purple;font-family:Georgia,serif;font-style:italic}.entry-content q:before{content:"“"}.entry-content q:after{content:"”"}.entry-content pre{padding:5px;background:#f0f0f0;border:1px solid #ddd;overflow:scroll;overflow-wrap:initial}.entry-content table{table-layout:fixed;max-width:100%}.entry-content ul,.entry-content ol{margin-left:30px}.entry-content ul{list-style-type:square}.entry-enclosures h3{font-weight:500}.entry-enclosure{border:1px dotted #ddd;padding:5px;margin-top:10px;max-width:100%}.entry-enclosure-download{font-size:.85em;overflow-wrap:break-word}.enclosure-video video,.enclosure-image img{max-width:100%}.confirm{font-weight:500;color:#ed2d04}.confirm a{color:#ed2d04}.loading{font-style:italic}.bookmarklet{border:1px dashed #ccc;border-radius:5px;padding:15px;margin:15px;text-align:center}.bookmarklet a{font-weight:600;text-decoration:none;font-size:1.2em}`, "sansserif": `body,.entry-content,.entry-content blockquote,.entry-content q{font-family:-apple-system,BlinkMacSystemFont,segoe ui,Roboto,helvetica neue,Arial,sans-serif,apple color emoji,segoe ui emoji,segoe ui symbol}.entry-content{font-size:1.17em;font-weight:400}`, } var StylesheetsChecksums = map[string]string{ - "black": "15eb9481c7dabbb39280570f388efd8bfcb36c8669294d003528cecb67c05e60", - "common": "0a4fe0fc0f511777de89a936d89e35916aa5be64ce93320fdd21f964e778f43f", + "black": "b24913d7c08627be7ffac8b8ce9b1a5c4ba9ff7f53b8a4644b4e48ad37ba7efd", + "common": "844a23602dafb736d9856e05a6f544b23745854d37de4017aaef61fbb4c22f1e", "sansserif": "3d60fb97530e032f350ae14223924f38a369044b9a89c25f9506c00604f7189b", } diff --git a/ui/static/css/black.css b/ui/static/css/black.css index 0f8bd3a..c280687 100644 --- a/ui/static/css/black.css +++ b/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 { diff --git a/ui/static/css/common.css b/ui/static/css/common.css index 1b1adaa..d27965b 100644 --- a/ui/static/css/common.css +++ b/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 { diff --git a/ui/static/js.go b/ui/static/js.go index 192bd0e..3fc04c5 100644 --- a/ui/static/js.go +++ b/ui/static/js.go @@ -1,5 +1,5 @@ // Code generated by go generate; DO NOT EDIT. -// 2018-06-29 20:26:34.577543639 -0700 PDT m=+0.034529605 +// 2018-07-04 22:00:22.164970025 -0700 PDT m=+0.026863826 package static @@ -29,8 +29,8 @@ listen(){let elements=document.querySelectorAll(".touch-item");elements.forEach( class KeyboardHandler{constructor(){this.queue=[];this.shortcuts={};} on(combination,callback){this.shortcuts[combination]=callback;} listen(){document.onkeydown=(event)=>{if(this.isEventIgnored(event)){return;} -let key=this.getKey(event);this.queue.push(key);for(let combination in this.shortcuts){let keys=combination.split(" ");if(keys.every((value,index)=>value===this.queue[index])){this.queue=[];this.shortcuts[combination]();return;} -if(keys.length===1&&key===keys[0]){this.queue=[];this.shortcuts[combination]();return;}} +let key=this.getKey(event);this.queue.push(key);for(let combination in this.shortcuts){let keys=combination.split(" ");if(keys.every((value,index)=>value===this.queue[index])){this.queue=[];this.shortcuts[combination](event);return;} +if(keys.length===1&&key===keys[0]){this.queue=[];this.shortcuts[combination](event);return;}} if(this.queue.length>=2){this.queue=[];}};} isEventIgnored(event){return event.target.tagName==="INPUT"||event.target.tagName==="TEXTAREA";} getKey(event){const mapping={'Esc':'Escape','Up':'ArrowUp','Down':'ArrowDown','Left':'ArrowLeft','Right':'ArrowRight'};for(let key in mapping){if(mapping.hasOwnProperty(key)&&key===event.key){return mapping[key];}} @@ -59,12 +59,16 @@ element.innerHTML=element.dataset.labelLoading;let request=new RequestBuilder(el class ConfirmHandler{remove(url){let request=new RequestBuilder(url);request.withCallback(()=>window.location.reload());request.execute();} handle(event){let questionElement=document.createElement("span");let linkElement=event.target;let containerElement=linkElement.parentNode;linkElement.style.display="none";let yesElement=document.createElement("a");yesElement.href="#";yesElement.appendChild(document.createTextNode(linkElement.dataset.labelYes));yesElement.onclick=(event)=>{event.preventDefault();let loadingElement=document.createElement("span");loadingElement.className="loading";loadingElement.appendChild(document.createTextNode(linkElement.dataset.labelLoading));questionElement.remove();containerElement.appendChild(loadingElement);this.remove(linkElement.dataset.url);};let noElement=document.createElement("a");noElement.href="#";noElement.appendChild(document.createTextNode(linkElement.dataset.labelNo));noElement.onclick=(event)=>{event.preventDefault();linkElement.style.display="inline";questionElement.remove();};questionElement.className="confirm";questionElement.appendChild(document.createTextNode(linkElement.dataset.labelQuestion+" "));questionElement.appendChild(yesElement);questionElement.appendChild(document.createTextNode(", "));questionElement.appendChild(noElement);containerElement.appendChild(questionElement);}} class MenuHandler{clickMenuListItem(event){let element=event.target;if(element.tagName==="A"){window.location.href=element.getAttribute("href");}else{window.location.href=element.querySelector("a").getAttribute("href");}} -toggleMainMenu(){let menu=document.querySelector(".header nav ul");if(DomHelper.isVisible(menu)){menu.style.display="none";}else{menu.style.display="block";}}} +toggleMainMenu(){let menu=document.querySelector(".header nav ul");if(DomHelper.isVisible(menu)){menu.style.display="none";}else{menu.style.display="block";} +let searchElement=document.querySelector(".header .search");if(DomHelper.isVisible(searchElement)){searchElement.style.display="none";}else{searchElement.style.display="block";}}} class ModalHandler{static exists(){return document.getElementById("modal-container")!==null;} static open(fragment){if(ModalHandler.exists()){return;} let container=document.createElement("div");container.id="modal-container";container.appendChild(document.importNode(fragment,true));document.body.appendChild(container);let closeButton=document.querySelector("a.btn-close-modal");if(closeButton!==null){closeButton.onclick=(event)=>{event.preventDefault();ModalHandler.close();};}} static close(){let container=document.getElementById("modal-container");if(container!==null){container.parentNode.removeChild(container);}}} -class NavHandler{showKeyboardShortcuts(){let template=document.getElementById("keyboard-shortcuts");if(template!==null){ModalHandler.open(template.content);}} +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){ModalHandler.open(template.content);}} markPageAsRead(){let items=DomHelper.getVisibleElements(".items .item");let entryIDs=[];items.forEach((element)=>{element.classList.add("item-status-read");entryIDs.push(parseInt(element.dataset.id,10));});if(entryIDs.length>0){EntryHandler.updateEntriesStatus(entryIDs,"read",()=>{this.goToPage("next",true);});}} saveEntry(){if(this.isListView()){let currentItem=document.querySelector(".current-item");if(currentItem!==null){let saveLink=currentItem.querySelector("a[data-save-entry]");if(saveLink){EntryHandler.saveEntry(saveLink);}}}else{let saveLink=document.querySelector("a[data-save-entry]");if(saveLink){EntryHandler.saveEntry(saveLink);}}} fetchOriginalContent(){if(!this.isListView()){let link=document.querySelector("a[data-fetch-content-entry]");if(link){EntryHandler.fetchOriginalContent(link);}}} @@ -87,9 +91,9 @@ if(currentItem===null){items[0].classList.add("current-item");return;} for(let i=0;i<items.length;i++){if(items[i].classList.contains("current-item")){items[i].classList.remove("current-item");if(i+1<items.length){items[i+1].classList.add("current-item");DomHelper.scrollPageTo(items[i+1]);} break;}}} isListView(){return document.querySelector(".items")!==null;}} -document.addEventListener("DOMContentLoaded",function(){FormHandler.handleSubmitButtons();let touchHandler=new TouchHandler();touchHandler.listen();let navHandler=new NavHandler();let keyboardHandler=new KeyboardHandler();keyboardHandler.on("g u",()=>navHandler.goToPage("unread"));keyboardHandler.on("g b",()=>navHandler.goToPage("starred"));keyboardHandler.on("g h",()=>navHandler.goToPage("history"));keyboardHandler.on("g f",()=>navHandler.goToPage("feeds"));keyboardHandler.on("g c",()=>navHandler.goToPage("categories"));keyboardHandler.on("g s",()=>navHandler.goToPage("settings"));keyboardHandler.on("ArrowLeft",()=>navHandler.goToPrevious());keyboardHandler.on("ArrowRight",()=>navHandler.goToNext());keyboardHandler.on("j",()=>navHandler.goToPrevious());keyboardHandler.on("p",()=>navHandler.goToPrevious());keyboardHandler.on("k",()=>navHandler.goToNext());keyboardHandler.on("n",()=>navHandler.goToNext());keyboardHandler.on("h",()=>navHandler.goToPage("previous"));keyboardHandler.on("l",()=>navHandler.goToPage("next"));keyboardHandler.on("o",()=>navHandler.openSelectedItem());keyboardHandler.on("v",()=>navHandler.openOriginalLink());keyboardHandler.on("m",()=>navHandler.toggleEntryStatus());keyboardHandler.on("A",()=>navHandler.markPageAsRead());keyboardHandler.on("s",()=>navHandler.saveEntry());keyboardHandler.on("d",()=>navHandler.fetchOriginalContent());keyboardHandler.on("f",()=>navHandler.toggleBookmark());keyboardHandler.on("?",()=>navHandler.showKeyboardShortcuts());keyboardHandler.on("Escape",()=>ModalHandler.close());keyboardHandler.listen();let mouseHandler=new MouseHandler();mouseHandler.onClick("a[data-save-entry]",(event)=>{event.preventDefault();EntryHandler.saveEntry(event.target);});mouseHandler.onClick("a[data-toggle-bookmark]",(event)=>{event.preventDefault();EntryHandler.toggleBookmark(event.target);});mouseHandler.onClick("a[data-toggle-status]",(event)=>{event.preventDefault();let currentItem=DomHelper.findParent(event.target,"item");if(currentItem){EntryHandler.toggleEntryStatus(currentItem);}});mouseHandler.onClick("a[data-fetch-content-entry]",(event)=>{event.preventDefault();EntryHandler.fetchOriginalContent(event.target);});mouseHandler.onClick("a[data-on-click=markPageAsRead]",()=>navHandler.markPageAsRead());mouseHandler.onClick("a[data-confirm]",(event)=>{(new ConfirmHandler()).handle(event);});if(document.documentElement.clientWidth<600){let menuHandler=new MenuHandler();mouseHandler.onClick(".logo",()=>menuHandler.toggleMainMenu());mouseHandler.onClick(".header nav li",(event)=>menuHandler.clickMenuListItem(event));}});})();`, +document.addEventListener("DOMContentLoaded",function(){FormHandler.handleSubmitButtons();let touchHandler=new TouchHandler();touchHandler.listen();let navHandler=new NavHandler();let keyboardHandler=new KeyboardHandler();keyboardHandler.on("g u",()=>navHandler.goToPage("unread"));keyboardHandler.on("g b",()=>navHandler.goToPage("starred"));keyboardHandler.on("g h",()=>navHandler.goToPage("history"));keyboardHandler.on("g f",()=>navHandler.goToPage("feeds"));keyboardHandler.on("g c",()=>navHandler.goToPage("categories"));keyboardHandler.on("g s",()=>navHandler.goToPage("settings"));keyboardHandler.on("ArrowLeft",()=>navHandler.goToPrevious());keyboardHandler.on("ArrowRight",()=>navHandler.goToNext());keyboardHandler.on("j",()=>navHandler.goToPrevious());keyboardHandler.on("p",()=>navHandler.goToPrevious());keyboardHandler.on("k",()=>navHandler.goToNext());keyboardHandler.on("n",()=>navHandler.goToNext());keyboardHandler.on("h",()=>navHandler.goToPage("previous"));keyboardHandler.on("l",()=>navHandler.goToPage("next"));keyboardHandler.on("o",()=>navHandler.openSelectedItem());keyboardHandler.on("v",()=>navHandler.openOriginalLink());keyboardHandler.on("m",()=>navHandler.toggleEntryStatus());keyboardHandler.on("A",()=>navHandler.markPageAsRead());keyboardHandler.on("s",()=>navHandler.saveEntry());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();let mouseHandler=new MouseHandler();mouseHandler.onClick("a[data-save-entry]",(event)=>{event.preventDefault();EntryHandler.saveEntry(event.target);});mouseHandler.onClick("a[data-toggle-bookmark]",(event)=>{event.preventDefault();EntryHandler.toggleBookmark(event.target);});mouseHandler.onClick("a[data-toggle-status]",(event)=>{event.preventDefault();let currentItem=DomHelper.findParent(event.target,"item");if(currentItem){EntryHandler.toggleEntryStatus(currentItem);}});mouseHandler.onClick("a[data-fetch-content-entry]",(event)=>{event.preventDefault();EntryHandler.fetchOriginalContent(event.target);});mouseHandler.onClick("a[data-on-click=markPageAsRead]",()=>navHandler.markPageAsRead());mouseHandler.onClick("a[data-confirm]",(event)=>{(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());mouseHandler.onClick(".header nav li",(event)=>menuHandler.clickMenuListItem(event));}});})();`, } var JavascriptChecksums = map[string]string{ - "app": "1cbc164e7e61cb058564c41d63530cfb8936c63b061b0381f7672f8010be70dd", + "app": "717f6c6431128b6263dc1f54edf6fd0c6efc3bcbc8c9baf23768c8f23ce53675", } diff --git a/ui/static/js/app.js b/ui/static/js/app.js index 7869d47..8ca2f8d 100644 --- a/ui/static/js/app.js +++ b/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()); |