1
0
mirror of https://github.com/Sonarr/Sonarr.git synced 2024-10-29 23:12:39 +01:00

New: Dark theme for login screen

Closes #6751
This commit is contained in:
Mark McDowall 2024-05-03 20:53:03 -07:00 committed by Mark McDowall
parent f81bb3ec19
commit cae134ec7b
8 changed files with 131 additions and 77 deletions

View File

@ -12,11 +12,10 @@ function App({ store, history }) {
<DocumentTitle title={window.Sonarr.instanceName}> <DocumentTitle title={window.Sonarr.instanceName}>
<Provider store={store}> <Provider store={store}>
<ConnectedRouter history={history}> <ConnectedRouter history={history}>
<ApplyTheme> <ApplyTheme />
<PageConnector> <PageConnector>
<AppRoutes app={App} /> <AppRoutes app={App} />
</PageConnector> </PageConnector>
</ApplyTheme>
</ConnectedRouter> </ConnectedRouter>
</Provider> </Provider>
</DocumentTitle> </DocumentTitle>

View File

@ -1,50 +0,0 @@
import PropTypes from 'prop-types';
import React, { Fragment, useCallback, useEffect } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import themes from 'Styles/Themes';
function createMapStateToProps() {
return createSelector(
(state) => state.settings.ui.item.theme || window.Sonarr.theme,
(
theme
) => {
return {
theme
};
}
);
}
function ApplyTheme({ theme, children }) {
// Update the CSS Variables
const updateCSSVariables = useCallback(() => {
const arrayOfVariableKeys = Object.keys(themes[theme]);
const arrayOfVariableValues = Object.values(themes[theme]);
// Loop through each array key and set the CSS Variables
arrayOfVariableKeys.forEach((cssVariableKey, index) => {
// Based on our snippet from MDN
document.documentElement.style.setProperty(
`--${cssVariableKey}`,
arrayOfVariableValues[index]
);
});
}, [theme]);
// On Component Mount and Component Update
useEffect(() => {
updateCSSVariables(theme);
}, [updateCSSVariables, theme]);
return <Fragment>{children}</Fragment>;
}
ApplyTheme.propTypes = {
theme: PropTypes.string.isRequired,
children: PropTypes.object.isRequired
};
export default connect(createMapStateToProps)(ApplyTheme);

View File

@ -0,0 +1,37 @@
import React, { Fragment, ReactNode, useCallback, useEffect } from 'react';
import { useSelector } from 'react-redux';
import { createSelector } from 'reselect';
import themes from 'Styles/Themes';
import AppState from './State/AppState';
interface ApplyThemeProps {
children: ReactNode;
}
function createThemeSelector() {
return createSelector(
(state: AppState) => state.settings.ui.item.theme || window.Sonarr.theme,
(theme) => {
return theme;
}
);
}
function ApplyTheme({ children }: ApplyThemeProps) {
const theme = useSelector(createThemeSelector());
const updateCSSVariables = useCallback(() => {
Object.entries(themes[theme]).forEach(([key, value]) => {
document.documentElement.style.setProperty(`--${key}`, value);
});
}, [theme]);
// On Component Mount and Component Update
useEffect(() => {
updateCSSVariables();
}, [updateCSSVariables, theme]);
return <Fragment>{children}</Fragment>;
}
export default ApplyTheme;

View File

@ -2,7 +2,7 @@ import * as dark from './dark';
import * as light from './light'; import * as light from './light';
const defaultDark = window.matchMedia('(prefers-color-scheme: dark)').matches; const defaultDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
const auto = defaultDark ? { ...dark } : { ...light }; const auto = defaultDark ? dark : light;
export default { export default {
auto, auto,

View File

@ -57,8 +57,8 @@
<style> <style>
body { body {
background-color: #f5f7fa; background-color: var(--pageBackground);
color: #656565; color: var(--textColor);
font-family: "Roboto", "open sans", "Helvetica Neue", Helvetica, Arial, font-family: "Roboto", "open sans", "Helvetica Neue", Helvetica, Arial,
sans-serif; sans-serif;
} }
@ -88,14 +88,14 @@
padding: 10px; padding: 10px;
border-top-left-radius: 4px; border-top-left-radius: 4px;
border-top-right-radius: 4px; border-top-right-radius: 4px;
background-color: #3a3f51; background-color: var(--themeDarkColor);
} }
.panel-body { .panel-body {
padding: 20px; padding: 20px;
border-bottom-right-radius: 4px; border-bottom-right-radius: 4px;
border-bottom-left-radius: 4px; border-bottom-left-radius: 4px;
background-color: #fff; background-color: var(--panelBackground);
} }
.sign-in { .sign-in {
@ -112,16 +112,17 @@
padding: 6px 16px; padding: 6px 16px;
width: 100%; width: 100%;
height: 35px; height: 35px;
border: 1px solid #dde6e9; background-color: var(--inputBackgroundColor);
border: 1px solid var(--inputBorderColor);
border-radius: 4px; border-radius: 4px;
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); box-shadow: inset 0 1px 1px var(--inputBoxShadowColor);
} }
.form-input:focus { .form-input:focus {
outline: 0; outline: 0;
border-color: #66afe9; border-color: var(--inputFocusBorderColor);
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), box-shadow: inset 0 1px 1px var(--inputBoxShadowColor),
0 0 8px rgba(102, 175, 233, 0.6); 0 0 8px var(--inputFocusBoxShadowColor);
} }
.button { .button {
@ -130,10 +131,10 @@
padding: 10px 0; padding: 10px 0;
width: 100%; width: 100%;
border: 1px solid; border: 1px solid;
border-color: #5899eb; border-color: var(--primaryBorderColor);
border-radius: 4px; border-radius: 4px;
background-color: #5d9cec; background-color: var(--primaryBackgroundColor);
color: #fff; color: var(--white);
vertical-align: middle; vertical-align: middle;
text-align: center; text-align: center;
white-space: nowrap; white-space: nowrap;
@ -141,9 +142,9 @@
} }
.button:hover { .button:hover {
border-color: #3483e7; border-color: var(--primaryHoverBorderColor);
background-color: #4b91ea; background-color: var(--primaryHoverBackgroundColor);
color: #fff; color: var(--white);
text-decoration: none; text-decoration: none;
} }
@ -165,24 +166,24 @@
.forgot-password { .forgot-password {
margin-left: auto; margin-left: auto;
color: #909fa7; color: var(--forgotPasswordColor);
text-decoration: none; text-decoration: none;
font-size: 13px; font-size: 13px;
} }
.forgot-password:focus, .forgot-password:focus,
.forgot-password:hover { .forgot-password:hover {
color: #748690; color: var(--forgotPasswordAltColor);
text-decoration: underline; text-decoration: underline;
} }
.forgot-password:visited { .forgot-password:visited {
color: #748690; color: var(--forgotPasswordAltColor);
} }
.login-failed { .login-failed {
margin-top: 20px; margin-top: 20px;
color: #f05050; color: var(--failedColor);
font-size: 14px; font-size: 14px;
} }
@ -291,5 +292,59 @@
loginFailedDiv.classList.remove("hidden"); loginFailedDiv.classList.remove("hidden");
} }
var light = {
white: '#fff',
pageBackground: '#f5f7fa',
textColor: '#656565',
themeDarkColor: '#3a3f51',
panelBackground: '#fff',
inputBackgroundColor: '#fff',
inputBorderColor: '#dde6e9',
inputBoxShadowColor: 'rgba(0, 0, 0, 0.075)',
inputFocusBorderColor: '#66afe9',
inputFocusBoxShadowColor: 'rgba(102, 175, 233, 0.6)',
primaryBackgroundColor: '#5d9cec',
primaryBorderColor: '#5899eb',
primaryHoverBackgroundColor: '#4b91ea',
primaryHoverBorderColor: '#3483e7',
failedColor: '#f05050',
forgotPasswordColor: '#909fa7',
forgotPasswordAltColor: '#748690'
};
var dark = {
white: '#fff',
pageBackground: '#202020',
textColor: '#656565',
themeDarkColor: '#494949',
panelBackground: '#111',
inputBackgroundColor: '#333',
inputBorderColor: '#dde6e9',
inputBoxShadowColor: 'rgba(0, 0, 0, 0.075)',
inputFocusBorderColor: '#66afe9',
inputFocusBoxShadowColor: 'rgba(102, 175, 233, 0.6)',
primaryBackgroundColor: '#5d9cec',
primaryBorderColor: '#5899eb',
primaryHoverBackgroundColor: '#4b91ea',
primaryHoverBorderColor: '#3483e7',
failedColor: '#f05050',
forgotPasswordColor: '#737d83',
forgotPasswordAltColor: '#546067'
};
var theme = "_THEME_";
var defaultDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
var finalTheme = theme === 'dark' || (theme === 'auto' && defaultDark) ?
dark :
light;
Object.entries(finalTheme).forEach(([key, value]) => {
document.documentElement.style.setProperty(
`--${key}`,
value
);
});
</script> </script>
</html> </html>

