aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorGravatar Frédéric Guillot <fred@miniflux.net>2017-12-02 17:04:01 -0800
committerGravatar Frédéric Guillot <fred@miniflux.net>2017-12-02 17:04:01 -0800
commit2f1367a8d4c33e7c6ba459cfc6756e079c7a1af4 (patch)
tree04a2c15d41f79c05b74b67c9bbab4bfb483be6e1
parent453ff64f29c5ca330b20fb9c3dd41bbf7e7b7396 (diff)
Make entries sorting configurable
-rw-r--r--README.md9
-rw-r--r--locale/translations.go9
-rw-r--r--locale/translations/fr_FR.json5
-rw-r--r--model/entry.go6
-rw-r--r--model/entry_test.go6
-rw-r--r--model/token.go6
-rw-r--r--model/user.go19
-rw-r--r--server/static/bin.go2
-rw-r--r--server/static/css.go6
-rw-r--r--server/static/css/common.css2
-rw-r--r--server/static/js.go2
-rw-r--r--server/template/common.go8
-rw-r--r--server/template/html/common/layout.html4
-rw-r--r--server/template/html/settings.html6
-rw-r--r--server/template/views.go10
-rw-r--r--server/ui/controller/category.go2
-rw-r--r--server/ui/controller/entry.go160
-rw-r--r--server/ui/controller/feed.go2
-rw-r--r--server/ui/controller/history.go2
-rw-r--r--server/ui/controller/settings.go9
-rw-r--r--server/ui/controller/unread.go2
-rw-r--r--server/ui/form/settings.go29
-rw-r--r--sql/schema_version_1.sql2
-rw-r--r--sql/schema_version_4.sql2
-rw-r--r--sql/sql.go10
-rw-r--r--storage/entry_query_builder.go16
-rw-r--r--storage/migration.go2
-rw-r--r--storage/user.go108
28 files changed, 253 insertions, 193 deletions
diff --git a/README.md b/README.md
index 807042d..9926dae 100644
--- a/README.md
+++ b/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
diff --git a/locale/translations.go b/locale/translations.go
index b9164fa..9553018 100644
--- a/locale/translations.go
+++ b/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",
}
diff --git a/locale/translations/fr_FR.json b/locale/translations/fr_FR.json
index 3b774cf..08f0729 100644
--- a/locale/translations/fr_FR.json
+++ b/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"
}
diff --git a/model/entry.go b/model/entry.go
index 79a3bcb..f9a9638 100644
--- a/model/entry.go
+++ b/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"
}
diff --git a/model/entry_test.go b/model/entry_test.go
index 2f8c25d..8b92d3f 100644
--- a/model/entry_test.go
+++ b/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"`)
}
}
diff --git a/model/token.go b/model/token.go
index 5626a77..3c5c323 100644
--- a/model/token.go
+++ b/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)
+}
diff --git a/model/user.go b/model/user.go
index c030d52..4889dc4 100644
--- a/model/user.go
+++ b/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.
diff --git a/server/static/bin.go b/server/static/bin.go
index 9987f8a..21cd59f 100644
--- a/server/static/bin.go
+++ b/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
diff --git a/server/static/css.go b/server/static/css.go
index e338124..d30f275 100644
--- a/server/static/css.go
+++ b/server/static/css.go
@@ -1,14 +1,14 @@
// Code generated by go generate; DO NOT EDIT.
-// 2017-11-29 21:35:36.026428194 -0800 PST m=+0.009240880
+// 2017-12-02 16:12:47.265273764 -0800 PST m=+0.010783080
package static
var Stylesheets = map[string]string{
"black": `body{background:#222;color:#efefef}h1,h2,h3{color:#aaa}a{color:#aaa}a:focus,a:hover{color:#ddd}.header li{border-color:#333}.header a{color:#ddd;font-weight:400}.header .active a{font-weight:400;color:#9b9494}.header a:focus,.header a:hover{color:rgba(82,168,236,.85)}.page-header h1{border-color:#333}.logo a:hover span{color:#555}table,th,td{border:1px solid #555}th{background:#333;color:#aaa;font-weight:400}tr:hover{background-color:#333;color:#aaa}input[type=url],input[type=password],input[type=text]{border:1px solid #555;background:#333;color:#ccc}input[type=url]:focus,input[type=password]:focus,input[type=text]:focus{color:#efefef;border-color:rgba(82,168,236,.8);box-shadow:0 0 8px rgba(82,168,236,.6)}.button-primary{border-color:#444;background:#333;color:#efefef}.button-primary:hover,.button-primary:focus{border-color:#888;background:#555}.alert,.alert-success,.alert-error,.alert-info,.alert-normal{color:#efefef;background-color:#333;border-color:#444}.panel{background:#333;border-color:#555}.unread-counter{color:#bbb}.category{color:#efefef;background-color:#333;border-color:#444}.category a{color:#999}.category a:hover,.category a:focus{color:#aaa}.pagination a{color:#aaa}.pagination-bottom{border-color:#333}.item{border-color:#666;padding:4px}.item.current-item{border-width:2px;border-color:rgba(82,168,236,.8);box-shadow:0 0 8px rgba(82,168,236,.6)}.item-title a{font-weight:400}.item-status-read .item-title a{color:#666}.item-status-read .item-title a:focus,.item-status-read .item-title a:hover{color:rgba(82,168,236,.6)}.item-meta a:hover,.item-meta a:focus{color:#aaa}.item-meta li:after{color:#ddd}.entry header{border-color:#333}.entry header h1 a{color:#bbb}.entry-content,.entry-content p,ul{color:#999}.entry-content pre,.entry-content code{color:#fff;background:#555;border-color:#888}.entry-enclosure{border-color:#333}`,
- "common": `*{margin:0;padding:0;box-sizing:border-box}body{font-family:helvetica neue,Helvetica,Arial,sans-serif;text-rendering:optimizeLegibility}.main{padding-left:5px;padding-right:5px}a{color:#36c}a:focus{outline:0;color:red;text-decoration:none;border:1px dotted #aaa}a:hover{color:#333;text-decoration:none}.header{margin-top:10px;margin-bottom:20px}.header nav ul{display:none}.header li{cursor:pointer;padding-left:10px;line-height:2.1em;font-size:1.2em;border-bottom:1px dotted #ddd}.header li:hover a{color:#888}.header a{font-size:.9em;color:#444;text-decoration:none;border:0}.header .active a{font-weight:600}.header a:hover,.header a:focus{color:#888}.page-header{margin-bottom:25px}.page-header h1{font-weight:500;border-bottom:1px dotted #ddd}.page-header ul{margin-left:25px}.page-header li{list-style-type:circle;line-height:1.8em}.logo{cursor:pointer;text-align:center}.logo a{color:#000;letter-spacing:1px}.logo a:hover{color:#396}.logo a span{color:#396}.logo a:hover span{color:#000}@media(min-width:600px){body{margin:auto;max-width:750px}.logo{text-align:left;float:left;margin-right:15px}.header nav ul{display:block}.header li{display:inline;padding:0;padding-right:15px;line-height:normal;border:0;font-size:1em}.page-header ul{margin-left:0}.page-header li{display:inline;padding-right:15px}}table{width:100%;border-collapse:collapse}table,th,td{border:1px solid #ddd}th,td{padding:5px;text-align:left}td{vertical-align:top}th{background:#fcfcfc}tr:hover{background-color:#f9f9f9}.column-40{width:40%}.column-25{width:25%}.column-20{width:20%}label{cursor:pointer;display:block}.radio-group{line-height:1.9em}div.radio-group label{display:inline-block}select{margin-bottom:15px}input[type=url],input[type=password],input[type=text]{border:1px solid #ccc;padding:3px;line-height:20px;width:250px;font-size:99%;margin-bottom:10px;margin-top:5px;-webkit-appearance:none}input[type=url]:focus,input[type=password]:focus,input[type=text]:focus{color:#000;border-color:rgba(82,168,236,.8);outline:0;box-shadow:0 0 8px rgba(82,168,236,.6)}::-moz-placeholder,::-ms-input-placeholder,::-webkit-input-placeholder{color:#ddd;padding-top:2px}.form-help{font-size:.9em;color:brown;margin-bottom:15px}a.button{text-decoration:none}.button{display:inline-block;-webkit-appearance:none;-moz-appearance:none;font-size:1.1em;cursor:pointer;padding:3px 10px;border:1px solid;border-radius:unset}.button-primary{border-color:#3079ed;background:#4d90fe;color:#fff}.button-primary:hover,.button-primary:focus{border-color:#2f5bb7;background:#357ae8}.button-danger{border-color:#b0281a;background:#d14836;color:#fff}.button-danger:hover,.button-danger:focus{color:#fff;background:#c53727}.button:disabled{color:#ccc;background:#f7f7f7;border-color:#ccc}.buttons{margin-top:10px;margin-bottom:20px}.alert{padding:8px 35px 8px 14px;margin-bottom:20px;color:#c09853;background-color:#fcf8e3;border:1px solid #fbeed5;border-radius:4px;overflow:auto}.alert h3{margin-top:0;margin-bottom:15px}.alert-success{color:#468847;background-color:#dff0d8;border-color:#d6e9c6}.alert-error{color:#b94a48;background-color:#f2dede;border-color:#eed3d7}.alert-error a{color:#b94a48}.alert-info{color:#3a87ad;background-color:#d9edf7;border-color:#bce8f1}.panel{color:#333;background-color:#f0f0f0;border:1px solid #ddd;border-radius:5px;padding:10px;margin-bottom:15px}.panel h3{font-weight:500;margin-top:0;margin-bottom:20px}.panel ul{margin-left:30px}.login-form{margin:50px auto 0;max-width:280px}.unread-counter{font-size:.8em;font-weight:300;color:#666}.category{font-size:.75em;background-color:#fffcd7;border:1px solid #d5d458;border-radius:5px;margin-left:.25em;padding:1px .4em;white-space:nowrap}.category a{color:#555;text-decoration:none}.category a:hover,.category a:focus{color:#000}.pagination{font-size:1.1em;display:flex;align-items:center;padding-top:8px}.pagination-bottom{border-top:1px dotted #ddd;margin-bottom:15px;margin-top:50px}.pagination>div{flex:1}.pagination-next{text-align:right}.pagination-prev:before{content:"« "}.pagination-next:after{content:" »"}.pagination a{color:#333}.pagination a:hover,.pagination a:focus{text-decoration:none}.item{border:1px dotted #ddd;margin-bottom:20px;padding:5px;overflow:hidden}.item.current-item{border:3px solid #bce;padding:3px}.item-title a{text-decoration:none;font-weight:600}.item-status-read .item-title a{color:#777}.item-meta{color:#777;font-size:.8em}.item-meta a{color:#777;text-decoration:none}.item-meta a:hover,.item-meta a:focus{color:#333}.item-meta ul{margin-top:5px}.item-meta li{display:inline}.item-meta li:after{content:"|";color:#aaa}.item-meta li:last-child:after{content:""}.hide-read-items .item-status-read{display:none}.entry header{padding-bottom:5px;border-bottom:1px dotted #ddd}.entry header h1{font-size:2em;line-height:1.25em;margin:30px 0}.entry header h1 a{text-decoration:none;color:#333}.entry header h1 a:hover,.entry header h1 a:focus{color:#666}.entry-meta{font-size:.95em;margin:0 0 20px;color:#666;overflow-wrap:break-word}.entry-website img{vertical-align:top}.entry-website a{color:#666;vertical-align:top;text-decoration:none}.entry-website a:hover,.entry-website a:focus{text-decoration:underline}.entry-date{font-size:.65em;font-style:italic;color:#555}.entry-content{padding-top:15px;font-size:1.2em;font-weight:300;font-family:Georgia,times new roman,Times,serif;color:#555;line-height:1.4em;overflow-wrap:break-word}.entry-content h1,h2,h3,h4,h5,h6{margin-top:15px;margin-bottom:10px}.entry-content iframe,.entry-content video,.entry-content img{max-width:100%}.entry-content figure img{border:1px solid #000}.entry-content figcaption{font-size:.75em;text-transform:uppercase;color:#777}.entry-content p{margin-top:10px;margin-bottom:15px}.entry-content a{overflow-wrap:break-word}.entry-content a:visited{color:purple}.entry-content dt{font-weight:500;margin-top:15px;color:#555}.entry-content dd{margin-left:15px;margin-top:5px;padding-left:20px;border-left:3px solid #ddd;color:#777;font-weight:300;line-height:1.4em}.entry-content blockquote{border-left:4px solid #ddd;padding-left:25px;margin-left:20px;margin-top:20px;margin-bottom:20px;color:#888;line-height:1.4em;font-family:Georgia,serif}.entry-content blockquote+p{color:#555;font-style:italic;font-weight:200}.entry-content q{color:purple;font-family:Georgia,serif;font-style:italic}.entry-content q:before{content:"“"}.entry-content q:after{content:"”"}.entry-content pre{padding:5px;background:#f0f0f0;border:1px solid #ddd;overflow:scroll;overflow-wrap:initial}.entry-content table{table-layout:fixed;max-width:100%}.entry-content ul,.entry-content ol{margin-left:30px}.entry-content ul{list-style-type:square}.entry-enclosures h3{font-weight:500}.entry-enclosure{border:1px dotted #ddd;padding:5px;margin-top:10px;max-width:100%}.entry-enclosure-download{font-size:.85em}.enclosure-video video,.enclosure-image img{max-width:100%}.confirm{font-weight:500;color:#ed2d04}.confirm a{color:#ed2d04}.loading{font-style:italic}.bookmarklet{border:1px dashed #ccc;border-radius:5px;padding:15px;margin:15px;text-align:center}.bookmarklet a{font-weight:600;text-decoration:none;font-size:1.2em}`,
+ "common": `*{margin:0;padding:0;box-sizing:border-box}body{font-family:helvetica neue,Helvetica,Arial,sans-serif;text-rendering:optimizeLegibility}main{padding-left:5px;padding-right:5px}a{color:#36c}a:focus{outline:0;color:red;text-decoration:none;border:1px dotted #aaa}a:hover{color:#333;text-decoration:none}.header{margin-top:10px;margin-bottom:20px}.header nav ul{display:none}.header li{cursor:pointer;padding-left:10px;line-height:2.1em;font-size:1.2em;border-bottom:1px dotted #ddd}.header li:hover a{color:#888}.header a{font-size:.9em;color:#444;text-decoration:none;border:0}.header .active a{font-weight:600}.header a:hover,.header a:focus{color:#888}.page-header{margin-bottom:25px}.page-header h1{font-weight:500;border-bottom:1px dotted #ddd}.page-header ul{margin-left:25px}.page-header li{list-style-type:circle;line-height:1.8em}.logo{cursor:pointer;text-align:center}.logo a{color:#000;letter-spacing:1px}.logo a:hover{color:#396}.logo a span{color:#396}.logo a:hover span{color:#000}@media(min-width:600px){body{margin:auto;max-width:750px}.logo{text-align:left;float:left;margin-right:15px}.header nav ul{display:block}.header li{display:inline;padding:0;padding-right:15px;line-height:normal;border:0;font-size:1em}.page-header ul{margin-left:0}.page-header li{display:inline;padding-right:15px}}table{width:100%;border-collapse:collapse}table,th,td{border:1px solid #ddd}th,td{padding:5px;text-align:left}td{vertical-align:top}th{background:#fcfcfc}tr:hover{background-color:#f9f9f9}.column-40{width:40%}.column-25{width:25%}.column-20{width:20%}label{cursor:pointer;display:block}.radio-group{line-height:1.9em}div.radio-group label{display:inline-block}select{margin-bottom:15px}input[type=url],input[type=password],input[type=text]{border:1px solid #ccc;padding:3px;line-height:20px;width:250px;font-size:99%;margin-bottom:10px;margin-top:5px;-webkit-appearance:none}input[type=url]:focus,input[type=password]:focus,input[type=text]:focus{color:#000;border-color:rgba(82,168,236,.8);outline:0;box-shadow:0 0 8px rgba(82,168,236,.6)}::-moz-placeholder,::-ms-input-placeholder,::-webkit-input-placeholder{color:#ddd;padding-top:2px}.form-help{font-size:.9em;color:brown;margin-bottom:15px}a.button{text-decoration:none}.button{display:inline-block;-webkit-appearance:none;-moz-appearance:none;font-size:1.1em;cursor:pointer;padding:3px 10px;border:1px solid;border-radius:unset}.button-primary{border-color:#3079ed;background:#4d90fe;color:#fff}.button-primary:hover,.button-primary:focus{border-color:#2f5bb7;background:#357ae8}.button-danger{border-color:#b0281a;background:#d14836;color:#fff}.button-danger:hover,.button-danger:focus{color:#fff;background:#c53727}.button:disabled{color:#ccc;background:#f7f7f7;border-color:#ccc}.buttons{margin-top:10px;margin-bottom:20px}.alert{padding:8px 35px 8px 14px;margin-bottom:20px;color:#c09853;background-color:#fcf8e3;border:1px solid #fbeed5;border-radius:4px;overflow:auto}.alert h3{margin-top:0;margin-bottom:15px}.alert-success{color:#468847;background-color:#dff0d8;border-color:#d6e9c6}.alert-error{color:#b94a48;background-color:#f2dede;border-color:#eed3d7}.alert-error a{color:#b94a48}.alert-info{color:#3a87ad;background-color:#d9edf7;border-color:#bce8f1}.panel{color:#333;background-color:#f0f0f0;border:1px solid #ddd;border-radius:5px;padding:10px;margin-bottom:15px}.panel h3{font-weight:500;margin-top:0;margin-bottom:20px}.panel ul{margin-left:30px}.login-form{margin:50px auto 0;max-width:280px}.unread-counter{font-size:.8em;font-weight:300;color:#666}.category{font-size:.75em;background-color:#fffcd7;border:1px solid #d5d458;border-radius:5px;margin-left:.25em;padding:1px .4em;white-space:nowrap}.category a{color:#555;text-decoration:none}.category a:hover,.category a:focus{color:#000}.pagination{font-size:1.1em;display:flex;align-items:center;padding-top:8px}.pagination-bottom{border-top:1px dotted #ddd;margin-bottom:15px;margin-top:50px}.pagination>div{flex:1}.pagination-next{text-align:right}.pagination-prev:before{content:"« "}.pagination-next:after{content:" »"}.pagination a{color:#333}.pagination a:hover,.pagination a:focus{text-decoration:none}.item{border:1px dotted #ddd;margin-bottom:20px;padding:5px;overflow:hidden}.item.current-item{border:3px solid #bce;padding:3px}.item-title a{text-decoration:none;font-weight:600}.item-status-read .item-title a{color:#777}.item-meta{color:#777;font-size:.8em}.item-meta a{color:#777;text-decoration:none}.item-meta a:hover,.item-meta a:focus{color:#333}.item-meta ul{margin-top:5px}.item-meta li{display:inline}.item-meta li:after{content:"|";color:#aaa}.item-meta li:last-child:after{content:""}.hide-read-items .item-status-read{display:none}.entry header{padding-bottom:5px;border-bottom:1px dotted #ddd}.entry header h1{font-size:2em;line-height:1.25em;margin:30px 0}.entry header h1 a{text-decoration:none;color:#333}.entry header h1 a:hover,.entry header h1 a:focus{color:#666}.entry-meta{font-size:.95em;margin:0 0 20px;color:#666;overflow-wrap:break-word}.entry-website img{vertical-align:top}.entry-website a{color:#666;vertical-align:top;text-decoration:none}.entry-website a:hover,.entry-website a:focus{text-decoration:underline}.entry-date{font-size:.65em;font-style:italic;color:#555}.entry-content{padding-top:15px;font-size:1.2em;font-weight:300;font-family:Georgia,times new roman,Times,serif;color:#555;line-height:1.4em;overflow-wrap:break-word}.entry-content h1,h2,h3,h4,h5,h6{margin-top:15px;margin-bottom:10px}.entry-content iframe,.entry-content video,.entry-content img{max-width:100%}.entry-content figure img{border:1px solid #000}.entry-content figcaption{font-size:.75em;text-transform:uppercase;color:#777}.entry-content p{margin-top:10px;margin-bottom:15px}.entry-content a{overflow-wrap:break-word}.entry-content a:visited{color:purple}.entry-content dt{font-weight:500;margin-top:15px;color:#555}.entry-content dd{margin-left:15px;margin-top:5px;padding-left:20px;border-left:3px solid #ddd;color:#777;font-weight:300;line-height:1.4em}.entry-content blockquote{border-left:4px solid #ddd;padding-left:25px;margin-left:20px;margin-top:20px;margin-bottom:20px;color:#888;line-height:1.4em;font-family:Georgia,serif}.entry-content blockquote+p{color:#555;font-style:italic;font-weight:200}.entry-content q{color:purple;font-family:Georgia,serif;font-style:italic}.entry-content q:before{content:"“"}.entry-content q:after{content:"”"}.entry-content pre{padding:5px;background:#f0f0f0;border:1px solid #ddd;overflow:scroll;overflow-wrap:initial}.entry-content table{table-layout:fixed;max-width:100%}.entry-content ul,.entry-content ol{margin-left:30px}.entry-content ul{list-style-type:square}.entry-enclosures h3{font-weight:500}.entry-enclosure{border:1px dotted #ddd;padding:5px;margin-top:10px;max-width:100%}.entry-enclosure-download{font-size:.85em}.enclosure-video video,.enclosure-image img{max-width:100%}.confirm{font-weight:500;color:#ed2d04}.confirm a{color:#ed2d04}.loading{font-style:italic}.bookmarklet{border:1px dashed #ccc;border-radius:5px;padding:15px;margin:15px;text-align:center}.bookmarklet a{font-weight:600;text-decoration:none;font-size:1.2em}`,
}
var StylesheetsChecksums = map[string]string{
"black": "38e7fee92187a036ce37f3c15fde2deff59a55c5ab693c7b8578af79d6a117d2",
- "common": "c0c71c5451d0d22d2826b0525c33f37e54a8f20d826eb182aac0b5636019056c",
+ "common": "921e622bf833cb8a843766b4bf71263e4cf8cc4a0c8678692b363a2d3b44f465",
}
diff --git a/server/static/css/common.css b/server/static/css/common.css
index 23dd897..411ab8f 100644
--- a/server/static/css/common.css
+++ b/server/static/css/common.css
@@ -10,7 +10,7 @@ body {
text-rendering: optimizeLegibility;
}
-.main {
+main {
padding-left: 5px;
padding-right: 5px;
}
diff --git a/server/static/js.go b/server/static/js.go
index 27bf361..a0c535a 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-02 14:53:55.175825378 -0800 PST m=+0.009022020
+// 2017-12-02 16:12:47.268772139 -0800 PST m=+0.014281455
package static
diff --git a/server/template/common.go b/server/template/common.go
index 79aae94..461f8e5 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-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",
}
diff --git a/server/template/html/common/layout.html b/server/template/html/common/layout.html
index 5fa8714..fcc8967 100644
--- a/server/template/html/common/layout.html
+++ b/server/template/html/common/layout.html
@@ -51,9 +51,9 @@
</nav>
</header>
{{ end }}
- <section class="main">
+ <main>
{{template "content" .}}
- </section>
+ </main>
</body>
</html>
{{ end }} \ No newline at end of file
diff --git a/server/template/html/settings.html b/server/template/html/settings.html
index 23a5f42..8e66a10 100644
--- a/server/template/html/settings.html
+++ b/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>
diff --git a/server/template/views.go b/server/template/views.go
index 400f3d9..bc78075 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-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",
}
diff --git a/server/ui/controller/category.go b/server/ui/controller/category.go
index b718d0d..75b7f90 100644
--- a/server/ui/controller/category.go
+++ b/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)
diff --git a/server/ui/controller/entry.go b/server/ui/controller/entry.go
index 7915af5..933f0c6 100644
--- a/server/ui/controller/entry.go
+++ b/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
+}
diff --git a/server/ui/controller/feed.go b/server/ui/controller/feed.go
index 85a94cf..eeb66c4 100644
--- a/server/ui/controller/feed.go
+++ b/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)
diff --git a/server/ui/controller/history.go b/server/ui/controller/history.go
index 9b4e316..1052517 100644
--- a/server/ui/controller/history.go
+++ b/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)
diff --git a/server/ui/controller/settings.go b/server/ui/controller/settings.go
index e8ab20b..a951e5d 100644
--- a/server/ui/controller/settings.go
+++ b/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
diff --git a/server/ui/controller/unread.go b/server/ui/controller/unread.go
index a114250..e0f120a 100644
--- a/server/ui/controller/unread.go
+++ b/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)
diff --git a/server/ui/form/settings.go b/server/ui/form/settings.go
index 3d37a0f..2ebaa6d 100644
--- a/server/ui/form/settings.go
+++ b/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"),
}
}
diff --git a/sql/schema_version_1.sql b/sql/schema_version_1.sql
index e32a38c..05f8f34 100644
--- a/sql/schema_version_1.sql
+++ b/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,
diff --git a/sql/schema_version_4.sql b/sql/schema_version_4.sql
new file mode 100644
index 0000000..988c486
--- /dev/null
+++ b/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';
diff --git a/sql/sql.go b/sql/sql.go
index 1f6b597..1a0da74 100644
--- a/sql/sql.go
+++ b/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",
}
diff --git a/storage/entry_query_builder.go b/storage/entry_query_builder.go
index 2c2f270..6f7f4bd 100644
--- a/storage/entry_query_builder.go
+++ b/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)
diff --git a/storage/migration.go b/storage/migration.go
index 5060a34..9cfde8b 100644
--- a/storage/migration.go
+++ b/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() {
diff --git a/storage/user.go b/storage/user.go
index fdbbfda..7d296a9 100644
--- a/storage/user.go
+++ b/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(