Repository: 12rambau/sepal_ui

Issue:
automatically detect the theme used and adapt the frontend accordingly 
I have seen it on several  zoom : it's so cool to display everything in dark but look so wrong when people are actually just developing in there own env in light theme. 

Several situation that I would like to handle : 
- voila theme
- jupyterLab theme 
- jupyter notebook theme

Discussion:

Patch:
diff --git a/docs/source/modules/sepal_ui.rst b/docs/source/modules/sepal_ui.rst
index 366a3fb2..2650327b 100644
--- a/docs/source/modules/sepal_ui.rst
+++ b/docs/source/modules/sepal_ui.rst
@@ -24,3 +24,11 @@ Content
     sepal_ui.theme
     sepal_ui.color
     sepal_ui.config_file
+    
+.. rubric:: Attributes
+
+.. autosummary::
+    
+    sepal_ui.get_theme
+    
+.. autofunction:: sepal_ui.get_theme
diff --git a/docs/source/modules/sepal_ui.scripts.utils.rst b/docs/source/modules/sepal_ui.scripts.utils.rst
index d4a30a14..6c404e8a 100644
--- a/docs/source/modules/sepal_ui.scripts.utils.rst
+++ b/docs/source/modules/sepal_ui.scripts.utils.rst
@@ -23,6 +23,7 @@ sepal\_ui.scripts.utils
         switch
         to_colors
         set_config_locale
+        set_config_theme
       
 .. autofunction:: sepal_ui.scripts.utils.catch_errors
 
@@ -54,6 +55,8 @@ sepal\_ui.scripts.utils
 
 .. autofunction:: sepal_ui.scripts.utils.set_config_locale
 
+.. autofunction:: sepal_ui.scripts.utils.set_config_theme
+
 
       
 
diff --git a/docs/source/modules/sepal_ui.sepalwidgets.AppBar.rst b/docs/source/modules/sepal_ui.sepalwidgets.AppBar.rst
index 3787c444..011eae2b 100644
--- a/docs/source/modules/sepal_ui.sepalwidgets.AppBar.rst
+++ b/docs/source/modules/sepal_ui.sepalwidgets.AppBar.rst
@@ -10,6 +10,7 @@ sepal\_ui.sepalwidgets.AppBar
         ~AppBar.toogle_button
         ~AppBar.title
         ~AppBar.locale
+        ~AppBar.theme
     
     .. rubric:: Methods
     
diff --git a/docs/source/modules/sepal_ui.sepalwidgets.ThemeSelect.rst b/docs/source/modules/sepal_ui.sepalwidgets.ThemeSelect.rst
new file mode 100644
index 00000000..5c4e14d0
--- /dev/null
+++ b/docs/source/modules/sepal_ui.sepalwidgets.ThemeSelect.rst
@@ -0,0 +1,21 @@
+sepal\_ui.sepalwidgets.ThemeSelect
+==================================
+
+.. autoclass:: sepal_ui.sepalwidgets.ThemeSelect
+
+    .. rubric:: Attributes
+    
+    .. autosummary::
+    
+        ~ThemeSelect.THEME_ICONS
+        ~ThemeSelect.theme
+        
+    .. rubric:: Methods
+    
+    .. autosummary::
+        :nosignatures:
+        
+        ~ThemeSelect.toggle_theme
+        
+.. automethod:: sepal_ui.sepalwidgets.ThemeSelect.toggle_theme
+        
\ No newline at end of file
diff --git a/docs/source/modules/sepal_ui.sepalwidgets.rst b/docs/source/modules/sepal_ui.sepalwidgets.rst
index b1bc9ef1..9d0c6864 100644
--- a/docs/source/modules/sepal_ui.sepalwidgets.rst
+++ b/docs/source/modules/sepal_ui.sepalwidgets.rst
@@ -33,3 +33,4 @@ sepal\_ui.sepalwidgets
    sepal_ui.sepalwidgets.Tooltip
    sepal_ui.sepalwidgets.VectorField 
    sepal_ui.sepalwidgets.LocaleSelect
