Add dark mode
authorr <r@freesoftwareextremist.com>
Sun, 12 Jan 2020 17:16:57 +0000 (17:16 +0000)
committerr <r@freesoftwareextremist.com>
Sun, 12 Jan 2020 17:16:57 +0000 (17:16 +0000)
15 files changed:
mastodon/status.go
model/postContext.go
model/settings.go
renderer/model.go
renderer/renderer.go
service/service.go
service/transport.go
static/fluoride.js
static/main.css
templates/header.tmpl
templates/notification.tmpl
templates/postform.tmpl
templates/settings.tmpl
templates/status.tmpl
templates/user.tmpl

index 06fdd20ce46aa43b3a6e740f7c5a23e8bebd6e8f..e6e247f6a6b32e055311d71611442b8df85686d7 100644 (file)
@@ -57,6 +57,7 @@ type Status struct {
        ThreadInNewTab  bool                   `json:"thread_in_new_tab"`
        MaskNSFW        bool                   `json:"mask_nsfw"`
        RetweetedByID   string                 `json:"retweeted_by_id"`
+       DarkMode        bool                   `json:"dark_mode"`
 }
 
 // Context hold information for mastodon context.
index 3098437658d89d811c1ecf3f43da5cae31cc87eb..58997f770ca9a1f6fb767b08396d6534157d0f08 100644 (file)
@@ -9,6 +9,7 @@ type PostContext struct {
        DefaultVisibility string
        ReplyContext      *ReplyContext
        Formats           []PostFormat
+       DarkMode          bool
 }
 
 type ReplyContext struct {
index b8eeffc5c42f4b509cd93fc33b01b44e40b101b8..7d227475e60a730ba945a6a7112ef81eb5246d97 100644 (file)
@@ -6,6 +6,7 @@ type Settings struct {
        ThreadInNewTab    bool   `json:"thread_in_new_tab"`
        MaskNSFW          bool   `json:"mask_nfsw"`
        FluorideMode      bool   `json:"fluoride_mode"`
+       DarkMode          bool   `json:"dark_mode"`
 }
 
 func NewSettings() *Settings {
@@ -15,5 +16,6 @@ func NewSettings() *Settings {
                ThreadInNewTab:    false,
                MaskNSFW:          true,
                FluorideMode:      false,
+               DarkMode:          false,
        }
 }
index 102ce55a87beab8d87fe63211bcdb79116f42609..64b99461ac048a2401a7e79a9a1ddbbbfc17cdf9 100644 (file)
@@ -10,6 +10,7 @@ type HeaderData struct {
        NotificationCount int
        CustomCSS         string
        FluorideMode      bool
+       DarkMode          bool
 }
 
 type NavbarData struct {
@@ -58,6 +59,7 @@ type NotificationData struct {
        Notifications []*mastodon.Notification
        HasNext       bool
        NextLink      string
+       DarkMode      bool
 }
 
 type UserData struct {
@@ -66,6 +68,7 @@ type UserData struct {
        Statuses []*mastodon.Status
        HasNext  bool
        NextLink string
+       DarkMode bool
 }
 
 type AboutData struct {
index 406531260ea0f14273f809ebc69a643bd308e70a..42bffad40398909f51a56968a338214f52961572 100644 (file)
@@ -11,6 +11,39 @@ import (
        "mastodon"
 )
 
+var (
+       icons = map[string]string{
+               "envelope":          "/static/icons/envelope.png",
+               "dark-envelope":     "/static/icons/dark-envelope.png",
+               "globe":             "/static/icons/globe.png",
+               "dark-globe":        "/static/icons/dark-globe.png",
+               "liked":             "/static/icons/liked.png",
+               "dark-liked":        "/static/icons/liked.png",
+               "link":              "/static/icons/link.png",
+               "dark-link":         "/static/icons/dark-link.png",
+               "lock":              "/static/icons/lock.png",
+               "dark-lock":         "/static/icons/dark-lock.png",
+               "mail-forward":      "/static/icons/mail-forward.png",
+               "dark-mail-forward": "/static/icons/dark-mail-forward.png",
+               "reply":             "/static/icons/reply.png",
+               "dark-reply":        "/static/icons/dark-reply.png",
+               "retweet":           "/static/icons/retweet.png",
+               "dark-retweet":      "/static/icons/dark-retweet.png",
+               "retweeted":         "/static/icons/retweeted.png",
+               "dark-retweeted":    "/static/icons/retweeted.png",
+               "smile-o":           "/static/icons/smile-o.png",
+               "dark-smile-o":      "/static/icons/dark-smile-o.png",
+               "star-o":            "/static/icons/star-o.png",
+               "dark-star-o":       "/static/icons/dark-star-o.png",
+               "star":              "/static/icons/star.png",
+               "dark-star":         "/static/icons/dark-star.png",
+               "unlock-alt":        "/static/icons/unlock-alt.png",
+               "dark-unlock-alt":   "/static/icons/dark-unlock-alt.png",
+               "user-plus":         "/static/icons/user-plus.png",
+               "dark-user-plus":    "/static/icons/dark-user-plus.png",
+       }
+)
+
 type Renderer interface {
        RenderErrorPage(ctx context.Context, writer io.Writer, data *ErrorData)
        RenderHomePage(ctx context.Context, writer io.Writer, data *HomePageData) (err error)
@@ -42,6 +75,7 @@ func NewRenderer(templateGlobPattern string) (r *renderer, err error) {
                "TimeSince":               TimeSince,
                "FormatTimeRFC3339":       FormatTimeRFC3339,
                "FormatTimeRFC822":        FormatTimeRFC822,
+               "GetIcon":                 GetIcon,
        }).ParseGlob(templateGlobPattern)
        if err != nil {
                return
@@ -180,3 +214,11 @@ func FormatTimeRFC3339(t time.Time) string {
 func FormatTimeRFC822(t time.Time) string {
        return t.Format(time.RFC822)
 }
+
+func GetIcon(name string, darkMode bool) (icon string) {
+       if darkMode {
+               name = "dark-" + name
+       }
+       icon, _ = icons[name]
+       return
+}
index f43c463f55e57aabb2f7871e2fccf2b88544bcff..b3c21038f614fab9257a8879e4462f0971e479c2 100644 (file)
@@ -281,10 +281,12 @@ func (svc *service) ServeTimelinePage(ctx context.Context, client io.Writer,
        for i := range statuses {
                statuses[i].ThreadInNewTab = c.Session.Settings.ThreadInNewTab
                statuses[i].MaskNSFW = c.Session.Settings.MaskNSFW
+               statuses[i].DarkMode = c.Session.Settings.DarkMode
                if statuses[i].Reblog != nil {
                        statuses[i].Reblog.RetweetedByID = statuses[i].ID
                        statuses[i].Reblog.ThreadInNewTab = c.Session.Settings.ThreadInNewTab
                        statuses[i].Reblog.MaskNSFW = c.Session.Settings.MaskNSFW
+                       statuses[i].Reblog.DarkMode = c.Session.Settings.DarkMode
                }
        }
 
@@ -317,6 +319,7 @@ func (svc *service) ServeTimelinePage(ctx context.Context, client io.Writer,
        postContext := model.PostContext{
                DefaultVisibility: c.Session.Settings.DefaultVisibility,
                Formats:           svc.postFormats,
+               DarkMode:          c.Session.Settings.DarkMode,
        }
 
        commonData, err := svc.getCommonData(ctx, client, c, timelineType+" timeline ")
@@ -385,6 +388,7 @@ func (svc *service) ServeThreadPage(ctx context.Context, client io.Writer, c *mo
                                InReplyToName: status.Account.Acct,
                                ReplyContent:  content,
                        },
+                       DarkMode: c.Session.Settings.DarkMode,
                }
        }
 
@@ -401,6 +405,7 @@ func (svc *service) ServeThreadPage(ctx context.Context, client io.Writer, c *mo
                statuses[i].ShowReplies = true
                statuses[i].ReplyMap = replyMap
                statuses[i].MaskNSFW = c.Session.Settings.MaskNSFW
+               statuses[i].DarkMode = c.Session.Settings.DarkMode
                addToReplyMap(replyMap, statuses[i].InReplyToID, statuses[i].ID, i+1)
        }
 
@@ -444,6 +449,7 @@ func (svc *service) ServeNotificationPage(ctx context.Context, client io.Writer,
                if notifications[i].Status != nil {
                        notifications[i].Status.CreatedAt = notifications[i].CreatedAt
                        notifications[i].Status.MaskNSFW = c.Session.Settings.MaskNSFW
+                       notifications[i].Status.DarkMode = c.Session.Settings.DarkMode
                        switch notifications[i].Type {
                        case "reblog", "favourite":
                                notifications[i].Status.HideAccountInfo = true
@@ -476,6 +482,7 @@ func (svc *service) ServeNotificationPage(ctx context.Context, client io.Writer,
                HasNext:       hasNext,
                NextLink:      nextLink,
                CommonData:    commonData,
+               DarkMode:      c.Session.Settings.DarkMode,
        }
        err = svc.renderer.RenderNotificationPage(ctx, client, data)
        if err != nil {
@@ -507,8 +514,10 @@ func (svc *service) ServeUserPage(ctx context.Context, client io.Writer, c *mode
 
        for i := range statuses {
                statuses[i].MaskNSFW = c.Session.Settings.MaskNSFW
+               statuses[i].DarkMode = c.Session.Settings.DarkMode
                if statuses[i].Reblog != nil {
                        statuses[i].Reblog.MaskNSFW = c.Session.Settings.MaskNSFW
+                       statuses[i].Reblog.DarkMode = c.Session.Settings.DarkMode
                }
        }
 
@@ -528,6 +537,7 @@ func (svc *service) ServeUserPage(ctx context.Context, client io.Writer, c *mode
                HasNext:    hasNext,
                NextLink:   nextLink,
                CommonData: commonData,
+               DarkMode:   c.Session.Settings.DarkMode,
        }
 
        err = svc.renderer.RenderUserPage(ctx, client, data)
@@ -723,6 +733,7 @@ func (svc *service) ServeSearchPage(ctx context.Context, client io.Writer, c *mo
                hasNext = len(results.Statuses) == 20
                for i := range results.Statuses {
                        results.Statuses[i].MaskNSFW = c.Session.Settings.MaskNSFW
+                       results.Statuses[i].DarkMode = c.Session.Settings.DarkMode
                }
 
        }
@@ -827,6 +838,7 @@ func (svc *service) getCommonData(ctx context.Context, client io.Writer, c *mode
 
                data.HeaderData.NotificationCount = notificationCount
                data.HeaderData.FluorideMode = c.Session.Settings.FluorideMode
+               data.HeaderData.DarkMode = c.Session.Settings.DarkMode
        }
 
        return
index d89b8547b2451922ac2a67a149a62ac86e672cd0..0072f9571375912caf0b384c089afffb49c0cca7 100644 (file)
@@ -448,12 +448,14 @@ func NewHandler(s Service, staticDir string) http.Handler {
                threadInNewTab := req.FormValue("thread_in_new_tab") == "true"
                maskNSFW := req.FormValue("mask_nsfw") == "true"
                fluorideMode := req.FormValue("fluoride_mode") == "true"
+               darkMode := req.FormValue("dark_mode") == "true"
                settings := &model.Settings{
                        DefaultVisibility: visibility,
                        CopyScope:         copyScope,
                        ThreadInNewTab:    threadInNewTab,
                        MaskNSFW:          maskNSFW,
                        FluorideMode:      fluorideMode,
+                       DarkMode:      darkMode,
                }
 
                err := s.SaveSettings(ctx, w, nil, settings)
index 25ab4a328b56d5ef36e7715c7f58fc941cd790f1..6a1b5fbca7adfad5110a236add8297436cec05c8 100644 (file)
@@ -1,8 +1,12 @@
 var actionIcons = {
        "like": "/static/icons/star-o.png",
+       "dark-like": "/static/icons/dark-star-o.png",
        "unlike": "/static/icons/liked.png",
+       "dark-unlike": "/static/icons/liked.png",
        "retweet": "/static/icons/retweet.png",
-       "unretweet": "/static/icons/retweeted.png"
+       "dark-retweet": "/static/icons/dark-retweet.png",
+       "unretweet": "/static/icons/retweeted.png",
+       "dark-unretweet": "/static/icons/retweeted.png"
 };
 
 var reverseActions = {
@@ -31,7 +35,11 @@ function http(method, url, success, error) {
 }
 
 function updateActionForm(id, f, action) {
-       f.children[1].src = actionIcons[action];
+       if (Array.from(document.body.classList).indexOf("dark") > -1) {
+               f.children[1].src = actionIcons["dark-" + action];
+       } else {
+               f.children[1].src = actionIcons[action];
+       }
        f.action = "/" + action + "/" + id;
        f.dataset.action = action;
 }
index 553c9a21cfd10bdf1ef52024dd5a5723a3de67f3..acbca051901fd0e687bca5ae5a4d82824ee56620 100644 (file)
@@ -99,8 +99,8 @@
 }
 
 .status-action a:hover,
-.status-action a:hover i {
-       opacity: 0.8;
+.status-action input:hover {
+       opacity: 0.6;
 }
 
 .status-action a.status-time {
        z-index: 3;
        margin: 0 8px 0 8px;
 }
+
+.dark {
+       background-color: #222222;
+       background-image: none;
+       color: #eaeaea;
+}
+
+.dark a {
+       color: #81a2be;
+}
+
+.dark .status-action a {
+       color: #999999;
+}
+
+.dark #post-content {
+       background-color: #333333;
+       border: 1px solid #444444;
+       color: #eaeaea;
+}
+
+.dark #reply-popup,
+.dark #reply-to-popup {
+       background-color: #222222;
+       border-color: #444444;
+}
index d51159056243a95ce09349998356a4abb06728c2..10afe8aba5808905e37aa31a697c9568920a164b 100644 (file)
@@ -12,4 +12,4 @@
        <script src="/static/fluoride.js"></script>
        {{end}}
 </head>
-<body>
+<body {{if .DarkMode}}class="dark"{{end}}>
index d68382f8df7b7da8f2cc669f7bfb8d7c5ac0ef5f..79d6d908920bd7e03e0e1d3fb62e693b16c5aaa1 100644 (file)
@@ -14,7 +14,7 @@
                <div>
                        <div class="notification-info-text">
                                <span class="status-dname"> {{EmojiFilter .Account.DisplayName .Account.Emojis}} </span>  
-                               <img class="icon" src="/static/icons/user-plus.png" alt="followed" />
+                               <img class="icon" src="{{GetIcon "user-plus" .DarkMode}}" alt="followed" />
                                <span> followed you </span>
                        </div>
                        <div class="notification-follow-uname">
@@ -36,7 +36,7 @@
                <div>
                        <div class="notification-info-text">
                                <span class="status-dname"> {{EmojiFilter .Account.DisplayName .Account.Emojis}} </span>  
-                               <img class="icon" src="/static/icons/retweeted.png" alt="retweeted" />
+                               <img class="icon" src="{{GetIcon "retweeted" .DarkMode}}" alt="retweeted" />
                                <span> retweeted your post </span>
                        </div>
                        {{template "status" .Status}}
@@ -53,7 +53,7 @@
                <div>
                        <div class="notification-info-text">
                                <span class="status-dname"> {{EmojiFilter .Account.DisplayName .Account.Emojis}} </span>  
-                               <img class="icon" src="/static/icons/liked.png" alt="liked" />
+                               <img class="icon" src="{{GetIcon "liked" .DarkMode}}" alt="liked" />
                                <span> liked your post </span>
                        </div>
                        {{template "status" .Status}}
index 4e2d4a40142f0273f0ed3870f5d9ce7d2ed2b938..ff70eaf5db442ffd29ad062515ba5b69b94881c6 100644 (file)
@@ -6,7 +6,7 @@
        <label for="post-content" class="post-form-title"> New post </label>
        {{end}}
        <a class="post-form-emoji-link" href="/emojis" target="_blank" title="emoji reference">
-               <img class="icon post-emoji" src="/static/icons/smile-o.png" alt="emojis" />
+               <img class="icon post-emoji" src="{{GetIcon "smile-o" .DarkMode}}" alt="emojis" />
        </a>
        <div class="post-form-content-container">
                <textarea id="post-content" name="content" class="post-content" cols="50" rows="5">{{if .ReplyContext}}{{.ReplyContext.ReplyContent}}{{end}}</textarea>
index d15c47b306b51dd762c9c0a068b196160690e702..06e2a9a56d8fcf409869d66b7fdddc04d573611a 100644 (file)
                <input id="fluoride-mode" name="fluoride_mode" type="checkbox" value="true" {{if .Settings.FluorideMode}}checked{{end}}>
                <label for="fluoride-mode"> Enable Fluoride Mode </label>
        </div>
+       <div class="settings-form-field">
+               <input id="dark-mode" name="dark_mode" type="checkbox" value="true" {{if .Settings.DarkMode}}checked{{end}}>
+               <label for="dark-mode"> Use dark theme </label>
+       </div>
        <button type="submit"> Save </button>
 </form>
 
index 9c9d55dad01b40ab6c7666895f5be6f141d63c2a..91d666d3a418f45e4237e22c7e75a7b284439123 100644 (file)
@@ -5,7 +5,7 @@
                        <img class="status-profile-img" src="{{.Account.AvatarStatic}}" alt="avatar" />
                </a>
                <span class="status-dname"> {{EmojiFilter .Account.DisplayName .Account.Emojis}} </span>  
-               <img class="icon" src="/static/icons/retweeted.png" alt="retweeted" />
+               <img class="icon" src="{{GetIcon "retweeted" .DarkMode}}" alt="retweeted" />
                retweeted
        </div>
        {{template "status" .Reblog}}
                                </a>
                                <a class="status-visibility">
                                        {{if eq .Visibility "public"}}
-                                       <img class="icon" src="/static/icons/globe.png" alt="Public" title="Public" />
+                                       <img class="icon" src="{{GetIcon "globe" .DarkMode}}" alt="Public" title="Public" />
                                        {{else if eq .Visibility "unlisted"}}
-                                       <img class="icon" src="/static/icons/unlock-alt.png" alt="Unlisted" title="Unlisted" />
+                                       <img class="icon" src="{{GetIcon "unlock-alt" .DarkMode}}" alt="Unlisted" title="Unlisted" />
                                        {{else if eq .Visibility "private"}}
-                                       <img class="icon" src="/static/icons/lock.png" alt="Private" title="Private" />
+                                       <img class="icon" src="{{GetIcon "lock" .DarkMode}}" alt="Private" title="Private" />
                                        {{else if eq .Visibility "direct"}}
-                                       <img class="icon" src="/static/icons/envelope.png" alt="Direct" title="Direct" />
+                                       <img class="icon" src="{{GetIcon "envelope" .DarkMode}}" alt="Direct" title="Direct" />
                                        {{end}}
                                </a>
                                <a class="remote-link" href="{{.URL}}" target="_blank" title="source">
-                                       <img class="icon" src="/static/icons/link.png" alt="source" />
+                                       <img class="icon" src="{{GetIcon "link" .DarkMode}}" alt="source" />
                                </a>
                        </div>
                        {{end}}
@@ -46,7 +46,7 @@
                                {{if .InReplyToID}}
                                <div class="status-reply-to">
                                        <a class="status-reply-to-link" href="{{if not .ShowReplies}}/thread/{{.InReplyToID}}{{end}}#status-{{.InReplyToID}}"> 
-                                               <img class="icon" src="/static/icons/mail-forward.png" alt="reply to" /> reply to {{.Pleroma.InReplyToAccountAcct}} 
+                                               <img class="icon" src="{{GetIcon "mail-forward" .DarkMode}}" alt="reply to" /> reply to {{.Pleroma.InReplyToAccountAcct}} 
                                        </a>
                                </div>
                                {{if index .ReplyMap .ID}} <span class="status-reply-info-divider"> - </span> {{end}}
@@ -93,7 +93,7 @@
                        <div class="status-action-container"> 
                                <div class="status-action">
                                        <a class="status-you" href="/thread/{{.ID}}?reply=true#status-{{.ID}}" title="reply"> 
-                                               <img class="icon" src="/static/icons/reply.png" alt="reply" />
+                                               <img class="icon" src="{{GetIcon "reply" .DarkMode}}" alt="reply" />
                                        </a>
                                        <a class="status-reply-count" href="/thread/{{.ID}}#status-{{.ID}}" {{if .ThreadInNewTab}}target="_blank"{{end}}>
                                                {{DisplayInteractionCount .RepliesCount}}
                                <div class="status-action">
                                        {{if or (eq .Visibility "private") (eq .Visibility "direct")}}
                                        <a class="status-retweet" title="this status cannot be retweeted"> 
-                                               <img class="icon" src="/static/icons/retweet.png" alt="retweet" />
+                                               <img class="icon" src="{{GetIcon "retweet" .DarkMode}}" alt="retweet" />
                                        </a>
                                        {{else}}
                                        {{if .Reblogged}}
                                        <form class="status-retweet" data-action="unretweet" action="/unretweet/{{.ID}}" method="post">
                                                <input type="hidden" name="retweeted_by_id" value="{{.RetweetedByID}}" />
-                                               <input type="image" src="/static/icons/retweeted.png" alt="undo retweet" class="icon" title="undo retweet">
+                                               <input type="image" src="{{GetIcon "retweeted" .DarkMode}}" alt="undo retweet" class="icon" title="undo retweet">
                                        </form>
                                        {{else}}
                                        <form class="status-retweet" data-action="retweet" action="/retweet/{{.ID}}" method="post">
                                                <input type="hidden" name="retweeted_by_id" value="{{.RetweetedByID}}" />
-                                               <input type="image" src="/static/icons/retweet.png" alt="retweet" class="icon" title="retweet">
+                                               <input type="image" src="{{GetIcon "retweet" .DarkMode}}" alt="retweet" class="icon" title="retweet">
                                        </form>
                                        {{end}}
                                        {{end}}
                                        {{if .Favourited}}
                                        <form class="status-like" data-action="unlike" action="/unlike/{{.ID}}" method="post">
                                                <input type="hidden" name="retweeted_by_id" value="{{.RetweetedByID}}" />
-                                               <input type="image" src="/static/icons/liked.png" alt="unlike" class="icon" title="unlike">
+                                               <input type="image" src="{{GetIcon "liked" .DarkMode}}" alt="unlike" class="icon" title="unlike">
                                        </form>
                                        {{else}}
                                        <form class="status-like" data-action="like" action="/like/{{.ID}}" method="post">
                                                <input type="hidden" name="retweeted_by_id" value="{{.RetweetedByID}}" />
-                                               <input type="image" src="/static/icons/star-o.png" alt="like" class="icon" title="like">
+                                               <input type="image" src="{{GetIcon "star-o" .DarkMode}}" alt="like" class="icon" title="like">
                                        </form>
                                        {{end}}
                                        <a class="status-like-count" href="/likedby/{{.ID}}" title="click to see the the list"> 
index 53de635fcca42b6f8fb4c75400d80ec9a7d1f5c9..60536bb361bd335e6563c19723ee9e53f073b744 100644 (file)
@@ -14,7 +14,7 @@
                        <span class="status-dname"> {{EmojiFilter .User.DisplayName .User.Emojis}} </span>  
                        <span class="status-uname"> {{.User.Acct}} </span>
                        <a class="remote-link" href="{{.User.URL}}" target="_blank" title="remote profile">
-                               <img class="icon" src="/static/icons/link.png" alt="link" />
+                               <img class="icon" src="{{GetIcon "link" .DarkMode}}" alt="link" />
                        </a>
                </div>
                <div>