aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorGravatar Frédéric Guillot <fred@miniflux.net>2017-12-22 11:33:01 -0800
committerGravatar Frédéric Guillot <fred@miniflux.net>2017-12-22 11:33:01 -0800
commit9868f900e972edd6d4811b4a93b1955e4222e9b1 (patch)
treef366decff3a14c58a6869a1993b5698d6acce19a
parentb153fa8b3cd2e48bbe13326695f11d2013427ebc (diff)
Add bookmarks
-rw-r--r--Gopkg.lock2
-rw-r--r--http/client.go2
-rw-r--r--integration_test.go120
-rw-r--r--locale/translations.go12
-rw-r--r--locale/translations/fr_FR.json8
-rw-r--r--model/entry.go1
-rw-r--r--server/api/controller/entry.go47
-rw-r--r--server/fever/fever.go35
-rw-r--r--server/routes.go7
-rw-r--r--server/static/bin.go2
-rw-r--r--server/static/css.go2
-rw-r--r--server/static/js.go10
-rw-r--r--server/static/js/app.js42
-rw-r--r--server/template/common.go17
-rw-r--r--server/template/html/category_entries.html10
-rw-r--r--server/template/html/common/layout.html13
-rw-r--r--server/template/html/entry.html10
-rw-r--r--server/template/html/feed_entries.html10
-rw-r--r--server/template/html/history.html10
-rw-r--r--server/template/html/starred.html61
-rw-r--r--server/template/html/unread.html10
-rw-r--r--server/template/views.go125
-rw-r--r--server/ui/controller/entry.go71
-rw-r--r--server/ui/controller/starred.go68
-rw-r--r--sql/schema_version_12.sql1
-rw-r--r--sql/sql.go4
-rw-r--r--storage/entry.go15
-rw-r--r--storage/entry_query_builder.go16
-rw-r--r--storage/migration.go2
-rw-r--r--vendor/github.com/miniflux/miniflux-go/client.go32
-rw-r--r--vendor/github.com/miniflux/miniflux-go/miniflux.go1
31 files changed, 688 insertions, 78 deletions
diff --git a/Gopkg.lock b/Gopkg.lock
index 1c5f2ee..2fe22e2 100644
--- a/Gopkg.lock
+++ b/Gopkg.lock
@@ -41,7 +41,7 @@
branch = "master"
name = "github.com/miniflux/miniflux-go"
packages = ["."]
- revision = "ecd111d16e0ce1468cb3b786135c18b3fdc96213"
+ revision = "60d72460e62282aa90cb43fa3a87596900b87678"
[[projects]]
name = "github.com/tdewolff/minify"
diff --git a/http/client.go b/http/client.go
index 304a9cc..af9f06c 100644
--- a/http/client.go
+++ b/http/client.go
@@ -36,8 +36,6 @@ type Client struct {
// Get execute a GET HTTP request.
func (c *Client) Get() (*Response, error) {
- defer helper.ExecutionTime(time.Now(), fmt.Sprintf("[HttpClient:Get] url=%s", c.url))
-
request, err := c.buildRequest(http.MethodGet, nil)
if err != nil {
return nil, err
diff --git a/integration_test.go b/integration_test.go
index 678322e..a76d8c7 100644
--- a/integration_test.go
+++ b/integration_test.go
@@ -22,6 +22,8 @@ const (
testAdminPassword = "test123"
testStandardPassword = "secret"
testFeedURL = "https://github.com/miniflux/miniflux/commits/master.atom"
+ testFeedTitle = "Recent Commits to miniflux:master"
+ testWebsiteURL = "https://github.com/miniflux/miniflux/commits/master"
)
func TestWithBadEndpoint(t *testing.T) {
@@ -486,7 +488,7 @@ func TestCannotDeleteCategoryOfAnotherUser(t *testing.T) {
func TestDiscoverSubscriptions(t *testing.T) {
client := miniflux.NewClient(testBaseURL, testAdminUsername, testAdminPassword)
- subscriptions, err := client.Discover("https://miniflux.net")
+ subscriptions, err := client.Discover(testWebsiteURL)
if err != nil {
t.Fatal(err)
}
@@ -495,16 +497,16 @@ func TestDiscoverSubscriptions(t *testing.T) {
t.Fatalf(`Invalid number of subscriptions, got "%v" instead of "%v"`, len(subscriptions), 2)
}
- if subscriptions[0].Title != "Feed" {
- t.Fatalf(`Invalid feed title, got "%v" instead of "%v"`, subscriptions[0].Title, "Feed")
+ if subscriptions[0].Title != testFeedTitle {
+ t.Fatalf(`Invalid feed title, got "%v" instead of "%v"`, subscriptions[0].Title, testFeedTitle)
}
if subscriptions[0].Type != "atom" {
t.Fatalf(`Invalid feed type, got "%v" instead of "%v"`, subscriptions[0].Type, "atom")
}
- if subscriptions[0].URL != "https://miniflux.net/feed" {
- t.Fatalf(`Invalid feed URL, got "%v" instead of "%v"`, subscriptions[0].URL, "https://miniflux.net/feed")
+ if subscriptions[0].URL != testFeedURL {
+ t.Fatalf(`Invalid feed URL, got "%v" instead of "%v"`, subscriptions[0].URL, testFeedURL)
}
}
@@ -522,7 +524,7 @@ func TestCreateFeed(t *testing.T) {
t.Fatal(err)
}
- feedID, err := client.CreateFeed("https://miniflux.net/feed", categories[0].ID)
+ feedID, err := client.CreateFeed(testFeedURL, categories[0].ID)
if err != nil {
t.Fatal(err)
}
@@ -546,7 +548,7 @@ func TestCannotCreateDuplicatedFeed(t *testing.T) {
t.Fatal(err)
}
- feedID, err := client.CreateFeed("https://miniflux.net/feed", categories[0].ID)
+ feedID, err := client.CreateFeed(testFeedURL, categories[0].ID)
if err != nil {
t.Fatal(err)
}
@@ -555,7 +557,7 @@ func TestCannotCreateDuplicatedFeed(t *testing.T) {
t.Fatalf(`Invalid feed ID, got "%v"`, feedID)
}
- _, err = client.CreateFeed("https://miniflux.net/feed", categories[0].ID)
+ _, err = client.CreateFeed(testFeedURL, categories[0].ID)
if err == nil {
t.Fatal(`Duplicated feeds should not be allowed`)
}
@@ -570,7 +572,7 @@ func TestCreateFeedWithInexistingCategory(t *testing.T) {
}
client = miniflux.NewClient(testBaseURL, username, testStandardPassword)
- _, err = client.CreateFeed("https://miniflux.net/feed", -1)
+ _, err = client.CreateFeed(testFeedURL, -1)
if err == nil {
t.Fatal(`Feeds should not be created with inexisting category`)
}
@@ -590,7 +592,7 @@ func TestUpdateFeed(t *testing.T) {
t.Fatal(err)
}
- feedID, err := client.CreateFeed("https://miniflux.net/feed", categories[0].ID)
+ feedID, err := client.CreateFeed(testFeedURL, categories[0].ID)
if err != nil {
t.Fatal(err)
}
@@ -630,7 +632,7 @@ func TestDeleteFeed(t *testing.T) {
t.Fatal(err)
}
- feedID, err := client.CreateFeed("https://miniflux.net/feed", categories[0].ID)
+ feedID, err := client.CreateFeed(testFeedURL, categories[0].ID)
if err != nil {
t.Fatal(err)
}
@@ -655,7 +657,7 @@ func TestRefreshFeed(t *testing.T) {
t.Fatal(err)
}
- feedID, err := client.CreateFeed("https://miniflux.net/feed", categories[0].ID)
+ feedID, err := client.CreateFeed(testFeedURL, categories[0].ID)
if err != nil {
t.Fatal(err)
}
@@ -680,7 +682,7 @@ func TestGetFeed(t *testing.T) {
t.Fatal(err)
}
- feedID, err := client.CreateFeed("https://miniflux.net/feed", categories[0].ID)
+ feedID, err := client.CreateFeed(testFeedURL, categories[0].ID)
if err != nil {
t.Fatal(err)
}
@@ -690,16 +692,16 @@ func TestGetFeed(t *testing.T) {
t.Fatal(err)
}
- if feed.Title != "Miniflux" {
- t.Fatalf(`Invalid feed title, got "%v" instead of "%v"`, feed.Title, "Miniflux")
+ if feed.Title != testFeedTitle {
+ t.Fatalf(`Invalid feed title, got "%v" instead of "%v"`, feed.Title, testFeedTitle)
}
- if feed.SiteURL != "https://miniflux.net/" {
- t.Fatalf(`Invalid site URL, got "%v" instead of "%v"`, feed.SiteURL, "https://miniflux.net/")
+ if feed.SiteURL != testWebsiteURL {
+ t.Fatalf(`Invalid site URL, got "%v" instead of "%v"`, feed.SiteURL, testWebsiteURL)
}
- if feed.FeedURL != "https://miniflux.net/feed" {
- t.Fatalf(`Invalid feed URL, got "%v" instead of "%v"`, feed.FeedURL, "https://miniflux.net/feed")
+ if feed.FeedURL != testFeedURL {
+ t.Fatalf(`Invalid feed URL, got "%v" instead of "%v"`, feed.FeedURL, testFeedURL)
}
if feed.Category.ID != categories[0].ID {
@@ -780,7 +782,7 @@ func TestGetFeeds(t *testing.T) {
t.Fatal(err)
}
- feedID, err := client.CreateFeed("https://miniflux.net/feed", categories[0].ID)
+ feedID, err := client.CreateFeed(testFeedURL, categories[0].ID)
if err != nil {
t.Fatal(err)
}
@@ -798,16 +800,16 @@ func TestGetFeeds(t *testing.T) {
t.Fatalf(`Invalid feed ID, got "%v" instead of "%v"`, feeds[0].ID, feedID)
}
- if feeds[0].Title != "Miniflux" {
- t.Fatalf(`Invalid feed title, got "%v" instead of "%v"`, feeds[0].Title, "Miniflux")
+ if feeds[0].Title != testFeedTitle {
+ t.Fatalf(`Invalid feed title, got "%v" instead of "%v"`, feeds[0].Title, testFeedTitle)
}
- if feeds[0].SiteURL != "https://miniflux.net/" {
- t.Fatalf(`Invalid site URL, got "%v" instead of "%v"`, feeds[0].SiteURL, "https://miniflux.net/")
+ if feeds[0].SiteURL != testWebsiteURL {
+ t.Fatalf(`Invalid site URL, got "%v" instead of "%v"`, feeds[0].SiteURL, testWebsiteURL)
}
- if feeds[0].FeedURL != "https://miniflux.net/feed" {
- t.Fatalf(`Invalid feed URL, got "%v" instead of "%v"`, feeds[0].FeedURL, "https://miniflux.net/feed")
+ if feeds[0].FeedURL != testFeedURL {
+ t.Fatalf(`Invalid feed URL, got "%v" instead of "%v"`, feeds[0].FeedURL, testFeedURL)
}
if feeds[0].Category.ID != categories[0].ID {
@@ -837,7 +839,7 @@ func TestGetAllFeedEntries(t *testing.T) {
t.Fatal(err)
}
- feedID, err := client.CreateFeed("https://miniflux.net/feed", categories[0].ID)
+ feedID, err := client.CreateFeed(testFeedURL, categories[0].ID)
if err != nil {
t.Fatal(err)
}
@@ -883,7 +885,7 @@ func TestGetAllEntries(t *testing.T) {
t.Fatal(err)
}
- _, err = client.CreateFeed("https://miniflux.net/feed", categories[0].ID)
+ _, err = client.CreateFeed(testFeedURL, categories[0].ID)
if err != nil {
t.Fatal(err)
}
@@ -930,7 +932,7 @@ func TestInvalidFilters(t *testing.T) {
t.Fatal(err)
}
- _, err = client.CreateFeed("https://miniflux.net/feed", categories[0].ID)
+ _, err = client.CreateFeed(testFeedURL, categories[0].ID)
if err != nil {
t.Fatal(err)
}
@@ -965,7 +967,7 @@ func TestGetEntry(t *testing.T) {
t.Fatal(err)
}
- _, err = client.CreateFeed("https://miniflux.net/feed", categories[0].ID)
+ _, err = client.CreateFeed(testFeedURL, categories[0].ID)
if err != nil {
t.Fatal(err)
}
@@ -975,7 +977,16 @@ func TestGetEntry(t *testing.T) {
t.Fatal(err)
}
- entry, err := client.Entry(result.Entries[0].FeedID, result.Entries[0].ID)
+ entry, err := client.FeedEntry(result.Entries[0].FeedID, result.Entries[0].ID)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if entry.ID != result.Entries[0].ID {
+ t.Fatal("Wrong entry returned")
+ }
+
+ entry, err = client.Entry(result.Entries[0].ID)
if err != nil {
t.Fatal(err)
}
@@ -999,7 +1010,7 @@ func TestUpdateStatus(t *testing.T) {
t.Fatal(err)
}
- _, err = client.CreateFeed("https://miniflux.net/feed", categories[0].ID)
+ _, err = client.CreateFeed(testFeedURL, categories[0].ID)
if err != nil {
t.Fatal(err)
}
@@ -1014,7 +1025,7 @@ func TestUpdateStatus(t *testing.T) {
t.Fatal(err)
}
- entry, err := client.Entry(result.Entries[0].FeedID, result.Entries[0].ID)
+ entry, err := client.Entry(result.Entries[0].ID)
if err != nil {
t.Fatal(err)
}
@@ -1029,6 +1040,49 @@ func TestUpdateStatus(t *testing.T) {
}
}
+func TestToggleBookmark(t *testing.T) {
+ username := getRandomUsername()
+ client := miniflux.NewClient(testBaseURL, testAdminUsername, testAdminPassword)
+ _, err := client.CreateUser(username, testStandardPassword, false)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ client = miniflux.NewClient(testBaseURL, username, testStandardPassword)
+ categories, err := client.Categories()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ _, err = client.CreateFeed(testFeedURL, categories[0].ID)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ result, err := client.Entries(&miniflux.Filter{Limit: 1})
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if result.Entries[0].Starred {
+ t.Fatal("The entry should not be starred")
+ }
+
+ err = client.ToggleBookmark(result.Entries[0].ID)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ entry, err := client.Entry(result.Entries[0].ID)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if !entry.Starred {
+ t.Fatal("The entry should be starred")
+ }
+}
+
func getRandomUsername() string {
rand.Seed(time.Now().UnixNano())
var suffix []string
diff --git a/locale/translations.go b/locale/translations.go
index 7a82d05..7b096be 100644
--- a/locale/translations.go
+++ b/locale/translations.go
@@ -1,5 +1,5 @@
// Code generated by go generate; DO NOT EDIT.
-// 2017-12-18 18:49:32.159555255 -0800 PST m=+0.041213049
+// 2017-12-22 11:25:01.98320223 -0800 PST m=+0.048169992
package locale
@@ -177,12 +177,18 @@ var translations = map[string]string{
"Wallabag Client ID": "Identifiant du client Wallabag",
"Wallabag Client Secret": "Clé secrète du client Wallabag",
"Wallabag Username": "Nom d'utilisateur de Wallabag",
- "Wallabag Password": "Mot de passe de Wallabag"
+ "Wallabag Password": "Mot de passe de Wallabag",
+ "Keyboard Shortcut: %s": "Raccourci clavier : %s",
+ "Favorites": "Favoris",
+ "Star": "Favoris",
+ "Unstar": "Enlever favoris",
+ "Starred": "Favoris",
+ "There is no bookmark at the moment.": "Il n'y a aucun favoris pour le moment."
}
`,
}
var translationsChecksums = map[string]string{
"en_US": "6fe95384260941e8a5a3c695a655a932e0a8a6a572c1e45cb2b1ae8baa01b897",
- "fr_FR": "3a71dbf4fcdb488acdaf43530e521a0c17a28ef637fbd60b204e468afb0dbe09",
+ "fr_FR": "e6817ae43e1412d2687036fb4c1b9f6ea4a2329dcb1eddfa01ebbad732c7b401",
}
diff --git a/locale/translations/fr_FR.json b/locale/translations/fr_FR.json
index 1a5cbd6..7d7aca9 100644
--- a/locale/translations/fr_FR.json
+++ b/locale/translations/fr_FR.json
@@ -161,5 +161,11 @@
"Wallabag Client ID": "Identifiant du client Wallabag",
"Wallabag Client Secret": "Clé secrète du client Wallabag",
"Wallabag Username": "Nom d'utilisateur de Wallabag",
- "Wallabag Password": "Mot de passe de Wallabag"
+ "Wallabag Password": "Mot de passe de Wallabag",
+ "Keyboard Shortcut: %s": "Raccourci clavier : %s",
+ "Favorites": "Favoris",
+ "Star": "Favoris",
+ "Unstar": "Enlever favoris",
+ "Starred": "Favoris",
+ "There is no bookmark at the moment.": "Il n'y a aucun favoris pour le moment."
}
diff --git a/model/entry.go b/model/entry.go
index f9a9638..e08e844 100644
--- a/model/entry.go
+++ b/model/entry.go
@@ -30,6 +30,7 @@ type Entry struct {
Date time.Time `json:"published_at"`
Content string `json:"content"`
Author string `json:"author"`
+ Starred bool `json:"starred"`
Enclosures EnclosureList `json:"enclosures,omitempty"`
Feed *Feed `json:"feed,omitempty"`
Category *Category `json:"category,omitempty"`
diff --git a/server/api/controller/entry.go b/server/api/controller/entry.go
index 6d6ab7e..0de794d 100644
--- a/server/api/controller/entry.go
+++ b/server/api/controller/entry.go
@@ -12,8 +12,8 @@ import (
"github.com/miniflux/miniflux/server/core"
)
-// GetEntry is the API handler to get a single feed entry.
-func (c *Controller) GetEntry(ctx *core.Context, request *core.Request, response *core.Response) {
+// GetFeedEntry is the API handler to get a single feed entry.
+func (c *Controller) GetFeedEntry(ctx *core.Context, request *core.Request, response *core.Response) {
userID := ctx.UserID()
feedID, err := request.IntegerParam("feedID")
if err != nil {
@@ -45,6 +45,32 @@ func (c *Controller) GetEntry(ctx *core.Context, request *core.Request, response
response.JSON().Standard(entry)
}
+// GetEntry is the API handler to get a single entry.
+func (c *Controller) GetEntry(ctx *core.Context, request *core.Request, response *core.Response) {
+ userID := ctx.UserID()
+ entryID, err := request.IntegerParam("entryID")
+ if err != nil {
+ response.JSON().BadRequest(err)
+ return
+ }
+
+ builder := c.store.GetEntryQueryBuilder(userID, ctx.UserTimezone())
+ builder.WithEntryID(entryID)
+
+ entry, err := builder.GetEntry()
+ if err != nil {
+ response.JSON().ServerError(errors.New("Unable to fetch this entry from the database"))
+ return
+ }
+
+ if entry == nil {
+ response.JSON().NotFound(errors.New("Entry not found"))
+ return
+ }
+
+ response.JSON().Standard(entry)
+}
+
// GetFeedEntries is the API handler to get all feed entries.
func (c *Controller) GetFeedEntries(ctx *core.Context, request *core.Request, response *core.Response) {
userID := ctx.UserID()
@@ -179,3 +205,20 @@ func (c *Controller) SetEntryStatus(ctx *core.Context, request *core.Request, re
response.JSON().NoContent()
}
+
+// ToggleBookmark is the API handler to toggle bookmark status.
+func (c *Controller) ToggleBookmark(ctx *core.Context, request *core.Request, response *core.Response) {
+ userID := ctx.UserID()
+ entryID, err := request.IntegerParam("entryID")
+ if err != nil {
+ response.JSON().BadRequest(err)
+ return
+ }
+
+ if err := c.store.ToggleBookmark(userID, entryID); err != nil {
+ response.JSON().ServerError(errors.New("Unable to toggle bookmark value"))
+ return
+ }
+
+ response.JSON().NoContent()
+}
diff --git a/server/fever/fever.go b/server/fever/fever.go
index 92d0fa0..322135f 100644
--- a/server/fever/fever.go
+++ b/server/fever/fever.go
@@ -88,7 +88,7 @@ type savedResponse struct {
type linksResponse struct {
baseResponse
- Links []string `json:"links"`
+ Links string `json:"links"`
}
type group struct {
@@ -242,6 +242,7 @@ func (c *Controller) handleFeeds(ctx *core.Context, request *core.Request, respo
}
var result feedsResponse
+ result.Feeds = make([]feed, 0)
for _, f := range feeds {
result.Feeds = append(result.Feeds, feed{
ID: f.ID,
@@ -387,6 +388,11 @@ func (c *Controller) handleItems(ctx *core.Context, request *core.Request, respo
isRead = 1
}
+ isSaved := 0
+ if entry.Starred {
+ isSaved = 1
+ }
+
result.Items = append(result.Items, item{
ID: entry.ID,
FeedID: entry.FeedID,
@@ -394,7 +400,7 @@ func (c *Controller) handleItems(ctx *core.Context, request *core.Request, respo
Author: entry.Author,
HTML: entry.Content,
URL: entry.URL,
- IsSaved: 0,
+ IsSaved: isSaved,
IsRead: isRead,
CreatedAt: entry.Date.Unix(),
})
@@ -446,7 +452,21 @@ func (c *Controller) handleSavedItems(ctx *core.Context, request *core.Request,
userID := ctx.UserID()
logger.Debug("[Fever] Fetching saved items for userID=%d", userID)
- var result savedResponse
+ builder := c.store.GetEntryQueryBuilder(userID, ctx.UserTimezone())
+ builder.WithStarred()
+
+ entryIDs, err := builder.GetEntryIDs()
+ if err != nil {
+ response.JSON().ServerError(err)
+ return
+ }
+
+ var itemsIDs []string
+ for _, entryID := range entryIDs {
+ itemsIDs = append(itemsIDs, strconv.FormatInt(entryID, 10))
+ }
+
+ result := &savedResponse{ItemIDs: strings.Join(itemsIDs, ",")}
result.SetCommonValues()
response.JSON().Standard(result)
}
@@ -473,7 +493,7 @@ func (c *Controller) handleLinks(ctx *core.Context, request *core.Request, respo
userID := ctx.UserID()
logger.Debug("[Fever] Fetching links for userID=%d", userID)
- var result linksResponse
+ result := &linksResponse{Links: ""}
result.SetCommonValues()
response.JSON().Standard(result)
}
@@ -512,6 +532,11 @@ func (c *Controller) handleWriteItems(ctx *core.Context, request *core.Request,
case "unread":
c.store.SetEntriesStatus(userID, []int64{entryID}, model.EntryStatusUnread)
case "saved":
+ if err := c.store.ToggleBookmark(userID, entryID); err != nil {
+ response.JSON().ServerError(err)
+ return
+ }
+
settings, err := c.store.Integration(userID)
if err != nil {
response.JSON().ServerError(err)
@@ -619,7 +644,7 @@ func (c *Controller) buildFeedGroups(feeds model.Feeds) []feedsGroups {
feedsGroupedByCategory[feed.Category.ID] = append(feedsGroupedByCategory[feed.Category.ID], strconv.FormatInt(feed.ID, 10))
}
- var result []feedsGroups
+ result := make([]feedsGroups, 0)
for categoryID, feedIDs := range feedsGroupedByCategory {
result = append(result, feedsGroups{
GroupID: categoryID,
diff --git a/server/routes.go b/server/routes.go
index 8aa849e..06af581 100644
--- a/server/routes.go
+++ b/server/routes.go
@@ -70,9 +70,11 @@ func getRoutes(cfg *config.Config, store *storage.Storage, feedHandler *feed.Han
router.Handle("/v1/feeds/{feedID}/icon", apiHandler.Use(apiController.FeedIcon)).Methods("GET")
router.Handle("/v1/feeds/{feedID}/entries", apiHandler.Use(apiController.GetFeedEntries)).Methods("GET")
- router.Handle("/v1/feeds/{feedID}/entries/{entryID}", apiHandler.Use(apiController.GetEntry)).Methods("GET")
+ router.Handle("/v1/feeds/{feedID}/entries/{entryID}", apiHandler.Use(apiController.GetFeedEntry)).Methods("GET")
router.Handle("/v1/entries", apiHandler.Use(apiController.GetEntries)).Methods("GET")
router.Handle("/v1/entries", apiHandler.Use(apiController.SetEntryStatus)).Methods("PUT")
+ router.Handle("/v1/entries/{entryID}", apiHandler.Use(apiController.GetEntry)).Methods("GET")
+ router.Handle("/v1/entries/{entryID}/bookmark", apiHandler.Use(apiController.ToggleBookmark)).Methods("PUT")
router.Handle("/stylesheets/{name}.css", uiHandler.Use(uiController.Stylesheet)).Name("stylesheet").Methods("GET")
router.Handle("/js", uiHandler.Use(uiController.Javascript)).Name("javascript").Methods("GET")
@@ -85,6 +87,7 @@ func getRoutes(cfg *config.Config, store *storage.Storage, feedHandler *feed.Han
router.Handle("/unread", uiHandler.Use(uiController.ShowUnreadPage)).Name("unread").Methods("GET")
router.Handle("/history", uiHandler.Use(uiController.ShowHistoryPage)).Name("history").Methods("GET")
+ router.Handle("/starred", uiHandler.Use(uiController.ShowStarredPage)).Name("starred").Methods("GET")
router.Handle("/feed/{feedID}/refresh", uiHandler.Use(uiController.RefreshFeed)).Name("refreshFeed").Methods("GET")
router.Handle("/feed/{feedID}/edit", uiHandler.Use(uiController.EditFeed)).Name("editFeed").Methods("GET")
@@ -99,10 +102,12 @@ func getRoutes(cfg *config.Config, store *storage.Storage, feedHandler *feed.Han
router.Handle("/history/flush", uiHandler.Use(uiController.FlushHistory)).Name("flushHistory").Methods("GET")
router.Handle("/feed/{feedID}/entry/{entryID}", uiHandler.Use(uiController.ShowFeedEntry)).Name("feedEntry").Methods("GET")
router.Handle("/category/{categoryID}/entry/{entryID}", uiHandler.Use(uiController.ShowCategoryEntry)).Name("categoryEntry").Methods("GET")
+ router.Handle("/starred/entry/{entryID}", uiHandler.Use(uiController.ShowStarredEntry)).Name("starredEntry").Methods("GET")
router.Handle("/entry/status", uiHandler.Use(uiController.UpdateEntriesStatus)).Name("updateEntriesStatus").Methods("POST")
router.Handle("/entry/save/{entryID}", uiHandler.Use(uiController.SaveEntry)).Name("saveEntry").Methods("POST")
router.Handle("/entry/download/{entryID}", uiHandler.Use(uiController.FetchContent)).Name("fetchContent").Methods("POST")
+ router.Handle("/entry/bookmark/{entryID}", uiHandler.Use(uiController.ToggleBookmark)).Name("toggleBookmark").Methods("POST")
router.Handle("/categories", uiHandler.Use(uiController.ShowCategories)).Name("categories").Methods("GET")
router.Handle("/category/create", uiHandler.Use(uiController.CreateCategory)).Name("createCategory").Methods("GET")
diff --git a/server/static/bin.go b/server/static/bin.go
index 6fbc99c..b464f30 100644
--- a/server/static/bin.go
+++ b/server/static/bin.go
@@ -1,5 +1,5 @@
// Code generated by go generate; DO NOT EDIT.
-// 2017-12-15 21:24:38.374067217 -0800 PST m=+0.003159627
+// 2017-12-22 11:25:01.957187237 -0800 PST m=+0.022154999
package static
diff --git a/server/static/css.go b/server/static/css.go
index 70a9d8f..c2aaa9f 100644
--- a/server/static/css.go
+++ b/server/static/css.go
@@ -1,5 +1,5 @@
// Code generated by go generate; DO NOT EDIT.
-// 2017-12-15 18:49:24.040014054 -0800 PST m=+0.012609926
+// 2017-12-22 11:25:01.96382557 -0800 PST m=+0.028793332
package static
diff --git a/server/static/js.go b/server/static/js.go
index 07d63fe..61cb18e 100644
--- a/server/static/js.go
+++ b/server/static/js.go
@@ -1,5 +1,5 @@
// Code generated by go generate; DO NOT EDIT.
-// 2017-12-15 18:49:24.041876548 -0800 PST m=+0.014472420
+// 2017-12-22 11:25:01.967857672 -0800 PST m=+0.032825434
package static
@@ -43,6 +43,7 @@ return "";}
execute(){fetch(new Request(this.url,this.options)).then((response)=>{if(this.callback){this.callback(response);}});}}
class EntryHandler{static updateEntriesStatus(entryIDs,status,callback){let url=document.body.dataset.entriesStatusUrl;let request=new RequestBuilder(url);request.withBody({entry_ids:entryIDs,status:status});request.withCallback(callback);request.execute();}
static toggleEntryStatus(element){let entryID=parseInt(element.dataset.id,10);let statuses={read:"unread",unread:"read"};for(let currentStatus in statuses){let newStatus=statuses[currentStatus];if(element.classList.contains("item-status-"+currentStatus)){element.classList.remove("item-status-"+currentStatus);element.classList.add("item-status-"+newStatus);this.updateEntriesStatus([entryID],newStatus);break;}}}
+static toggleBookmark(element){element.innerHTML=element.dataset.labelLoading;let request=new RequestBuilder(element.dataset.bookmarkUrl);request.withCallback(()=>{if(element.dataset.value==="star"){element.innerHTML=element.dataset.labelStar;element.dataset.value="unstar";}else{element.innerHTML=element.dataset.labelUnstar;element.dataset.value="star";}});request.execute();}
static markEntryAsRead(element){if(element.classList.contains("item-status-unread")){element.classList.remove("item-status-unread");element.classList.add("item-status-read");let entryID=parseInt(element.dataset.id,10);this.updateEntriesStatus([entryID],"read");}}
static saveEntry(element){if(element.dataset.completed){return;}
element.innerHTML=element.dataset.labelLoading;let request=new RequestBuilder(element.dataset.saveUrl);request.withCallback(()=>{element.innerHTML=element.dataset.labelDone;element.dataset.completed=true;});request.execute();}
@@ -56,6 +57,9 @@ class NavHandler{markPageAsRead(){let items=DomHelper.getVisibleElements(".items
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);}}}
toggleEntryStatus(){let currentItem=document.querySelector(".current-item");if(currentItem!==null){this.goToNextListItem();EntryHandler.toggleEntryStatus(currentItem);}}
+toggleBookmark(){if(!this.isListView()){this.toggleBookmarkLink(document.querySelector(".entry"));return;}
+let currentItem=document.querySelector(".current-item");if(currentItem!==null){this.toggleBookmarkLink(currentItem);}}
+toggleBookmarkLink(parent){let bookmarkLink=parent.querySelector("a[data-toggle-bookmark]");if(bookmarkLink){EntryHandler.toggleBookmark(bookmarkLink);}}
openOriginalLink(){let entryLink=document.querySelector(".entry h1 a");if(entryLink!==null){DomHelper.openNewTab(entryLink.getAttribute("href"));return;}
let currentItemOriginalLink=document.querySelector(".current-item a[data-original-link]");if(currentItemOriginalLink!==null){DomHelper.openNewTab(currentItemOriginalLink.getAttribute("href"));let currentItem=document.querySelector(".current-item");this.goToNextListItem();EntryHandler.markEntryAsRead(currentItem);}}
openSelectedItem(){let currentItemLink=document.querySelector(".current-item .item-title a");if(currentItemLink!==null){window.location.href=currentItemLink.getAttribute("href");}}
@@ -71,9 +75,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 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.listen();let mouseHandler=new MouseHandler();mouseHandler.onClick("a[data-save-entry]",(event)=>{event.preventDefault();EntryHandler.saveEntry(event.target);});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.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-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));}});})();`,
}
var JavascriptChecksums = map[string]string{
- "app": "a70092cda52d5c3673e789868d8cfeb73a890e1a931b102a738021b5c2a65519",
+ "app": "835ca386dadfc0a7fc3aa6000419051bb8f99f23653c875423f79ff037dcd2da",
}
diff --git a/server/static/js/app.js b/server/static/js/app.js
index 3305d37..a28e510 100644
--- a/server/static/js/app.js
+++ b/server/static/js/app.js
@@ -300,6 +300,22 @@ class EntryHandler {
}
}
+ static toggleBookmark(element) {
+ element.innerHTML = element.dataset.labelLoading;
+
+ let request = new RequestBuilder(element.dataset.bookmarkUrl);
+ request.withCallback(() => {
+ if (element.dataset.value === "star") {
+ element.innerHTML = element.dataset.labelStar;
+ element.dataset.value = "unstar";
+ } else {
+ element.innerHTML = element.dataset.labelUnstar;
+ element.dataset.value = "star";
+ }
+ });
+ request.execute();
+ }
+
static markEntryAsRead(element) {
if (element.classList.contains("item-status-unread")) {
element.classList.remove("item-status-unread");
@@ -468,6 +484,25 @@ class NavHandler {
}
}
+ toggleBookmark() {
+ if (! this.isListView()) {
+ this.toggleBookmarkLink(document.querySelector(".entry"));
+ return;
+ }
+
+ let currentItem = document.querySelector(".current-item");
+ if (currentItem !== null) {
+ this.toggleBookmarkLink(currentItem);
+ }
+ }
+
+ toggleBookmarkLink(parent) {
+ let bookmarkLink = parent.querySelector("a[data-toggle-bookmark]");
+ if (bookmarkLink) {
+ EntryHandler.toggleBookmark(bookmarkLink);
+ }
+ }
+
openOriginalLink() {
let entryLink = document.querySelector(".entry h1 a");
if (entryLink !== null) {
@@ -588,6 +623,7 @@ document.addEventListener("DOMContentLoaded", function() {
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"));
@@ -606,6 +642,7 @@ document.addEventListener("DOMContentLoaded", function() {
keyboardHandler.on("A", () => navHandler.markPageAsRead());
keyboardHandler.on("s", () => navHandler.saveEntry());
keyboardHandler.on("d", () => navHandler.fetchOriginalContent());
+ keyboardHandler.on("f", () => navHandler.toggleBookmark());
keyboardHandler.listen();
let mouseHandler = new MouseHandler();
@@ -614,6 +651,11 @@ document.addEventListener("DOMContentLoaded", function() {
EntryHandler.saveEntry(event.target);
});
+ mouseHandler.onClick("a[data-toggle-bookmark]", (event) => {
+ event.preventDefault();
+ EntryHandler.toggleBookmark(event.target);
+ });
+
mouseHandler.onClick("a[data-fetch-content-entry]", (event) => {
event.preventDefault();
EntryHandler.fetchOriginalContent(event.target);
diff --git a/server/template/common.go b/server/template/common.go
index 6e7b8b9..ca38f7d 100644
--- a/server/template/common.go
+++ b/server/template/common.go
@@ -1,5 +1,5 @@
// Code generated by go generate; DO NOT EDIT.
-// 2017-12-16 17:48:32.321995978 -0800 PST m=+0.055632657
+// 2017-12-22 11:25:01.981502305 -0800 PST m=+0.046470067
package template
@@ -63,22 +63,25 @@ var templateCommonMap = map[string]string{
<a href="{{ route "unread" }}">Mini<span>flux</span></a>
</div>
<ul>
- <li {{ if eq .menu "unread" }}class="active"{{ end }}>
+ <li {{ if eq .menu "unread" }}class="active"{{ end }} title="{{ t "Keyboard Shortcut: %s" "g u" }}">
<a href="{{ route "unread" }}" data-page="unread">{{ t "Unread" }}</a>
{{ if gt .countUnread 0 }}
<span class="unread-counter" title="Unread articles">({{ .countUnread }})</span>
{{ end }}
</li>
- <li {{ if eq .menu "history" }}class="active"{{ end }}>
+ <li {{ if eq .menu "starred" }}class="active"{{ end }} title="{{ t "Keyboard Shortcut: %s" "g b" }}">
+ <a href="{{ route "starred" }}" data-page="starred">{{ t "Starred" }}</a>
+ </li>
+ <li {{ if eq .menu "history" }}class="active"{{ end }} title="{{ t "Keyboard Shortcut: %s" "g h" }}">
<a href="{{ route "history" }}" data-page="history">{{ t "History" }}</a>
</li>
- <li {{ if eq .menu "feeds" }}class="active"{{ end }}>
+ <li {{ if eq .menu "feeds" }}class="active"{{ end }} title="{{ t "Keyboard Shortcut: %s" "g f" }}">
<a href="{{ route "feeds" }}" data-page="feeds">{{ t "Feeds" }}</a>
</li>
- <li {{ if eq .menu "categories" }}class="active"{{ end }}>
+ <li {{ if eq .menu "categories" }}class="active"{{ end }} title="{{ t "Keyboard Shortcut: %s" "g c" }}">
<a href="{{ route "categories" }}" data-page="categories">{{ t "Categories" }}</a>
</li>
- <li {{ if eq .menu "settings" }}class="active"{{ end }}>
+ <li {{ if eq .menu "settings" }}class="active"{{ end }} title="{{ t "Keyboard Shortcut: %s" "g s" }}">
<a href="{{ route "settings" }}" data-page="settings">{{ t "Settings" }}</a>
</li>
<li>
@@ -124,6 +127,6 @@ var templateCommonMap = map[string]string{
var templateCommonMapChecksums = map[string]string{
"entry_pagination": "f1465fa70f585ae8043b200ec9de5bf437ffbb0c19fb7aefc015c3555614ee27",
- "layout": "ff5e3d87a48e4d3aeceda4aabe6c2c2f607006c6b6e83dfcab6c5eb255a1e6f2",
+ "layout": "ade38fbe1058c8dac86b973c289a716e3f97289735e7ad8e8d1731dc6807e38c",
"pagination": "6ff462c2b2a53bc5448b651da017f40a39f1d4f16cef4b2f09784f0797286924",
}
diff --git a/server/template/html/category_entries.html b/server/template/html/category_entries.html
index d86b103..ff73a16 100644
--- a/server/template/html/category_entries.html
+++ b/server/template/html/category_entries.html
@@ -47,6 +47,16 @@
<li>
<a href="{{ .URL }}" target="_blank" rel="noopener noreferrer" referrerpolicy="no-referrer" data-original-link="true">{{ t "Original" }}</a>
</li>
+ <li>
+ <a href="#"
+ data-toggle-bookmark="true"
+ data-bookmark-url="{{ route "toggleBookmark" "entryID" .ID }}"
+ data-label-loading="{{ t "Saving..." }}"
+ data-label-star="☆ {{ t "Star" }}"
+ data-label-unstar="★ {{ t "Unstar" }}"
+ data-value="{{ if .Starred }}star{{ else }}unstar{{ end }}"
+ >{{ if .Starred }}★ {{ t "Unstar" }}{{ else }}☆ {{ t "Star" }}{{ end }}</a>
+ </li>
</ul>
</div>
</article>
diff --git a/server/template/html/common/layout.html b/server/template/html/common/layout.html
index bd7836d..91d7a89 100644
--- a/server/template/html/common/layout.html
+++ b/server/template/html/common/layout.html
@@ -38,22 +38,25 @@
<a href="{{ route "unread" }}">Mini<span>flux</span></a>
</div>
<ul>
- <li {{ if eq .menu "unread" }}class="active"{{ end }}>
+ <li {{ if eq .menu "unread" }}class="active"{{ end }} title="{{ t "Keyboard Shortcut: %s" "g u" }}">
<a href="{{ route "unread" }}" data-page="unread">{{ t "Unread" }}</a>
{{ if gt .countUnread 0 }}
<span class="unread-counter" title="Unread articles">({{ .countUnread }})</span>
{{ end }}
</li>
- <li {{ if eq .menu "history" }}class="active"{{ end }}>
+ <li {{ if eq .menu "starred" }}class="active"{{ end }} title="{{ t "Keyboard Shortcut: %s" "g b" }}">
+ <a href="{{ route "starred" }}" data-page="starred">{{ t "Starred" }}</a>
+ </li>
+ <li {{ if eq .menu "history" }}class="active"{{ end }} title="{{ t "Keyboard Shortcut: %s" "g h" }}">
<a href="{{ route "history" }}" data-page="history">{{ t "History" }}</a>
</li>
- <li {{ if eq .menu "feeds" }}class="active"{{ end }}>
+ <li {{ if eq .menu "feeds" }}class="active"{{ end }} title="{{ t "Keyboard Shortcut: %s" "g f" }}">
<a href="{{ route "feeds" }}" data-page="feeds">{{ t "Feeds" }}</a>
</li>
- <li {{ if eq .menu "categories" }}class="active"{{ end }}>
+ <li {{ if eq .menu "categories" }}class="active"{{ end }} title="{{ t "Keyboard Shortcut: %s" "g c" }}">
<a href="{{ route "categories" }}" data-page="categories">{{ t "Categories" }}</a>
</li>
- <li {{ if eq .menu "settings" }}class="active"{{ end }}>
+ <li {{ if eq .menu "settings" }}class="active"{{ end }} title="{{ t "Keyboard Shortcut: %s" "g s" }}">
<a href="{{ route "settings" }}" data-page="settings">{{ t "Settings" }}</a>
</li>
<li>
diff --git a/server/template/html/entry.html b/server/template/html/entry.html
index 25d7d16..66d08fb 100644
--- a/server/template/html/entry.html
+++ b/server/template/html/entry.html
@@ -10,6 +10,16 @@
<ul>
<li>
<a href="#"
+ data-toggle-bookmark="true"
+ data-bookmark-url="{{ route "toggleBookmark" "entryID" .entry.ID }}"
+ data-label-loading="{{ t "Saving..." }}"
+ data-label-star="☆ {{ t "Star" }}"
+ data-label-unstar="★ {{ t "Unstar" }}"
+ data-value="{{ if .Starred }}star{{ else }}unstar{{ end }}"
+ >{{ if .entry.Starred }}★ {{ t "Unstar" }}{{ else }}☆ {{ t "Star" }}{{ end }}</a>
+ </li>
+ <li>
+ <a href="#"
title="{{ t "Save this article" }}"
data-save-entry="true"
data-save-url="{{ route "saveEntry" "entryID" .entry.ID }}"
diff --git a/server/template/html/feed_entries.html b/server/template/html/feed_entries.html
index 4f23e4e..4317f88 100644
--- a/server/template/html/feed_entries.html
+++ b/server/template/html/feed_entries.html
@@ -58,6 +58,16 @@
<li>
<a href="{{ .URL }}" target="_blank" rel="noopener noreferrer" referrerpolicy="no-referrer" data-original-link="true">{{ t "Original" }}</a>
</li>
+ <li>
+ <a href="#"
+ data-toggle-bookmark="true"
+ data-bookmark-url="{{ route "toggleBookmark" "entryID" .ID }}"
+ data-label-loading="{{ t "Saving..." }}"
+ data-label-star="☆ {{ t "Star" }}"
+ data-label-unstar="★ {{ t "Unstar" }}"
+ data-value="{{ if .Starred }}star{{ else }}unstar{{ end }}"
+ >{{ if .Starred }}★ {{ t "Unstar" }}{{ else }}☆ {{ t "Star" }}{{ end }}</a>
+ </li>
</ul>
</div>
</article>
diff --git a/server/template/html/history.html b/server/template/html/history.html
index 00613c7..5baa0df 100644
--- a/server/template/html/history.html
+++ b/server/template/html/history.html
@@ -47,6 +47,16 @@
<li>
<a href="{{ .URL }}" target="_blank" rel="noopener noreferrer" referrerpolicy="no-referrer" data-original-link="true">{{ t "Original" }}</a>
</li>
+ <li>
+ <a href="#"
+ data-toggle-bookmark="true"
+ data-bookmark-url="{{ route "toggleBookmark" "entryID" .ID }}"
+ data-label-loading="{{ t "Saving..." }}"
+ data-label-star="☆ {{ t "Star" }}"
+ data-label-unstar="★ {{ t "Unstar" }}"
+ data-value="{{ if .Starred }}star{{ else }}unstar{{ end }}"
+ >{{ if .Starred }}★ {{ t "Unstar" }}{{ else }}☆ {{ t "Star" }}{{ end }}</a>
+ </li>
</ul>
</div>
</article>
diff --git a/server/template/html/starred.html b/server/template/html/starred.html
new file mode 100644
index 0000000..1ed1b13
--- /dev/null
+++ b/server/template/html/starred.html
@@ -0,0 +1,61 @@
+{{ define "title"}}{{ t "Favorites" }} ({{ .total }}){{ end }}
+
+{{ define "content"}}
+<section class="page-header">
+ <h1>{{ t "Favorites" }} ({{ .total }})</h1>
+</section>
+
+{{ if not .entries }}
+ <p class="alert alert-info">{{ t "There is no bookmark at the moment." }}</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 "starredEntry" "entryID" .ID }}">{{ .Title }}</a>
+ </span>
+ <span class="category"><a href="{{ route "categoryEntries" "categoryID" .Feed.Category.ID }}">{{ .Feed.Category.Title }}</a></span>
+ </div>
+ <div class="item-meta">
+ <ul>
+ <li>
+ <a href="{{ route "feedEntries" "feedID" .Feed.ID }}" title="{{ .Feed.Title }}">{{ domain .Feed.SiteURL }}</a>
+ </li>
+ <li>
+ <time datetime="{{ isodate .Date }}" title="{{ isodate .Date }}">{{ elapsed .Date }}</time>
+ </li>
+ <li>
+ <a href="#"
+ title="{{ t "Save this article" }}"
+ data-save-entry="true"
+ data-save-url="{{ route "saveEntry" "entryID" .ID }}"
+ data-label-loading="{{ t "Saving..." }}"
+ data-label-done="{{ t "Done!" }}"
+ >{{ t "Save" }}</a>
+ </li>
+ <li>
+ <a href="{{ .URL }}" target="_blank" rel="noopener noreferrer" referrerpolicy="no-referrer" data-original-link="true">{{ t "Original" }}</a>
+ </li>
+ <li>
+ <a href="#"
+ data-toggle-bookmark="true"
+ data-bookmark-url="{{ route "toggleBookmark" "entryID" .ID }}"
+ data-label-loading="{{ t "Saving..." }}"
+ data-label-star="☆ {{ t "Star" }}"
+ data-label-unstar="★ {{ t "Unstar" }}"
+ data-value="{{ if .Starred }}star{{ else }}unstar{{ end }}"
+ >{{ if .Starred }}★ {{ t "Unstar" }}{{ else }}☆ {{ t "Star" }}{{ end }}</a>
+ </li>
+ </ul>
+ </div>
+ </article>
+ {{ end }}
+ </div>
+ {{ template "pagination" .pagination }}
+{{ end }}
+
+{{ end }}
diff --git a/server/template/html/unread.html b/server/template/html/unread.html
index a007197..0240294 100644
--- a/server/template/html/unread.html
+++ b/server/template/html/unread.html
@@ -47,6 +47,16 @@
<li>
<a href="{{ .URL }}" target="_blank" rel="noopener noreferrer" referrerpolicy="no-referrer" data-original-link="true">{{ t "Original" }}</a>
</li>
+ <li>
+ <a href="#"
+ data-toggle-bookmark="true"
+ data-bookmark-url="{{ route "toggleBookmark" "entryID" .ID }}"
+ data-label-loading="{{ t "Saving..." }}"
+ data-label-star="☆ {{ t "Star" }}"
+ data-label-unstar="★ {{ t "Unstar" }}"
+ data-value="{{ if .Starred }}star{{ else }}unstar{{ end }}"
+ >{{ if .Starred }}★ {{ t "Unstar" }}{{ else }}☆ {{ t "Star" }}{{ end }}</a>
+ </li>
</ul>
</div>
</article>
diff --git a/server/template/views.go b/server/template/views.go
index 571727e..0cdb20a 100644
--- a/server/template/views.go
+++ b/server/template/views.go
@@ -1,5 +1,5 @@
// Code generated by go generate; DO NOT EDIT.
-// 2017-12-18 18:49:32.144679579 -0800 PST m=+0.026337373
+// 2017-12-22 11:25:01.96909666 -0800 PST m=+0.034064422
package template
@@ -199,6 +199,16 @@ var templateViewsMap = map[string]string{
<li>
<a href="{{ .URL }}" target="_blank" rel="noopener noreferrer" referrerpolicy="no-referrer" data-original-link="true">{{ t "Original" }}</a>
</li>
+ <li>
+ <a href="#"
+ data-toggle-bookmark="true"
+ data-bookmark-url="{{ route "toggleBookmark" "entryID" .ID }}"
+ data-label-loading="{{ t "Saving..." }}"
+ data-label-star="☆ {{ t "Star" }}"
+ data-label-unstar="★ {{ t "Unstar" }}"
+ data-value="{{ if .Starred }}star{{ else }}unstar{{ end }}"
+ >{{ if .Starred }}★ {{ t "Unstar" }}{{ else }}☆ {{ t "Star" }}{{ end }}</a>
+ </li>
</ul>
</div>
</article>
@@ -482,6 +492,16 @@ var templateViewsMap = map[string]string{
<ul>
<li>
<a href="#"
+ data-toggle-bookmark="true"
+ data-bookmark-url="{{ route "toggleBookmark" "entryID" .entry.ID }}"
+ data-label-loading="{{ t "Saving..." }}"
+ data-label-star="☆ {{ t "Star" }}"
+ data-label-unstar="★ {{ t "Unstar" }}"
+ data-value="{{ if .Starred }}star{{ else }}unstar{{ end }}"
+ >{{ if .entry.Starred }}★ {{ t "Unstar" }}{{ else }}☆ {{ t "Star" }}{{ end }}</a>
+ </li>
+ <li>
+ <a href="#"
title="{{ t "Save this article" }}"
data-save-entry="true"
data-save-url="{{ route "saveEntry" "entryID" .entry.ID }}"
@@ -630,6 +650,16 @@ var templateViewsMap = map[string]string{
<li>
<a href="{{ .URL }}" target="_blank" rel="noopener noreferrer" referrerpolicy="no-referrer" data-original-link="true">{{ t "Original" }}</a>
</li>
+ <li>
+ <a href="#"
+ data-toggle-bookmark="true"
+ data-bookmark-url="{{ route "toggleBookmark" "entryID" .ID }}"
+ data-label-loading="{{ t "Saving..." }}"
+ data-label-star="☆ {{ t "Star" }}"
+ data-label-unstar="★ {{ t "Unstar" }}"
+ data-value="{{ if .Starred }}star{{ else }}unstar{{ end }}"
+ >{{ if .Starred }}★ {{ t "Unstar" }}{{ else }}☆ {{ t "Star" }}{{ end }}</a>
+ </li>
</ul>
</div>
</article>
@@ -764,6 +794,16 @@ var templateViewsMap = map[string]string{
<li>
<a href="{{ .URL }}" target="_blank" rel="noopener noreferrer" referrerpolicy="no-referrer" data-original-link="true">{{ t "Original" }}</a>
</li>
+ <li>
+ <a href="#"
+ data-toggle-bookmark="true"
+ data-bookmark-url="{{ route "toggleBookmark" "entryID" .ID }}"
+ data-label-loading="{{ t "Saving..." }}"
+ data-label-star="☆ {{ t "Star" }}"
+ data-label-unstar="★ {{ t "Unstar" }}"
+ data-value="{{ if .Starred }}star{{ else }}unstar{{ end }}"
+ >{{ if .Starred }}★ {{ t "Unstar" }}{{ else }}☆ {{ t "Star" }}{{ end }}</a>
+ </li>
</ul>
</div>
</article>
@@ -1086,6 +1126,68 @@ var templateViewsMap = map[string]string{
{{ end }}
`,
+ "starred": `{{ define "title"}}{{ t "Favorites" }} ({{ .total }}){{ end }}
+
+{{ define "content"}}
+<section class="page-header">
+ <h1>{{ t "Favorites" }} ({{ .total }})</h1>
+</section>
+
+{{ if not .entries }}
+ <p class="alert alert-info">{{ t "There is no bookmark at the moment." }}</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 "starredEntry" "entryID" .ID }}">{{ .Title }}</a>
+ </span>
+ <span class="category"><a href="{{ route "categoryEntries" "categoryID" .Feed.Category.ID }}">{{ .Feed.Category.Title }}</a></span>
+ </div>
+ <div class="item-meta">
+ <ul>
+ <li>
+ <a href="{{ route "feedEntries" "feedID" .Feed.ID }}" title="{{ .Feed.Title }}">{{ domain .Feed.SiteURL }}</a>
+ </li>
+ <li>
+ <time datetime="{{ isodate .Date }}" title="{{ isodate .Date }}">{{ elapsed .Date }}</time>
+ </li>
+ <li>
+ <a href="#"
+ title="{{ t "Save this article" }}"
+ data-save-entry="true"
+ data-save-url="{{ route "saveEntry" "entryID" .ID }}"
+ data-label-loading="{{ t "Saving..." }}"
+ data-label-done="{{ t "Done!" }}"
+ >{{ t "Save" }}</a>
+ </li>
+ <li>
+ <a href="{{ .URL }}" target="_blank" rel="noopener noreferrer" referrerpolicy="no-referrer" data-original-link="true">{{ t "Original" }}</a>
+ </li>
+ <li>
+ <a href="#"
+ data-toggle-bookmark="true"
+ data-bookmark-url="{{ route "toggleBookmark" "entryID" .ID }}"
+ data-label-loading="{{ t "Saving..." }}"
+ data-label-star="☆ {{ t "Star" }}"
+ data-label-unstar="★ {{ t "Unstar" }}"
+ data-value="{{ if .Starred }}star{{ else }}unstar{{ end }}"
+ >{{ if .Starred }}★ {{ t "Unstar" }}{{ else }}☆ {{ t "Star" }}{{ end }}</a>
+ </li>
+ </ul>
+ </div>
+ </article>
+ {{ end }}
+ </div>
+ {{ template "pagination" .pagination }}
+{{ end }}
+
+{{ end }}
+`,
"unread": `{{ define "title"}}{{ t "Unread Items" }} {{ if gt .countUnread 0 }}({{ .countUnread }}){{ end }} {{ end }}
{{ define "content"}}
@@ -1135,6 +1237,16 @@ var templateViewsMap = map[string]string{
<li>
<a href="{{ .URL }}" target="_blank" rel="noopener noreferrer" referrerpolicy="no-referrer" data-original-link="true">{{ t "Original" }}</a>
</li>
+ <li>
+ <a href="#"
+ data-toggle-bookmark="true"
+ data-bookmark-url="{{ route "toggleBookmark" "entryID" .ID }}"
+ data-label-loading="{{ t "Saving..." }}"
+ data-label-star="☆ {{ t "Star" }}"
+ data-label-unstar="★ {{ t "Unstar" }}"
+ data-value="{{ if .Starred }}star{{ else }}unstar{{ end }}"
+ >{{ if .Starred }}★ {{ t "Unstar" }}{{ else }}☆ {{ t "Star" }}{{ end }}</a>
+ </li>
</ul>
</div>
</article>
@@ -1211,22 +1323,23 @@ var templateViewsMapChecksums = map[string]string{
"about": "ad2fb778fc73c39b733b3f81b13e5c7d689b041fadd24ee2d4577f545aa788ad",
"add_subscription": "053c920b0d7e109ea19dce6a448e304ce720db8633588ea04db16677f7209a7b",
"categories": "ca1280cd157bb527d4fc907da67b05a8347378f6dce965b9389d4bcdf3600a11",
- "category_entries": "951cdacf38fcaed5cdd63a00dc800e26039236b94b556a68e4409012b0095ece",
+ "category_entries": "ce59529666520b8363c9588ce2c437de5a3f6d91941e5c46be25ca08f6900364",
"choose_subscription": "a325f9c976ca2b2dc148e25c8fef0cf6ccab0e04e86e604e7812bb18dc4cdde1",
"create_category": "2b82af5d2dcd67898dc5daa57a6461e6ff8121a6089b2a2a1be909f35e4a2275",
"create_user": "45e226df757126d5fe7c464e295e9a34f07952cfdb71e31e49839850d35af139",
"edit_category": "cee720faadcec58289b707ad30af623d2ee66c1ce23a732965463250d7ff41c5",
"edit_feed": "7e78f0821312557ca05eb840fd52bcb60509c6da205e8ffce11eb08f65ae143d",
"edit_user": "82d9749d76ddbd2352816d813c4b1f6d92f2222de678b4afe5821090246735c7",
- "entry": "ebcf9bb35812dd02759718f7f7411267e6a6c8efd59a9aa0a0e735bcb88efeff",
- "feed_entries": "547c19eb36b20e350ce70ed045173b064cdcd6b114afb241c9f2dda9d88fcc27",
+ "entry": "6b4405e0c8e4a7d31874659f8835f4e43e01dc3c20686091517ac750196dd70f",
+ "feed_entries": "ac93cb9a90f93ddd9dd8a67d7e160592ecb9f5e465ee9679bb14eecd8d4caf20",
"feeds": "c22af39b42ba9ca69ea0914ca789303ec2c5b484abcd4eaa49016e365381257c",
- "history": "9a67599a5d8d67ef958e3f07da339b749f42892667547c9e60a54477e8d32a56",
+ "history": "abc7ea29f7d54f28f73fe14979bbd03dbc41fa6a7c86f95f56d6e94f7b09b9ba",
"import": "73b5112e20bfd232bf73334544186ea419505936bc237d481517a8622901878f",
"integrations": "3c14d7de904911aad7f3ebec6d1a20b50843287f58125c526e167f429f3d455d",
"login": "04f3ce79bfa5753f69e0d956c2a8999c0da549c7925634a3e8134975da0b0e0f",
"sessions": "878dbe8f8ea783b44130c495814179519fa5c3aa2666ac87508f94d58dd008bf",
"settings": "ea2505b9d0a6d6bb594dba87a92079de19baa6d494f0651693a7685489fb7de9",
- "unread": "745d9a1c70c7327aa0ae37328c2736ba6a5f6493db44ef7f12d4da241491b71f",
+ "starred": "33dd40d1a24739e9d05f9cc4b66497cfdb8c86a7abb209a66ca65c2fbafc7d87",
+ "unread": "d990b41e03912600f10069b33376c541a8ef518f302a60fd28763e97d44c85ba",
"users": "44677e28bb5347799ed0020c90ec785aadec4b1454446d92411cfdaf6e32110b",
}
diff --git a/server/ui/controller/entry.go b/server/ui/controller/entry.go
index 01a9ac5..9a1ed8f 100644
--- a/server/ui/controller/entry.go
+++ b/server/ui/controller/entry.go
@@ -373,6 +373,75 @@ func (c *Controller) ShowReadEntry(ctx *core.Context, request *core.Request, res
}))
}
+// ShowStarredEntry shows a single feed entry in "starred" mode.
+func (c *Controller) ShowStarredEntry(ctx *core.Context, request *core.Request, response *core.Response) {
+ user := ctx.LoggedUser()
+
+ entryID, err := request.IntegerParam("entryID")
+ if err != nil {
+ response.HTML().BadRequest(err)
+ return
+ }
+
+ builder := c.store.GetEntryQueryBuilder(user.ID, user.Timezone)
+ builder.WithEntryID(entryID)
+ builder.WithoutStatus(model.EntryStatusRemoved)
+
+ entry, err := builder.GetEntry()
+ if err != nil {
+ response.HTML().ServerError(err)
+ return
+ }
+
+ if entry == nil {
+ response.HTML().NotFound()
+ return
+ }
+
+ if entry.Status == model.EntryStatusUnread {
+ err = c.store.SetEntriesStatus(user.ID, []int64{entry.ID}, model.EntryStatusRead)
+ if err != nil {
+ logger.Error("[Controller:ShowReadEntry] %v", err)
+ response.HTML().ServerError(nil)
+ return
+ }
+ }
+
+ args, err := c.getCommonTemplateArgs(ctx)
+ if err != nil {
+ response.HTML().ServerError(err)
+ return
+ }
+
+ builder = c.store.GetEntryQueryBuilder(user.ID, user.Timezone)
+ builder.WithStarred()
+
+ prevEntry, nextEntry, err := c.getEntryPrevNext(user, builder, entry.ID)
+ if err != nil {
+ response.HTML().ServerError(err)
+ return
+ }
+
+ nextEntryRoute := ""
+ if nextEntry != nil {
+ nextEntryRoute = ctx.Route("starredEntry", "entryID", nextEntry.ID)
+ }
+
+ prevEntryRoute := ""
+ if prevEntry != nil {
+ prevEntryRoute = ctx.Route("starredEntry", "entryID", prevEntry.ID)
+ }
+
+ response.HTML().Render("entry", args.Merge(tplParams{
+ "entry": entry,
+ "prevEntry": prevEntry,
+ "nextEntry": nextEntry,
+ "nextEntryRoute": nextEntryRoute,
+ "prevEntryRoute": prevEntryRoute,
+ "menu": "starred",
+ }))
+}
+
// UpdateEntriesStatus handles Ajax request to update the status for a list of entries.
func (c *Controller) UpdateEntriesStatus(ctx *core.Context, request *core.Request, response *core.Response) {
user := ctx.LoggedUser()
@@ -412,7 +481,7 @@ func (c *Controller) getEntryPrevNext(user *model.User, builder *storage.EntryQu
n := len(entries)
for i := 0; i < n; i++ {
if entries[i].ID == entryID {
- if i-1 > 0 {
+ if i-1 >= 0 {
prev = entries[i-1]
}
diff --git a/server/ui/controller/starred.go b/server/ui/controller/starred.go
new file mode 100644
index 0000000..c035157
--- /dev/null
+++ b/server/ui/controller/starred.go
@@ -0,0 +1,68 @@
+// Copyright 2017 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 controller
+
+import (
+ "github.com/miniflux/miniflux/logger"
+ "github.com/miniflux/miniflux/model"
+ "github.com/miniflux/miniflux/server/core"
+)
+
+// ShowStarredPage renders the page with all starred entries.
+func (c *Controller) ShowStarredPage(ctx *core.Context, request *core.Request, response *core.Response) {
+ user := ctx.LoggedUser()
+ offset := request.QueryIntegerParam("offset", 0)
+
+ args, err := c.getCommonTemplateArgs(ctx)
+ if err != nil {
+ response.HTML().ServerError(err)
+ return
+ }
+
+ builder := c.store.GetEntryQueryBuilder(user.ID, user.Timezone)
+ builder.WithoutStatus(model.EntryStatusRemoved)
+ builder.WithStarred()
+ builder.WithOrder(model.DefaultSortingOrder)
+ builder.WithDirection(user.EntryDirection)
+ builder.WithOffset(offset)
+ builder.WithLimit(nbItemsPerPage)
+
+ entries, err := builder.GetEntries()
+ if err != nil {
+ response.HTML().ServerError(err)
+ return
+ }
+
+ count, err := builder.CountEntries()
+ if err != nil {
+ response.HTML().ServerError(err)
+ return
+ }
+
+ response.HTML().Render("starred", args.Merge(tplParams{
+ "entries": entries,
+ "total": count,
+ "pagination": c.getPagination(ctx.Route("starred"), count, offset),
+ "menu": "starred",
+ }))
+}
+
+// ToggleBookmark handles Ajax request to toggle bookmark value.
+func (c *Controller) ToggleBookmark(ctx *core.Context, request *core.Request, response *core.Response) {
+ user := ctx.LoggedUser()
+ entryID, err := request.IntegerParam("entryID")
+ if err != nil {
+ response.HTML().BadRequest(err)
+ return
+ }
+
+ if err := c.store.ToggleBookmark(user.ID, entryID); err != nil {
+ logger.Error("[Controller:UpdateEntryStatus] %v", err)
+ response.JSON().ServerError(nil)
+ return
+ }
+
+ response.JSON().Standard("OK")
+}
diff --git a/sql/schema_version_12.sql b/sql/schema_version_12.sql
new file mode 100644
index 0000000..34bc9fd
--- /dev/null
+++ b/sql/schema_version_12.sql
@@ -0,0 +1 @@
+alter table entries add column starred bool default 'f'; \ No newline at end of file
diff --git a/sql/sql.go b/sql/sql.go
index 3cbfe11..f70b53f 100644
--- a/sql/sql.go
+++ b/sql/sql.go
@@ -1,5 +1,5 @@
// Code generated by go generate; DO NOT EDIT.
-// 2017-12-18 18:49:32.121198779 -0800 PST m=+0.002856573
+// 2017-12-22 11:25:01.937552528 -0800 PST m=+0.002520290
package sql
@@ -122,6 +122,7 @@ alter table integrations add column wallabag_client_id text default '';
alter table integrations add column wallabag_client_secret text default '';
alter table integrations add column wallabag_username text default '';
alter table integrations add column wallabag_password text default '';`,
+ "schema_version_12": `alter table entries add column starred bool default 'f';`,
"schema_version_2": `create extension if not exists hstore;
alter table users add column extra hstore;
create index users_extra_idx on users using gin(extra);
@@ -164,6 +165,7 @@ var SqlMapChecksums = map[string]string{
"schema_version_1": "7be580fc8a93db5da54b2f6e87019803c33b0b0c28482c7af80cef873bdac4e2",
"schema_version_10": "8faf15ddeff7c8cc305e66218face11ed92b97df2bdc2d0d7944d61441656795",
"schema_version_11": "dc5bbc302e01e425b49c48ddcd8e29e3ab2bb8e73a6cd1858a6ba9fbec0b5243",
+ "schema_version_12": "a95abab6cdf64811fc744abd37457e2928939d999c5ef00d2bdd9398e16f32fb",
"schema_version_2": "e8e9ff32478df04fcddad10a34cba2e8bb1e67e7977b5bd6cdc4c31ec94282b4",
"schema_version_3": "a54745dbc1c51c000f74d4e5068f1e2f43e83309f023415b1749a47d5c1e0f12",
"schema_version_4": "216ea3a7d3e1704e40c797b5dc47456517c27dbb6ca98bf88812f4f63d74b5d9",
diff --git a/storage/entry.go b/storage/entry.go
index 6d67397..8a12f5e 100644
--- a/storage/entry.go
+++ b/storage/entry.go
@@ -179,11 +179,24 @@ func (s *Storage) SetEntriesStatus(userID int64, entryIDs []int64, status string
return nil
}
+// ToggleBookmark toggles entry bookmark value.
+func (s *Storage) ToggleBookmark(userID int64, entryID int64) error {
+ defer helper.ExecutionTime(time.Now(), fmt.Sprintf("[Storage:ToggleBookmark] userID=%d, entryID=%d", userID, entryID))
+
+ query := `UPDATE entries SET starred = NOT starred WHERE user_id=$1 AND id=$2`
+ _, err := s.db.Exec(query, userID, entryID)
+ if err != nil {
+ return fmt.Errorf("unable to update toggle bookmark: %v", err)
+ }
+
+ return nil
+}
+
// FlushHistory set all entries with the status "read" to "removed".
func (s *Storage) FlushHistory(userID int64) error {
defer helper.ExecutionTime(time.Now(), fmt.Sprintf("[Storage:FlushHistory] userID=%d", userID))
- query := `UPDATE entries SET status=$1 WHERE user_id=$2 AND status=$3`
+ query := `UPDATE entries SET status=$1 WHERE user_id=$2 AND status=$3 AND starred='f'`
_, err := s.db.Exec(query, model.EntryStatusRemoved, userID, model.EntryStatusRead)
if err != nil {
return fmt.Errorf("unable to flush history: %v", err)
diff --git a/storage/entry_query_builder.go b/storage/entry_query_builder.go
index 57e708c..040c717 100644
--- a/storage/entry_query_builder.go
+++ b/storage/entry_query_builder.go
@@ -32,6 +32,13 @@ type EntryQueryBuilder struct {
greaterThanEntryID int64
entryIDs []int64
before *time.Time
+ starred bool
+}
+
+// WithStarred adds starred filter.
+func (e *EntryQueryBuilder) WithStarred() *EntryQueryBuilder {
+ e.starred = true
+ return e
}
// Before add condition base on the entry date.
@@ -150,7 +157,8 @@ func (e *EntryQueryBuilder) GetEntries() (model.Entries, error) {
query := `
SELECT
- e.id, e.user_id, e.feed_id, e.hash, e.published_at at time zone '%s', e.title, e.url, e.author, e.content, e.status,
+ e.id, e.user_id, e.feed_id, e.hash, e.published_at at time zone '%s', e.title,
+ e.url, e.author, e.content, e.status, e.starred,
f.title as feed_title, f.feed_url, f.site_url, f.checked_at,
f.category_id, c.title as category_title, f.scraper_rules, f.rewrite_rules, f.crawler,
fi.icon_id
@@ -191,6 +199,7 @@ func (e *EntryQueryBuilder) GetEntries() (model.Entries, error) {
&entry.Author,
&entry.Content,
&entry.Status,
+ &entry.Starred,
&entry.Feed.Title,
&entry.Feed.FeedURL,
&entry.Feed.SiteURL,
@@ -303,6 +312,10 @@ func (e *EntryQueryBuilder) buildCondition() ([]interface{}, string) {
args = append(args, e.before)
}
+ if e.starred {
+ conditions = append(conditions, "e.starred is true")
+ }
+
return args, strings.Join(conditions, " AND ")
}
@@ -334,5 +347,6 @@ func NewEntryQueryBuilder(store *Storage, userID int64, timezone string) *EntryQ
store: store,
userID: userID,
timezone: timezone,
+ starred: false,
}
}
diff --git a/storage/migration.go b/storage/migration.go
index 0408330..c10da45 100644
--- a/storage/migration.go
+++ b/storage/migration.go
@@ -12,7 +12,7 @@ import (
"github.com/miniflux/miniflux/sql"
)
-const schemaVersion = 11
+const schemaVersion = 12
// Migrate run database migrations.
func (s *Storage) Migrate() {
diff --git a/vendor/github.com/miniflux/miniflux-go/client.go b/vendor/github.com/miniflux/miniflux-go/client.go
index 905ec53..7350a70 100644
--- a/vendor/github.com/miniflux/miniflux-go/client.go
+++ b/vendor/github.com/miniflux/miniflux-go/client.go
@@ -291,8 +291,8 @@ func (c *Client) FeedIcon(feedID int64) (*FeedIcon, error) {
return feedIcon, nil
}
-// Entry gets a single feed entry.
-func (c *Client) Entry(feedID, entryID int64) (*Entry, error) {
+// FeedEntry gets a single feed entry.
+func (c *Client) FeedEntry(feedID, entryID int64) (*Entry, error) {
body, err := c.request.Get(fmt.Sprintf("/v1/feeds/%d/entries/%d", feedID, entryID))
if err != nil {
return nil, err
@@ -308,6 +308,23 @@ func (c *Client) Entry(feedID, entryID int64) (*Entry, error) {
return entry, nil
}
+// Entry gets a single entry.
+func (c *Client) Entry(entryID int64) (*Entry, error) {
+ body, err := c.request.Get(fmt.Sprintf("/v1/entries/%d", entryID))
+ if err != nil {
+ return nil, err
+ }
+ defer body.Close()
+
+ var entry *Entry
+ decoder := json.NewDecoder(body)
+ if err := decoder.Decode(&entry); err != nil {
+ return nil, fmt.Errorf("miniflux: response error (%v)", err)
+ }
+
+ return entry, nil
+}
+
// Entries fetch entries.
func (c *Client) Entries(filter *Filter) (*EntryResultSet, error) {
path := buildFilterQueryString("/v1/entries", filter)
@@ -362,6 +379,17 @@ func (c *Client) UpdateEntries(entryIDs []int64, status string) error {
return nil
}
+// ToggleBookmark toggles entry bookmark value.
+func (c *Client) ToggleBookmark(entryID int64) error {
+ body, err := c.request.Put(fmt.Sprintf("/v1/entries/%d/bookmark", entryID), nil)
+ if err != nil {
+ return err
+ }
+ body.Close()
+
+ return nil
+}
+
// NewClient returns a new Client.
func NewClient(endpoint, username, password string) *Client {
return &Client{request: &request{endpoint: endpoint, username: username, password: password}}
diff --git a/vendor/github.com/miniflux/miniflux-go/miniflux.go b/vendor/github.com/miniflux/miniflux-go/miniflux.go
index aefabc8..9cbdc72 100644
--- a/vendor/github.com/miniflux/miniflux-go/miniflux.go
+++ b/vendor/github.com/miniflux/miniflux-go/miniflux.go
@@ -101,6 +101,7 @@ type Entry struct {
Date time.Time `json:"published_at"`
Content string `json:"content"`
Author string `json:"author"`
+ Starred bool `json:"starred"`
Enclosures Enclosures `json:"enclosures,omitempty"`
Feed *Feed `json:"feed,omitempty"`
Category *Category `json:"category,omitempty"`