+   sepal_ui.sepalwidgets.ThemeSelect
diff --git a/sepal_ui/__init__.py b/sepal_ui/__init__.py
index 69eccad6..bf79e175 100644
--- a/sepal_ui/__init__.py
+++ b/sepal_ui/__init__.py
@@ -1,18 +1,49 @@
+from types import SimpleNamespace
+from pathlib import Path
+from configparser import ConfigParser
+
+import ipyvuetify as v
+
+from sepal_ui.frontend import styles
+
 __author__ = """Pierrick Rambaud"""
 __email__ = "pierrick.rambaud49@gmail.com"
 __version__ = "2.6.2"
 
-# direct access to colors
-from sepal_ui.frontend import styles
-import ipyvuetify as v
-from types import SimpleNamespace
-from pathlib import Path
 
-theme = v.theme.themes.dark if v.theme.dark else v.theme.themes.light
+def get_theme(config_file):
+    """
+    get the theme from the config file (default to dark)
+
+    Return:
+        (str): the theme to use
+    """
+
+    # init theme
+    theme = "dark"
+
+    # read the config file if existing
+    if config_file.is_file():
+        config = ConfigParser()
+        config.read(config_file)
+        theme = config.get("sepal-ui", "theme", fallback="dark")
+
+    # set vuetify theme
+    v.theme.dark = True if theme == "dark" else False
+
+    return theme
+
+
+config_file = Path.home() / ".sepal-ui-config"
+"Pathlib.Path: the configuration file generated by sepal-ui based application to save parameters as language or theme"
+
+theme = getattr(v.theme.themes, get_theme(config_file))
 "traitlets: the theme used in sepal"
 
 color = SimpleNamespace(
-    bg=styles.bg_color,
+    main=theme.main,
+    darker=theme.darker,
+    bg=theme.bg_color,
     primary=theme.primary,
     accent=theme.accent,
     secondary=theme.secondary,
@@ -20,8 +51,6 @@ color = SimpleNamespace(
     info=theme.info,
     warning=theme.warning,
     error=theme.error,
+    menu=theme.menu,
 )
-'SimpleNamespace: the colors of sepal. members are in the following list: "bg, primary, accent, secondary, success, info, warning, error". They will render according to the selected theme.'
-
-config_file = Path.home() / ".sepal-ui-config"
-"Pathlib.Path: the configuration file generated by sepal-ui based application to save parameters as language or theme"
+'SimpleNamespace: the colors of sepal. members are in the following list: "main, darker, bg, primary, accent, secondary, success, info, warning, error, menu". They will render according to the selected theme.'
diff --git a/sepal_ui/aoi/aoi_model.py b/sepal_ui/aoi/aoi_model.py
index d713e7b9..0ecfdd74 100644
--- a/sepal_ui/aoi/aoi_model.py
+++ b/sepal_ui/aoi/aoi_model.py
@@ -10,6 +10,7 @@ from ipyleaflet import GeoJSON
 import geemap
 import ee
 
+from sepal_ui import color
 from sepal_ui.frontend.styles import AOI_STYLE
 from sepal_ui.scripts import utils as su
 from sepal_ui.scripts import gee
@@ -626,9 +627,12 @@ class AoiModel(Model):
         for f in data["features"]:
             f["properties"]["name"] = self.name
 
+        # adapt the style to the theme
+        style = {**AOI_STYLE, "color": color.success, "fillColor": color.success}
+
         # create a GeoJSON object
         self.ipygeojson = GeoJSON(
-            data=data, style=AOI_STYLE, name="aoi", attribution="SEPA(c)"
+            data=data, style=style, name="aoi", attribution="SEPAL(c)"
         )
 
         return self.ipygeojson
diff --git a/sepal_ui/bin/module_theme b/sepal_ui/bin/module_theme
new file mode 100755
index 00000000..8d153333
--- /dev/null
+++ b/sepal_ui/bin/module_theme
@@ -0,0 +1,55 @@
+#!/usr/bin/python3
+
+from colorama import init, Fore, Style
+
+from sepal_ui.scripts import utils as su
+
+# init colors for all plateforms
+init()
+
+
+def check_theme(theme):
+    """
+    Check if the theme is a legit name
+
+    Return:
+        (bool)
+    """
+
+    themes = ["dark", "light"]
+
+    return theme in themes
+
+
+if __name__ == "__main__":
+
+    # welcome the user
+    print(Fore.YELLOW)
+    print("##########################################")
+    print("#                                        #")
+    print("#      SEPAL MODULE THEMING TOOL         #")
+    print("#                                        #")
+    print("##########################################")
+    print(Fore.RESET)
+    print(f"Welcome in the {Style.BRIGHT}module theming{Style.NORMAL} interface.")
+
+    # select a language
+    is_theme = False
+    while is_theme is False:
+
+        theme = input(f"{Fore.CYAN}Provide a theme name: \n{Fore.RESET}")
+        is_theme = check_theme(theme)
+
+        # display an error if the language does not exist
+        if is_theme is False:
+            print(
+                f'{Fore.RED} The provided theme name ("{theme}") is not a supported theme. {Fore.RESET}'
+            )
+
+    # write the new color code in the config file
+    su.set_config_theme(theme)
+
+    # display information
+    print(
+        f'{Fore.GREEN} The provided theme ("{theme}") has been set as default theme for all SEPAL applications.{Fore.RESET}'
+    )
diff --git a/sepal_ui/frontend/styles.py b/sepal_ui/frontend/styles.py
index cf4b2971..a48b3dea 100644
--- a/sepal_ui/frontend/styles.py
+++ b/sepal_ui/frontend/styles.py
@@ -2,24 +2,28 @@ from traitlets import Unicode
 from IPython.display import display
 import ipyvuetify as v
 
-# change vuetify theming
-v.theme.dark = True
-
 # set the colors for the dark theme
-v.theme.themes.dark.primary = "#B3842E"
+v.theme.themes.dark.primary = "#b3842e"
 v.theme.themes.dark.accent = "#a1458e"
 v.theme.themes.dark.secondary = "#324a88"
-v.theme.themes.dark.success = "#3F802A"
-v.theme.themes.dark.info = "#79B1C9"
+v.theme.themes.dark.success = "#3f802a"
+v.theme.themes.dark.info = "#79b1c9"
 v.theme.themes.dark.warning = "#b8721d"
-v.theme.themes.dark.error = "#A63228"
+v.theme.themes.dark.error = "#a63228"
 
-# fixed colors
-sepal_main = "#24221F"
-sepal_darker = "#1a1a1a"
+# fixed colors for drawer and appbar
+v.theme.themes.dark.main = "#24221f"
+v.theme.themes.light.main = "#2e7d32"
+v.theme.themes.dark.darker = "#1a1a1a"
+v.theme.themes.light.darker = "#005005"
 
 # set the background
-bg_color = "#121212" if v.theme.dark else "#fff"
+v.theme.themes.dark.bg_color = "#121212"
+v.theme.themes.light.bg_color = "#fff"
+
+# set a specific color for menus
+v.theme.themes.dark.menu = "#424242"
+v.theme.themes.light.menu = "#fff"
 
 
 class Styles(v.VuetifyTemplate):
@@ -49,30 +53,33 @@ class Styles(v.VuetifyTemplate):
 styles = Styles()
 display(styles)
 
-COMPONENTS = {
-    "PROGRESS_BAR": {
-        "color": "indigo",
-    }
-}
-
 # default styling of the aoi layer
 AOI_STYLE = {
     "stroke": True,
-    "color": v.theme.themes.dark.success,
+    "color": "grey",
     "weight": 2,
     "opacity": 1,
     "fill": True,
-    "fillColor": v.theme.themes.dark.success,
+    "fillColor": "grey",
     "fillOpacity": 0.4,
 }
 
+# the colors are set as follow.
+# 1 (True): dark theme
+# 0 (false): light theme
+# This will need to be changed if we want to support more than 2 theme
+COMPONENTS = {
+    "PROGRESS_BAR": {
+        "color": ["#2196f3", "#3f51b5"],
+    }
+}
 
-_folder = {"color": "amber", "icon": "far fa-folder"}
-_table = {"color": "green accent-4", "icon": "far fa-table"}
-_vector = {"color": "deep-purple", "icon": "far fa-vector-square"}
-_other = {"color": "light-blue", "icon": "far fa-file"}
-_parent = {"color": "white", "icon": "far fa-folder-open"}
-_image = {"color": "deep-purple", "icon": "far fa-image"}
+_folder = {"color": ["#ffca28", "#ffc107"], "icon": "far fa-folder"}
+_table = {"color": ["#4caf50", "#00c853"], "icon": "far fa-table"}
+_vector = {"color": ["#9c27b0", "#673ab7"], "icon": "far fa-vector-square"}
+_other = {"color": ["#00bcd4", "#03a9f4"], "icon": "far fa-file"}
+_parent = {"color": ["#424242", "#ffffff"], "icon": "far fa-folder-open"}
+_image = {"color": ["#9c27b0", "#673ab7"], "icon": "far fa-image"}
 
 ICON_TYPES = {
     "": _folder,
diff --git a/sepal_ui/mapping/mapping.py b/sepal_ui/mapping/mapping.py
index ab44e84c..1dae10f3 100644
--- a/sepal_ui/mapping/mapping.py
+++ b/sepal_ui/mapping/mapping.py
@@ -100,7 +100,10 @@ class SepalMap(geemap.Map):
         if len(basemaps):
             [self.add_basemap(basemap) for basemap in set(basemaps)]
         else:
-            self.add_basemap("CartoDB.DarkMatter")
+            default_basemap = (
+                "CartoDB.DarkMatter" if v.theme.dark is True else "CartoDB.Positron"
+            )
+            self.add_basemap(default_basemap)
 
         # add the base controls
         self.clear_controls()
diff --git a/sepal_ui/message/en/locale.json b/sepal_ui/message/en/locale.json
index da5c4efc..c96a4857 100644
--- a/sepal_ui/message/en/locale.json
+++ b/sepal_ui/message/en/locale.json
@@ -155,7 +155,10 @@
 		}
 	},
     "locale": {
-        "change": "The locale \"{0}\" will now be used as default language in SEPAL apps. please restart the current application to take this change into account. Note that if \"{0}\" is not avalaible in another application, SEPAL will fallback to the closest possible language",
+        "change": "The locale \"{0}\" will now be used as default language in SEPAL apps. Please restart the current application to take this change into account. Note that if \"{0}\" is not avalaible in another application, SEPAL will fallback to the closest possible language",
         "fallback": "The \"{0}\" locale is not available for this application. We will be using \"{1}\" instead. Please report to the maintainer if you want to add \"{0}\" to the suported languages."
+    },
+    "theme": {
+        "change": "The theme \"{0}\" will be used as default theme in SEPAL apps. Please restart the current application to take this change into account."
     }
 }
