Add support for editing texture group in editor (without reimporting)
This commit is contained in:
@@ -4,7 +4,10 @@ using System.Xml;
|
||||
using FlaxEditor.Content;
|
||||
using FlaxEditor.Content.Import;
|
||||
using FlaxEditor.CustomEditors;
|
||||
using FlaxEditor.CustomEditors.Dedicated;
|
||||
using FlaxEditor.CustomEditors.Editors;
|
||||
using FlaxEditor.GUI;
|
||||
using FlaxEditor.Scripting;
|
||||
using FlaxEditor.Viewport.Previews;
|
||||
using FlaxEngine;
|
||||
using FlaxEngine.GUI;
|
||||
@@ -18,47 +21,57 @@ namespace FlaxEditor.Windows.Assets
|
||||
/// <seealso cref="FlaxEditor.Windows.Assets.AssetEditorWindow" />
|
||||
public sealed class TextureWindow : AssetEditorWindowBase<Texture>
|
||||
{
|
||||
private sealed class ProxyEditor : GenericEditor
|
||||
{
|
||||
public override void Initialize(LayoutElementsContainer layout)
|
||||
{
|
||||
var window = ((PropertiesProxy)Values[0])._window;
|
||||
var texture = window?.Asset;
|
||||
if (texture == null || !texture.IsLoaded)
|
||||
{
|
||||
layout.Label("Loading...", TextAlignment.Center);
|
||||
return;
|
||||
}
|
||||
|
||||
// Texture info
|
||||
var general = layout.Group("General");
|
||||
general.Label("Format: " + texture.Format);
|
||||
general.Label(string.Format("Size: {0}x{1}", texture.Width, texture.Height));
|
||||
general.Label("Mip levels: " + texture.MipLevels);
|
||||
general.Label("Memory usage: " + Utilities.Utils.FormatBytesCount(texture.TotalMemoryUsage));
|
||||
|
||||
// Texture properties
|
||||
var properties = layout.Group("Properties");
|
||||
var textureGroup = new CustomValueContainer(new ScriptType(typeof(int)), texture.TextureGroup,
|
||||
(instance, index) => texture.TextureGroup,
|
||||
(instance, index, value) =>
|
||||
{
|
||||
texture.TextureGroup = (int)value;
|
||||
window.MarkAsEdited();
|
||||
});
|
||||
properties.Property("Texture Group", textureGroup, new TextureGroupEditor(), "The texture group used by this texture.");
|
||||
|
||||
// Import settings
|
||||
base.Initialize(layout);
|
||||
|
||||
// Reimport
|
||||
layout.Space(10);
|
||||
var reimportButton = layout.Button("Reimport");
|
||||
reimportButton.Button.Clicked += () => ((PropertiesProxy)Values[0]).Reimport();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The texture properties proxy object.
|
||||
/// </summary>
|
||||
[CustomEditor(typeof(ProxyEditor))]
|
||||
private sealed class PropertiesProxy
|
||||
{
|
||||
private TextureWindow _window;
|
||||
internal TextureWindow _window;
|
||||
|
||||
[EditorOrder(1000), EditorDisplay("Import Settings", EditorDisplayAttribute.InlineStyle)]
|
||||
public TextureImportSettings ImportSettings = new TextureImportSettings();
|
||||
|
||||
public sealed class ProxyEditor : GenericEditor
|
||||
{
|
||||
public override void Initialize(LayoutElementsContainer layout)
|
||||
{
|
||||
var window = ((PropertiesProxy)Values[0])._window;
|
||||
if (window == null)
|
||||
{
|
||||
layout.Label("Loading...", TextAlignment.Center);
|
||||
return;
|
||||
}
|
||||
|
||||
// Texture properties
|
||||
{
|
||||
var texture = window.Asset;
|
||||
|
||||
var group = layout.Group("General");
|
||||
group.Label("Format: " + texture.Format);
|
||||
group.Label(string.Format("Size: {0}x{1}", texture.Width, texture.Height));
|
||||
group.Label("Mip levels: " + texture.MipLevels);
|
||||
group.Label("Memory usage: " + Utilities.Utils.FormatBytesCount(texture.TotalMemoryUsage));
|
||||
}
|
||||
|
||||
base.Initialize(layout);
|
||||
|
||||
layout.Space(10);
|
||||
var reimportButton = layout.Button("Reimport");
|
||||
reimportButton.Button.Clicked += () => ((PropertiesProxy)Values[0]).Reimport();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gathers parameters from the specified texture.
|
||||
/// </summary>
|
||||
@@ -110,7 +123,7 @@ namespace FlaxEditor.Windows.Assets
|
||||
private readonly SplitPanel _split;
|
||||
private readonly TexturePreview _preview;
|
||||
private readonly CustomEditorPresenter _propertiesEditor;
|
||||
|
||||
private readonly ToolStripButton _saveButton;
|
||||
private readonly PropertiesProxy _properties;
|
||||
private bool _isWaitingForLoad;
|
||||
|
||||
@@ -140,6 +153,7 @@ namespace FlaxEditor.Windows.Assets
|
||||
_propertiesEditor.Select(_properties);
|
||||
|
||||
// Toolstrip
|
||||
_saveButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Save64, Save).LinkTooltip("Save");
|
||||
_toolstrip.AddButton(Editor.Icons.Import64, () => Editor.ContentImporting.Reimport((BinaryAssetItem)Item)).LinkTooltip("Reimport");
|
||||
_toolstrip.AddSeparator();
|
||||
_toolstrip.AddButton(Editor.Icons.CenterView64, _preview.CenterView).LinkTooltip("Center view");
|
||||
@@ -173,6 +187,14 @@ namespace FlaxEditor.Windows.Assets
|
||||
_isWaitingForLoad = true;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void UpdateToolstrip()
|
||||
{
|
||||
_saveButton.Enabled = IsEdited;
|
||||
|
||||
base.UpdateToolstrip();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void OnClose()
|
||||
{
|
||||
@@ -182,6 +204,21 @@ namespace FlaxEditor.Windows.Assets
|
||||
base.OnClose();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Save()
|
||||
{
|
||||
if (!IsEdited)
|
||||
return;
|
||||
|
||||
if (Asset.Save())
|
||||
{
|
||||
Editor.LogError("Cannot save asset.");
|
||||
return;
|
||||
}
|
||||
|
||||
ClearEditedFlag();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Update(float deltaTime)
|
||||
{
|
||||
|
||||
@@ -401,7 +401,7 @@ Asset::LoadResult AudioClip::load()
|
||||
if (AudioHeader.Streamable)
|
||||
{
|
||||
// Do nothing because data streaming starts when any AudioSource requests the data
|
||||
startStreaming(false);
|
||||
StartStreaming(false);
|
||||
return LoadResult::Ok;
|
||||
}
|
||||
|
||||
@@ -450,7 +450,7 @@ Asset::LoadResult AudioClip::load()
|
||||
|
||||
void AudioClip::unload(bool isReloading)
|
||||
{
|
||||
stopStreaming();
|
||||
StopStreaming();
|
||||
StreamingQueue.Clear();
|
||||
if (Buffers.HasItems())
|
||||
{
|
||||
|
||||
@@ -111,6 +111,11 @@ Asset::Asset(const SpawnParams& params, const AssetInfo* info)
|
||||
{
|
||||
}
|
||||
|
||||
int32 Asset::GetReferencesCount() const
|
||||
{
|
||||
return (int32)Platform::AtomicRead(const_cast<int64 volatile*>(&_refCount));
|
||||
}
|
||||
|
||||
String Asset::ToString() const
|
||||
{
|
||||
return String::Format(TEXT("{0}, {1}, {2}"), GetTypeName(), GetID(), GetPath());
|
||||
@@ -435,6 +440,15 @@ void Asset::startLoading()
|
||||
_loadingTask->Start();
|
||||
}
|
||||
|
||||
void Asset::releaseStorage()
|
||||
{
|
||||
}
|
||||
|
||||
bool Asset::IsInternalType() const
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Asset::onLoad(LoadAssetTask* task)
|
||||
{
|
||||
// It may fail when task is cancelled and new one is created later (don't crash but just end with an error)
|
||||
|
||||
@@ -82,10 +82,7 @@ public:
|
||||
/// <summary>
|
||||
/// Gets asset's reference count. Asset will be automatically unloaded when this reaches zero.
|
||||
/// </summary>
|
||||
API_PROPERTY() int32 GetReferencesCount() const
|
||||
{
|
||||
return (int32)Platform::AtomicRead(const_cast<int64 volatile*>(&_refCount));
|
||||
}
|
||||
API_PROPERTY() int32 GetReferencesCount() const;
|
||||
|
||||
/// <summary>
|
||||
/// Adds reference to that asset.
|
||||
@@ -213,9 +210,7 @@ protected:
|
||||
/// <summary>
|
||||
/// Releases the storage file/container handle to prevent issues when renaming or moving the asset.
|
||||
/// </summary>
|
||||
virtual void releaseStorage()
|
||||
{
|
||||
}
|
||||
virtual void releaseStorage();
|
||||
|
||||
/// <summary>
|
||||
/// Loads asset
|
||||
@@ -231,10 +226,7 @@ protected:
|
||||
|
||||
protected:
|
||||
|
||||
virtual bool IsInternalType() const
|
||||
{
|
||||
return false;
|
||||
}
|
||||
virtual bool IsInternalType() const;
|
||||
|
||||
bool onLoad(LoadAssetTask* task);
|
||||
void onLoaded();
|
||||
|
||||
@@ -570,7 +570,7 @@ bool Model::Init(const Span<int32>& meshesCountPerLod)
|
||||
}
|
||||
|
||||
// Dispose previous data and disable streaming (will start data uploading tasks manually)
|
||||
stopStreaming();
|
||||
StopStreaming();
|
||||
|
||||
// Setup
|
||||
MaterialSlots.Resize(1);
|
||||
@@ -827,7 +827,7 @@ Asset::LoadResult Model::load()
|
||||
#endif
|
||||
|
||||
// Request resource streaming
|
||||
startStreaming(true);
|
||||
StartStreaming(true);
|
||||
|
||||
return LoadResult::Ok;
|
||||
}
|
||||
|
||||
@@ -670,7 +670,7 @@ bool SkinnedModel::Init(const Span<int32>& meshesCountPerLod)
|
||||
}
|
||||
|
||||
// Dispose previous data and disable streaming (will start data uploading tasks manually)
|
||||
stopStreaming();
|
||||
StopStreaming();
|
||||
|
||||
// Setup
|
||||
MaterialSlots.Resize(1);
|
||||
@@ -973,7 +973,7 @@ Asset::LoadResult SkinnedModel::load()
|
||||
}
|
||||
|
||||
// Request resource streaming
|
||||
startStreaming(true);
|
||||
StartStreaming(true);
|
||||
|
||||
return LoadResult::Ok;
|
||||
}
|
||||
|
||||
@@ -17,6 +17,16 @@ Texture::Texture(const SpawnParams& params, const AssetInfo* info)
|
||||
{
|
||||
}
|
||||
|
||||
TextureFormatType Texture::GetFormatType() const
|
||||
{
|
||||
return _texture.GetFormatType();
|
||||
}
|
||||
|
||||
bool Texture::IsNormalMap() const
|
||||
{
|
||||
return _texture.GetFormatType() == TextureFormatType::NormalMap;
|
||||
}
|
||||
|
||||
#if USE_EDITOR
|
||||
|
||||
bool Texture::Save(const StringView& path, const InitData* customData)
|
||||
|
||||
@@ -10,23 +10,16 @@
|
||||
API_CLASS(NoSpawn) class FLAXENGINE_API Texture : public TextureBase
|
||||
{
|
||||
DECLARE_BINARY_ASSET_HEADER(Texture, TexturesSerializedVersion);
|
||||
public:
|
||||
|
||||
/// <summary>
|
||||
/// Gets the texture format type.
|
||||
/// </summary>
|
||||
FORCE_INLINE TextureFormatType GetFormatType() const
|
||||
{
|
||||
return _texture.GetFormatType();
|
||||
}
|
||||
TextureFormatType GetFormatType() const;
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if texture is a normal map.
|
||||
/// </summary>
|
||||
API_PROPERTY() FORCE_INLINE bool IsNormalMap() const
|
||||
{
|
||||
return GetFormatType() == TextureFormatType::NormalMap;
|
||||
}
|
||||
API_PROPERTY() bool IsNormalMap() const;
|
||||
|
||||
public:
|
||||
|
||||
|
||||
@@ -117,7 +117,7 @@ bool StreamingTexture::Create(const TextureHeader& header)
|
||||
#else
|
||||
bool isDynamic = false;
|
||||
#endif
|
||||
startStreaming(isDynamic);
|
||||
StartStreaming(isDynamic);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
/// </summary>
|
||||
class FLAXENGINE_API StreamingTexture : public Object, public StreamableResource
|
||||
{
|
||||
friend class TextureBase;
|
||||
friend class StreamTextureMipTask;
|
||||
friend class StreamTextureResizeTask;
|
||||
protected:
|
||||
@@ -60,7 +61,6 @@ public:
|
||||
/// <summary>
|
||||
/// Gets total texture width (in texels)
|
||||
/// </summary>
|
||||
/// <returns>Texture width</returns>
|
||||
FORCE_INLINE int32 TotalWidth() const
|
||||
{
|
||||
return _header.Width;
|
||||
|
||||
@@ -97,6 +97,20 @@ uint64 TextureBase::GetTotalMemoryUsage() const
|
||||
return _texture.GetTotalMemoryUsage();
|
||||
}
|
||||
|
||||
int32 TextureBase::GetTextureGroup() const
|
||||
{
|
||||
return _texture._header.TextureGroup;
|
||||
}
|
||||
|
||||
void TextureBase::SetTextureGroup(int32 textureGroup)
|
||||
{
|
||||
if (_texture._header.TextureGroup != textureGroup)
|
||||
{
|
||||
_texture._header.TextureGroup = textureGroup;
|
||||
_texture.RequestStreamingUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
BytesContainer TextureBase::GetMipData(int32 mipIndex, int32& rowPitch, int32& slicePitch)
|
||||
{
|
||||
BytesContainer result;
|
||||
@@ -114,7 +128,6 @@ BytesContainer TextureBase::GetMipData(int32 mipIndex, int32& rowPitch, int32& s
|
||||
}
|
||||
else
|
||||
{
|
||||
// Wait for the asset header to be loaded
|
||||
if (WaitForLoaded())
|
||||
return result;
|
||||
|
||||
@@ -127,7 +140,7 @@ BytesContainer TextureBase::GetMipData(int32 mipIndex, int32& rowPitch, int32& s
|
||||
slicePitch = slicePitch1;
|
||||
|
||||
// Ensure to have chunk loaded
|
||||
if (LoadChunk(calculateChunkIndex(mipIndex)))
|
||||
if (LoadChunk(CalculateChunkIndex(mipIndex)))
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -261,7 +274,7 @@ bool TextureBase::Init(void* ptr)
|
||||
return Init(initData);
|
||||
}
|
||||
|
||||
int32 TextureBase::calculateChunkIndex(int32 mipIndex) const
|
||||
int32 TextureBase::CalculateChunkIndex(int32 mipIndex) const
|
||||
{
|
||||
// Mips are in 0-13 chunks
|
||||
return mipIndex;
|
||||
@@ -287,7 +300,7 @@ Task* TextureBase::RequestMipDataAsync(int32 mipIndex)
|
||||
if (_customData)
|
||||
return nullptr;
|
||||
|
||||
auto chunkIndex = calculateChunkIndex(mipIndex);
|
||||
auto chunkIndex = CalculateChunkIndex(mipIndex);
|
||||
return (Task*)_parent->RequestChunkDataAsync(chunkIndex);
|
||||
}
|
||||
|
||||
@@ -304,7 +317,7 @@ void TextureBase::GetMipData(int32 mipIndex, BytesContainer& data) const
|
||||
return;
|
||||
}
|
||||
|
||||
auto chunkIndex = calculateChunkIndex(mipIndex);
|
||||
auto chunkIndex = CalculateChunkIndex(mipIndex);
|
||||
_parent->GetChunkData(chunkIndex, data);
|
||||
}
|
||||
|
||||
@@ -316,7 +329,7 @@ void TextureBase::GetMipDataWithLoading(int32 mipIndex, BytesContainer& data) co
|
||||
return;
|
||||
}
|
||||
|
||||
const auto chunkIndex = calculateChunkIndex(mipIndex);
|
||||
const auto chunkIndex = CalculateChunkIndex(mipIndex);
|
||||
_parent->LoadChunk(chunkIndex);
|
||||
_parent->GetChunkData(chunkIndex, data);
|
||||
}
|
||||
@@ -399,8 +412,6 @@ bool TextureBase::InitData::GenerateMip(int32 mipIndex, bool linear)
|
||||
// Allocate data
|
||||
const int32 dstMipWidth = Math::Max(1, Width >> mipIndex);
|
||||
const int32 dstMipHeight = Math::Max(1, Height >> mipIndex);
|
||||
const int32 srcMipWidth = Math::Max(1, Width >> (mipIndex - 1));
|
||||
const int32 srcMipHeight = Math::Max(1, Height >> (mipIndex - 1));
|
||||
const int32 pixelStride = PixelFormatExtensions::SizeInBytes(Format);
|
||||
dstMip.RowPitch = dstMipWidth * pixelStride;
|
||||
dstMip.SlicePitch = dstMip.RowPitch * dstMipHeight;
|
||||
|
||||
@@ -50,9 +50,6 @@ protected:
|
||||
|
||||
StreamingTexture _texture;
|
||||
InitData* _customData;
|
||||
|
||||
private:
|
||||
|
||||
BinaryAsset* _parent;
|
||||
|
||||
public:
|
||||
@@ -126,6 +123,16 @@ public:
|
||||
/// Gets the total memory usage that texture may have in use (if loaded to the maximum quality). Exact value may differ due to memory alignment and resource allocation policy.
|
||||
/// </summary>
|
||||
API_PROPERTY() uint64 GetTotalMemoryUsage() const;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the index of the texture group used by this texture.
|
||||
/// </summary>
|
||||
API_PROPERTY() int32 GetTextureGroup() const;
|
||||
|
||||
/// <summary>
|
||||
/// Sets the index of the texture group used by this texture.
|
||||
/// </summary>
|
||||
API_PROPERTY() void SetTextureGroup(int32 textureGroup);
|
||||
|
||||
public:
|
||||
|
||||
@@ -155,7 +162,7 @@ public:
|
||||
|
||||
protected:
|
||||
|
||||
virtual int32 calculateChunkIndex(int32 mipIndex) const;
|
||||
virtual int32 CalculateChunkIndex(int32 mipIndex) const;
|
||||
|
||||
private:
|
||||
|
||||
|
||||
@@ -14,10 +14,10 @@ StreamableResource::StreamableResource(StreamingGroup* group)
|
||||
|
||||
StreamableResource::~StreamableResource()
|
||||
{
|
||||
stopStreaming();
|
||||
StopStreaming();
|
||||
}
|
||||
|
||||
void StreamableResource::startStreaming(bool isDynamic)
|
||||
void StreamableResource::StartStreaming(bool isDynamic)
|
||||
{
|
||||
_isDynamic = isDynamic;
|
||||
|
||||
@@ -28,7 +28,7 @@ void StreamableResource::startStreaming(bool isDynamic)
|
||||
}
|
||||
}
|
||||
|
||||
void StreamableResource::stopStreaming()
|
||||
void StreamableResource::StopStreaming()
|
||||
{
|
||||
if (_isStreaming == true)
|
||||
{
|
||||
|
||||
@@ -105,7 +105,6 @@ public:
|
||||
|
||||
struct StreamingCache
|
||||
{
|
||||
//float MinDstSinceLastUpdate = MAX_float;
|
||||
DateTime LastUpdate = 0;
|
||||
int32 TargetResidency = 0;
|
||||
DateTime TargetResidencyChange = 0;
|
||||
@@ -124,6 +123,6 @@ public:
|
||||
|
||||
protected:
|
||||
|
||||
void startStreaming(bool isDynamic);
|
||||
void stopStreaming();
|
||||
void StartStreaming(bool isDynamic);
|
||||
void StopStreaming();
|
||||
};
|
||||
|
||||
@@ -59,7 +59,6 @@ void UpdateResource(StreamableResource* resource, DateTime now)
|
||||
if (resource->IsDynamic())
|
||||
{
|
||||
targetQuality = handler->CalculateTargetQuality(resource, now);
|
||||
// TODO: here we should apply resources group master scale (based on game settings quality level and memory level)
|
||||
targetQuality = Math::Saturate(targetQuality);
|
||||
}
|
||||
|
||||
|
||||
@@ -14,9 +14,6 @@ class FLAXENGINE_API StreamingGroup
|
||||
{
|
||||
public:
|
||||
|
||||
/// <summary>
|
||||
/// Declares the Group type
|
||||
/// </summary>
|
||||
DECLARE_ENUM_4(Type, Custom, Textures, Models, Audio);
|
||||
|
||||
protected:
|
||||
@@ -38,7 +35,6 @@ public:
|
||||
/// <summary>
|
||||
/// Gets the group type.
|
||||
/// </summary>
|
||||
/// <returns>Type</returns>
|
||||
FORCE_INLINE Type GetType() const
|
||||
{
|
||||
return _type;
|
||||
@@ -47,7 +43,6 @@ public:
|
||||
/// <summary>
|
||||
/// Gets the group type name.
|
||||
/// </summary>
|
||||
/// <returns>Typename</returns>
|
||||
FORCE_INLINE const Char* GetTypename() const
|
||||
{
|
||||
return ToString(_type);
|
||||
@@ -56,7 +51,6 @@ public:
|
||||
/// <summary>
|
||||
/// Gets the group streaming handler used by this group.
|
||||
/// </summary>
|
||||
/// <returns>Handler</returns>
|
||||
FORCE_INLINE IStreamingHandler* GetHandler() const
|
||||
{
|
||||
return _handler;
|
||||
@@ -88,7 +82,6 @@ public:
|
||||
/// <summary>
|
||||
/// Gets textures group.
|
||||
/// </summary>
|
||||
/// <returns>Group</returns>
|
||||
FORCE_INLINE StreamingGroup* Textures() const
|
||||
{
|
||||
return _textures;
|
||||
@@ -97,7 +90,6 @@ public:
|
||||
/// <summary>
|
||||
/// Gets models group.
|
||||
/// </summary>
|
||||
/// <returns>Group</returns>
|
||||
FORCE_INLINE StreamingGroup* Models() const
|
||||
{
|
||||
return _models;
|
||||
@@ -106,7 +98,6 @@ public:
|
||||
/// <summary>
|
||||
/// Gets skinned models group.
|
||||
/// </summary>
|
||||
/// <returns>Group</returns>
|
||||
FORCE_INLINE StreamingGroup* SkinnedModels() const
|
||||
{
|
||||
return _skinnedModels;
|
||||
@@ -115,7 +106,6 @@ public:
|
||||
/// <summary>
|
||||
/// Gets audio group.
|
||||
/// </summary>
|
||||
/// <returns>Group</returns>
|
||||
FORCE_INLINE StreamingGroup* Audio() const
|
||||
{
|
||||
return _audio;
|
||||
@@ -126,7 +116,6 @@ public:
|
||||
/// <summary>
|
||||
/// Gets all the groups.
|
||||
/// </summary>
|
||||
/// <returns>Groups.</returns>
|
||||
FORCE_INLINE const Array<StreamingGroup*>& Groups() const
|
||||
{
|
||||
return _groups;
|
||||
@@ -135,7 +124,6 @@ public:
|
||||
/// <summary>
|
||||
/// Gets all the handlers.
|
||||
/// </summary>
|
||||
/// <returns>Groups.</returns>
|
||||
FORCE_INLINE const Array<IStreamingHandler*>& Handlers() const
|
||||
{
|
||||
return _handlers;
|
||||
|
||||
Reference in New Issue
Block a user