mirror of
https://github.com/Prowlarr/Prowlarr.git
synced 2025-09-17 17:14:18 +02:00
New: Native Theme Engine
This commit is contained in:
@@ -4,6 +4,7 @@ import React from 'react';
|
|||||||
import DocumentTitle from 'react-document-title';
|
import DocumentTitle from 'react-document-title';
|
||||||
import { Provider } from 'react-redux';
|
import { Provider } from 'react-redux';
|
||||||
import PageConnector from 'Components/Page/PageConnector';
|
import PageConnector from 'Components/Page/PageConnector';
|
||||||
|
import ApplyTheme from './ApplyTheme';
|
||||||
import AppRoutes from './AppRoutes';
|
import AppRoutes from './AppRoutes';
|
||||||
|
|
||||||
function App({ store, history }) {
|
function App({ store, history }) {
|
||||||
@@ -11,9 +12,11 @@ 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>
|
||||||
<PageConnector>
|
<PageConnector>
|
||||||
<AppRoutes app={App} />
|
<AppRoutes app={App} />
|
||||||
</PageConnector>
|
</PageConnector>
|
||||||
|
</ApplyTheme>
|
||||||
</ConnectedRouter>
|
</ConnectedRouter>
|
||||||
</Provider>
|
</Provider>
|
||||||
</DocumentTitle>
|
</DocumentTitle>
|
||||||
|
49
frontend/src/App/ApplyTheme.js
Normal file
49
frontend/src/App/ApplyTheme.js
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Fragment, 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
|
||||||
|
function updateCSSVariables() {
|
||||||
|
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]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// On Component Mount and Component Update
|
||||||
|
useEffect(() => {
|
||||||
|
updateCSSVariables(theme);
|
||||||
|
}, [theme]);
|
||||||
|
|
||||||
|
return <Fragment>{children}</Fragment>;
|
||||||
|
}
|
||||||
|
|
||||||
|
ApplyTheme.propTypes = {
|
||||||
|
theme: PropTypes.string.isRequired,
|
||||||
|
children: PropTypes.object.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(createMapStateToProps)(ApplyTheme);
|
@@ -10,6 +10,8 @@ import PageContent from 'Components/Page/PageContent';
|
|||||||
import PageContentBody from 'Components/Page/PageContentBody';
|
import PageContentBody from 'Components/Page/PageContentBody';
|
||||||
import { inputTypes } from 'Helpers/Props';
|
import { inputTypes } from 'Helpers/Props';
|
||||||
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
|
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
|
||||||
|
import themes from 'Styles/Themes';
|
||||||
|
import titleCase from 'Utilities/String/titleCase';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
|
|
||||||
export const firstDayOfWeekOptions = [
|
export const firstDayOfWeekOptions = [
|
||||||
@@ -62,6 +64,9 @@ class UISettings extends Component {
|
|||||||
|
|
||||||
const uiLanguages = languages.filter((item) => item.value !== 'Original');
|
const uiLanguages = languages.filter((item) => item.value !== 'Original');
|
||||||
|
|
||||||
|
const themeOptions = Object.keys(themes)
|
||||||
|
.map((theme) => ({ key: theme, value: titleCase(theme) }));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContent title={translate('UISettings')}>
|
<PageContent title={translate('UISettings')}>
|
||||||
<SettingsToolbarConnector
|
<SettingsToolbarConnector
|
||||||
@@ -138,6 +143,17 @@ class UISettings extends Component {
|
|||||||
</FieldSet>
|
</FieldSet>
|
||||||
|
|
||||||
<FieldSet legend={translate('Style')}>
|
<FieldSet legend={translate('Style')}>
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('Theme')}</FormLabel>
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.SELECT}
|
||||||
|
name="theme"
|
||||||
|
helpText={translate('ThemeHelpText', ['Theme.Park'])}
|
||||||
|
values={themeOptions}
|
||||||
|
onChange={onInputChange}
|
||||||
|
{...settings.theme}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
<FormGroup>
|
<FormGroup>
|
||||||
<FormLabel>{translate('SettingsEnableColorImpairedMode')}</FormLabel>
|
<FormLabel>{translate('SettingsEnableColorImpairedMode')}</FormLabel>
|
||||||
<FormInputGroup
|
<FormInputGroup
|
||||||
|
5
frontend/src/Styles/Themes/index.js
Normal file
5
frontend/src/Styles/Themes/index.js
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import * as light from './light';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
light
|
||||||
|
};
|
191
frontend/src/Styles/Themes/light.js
Normal file
191
frontend/src/Styles/Themes/light.js
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
const prowlarrOrange = '#e66000';
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
textColor: '#515253',
|
||||||
|
defaultColor: '#333',
|
||||||
|
disabledColor: '#999',
|
||||||
|
dimColor: '#555',
|
||||||
|
black: '#000',
|
||||||
|
white: '#fff',
|
||||||
|
offWhite: '#f5f7fa',
|
||||||
|
primaryColor: '#5d9cec',
|
||||||
|
selectedColor: '#f9be03',
|
||||||
|
successColor: '#27c24c',
|
||||||
|
dangerColor: '#f05050',
|
||||||
|
warningColor: '#ffa500',
|
||||||
|
infoColor: '#5d9cec',
|
||||||
|
queueColor: '#7a43b6',
|
||||||
|
purple: '#7a43b6',
|
||||||
|
pink: '#ff69b4',
|
||||||
|
prowlarrOrange,
|
||||||
|
helpTextColor: '#909293',
|
||||||
|
darkGray: '#888',
|
||||||
|
gray: '#adadad',
|
||||||
|
lightGray: '#ddd',
|
||||||
|
disabledInputColor: '#808080',
|
||||||
|
|
||||||
|
// Theme Colors
|
||||||
|
|
||||||
|
themeBlue: prowlarrOrange,
|
||||||
|
themeRed: '#c4273c',
|
||||||
|
themeDarkColor: '#595959',
|
||||||
|
themeLightColor: '#707070',
|
||||||
|
|
||||||
|
torrentColor: '#00853d',
|
||||||
|
usenetColor: '#17b1d9',
|
||||||
|
|
||||||
|
// Links
|
||||||
|
defaultLinkHoverColor: '#fff',
|
||||||
|
linkColor: '#5d9cec',
|
||||||
|
linkHoverColor: '#1b72e2',
|
||||||
|
|
||||||
|
// Sidebar
|
||||||
|
|
||||||
|
sidebarColor: '#e1e2e3',
|
||||||
|
sidebarBackgroundColor: '#595959',
|
||||||
|
sidebarActiveBackgroundColor: '#333333',
|
||||||
|
|
||||||
|
// Toolbar
|
||||||
|
toolbarColor: '#e1e2e3',
|
||||||
|
toolbarBackgroundColor: '#707070',
|
||||||
|
toolbarMenuItemBackgroundColor: '#606060',
|
||||||
|
toolbarMenuItemHoverBackgroundColor: '#515151',
|
||||||
|
toolbarLabelColor: '#e1e2e3',
|
||||||
|
|
||||||
|
// Accents
|
||||||
|
borderColor: '#e5e5e5',
|
||||||
|
inputBorderColor: '#dde6e9',
|
||||||
|
inputBoxShadowColor: 'rgba(0, 0, 0, 0.075)',
|
||||||
|
inputFocusBorderColor: '#66afe9',
|
||||||
|
inputFocusBoxShadowColor: 'rgba(102, 175, 233, 0.6)',
|
||||||
|
inputErrorBorderColor: '#f05050',
|
||||||
|
inputErrorBoxShadowColor: 'rgba(240, 80, 80, 0.6)',
|
||||||
|
inputWarningBorderColor: '#ffa500',
|
||||||
|
inputWarningBoxShadowColor: 'rgba(255, 165, 0, 0.6)',
|
||||||
|
colorImpairedGradient: '#ffffff',
|
||||||
|
colorImpairedGradientDark: '#f4f5f6',
|
||||||
|
|
||||||
|
//
|
||||||
|
// Buttons
|
||||||
|
|
||||||
|
defaultBackgroundColor: '#fff',
|
||||||
|
defaultBorderColor: '#eaeaea',
|
||||||
|
defaultHoverBackgroundColor: '#f5f5f5',
|
||||||
|
defaultHoverBorderColor: '#d6d6d6;',
|
||||||
|
|
||||||
|
primaryBackgroundColor: '#5d9cec',
|
||||||
|
primaryBorderColor: '#5899eb',
|
||||||
|
primaryHoverBackgroundColor: '#4b91ea',
|
||||||
|
primaryHoverBorderColor: '#3483e7;',
|
||||||
|
|
||||||
|
successBackgroundColor: '#27c24c',
|
||||||
|
successBorderColor: '#26be4a',
|
||||||
|
successHoverBackgroundColor: '#24b145',
|
||||||
|
successHoverBorderColor: '#1f9c3d;',
|
||||||
|
|
||||||
|
warningBackgroundColor: '#ff902b',
|
||||||
|
warningBorderColor: '#ff8d26',
|
||||||
|
warningHoverBackgroundColor: '#ff8517',
|
||||||
|
warningHoverBorderColor: '#fc7800;',
|
||||||
|
|
||||||
|
dangerBackgroundColor: '#f05050',
|
||||||
|
dangerBorderColor: '#f04b4b',
|
||||||
|
dangerHoverBackgroundColor: '#ee3d3d',
|
||||||
|
dangerHoverBorderColor: '#ec2626;',
|
||||||
|
|
||||||
|
iconButtonDisabledColor: '#7a7a7a',
|
||||||
|
iconButtonHoverColor: '#666',
|
||||||
|
iconButtonHoverLightColor: '#ccc',
|
||||||
|
|
||||||
|
//
|
||||||
|
// Modal
|
||||||
|
|
||||||
|
modalBackdropBackgroundColor: 'rgba(0, 0, 0, 0.6)',
|
||||||
|
modalBackgroundColor: '#fff',
|
||||||
|
modalCloseButtonHoverColor: '#888',
|
||||||
|
|
||||||
|
//
|
||||||
|
// Menu
|
||||||
|
menuItemColor: '#e1e2e3',
|
||||||
|
menuItemHoverColor: '#fbfcfc',
|
||||||
|
menuItemHoverBackgroundColor: '#f5f7fa',
|
||||||
|
|
||||||
|
//
|
||||||
|
// Toolbar
|
||||||
|
|
||||||
|
toobarButtonHoverColor: '#e66000',
|
||||||
|
toobarButtonSelectedColor: '#e66000',
|
||||||
|
|
||||||
|
//
|
||||||
|
// Scroller
|
||||||
|
|
||||||
|
scrollbarBackgroundColor: '#9ea4b9',
|
||||||
|
scrollbarHoverBackgroundColor: '#656d8c',
|
||||||
|
|
||||||
|
//
|
||||||
|
// Card
|
||||||
|
|
||||||
|
cardShadowColor: '#e1e1e1',
|
||||||
|
cardAlternateBackgroundColor: '#f5f5f5',
|
||||||
|
|
||||||
|
//
|
||||||
|
// Alert
|
||||||
|
|
||||||
|
alertDangerBorderColor: '#ebccd1',
|
||||||
|
alertDangerBackgroundColor: '#f2dede',
|
||||||
|
alertDangerColor: '#a94442',
|
||||||
|
|
||||||
|
alertInfoBorderColor: '#bce8f1',
|
||||||
|
alertInfoBackgroundColor: '#d9edf7',
|
||||||
|
alertInfoColor: '#31708f',
|
||||||
|
|
||||||
|
alertSuccessBorderColor: '#d6e9c6',
|
||||||
|
alertSuccessBackgroundColor: '#dff0d8',
|
||||||
|
alertSuccessColor: '#3c763d',
|
||||||
|
|
||||||
|
alertWarningBorderColor: '#faebcc',
|
||||||
|
alertWarningBackgroundColor: '#fcf8e3',
|
||||||
|
alertWarningColor: '#8a6d3b',
|
||||||
|
|
||||||
|
//
|
||||||
|
// Slider
|
||||||
|
|
||||||
|
sliderAccentColor: '#5d9cec',
|
||||||
|
|
||||||
|
//
|
||||||
|
// Form
|
||||||
|
|
||||||
|
advancedFormLabelColor: '#ff902b',
|
||||||
|
disabledCheckInputColor: '#ddd',
|
||||||
|
|
||||||
|
//
|
||||||
|
// Popover
|
||||||
|
|
||||||
|
popoverTitleBackgroundColor: '#f7f7f7',
|
||||||
|
popoverTitleBorderColor: '#ebebeb',
|
||||||
|
popoverShadowColor: 'rgba(0, 0, 0, 0.2)',
|
||||||
|
popoverArrowBorderColor: '#fff',
|
||||||
|
|
||||||
|
popoverTitleBackgroundInverseColor: '#595959',
|
||||||
|
popoverTitleBorderInverseColor: '#707070',
|
||||||
|
popoverShadowInverseColor: 'rgba(0, 0, 0, 0.2)',
|
||||||
|
popoverArrowBorderInverseColor: 'rgba(58, 63, 81, 0.75)',
|
||||||
|
|
||||||
|
//
|
||||||
|
// Calendar
|
||||||
|
|
||||||
|
calendarTodayBackgroundColor: '#ddd',
|
||||||
|
calendarBorderColor: '#cecece',
|
||||||
|
calendarTextDim: '#666',
|
||||||
|
|
||||||
|
//
|
||||||
|
// Table
|
||||||
|
|
||||||
|
tableRowHoverBackgroundColor: '#fafbfc',
|
||||||
|
|
||||||
|
//
|
||||||
|
// Charts
|
||||||
|
|
||||||
|
failedColors: ['#ffbeb2', '#feb4a6', '#fdab9b', '#fca290', '#fb9984', '#fa8f79', '#f9856e', '#f77b66', '#f5715d', '#f36754', '#f05c4d', '#ec5049', '#e74545', '#e13b42', '#da323f', '#d3293d', '#ca223c', '#c11a3b', '#b8163a', '#ae123a'],
|
||||||
|
chartColors: ['#f4d166', '#f6c760', '#f8bc58', '#f8b252', '#f7a84a', '#f69e41', '#f49538', '#f38b2f', '#f28026', '#f0751e', '#eb6c1c', '#e4641e', '#de5d1f', '#d75521', '#cf4f22', '#c64a22', '#bc4623', '#b24223', '#a83e24', '#9e3a26']
|
||||||
|
};
|
@@ -55,6 +55,7 @@ namespace NzbDrone.Core.Configuration
|
|||||||
string PostgresPassword { get; }
|
string PostgresPassword { get; }
|
||||||
string PostgresMainDb { get; }
|
string PostgresMainDb { get; }
|
||||||
string PostgresLogDb { get; }
|
string PostgresLogDb { get; }
|
||||||
|
string Theme { get; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class ConfigFileProvider : IConfigFileProvider
|
public class ConfigFileProvider : IConfigFileProvider
|
||||||
@@ -199,6 +200,7 @@ namespace NzbDrone.Core.Configuration
|
|||||||
public string PostgresPassword => GetValue("PostgresPassword", string.Empty, persist: false);
|
public string PostgresPassword => GetValue("PostgresPassword", string.Empty, persist: false);
|
||||||
public string PostgresMainDb => GetValue("PostgresMainDb", "prowlarr-main", persist: false);
|
public string PostgresMainDb => GetValue("PostgresMainDb", "prowlarr-main", persist: false);
|
||||||
public string PostgresLogDb => GetValue("PostgresLogDb", "prowlarr-log", persist: false);
|
public string PostgresLogDb => GetValue("PostgresLogDb", "prowlarr-log", persist: false);
|
||||||
|
public string Theme => GetValue("Theme", "light", persist: false);
|
||||||
public int PostgresPort => GetValueInt("PostgresPort", 5432, persist: false);
|
public int PostgresPort => GetValueInt("PostgresPort", 5432, persist: false);
|
||||||
public bool LogSql => GetValueBoolean("LogSql", false, persist: false);
|
public bool LogSql => GetValueBoolean("LogSql", false, persist: false);
|
||||||
public int LogRotate => GetValueInt("LogRotate", 50, persist: false);
|
public int LogRotate => GetValueInt("LogRotate", 50, persist: false);
|
||||||
|
@@ -391,6 +391,7 @@
|
|||||||
"TestAllApps": "Test All Apps",
|
"TestAllApps": "Test All Apps",
|
||||||
"TestAllClients": "Test All Clients",
|
"TestAllClients": "Test All Clients",
|
||||||
"TestAllIndexers": "Test All Indexers",
|
"TestAllIndexers": "Test All Indexers",
|
||||||
|
"ThemeHelpText": "Change Prowlarr UI theme, inspired by {0}",
|
||||||
"Time": "Time",
|
"Time": "Time",
|
||||||
"Title": "Title",
|
"Title": "Title",
|
||||||
"Today": "Today",
|
"Today": "Today",
|
||||||
|
@@ -1,4 +1,8 @@
|
|||||||
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using NzbDrone.Core.Configuration;
|
using NzbDrone.Core.Configuration;
|
||||||
|
using NzbDrone.Http.REST.Attributes;
|
||||||
using Prowlarr.Http;
|
using Prowlarr.Http;
|
||||||
|
|
||||||
namespace Prowlarr.Api.V1.Config
|
namespace Prowlarr.Api.V1.Config
|
||||||
@@ -6,14 +10,30 @@ namespace Prowlarr.Api.V1.Config
|
|||||||
[V1ApiController("config/ui")]
|
[V1ApiController("config/ui")]
|
||||||
public class UiConfigController : ConfigController<UiConfigResource>
|
public class UiConfigController : ConfigController<UiConfigResource>
|
||||||
{
|
{
|
||||||
public UiConfigController(IConfigService configService)
|
private readonly IConfigFileProvider _configFileProvider;
|
||||||
|
|
||||||
|
public UiConfigController(IConfigFileProvider configFileProvider, IConfigService configService)
|
||||||
: base(configService)
|
: base(configService)
|
||||||
{
|
{
|
||||||
|
_configFileProvider = configFileProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
[RestPutById]
|
||||||
|
public override ActionResult<UiConfigResource> SaveConfig(UiConfigResource resource)
|
||||||
|
{
|
||||||
|
var dictionary = resource.GetType()
|
||||||
|
.GetProperties(BindingFlags.Instance | BindingFlags.Public)
|
||||||
|
.ToDictionary(prop => prop.Name, prop => prop.GetValue(resource, null));
|
||||||
|
|
||||||
|
_configFileProvider.SaveConfigDictionary(dictionary);
|
||||||
|
_configService.SaveConfigDictionary(dictionary);
|
||||||
|
|
||||||
|
return Accepted(resource.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override UiConfigResource ToResource(IConfigService model)
|
protected override UiConfigResource ToResource(IConfigService model)
|
||||||
{
|
{
|
||||||
return UiConfigResourceMapper.ToResource(model);
|
return UiConfigResourceMapper.ToResource(_configFileProvider, model);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -17,11 +17,12 @@ namespace Prowlarr.Api.V1.Config
|
|||||||
|
|
||||||
public bool EnableColorImpairedMode { get; set; }
|
public bool EnableColorImpairedMode { get; set; }
|
||||||
public int UILanguage { get; set; }
|
public int UILanguage { get; set; }
|
||||||
|
public string Theme { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class UiConfigResourceMapper
|
public static class UiConfigResourceMapper
|
||||||
{
|
{
|
||||||
public static UiConfigResource ToResource(IConfigService model)
|
public static UiConfigResource ToResource(IConfigFileProvider config, IConfigService model)
|
||||||
{
|
{
|
||||||
return new UiConfigResource
|
return new UiConfigResource
|
||||||
{
|
{
|
||||||
@@ -34,7 +35,8 @@ namespace Prowlarr.Api.V1.Config
|
|||||||
ShowRelativeDates = model.ShowRelativeDates,
|
ShowRelativeDates = model.ShowRelativeDates,
|
||||||
|
|
||||||
EnableColorImpairedMode = model.EnableColorImpairedMode,
|
EnableColorImpairedMode = model.EnableColorImpairedMode,
|
||||||
UILanguage = model.UILanguage
|
UILanguage = model.UILanguage,
|
||||||
|
Theme = config.Theme
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -50,6 +50,7 @@ namespace Prowlarr.Http.Frontend
|
|||||||
builder.AppendLine($" release: '{BuildInfo.Release}',");
|
builder.AppendLine($" release: '{BuildInfo.Release}',");
|
||||||
builder.AppendLine($" version: '{BuildInfo.Version.ToString()}',");
|
builder.AppendLine($" version: '{BuildInfo.Version.ToString()}',");
|
||||||
builder.AppendLine($" instanceName: '{_configFileProvider.InstanceName.ToString()}',");
|
builder.AppendLine($" instanceName: '{_configFileProvider.InstanceName.ToString()}',");
|
||||||
|
builder.AppendLine($" theme: '{_configFileProvider.Theme.ToString()}',");
|
||||||
builder.AppendLine($" branch: '{_configFileProvider.Branch.ToLower()}',");
|
builder.AppendLine($" branch: '{_configFileProvider.Branch.ToLower()}',");
|
||||||
builder.AppendLine($" analytics: {_analyticsService.IsEnabled.ToString().ToLowerInvariant()},");
|
builder.AppendLine($" analytics: {_analyticsService.IsEnabled.ToString().ToLowerInvariant()},");
|
||||||
builder.AppendLine($" userHash: '{HashUtil.AnonymousToken()}',");
|
builder.AppendLine($" userHash: '{HashUtil.AnonymousToken()}',");
|
||||||
|
Reference in New Issue
Block a user