From a291d8a38b40569fdd1f00125ca0b29e4b9264f2 Mon Sep 17 00:00:00 2001 From: Frédéric Guillot Date: Wed, 18 Jul 2018 22:30:05 -0700 Subject: Improve themes handling - Store user theme in session - Logged out users will keep their theme - Add theme background color to web manifest and meta tag --- http/context/context.go | 9 ++++++ middleware/app_session.go | 1 + middleware/context_keys.go | 3 ++ model/app_session.go | 59 ++++++++++++++++++++++++++++++++++++++++ model/session.go | 58 --------------------------------------- model/theme.go | 12 ++++++++ template/common.go | 14 ++++------ template/functions.go | 4 +++ template/html/common/layout.html | 12 ++++---- ui/login_check.go | 5 ++-- ui/logout.go | 1 + ui/oauth2_callback.go | 1 + ui/session/session.go | 7 ++++- ui/settings_update.go | 1 + ui/static_manifest.go | 31 +++++++++++++-------- ui/view/view.go | 1 + 16 files changed, 132 insertions(+), 87 deletions(-) create mode 100644 model/app_session.go delete mode 100644 model/session.go diff --git a/http/context/context.go b/http/context/context.go index f0fed23..4ca95c5 100644 --- a/http/context/context.go +++ b/http/context/context.go @@ -48,6 +48,15 @@ func (c *Context) UserLanguage() string { return language } +// UserTheme get the theme used by the current logged user. +func (c *Context) UserTheme() string { + theme := c.getContextStringValue(middleware.UserThemeContextKey) + if theme == "" { + theme = "default" + } + return theme +} + // CSRF returns the current CSRF token. func (c *Context) CSRF() string { return c.getContextStringValue(middleware.CSRFContextKey) diff --git a/middleware/app_session.go b/middleware/app_session.go index 806debd..ae1d8e9 100644 --- a/middleware/app_session.go +++ b/middleware/app_session.go @@ -55,6 +55,7 @@ func (m *Middleware) AppSession(next http.Handler) http.Handler { ctx = context.WithValue(ctx, FlashMessageContextKey, session.Data.FlashMessage) ctx = context.WithValue(ctx, FlashErrorMessageContextKey, session.Data.FlashErrorMessage) ctx = context.WithValue(ctx, UserLanguageContextKey, session.Data.Language) + ctx = context.WithValue(ctx, UserThemeContextKey, session.Data.Theme) ctx = context.WithValue(ctx, PocketRequestTokenContextKey, session.Data.PocketRequestToken) next.ServeHTTP(w, r.WithContext(ctx)) }) diff --git a/middleware/context_keys.go b/middleware/context_keys.go index 026da05..03a3e2b 100644 --- a/middleware/context_keys.go +++ b/middleware/context_keys.go @@ -32,6 +32,9 @@ var ( // UserLanguageContextKey is the context key to store user language. UserLanguageContextKey = &ContextKey{"UserLanguageContextKey"} + // UserThemeContextKey is the context key to store user theme. + UserThemeContextKey = &ContextKey{"UserThemeContextKey"} + // SessionIDContextKey is the context key used to store the session ID. SessionIDContextKey = &ContextKey{"SessionID"} diff --git a/model/app_session.go b/model/app_session.go new file mode 100644 index 0000000..e9ee06e --- /dev/null +++ b/model/app_session.go @@ -0,0 +1,59 @@ +// 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 model + +import ( + "database/sql/driver" + "encoding/json" + "errors" + "fmt" +) + +// SessionData represents the data attached to the session. +type SessionData struct { + CSRF string `json:"csrf"` + OAuth2State string `json:"oauth2_state"` + FlashMessage string `json:"flash_message"` + FlashErrorMessage string `json:"flash_error_message"` + Language string `json:"language"` + Theme string `json:"Theme"` + PocketRequestToken string `json:"pocket_request_token"` +} + +func (s SessionData) String() string { + return fmt.Sprintf(`CSRF=%q, "OAuth2State=%q, FlashMsg=%q, FlashErrorMsg=%q, Lang=%q, Theme=%q`, + s.CSRF, s.OAuth2State, s.FlashMessage, s.FlashErrorMessage, s.Language, s.Theme) +} + +// Value converts the session data to JSON. +func (s SessionData) Value() (driver.Value, error) { + j, err := json.Marshal(s) + return j, err +} + +// Scan converts raw JSON data. +func (s *SessionData) Scan(src interface{}) error { + source, ok := src.([]byte) + if !ok { + return errors.New("session: unable to assert type of src") + } + + err := json.Unmarshal(source, s) + if err != nil { + return fmt.Errorf("session: %v", err) + } + + return err +} + +// Session represents a session in the system. +type Session struct { + ID string + Data *SessionData +} + +func (s *Session) String() string { + return fmt.Sprintf(`ID="%s", Data={%v}`, s.ID, s.Data) +} diff --git a/model/session.go b/model/session.go deleted file mode 100644 index 763f709..0000000 --- a/model/session.go +++ /dev/null @@ -1,58 +0,0 @@ -// 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 model - -import ( - "database/sql/driver" - "encoding/json" - "errors" - "fmt" -) - -// SessionData represents the data attached to the session. -type SessionData struct { - CSRF string `json:"csrf"` - OAuth2State string `json:"oauth2_state"` - FlashMessage string `json:"flash_message"` - FlashErrorMessage string `json:"flash_error_message"` - Language string `json:"language"` - PocketRequestToken string `json:"pocket_request_token"` -} - -func (s SessionData) String() string { - return fmt.Sprintf(`CSRF="%s", "OAuth2State="%s", FlashMessage="%s", FlashErrorMessage="%s", Lang="%s"`, - s.CSRF, s.OAuth2State, s.FlashMessage, s.FlashErrorMessage, s.Language) -} - -// Value converts the session data to JSON. -func (s SessionData) Value() (driver.Value, error) { - j, err := json.Marshal(s) - return j, err -} - -// Scan converts raw JSON data. -func (s *SessionData) Scan(src interface{}) error { - source, ok := src.([]byte) - if !ok { - return errors.New("session: unable to assert type of src") - } - - err := json.Unmarshal(source, s) - if err != nil { - return fmt.Errorf("session: %v", err) - } - - return err -} - -// Session represents a session in the system. -type Session struct { - ID string - Data *SessionData -} - -func (s *Session) String() string { - return fmt.Sprintf(`ID="%s", Data={%v}`, s.ID, s.Data) -} diff --git a/model/theme.go b/model/theme.go index 5d32df4..f58f91c 100644 --- a/model/theme.go +++ b/model/theme.go @@ -15,6 +15,18 @@ func Themes() map[string]string { } } +// ThemeColor returns the color for the address bar or/and the browser color. +// https://developer.mozilla.org/en-US/docs/Web/Manifest#theme_color +// https://developers.google.com/web/tools/lighthouse/audits/address-bar +func ThemeColor(theme string) string { + switch theme { + case "black": + return "#222" + default: + return "#fff" + } +} + // ValidateTheme validates theme value. func ValidateTheme(theme string) error { for key := range Themes() { diff --git a/template/common.go b/template/common.go index 02b6748..2db5d44 100644 --- a/template/common.go +++ b/template/common.go @@ -77,8 +77,9 @@ var templateCommonMap = map[string]string{ - + {{template "title" .}} - Miniflux + @@ -104,12 +105,9 @@ var templateCommonMap = map[string]string{ {{ if .csrf }} {{ end }} - {{template "title" .}} - Miniflux - {{ if .user }} - - {{ else }} - - {{ end }} + + + @@ -241,6 +239,6 @@ var templateCommonMap = map[string]string{ var templateCommonMapChecksums = map[string]string{ "entry_pagination": "756ef122f3ebc73754b5fc4304bf05e59da0ab4af030b2509ff4c9b4a74096ce", "item_meta": "2da78476f6c7fb8742c969ad1bfc20b7b61fddf97d79a77baf3cabda52f6fb49", - "layout": "0d226847454115497b3ef7d67381ae231459c8dcde974eb1a7f4a115957c0e86", + "layout": "16658c13e91cab88ba4c49f14654a95b1db12054cc96def3e40360a52acc6c54", "pagination": "b592d58ea9d6abf2dc0b158621404cbfaeea5413b1c8b8b9818725963096b196", } diff --git a/template/functions.go b/template/functions.go index 4673487..225984b 100644 --- a/template/functions.go +++ b/template/functions.go @@ -15,6 +15,7 @@ import ( "github.com/miniflux/miniflux/config" "github.com/miniflux/miniflux/filter" "github.com/miniflux/miniflux/http/route" + "github.com/miniflux/miniflux/model" "github.com/miniflux/miniflux/url" ) @@ -90,6 +91,9 @@ func (f *funcMap) Map() template.FuncMap { return str }, + "theme_color": func(theme string) string { + return model.ThemeColor(theme) + }, // These functions are overrided at runtime after the parsing. "elapsed": func(timezone string, t time.Time) string { diff --git a/template/html/common/layout.html b/template/html/common/layout.html index e396896..dbf7079 100644 --- a/template/html/common/layout.html +++ b/template/html/common/layout.html @@ -3,8 +3,9 @@ - + {{template "title" .}} - Miniflux + @@ -30,12 +31,9 @@ {{ if .csrf }} {{ end }} - {{template "title" .}} - Miniflux - {{ if .user }} - - {{ else }} - - {{ end }} + + + diff --git a/ui/login_check.go b/ui/login_check.go index 71e7854..36fafea 100644 --- a/ui/login_check.go +++ b/ui/login_check.go @@ -47,13 +47,14 @@ func (c *Controller) CheckLogin(w http.ResponseWriter, r *http.Request) { logger.Info("[Controller:CheckLogin] username=%s just logged in", authForm.Username) c.store.SetLastLogin(userID) - userLanguage, err := c.store.UserLanguage(userID) + user, err := c.store.UserByID(userID) if err != nil { html.ServerError(w, err) return } - sess.SetLanguage(userLanguage) + sess.SetLanguage(user.Language) + sess.SetTheme(user.Theme) http.SetCookie(w, cookie.New( cookie.CookieUserSessionID, diff --git a/ui/logout.go b/ui/logout.go index 2946d1a..0c777ce 100644 --- a/ui/logout.go +++ b/ui/logout.go @@ -28,6 +28,7 @@ func (c *Controller) Logout(w http.ResponseWriter, r *http.Request) { } sess.SetLanguage(user.Language) + sess.SetTheme(user.Theme) if err := c.store.RemoveUserSessionByToken(user.ID, ctx.UserSessionToken()); err != nil { logger.Error("[Controller:Logout] %v", err) diff --git a/ui/oauth2_callback.go b/ui/oauth2_callback.go index 23e379b..a39c0ac 100644 --- a/ui/oauth2_callback.go +++ b/ui/oauth2_callback.go @@ -114,6 +114,7 @@ func (c *Controller) OAuth2Callback(w http.ResponseWriter, r *http.Request) { logger.Info("[Controller:OAuth2Callback] username=%s just logged in", user.Username) c.store.SetLastLogin(user.ID) sess.SetLanguage(user.Language) + sess.SetTheme(user.Theme) http.SetCookie(w, cookie.New( cookie.CookieUserSessionID, diff --git a/ui/session/session.go b/ui/session/session.go index 3d630a8..474bd18 100644 --- a/ui/session/session.go +++ b/ui/session/session.go @@ -51,11 +51,16 @@ func (s *Session) FlashErrorMessage() string { return message } -// SetLanguage updates language field in session. +// SetLanguage updates the language field in session. func (s *Session) SetLanguage(language string) { s.store.UpdateSessionField(s.ctx.SessionID(), "language", language) } +// SetTheme updates the theme field in session. +func (s *Session) SetTheme(theme string) { + s.store.UpdateSessionField(s.ctx.SessionID(), "theme", theme) +} + // SetPocketRequestToken updates Pocket Request Token. func (s *Session) SetPocketRequestToken(requestToken string) { s.store.UpdateSessionField(s.ctx.SessionID(), "pocket_request_token", requestToken) diff --git a/ui/settings_update.go b/ui/settings_update.go index f78b290..229042d 100644 --- a/ui/settings_update.go +++ b/ui/settings_update.go @@ -68,6 +68,7 @@ func (c *Controller) UpdateSettings(w http.ResponseWriter, r *http.Request) { } sess.SetLanguage(user.Language) + sess.SetTheme(user.Theme) sess.NewFlashMessage(c.translator.GetLanguage(ctx.UserLanguage()).Get("Preferences saved!")) response.Redirect(w, r, route.Path(c.router, "settings")) } diff --git a/ui/static_manifest.go b/ui/static_manifest.go index 47de9f3..27abaec 100644 --- a/ui/static_manifest.go +++ b/ui/static_manifest.go @@ -7,8 +7,10 @@ package ui import ( "net/http" + "github.com/miniflux/miniflux/http/context" "github.com/miniflux/miniflux/http/response/json" "github.com/miniflux/miniflux/http/route" + "github.com/miniflux/miniflux/model" ) // WebManifest renders web manifest file. @@ -20,20 +22,27 @@ func (c *Controller) WebManifest(w http.ResponseWriter, r *http.Request) { } type webManifest struct { - Name string `json:"name"` - Description string `json:"description"` - ShortName string `json:"short_name"` - StartURL string `json:"start_url"` - Icons []webManifestIcon `json:"icons"` - Display string `json:"display"` + Name string `json:"name"` + Description string `json:"description"` + ShortName string `json:"short_name"` + StartURL string `json:"start_url"` + Icons []webManifestIcon `json:"icons"` + Display string `json:"display"` + ThemeColor string `json:"theme_color"` + BackgroundColor string `json:"background_color"` } + ctx := context.New(r) + themeColor := model.ThemeColor(ctx.UserTheme()) + manifest := &webManifest{ - Name: "Miniflux", - ShortName: "Miniflux", - Description: "Minimalist Feed Reader", - Display: "minimal-ui", - StartURL: route.Path(c.router, "unread"), + Name: "Miniflux", + ShortName: "Miniflux", + Description: "Minimalist Feed Reader", + Display: "minimal-ui", + StartURL: route.Path(c.router, "unread"), + ThemeColor: themeColor, + BackgroundColor: themeColor, Icons: []webManifestIcon{ webManifestIcon{Source: route.Path(c.router, "appIcon", "filename", "icon-120.png"), Sizes: "120x120", Type: "image/png"}, webManifestIcon{Source: route.Path(c.router, "appIcon", "filename", "icon-192.png"), Sizes: "192x192", Type: "image/png"}, diff --git a/ui/view/view.go b/ui/view/view.go index a1c6646..0854222 100644 --- a/ui/view/view.go +++ b/ui/view/view.go @@ -35,5 +35,6 @@ func New(tpl *template.Engine, ctx *context.Context, sess *session.Session) *Vie b.params["csrf"] = ctx.CSRF() b.params["flashMessage"] = sess.FlashMessage() b.params["flashErrorMessage"] = sess.FlashErrorMessage() + b.params["theme"] = ctx.UserTheme() return b } -- cgit v1.2.3