From a7dca7aa56234fc76029edf4718a85af1abbe5eb Mon Sep 17 00:00:00 2001 From: Dimitry Kolyshev Date: Thu, 19 Jan 2023 13:46:44 +0300 Subject: [PATCH] Pull request 1713: 613 dark theme vol.2 Updates #613. Squashed commit of the following: commit 9a3ba15bdb8476dde045631362d76572a68a4e5c Author: Dimitry Kolyshev Date: Thu Jan 19 12:46:48 2023 +0700 client: review dark theme colors commit c456cdcb6ffec044917b0396ad3cfa8b5c3bce10 Author: Dimitry Kolyshev Date: Wed Jan 18 23:46:32 2023 +0700 client: review dark theme colors commit 4b042ba4f433c89488b18ec38f5864d5ffaf8657 Author: Dimitry Kolyshev Date: Wed Jan 18 23:34:37 2023 +0700 client: review dark theme colors commit 1ff34f751b4d9342495070af85f3dae86c285aee Author: Dimitry Kolyshev Date: Wed Jan 18 18:59:46 2023 +0700 client: default language commit ca9abc11a8c155c78f73903906ed452e95764e99 Author: Dimitry Kolyshev Date: Wed Jan 18 12:23:50 2023 +0700 client: default language commit 01a057a40e879d1bc2013989d6eb56a296ba16a3 Author: Dimitry Kolyshev Date: Wed Jan 18 11:53:58 2023 +0700 client: review dark theme colors commit aa3658f35c49f8ae19077b333075207082383a17 Author: Dimitry Kolyshev Date: Wed Jan 18 11:14:08 2023 +0700 client: review dark theme colors commit 8df726382155fcc3b2e6e632d53f42d0be638c77 Author: Dimitry Kolyshev Date: Tue Jan 17 13:41:21 2023 +0700 client: dark theme colors commit bd84ae46272743874d2350291b22b90b7fbedf0e Author: Dimitry Kolyshev Date: Tue Jan 17 13:05:46 2023 +0700 client: dark theme colors commit 136ea5608a1a22b6a54a362741b2fdd708f345e2 Author: Dimitry Kolyshev Date: Tue Jan 17 10:51:22 2023 +0700 all: docs commit 784be8741b730a12d665d7e2a29c140c0746e927 Author: Dimitry Kolyshev Date: Mon Jan 16 17:59:26 2023 +0700 client: imp code commit a83de0948fe034e7be35c04a607b1171915c0263 Author: Dimitry Kolyshev Date: Mon Jan 16 14:13:54 2023 +0700 client: dark theme login commit 6c4ef19da01efc6a1e4ea76085b7b7382c331eca Author: Dimitry Kolyshev Date: Mon Jan 16 13:27:35 2023 +0700 client: dark theme css commit 5cf564ea1203e9472d200975ee98d93b6b868210 Author: Dimitry Kolyshev Date: Mon Jan 16 13:18:00 2023 +0700 client: imp code commit 1bbbd20972345c08e944b7c4bb0330c0d30d827a Author: Dimitry Kolyshev Date: Mon Jan 16 12:49:03 2023 +0700 client: imp code commit cb07680d3cd7acdbdb7bdc9d9abaa388a9df8e4c Author: Dimitry Kolyshev Date: Mon Jan 16 12:24:14 2023 +0700 client: auto theme handling commit d34910d4dd5edb694b6da55a2260b4fea784e7e0 Author: Dimitry Kolyshev Date: Fri Jan 13 23:31:24 2023 +0700 home: imp docs commit 8abe4f6f7182dc3dbcbbe73652294ee6584a4f2f Author: Dimitry Kolyshev Date: Thu Jan 12 18:35:48 2023 +0700 client: theme dropdown --- CHANGELOG.md | 1 + client/src/__locales/en.json | 3 + client/src/actions/index.js | 16 ++--- client/src/api/Api.js | 41 +++++++++---- client/src/components/App/index.css | 48 +++++++++++++++ client/src/components/App/index.js | 45 +++++++++++++- client/src/components/Header/Header.css | 2 +- .../src/components/Logs/Cells/IconTooltip.css | 3 +- client/src/components/Logs/Logs.css | 14 ++--- client/src/components/Settings/Settings.css | 2 +- client/src/components/ui/Checkbox.css | 2 +- client/src/components/ui/Footer.css | 10 ++++ client/src/components/ui/Footer.js | 36 ++++++++++- client/src/components/ui/Modal.css | 1 + client/src/components/ui/ReactTable.css | 20 +++++++ client/src/components/ui/Select.css | 34 ++++++++++- client/src/components/ui/Tabler.css | 59 +++++++++++-------- client/src/components/ui/Tooltip.css | 5 ++ client/src/helpers/constants.js | 8 +++ client/src/helpers/helpers.js | 9 +++ client/src/reducers/dashboard.js | 14 ++--- 21 files changed, 304 insertions(+), 69 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dc4f30fa..dd101bdb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ and this project adheres to ### Added +- Experimental Dark UI theme ([#613]). - The new HTTP API `PUT /control/profile/update`, that updates current user language and UI theme. The format of request body is described in `openapi/openapi.yaml`. diff --git a/client/src/__locales/en.json b/client/src/__locales/en.json index 1771afbd..f653bc37 100644 --- a/client/src/__locales/en.json +++ b/client/src/__locales/en.json @@ -298,6 +298,9 @@ "blocking_mode_nxdomain": "NXDOMAIN: Respond with NXDOMAIN code", "blocking_mode_null_ip": "Null IP: Respond with zero IP address (0.0.0.0 for A; :: for AAAA)", "blocking_mode_custom_ip": "Custom IP: Respond with a manually set IP address", + "theme_auto": "Auto", + "theme_light": "Light", + "theme_dark": "Dark", "upstream_dns_client_desc": "If you keep this field empty, AdGuard Home will use the servers configured in the <0>DNS settings.", "tracker_source": "Tracker source", "source_label": "Source", diff --git a/client/src/actions/index.js b/client/src/actions/index.js index 2242490b..d86ea43e 100644 --- a/client/src/actions/index.js +++ b/client/src/actions/index.js @@ -363,18 +363,18 @@ export const changeLanguage = (lang) => async (dispatch) => { } }; -export const getLanguageRequest = createAction('GET_LANGUAGE_REQUEST'); -export const getLanguageFailure = createAction('GET_LANGUAGE_FAILURE'); -export const getLanguageSuccess = createAction('GET_LANGUAGE_SUCCESS'); +export const changeThemeRequest = createAction('CHANGE_THEME_REQUEST'); +export const changeThemeFailure = createAction('CHANGE_THEME_FAILURE'); +export const changeThemeSuccess = createAction('CHANGE_THEME_SUCCESS'); -export const getLanguage = () => async (dispatch) => { - dispatch(getLanguageRequest()); +export const changeTheme = (theme) => async (dispatch) => { + dispatch(changeThemeRequest()); try { - const langSettings = await apiClient.getCurrentLanguage(); - dispatch(getLanguageSuccess(langSettings.language)); + await apiClient.changeTheme({ theme }); + dispatch(changeThemeSuccess({ theme })); } catch (error) { dispatch(addErrorToast({ error })); - dispatch(getLanguageFailure()); + dispatch(changeThemeFailure()); } }; diff --git a/client/src/api/Api.js b/client/src/api/Api.js index 06f40c6c..d984bbb8 100644 --- a/client/src/api/Api.js +++ b/client/src/api/Api.js @@ -1,8 +1,12 @@ import axios from 'axios'; import { getPathWithQueryString } from '../helpers/helpers'; -import { QUERY_LOGS_PAGE_LIMIT, HTML_PAGES, R_PATH_LAST_PART } from '../helpers/constants'; +import { + QUERY_LOGS_PAGE_LIMIT, HTML_PAGES, R_PATH_LAST_PART, THEMES, +} from '../helpers/constants'; import { BASE_URL } from '../../constants'; +import i18n from '../i18n'; +import { LANGUAGES } from '../helpers/twosky'; class Api { baseUrl = BASE_URL; @@ -224,21 +228,21 @@ class Api { } // Language - CURRENT_LANGUAGE = { path: 'i18n/current_language', method: 'GET' }; - CHANGE_LANGUAGE = { path: 'i18n/change_language', method: 'POST' }; + async changeLanguage(config) { + const profile = await this.getProfile(); + profile.language = config.language; - getCurrentLanguage() { - const { path, method } = this.CURRENT_LANGUAGE; - return this.makeRequest(path, method); + return this.setProfile(profile); } - changeLanguage(config) { - const { path, method } = this.CHANGE_LANGUAGE; - const parameters = { - data: config, - }; - return this.makeRequest(path, method, parameters); + // Theme + + async changeTheme(config) { + const profile = await this.getProfile(); + profile.theme = config.theme; + + return this.setProfile(profile); } // DHCP @@ -571,11 +575,24 @@ class Api { // Profile GET_PROFILE = { path: 'profile', method: 'GET' }; + UPDATE_PROFILE = { path: 'profile/update', method: 'PUT' }; + getProfile() { const { path, method } = this.GET_PROFILE; return this.makeRequest(path, method); } + setProfile(data) { + const theme = data.theme ? data.theme : THEMES.auto; + const defaultLanguage = i18n.language ? i18n.language : LANGUAGES.en; + const language = data.language ? data.language : defaultLanguage; + + const { path, method } = this.UPDATE_PROFILE; + const config = { data: { theme, language } }; + + return this.makeRequest(path, method, config); + } + // DNS config GET_DNS_CONFIG = { path: 'dns_info', method: 'GET' }; diff --git a/client/src/components/App/index.css b/client/src/components/App/index.css index fa8ca788..751a8e22 100644 --- a/client/src/components/App/index.css +++ b/client/src/components/App/index.css @@ -1,4 +1,26 @@ :root { + --bgcolor: #f5f7fb; + --mcolor: #495057; + --scolor: rgba(74, 74, 74, 0.7); + --border-color: rgba(0, 40, 100, 0.12); + --header-bgcolor: #fff; + --card-bgcolor: #fff; + --card-border-color: rgba(0, 40, 100, 0.12); + --ctrl-bgcolor: #fff; + --ctrl-select-bgcolor: rgba(69, 79, 94, 0.12); + --ctrl-dropdown-color: #212529; + --ctrl-dropdown-bgcolor-focus: #f8f9fa; + --ctrl-dropdown-color-focus: #16181b; + --btn-success-bgcolor: #5eba00; + --form-disabled-bgcolor: #f8f9fa; + --form-disabled-color: #495057; + --rt-nodata-bgcolor: rgba(255,255,255,0.8); + --rt-nodata-color: rgba(0,0,0,0.5); + --modal-overlay-bgcolor: rgba(255, 255, 255, 0.75); + --logs__table-bgcolor: #fff; + --logs__row--blue-bgcolor: #e5effd; + --logs__row--white-bgcolor: #fff; + --detailed-info-color: #888888; --yellow-pale: rgba(247, 181, 0, 0.1); --green79: #67b279; --gray-a5: #a5a5a5; @@ -8,6 +30,32 @@ --font-size-disable-autozoom: 1rem; } +[data-theme="dark"] { + --bgcolor: #131313; + --mcolor: #e6e6e6; + --scolor: #a5a5a5; + --header-bgcolor: #131313; + --border-color: #222; + --card-bgcolor: #1c1c1c; + --card-border-color: #3d3d3d; + --ctrl-bgcolor: #1c1c1c; + --ctrl-select-bgcolor: #3d3d3d; + --ctrl-dropdown-color: #fff; + --ctrl-dropdown-bgcolor-focus: #000; + --ctrl-dropdown-color-focus: #fff; + --btn-success-bgcolor: #67b279; + --form-disabled-bgcolor: #3d3d3d; + --form-disabled-color: #a5a5a5; + --logs__text-color: #f3f3f3; + --rt-nodata-bgcolor: #1c1c1c; + --rt-nodata-color: #fff; + --modal-overlay-bgcolor: #1c1c1c; + --logs__table-bgcolor: #3d3d3d; + --logs__row--blue-bgcolor: #467fcf; + --logs__row--white-bgcolor: #1c1c1c; + --detailed-info-color: #fff; +} + body { margin: 0; padding: 0; diff --git a/client/src/components/App/index.js b/client/src/components/App/index.js index 6d65ccb8..819bb0c6 100644 --- a/client/src/components/App/index.js +++ b/client/src/components/App/index.js @@ -20,8 +20,13 @@ import EncryptionTopline from '../ui/EncryptionTopline'; import Icons from '../ui/Icons'; import i18n from '../../i18n'; import Loading from '../ui/Loading'; -import { FILTERS_URLS, MENU_URLS, SETTINGS_URLS } from '../../helpers/constants'; -import { getLogsUrlParams, setHtmlLangAttr } from '../../helpers/helpers'; +import { + FILTERS_URLS, + MENU_URLS, + SETTINGS_URLS, + THEMES, +} from '../../helpers/constants'; +import { getLogsUrlParams, setHtmlLangAttr, setUITheme } from '../../helpers/helpers'; import Header from '../Header'; import { changeLanguage, getDnsStatus } from '../../actions'; @@ -109,6 +114,7 @@ const App = () => { isCoreRunning, isUpdateAvailable, processing, + theme, } = useSelector((state) => state.dashboard, shallowEqual); const { processing: processingEncryption } = useSelector(( @@ -138,6 +144,41 @@ const App = () => { setLanguage(); }, [language]); + const handleAutoTheme = (e, accountTheme) => { + if (accountTheme !== THEMES.auto) { + return; + } + + if (e.matches) { + setUITheme(THEMES.dark); + } else { + setUITheme(THEMES.light); + } + }; + + useEffect(() => { + if (theme !== THEMES.auto) { + setUITheme(theme); + + return; + } + + const colorSchemeMedia = window.matchMedia('(prefers-color-scheme: dark)'); + const prefersDark = colorSchemeMedia.matches; + setUITheme(prefersDark ? THEMES.dark : THEMES.light); + + if (colorSchemeMedia.addEventListener !== undefined) { + colorSchemeMedia.addEventListener('change', (e) => { + handleAutoTheme(e, theme); + }); + } else { + // Deprecated addListener for older versions of Safari. + colorSchemeMedia.addListener((e) => { + handleAutoTheme(e, theme); + }); + } + }, [theme]); + const reloadPage = () => { window.location.reload(); }; diff --git a/client/src/components/Header/Header.css b/client/src/components/Header/Header.css index a5fe802e..c5412f7a 100644 --- a/client/src/components/Header/Header.css +++ b/client/src/components/Header/Header.css @@ -47,7 +47,7 @@ width: 250px; height: 100vh; transition: transform 0.3s ease; - background-color: #fff; + background-color: var(--header-bgcolor); overflow-y: auto; } diff --git a/client/src/components/Logs/Cells/IconTooltip.css b/client/src/components/Logs/Cells/IconTooltip.css index 245c14d9..8f7eb453 100644 --- a/client/src/components/Logs/Cells/IconTooltip.css +++ b/client/src/components/Logs/Cells/IconTooltip.css @@ -4,7 +4,8 @@ box-shadow: 2px 4px 8px rgba(0, 0, 0, 0.2); border-radius: 4px !important; pointer-events: auto !important; - background-color: var(--white); + background-color: var(--ctrl-bgcolor); + color: var(--scolor); z-index: 102; overflow-y: auto; max-height: 100%; diff --git a/client/src/components/Logs/Logs.css b/client/src/components/Logs/Logs.css index 8df1d62b..358c2a6a 100644 --- a/client/src/components/Logs/Logs.css +++ b/client/src/components/Logs/Logs.css @@ -31,7 +31,7 @@ overflow: hidden; font-size: 1rem; font-family: var(--font-family-sans-serif); - color: var(--gray-4d); + color: var(--logs__text-color); letter-spacing: 0; line-height: 1.5rem; } @@ -48,7 +48,7 @@ .detailed-info { font-size: 0.8rem; line-height: 1.4; - color: #888888; + color: var(--detailed-info-color); } .logs__text--link { @@ -369,7 +369,7 @@ /* QUERY_STATUS_COLORS */ .logs__row--blue { - background-color: var(--blue); + background-color: var(--logs__row--blue-bgcolor); } .logs__row--green { @@ -381,7 +381,7 @@ } .logs__row--white { - background-color: var(--white); + background-color: var(--logs__row--white-bgcolor); } .logs__row--yellow { @@ -389,8 +389,8 @@ } .logs__no-data { - color: var(--gray-4d); - background-color: var(--white80); + color: var(--mcolor); + background-color: var(--logs__table-bgcolor); pointer-events: none; font-weight: 600; text-align: center; @@ -403,7 +403,7 @@ } .logs__table { - background-color: var(--white); + background-color: var(--logs__table-bgcolor); border: 0; border-radius: 8px; min-height: 43rem; diff --git a/client/src/components/Settings/Settings.css b/client/src/components/Settings/Settings.css index 3fd560f9..ba6d2462 100644 --- a/client/src/components/Settings/Settings.css +++ b/client/src/components/Settings/Settings.css @@ -77,7 +77,7 @@ .form__desc { margin-top: 10px; font-size: 13px; - color: rgba(74, 74, 74, 0.7); + color: var(--scolor); } .form__desc--top { diff --git a/client/src/components/ui/Checkbox.css b/client/src/components/ui/Checkbox.css index bab88c79..2a556fb0 100644 --- a/client/src/components/ui/Checkbox.css +++ b/client/src/components/ui/Checkbox.css @@ -107,5 +107,5 @@ .checkbox__label-subtitle { display: block; line-height: 1.5; - color: rgba(74, 74, 74, 0.7); + color: var(--scolor); } diff --git a/client/src/components/ui/Footer.css b/client/src/components/ui/Footer.css index 66fbe5e2..fd0dca2b 100644 --- a/client/src/components/ui/Footer.css +++ b/client/src/components/ui/Footer.css @@ -18,6 +18,11 @@ align-items: center; } +.footer__column--theme { + min-width: 220px; + margin-bottom: 0; +} + .footer__column--language { min-width: 220px; margin-bottom: 0; @@ -49,6 +54,11 @@ } .footer__column--language { + min-width: initial; + margin-left: 20px; + } + + .footer__column--theme { min-width: initial; margin-left: auto; } diff --git a/client/src/components/ui/Footer.js b/client/src/components/ui/Footer.js index 393e16fe..c1d1b40e 100644 --- a/client/src/components/ui/Footer.js +++ b/client/src/components/ui/Footer.js @@ -1,8 +1,9 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; +import { useDispatch, useSelector } from 'react-redux'; import classNames from 'classnames'; -import { REPOSITORY, PRIVACY_POLICY_LINK } from '../../helpers/constants'; +import { REPOSITORY, PRIVACY_POLICY_LINK, THEMES } from '../../helpers/constants'; import { LANGUAGES } from '../../helpers/twosky'; import i18n from '../../i18n'; @@ -10,6 +11,7 @@ import Version from './Version'; import './Footer.css'; import './Select.css'; import { setHtmlLangAttr } from '../../helpers/helpers'; +import { changeTheme } from '../../actions'; const linksData = [ { @@ -29,6 +31,11 @@ const linksData = [ const Footer = () => { const { t } = useTranslation(); + const dispatch = useDispatch(); + + const currentTheme = useSelector((state) => (state.dashboard ? state.dashboard.theme : 'auto')); + const profileName = useSelector((state) => (state.dashboard ? state.dashboard.name : '')); + const isLoggedIn = profileName !== ''; const getYear = () => { const today = new Date(); @@ -41,6 +48,11 @@ const Footer = () => { setHtmlLangAttr(value); }; + const onThemeChanged = (event) => { + const { value } = event.target; + dispatch(changeTheme(value)); + }; + const renderCopyright = () =>
{t('copyright')} © {getYear()}{' '} @@ -58,6 +70,25 @@ const Footer = () => { {t(name)} ); + const renderThemeSelect = (currentTheme, isLoggedIn) => { + if (!isLoggedIn) { + return ''; + } + + return ; + }; + return ( <>