mirror of
https://github.com/microsoft/PowerToys.git
synced 2025-12-16 11:48:06 +01:00
shortcut icons
This commit is contained in:
@@ -271,27 +271,5 @@ namespace ProjectsEditor.Models
|
|||||||
|
|
||||||
return new Rectangle((int)minX, (int)minY, (int)(maxX - minX), (int)(maxY - minY));
|
return new Rectangle((int)minX, (int)minY, (int)(maxX - minX), (int)(maxY - minY));
|
||||||
}
|
}
|
||||||
|
|
||||||
internal string GetShortcutChars()
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(Name))
|
|
||||||
{
|
|
||||||
return "PR";
|
|
||||||
}
|
|
||||||
|
|
||||||
string[] words = Name.Trim().ToUpperInvariant().Split(' ');
|
|
||||||
if (words.Length > 2)
|
|
||||||
{
|
|
||||||
return $"{words[0][0]}{words[1][0]}{words[2][0]}";
|
|
||||||
}
|
|
||||||
else if (words.Length == 2)
|
|
||||||
{
|
|
||||||
return $"{words[0][0]}{words[1][0]}";
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return words[0].Substring(0, Math.Min(3, words[0].Length));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,34 +19,7 @@ namespace ProjectsEditor.Utils
|
|||||||
{
|
{
|
||||||
public class DrawHelper
|
public class DrawHelper
|
||||||
{
|
{
|
||||||
private const int IconSize = 128;
|
|
||||||
private static Font font = new("Tahoma", 24);
|
private static Font font = new("Tahoma", 24);
|
||||||
private static List<Brush> iconBrushes = new List<Brush>
|
|
||||||
{
|
|
||||||
////Brushes.Gold,
|
|
||||||
////Brushes.SteelBlue,
|
|
||||||
////Brushes.SkyBlue,
|
|
||||||
////Brushes.DarkGoldenrod,
|
|
||||||
////Brushes.ForestGreen,
|
|
||||||
////Brushes.Peru,
|
|
||||||
////Brushes.Chartreuse,
|
|
||||||
////Brushes.LightPink,
|
|
||||||
////Brushes.CadetBlue,
|
|
||||||
////Brushes.DarkSalmon,
|
|
||||||
////Brushes.Orange,
|
|
||||||
////Brushes.DarkSeaGreen,
|
|
||||||
////Brushes.Yellow,
|
|
||||||
////Brushes.Green,
|
|
||||||
////Brushes.Orange,
|
|
||||||
////Brushes.White,
|
|
||||||
new SolidBrush(Color.FromArgb(255, 40, 101, 120)),
|
|
||||||
new SolidBrush(Color.FromArgb(255, 58, 91, 153)),
|
|
||||||
new SolidBrush(Color.FromArgb(255, 87, 88, 163)),
|
|
||||||
new SolidBrush(Color.FromArgb(255, 116, 87, 160)),
|
|
||||||
new SolidBrush(Color.FromArgb(255, 139, 82, 145)),
|
|
||||||
};
|
|
||||||
|
|
||||||
private static int iconBrushIndex;
|
|
||||||
|
|
||||||
public static BitmapImage DrawPreview(Project project, Rectangle bounds)
|
public static BitmapImage DrawPreview(Project project, Rectangle bounds)
|
||||||
{
|
{
|
||||||
@@ -239,164 +212,7 @@ namespace ProjectsEditor.Utils
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static GraphicsPath RoundedRect(Rectangle bounds)
|
public static BitmapImage DrawPreviewIcons(Project project)
|
||||||
{
|
|
||||||
int minorSize = Math.Min(bounds.Width, bounds.Height);
|
|
||||||
int radius = (int)(minorSize / 8);
|
|
||||||
|
|
||||||
int diameter = radius * 2;
|
|
||||||
Size size = new Size(diameter, diameter);
|
|
||||||
Rectangle arc = new Rectangle(bounds.Location, size);
|
|
||||||
GraphicsPath path = new GraphicsPath();
|
|
||||||
|
|
||||||
if (radius == 0)
|
|
||||||
{
|
|
||||||
path.AddRectangle(bounds);
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
|
|
||||||
// top left arc
|
|
||||||
path.AddArc(arc, 180, 90);
|
|
||||||
|
|
||||||
// top right arc
|
|
||||||
arc.X = bounds.Right - diameter;
|
|
||||||
path.AddArc(arc, 270, 90);
|
|
||||||
|
|
||||||
// bottom right arc
|
|
||||||
arc.Y = bounds.Bottom - diameter;
|
|
||||||
path.AddArc(arc, 0, 90);
|
|
||||||
|
|
||||||
// bottom left arc
|
|
||||||
arc.X = bounds.Left;
|
|
||||||
path.AddArc(arc, 90, 90);
|
|
||||||
|
|
||||||
path.CloseFigure();
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static string CreateShortcutIcon(Project project, out Bitmap bitmap)
|
|
||||||
{
|
|
||||||
object shDesktop = (object)"Desktop";
|
|
||||||
IWshRuntimeLibrary.WshShell shell = new IWshRuntimeLibrary.WshShell();
|
|
||||||
string shortcutIconFilename = (string)shell.SpecialFolders.Item(ref shDesktop) + $"\\{project.Name}.ico";
|
|
||||||
bitmap = new Bitmap(IconSize, IconSize);
|
|
||||||
using (Graphics graphics = Graphics.FromImage(bitmap))
|
|
||||||
{
|
|
||||||
graphics.SmoothingMode = SmoothingMode.AntiAlias;
|
|
||||||
graphics.InterpolationMode = InterpolationMode.HighQualityBicubic;
|
|
||||||
graphics.PixelOffsetMode = PixelOffsetMode.HighQuality;
|
|
||||||
|
|
||||||
// if (project != null)
|
|
||||||
// {
|
|
||||||
// List<Application> selectedApps = project.Applications.Where(x => x.IsSelected).ToList();
|
|
||||||
// if (selectedApps.Count > 0)
|
|
||||||
// {
|
|
||||||
// graphics.DrawIcon(selectedApps[0].Icon, new Rectangle(0, 0, IconSize / 2, IconSize / 2));
|
|
||||||
// }
|
|
||||||
// if (selectedApps.Count > 1)
|
|
||||||
// {
|
|
||||||
// graphics.DrawIcon(selectedApps[1].Icon, new Rectangle(IconSize / 2, 0, IconSize / 2, IconSize / 2));
|
|
||||||
// }
|
|
||||||
// if (selectedApps.Count > 2)
|
|
||||||
// {
|
|
||||||
// graphics.DrawIcon(selectedApps[2].Icon, new Rectangle(0, IconSize / 2, IconSize / 2, IconSize / 2));
|
|
||||||
// }
|
|
||||||
// if (selectedApps.Count > 3)
|
|
||||||
// {
|
|
||||||
// graphics.DrawIcon(selectedApps[3].Icon, new Rectangle(IconSize / 2, IconSize / 2, IconSize / 2, IconSize / 2));
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// graphics.FillRectangle(new System.Drawing.SolidBrush(Color.FromArgb(128, 32, 32, 32)), 0, 0, IconSize, IconSize);
|
|
||||||
graphics.FillEllipse(iconBrushes[iconBrushIndex], 0, 0, IconSize, IconSize);
|
|
||||||
|
|
||||||
string shortcutChars = "PR";
|
|
||||||
|
|
||||||
if (project != null)
|
|
||||||
{
|
|
||||||
shortcutChars = project.GetShortcutChars();
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle indexBounds;
|
|
||||||
if (shortcutChars.Length > 1)
|
|
||||||
{
|
|
||||||
indexBounds = new Rectangle(0, 0, IconSize, IconSize);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
indexBounds = new Rectangle(IconSize / 4, 0, IconSize / 2, IconSize);
|
|
||||||
}
|
|
||||||
|
|
||||||
var textSize = graphics.MeasureString(shortcutChars, font);
|
|
||||||
var state = graphics.Save();
|
|
||||||
graphics.TranslateTransform(indexBounds.Left, indexBounds.Top);
|
|
||||||
graphics.ScaleTransform(indexBounds.Width / textSize.Width, indexBounds.Height / textSize.Height);
|
|
||||||
graphics.DrawString(shortcutChars, font, Brushes.White, PointF.Empty);
|
|
||||||
graphics.Restore(state);
|
|
||||||
iconBrushIndex++;
|
|
||||||
if (iconBrushIndex >= iconBrushes.Count)
|
|
||||||
{
|
|
||||||
iconBrushIndex = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
FileStream fileStream = new FileStream(shortcutIconFilename, FileMode.OpenOrCreate);
|
|
||||||
|
|
||||||
using (var memoryStream = new MemoryStream())
|
|
||||||
{
|
|
||||||
bitmap.Save(memoryStream, ImageFormat.Png);
|
|
||||||
|
|
||||||
BinaryWriter iconWriter = new BinaryWriter(fileStream);
|
|
||||||
if (fileStream != null && iconWriter != null)
|
|
||||||
{
|
|
||||||
// 0-1 reserved, 0
|
|
||||||
iconWriter.Write((byte)0);
|
|
||||||
iconWriter.Write((byte)0);
|
|
||||||
|
|
||||||
// 2-3 image type, 1 = icon, 2 = cursor
|
|
||||||
iconWriter.Write((short)1);
|
|
||||||
|
|
||||||
// 4-5 number of images
|
|
||||||
iconWriter.Write((short)1);
|
|
||||||
|
|
||||||
// image entry 1
|
|
||||||
// 0 image width
|
|
||||||
iconWriter.Write((byte)IconSize);
|
|
||||||
|
|
||||||
// 1 image height
|
|
||||||
iconWriter.Write((byte)IconSize);
|
|
||||||
|
|
||||||
// 2 number of colors
|
|
||||||
iconWriter.Write((byte)0);
|
|
||||||
|
|
||||||
// 3 reserved
|
|
||||||
iconWriter.Write((byte)0);
|
|
||||||
|
|
||||||
// 4-5 color planes
|
|
||||||
iconWriter.Write((short)0);
|
|
||||||
|
|
||||||
// 6-7 bits per pixel
|
|
||||||
iconWriter.Write((short)32);
|
|
||||||
|
|
||||||
// 8-11 size of image data
|
|
||||||
iconWriter.Write((int)memoryStream.Length);
|
|
||||||
|
|
||||||
// 12-15 offset of image data
|
|
||||||
iconWriter.Write((int)(6 + 16));
|
|
||||||
|
|
||||||
// write image data
|
|
||||||
// png data must contain the whole png data file
|
|
||||||
iconWriter.Write(memoryStream.ToArray());
|
|
||||||
|
|
||||||
iconWriter.Flush();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fileStream.Flush();
|
|
||||||
fileStream.Close();
|
|
||||||
return shortcutIconFilename;
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static BitmapImage DrawPreviewIcons(Project project)
|
|
||||||
{
|
{
|
||||||
var selectedApps = project.Applications.Where(x => x.IsSelected);
|
var selectedApps = project.Applications.Where(x => x.IsSelected);
|
||||||
int appsCount = selectedApps.Count();
|
int appsCount = selectedApps.Count();
|
||||||
@@ -443,46 +259,39 @@ namespace ProjectsEditor.Utils
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void CreateExamples(Project project)
|
private static GraphicsPath RoundedRect(Rectangle bounds)
|
||||||
{
|
{
|
||||||
Bitmap bitmap = new Bitmap(IconSize + 1000, IconSize * iconBrushes.Count);
|
int minorSize = Math.Min(bounds.Width, bounds.Height);
|
||||||
using (Graphics graphics = Graphics.FromImage(bitmap))
|
int radius = (int)(minorSize / 8);
|
||||||
|
|
||||||
|
int diameter = radius * 2;
|
||||||
|
Size size = new Size(diameter, diameter);
|
||||||
|
Rectangle arc = new Rectangle(bounds.Location, size);
|
||||||
|
GraphicsPath path = new GraphicsPath();
|
||||||
|
|
||||||
|
if (radius == 0)
|
||||||
{
|
{
|
||||||
for (int brushIndex = 0; brushIndex < iconBrushes.Count; brushIndex++)
|
path.AddRectangle(bounds);
|
||||||
{
|
return path;
|
||||||
graphics.SmoothingMode = SmoothingMode.AntiAlias;
|
|
||||||
graphics.InterpolationMode = InterpolationMode.HighQualityBicubic;
|
|
||||||
graphics.PixelOffsetMode = PixelOffsetMode.HighQuality;
|
|
||||||
|
|
||||||
graphics.FillEllipse(iconBrushes[brushIndex], 0, IconSize * brushIndex, IconSize, IconSize);
|
|
||||||
|
|
||||||
string shortcutChars = "PR";
|
|
||||||
|
|
||||||
Rectangle indexBounds;
|
|
||||||
indexBounds = new Rectangle(0, IconSize * brushIndex, IconSize, IconSize);
|
|
||||||
|
|
||||||
var textSize = graphics.MeasureString(shortcutChars, font);
|
|
||||||
var state = graphics.Save();
|
|
||||||
graphics.TranslateTransform(indexBounds.Left, indexBounds.Top);
|
|
||||||
graphics.ScaleTransform(indexBounds.Width / textSize.Width, indexBounds.Height / textSize.Height);
|
|
||||||
graphics.DrawString(shortcutChars, font, Brushes.Black, 0, 0);
|
|
||||||
graphics.Restore(state);
|
|
||||||
|
|
||||||
var b = (SolidBrush)iconBrushes[brushIndex];
|
|
||||||
var colorName = (from p in typeof(System.Drawing.Color).GetProperties()
|
|
||||||
where p.PropertyType.Equals(typeof(System.Drawing.Color))
|
|
||||||
let value = (System.Drawing.Color)p.GetValue(null, null)
|
|
||||||
where value.R == b.Color.R &&
|
|
||||||
value.G == b.Color.G &&
|
|
||||||
value.B == b.Color.B &&
|
|
||||||
value.A == b.Color.A
|
|
||||||
select p.Name).DefaultIfEmpty("unknown").First();
|
|
||||||
|
|
||||||
graphics.DrawString(colorName, font, Brushes.White, IconSize, IconSize * brushIndex);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bitmap.Save(@"C:\temp\shortcutIcons.png");
|
// top left arc
|
||||||
|
path.AddArc(arc, 180, 90);
|
||||||
|
|
||||||
|
// top right arc
|
||||||
|
arc.X = bounds.Right - diameter;
|
||||||
|
path.AddArc(arc, 270, 90);
|
||||||
|
|
||||||
|
// bottom right arc
|
||||||
|
arc.Y = bounds.Bottom - diameter;
|
||||||
|
path.AddArc(arc, 0, 90);
|
||||||
|
|
||||||
|
// bottom left arc
|
||||||
|
arc.X = bounds.Left;
|
||||||
|
path.AddArc(arc, 90, 90);
|
||||||
|
|
||||||
|
path.CloseFigure();
|
||||||
|
return path;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
147
src/modules/Projects/ProjectsEditor/Utils/ProjectIcon.cs
Normal file
147
src/modules/Projects/ProjectsEditor/Utils/ProjectIcon.cs
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
// Copyright (c) Microsoft Corporation
|
||||||
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Drawing;
|
||||||
|
using System.Drawing.Drawing2D;
|
||||||
|
using System.Drawing.Imaging;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace ProjectsEditor.Utils
|
||||||
|
{
|
||||||
|
public class ProjectIcon : IDisposable
|
||||||
|
{
|
||||||
|
private const int IconSize = 128;
|
||||||
|
|
||||||
|
public static readonly Brush LightThemeIconBackground = new SolidBrush(Color.FromArgb(255, 239, 243, 251));
|
||||||
|
public static readonly Brush LightThemeIconForeground = new SolidBrush(Color.FromArgb(255, 47, 50, 56));
|
||||||
|
public static readonly Brush DarkThemeIconBackground = new SolidBrush(Color.FromArgb(255, 55, 55, 55));
|
||||||
|
public static readonly Brush DarkThemeIconForeground = new SolidBrush(Color.FromArgb(255, 228, 228, 228));
|
||||||
|
|
||||||
|
public static readonly Font IconFont = new("Aptos", 24, FontStyle.Bold);
|
||||||
|
|
||||||
|
public static string IconTextFromProjectName(string projectName)
|
||||||
|
{
|
||||||
|
string result = string.Empty;
|
||||||
|
char[] delimiterChars = { ' ', ',', '.', ':', '-', '\t' };
|
||||||
|
string[] words = projectName.Split(delimiterChars);
|
||||||
|
|
||||||
|
foreach (string word in words)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(word))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (word.All(char.IsDigit))
|
||||||
|
{
|
||||||
|
result += word;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
result += word.ToUpper(System.Globalization.CultureInfo.CurrentCulture).ToCharArray()[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Bitmap DrawIcon(string text)
|
||||||
|
{
|
||||||
|
Brush background = Common.ThemeManager.GetCurrentTheme() == Common.Theme.Dark ? DarkThemeIconBackground : LightThemeIconBackground;
|
||||||
|
Brush foreground = Common.ThemeManager.GetCurrentTheme() == Common.Theme.Dark ? DarkThemeIconForeground : LightThemeIconForeground;
|
||||||
|
Bitmap bitmap = new Bitmap(IconSize, IconSize);
|
||||||
|
|
||||||
|
using (Graphics graphics = Graphics.FromImage(bitmap))
|
||||||
|
{
|
||||||
|
graphics.SmoothingMode = SmoothingMode.AntiAlias;
|
||||||
|
graphics.InterpolationMode = InterpolationMode.HighQualityBicubic;
|
||||||
|
graphics.PixelOffsetMode = PixelOffsetMode.HighQuality;
|
||||||
|
graphics.FillEllipse(background, 0, 0, IconSize, IconSize);
|
||||||
|
|
||||||
|
var textSize = graphics.MeasureString(text, IconFont);
|
||||||
|
var state = graphics.Save();
|
||||||
|
|
||||||
|
// Calculate scaling factors
|
||||||
|
float scaleX = (float)IconSize / textSize.Width;
|
||||||
|
float scaleY = (float)IconSize / textSize.Height;
|
||||||
|
float scale = Math.Min(scaleX, scaleY) * 0.8f; // Use the smaller scale factor to maintain aspect ratio
|
||||||
|
|
||||||
|
// Calculate the position to center the text
|
||||||
|
float textX = (IconSize - (textSize.Width * scale)) / 2;
|
||||||
|
float textY = ((IconSize - (textSize.Height * scale)) / 2) + 6;
|
||||||
|
|
||||||
|
graphics.TranslateTransform(textX, textY);
|
||||||
|
graphics.ScaleTransform(scale, scale);
|
||||||
|
graphics.DrawString(text, IconFont, foreground, 0, 0);
|
||||||
|
graphics.Restore(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
return bitmap;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void SaveIcon(Bitmap icon, string path)
|
||||||
|
{
|
||||||
|
FileStream fileStream = new FileStream(path, FileMode.OpenOrCreate);
|
||||||
|
using (var memoryStream = new MemoryStream())
|
||||||
|
{
|
||||||
|
icon.Save(memoryStream, ImageFormat.Png);
|
||||||
|
|
||||||
|
BinaryWriter iconWriter = new BinaryWriter(fileStream);
|
||||||
|
if (fileStream != null && iconWriter != null)
|
||||||
|
{
|
||||||
|
// 0-1 reserved, 0
|
||||||
|
iconWriter.Write((byte)0);
|
||||||
|
iconWriter.Write((byte)0);
|
||||||
|
|
||||||
|
// 2-3 image type, 1 = icon, 2 = cursor
|
||||||
|
iconWriter.Write((short)1);
|
||||||
|
|
||||||
|
// 4-5 number of images
|
||||||
|
iconWriter.Write((short)1);
|
||||||
|
|
||||||
|
// image entry 1
|
||||||
|
// 0 image width
|
||||||
|
iconWriter.Write((byte)IconSize);
|
||||||
|
|
||||||
|
// 1 image height
|
||||||
|
iconWriter.Write((byte)IconSize);
|
||||||
|
|
||||||
|
// 2 number of colors
|
||||||
|
iconWriter.Write((byte)0);
|
||||||
|
|
||||||
|
// 3 reserved
|
||||||
|
iconWriter.Write((byte)0);
|
||||||
|
|
||||||
|
// 4-5 color planes
|
||||||
|
iconWriter.Write((short)0);
|
||||||
|
|
||||||
|
// 6-7 bits per pixel
|
||||||
|
iconWriter.Write((short)32);
|
||||||
|
|
||||||
|
// 8-11 size of image data
|
||||||
|
iconWriter.Write((int)memoryStream.Length);
|
||||||
|
|
||||||
|
// 12-15 offset of image data
|
||||||
|
iconWriter.Write((int)(6 + 16));
|
||||||
|
|
||||||
|
// write image data
|
||||||
|
// png data must contain the whole png data file
|
||||||
|
iconWriter.Write(memoryStream.ToArray());
|
||||||
|
|
||||||
|
iconWriter.Flush();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fileStream.Flush();
|
||||||
|
fileStream.Close();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -145,8 +145,12 @@ namespace ProjectsEditor.ViewModels
|
|||||||
shortcut.TargetPath = Path.Combine(basePath, "ProjectsLauncher.exe");
|
shortcut.TargetPath = Path.Combine(basePath, "ProjectsLauncher.exe");
|
||||||
shortcut.Arguments = '"' + project.Id + '"';
|
shortcut.Arguments = '"' + project.Id + '"';
|
||||||
shortcut.WorkingDirectory = basePath;
|
shortcut.WorkingDirectory = basePath;
|
||||||
string iconFilename = DrawHelper.CreateShortcutIcon(project, out Bitmap bitmap);
|
|
||||||
shortcut.IconLocation = iconFilename;
|
string shortcutIconFilename = (string)shell.SpecialFolders.Item(ref shDesktop) + $"\\{project.Name}.ico";
|
||||||
|
Bitmap icon = ProjectIcon.DrawIcon(ProjectIcon.IconTextFromProjectName(project.Name));
|
||||||
|
ProjectIcon.SaveIcon(icon, shortcutIconFilename);
|
||||||
|
|
||||||
|
shortcut.IconLocation = shortcutIconFilename;
|
||||||
shortcut.Save();
|
shortcut.Save();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user