From 9868f900e972edd6d4811b4a93b1955e4222e9b1 Mon Sep 17 00:00:00 2001 From: Frédéric Guillot Date: Fri, 22 Dec 2017 11:33:01 -0800 Subject: Add bookmarks --- Gopkg.lock | 2 +- http/client.go | 2 - integration_test.go | 120 ++++++++++++++------ locale/translations.go | 12 +- locale/translations/fr_FR.json | 8 +- model/entry.go | 1 + server/api/controller/entry.go | 47 +++++++- server/fever/fever.go | 35 +++++- server/routes.go | 7 +- server/static/bin.go | 2 +- server/static/css.go | 2 +- server/static/js.go | 10 +- server/static/js/app.js | 42 +++++++ server/template/common.go | 17 +-- server/template/html/category_entries.html | 10 ++ server/template/html/common/layout.html | 13 ++- server/template/html/entry.html | 10 ++ server/template/html/feed_entries.html | 10 ++ server/template/html/history.html | 10 ++ server/template/html/starred.html | 61 ++++++++++ server/template/html/unread.html | 10 ++ server/template/views.go | 125 ++++++++++++++++++++- server/ui/controller/entry.go | 71 +++++++++++- server/ui/controller/starred.go | 68 +++++++++++ sql/schema_version_12.sql | 1 + sql/sql.go | 4 +- storage/entry.go | 15 ++- storage/entry_query_builder.go | 16 ++- storage/migration.go | 2 +- vendor/github.com/miniflux/miniflux-go/client.go | 32 +++++- vendor/github.com/miniflux/miniflux-go/miniflux.go | 1 + 31 files changed, 688 insertions(+), 78 deletions(-) create mode 100644 server/template/html/starred.html create mode 100644 server/ui/controller/starred.go create mode 100644 sql/schema_version_12.sql 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;inavHandler.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{ Miniflux 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 @@ Miniflux 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"}} + + +{{ if not .entries }} +

{{ t "There is no bookmark at the moment." }}

+{{ else }} +
+ {{ range .entries }} + + {{ end }} +
+ {{ 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 @@
  • {{ t "Original" }}
  • +
  • + {{ if .Starred }}★ {{ t "Unstar" }}{{ else }}☆ {{ t "Star" }}{{ end }} +
  • 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{
  • {{ t "Original" }}
  • +
  • + {{ if .Starred }}★ {{ t "Unstar" }}{{ else }}☆ {{ t "Star" }}{{ end }} +
  • @@ -480,6 +490,16 @@ var templateViewsMap = map[string]string{
    @@ -764,6 +794,16 @@ var templateViewsMap = map[string]string{
  • {{ t "Original" }}
  • +
  • + {{ if .Starred }}★ {{ t "Unstar" }}{{ else }}☆ {{ t "Star" }}{{ end }} +
  • @@ -1084,6 +1124,68 @@ var templateViewsMap = map[string]string{ {{ end }} +{{ end }} +`, + "starred": `{{ define "title"}}{{ t "Favorites" }} ({{ .total }}){{ end }} + +{{ define "content"}} + + +{{ if not .entries }} +

    {{ t "There is no bookmark at the moment." }}

    +{{ else }} +
    + {{ range .entries }} + + {{ end }} +
    + {{ template "pagination" .pagination }} +{{ end }} + {{ end }} `, "unread": `{{ define "title"}}{{ t "Unread Items" }} {{ if gt .countUnread 0 }}({{ .countUnread }}){{ end }} {{ end }} @@ -1135,6 +1237,16 @@ var templateViewsMap = map[string]string{
  • {{ t "Original" }}
  • +
  • + {{ if .Starred }}★ {{ t "Unstar" }}{{ else }}☆ {{ t "Star" }}{{ end }} +
  • @@ -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"` -- cgit v1.2.3