mirror of
https://github.com/Sonarr/Sonarr.git
synced 2024-10-29 23:12:39 +01:00
parent
f81bb3ec19
commit
cae134ec7b
@ -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>
|
||||||
|
@ -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);
|
|
37
frontend/src/App/ApplyTheme.tsx
Normal file
37
frontend/src/App/ApplyTheme.tsx
Normal 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;
|
@ -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,
|
||||||
|
@ -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>
|
||||||
|
@ -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;
|
||||||
|
@ -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)
|
||||||
{
|
{
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user