New: Dark theme for login screen

(cherry picked from commit cae134ec7b331d1c906343716472f3d043614b2c)
This commit is contained in:
Mark McDowall
2024-05-03 20:53:03 -07:00
committed by Bogdan
parent 076a4f2574
commit 596efe8fb0
10 changed files with 137 additions and 82 deletions

View File

@@ -12,11 +12,10 @@ function App({ store, history }) {
<DocumentTitle title={window.Prowlarr.instanceName}> <DocumentTitle title={window.Prowlarr.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.Prowlarr.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.Prowlarr.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

@@ -188,7 +188,7 @@ module.exports = {
// Charts // Charts
chartBackgroundColor: '#262626', chartBackgroundColor: '#262626',
failedColors: ['#ffbeb2', '#feb4a6', '#fdab9b', '#fca290', '#fb9984', '#fa8f79', '#f9856e', '#f77b66', '#f5715d', '#f36754', '#f05c4d', '#ec5049', '#e74545', '#e13b42', '#da323f', '#d3293d', '#ca223c', '#c11a3b', '#b8163a', '#ae123a'], failedColors: ['#ffbeb2', '#feb4a6', '#fdab9b', '#fca290', '#fb9984', '#fa8f79', '#f9856e', '#f77b66', '#f5715d', '#f36754', '#f05c4d', '#ec5049', '#e74545', '#e13b42', '#da323f', '#d3293d', '#ca223c', '#c11a3b', '#b8163a', '#ae123a'].join(','),
chartColorsDiversified: ['#90caf9', '#f4d166', '#ff8a65', '#ce93d8', '#80cba9', '#ffab91', '#8097ea', '#bcaaa4', '#a57583', '#e4e498', '#9e96af', '#c6ab81', '#6972c6', '#619fc6', '#81ad81', '#f48fb1', '#82afca', '#b5b071', '#8b959b', '#7ec0b4'], chartColorsDiversified: ['#90caf9', '#f4d166', '#ff8a65', '#ce93d8', '#80cba9', '#ffab91', '#8097ea', '#bcaaa4', '#a57583', '#e4e498', '#9e96af', '#c6ab81', '#6972c6', '#619fc6', '#81ad81', '#f48fb1', '#82afca', '#b5b071', '#8b959b', '#7ec0b4'].join(','),
chartColors: ['#f4d166', '#f6c760', '#f8bc58', '#f8b252', '#f7a84a', '#f69e41', '#f49538', '#f38b2f', '#f28026', '#f0751e', '#eb6c1c', '#e4641e', '#de5d1f', '#d75521', '#cf4f22', '#c64a22', '#bc4623', '#b24223', '#a83e24', '#9e3a26'] chartColors: ['#f4d166', '#f6c760', '#f8bc58', '#f8b252', '#f7a84a', '#f69e41', '#f49538', '#f38b2f', '#f28026', '#f0751e', '#eb6c1c', '#e4641e', '#de5d1f', '#d75521', '#cf4f22', '#c64a22', '#bc4623', '#b24223', '#a83e24', '#9e3a26'].join(',')
}; };

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

@@ -188,7 +188,7 @@ module.exports = {
// Charts // Charts
chartBackgroundColor: '#fff', chartBackgroundColor: '#fff',
failedColors: ['#ffbeb2', '#feb4a6', '#fdab9b', '#fca290', '#fb9984', '#fa8f79', '#f9856e', '#f77b66', '#f5715d', '#f36754', '#f05c4d', '#ec5049', '#e74545', '#e13b42', '#da323f', '#d3293d', '#ca223c', '#c11a3b', '#b8163a', '#ae123a'], failedColors: ['#ffbeb2', '#feb4a6', '#fdab9b', '#fca290', '#fb9984', '#fa8f79', '#f9856e', '#f77b66', '#f5715d', '#f36754', '#f05c4d', '#ec5049', '#e74545', '#e13b42', '#da323f', '#d3293d', '#ca223c', '#c11a3b', '#b8163a', '#ae123a'].join(','),
chartColorsDiversified: ['#90caf9', '#f4d166', '#ff8a65', '#ce93d8', '#80cba9', '#ffab91', '#8097ea', '#bcaaa4', '#a57583', '#e4e498', '#9e96af', '#c6ab81', '#6972c6', '#619fc6', '#81ad81', '#f48fb1', '#82afca', '#b5b071', '#8b959b', '#7ec0b4'], chartColorsDiversified: ['#90caf9', '#f4d166', '#ff8a65', '#ce93d8', '#80cba9', '#ffab91', '#8097ea', '#bcaaa4', '#a57583', '#e4e498', '#9e96af', '#c6ab81', '#6972c6', '#619fc6', '#81ad81', '#f48fb1', '#82afca', '#b5b071', '#8b959b', '#7ec0b4'].join(','),
chartColors: ['#f4d166', '#f6c760', '#f8bc58', '#f8b252', '#f7a84a', '#f69e41', '#f49538', '#f38b2f', '#f28026', '#f0751e', '#eb6c1c', '#e4641e', '#de5d1f', '#d75521', '#cf4f22', '#c64a22', '#bc4623', '#b24223', '#a83e24', '#9e3a26'] chartColors: ['#f4d166', '#f6c760', '#f8bc58', '#f8b252', '#f7a84a', '#f69e41', '#f49538', '#f38b2f', '#f28026', '#f0751e', '#eb6c1c', '#e4641e', '#de5d1f', '#d75521', '#cf4f22', '#c64a22', '#bc4623', '#b24223', '#a83e24', '#9e3a26'].join(',')
}; };

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: #464b51; 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: '#464b51',
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,4 +1,5 @@
export interface UiSettings { export interface UiSettings {
theme: 'auto' | 'dark' | 'light';
showRelativeDates: boolean; showRelativeDates: boolean;
shortDateFormat: string; shortDateFormat: string;
longDateFormat: string; longDateFormat: string;

View File

@@ -39,7 +39,7 @@ namespace Prowlarr.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 Prowlarr.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 Prowlarr.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 Prowlarr.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;
}
} }
} }