View File

@ -1,5 +1,5 @@
export interface UiSettings { export interface UiSettings {
theme: string; theme: 'auto' | 'dark' | 'light';
showRelativeDates: boolean; showRelativeDates: boolean;
shortDateFormat: string; shortDateFormat: string;
longDateFormat: string; longDateFormat: string;

View File

@ -39,7 +39,7 @@ namespace Sonarr.Http.Frontend.Mappers
return stream; return stream;
} }
protected string GetHtmlText() protected virtual string GetHtmlText()
{ {
if (RuntimeInfo.IsProduction && _generatedContent != null) if (RuntimeInfo.IsProduction && _generatedContent != null)
{ {

View File

@ -9,6 +9,8 @@ namespace Sonarr.Http.Frontend.Mappers
{ {
public class LoginHtmlMapper : HtmlMapperBase public class LoginHtmlMapper : HtmlMapperBase
{ {
private readonly IConfigFileProvider _configFileProvider;
public LoginHtmlMapper(IAppFolderInfo appFolderInfo, public LoginHtmlMapper(IAppFolderInfo appFolderInfo,
IDiskProvider diskProvider, IDiskProvider diskProvider,
Lazy<ICacheBreakerProvider> cacheBreakProviderFactory, Lazy<ICacheBreakerProvider> cacheBreakProviderFactory,
@ -16,6 +18,7 @@ namespace Sonarr.Http.Frontend.Mappers
Logger logger) Logger logger)
: base(diskProvider, cacheBreakProviderFactory, logger) : base(diskProvider, cacheBreakProviderFactory, logger)
{ {
_configFileProvider = configFileProvider;
HtmlPath = Path.Combine(appFolderInfo.StartUpFolder, configFileProvider.UiFolder, "login.html"); HtmlPath = Path.Combine(appFolderInfo.StartUpFolder, configFileProvider.UiFolder, "login.html");
UrlBase = configFileProvider.UrlBase; UrlBase = configFileProvider.UrlBase;
} }
@ -29,5 +32,15 @@ namespace Sonarr.Http.Frontend.Mappers
{ {
return resourceUrl.StartsWith("/login"); return resourceUrl.StartsWith("/login");
} }
protected override string GetHtmlText()
{
var html = base.GetHtmlText();
var theme = _configFileProvider.Theme;
html = html.Replace("_THEME_", theme);
return html;
}
} }
} }