Add bookmarks

This commit is contained in:
Frédéric Guillot 2017-12-22 11:33:01 -08:00
parent b153fa8b3c
commit 9868f900e9
31 changed files with 688 additions and 78 deletions

2
Gopkg.lock generated
View File

@ -41,7 +41,7 @@
branch = "master"
name = "github.com/miniflux/miniflux-go"
packages = ["."]
revision = "ecd111d16e0ce1468cb3b786135c18b3fdc96213"
revision = "60d72460e62282aa90cb43fa3a87596900b87678"
[[projects]]
name = "github.com/tdewolff/minify"

View File

@ -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

View File

@ -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

View File

@ -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",
}

View File

@ -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."
}

View File

@ -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"`

View File

@ -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()
}

View File

@ -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,

View File

@ -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")

View File

@ -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

View File

@ -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

View File

@ -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",
}

View File

@ -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);

View File

@ -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",
}

View File

@ -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>

View File

@ -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>

View File

@ -8,6 +8,16 @@
</h1>
<div class="entry-actions">
<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" }}"

View File

@ -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>

View File

@ -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>

View File

@ -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 }}

View File

@ -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>

View File

@ -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>
@ -480,6 +490,16 @@ var templateViewsMap = map[string]string{
</h1>
<div class="entry-actions">
<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" }}"
@ -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>
@ -1084,6 +1124,68 @@ var templateViewsMap = map[string]string{
</div>
{{ end }}
{{ 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 }}
@ -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",
}

View File

@ -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]
}

View File

@ -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")
}

View File

@ -0,0 +1 @@
alter table entries add column starred bool default 'f';

View File

@ -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",

View File

@ -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)

View File

@ -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,
}
}

View File

@ -12,7 +12,7 @@ import (
"github.com/miniflux/miniflux/sql"
)
const schemaVersion = 11
const schemaVersion = 12
// Migrate run database migrations.
func (s *Storage) Migrate() {

View File

@ -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}}

View File

@ -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"`