\ No newline at end of file
diff --git a/sepal_ui/scripts/utils.py b/sepal_ui/scripts/utils.py
index adbce606..2cd55df9 100644
--- a/sepal_ui/scripts/utils.py
+++ b/sepal_ui/scripts/utils.py
@@ -521,3 +521,31 @@ def set_config_locale(locale):
     config.write(config_file.open("w"))
 
     return
+
+
+@versionadded(version="2.7.0")
+def set_config_theme(theme):
+    """
+    Set the provided theme in the sepal-ui config file
+
+    Args:
+        theme (str): a theme name (currently supporting "dark" and "light")
+    """
+
+    config = ConfigParser()
+
+    # read the existing file if available
+    if config_file.is_file():
+        config.read(config_file)
+
+    # set the section if needed
+    if "sepal-ui" not in config.sections():
+        config.add_section("sepal-ui")
+
+    # set the value
+    config.set("sepal-ui", "theme", theme)
+
+    # save back the file
+    config.write(config_file.open("w"))
+
+    return
diff --git a/sepal_ui/sepalwidgets/app.py b/sepal_ui/sepalwidgets/app.py
index 2d26b7e1..15bcd5f2 100644
--- a/sepal_ui/sepalwidgets/app.py
+++ b/sepal_ui/sepalwidgets/app.py
@@ -2,20 +2,29 @@ from traitlets import link, Bool, observe
 from functools import partial
 from datetime import datetime
 from pathlib import Path
