Ver Fonte

Merge pull request #81 from zedeus/themes

Themes
Zed há 6 anos atrás
pai
commit
510abb9b50

+ 1 - 2
README.md

@@ -10,7 +10,7 @@ Inspired by the [invidio.us](https://github.com/omarroth/invidious) project.
 - Prevents Twitter from tracking your IP or JavaScript fingerprint
 - Unofficial API (no rate limits or developer account required)
 - AGPLv3 licensed, no proprietary instances permitted
-- Dark theme
+- Themes
 - Lightweight (for [@nim_lang](https://twitter.com/nim_lang), 36KB vs 580KB from twitter.com)
 - Native RSS feeds
 - Mobile support (responsive design)
@@ -21,7 +21,6 @@ Inspired by the [invidio.us](https://github.com/omarroth/invidious) project.
 - More caching (waiting for [moigagoo/norm#19](https://github.com/moigagoo/norm/pull/19))
 - Simple account system with customizable feed
 - Json API endpoints
-- Themes
 - Nitter logo
 - Emoji support (WIP, uses native font for now)
 

+ 3 - 0
nitter.conf

@@ -9,3 +9,6 @@ hostname = "nitter.net"
 [Cache]
 directory = "./tmp"
 profileMinutes = 10  # how long to cache profiles
+
+[Config]
+defaultTheme = "Dark"

+ 1 - 1
nitter.nimble

@@ -11,7 +11,7 @@ bin           = @["nitter"]
 # Dependencies
 
 requires "nim >= 0.19.9"
-requires "norm >= 1.0.17"
+requires "norm#head"
 requires "https://github.com/dom96/httpbeast#head"
 requires "jester >= 0.4.3"
 requires "regex >= 0.11.2"

+ 3 - 0
public/css/themes/dark.css

@@ -0,0 +1,3 @@
+body {
+    /* uses default values */
+}

+ 37 - 0
public/css/themes/light.css

@@ -0,0 +1,37 @@
+body {
+    --bg_color: #E6ECF0;
+    --fg_color: #0F0F0F;
+    --fg_faded: #657786;
+    --fg_dark: var(--fg_faded);
+    --fg_nav: var(--accent);
+
+    --bg_panel: #FFFFFF;
+    --bg_elements: #FDFDFD;
+    --bg_overlays: #FFFFFF;
+    --bg_hover: #F5F8FA;
+
+    --grey: var(--fg_faded);
+    --dark_grey: #D6D6D6;
+    --darker_grey: #CECECE;
+    --darkest_grey: #ECECEC;
+    --border_grey: #E6ECF0;
+
+    --accent: #1DA1F2;
+    --accent_light: #A0EDFF;
+    --accent_dark: var(--accent);
+    --accent_border: #1DA1F296;
+
+    --play_button: #D84D4D;
+    --play_button_hover: #FF6C60;
+
+    --more_replies_dots: #0199F7;
+    --error_red: #FF7266;
+
+    --verified_blue: var(--accent);
+    --icon_text: ##F8F8F2;
+
+    --tab: var(--accent);
+    --tab_selected: #000000;
+
+    --profile_stat: var(--fg_dark);
+}

+ 3 - 1
src/config.nim

@@ -21,5 +21,7 @@ proc getConfig*(path: string): Config =
     hostname: cfg.get("Server", "hostname", "nitter.net"),
 
     cacheDir: cfg.get("Cache", "directory", "/tmp/nitter"),
-    profileCacheTime: cfg.get("Cache", "profileMinutes", 10)
+    profileCacheTime: cfg.get("Cache", "profileMinutes", 10),
+
+    defaultTheme: cfg.get("Config", "defaultTheme", "Dark")
   )

+ 13 - 7
src/prefs.nim

@@ -14,6 +14,10 @@ static:
   if missing.len > 0:
     raiseAssert("{$1} missing from the Prefs type" % missing.join(", "))
 
+template safeAddColumn(field: typedesc): untyped =
+  try: field.addColumn
+  except DbError: discard
+
 dbFromTypes("prefs.db", "", "", "", [Prefs])
 
 withDb:
@@ -21,10 +25,12 @@ withDb:
     createTables()
   except DbError:
     discard
+  Prefs.theme.safeAddColumn
 
-proc getDefaultPrefs(hostname: string): Prefs =
+proc getDefaultPrefs(cfg: Config): Prefs =
   result = genDefaultPrefs()
-  result.replaceTwitter = hostname
+  result.replaceTwitter = cfg.hostname
+  result.theme = cfg.defaultTheme
 
 proc cache*(prefs: var Prefs) =
   withDb:
@@ -35,18 +41,18 @@ proc cache*(prefs: var Prefs) =
     except AssertionError, KeyError:
       prefs.insert()
 
-proc getPrefs*(id, hostname: string): Prefs =
+proc getPrefs*(id: string; cfg: Config): Prefs =
   if id.len == 0:
-    return getDefaultPrefs(hostname)
+    return getDefaultPrefs(cfg)
 
   withDb:
     try:
       result.getOne("id = ?", id)
     except KeyError:
-      result = getDefaultPrefs(hostname)
+      result = getDefaultPrefs(cfg)
 
-proc resetPrefs*(prefs: var Prefs; hostname: string) =
-  var defPrefs = getDefaultPrefs(hostname)
+proc resetPrefs*(prefs: var Prefs; cfg: Config) =
+  var defPrefs = getDefaultPrefs(cfg)
   defPrefs.id = prefs.id
   cache(defPrefs)
   prefs = defPrefs

+ 6 - 1
src/prefs_impl.nim

@@ -51,6 +51,9 @@ const prefList*: OrderedTable[string, seq[Pref]] = {
   ],
 
   "Display": @[
+    Pref(kind: select, name: "theme", label: "Theme",
+         defaultOption: "Dark"),
+
     Pref(kind: checkbox, name: "hideTweetStats",
          label: "Hide tweet stats (replies, retweets, likes)",
          defaultState: false),
@@ -94,10 +97,12 @@ macro genUpdatePrefs*(): untyped =
     of input:
       result.add quote do: prefs.`ident` = xmltree.escape(strip(`value`))
     of select:
+      let name = pref.name
       let options = pref.options
       let default = pref.defaultOption
       result.add quote do:
-        if `value` in `options`: prefs.`ident` = `value`
+        if `name` == "theme": prefs.`ident` = `value`
+        elif `value` in `options`: prefs.`ident` = `value`
         else: prefs.`ident` = `default`
 
   result.add quote do:

+ 8 - 3
src/routes/preferences.nim

@@ -1,4 +1,4 @@
-import strutils, uri
+import strutils, uri, os, algorithm
 
 import jester
 
@@ -8,13 +8,18 @@ import ../views/[general, preferences]
 
 export preferences
 
+proc findThemes*(dir: string): seq[string] =
+  for kind, path in walkDir(dir / "css" / "themes"):
+    result.add path.splitFile.name.capitalizeAscii
+  sort(result)
+
 proc createPrefRouter*(cfg: Config) =
   router preferences:
     template savePrefs(): untyped =
       setCookie("preferences", $prefs.id, daysForward(360), httpOnly=true, secure=cfg.useHttps)
 
     get "/settings":
-      let html = renderPreferences(cookiePrefs(), refPath())
+      let html = renderPreferences(cookiePrefs(), refPath(), findThemes(cfg.staticDir))
       resp renderMain(html, request, cfg, "Preferences")
 
     get "/settings/@i?":
@@ -28,7 +33,7 @@ proc createPrefRouter*(cfg: Config) =
 
     post "/resetprefs":
       var prefs = cookiePrefs()
-      resetPrefs(prefs, cfg.hostname)
+      resetPrefs(prefs, cfg)
       savePrefs()
       redirect($(parseUri("/settings") ? filterParams(request.params)))
 

+ 1 - 1
src/routes/router_utils.nim

@@ -2,7 +2,7 @@ import ../utils, ../prefs
 export utils, prefs
 
 template cookiePrefs*(): untyped {.dirty.} =
-  getPrefs(request.cookies.getOrDefault("preferences"), cfg.hostname)
+  getPrefs(request.cookies.getOrDefault("preferences"), cfg)
 
 template getPath*(): untyped {.dirty.} =
   $(parseUri(request.path) ? filterParams(request.params))

+ 6 - 6
src/sass/general.scss

@@ -7,15 +7,15 @@
 }
 
 .error-panel {
-    @include center-panel($error_red);
+    @include center-panel(var(--error_red));
 }
 
 .search-bar > form {
-    @include center-panel($darkest-grey);
+    @include center-panel(var(--darkest_grey));
 
     button {
-        background: #303030;
-        color: $fg_color;
+        background: var(--bg_elements);
+        color: var(--fg_color);
         border: 0;
         border-radius: 3px;
         cursor: pointer;
@@ -27,8 +27,8 @@
     input {
         font-size: 16px;
         width: 100%;
-        background: $bg_elements;
-        color: $fg_color;
+        background: var(--bg_elements);
+        color: var(--fg_color);
         border: 0;
         border-radius: 4px;
         padding: 4px;

+ 4 - 4
src/sass/include/_mixins.css

@@ -19,11 +19,11 @@
 
     &:hover {
         .overlay-circle {
-            border-color: $accent;
+            border-color: var(--play_button_hover);
         }
 
         .overlay-triangle {
-            border-color: transparent transparent transparent $accent;
+            border-color: transparent transparent transparent var(--play_button_hover);
         }
     }
 }
@@ -51,11 +51,11 @@
 
 @mixin input-colors {
     &:hover {
-        border-color: $accent;
+        border-color: var(--accent);
     }
 
     &:active {
-        border-color: $accent_light;
+        border-color: var(--accent_light);
     }
 }
 

+ 10 - 2
src/sass/include/_variables.scss

@@ -2,11 +2,13 @@
 $bg_color: #0F0F0F;
 $fg_color: #F8F8F2;
 $fg_faded: #F8F8F2CF;
-$fg_dark: #9d9da0;
+$fg_dark: #FF6C60;
+$fg_nav: #FF6C60;
 
 $bg_panel: #161616;
 $bg_elements: #121212;
 $bg_overlays: #1F1F1F;
+$bg_hover: #1A1A1A;
 
 $grey: #888889;
 $dark_grey: #404040;
@@ -19,11 +21,17 @@ $accent_light: #FFACA0;
 $accent_dark: #8A3731;
 $accent_border: #FF6C6091;
 
-$play_button_red: #D8574D;
+$play_button: #D8574D;
+$play_button_hover: #FF6C60;
+
 $more_replies_dots: #AD433B;
 $error_red: #420A05;
 
 $verified_blue: #1DA1F2;
+$icon_text: $fg_color;
+
+$tab: $fg_color;
+$tab_selected: $accent;
 
 $shadow: rgba(0,0,0,.6);
 $shadow_dark: rgba(0,0,0,.2);

+ 44 - 7
src/sass/index.scss

@@ -9,8 +9,45 @@
 @import 'search';
 
 body {
-    background-color: $bg_color;
-    color: $fg_color;
+    // colors
+    --bg_color: #{$bg_color};
+    --fg_color: #{$fg_color};
+    --fg_faded: #{$fg_faded};
+    --fg_dark: #{$fg_dark};
+    --fg_nav: #{$fg_nav};
+
+    --bg_panel: #{$bg_panel};
+    --bg_elements: #{$bg_elements};
+    --bg_overlays: #{$bg_overlays};
+    --bg_hover: #{$bg_hover};
+
+    --grey: #{$grey};
+    --dark_grey: #{$dark_grey};
+    --darker_grey: #{$darker_grey};
+    --darkest_grey: #{$darkest_grey};
+    --border_grey: #{$border_grey};
+
+    --accent: #{$accent};
+    --accent_light: #{$accent_light};
+    --accent_dark: #{$accent_dark};
+    --accent_border: #{$accent_border};
+
+    --play_button: #{$play_button};
+    --play_button_hover: #{$play_button_hover};
+
+    --more_replies_dots: #{$more_replies_dots};
+    --error_red: #{$error_red};
+
+    --verified_blue: #{$verified_blue};
+    --icon_text: #{$icon_text};
+
+    --tab: #{$fg_color};
+    --tab_selected: #{$accent};
+
+    --profile_stat: #{$fg_color};
+
+    background-color: var(--bg_color);
+    color: var(--fg_color);
     font-family: $font_0, $font_1, $font_2, $font_3;
     font-size: 14px;
     line-height: 1.3;
@@ -36,7 +73,7 @@ p {
 }
 
 a {
-    color: $accent;
+    color: var(--accent);
 
     &:hover {
         text-decoration: underline;
@@ -54,7 +91,7 @@ legend {
     padding: .6em 0 .3em 0;
     border: 0;
     font-size: 16px;
-    border-bottom: 1px solid $border_grey;
+    border-bottom: 1px solid var(--border_grey);
     margin-bottom: 8px;
 }
 
@@ -80,7 +117,7 @@ ul {
     width: 100%;
     margin: 0 auto;
     margin-top: 10px;
-    background-color: $bg_overlays;
+    background-color: var(--bg_overlays);
     padding: 10px 15px;
     align-self: start;
 
@@ -91,8 +128,8 @@ ul {
 }
 
 .verified-icon {
-    color: $fg_color;
-    background-color: $verified_blue;
+    color: var(--icon_text);
+    background-color: var(--verified_blue);
     border-radius: 50%;
     flex-shrink: 0;
     margin: 2px 0 3px 3px;

+ 26 - 15
src/sass/inputs.scss

@@ -3,9 +3,9 @@
 
 button {
     @include input-colors;
-    background-color: $bg_elements;
-    color: $fg_color;
-    border: 1px solid $accent_border;
+    background-color: var(--bg_elements);
+    color: var(--fg_color);
+    border: 1px solid var(--accent_border);
     padding: 3px 6px;
     font-size: 14px;
     cursor: pointer;
@@ -13,12 +13,13 @@ button {
 }
 
 input[type="text"],
-input[type="date"] {
+input[type="date"],
+select {
     @include input-colors;
-    background-color: $bg_elements;
+    background-color: var(--bg_elements);
     padding: 1px 4px;
-    color: $fg_color;
-    border: 1px solid $accent_border;
+    color: var(--fg_color);
+    border: 1px solid var(--accent_border);
     border-radius: 0;
     font-size: 14px;
 }
@@ -40,8 +41,8 @@ input::-webkit-calendar-picker-indicator {
 input::-webkit-datetime-edit-day-field:focus,
 input::-webkit-datetime-edit-month-field:focus,
 input::-webkit-datetime-edit-year-field:focus {
-    background-color: $accent;
-    color: $fg_color;
+    background-color: var(--accent);
+    color: var(--fg_color);
     outline: none;
 }
 
@@ -64,7 +65,7 @@ input::-webkit-datetime-edit-year-field:focus {
 }
 
 .icon-button button {
-    color: $accent;
+    color: var(--accent);
     text-decoration: none;
     background: none;
     border: none;
@@ -73,7 +74,7 @@ input::-webkit-datetime-edit-year-field:focus {
     padding-left: 4px;
 
     &:hover {
-        color: $accent_light;
+        color: var(--accent_light);
     }
 }
 
@@ -83,8 +84,8 @@ input::-webkit-datetime-edit-year-field:focus {
     right: 0;
     height: 17px;
     width: 17px;
-    background-color: $bg_elements;
-    border: 1px solid $accent_border;
+    background-color: var(--bg_elements);
+    border: 1px solid var(--accent_border);
 
     &:after {
         content: "";
@@ -114,11 +115,11 @@ input::-webkit-datetime-edit-year-field:focus {
     }
 
     &:hover input ~ .checkbox {
-        border-color: $accent;
+        border-color: var(--accent);
     }
 
     &:active input ~ .checkbox {
-        border-color: $accent_light;
+        border-color: var(--accent_light);
     }
 
     .checkbox:after {
@@ -143,6 +144,16 @@ input::-webkit-datetime-edit-year-field:focus {
         padding-right: 135px;
     }
 
+    select {
+        position: absolute;
+        top: 0;
+        right: 0;
+        display: block;
+        -moz-appearance: none;
+        -webkit-appearance: none;
+        appearance: none;
+    }
+
     input[type="text"] {
         position: absolute;
         right: 0;

+ 7 - 3
src/sass/navbar.scss

@@ -4,12 +4,16 @@ nav {
     display: flex;
     align-items: center;
     position: fixed;
-    background-color: $bg_overlays;
+    background-color: var(--bg_overlays);
     box-shadow: 0 0 4px $shadow;
     padding: 0;
     width: 100%;
     height: 50px;
     z-index: 1000;
+
+    a, .icon-button button {
+        color: var(--fg_nav);
+    }
 }
 
 .inner-nav {
@@ -26,7 +30,7 @@ nav {
     font-weight: 600;
 
     &:hover {
-        color: $accent_light;
+        color: var(--accent_light);
         text-decoration: unset;
     }
 }
@@ -54,7 +58,7 @@ nav {
         padding-left: 4px;
 
         &:hover {
-            color: $accent_light;
+            color: var(--accent_light);
             text-decoration: unset;
         }
     }

+ 8 - 6
src/sass/profile/card.scss

@@ -3,7 +3,7 @@
 
 .profile-card {
     flex-wrap: wrap;
-    background: $bg_panel;
+    background: var(--bg_panel);
     padding: 12px;
     display: flex;
 }
@@ -20,14 +20,14 @@
 
 .profile-card-username {
     @include breakable;
-    color: $fg_color;
+    color: var(--fg_color);
     font-size: 14px;
     display: block;
 }
 
 .profile-card-fullname {
     @include breakable;
-    color: $fg_color;
+    color: var(--fg_color);
     font-size: 16px;
     font-weight: bold;
     text-shadow: none;
@@ -45,8 +45,8 @@
         width: calc(100% - 8px);
         height: 100%;
         margin: 0;
-        border: 4px solid $darker_grey;
-        background: $bg_color;
+        border: 4px solid var(--darker_grey);
+        background: var(--bg_color);
     }
 }
 
@@ -67,7 +67,7 @@
     }
 
     .profile-joindate, .profile-location, profile-website {
-        color: $fg_faded;
+        color: var(--fg_faded);
         margin: 2px 0;
         width: 100%;
     }
@@ -94,10 +94,12 @@
 
 .profile-stat-header {
     font-weight: bold;
+    color: var(--profile_stat);
 }
 
 .profile-stat-num {
     display: block;
+    color: var(--profile_stat);
 }
 
 @media(max-width: 600px) {

+ 2 - 2
src/sass/profile/photo-rail.scss

@@ -3,7 +3,7 @@
 .photo-rail {
     &-card {
         float: left;
-        background: $bg_panel;
+        background: var(--bg_panel);
         border-radius: 0 0 4px 4px;
         width: 100%;
         margin: 5px 0;
@@ -18,7 +18,7 @@
         display: none;
         width: calc(100% - 24px);
         float: unset;
-        color: $accent;
+        color: var(--accent);
         justify-content: space-between;
     }
 

+ 3 - 3
src/sass/search.scss

@@ -29,9 +29,9 @@
 
     > label {
         display: inline;
-        background-color: #121212;
-        color: #F8F8F2;
-        border: 1px solid #FF6C6091;
+        background-color: var(--bg_elements);
+        color: var(--fg_color);
+        border: 1px solid var(--accent_border);
         padding: 1px 6px 2px 6px;
         font-size: 14px;
         cursor: pointer;

+ 19 - 19
src/sass/timeline.scss

@@ -5,15 +5,15 @@
 }
 
 .timeline {
-    background-color: $bg_panel;
+    background-color: var(--bg_panel);
 
     > div:not(:first-child) {
-        border-top: 1px solid $border_grey;
+        border-top: 1px solid var(--border_grey);
     }
 }
 
 .timeline-header {
-    background-color: $bg_panel;
+    background-color: var(--bg_panel);
     text-align: center;
     padding: 8px;
     display: block;
@@ -31,7 +31,7 @@
     flex-wrap: wrap;
     list-style: none;
     margin: 0 0 5px 0;
-    background-color: $bg_panel;
+    background-color: var(--bg_panel);
     padding: 0;
 }
 
@@ -42,7 +42,7 @@
 
     a {
         border-bottom: .1rem solid transparent;
-        color: inherit;
+        color: var(--tab);
         display: block;
         padding: 8px 0;
         text-decoration: none;
@@ -53,14 +53,14 @@
         }
 
         &.active {
-            border-bottom-color: $accent;
-            color: $accent;
+            border-bottom-color: var(--tab_selected);
+            color: var(--tab_selected);
         }
     }
 
     &.active a {
-        border-bottom-color: $accent;
-        color: $accent;
+        border-bottom-color: var(--tab_selected);
+        color: var(--tab_selected);
     }
 
     &.wide {
@@ -69,7 +69,7 @@
 }
 
 .timeline-footer {
-    background-color: $bg_panel;
+    background-color: var(--bg_panel);
     padding: 6px 0;
 }
 
@@ -81,48 +81,48 @@
     }
 
     h2 {
-        color: $accent;
+        color: var(--accent);
         font-size: 20px;
         font-weight: 600;
     }
 }
 
 .timeline-none {
-    color: $accent;
+    color: var(--accent);
     font-size: 20px;
     font-weight: 600;
     text-align: center;
 }
 
 .timeline-end {
-    background-color: $bg_panel;
-    color: $accent;
+    background-color: var(--bg_panel);
+    color: var(--accent);
     font-size: 16px;
     font-weight: 600;
     text-align: center;
 }
 
 .show-more {
-    background-color: $bg_panel;
+    background-color: var(--bg_panel);
     text-align: center;
     padding: .75em 0;
     display: block !important;
 
     a {
-        background-color: $darkest_grey;
+        background-color: var(--darkest_grey);
         display: inline-block;
         height: 2em;
         padding: 0 2em;
         line-height: 2em;
 
         &:hover {
-            background-color: $darker_grey;
+            background-color: var(--darker_grey);
         }
     }
 }
 
 .top-ref {
-    background-color: #0f0f0f;
+    background-color: var(--bg_color);
     border-top: none !important;
 
     .icon-down {
@@ -132,7 +132,7 @@
         text-decoration: none;
 
         &:hover {
-            color: $accent_light;
+            color: var(--accent_light);
         }
 
         &::before {

+ 11 - 7
src/sass/tweet/_base.scss

@@ -52,7 +52,7 @@
     max-width: 80%;
     font-size: 14px;
     font-weight: 700;
-    color: $fg_color;
+    color: var(--fg_color);
 }
 
 .username {
@@ -68,10 +68,14 @@
     margin-left: 4px;
 }
 
+.tweet-date a, .username, .show-more a {
+    color: var(--fg_dark);
+}
+
 .tweet-published {
     margin: 0;
     margin-top: 5px;
-    color: $grey;
+    color: var(--grey);
     pointer-events: all;
 }
 
@@ -89,7 +93,7 @@
 }
 
 .replying-to {
-    color: $fg_dark;
+    color: var(--fg_faded);
     margin: -2px 0 4px;
 
     a {
@@ -99,7 +103,7 @@
 
 .retweet-header, .pinned, .tweet-stats {
     align-content: center;
-    color: $grey;
+    color: var(--grey);
     display: flex;
     flex-shrink: 0;
     flex-wrap: wrap;
@@ -134,9 +138,9 @@
     width: 100%;
     height: 100%;
     padding: 12px;
-    border: solid 1px $dark_grey;
+    border: solid 1px var(--dark_grey);
     border-radius: 10px;
-    background-color: $bg_color;
+    background-color: var(--bg_color);
 }
 
 .tweet-link {
@@ -147,6 +151,6 @@
     position: absolute;
 
     &:hover {
-        background-color: #1a1a1a;
+        background-color: var(--bg_hover);
     }
 }

+ 6 - 6
src/sass/tweet/card.scss

@@ -10,15 +10,15 @@
     border-radius: 10px;
     border-width: 1px;
     border-style: solid;
-    border-color: $dark_grey;
-    background-color: $bg_elements;
+    border-color: var(--dark_grey);
+    background-color: var(--bg_elements);
     overflow: hidden;
     color: inherit;
     display: flex;
     text-decoration: none !important;
 
     &:hover {
-        border-color: $grey;
+        border-color: var(--grey);
     }
 
     .attachments {
@@ -43,7 +43,7 @@
 
 .card-destination {
     @include ellipsis;
-    color: $grey;
+    color: var(--grey);
     display: block;
 }
 
@@ -73,7 +73,7 @@
     left: 0;
     bottom: 0;
     right: 0;
-    background-color: $fg_color;
+    background-color: var(--fg_color);
 
     img {
         width: 100%;
@@ -107,7 +107,7 @@
     .card-image {
         position: unset;
         border-style: solid;
-        border-color: $dark_grey;
+        border-color: var(--dark_grey);
         border-width: 0;
         border-bottom-width: 1px;
     }

+ 4 - 4
src/sass/tweet/media.scss

@@ -26,7 +26,7 @@
     border-radius: 7px;
     overflow: hidden;
     flex-flow: column;
-    background-color: $bg_color;
+    background-color: var(--bg_color);
     align-items: center;
     pointer-events: all;
 
@@ -81,13 +81,13 @@
 
 .overlay-circle {
     border-radius: 50%;
-    background-color: $dark_grey;
+    background-color: var(--dark_grey);
     width: 40px;
     height: 40px;
     align-items: center;
     display: flex;
     border-width: 5px;
-    border-color: $play_button_red;
+    border-color: var(--play_button);
     border-style: solid;
 }
 
@@ -96,7 +96,7 @@
     height: 0;
     border-style: solid;
     border-width: 12px 0 12px 17px;
-    border-color: transparent transparent transparent $play_button_red;
+    border-color: transparent transparent transparent var(--play_button);
     margin-left: 14px;
 }
 

+ 4 - 4
src/sass/tweet/poll.scss

@@ -5,7 +5,7 @@
     position: relative;
     margin: 6px 0;
     height: 26px;
-    background: $bg_color;
+    background: var(--bg_color);
     border-radius: 5px;
     display: flex;
     align-items: center;
@@ -14,7 +14,7 @@
 .poll-choice-bar {
     height: 100%;
     position: absolute;
-    background: $dark_grey;
+    background: var(--dark_grey);
 }
 
 .poll-choice-value {
@@ -33,10 +33,10 @@
 }
 
 .poll-info {
-    color: $grey;
+    color: var(--grey);
     pointer-events: all;
 }
 
 .leader .poll-choice-bar {
-    background: $accent_dark;
+    background: var(--accent_dark);
 }

+ 6 - 6
src/sass/tweet/quote.scss

@@ -2,20 +2,20 @@
 
 .quote {
     margin-top: 10px;
-    border: solid 1px $dark_grey;
+    border: solid 1px var(--dark_grey);
     border-radius: 10px;
-    background-color: $bg_elements;
+    background-color: var(--bg_elements);
     overflow: auto;
     padding: 6px;
     position: relative;
     pointer-events: all;
 
     &:hover {
-        border-color: $grey;
+        border-color: var(--grey);
     }
 
     &.unavailable:hover {
-        border-color: $dark_grey;
+        border-color: var(--dark_grey);
     }
 }
 
@@ -85,7 +85,7 @@
 }
 
 .quote-sensitive {
-    background: $darker_grey;
+    background: var(--darker_grey);
     width: 102px;
     height: 102px;
     border-radius: 12px;
@@ -96,7 +96,7 @@
 
 .quote-sensitive-icon {
     font-size: 40px;
-    color: $grey;
+    color: var(--grey);
 }
 
 @media(max-width: 600px) {

+ 4 - 4
src/sass/tweet/thread.scss

@@ -11,7 +11,7 @@
 
 .main-thread {
     margin-bottom: 20px;
-    background-color: $bg_panel;
+    background-color: var(--bg_panel);
 }
 
 .main-tweet, .replies {
@@ -24,14 +24,14 @@
 }
 
 .reply {
-    background-color: $bg_panel;
+    background-color: var(--bg_panel);
     margin-bottom: 10px;
 }
 
 .thread-line {
     .timeline-item::before,
     &.timeline-item::before {
-        background: $accent_dark;
+        background: var(--accent_dark);
         content: '';
         position: relative;
         min-width: 3px;
@@ -53,7 +53,7 @@
     .more-replies::before {
         content: '...';
         background: unset;
-        color: $more_replies_dots;
+        color: var(--more_replies_dots);
         font-weight: bold;
         font-size: 20px;
         line-height: 0.25em;

+ 1 - 0
src/types.nim

@@ -177,6 +177,7 @@ type
     hostname*: string
     cacheDir*: string
     profileCacheTime*: int
+    defaultTheme*: string
 
 proc contains*(thread: Chain; tweet: Tweet): bool =
   thread.content.anyIt(it.id == tweet.id)

+ 3 - 1
src/views/general.nim

@@ -29,11 +29,13 @@ proc renderNavbar*(title, rss: string; req: Request): VNode =
 
 proc renderMain*(body: VNode; req: Request; cfg: Config; titleText=""; desc="";
                  rss=""; `type`="article"; video=""; images: seq[string] = @[]): string =
-  let prefs = getPrefs(req.cookies.getOrDefault("preferences"), cfg.hostname)
+  let prefs = getPrefs(req.cookies.getOrDefault("preferences"), cfg)
+  let theme = "/css/themes/" & toLowerAscii(prefs.theme) & ".css"
   let node = buildHtml(html(lang="en")):
     head:
       link(rel="stylesheet", `type`="text/css", href="/css/style.css")
       link(rel="stylesheet", `type`="text/css", href="/css/fontello.css")
+      link(rel="stylesheet", `type`="text/css", href=theme)
 
       link(rel="apple-touch-icon", sizes="180x180", href="/apple-touch-icon.png")
       link(rel="icon", type="image/png", sizes="32x32", href="/favicon-32x32.png")

+ 7 - 3
src/views/preferences.nim

@@ -1,4 +1,4 @@
-import tables, macros, strutils
+import tables, macros, strutils, os
 import karax/[karaxdsl, vdom, vstyles]
 
 import renderutils
@@ -22,12 +22,16 @@ macro renderPrefs*(): untyped =
 
       case pref.kind
       of checkbox: discard
-      of select: stmt[0].add newLit(pref.options)
       of input: stmt[0].add newLit(pref.placeholder)
+      of select:
+        if pref.name == "theme":
+          stmt[0].add ident("themes")
+        else:
+          stmt[0].add newLit(pref.options)
 
       result[2].add stmt
 
-proc renderPreferences*(prefs: Prefs; path: string): VNode =
+proc renderPreferences*(prefs: Prefs; path: string; themes: seq[string]): VNode =
   buildHtml(tdiv(class="overlay-panel")):
     fieldset(class="preferences"):
       form(`method`="post", action="/saveprefs"):

+ 1 - 1
src/views/renderutils.nim

@@ -71,7 +71,7 @@ proc genInput*(pref, label, state, placeholder: string; class=""; autofocus=fals
     verbatim &"<input name={pref} type=\"text\" placeholder=\"{p}\" value=\"{s}\" {a}/>"
 
 proc genSelect*(pref, label, state: string; options: seq[string]): VNode =
-  buildHtml(tdiv(class="pref-group")):
+  buildHtml(tdiv(class="pref-group pref-input")):
     label(`for`=pref): text label
     select(name=pref):
       for opt in options: