diff --git a/.streamlit/config.toml b/.streamlit/config.toml index dfd04be..b239d66 100644 --- a/.streamlit/config.toml +++ b/.streamlit/config.toml @@ -14,12 +14,5 @@ hideWelcomeMessage = true toolbarMode = "viewer" showSidebarNavigation = false -[theme] -base="light" -backgroundColor = "#eee" -secondaryBackgroundColor = "#fff" -primaryColor = "black" -baseRadius = "none" - [runner] magicEnabled = false \ No newline at end of file diff --git a/app/pages/settings.py b/app/pages/settings.py index 6cb4b49..8db6912 100644 --- a/app/pages/settings.py +++ b/app/pages/settings.py @@ -1,17 +1,27 @@ import streamlit as st -from pandas.core.ops.docstrings import key -from pandas.io.formats.style import Styler import queries.crud as crud +import themes as th is_login_enabled = hasattr(st, 'user') and hasattr(st.user, 'is_logged_in') is_logged_in = is_login_enabled and st.user.is_logged_in st.title("Settings") - if hasattr(st.session_state, 'user_name'): st.markdown(f"Currently logged in as **{st.session_state.user_name}**") + +st.header("Theme") +themes = st.session_state.themes +with st.container(horizontal=True, width="stretch"): + for theme in ["light", "dark"]: + if st.button(label=themes[theme]["button_face_label"], + icon = themes[theme]["button_face_icon"], + disabled = (theme == st.session_state.current_theme), + width = "stretch"): + th.change_theme(theme) + st.rerun() + st.header("Colors") with st.container(key="settings-color-selector"): palettes = crud.get_color_palettes() diff --git a/app/queries/crud.py b/app/queries/crud.py index 3db247b..e346f51 100644 --- a/app/queries/crud.py +++ b/app/queries/crud.py @@ -87,7 +87,7 @@ def get_color_palettes(): return None -def get_color_palette(): +def get_color_palette() -> int: user_id = int(st.session_state.user_id) try: return int(connection().query('SELECT color_palette_id FROM users WHERE id = :id''', params={'id': user_id})['color_palette_id'][0]) @@ -123,3 +123,26 @@ def get_colors(): except Exception as e: logger.error(e) return None + + +def set_theme(theme:str): + user_id = int(st.session_state.user_id) + logger.info("Changing theme for user %d to %s", user_id, theme) + with connection().session as session: + try: + query = text('UPDATE users SET theme = :theme WHERE id = :user') + session.execute(query, { + 'theme': theme, + 'user': user_id + }) + except Exception as e: + logger.error(e) + session.rollback() + +def get_theme() -> str: + user_id = int(st.session_state.user_id) + try: + return connection().query('SELECT theme FROM users u WHERE u.id = :id', params={'id': user_id})['theme'][0] + except Exception as e: + logger.error(e) + return None \ No newline at end of file diff --git a/app/queries/user.py b/app/queries/user.py index 8da0d1d..01bffbd 100644 --- a/app/queries/user.py +++ b/app/queries/user.py @@ -43,25 +43,27 @@ def create_user(email, name, oidc_user_id): def set_user_in_session(user: UserInfoProxy): - email = user.email - user_id = user.sub - if hasattr(user, 'name'): - name = user.name - else: - name = None - user_entity = find_user_by_oidc_id(user_id) + email = user.email if hasattr(user, "email") else None + user_id = user.sub if hasattr(user, "sub") else None + name = user.name if hasattr(user, "name") else None + + user_entity = find_user_by_oidc_id(user_id) if user_id else st.dataframe() if user_entity.empty: - user_entity = find_user_by_email(email) + user_entity = find_user_by_email(email) if email else st.dataframe() if user_entity.empty: user_entity = find_default_user() - if user_entity.empty: + if user_entity.empty and email and name and user_id: user_entity = create_user(email, name, user_id) - else: + elif name: update_default_user(email, name, user_id) user_entity = find_user_by_oidc_id(user_id) - st.session_state.user_id = user_entity["id"][0] - st.session_state.user_name = user_entity["name"][0] - st.session_state.user_email = user_entity["email"][0] - st.session_state.user_external_id = user_entity["oidc_user_id"][0] \ No newline at end of file + if not user_entity.empty: + st.session_state.user_id = user_entity["id"][0] + st.session_state.user_name = user_entity["name"][0] + st.session_state.user_email = user_entity["email"][0] + st.session_state.user_external_id = user_entity["oidc_user_id"][0] + st.session_state.current_theme = user_entity["theme"][0] + else: + logger.warn("No active user found!") \ No newline at end of file diff --git a/app/streamlit_app.py b/app/streamlit_app.py index 1474b04..e58488c 100644 --- a/app/streamlit_app.py +++ b/app/streamlit_app.py @@ -5,10 +5,12 @@ import queries.user as user_queries from logger import init_logger from styles import init_styles from user import init_user, is_login_enabled, is_logged_in +from themes import init_themes init_logger() init_user() init_styles() +init_themes() if is_login_enabled and not is_logged_in: with st.container(width="stretch", height="stretch", horizontal_alignment="center"): diff --git a/app/themes.py b/app/themes.py new file mode 100644 index 0000000..e6b4472 --- /dev/null +++ b/app/themes.py @@ -0,0 +1,36 @@ +import streamlit as st +from queries import crud + +def init_themes(): + + if 'themes' not in st.session_state: + st.session_state.themes = { + "dark": { + "theme.base": "dark", + "theme.backgroundColor": "black", + "theme.primaryColor": "#c98bdb", + "theme.secondaryBackgroundColor": "#5591f5", + "theme.textColor": "white", + "button_face_label": "Dark", + "button_face_icon": ":material/dark_mode:" + }, + "light": { + "theme.base": "light", + "theme.backgroundColor": "white", + "theme.primaryColor": "#5591f5", + "theme.secondaryBackgroundColor": "#82E1D7", + "theme.textColor": "#0a1464", + "button_face_label": "Light", + "button_face_icon": ":material/light_mode:" + }, + } + + if 'current_theme' not in st.session_state: + st.session_state.current_theme = 'light' + change_theme('light') + +def change_theme(theme): + crud.set_theme(theme) + for key, val in st.session_state.themes[theme].items(): + if key.startswith("theme"): + st._config.set_option(key, val) diff --git a/app/user.py b/app/user.py index 079aa32..45c3cc7 100644 --- a/app/user.py +++ b/app/user.py @@ -5,7 +5,4 @@ is_login_enabled = hasattr(st, 'user') and hasattr(st.user, 'is_logged_in') is_logged_in = is_login_enabled and st.user.is_logged_in def init_user(): - if is_logged_in: - user_queries.set_user_in_session(st.user) - else: - st.session_state.user_id = 1 # default user \ No newline at end of file + user_queries.set_user_in_session(st.user) \ No newline at end of file diff --git a/css/theme.css b/css/theme.css index 0527b8a..c048a32 100644 --- a/css/theme.css +++ b/css/theme.css @@ -15,7 +15,6 @@ } .stPageLink a { - background: whitesmoke; height: 40px; width: 45px; border: 1px solid silver; @@ -34,7 +33,6 @@ border: 1px solid gray; padding: 10px; border-radius: 5px; - background-color: whitesmoke; } .st-key-new_counter_color_selector div[role = "radiogroup"] { diff --git a/migrations/versions/20260430181243_theme.py b/migrations/versions/20260430181243_theme.py new file mode 100644 index 0000000..0fbbc2c --- /dev/null +++ b/migrations/versions/20260430181243_theme.py @@ -0,0 +1,25 @@ +"""theme + +Revision ID: 8be315e8a5dc +Revises: 720abfadcd44 +Create Date: 2026-04-30 18:12:43.026620 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '8be315e8a5dc' +down_revision: Union[str, Sequence[str], None] = '720abfadcd44' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.add_column('users', sa.Column('theme', sa.String(), nullable=False, server_default='light')) + +def downgrade() -> None: + op.drop_column('users', 'theme') diff --git a/tests/conftest.py b/tests/conftest.py index a246392..46f442d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -32,6 +32,7 @@ def user_config(): st.session_state.user_name = "Test User" st.session_state.user_email = "test@test.local" st.session_state.user_external_id = "111-2222-3333" + st.session_state.current_theme = "light" @pytest.fixture def app() -> AppTest: diff --git a/tests/ui/settings_ui_test.py b/tests/ui/settings_ui_test.py index 5373dab..d09d29c 100644 --- a/tests/ui/settings_ui_test.py +++ b/tests/ui/settings_ui_test.py @@ -1,15 +1,33 @@ +import queries.crud as crud + +def test_change_theme(app): + app.run() + app.switch_page("pages/settings.py").run() + + assert app.session_state.current_theme =="light", "Light theme should be default" + + assert app.button[0].label == "Light" + assert app.button[0].disabled == True, "Light theme should be selected" + + assert app.button[1].label == "Dark" + assert app.button[1].disabled == False, "Dark theme should be de-selected" + + app.button[1].click().run() + + assert "dark" == crud.get_theme() + def test_change_color_palette(app): app.run() app.switch_page("pages/settings.py").run() - assert app.button[0].disabled == True - assert app.button[0].label == "Flames **(selected)**" + assert app.button[2].disabled == True, "First palette should be selected" + assert app.button[2].label == "Flames **(selected)**" - app.button[1].click().run() + app.button[3].click().run() - assert app.button[0].disabled == False - assert app.button[0].label == "Flames" + assert app.button[2].disabled == False, "First palette should be de-selected" + assert app.button[2].label == "Flames" - assert app.button[1].disabled == True - assert app.button[1].label == "Water **(selected)**" \ No newline at end of file + assert app.button[3].disabled == True, "Second palette should be selected" + assert app.button[3].label == "Water **(selected)**" \ No newline at end of file