+from itertools import cycle
 
 import ipyvuetify as v
 from deprecated.sphinx import versionadded
 import pandas as pd
 from ipywidgets import jsdlink
 
+import sepal_ui
 from sepal_ui.sepalwidgets.sepalwidget import SepalWidget
 from sepal_ui import color
-from sepal_ui.frontend.styles import sepal_main, sepal_darker
 from sepal_ui.frontend import js
 from sepal_ui.scripts import utils as su
 from sepal_ui.message import ms
 
-__all__ = ["AppBar", "DrawerItem", "NavDrawer", "Footer", "App", "LocaleSelect"]
+__all__ = [
+    "AppBar",
+    "DrawerItem",
+    "NavDrawer",
+    "Footer",
+    "App",
+    "LocaleSelect",
+    "ThemeSelect",
+]
 
 
 class AppBar(v.AppBar, SepalWidget):
@@ -37,6 +46,9 @@ class AppBar(v.AppBar, SepalWidget):
     locale = None
     "sw.LocaleSelect: the locale selector of all apps"
 
+    theme = None
+    "sw.ThemeSelect: the theme selector of all apps"
+
     def __init__(self, title="SEPAL module", translator=None, **kwargs):
 
         self.toggle_button = v.Btn(
@@ -47,13 +59,20 @@ class AppBar(v.AppBar, SepalWidget):
         self.title = v.ToolbarTitle(children=[title])
 
         self.locale = LocaleSelect(translator=translator)
+        self.theme = ThemeSelect()
 
         # set the default parameters
-        kwargs["color"] = kwargs.pop("color", sepal_main)
+        kwargs["color"] = kwargs.pop("color", color.main)
         kwargs["class_"] = kwargs.pop("class_", "white--text")
         kwargs["dense"] = kwargs.pop("dense", True)
         kwargs["app"] = True
-        kwargs["children"] = [self.toggle_button, self.title, v.Spacer(), self.locale]
+        kwargs["children"] = [
+            self.toggle_button,
+            self.title,
+            v.Spacer(),
+            self.locale,
+            self.theme,
+        ]
 
         super().__init__(**kwargs)
 
@@ -252,7 +271,7 @@ class NavDrawer(v.NavigationDrawer, SepalWidget):
         # set default parameters
         kwargs["v_model"] = kwargs.pop("v_model", True)
         kwargs["app"] = True
-        kwargs["color"] = kwargs.pop("color", sepal_darker)
+        kwargs["color"] = kwargs.pop("color", color.darker)
         kwargs["children"] = children
 
         # call the constructor
@@ -313,7 +332,7 @@ class Footer(v.Footer, SepalWidget):
         text = text if text != "" else "SEPAL \u00A9 {}".format(datetime.today().year)
 
         # set default parameters
-        kwargs["color"] = kwargs.pop("color", sepal_main)
+        kwargs["color"] = kwargs.pop("color", color.main)
         kwargs["class_"] = kwargs.pop("class_", "white--text")
         kwargs["app"] = True
         kwargs["children"] = [text]
@@ -414,6 +433,7 @@ class App(v.App, SepalWidget):
 
         # add js event
         self.appBar.locale.observe(self._locale_info, "value")
+        self.appBar.theme.observe(self._theme_info, "v_model")
 
     def show_tile(self, name):
         """
@@ -442,13 +462,14 @@ class App(v.App, SepalWidget):
         return self
 
     @versionadded(version="2.4.1", reason="New end user interaction method")
-    def add_banner(self, msg, **kwargs):
+    def add_banner(self, msg, id_=None, **kwargs):
         """
         Display an alert object on top of the app to communicate development information to end user (release date, known issues, beta version). The alert is dissmisable and prominent
 
         Args:
             msg (str): the message to write in the Alert
             kwargs: any arguments of the v.Alert constructor. if set, 'children' will be overwritten.
+            id_ (str, optional): unique banner identificator to avoid multiple aggregations.
 
         Return:
             self
@@ -457,16 +478,28 @@ class App(v.App, SepalWidget):
         kwargs["type"] = kwargs.pop("type", "info")
         kwargs["border"] = kwargs.pop("border", "left")
         kwargs["class_"] = kwargs.pop("class_", "mt-5")
-        kwargs["transition"] = kwargs.pop("transition", "slide-x-transition")
+        kwargs["transition"] = kwargs.pop("transition", "scroll-x-transition")
         kwargs["prominent"] = kwargs.pop("prominent", True)
         kwargs["dismissible"] = kwargs.pop("dismissible", True)
         kwargs["children"] = [msg]  # cannot be overwritten
+        kwargs["attributes"] = {"id": id_}
+        kwargs["v_model"] = kwargs.pop("v_model", False)
+
+        # Verify if alert is already in the app.
+        children = self.content.children.copy()
 
-        # create the alert
+        # remove already existing alert
+        alert = next((c for c in children if c.attributes.get("id") == id_), False)
+        alert is False or children.remove(alert)
+
+        # create alert
         alert = v.Alert(**kwargs)
 
-        # add the alert to the app
-        self.content.children = [alert] + self.content.children.copy()
+        # add the alert to the app if not already there
+        self.content.children = [alert] + children
+
+        # Display the alert
+        alert.v_model = True
 
         return self
 
@@ -475,7 +508,16 @@ class App(v.App, SepalWidget):
 
         if change["new"] != "":
             msg = ms.locale.change.format(change["new"])
-            self.add_banner(msg, type="success")
+            self.add_banner(msg, id_="locale")
+
+        return
+
+    def _theme_info(self, change):
+        """display information about the theme change"""
+
+        if change["new"] != "":
+            msg = ms.theme.change.format(change["new"])
+            self.add_banner(msg, id_="theme")
 
         return
 
@@ -535,7 +577,7 @@ class LocaleSelect(v.Menu, SepalWidget):
         self.language_list = v.List(
             dense=True,
             flat=True,
-            color="grey darken-3",
+            color=color.menu,
             v_model=True,
             max_height="300px",
             style_="overflow: auto; border-radius: 0 0 0 0;",
@@ -600,3 +642,62 @@ class LocaleSelect(v.Menu, SepalWidget):
         su.set_config_locale(loc.code)
 
         return
+
+
+class ThemeSelect(v.Btn, SepalWidget):
+    """
+    A theme selector for sepal-ui based application.
+
+    It displays the currently requested theme (default to dark).
+    When value is changed, the sepal-ui config file is updated. It is designed to be used in a AppBar component.
+
+    .. versionadded:: 2.7.0
+
+    Args:
+        kwargs (dict, optional): any arguments for a Btn object, children and v_model will be override
+    """
+
+    THEME_ICONS = {"dark": "fas fa-moon", "light": "fas fa-sun"}
+    "dict: the dictionnry of icons to use for each theme (used as keys)"
+
+    theme = "dark"
+    "str: the current theme of the widget (default to dark)"
+
+    def __init__(self, **kwargs):
+
+        # get the current theme name
+        self.theme = sepal_ui.get_theme(sepal_ui.config_file)
+
+        # set the btn parameters
+        kwargs["x_small"] = kwargs.pop("x_small", True)
+        kwargs["fab"] = kwargs.pop("fab", True)
+        kwargs["class_"] = kwargs.pop("class_", "ml-2")
+        kwargs["children"] = [v.Icon(children=[self.THEME_ICONS[self.theme]])]
+        kwargs["v_model"] = self.theme
+
+        # create the btn
+        super().__init__(**kwargs)
+
+        # add some js events
+        self.on_event("click", self.toggle_theme)
+
+    def toggle_theme(self, widget, event, data):
+        """
+        toggle the btn icon from dark to light and adapt the configuration file at the same time
+        """
+        # use a cycle to go through the themes
+        theme_cycle = cycle(self.THEME_ICONS.keys())
+        next(t for t in theme_cycle if t == self.theme)
+        self.theme = next(t for t in theme_cycle)
+
+        # change icon
+        self.color = "info"
+        self.children[0].children = [self.THEME_ICONS[self.theme]]
+
+        # change the paramater file
+        su.set_config_theme(self.theme)
+
+        # trigger other events by changing v_model
+        self.v_model = self.theme
+
+        return
diff --git a/sepal_ui/sepalwidgets/inputs.py b/sepal_ui/sepalwidgets/inputs.py
index bf1adf0b..365d4f0b 100644
--- a/sepal_ui/sepalwidgets/inputs.py
+++ b/sepal_ui/sepalwidgets/inputs.py
@@ -9,7 +9,7 @@ import ee
 import geopandas as gpd
 from natsort import humansorted
 
-
+from sepal_ui import color
 from sepal_ui.message import ms
 from sepal_ui.frontend.styles import COMPONENTS, ICON_TYPES
 from sepal_ui.scripts import utils as su
@@ -215,13 +215,13 @@ class FileInput(v.Flex, SepalWidget):
 
         self.loading = v.ProgressLinear(
             indeterminate=False,
-            background_color="grey darken-3",
-            color=COMPONENTS["PROGRESS_BAR"]["color"],
+            background_color=color.menu,
+            color=COMPONENTS["PROGRESS_BAR"]["color"][v.theme.dark],
         )
 
         self.file_list = v.List(
             dense=True,
-            color="grey darken-3",
+            color=color.menu,
             flat=True,
             v_model=True,
             max_height="300px",
@@ -374,13 +374,13 @@ class FileInput(v.Flex, SepalWidget):
 
             if el.is_dir():
                 icon = ICON_TYPES[""]["icon"]
-                color = ICON_TYPES[""]["color"]
+                color = ICON_TYPES[""]["color"][v.theme.dark]
             elif el.suffix in ICON_TYPES.keys():
                 icon = ICON_TYPES[el.suffix]["icon"]
-                color = ICON_TYPES[el.suffix]["color"]
+                color = ICON_TYPES[el.suffix]["color"][v.theme.dark]
             else:
                 icon = ICON_TYPES["DEFAULT"]["icon"]
-                color = ICON_TYPES["DEFAULT"]["color"]
+                color = ICON_TYPES["DEFAULT"]["color"][v.theme.dark]
 
             children = [
                 v.ListItemAction(children=[v.Icon(color=color, children=[icon])]),
@@ -405,7 +405,7 @@ class FileInput(v.Flex, SepalWidget):
                 v.ListItemAction(
                     children=[
                         v.Icon(
-                            color=ICON_TYPES["PARENT"]["color"],
+                            color=ICON_TYPES["PARENT"]["color"][v.theme.dark],
                             children=[ICON_TYPES["PARENT"]["icon"]],
                         )
                     ]
diff --git a/sepal_ui/translator/translator.py b/sepal_ui/translator/translator.py
index 3982aad6..f0a39f77 100644
--- a/sepal_ui/translator/translator.py
+++ b/sepal_ui/translator/translator.py
@@ -119,7 +119,7 @@ class Translator(SimpleNamespace):
             if config_file.is_file():
                 config = ConfigParser()
                 config.read(config_file)
-                target = config["sepal-ui"]["locale"]
+                target = config.get("sepal-ui", "locale", fallback="en")
             else:
                 return ("en", None)
 
diff --git a/setup.py b/setup.py
index d7f3d03e..1b207b7e 100644
--- a/setup.py
+++ b/setup.py
@@ -57,7 +57,7 @@ setup_params = {
             "pytest",
         ],
         "doc": [
-            "jupyter-sphinx @ git+git://github.com/jupyter/jupyter-sphinx.git",
+            "jupyter-sphinx @ git+https://github.com/jupyter/jupyter-sphinx.git",
             "pydata-sphinx-theme",
             "sphinx-notfound-page",
             "Sphinx",


Test patch:
diff --git a/tests/test_LocaleSelect.py b/tests/test_LocaleSelect.py
index 5da8c6e0..1813b58d 100644
--- a/tests/test_LocaleSelect.py
+++ b/tests/test_LocaleSelect.py
@@ -6,7 +6,7 @@ from sepal_ui import sepalwidgets as sw
 from sepal_ui import config_file
 
 
-class TestBtn:
+class TestLocalSelect:
     def test_init(self, locale_select):
 
         # minimal btn
diff --git a/tests/test_SepalMap.py b/tests/test_SepalMap.py
index 440ce972..43f32238 100644
--- a/tests/test_SepalMap.py
+++ b/tests/test_SepalMap.py
@@ -53,8 +53,8 @@ class TestSepalMap:
 
         m.set_drawing_controls(True)
         assert isinstance(m.dc, geemap.DrawControl)
-        assert m.dc.rectangle == {"shapeOptions": {"color": "#79B1C9"}}
-        assert m.dc.polygon == {"shapeOptions": {"color": "#79B1C9"}}
+        assert m.dc.rectangle == {"shapeOptions": {"color": "#79b1c9"}}
+        assert m.dc.polygon == {"shapeOptions": {"color": "#79b1c9"}}
         assert m.dc.marker == {}
         assert m.dc.polyline == {}
 
diff --git a/tests/test_ThemeSelect.py b/tests/test_ThemeSelect.py
new file mode 100644
index 00000000..0de07d20
--- /dev/null
+++ b/tests/test_ThemeSelect.py
@@ -0,0 +1,36 @@
+from configparser import ConfigParser
+
+import pytest
+
+from sepal_ui import sepalwidgets as sw
+from sepal_ui import config_file
+
+
+class TestThemeSelect:
+    def test_init(self, theme_select):
+
+        # minimal btn
+        assert isinstance(theme_select, sw.ThemeSelect)
+
+        return
+
+    def test_change_language(self, theme_select):
+
+        # change value
+        theme_select.fire_event("click", None)
+        config = ConfigParser()
+        config.read(config_file)
+        assert "sepal-ui" in config.sections()
+        assert config["sepal-ui"]["theme"] == "light"
+
+        return
+
+    @pytest.fixture
+    def theme_select(self):
+        """Create a simple theme_select"""
+
+        # destroy any existing config file
+        if config_file.is_file():
+            config_file.unlink()
+
+        return sw.ThemeSelect()
diff --git a/tests/test_utils.py b/tests/test_utils.py
index a648b462..05136ab4 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -326,3 +326,29 @@ class TestUtils:
         config_file.unlink()
 
         return
+
+    def test_set_config_theme(self):
+
+        # remove any config file that could exist
+        if config_file.is_file():
+            config_file.unlink()
+
+        # create a config_file with a set language
+        theme = "dark"
+        su.set_config_theme(theme)
+
+        config = ConfigParser()
+        config.read(config_file)
+        assert "sepal-ui" in config.sections()
+        assert config["sepal-ui"]["theme"] == theme
+
+        # change an existing locale
+        theme = "light"
+        su.set_config_theme(theme)
+        config.read(config_file)
+        assert config["sepal-ui"]["theme"] == theme
+
+        # destroy the file again
+        config_file.unlink()
+
+        return