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 settings0>.",
"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) => {