Quellcode durchsuchen

Make entries sorting configurable

Frédéric Guillot vor 8 Jahren
Ursprung
Commit
2f1367a8d4

+ 7 - 2
README.md

@@ -21,10 +21,15 @@ Notes
 
 Miniflux 2 still in development and **it's not ready to use**.
 
+- [Features](https://docs.miniflux.net/en/latest/features.html)
+- [Requirements](https://docs.miniflux.net/en/latest/requirements.html)
+- [Installation](https://docs.miniflux.net/en/latest/installation.html)
+- [Documentation](https://docs.miniflux.net/)
+
 TODO
 ----
 
-- [ ] Custom entries sorting
+- [X] Custom entries sorting
 - [ ] Webpage scraper (Readability)
 - [X] Bookmarklet
 - [ ] External integrations (Pinboard, Instapaper, Pocket?)
@@ -32,7 +37,7 @@ TODO
 - [X] Integration tests
 - [X] Flush history
 - [X] OAuth2
-- [ ] Touch events
+- [X] Touch events
 - [ ] Fever API?
 
 Credits

+ 6 - 3
locale/translations.go

@@ -1,5 +1,5 @@
 // Code generated by go generate; DO NOT EDIT.
-// 2017-11-27 21:07:53.23444885 -0800 PST m=+0.028635078
+// 2017-12-02 16:12:47.287568844 -0800 PST m=+0.033078160
 
 package locale
 
@@ -150,12 +150,15 @@ var translations = map[string]string{
     "Unlink my Google account": "Dissocier mon compte Google",
     "Link my Google account": "Associer mon compte Google",
     "Category not found for this user.": "Cette catégorie n'existe pas pour cet utilisateur.",
-    "Invalid theme.": "Le thème est invalide."
+    "Invalid theme.": "Le thème est invalide.",
+    "Entry Sorting": "Ordre des éléments",
+    "Older entries first": "Ancien éléments en premier",
+    "Recent entries first": "Éléments récents en premier"
 }
 `,
 }
 
 var translationsChecksums = map[string]string{
 	"en_US": "6fe95384260941e8a5a3c695a655a932e0a8a6a572c1e45cb2b1ae8baa01b897",
-	"fr_FR": "48622d4796fe4a461221565d84f52e22fb167a44a870b08ba32887897bdfbb1a",
+	"fr_FR": "5c054c06fa687f05fd4f6041b002207fe1fe304d6c0c0d094b8caa61a5071ba5",
 }

+ 4 - 1
locale/translations/fr_FR.json

@@ -134,5 +134,8 @@
     "Unlink my Google account": "Dissocier mon compte Google",
     "Link my Google account": "Associer mon compte Google",
     "Category not found for this user.": "Cette catégorie n'existe pas pour cet utilisateur.",
-    "Invalid theme.": "Le thème est invalide."
+    "Invalid theme.": "Le thème est invalide.",
+    "Entry Sorting": "Ordre des éléments",
+    "Older entries first": "Ancien éléments en premier",
+    "Recent entries first": "Éléments récents en premier"
 }

+ 3 - 3
model/entry.go

@@ -15,7 +15,7 @@ const (
 	EntryStatusRead         = "read"
 	EntryStatusRemoved      = "removed"
 	DefaultSortingOrder     = "published_at"
-	DefaultSortingDirection = "desc"
+	DefaultSortingDirection = "asc"
 )
 
 // Entry represents a feed item in the system.
@@ -81,8 +81,8 @@ func ValidateRange(offset, limit int) error {
 	return nil
 }
 
-// GetOppositeDirection returns the opposite sorting direction.
-func GetOppositeDirection(direction string) string {
+// OppositeDirection returns the opposite sorting direction.
+func OppositeDirection(direction string) string {
 	if direction == "asc" {
 		return "desc"
 	}

+ 3 - 3
model/entry_test.go

@@ -57,15 +57,15 @@ func TestValidateRange(t *testing.T) {
 }
 
 func TestGetOppositeDirection(t *testing.T) {
-	if GetOppositeDirection("asc") != "desc" {
+	if OppositeDirection("asc") != "desc" {
 		t.Errorf(`The opposite direction of "asc" should be "desc"`)
 	}
 
-	if GetOppositeDirection("desc") != "asc" {
+	if OppositeDirection("desc") != "asc" {
 		t.Errorf(`The opposite direction of "desc" should be "asc"`)
 	}
 
-	if GetOppositeDirection("invalid") != "asc" {
+	if OppositeDirection("invalid") != "asc" {
 		t.Errorf(`An invalid direction should return "asc"`)
 	}
 }

+ 6 - 0
model/token.go

@@ -4,8 +4,14 @@
 
 package model
 
+import "fmt"
+
 // Token represents a CSRF token in the system.
 type Token struct {
 	ID    string
 	Value string
 }
+
+func (t Token) String() string {
+	return fmt.Sprintf(`ID="%s"`, t.ID)
+}

+ 10 - 9
model/user.go

@@ -11,15 +11,16 @@ import (
 
 // User represents a user in the system.
 type User struct {
-	ID          int64             `json:"id"`
-	Username    string            `json:"username"`
-	Password    string            `json:"password,omitempty"`
-	IsAdmin     bool              `json:"is_admin,omitempty"`
-	Theme       string            `json:"theme,omitempty"`
-	Language    string            `json:"language,omitempty"`
-	Timezone    string            `json:"timezone,omitempty"`
-	LastLoginAt *time.Time        `json:"last_login_at,omitempty"`
-	Extra       map[string]string `json:"-"`
+	ID             int64             `json:"id"`
+	Username       string            `json:"username"`
+	Password       string            `json:"password,omitempty"`
+	IsAdmin        bool              `json:"is_admin,omitempty"`
+	Theme          string            `json:"theme,omitempty"`
+	Language       string            `json:"language,omitempty"`
+	Timezone       string            `json:"timezone,omitempty"`
+	EntryDirection string            `json:"entry_sorting_direction,omitempty"`
+	LastLoginAt    *time.Time        `json:"last_login_at,omitempty"`
+	Extra          map[string]string `json:"-"`
 }
 
 // NewUser returns a new User.

+ 1 - 1
server/static/bin.go

@@ -1,5 +1,5 @@
 // Code generated by go generate; DO NOT EDIT.
-// 2017-11-27 21:07:53.21170439 -0800 PST m=+0.005890618
+// 2017-12-02 16:12:47.261744369 -0800 PST m=+0.007253685
 
 package static
 

Datei-Diff unterdrückt, da er zu groß ist
+ 1 - 1
server/static/css.go


+ 1 - 1
server/static/css/common.css

@@ -10,7 +10,7 @@ body {
     text-rendering: optimizeLegibility;
 }
 
-.main {
+main {
     padding-left: 5px;
     padding-right: 5px;
 }

+ 1 - 1
server/static/js.go

@@ -1,5 +1,5 @@
 // Code generated by go generate; DO NOT EDIT.
-// 2017-12-02 14:53:55.175825378 -0800 PST m=+0.009022020
+// 2017-12-02 16:12:47.268772139 -0800 PST m=+0.014281455
 
 package static
 

+ 4 - 4
server/template/common.go

@@ -1,5 +1,5 @@
 // Code generated by go generate; DO NOT EDIT.
-// 2017-12-02 14:53:55.184002045 -0800 PST m=+0.017198687
+// 2017-12-02 16:12:47.286110197 -0800 PST m=+0.031619513
 
 package template
 
@@ -76,9 +76,9 @@ var templateCommonMap = map[string]string{
         </nav>
     </header>
     {{ end }}
-    <section class="main">
+    <main>
         {{template "content" .}}
-    </section>
+    </main>
 </body>
 </html>
 {{ end }}`,
@@ -106,6 +106,6 @@ var templateCommonMap = map[string]string{
 
 var templateCommonMapChecksums = map[string]string{
 	"entry_pagination": "f1465fa70f585ae8043b200ec9de5bf437ffbb0c19fb7aefc015c3555614ee27",
-	"layout":           "0a06f790d6caad2918c5038f7aa4a2f88ff3b31ed8f52749d45344f2be7bee53",
+	"layout":           "d1f96640bf90eca64571cfa4fe73be55b09d1d5a49da85b1ea9f9d4f9c670a07",
 	"pagination":       "6ff462c2b2a53bc5448b651da017f40a39f1d4f16cef4b2f09784f0797286924",
 }

+ 2 - 2
server/template/html/common/layout.html

@@ -51,9 +51,9 @@
         </nav>
     </header>
     {{ end }}
-    <section class="main">
+    <main>
         {{template "content" .}}
-    </section>
+    </main>
 </body>
 </html>
 {{ end }}

+ 6 - 0
server/template/html/settings.html

@@ -58,6 +58,12 @@
     {{ end }}
     </select>
 
+    <label for="form-entry-direction">{{ t "Entry Sorting" }}</label>
+    <select id="form-entry-direction" name="entry_direction">
+        <option value="asc" {{ if eq "asc" $.form.EntryDirection }}selected="selected"{{ end }}>{{ t "Older entries first" }}</option>
+        <option value="desc" {{ if eq "desc" $.form.EntryDirection }}selected="selected"{{ end }}>{{ t "Recent entries first" }}</option>
+    </select>
+
     <div class="buttons">
         <button type="submit" class="button button-primary" data-label-loading="{{ t "Loading..." }}">{{ t "Update" }}</button>
     </div>

+ 8 - 2
server/template/views.go

@@ -1,5 +1,5 @@
 // Code generated by go generate; DO NOT EDIT.
-// 2017-12-02 14:53:55.176980108 -0800 PST m=+0.010176750
+// 2017-12-02 16:12:47.271430439 -0800 PST m=+0.016939755
 
 package template
 
@@ -922,6 +922,12 @@ var templateViewsMap = map[string]string{
     {{ end }}
     </select>
 
+    <label for="form-entry-direction">{{ t "Entry Sorting" }}</label>
+    <select id="form-entry-direction" name="entry_direction">
+        <option value="asc" {{ if eq "asc" $.form.EntryDirection }}selected="selected"{{ end }}>{{ t "Older entries first" }}</option>
+        <option value="desc" {{ if eq "desc" $.form.EntryDirection }}selected="selected"{{ end }}>{{ t "Recent entries first" }}</option>
+    </select>
+
     <div class="buttons">
         <button type="submit" class="button button-primary" data-label-loading="{{ t "Loading..." }}">{{ t "Update" }}</button>
     </div>
@@ -1070,7 +1076,7 @@ var templateViewsMapChecksums = map[string]string{
 	"integrations":        "c485d6d9ed996635e55e73320610e6bcb01a41c1153e8e739ae2294b0b14b243",
 	"login":               "04f3ce79bfa5753f69e0d956c2a8999c0da549c7925634a3e8134975da0b0e0f",
 	"sessions":            "878dbe8f8ea783b44130c495814179519fa5c3aa2666ac87508f94d58dd008bf",
-	"settings":            "1e2df11f5436eb2d05ae1fae30dd6f1362613011edbfcc79ae8b23854fa348b4",
+	"settings":            "ea2505b9d0a6d6bb594dba87a92079de19baa6d494f0651693a7685489fb7de9",
 	"unread":              "3d8deab9119dc11f0d74a461e1ac89dc29931ba4645a043bb5b3eccba3cba5b8",
 	"users":               "44677e28bb5347799ed0020c90ec785aadec4b1454446d92411cfdaf6e32110b",
 }

+ 1 - 1
server/ui/controller/category.go

@@ -54,7 +54,7 @@ func (c *Controller) ShowCategoryEntries(ctx *core.Context, request *core.Reques
 	builder := c.store.GetEntryQueryBuilder(user.ID, user.Timezone)
 	builder.WithCategoryID(category.ID)
 	builder.WithOrder(model.DefaultSortingOrder)
-	builder.WithDirection(model.DefaultSortingDirection)
+	builder.WithDirection(user.EntryDirection)
 	builder.WithoutStatus(model.EntryStatusRemoved)
 	builder.WithOffset(offset)
 	builder.WithLimit(nbItemsPerPage)

+ 58 - 102
server/ui/controller/entry.go

@@ -11,12 +11,12 @@ import (
 	"github.com/miniflux/miniflux2/model"
 	"github.com/miniflux/miniflux2/server/core"
 	"github.com/miniflux/miniflux2/server/ui/payload"
+	"github.com/miniflux/miniflux2/storage"
 )
 
 // ShowFeedEntry shows a single feed entry in "feed" mode.
 func (c *Controller) ShowFeedEntry(ctx *core.Context, request *core.Request, response *core.Response) {
 	user := ctx.LoggedUser()
-	sortingDirection := model.DefaultSortingDirection
 
 	entryID, err := request.IntegerParam("entryID")
 	if err != nil {
@@ -46,33 +46,25 @@ func (c *Controller) ShowFeedEntry(ctx *core.Context, request *core.Request, res
 		return
 	}
 
-	args, err := c.getCommonTemplateArgs(ctx)
-	if err != nil {
-		response.HTML().ServerError(err)
-		return
+	if entry.Status == model.EntryStatusUnread {
+		err = c.store.SetEntriesStatus(user.ID, []int64{entry.ID}, model.EntryStatusRead)
+		if err != nil {
+			log.Println(err)
+			response.HTML().ServerError(nil)
+			return
+		}
 	}
 
-	builder = c.store.GetEntryQueryBuilder(user.ID, user.Timezone)
-	builder.WithoutStatus(model.EntryStatusRemoved)
-	builder.WithFeedID(feedID)
-	builder.WithCondition("e.id", "!=", entryID)
-	builder.WithCondition("e.published_at", "<=", entry.Date)
-	builder.WithOrder(model.DefaultSortingOrder)
-	builder.WithDirection(model.DefaultSortingDirection)
-	nextEntry, err := builder.GetEntry()
+	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.WithFeedID(feedID)
-	builder.WithCondition("e.id", "!=", entryID)
-	builder.WithCondition("e.published_at", ">=", entry.Date)
-	builder.WithOrder(model.DefaultSortingOrder)
-	builder.WithDirection(model.GetOppositeDirection(sortingDirection))
-	prevEntry, err := builder.GetEntry()
+
+	prevEntry, nextEntry, err := c.getEntryPrevNext(user, builder, entry.ID)
 	if err != nil {
 		response.HTML().ServerError(err)
 		return
@@ -88,14 +80,6 @@ func (c *Controller) ShowFeedEntry(ctx *core.Context, request *core.Request, res
 		prevEntryRoute = ctx.Route("feedEntry", "feedID", feedID, "entryID", prevEntry.ID)
 	}
 
-	if entry.Status == model.EntryStatusUnread {
-		err = c.store.SetEntriesStatus(user.ID, []int64{entry.ID}, model.EntryStatusRead)
-		if err != nil {
-			response.HTML().ServerError(err)
-			return
-		}
-	}
-
 	response.HTML().Render("entry", args.Merge(tplParams{
 		"entry":          entry,
 		"prevEntry":      prevEntry,
@@ -109,7 +93,6 @@ func (c *Controller) ShowFeedEntry(ctx *core.Context, request *core.Request, res
 // ShowCategoryEntry shows a single feed entry in "category" mode.
 func (c *Controller) ShowCategoryEntry(ctx *core.Context, request *core.Request, response *core.Response) {
 	user := ctx.LoggedUser()
-	sortingDirection := model.DefaultSortingDirection
 
 	categoryID, err := request.IntegerParam("categoryID")
 	if err != nil {
@@ -139,33 +122,25 @@ func (c *Controller) ShowCategoryEntry(ctx *core.Context, request *core.Request,
 		return
 	}
 
-	args, err := c.getCommonTemplateArgs(ctx)
-	if err != nil {
-		response.HTML().ServerError(err)
-		return
+	if entry.Status == model.EntryStatusUnread {
+		err = c.store.SetEntriesStatus(user.ID, []int64{entry.ID}, model.EntryStatusRead)
+		if err != nil {
+			log.Println(err)
+			response.HTML().ServerError(nil)
+			return
+		}
 	}
 
-	builder = c.store.GetEntryQueryBuilder(user.ID, user.Timezone)
-	builder.WithoutStatus(model.EntryStatusRemoved)
-	builder.WithCategoryID(categoryID)
-	builder.WithCondition("e.id", "!=", entryID)
-	builder.WithCondition("e.published_at", "<=", entry.Date)
-	builder.WithOrder(model.DefaultSortingOrder)
-	builder.WithDirection(sortingDirection)
-	nextEntry, err := builder.GetEntry()
+	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.WithCategoryID(categoryID)
-	builder.WithCondition("e.id", "!=", entryID)
-	builder.WithCondition("e.published_at", ">=", entry.Date)
-	builder.WithOrder(model.DefaultSortingOrder)
-	builder.WithDirection(model.GetOppositeDirection(sortingDirection))
-	prevEntry, err := builder.GetEntry()
+
+	prevEntry, nextEntry, err := c.getEntryPrevNext(user, builder, entry.ID)
 	if err != nil {
 		response.HTML().ServerError(err)
 		return
@@ -181,15 +156,6 @@ func (c *Controller) ShowCategoryEntry(ctx *core.Context, request *core.Request,
 		prevEntryRoute = ctx.Route("categoryEntry", "categoryID", categoryID, "entryID", prevEntry.ID)
 	}
 
-	if entry.Status == model.EntryStatusUnread {
-		err = c.store.SetEntriesStatus(user.ID, []int64{entry.ID}, model.EntryStatusRead)
-		if err != nil {
-			log.Println(err)
-			response.HTML().ServerError(nil)
-			return
-		}
-	}
-
 	response.HTML().Render("entry", args.Merge(tplParams{
 		"entry":          entry,
 		"prevEntry":      prevEntry,
@@ -203,7 +169,6 @@ func (c *Controller) ShowCategoryEntry(ctx *core.Context, request *core.Request,
 // ShowUnreadEntry shows a single feed entry in "unread" mode.
 func (c *Controller) ShowUnreadEntry(ctx *core.Context, request *core.Request, response *core.Response) {
 	user := ctx.LoggedUser()
-	sortingDirection := model.DefaultSortingDirection
 
 	entryID, err := request.IntegerParam("entryID")
 	if err != nil {
@@ -226,33 +191,25 @@ func (c *Controller) ShowUnreadEntry(ctx *core.Context, request *core.Request, r
 		return
 	}
 
-	args, err := c.getCommonTemplateArgs(ctx)
-	if err != nil {
-		response.HTML().ServerError(err)
-		return
+	if entry.Status == model.EntryStatusUnread {
+		err = c.store.SetEntriesStatus(user.ID, []int64{entry.ID}, model.EntryStatusRead)
+		if err != nil {
+			log.Println(err)
+			response.HTML().ServerError(nil)
+			return
+		}
 	}
 
-	builder = c.store.GetEntryQueryBuilder(user.ID, user.Timezone)
-	builder.WithoutStatus(model.EntryStatusRemoved)
-	builder.WithStatus(model.EntryStatusUnread)
-	builder.WithCondition("e.id", "!=", entryID)
-	builder.WithCondition("e.published_at", "<=", entry.Date)
-	builder.WithOrder(model.DefaultSortingOrder)
-	builder.WithDirection(sortingDirection)
-	nextEntry, err := builder.GetEntry()
+	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.WithStatus(model.EntryStatusUnread)
-	builder.WithCondition("e.id", "!=", entryID)
-	builder.WithCondition("e.published_at", ">=", entry.Date)
-	builder.WithOrder(model.DefaultSortingOrder)
-	builder.WithDirection(model.GetOppositeDirection(sortingDirection))
-	prevEntry, err := builder.GetEntry()
+
+	prevEntry, nextEntry, err := c.getEntryPrevNext(user, builder, entry.ID)
 	if err != nil {
 		response.HTML().ServerError(err)
 		return
@@ -268,15 +225,6 @@ func (c *Controller) ShowUnreadEntry(ctx *core.Context, request *core.Request, r
 		prevEntryRoute = ctx.Route("unreadEntry", "entryID", prevEntry.ID)
 	}
 
-	if entry.Status == model.EntryStatusUnread {
-		err = c.store.SetEntriesStatus(user.ID, []int64{entry.ID}, model.EntryStatusRead)
-		if err != nil {
-			log.Println(err)
-			response.HTML().ServerError(nil)
-			return
-		}
-	}
-
 	response.HTML().Render("entry", args.Merge(tplParams{
 		"entry":          entry,
 		"prevEntry":      prevEntry,
@@ -290,7 +238,6 @@ func (c *Controller) ShowUnreadEntry(ctx *core.Context, request *core.Request, r
 // ShowReadEntry shows a single feed entry in "history" mode.
 func (c *Controller) ShowReadEntry(ctx *core.Context, request *core.Request, response *core.Response) {
 	user := ctx.LoggedUser()
-	sortingDirection := model.DefaultSortingDirection
 
 	entryID, err := request.IntegerParam("entryID")
 	if err != nil {
@@ -320,26 +267,9 @@ func (c *Controller) ShowReadEntry(ctx *core.Context, request *core.Request, res
 	}
 
 	builder = c.store.GetEntryQueryBuilder(user.ID, user.Timezone)
-	builder.WithoutStatus(model.EntryStatusRemoved)
 	builder.WithStatus(model.EntryStatusRead)
-	builder.WithCondition("e.id", "!=", entryID)
-	builder.WithCondition("e.published_at", "<=", entry.Date)
-	builder.WithOrder(model.DefaultSortingOrder)
-	builder.WithDirection(sortingDirection)
-	nextEntry, err := builder.GetEntry()
-	if err != nil {
-		response.HTML().ServerError(err)
-		return
-	}
 
-	builder = c.store.GetEntryQueryBuilder(user.ID, user.Timezone)
-	builder.WithoutStatus(model.EntryStatusRemoved)
-	builder.WithStatus(model.EntryStatusRead)
-	builder.WithCondition("e.id", "!=", entryID)
-	builder.WithCondition("e.published_at", ">=", entry.Date)
-	builder.WithOrder(model.DefaultSortingOrder)
-	builder.WithDirection(model.GetOppositeDirection(sortingDirection))
-	prevEntry, err := builder.GetEntry()
+	prevEntry, nextEntry, err := c.getEntryPrevNext(user, builder, entry.ID)
 	if err != nil {
 		response.HTML().ServerError(err)
 		return
@@ -390,3 +320,29 @@ func (c *Controller) UpdateEntriesStatus(ctx *core.Context, request *core.Reques
 
 	response.JSON().Standard("OK")
 }
+
+func (c *Controller) getEntryPrevNext(user *model.User, builder *storage.EntryQueryBuilder, entryID int64) (prev *model.Entry, next *model.Entry, err error) {
+	builder.WithoutStatus(model.EntryStatusRemoved)
+	builder.WithOrder(model.DefaultSortingOrder)
+	builder.WithDirection(user.EntryDirection)
+
+	entries, err := builder.GetEntries()
+	if err != nil {
+		return nil, nil, err
+	}
+
+	n := len(entries)
+	for i := 0; i < n; i++ {
+		if entries[i].ID == entryID {
+			if i-1 > 0 {
+				prev = entries[i-1]
+			}
+
+			if i+1 < n {
+				next = entries[i+1]
+			}
+		}
+	}
+
+	return prev, next, nil
+}

+ 1 - 1
server/ui/controller/feed.go

@@ -72,7 +72,7 @@ func (c *Controller) ShowFeedEntries(ctx *core.Context, request *core.Request, r
 	builder.WithFeedID(feed.ID)
 	builder.WithoutStatus(model.EntryStatusRemoved)
 	builder.WithOrder(model.DefaultSortingOrder)
-	builder.WithDirection(model.DefaultSortingDirection)
+	builder.WithDirection(user.EntryDirection)
 	builder.WithOffset(offset)
 	builder.WithLimit(nbItemsPerPage)
 

+ 1 - 1
server/ui/controller/history.go

@@ -23,7 +23,7 @@ func (c *Controller) ShowHistoryPage(ctx *core.Context, request *core.Request, r
 	builder := c.store.GetEntryQueryBuilder(user.ID, user.Timezone)
 	builder.WithStatus(model.EntryStatusRead)
 	builder.WithOrder(model.DefaultSortingOrder)
-	builder.WithDirection(model.DefaultSortingDirection)
+	builder.WithDirection(user.EntryDirection)
 	builder.WithOffset(offset)
 	builder.WithLimit(nbItemsPerPage)
 

+ 5 - 4
server/ui/controller/settings.go

@@ -74,10 +74,11 @@ func (c *Controller) getSettingsFormTemplateArgs(ctx *core.Context, user *model.
 
 	if settingsForm == nil {
 		args["form"] = form.SettingsForm{
-			Username: user.Username,
-			Theme:    user.Theme,
-			Language: user.Language,
-			Timezone: user.Timezone,
+			Username:       user.Username,
+			Theme:          user.Theme,
+			Language:       user.Language,
+			Timezone:       user.Timezone,
+			EntryDirection: user.EntryDirection,
 		}
 	} else {
 		args["form"] = settingsForm

+ 1 - 1
server/ui/controller/unread.go

@@ -17,7 +17,7 @@ func (c *Controller) ShowUnreadPage(ctx *core.Context, request *core.Request, re
 	builder := c.store.GetEntryQueryBuilder(user.ID, user.Timezone)
 	builder.WithStatus(model.EntryStatusUnread)
 	builder.WithOrder(model.DefaultSortingOrder)
-	builder.WithDirection(model.DefaultSortingDirection)
+	builder.WithDirection(user.EntryDirection)
 	builder.WithOffset(offset)
 	builder.WithLimit(nbItemsPerPage)
 

+ 16 - 13
server/ui/form/settings.go

@@ -13,12 +13,13 @@ import (
 
 // SettingsForm represents the settings form.
 type SettingsForm struct {
-	Username     string
-	Password     string
-	Confirmation string
-	Theme        string
-	Language     string
-	Timezone     string
+	Username       string
+	Password       string
+	Confirmation   string
+	Theme          string
+	Language       string
+	Timezone       string
+	EntryDirection string
 }
 
 // Merge updates the fields of the given user.
@@ -27,6 +28,7 @@ func (s *SettingsForm) Merge(user *model.User) *model.User {
 	user.Theme = s.Theme
 	user.Language = s.Language
 	user.Timezone = s.Timezone
+	user.EntryDirection = s.EntryDirection
 
 	if s.Password != "" {
 		user.Password = s.Password
@@ -37,7 +39,7 @@ func (s *SettingsForm) Merge(user *model.User) *model.User {
 
 // Validate makes sure the form values are valid.
 func (s *SettingsForm) Validate() error {
-	if s.Username == "" || s.Theme == "" || s.Language == "" || s.Timezone == "" {
+	if s.Username == "" || s.Theme == "" || s.Language == "" || s.Timezone == "" || s.EntryDirection == "" {
 		return errors.NewLocalizedError("The username, theme, language and timezone fields are mandatory.")
 	}
 
@@ -57,11 +59,12 @@ func (s *SettingsForm) Validate() error {
 // NewSettingsForm returns a new SettingsForm.
 func NewSettingsForm(r *http.Request) *SettingsForm {
 	return &SettingsForm{
-		Username:     r.FormValue("username"),
-		Password:     r.FormValue("password"),
-		Confirmation: r.FormValue("confirmation"),
-		Theme:        r.FormValue("theme"),
-		Language:     r.FormValue("language"),
-		Timezone:     r.FormValue("timezone"),
+		Username:       r.FormValue("username"),
+		Password:       r.FormValue("password"),
+		Confirmation:   r.FormValue("confirmation"),
+		Theme:          r.FormValue("theme"),
+		Language:       r.FormValue("language"),
+		Timezone:       r.FormValue("timezone"),
+		EntryDirection: r.FormValue("entry_direction"),
 	}
 }

+ 1 - 1
sql/schema_version_1.sql

@@ -53,7 +53,7 @@ create table feeds (
     foreign key (category_id) references categories(id) on delete cascade
 );
 
-create type entry_status as enum ('unread', 'read', 'removed');
+create type entry_status as enum('unread', 'read', 'removed');
 
 create table entries (
     id bigserial not null,

+ 2 - 0
sql/schema_version_4.sql

@@ -0,0 +1,2 @@
+create type entry_sorting_direction as enum('asc', 'desc');
+alter table users add column entry_direction entry_sorting_direction default 'asc';

+ 7 - 3
sql/sql.go

@@ -1,5 +1,5 @@
 // Code generated by go generate; DO NOT EDIT.
-// 2017-12-01 21:46:13.639273113 -0800 PST m=+0.002204900
+// 2017-12-02 16:12:47.256865279 -0800 PST m=+0.002374595
 
 package sql
 
@@ -59,7 +59,7 @@ create table feeds (
     foreign key (category_id) references categories(id) on delete cascade
 );
 
-create type entry_status as enum ('unread', 'read', 'removed');
+create type entry_status as enum('unread', 'read', 'removed');
 
 create table entries (
     id bigserial not null,
@@ -118,10 +118,14 @@ create index users_extra_idx on users using gin(extra);
     created_at timestamp with time zone not null default now(),
     primary key(id, value)
 );`,
+	"schema_version_4": `create type entry_sorting_direction as enum('asc', 'desc');
+alter table users add column entry_direction entry_sorting_direction default 'asc';
+`,
 }
 
 var SqlMapChecksums = map[string]string{
-	"schema_version_1": "cb85ca7dd97a6e1348e00b65ea004253a7165bed9a772746613276e47ef93213",
+	"schema_version_1": "7be580fc8a93db5da54b2f6e87019803c33b0b0c28482c7af80cef873bdac4e2",
 	"schema_version_2": "e8e9ff32478df04fcddad10a34cba2e8bb1e67e7977b5bd6cdc4c31ec94282b4",
 	"schema_version_3": "a54745dbc1c51c000f74d4e5068f1e2f43e83309f023415b1749a47d5c1e0f12",
+	"schema_version_4": "216ea3a7d3e1704e40c797b5dc47456517c27dbb6ca98bf88812f4f63d74b5d9",
 }

+ 1 - 15
storage/entry_query_builder.go

@@ -27,15 +27,6 @@ type EntryQueryBuilder struct {
 	limit      int
 	offset     int
 	entryID    int64
-	conditions []string
-	args       []interface{}
-}
-
-// WithCondition defines a new condition.
-func (e *EntryQueryBuilder) WithCondition(column, operator string, value interface{}) *EntryQueryBuilder {
-	e.args = append(e.args, value)
-	e.conditions = append(e.conditions, fmt.Sprintf("%s %s $%d", column, operator, len(e.args)+1))
-	return e
 }
 
 // WithEntryID set the entryID.
@@ -187,7 +178,7 @@ func (e *EntryQueryBuilder) GetEntries() (model.Entries, error) {
 		)
 
 		if err != nil {
-			return nil, fmt.Errorf("Unable to fetch entry row: %v", err)
+			return nil, fmt.Errorf("unable to fetch entry row: %v", err)
 		}
 
 		if iconID == nil {
@@ -208,11 +199,6 @@ func (e *EntryQueryBuilder) buildCondition() ([]interface{}, string) {
 	args := []interface{}{e.userID}
 	conditions := []string{"e.user_id = $1"}
 
-	if len(e.conditions) > 0 {
-		conditions = append(conditions, e.conditions...)
-		args = append(args, e.args...)
-	}
-
 	if e.categoryID != 0 {
 		conditions = append(conditions, fmt.Sprintf("f.category_id=$%d", len(args)+1))
 		args = append(args, e.categoryID)

+ 1 - 1
storage/migration.go

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

+ 93 - 15
storage/user.go

@@ -8,6 +8,7 @@ import (
 	"database/sql"
 	"errors"
 	"fmt"
+	"log"
 	"strings"
 	"time"
 
@@ -110,23 +111,59 @@ func (s *Storage) RemoveExtraField(userID int64, field string) error {
 
 // UpdateUser updates a user.
 func (s *Storage) UpdateUser(user *model.User) error {
-	defer helper.ExecutionTime(time.Now(), fmt.Sprintf("[Storage:UpdateUser] username=%s", user.Username))
-	user.Username = strings.ToLower(user.Username)
-
+	defer helper.ExecutionTime(time.Now(), fmt.Sprintf("[Storage:UpdateUser] userID=%d", user.ID))
+	log.Println(user.EntryDirection)
 	if user.Password != "" {
 		hashedPassword, err := hashPassword(user.Password)
 		if err != nil {
 			return err
 		}
 
-		query := "UPDATE users SET username=$1, password=$2, is_admin=$3, theme=$4, language=$5, timezone=$6 WHERE id=$7"
-		_, err = s.db.Exec(query, user.Username, hashedPassword, user.IsAdmin, user.Theme, user.Language, user.Timezone, user.ID)
+		query := `UPDATE users SET
+			username=LOWER($1),
+			password=$2,
+			is_admin=$3,
+			theme=$4,
+			language=$5,
+			timezone=$6,
+			entry_direction=$7
+			WHERE id=$8`
+
+		_, err = s.db.Exec(
+			query,
+			user.Username,
+			hashedPassword,
+			user.IsAdmin,
+			user.Theme,
+			user.Language,
+			user.Timezone,
+			user.EntryDirection,
+			user.ID,
+		)
 		if err != nil {
 			return fmt.Errorf("unable to update user: %v", err)
 		}
 	} else {
-		query := "UPDATE users SET username=$1, is_admin=$2, theme=$3, language=$4, timezone=$5 WHERE id=$6"
-		_, err := s.db.Exec(query, user.Username, user.IsAdmin, user.Theme, user.Language, user.Timezone, user.ID)
+		query := `UPDATE users SET
+			username=$1,
+			is_admin=$2,
+			theme=$3,
+			language=$4,
+			timezone=$5,
+			entry_direction=$6
+			WHERE id=$7`
+
+		_, err := s.db.Exec(
+			query,
+			user.Username,
+			user.IsAdmin,
+			user.Theme,
+			user.Language,
+			user.Timezone,
+			user.EntryDirection,
+			user.ID,
+		)
+
 		if err != nil {
 			return fmt.Errorf("unable to update user: %v", err)
 		}
@@ -138,11 +175,24 @@ func (s *Storage) UpdateUser(user *model.User) error {
 // UserByID finds a user by the ID.
 func (s *Storage) UserByID(userID int64) (*model.User, error) {
 	defer helper.ExecutionTime(time.Now(), fmt.Sprintf("[Storage:UserByID] userID=%d", userID))
+	query := `SELECT
+		id, username, is_admin, theme, language, timezone, entry_direction, extra
+		FROM users
+		WHERE id = $1`
 
 	var user model.User
 	var extra hstore.Hstore
-	row := s.db.QueryRow("SELECT id, username, is_admin, theme, language, timezone, extra FROM users WHERE id = $1", userID)
-	err := row.Scan(&user.ID, &user.Username, &user.IsAdmin, &user.Theme, &user.Language, &user.Timezone, &extra)
+	row := s.db.QueryRow(query, userID)
+	err := row.Scan(
+		&user.ID,
+		&user.Username,
+		&user.IsAdmin,
+		&user.Theme,
+		&user.Language,
+		&user.Timezone,
+		&user.EntryDirection,
+		&extra,
+	)
 	if err == sql.ErrNoRows {
 		return nil, nil
 	} else if err != nil {
@@ -162,10 +212,22 @@ func (s *Storage) UserByID(userID int64) (*model.User, error) {
 // UserByUsername finds a user by the username.
 func (s *Storage) UserByUsername(username string) (*model.User, error) {
 	defer helper.ExecutionTime(time.Now(), fmt.Sprintf("[Storage:UserByUsername] username=%s", username))
+	query := `SELECT
+		id, username, is_admin, theme, language, timezone, entry_direction
+		FROM users
+		WHERE username=LOWER($1)`
 
 	var user model.User
-	row := s.db.QueryRow("SELECT id, username, is_admin, theme, language, timezone FROM users WHERE username=$1", username)
-	err := row.Scan(&user.ID, &user.Username, &user.IsAdmin, &user.Theme, &user.Language, &user.Timezone)
+	row := s.db.QueryRow(query, username)
+	err := row.Scan(
+		&user.ID,
+		&user.Username,
+		&user.IsAdmin,
+		&user.Theme,
+		&user.Language,
+		&user.Timezone,
+		&user.EntryDirection,
+	)
 	if err == sql.ErrNoRows {
 		return nil, nil
 	} else if err != nil {
@@ -178,10 +240,22 @@ func (s *Storage) UserByUsername(username string) (*model.User, error) {
 // UserByExtraField finds a user by an extra field value.
 func (s *Storage) UserByExtraField(field, value string) (*model.User, error) {
 	defer helper.ExecutionTime(time.Now(), fmt.Sprintf("[Storage:UserByExtraField] field=%s", field))
+	query := `SELECT
+		id, username, is_admin, theme, language, timezone, entry_direction
+		FROM users
+		WHERE extra->$1=$2`
+
 	var user model.User
-	query := `SELECT id, username, is_admin, theme, language, timezone FROM users WHERE extra->$1=$2`
 	row := s.db.QueryRow(query, field, value)
-	err := row.Scan(&user.ID, &user.Username, &user.IsAdmin, &user.Theme, &user.Language, &user.Timezone)
+	err := row.Scan(
+		&user.ID,
+		&user.Username,
+		&user.IsAdmin,
+		&user.Theme,
+		&user.Language,
+		&user.Timezone,
+		&user.EntryDirection,
+	)
 	if err == sql.ErrNoRows {
 		return nil, nil
 	} else if err != nil {
@@ -215,14 +289,18 @@ func (s *Storage) RemoveUser(userID int64) error {
 // Users returns all users.
 func (s *Storage) Users() (model.Users, error) {
 	defer helper.ExecutionTime(time.Now(), "[Storage:Users]")
+	query := `SELECT
+		id, username, is_admin, theme, language, timezone, last_login_at
+		FROM users
+		ORDER BY username ASC`
 
-	var users model.Users
-	rows, err := s.db.Query("SELECT id, username, is_admin, theme, language, timezone, last_login_at FROM users ORDER BY username ASC")
+	rows, err := s.db.Query(query)
 	if err != nil {
 		return nil, fmt.Errorf("unable to fetch users: %v", err)
 	}
 	defer rows.Close()
 
+	var users model.Users
 	for rows.Next() {
 		var user model.User
 		err := rows.Scan(

Einige Dateien werden nicht angezeigt, da zu viele Dateien in diesem Diff geändert wurden.