diff --git a/bamboo-specs/release.yaml b/bamboo-specs/release.yaml index dc9acae9..bfdfff3b 100644 --- a/bamboo-specs/release.yaml +++ b/bamboo-specs/release.yaml @@ -109,7 +109,8 @@ CHANNEL=${bamboo.channel}\ GPG_KEY_PASSPHRASE=${bamboo.gpgPassword}\ FRONTEND_PREBUILT=1\ - VERBOSE=1\ + PARALLELISM=1\ + VERBOSE=2\ build-release # TODO(a.garipov): Use more fine-grained artifact rules. 'artifacts': @@ -239,18 +240,12 @@ ;; esac - # Ignore errors from the Snapstore upload script, because it seems to - # have a lot of issues recently. - # - # TODO(a.garipov): Stop ignoring those errors once they fix the issues. - # - # See https://forum.snapcraft.io/t/unable-to-upload-promote-snaps-to-edge/33120. env\ SNAPCRAFT_CHANNEL="$snapchannel"\ SNAPCRAFT_EMAIL="${bamboo.snapcraftEmail}"\ SNAPCRAFT_MACAROON="${bamboo.snapcraftMacaroonPassword}"\ SNAPCRAFT_UBUNTU_DISCHARGE="${bamboo.snapcraftUbuntuDischargePassword}"\ - ../bamboo-deploy-publisher/deploy.sh adguard-home-snap || : + ../bamboo-deploy-publisher/deploy.sh adguard-home-snap 'final-tasks': - 'clean' 'requirements': 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/encryption.js b/client/src/actions/encryption.js index 2f58abd3..5db97607 100644 --- a/client/src/actions/encryption.js +++ b/client/src/actions/encryption.js @@ -41,6 +41,12 @@ export const setTlsConfig = (config) => async (dispatch, getState) => { response.certificate_chain = atob(response.certificate_chain); response.private_key = atob(response.private_key); + if (values.enabled && values.force_https && window.location.protocol === 'http:') { + window.location.reload(); + return; + } + redirectToCurrentProtocol(response, httpPort); + const dnsStatus = await apiClient.getGlobalStatus(); if (dnsStatus) { dispatch(dnsStatusSuccess(dnsStatus)); @@ -48,7 +54,6 @@ export const setTlsConfig = (config) => async (dispatch, getState) => { dispatch(setTlsConfigSuccess(response)); dispatch(addSuccessToast('encryption_config_saved')); - redirectToCurrentProtocol(response, httpPort); } catch (error) { dispatch(addErrorToast({ error })); dispatch(setTlsConfigFailure()); 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/Filters/Form.js b/client/src/components/Logs/Filters/Form.js index cf02ba5b..7b250ca5 100644 --- a/client/src/components/Logs/Filters/Form.js +++ b/client/src/components/Logs/Filters/Form.js @@ -155,7 +155,7 @@ const Form = (props) => { name={FORM_NAMES.search} component={renderFilterField} type="text" - className={classNames('form-control--search form-control--transparent', className)} + className={classNames('form-control form-control--search form-control--transparent', className)} placeholder={t('domain_or_client')} tooltip={t('query_log_strict_search')} onClearInputClick={onInputClear} diff --git a/client/src/components/Logs/Logs.css b/client/src/components/Logs/Logs.css index d365478b..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 { @@ -103,14 +103,12 @@ } .form-control--search { - box-shadow: 0 1px 0 #ddd; padding: 0 2.5rem; height: 2.25rem; flex-grow: 1; } .form-control--transparent { - border: 0 solid transparent !important; background-color: transparent !important; } @@ -174,10 +172,8 @@ display: inline-flex; align-items: center; justify-content: center; - - --size: 2.5rem; - width: var(--size); - height: var(--size); + width: 2.5rem; + height: 2.5rem; padding: 0; margin-left: 0.9375rem; background-color: transparent; @@ -373,7 +369,7 @@ /* QUERY_STATUS_COLORS */ .logs__row--blue { - background-color: var(--blue); + background-color: var(--logs__row--blue-bgcolor); } .logs__row--green { @@ -385,7 +381,7 @@ } .logs__row--white { - background-color: var(--white); + background-color: var(--logs__row--white-bgcolor); } .logs__row--yellow { @@ -393,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; @@ -407,7 +403,7 @@ } .logs__table { - background-color: var(--white); + background-color: var(--logs__table-bgcolor); border: 0; border-radius: 8px; min-height: 43rem; @@ -474,7 +470,7 @@ .filteringRules__filter { font-style: italic; - font-weight: normal; + font-weight: 400; margin-bottom: 1rem; } diff --git a/client/src/components/Settings/Clients/Form.js b/client/src/components/Settings/Clients/Form.js index 35d9764d..f6b12d1c 100644 --- a/client/src/components/Settings/Clients/Form.js +++ b/client/src/components/Settings/Clients/Form.js @@ -11,12 +11,13 @@ import Select from 'react-select'; import i18n from '../../../i18n'; import Tabs from '../../ui/Tabs'; import Examples from '../Dns/Upstream/Examples'; -import { toggleAllServices } from '../../../helpers/helpers'; +import { toggleAllServices, trimLinesAndRemoveEmpty } from '../../../helpers/helpers'; import { renderInputField, renderGroupField, CheckboxField, renderServiceField, + renderTextareaField, } from '../../../helpers/form'; import { validateClientId, validateRequiredValue } from '../../../helpers/validators'; import { CLIENT_ID_LINK, FORM_NAME } from '../../../helpers/constants'; @@ -230,10 +231,11 @@ let Form = (props) => { , 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 ( <>