Added ThemeSettings and ContainerSettings to Default Theme Template
Added ThemeSettings and ContainerSettings to Default Theme Template to Demonstrate how these features could be added to developers designing a theme as requested and discussed in Issue #2633
This commit is contained in:
parent
ea4b85ad54
commit
4400744f35
|
@ -1,20 +1,48 @@
|
||||||
@namespace [Owner].Theme.[Theme]
|
@namespace [Owner].Theme.[Theme]
|
||||||
@inherits ContainerBase
|
@inherits ContainerBase
|
||||||
|
@inject ISettingService SettingService
|
||||||
|
|
||||||
<div class="container">
|
<div class="@_classes">
|
||||||
|
@if (_title && ModuleState.Title != "-")
|
||||||
|
{
|
||||||
<div class="row px-4">
|
<div class="row px-4">
|
||||||
<div class="d-flex flex-nowrap">
|
<div class="d-flex flex-nowrap">
|
||||||
<ModuleActions /><h2><ModuleTitle /></h2>
|
<ModuleActions /><h2><ModuleTitle /></h2>
|
||||||
</div>
|
</div>
|
||||||
<hr class="app-rule" />
|
<hr class="app-rule" />
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<ModuleActions />
|
||||||
|
}
|
||||||
<div class="row px-4">
|
<div class="row px-4">
|
||||||
<div class="container">
|
<div class="container-fluid">
|
||||||
<ModuleInstance />
|
<ModuleInstance />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
public override string Name => "Container1";
|
public override string Name => "[Owner] [Theme] - Container1";
|
||||||
|
|
||||||
|
private bool _title = true;
|
||||||
|
private string _classes = "container-fluid";
|
||||||
|
|
||||||
|
protected override void OnParametersSet()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_title = bool.Parse(SettingService.GetSetting(ModuleState.Settings, GetType().Namespace + ":Title", "true"));
|
||||||
|
_classes += " " + SettingService.GetSetting(ModuleState.Settings, GetType().Namespace + ":Background", "");
|
||||||
|
_classes += " " + SettingService.GetSetting(ModuleState.Settings, GetType().Namespace + ":Text", "");
|
||||||
|
_classes += " " + SettingService.GetSetting(ModuleState.Settings, GetType().Namespace + ":Border", "");
|
||||||
|
_classes = _classes.Trim();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// error loading container settings
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
106
Oqtane.Server/wwwroot/Themes/Templates/External/Client/Containers/ContainerSettings.razor
vendored
Normal file
106
Oqtane.Server/wwwroot/Themes/Templates/External/Client/Containers/ContainerSettings.razor
vendored
Normal file
|
@ -0,0 +1,106 @@
|
||||||
|
@namespace [Owner].Theme.[Theme]
|
||||||
|
@inherits ModuleBase
|
||||||
|
@implements Oqtane.Interfaces.ISettingsControl
|
||||||
|
@inject ISettingService SettingService
|
||||||
|
@attribute [OqtaneIgnore]
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<div class="row mb-1 align-items-center">
|
||||||
|
<Label Class="col-sm-3" For="title" ResourceKey="Title" ResourceType="@resourceType" HelpText="Specify If The Module Title Should Be Displayed">Display Title?</Label>
|
||||||
|
<div class="col-sm-9">
|
||||||
|
<select id="title" class="form-select" @bind="@_title">
|
||||||
|
<option value="true">Yes</option>
|
||||||
|
<option value="false">No</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mb-1 align-items-center">
|
||||||
|
<Label Class="col-sm-3" For="background\" ResourceKey="Background" ResourceType="@resourceType" HelpText="Optionally Specify A Background Color For The Container">Background Color:</Label>
|
||||||
|
<div class="col-sm-9">
|
||||||
|
<select id="background" class="form-select" @bind="@_background">
|
||||||
|
<option value="">None</option>
|
||||||
|
<option value="bg-primary">Primary</option>
|
||||||
|
<option value="bg-secondary">Secondary</option>
|
||||||
|
<option value="bg-success">Success</option>
|
||||||
|
<option value="bg-danger">Danger</option>
|
||||||
|
<option value="bg-warning">Warning</option>
|
||||||
|
<option value="bg-info">Info</option>
|
||||||
|
<option value="bg-light">Light</option>
|
||||||
|
<option value="bg-dark">Dark</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mb-1 align-items-center">
|
||||||
|
<Label Class="col-sm-3" For="text" ResourceKey="Text" ResourceType="@resourceType" HelpText="Optionally Specify A Text Color For The Container">Text Color:</Label>
|
||||||
|
<div class="col-sm-9">
|
||||||
|
<select id="text" class="form-select" @bind="@_text">
|
||||||
|
<option value="">None</option>
|
||||||
|
<option value="text-primary">Primary</option>
|
||||||
|
<option value="text-secondary">Secondary</option>
|
||||||
|
<option value="text-success">Success</option>
|
||||||
|
<option value="text-danger">Danger</option>
|
||||||
|
<option value="text-warning">Warning</option>
|
||||||
|
<option value="text-info">Info</option>
|
||||||
|
<option value="text-light">Light</option>
|
||||||
|
<option value="text-dark">Dark</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mb-1 align-items-center">
|
||||||
|
<Label Class="col-sm-3" For="border" ResourceKey="Border" ResourceType="@resourceType" HelpText="Optionally Specify A Border For The Container">Border Color:</Label>
|
||||||
|
<div class="col-sm-9">
|
||||||
|
<select id="border" class="form-select" @bind="@_border">
|
||||||
|
<option value="">None</option>
|
||||||
|
<option value="border">Default</option>
|
||||||
|
<option value="border border-primary">Primary</option>
|
||||||
|
<option value="border border-secondary">Secondary</option>
|
||||||
|
<option value="border border-success">Success</option>
|
||||||
|
<option value="border border-danger">Danger</option>
|
||||||
|
<option value="border border-warning">Warning</option>
|
||||||
|
<option value="border border-info">Info</option>
|
||||||
|
<option value="border border-light">Light</option>
|
||||||
|
<option value="border border-dark">Dark</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
private string resourceType = "[Owner].Theme.[Theme].ContainerSettings, [Owner].Theme.[Theme].Client.Oqtane"; // for localization
|
||||||
|
private string _title = "true";
|
||||||
|
private string _background = "";
|
||||||
|
private string _text = "";
|
||||||
|
private string _border = "";
|
||||||
|
|
||||||
|
protected override void OnInitialized()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_title = SettingService.GetSetting(ModuleState.Settings, GetType().Namespace + ":Title", "true");
|
||||||
|
_background = SettingService.GetSetting(ModuleState.Settings, GetType().Namespace + ":Background", "");
|
||||||
|
_text = SettingService.GetSetting(ModuleState.Settings, GetType().Namespace + ":Text", "");
|
||||||
|
_border = SettingService.GetSetting(ModuleState.Settings, GetType().Namespace + ":Border", "");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
ModuleInstance.AddModuleMessage(ex.Message, MessageType.Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UpdateSettings()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var settings = await SettingService.GetModuleSettingsAsync(ModuleState.ModuleId);
|
||||||
|
settings = SettingService.SetSetting(settings, GetType().Namespace + ":Title", _title);
|
||||||
|
settings = SettingService.SetSetting(settings, GetType().Namespace + ":Background", _background);
|
||||||
|
settings = SettingService.SetSetting(settings, GetType().Namespace + ":Text", _text);
|
||||||
|
settings = SettingService.SetSetting(settings, GetType().Namespace + ":Border", _border);
|
||||||
|
await SettingService.UpdateModuleSettingsAsync(settings, ModuleState.ModuleId);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
ModuleInstance.AddModuleMessage(ex.Message, MessageType.Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -9,9 +9,11 @@ namespace [Owner].Theme.[Theme]
|
||||||
{
|
{
|
||||||
public Oqtane.Models.Theme Theme => new Oqtane.Models.Theme
|
public Oqtane.Models.Theme Theme => new Oqtane.Models.Theme
|
||||||
{
|
{
|
||||||
Name = "[Theme]",
|
Name = "[Owner] [Theme]",
|
||||||
Version = "1.0.0",
|
Version = "1.0.0",
|
||||||
PackageName = "[Owner].Theme.[Theme]",
|
PackageName = "[Owner].Theme.[Theme]",
|
||||||
|
ThemeSettingsType = "[Owner].Theme.[Theme].ThemeSettings, [Owner].Theme.[Theme].Client.Oqtane",
|
||||||
|
ContainerSettingsType = "[Owner].Theme.[Theme].ContainerSettings, [Owner].Theme.[Theme].Client.Oqtane",
|
||||||
Resources = new List<Resource>()
|
Resources = new List<Resource>()
|
||||||
{
|
{
|
||||||
// obtained from https://cdnjs.com/libraries
|
// obtained from https://cdnjs.com/libraries
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
@namespace [Owner].Theme.[Theme]
|
@namespace [Owner].Theme.[Theme]
|
||||||
@inherits ThemeBase
|
@inherits ThemeBase
|
||||||
|
@inject ISettingService SettingService
|
||||||
|
|
||||||
<main role="main">
|
<main role="main">
|
||||||
<nav class="navbar navbar-dark bg-primary fixed-top">
|
<nav class="navbar navbar-dark bg-primary fixed-top">
|
||||||
<Logo /><Menu Orientation="Horizontal" />
|
<Logo /><Menu Orientation="Horizontal" />
|
||||||
<div class="controls ms-auto">
|
<div class="controls ms-auto">
|
||||||
<div class="controls-group"><UserProfile /> <Login /> <ControlPanel ButtonClass="btn-outline-light" /></div>
|
<div class="controls-group"><UserProfile ShowRegister="@_register" /> <Login ShowLogin="@_login" /> <ControlPanel ButtonClass="btn-outline-light" /></div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
|
@ -90,11 +91,41 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Pane Name="Bottom Full Width" />
|
<Pane Name="Bottom Full Width" />
|
||||||
|
@if (_footer)
|
||||||
|
{
|
||||||
|
<div style="clear: both; height: 30px;"></div>
|
||||||
|
<div class="bg-primary fixed-bottom footer">
|
||||||
|
<Pane Name="Footer" />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<Pane Name="Footer" />
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
public override string Name => "Theme1";
|
public override string Name => "Theme1";
|
||||||
|
|
||||||
public override string Panes => PaneNames.Admin + ",Top Full Width,Top 100%,Left 50%,Right 50%,Left 33%,Center 33%,Right 33%,Left Outer 25%,Left Inner 25%,Right Inner 25%,Right Outer 25%,Left 25%,Center 50%,Right 25%,Left Sidebar 66%,Right Sidebar 33%,Left Sidebar 33%,Right Sidebar 66%,Bottom 100%,Bottom Full Width";
|
public override string Panes => PaneNames.Admin + ",Top Full Width,Top 100%,Left 50%,Right 50%,Left 33%,Center 33%,Right 33%,Left Outer 25%,Left Inner 25%,Right Inner 25%,Right Outer 25%,Left 25%,Center 50%,Right 25%,Left Sidebar 66%,Right Sidebar 33%,Left Sidebar 33%,Right Sidebar 66%,Bottom 100%,Bottom Full Width,Footer";
|
||||||
|
|
||||||
|
private bool _login = true;
|
||||||
|
private bool _register = true;
|
||||||
|
private bool _footer = false;
|
||||||
|
|
||||||
|
protected override void OnParametersSet()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var settings = SettingService.MergeSettings(PageState.Site.Settings, PageState.Page.Settings);
|
||||||
|
_login = bool.Parse(SettingService.GetSetting(settings, GetType().Namespace + ":Login", "true"));
|
||||||
|
_register = bool.Parse(SettingService.GetSetting(settings, GetType().Namespace + ":Register", "true"));
|
||||||
|
_footer = bool.Parse(SettingService.GetSetting(settings, GetType().Namespace + ":Footer", "false"));
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// error loading theme settings
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
160
Oqtane.Server/wwwroot/Themes/Templates/External/Client/Themes/ThemeSettings.razor
vendored
Normal file
160
Oqtane.Server/wwwroot/Themes/Templates/External/Client/Themes/ThemeSettings.razor
vendored
Normal file
|
@ -0,0 +1,160 @@
|
||||||
|
@namespace [Owner].Theme.[Theme]
|
||||||
|
@inherits ModuleBase
|
||||||
|
@implements Oqtane.Interfaces.ISettingsControl
|
||||||
|
@inject ISettingService SettingService
|
||||||
|
@inject IStringLocalizer<ThemeSettings> Localizer
|
||||||
|
@inject IStringLocalizer<SharedResources> SharedLocalizer
|
||||||
|
@attribute [OqtaneIgnore]
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<div class="row mb-1 align-items-center">
|
||||||
|
<Label Class="col-sm-3" For="scope" ResourceKey="Scope" ResourceType="@resourceType" HelpText="Specify if the settings are applicable to this page or the entire site.">Setting Scope:</Label>
|
||||||
|
<div class="col-sm-9">
|
||||||
|
<select id="scope" class="form-select" value="@_scope" @onchange="(e => ScopeChanged(e))">
|
||||||
|
@if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin))
|
||||||
|
{
|
||||||
|
<option value="site">@Localizer["Site"]</option>
|
||||||
|
}
|
||||||
|
<option value="page">@Localizer["Page"]</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mb-1 align-items-center">
|
||||||
|
<Label Class="col-sm-3" For="login" ResourceKey="Login" ResourceType="@resourceType" HelpText="Specify if a Login option should be displayed. Note that this option does not prevent the login page from being accessible via a direct url.">Show Login?</Label>
|
||||||
|
<div class="col-sm-9">
|
||||||
|
<select id="login" class="form-select" @bind="@_login">
|
||||||
|
<option value="-"><@SharedLocalizer["Not Specified"]></option>
|
||||||
|
<option value="true">@SharedLocalizer["Yes"]</option>
|
||||||
|
<option value="false">@SharedLocalizer["No"]</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mb-1 align-items-center">
|
||||||
|
<Label Class="col-sm-3" For="register" ResourceKey="Register" ResourceType="@resourceType" HelpText="Specify if a Register option should be displayed. Note that this option is also dependent on the Allow Registration option in Site Settings.">Show Register?</Label>
|
||||||
|
<div class="col-sm-9">
|
||||||
|
<select id="register" class="form-select" @bind="@_register">
|
||||||
|
<option value="-"><@SharedLocalizer["Not Specified"]></option>
|
||||||
|
<option value="true">@SharedLocalizer["Yes"]</option>
|
||||||
|
<option value="false">@SharedLocalizer["No"]</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mb-1 align-items-center">
|
||||||
|
<Label Class="col-sm-3" For="footer" ResourceKey="Footer" ResourceType="@resourceType" HelpText="Specify if a Footer pane should always be displayed in a fixed location at the bottom of the page">Display Fixed Footer?</Label>
|
||||||
|
<div class="col-sm-9">
|
||||||
|
<select id="footer" class="form-select" @bind="@_footer">
|
||||||
|
<option value="-"><@SharedLocalizer["Not Specified"]></option>
|
||||||
|
<option value="true">@SharedLocalizer["Yes"]</option>
|
||||||
|
<option value="false">@SharedLocalizer["No"]</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
private int pageId = -1;
|
||||||
|
private string resourceType = "[Owner].Theme.[Theme].ThemeSettings, [Owner].Theme.[Theme].Client.Oqtane"; // for localization
|
||||||
|
private string _scope = "page";
|
||||||
|
private string _login = "-";
|
||||||
|
private string _register = "-";
|
||||||
|
private string _footer = "-";
|
||||||
|
|
||||||
|
protected override async Task OnInitializedAsync()
|
||||||
|
{
|
||||||
|
if (PageState.QueryString.ContainsKey("id"))
|
||||||
|
{
|
||||||
|
pageId = int.Parse(PageState.QueryString["id"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await LoadSettings();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
await logger.LogError(ex, "Error Loading Settings {Error}", ex.Message);
|
||||||
|
AddModuleMessage("Error Loading Settings", MessageType.Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task LoadSettings()
|
||||||
|
{
|
||||||
|
if (_scope == "site")
|
||||||
|
{
|
||||||
|
var settings = PageState.Site.Settings;
|
||||||
|
_login = SettingService.GetSetting(settings, GetType().Namespace + ":Login", "true");
|
||||||
|
_register = SettingService.GetSetting(settings, GetType().Namespace + ":Register", "true");
|
||||||
|
_footer = SettingService.GetSetting(settings, GetType().Namespace + ":Footer", "false");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var settings = await SettingService.GetPageSettingsAsync(pageId);
|
||||||
|
settings = SettingService.MergeSettings(PageState.Site.Settings, settings);
|
||||||
|
_login = SettingService.GetSetting(settings, GetType().Namespace + ":Login", "-");
|
||||||
|
_register = SettingService.GetSetting(settings, GetType().Namespace + ":Register", "-");
|
||||||
|
_footer = SettingService.GetSetting(settings, GetType().Namespace + ":Footer", "-");
|
||||||
|
}
|
||||||
|
await Task.Yield();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ScopeChanged(ChangeEventArgs eventArgs)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_scope = (string)eventArgs.Value;
|
||||||
|
await LoadSettings();
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
await logger.LogError(ex, "Error Loading Settings {Error}", ex.Message);
|
||||||
|
AddModuleMessage("Error Loading Settings", MessageType.Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UpdateSettings()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (_scope == "site")
|
||||||
|
{
|
||||||
|
var settings = await SettingService.GetSiteSettingsAsync(PageState.Site.SiteId);
|
||||||
|
if (_login != "-")
|
||||||
|
{
|
||||||
|
settings = SettingService.SetSetting(settings, GetType().Namespace + ":Login", _login, true);
|
||||||
|
}
|
||||||
|
if (_register != "-")
|
||||||
|
{
|
||||||
|
settings = SettingService.SetSetting(settings, GetType().Namespace + ":Register", _register, true);
|
||||||
|
}
|
||||||
|
if (_footer != "-")
|
||||||
|
{
|
||||||
|
settings = SettingService.SetSetting(settings, GetType().Namespace + ":Footer", _footer, true);
|
||||||
|
}
|
||||||
|
await SettingService.UpdateSiteSettingsAsync(settings, PageState.Site.SiteId);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var settings = await SettingService.GetPageSettingsAsync(pageId);
|
||||||
|
if (_login != "-")
|
||||||
|
{
|
||||||
|
settings = SettingService.SetSetting(settings, GetType().Namespace + ":Login", _login);
|
||||||
|
}
|
||||||
|
if (_register != "-")
|
||||||
|
{
|
||||||
|
settings = SettingService.SetSetting(settings, GetType().Namespace + ":Register", _register);
|
||||||
|
}
|
||||||
|
if (_footer != "-")
|
||||||
|
{
|
||||||
|
settings = SettingService.SetSetting(settings, GetType().Namespace + ":Footer", _footer);
|
||||||
|
}
|
||||||
|
await SettingService.UpdatePageSettingsAsync(settings, pageId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
await logger.LogError(ex, "Error Saving Settings {Error}", ex.Message);
|
||||||
|
AddModuleMessage("Error Saving Settings", MessageType.Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,13 +1,17 @@
|
||||||
@using System
|
@using System
|
||||||
@using System.Linq
|
@using System.Linq
|
||||||
@using System.Collections.Generic
|
@using System.Collections.Generic
|
||||||
@using System.Net.Http
|
@using System.Net.Http
|
||||||
@using System.Net.Http.Json
|
@using System.Net.Http.Json
|
||||||
|
|
||||||
|
@using Microsoft.AspNetCore.Components.Authorization
|
||||||
@using Microsoft.AspNetCore.Components.Routing
|
@using Microsoft.AspNetCore.Components.Routing
|
||||||
@using Microsoft.AspNetCore.Components.Web
|
@using Microsoft.AspNetCore.Components.Web
|
||||||
|
@using Microsoft.Extensions.Localization
|
||||||
@using Microsoft.JSInterop
|
@using Microsoft.JSInterop
|
||||||
|
|
||||||
|
@using Oqtane
|
||||||
|
@using Oqtane.Client
|
||||||
@using Oqtane.Models
|
@using Oqtane.Models
|
||||||
@using Oqtane.Modules
|
@using Oqtane.Modules
|
||||||
@using Oqtane.Modules.Controls
|
@using Oqtane.Modules.Controls
|
||||||
|
|
Loading…
Reference in New Issue
Block a user