Doc Texts
Fixed Version number
This commit is contained in:
parent
7fdc1a0ad4
commit
e13c5006c8
|
@ -1,9 +1,12 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using SweetLib.Classes.Storer;
|
||||
using SweetLib.Utils.Logger;
|
||||
using SweetLib.Utils.Logger.Message;
|
||||
|
||||
namespace SweetLib.Demo.Console
|
||||
{
|
||||
|
@ -21,6 +24,10 @@ namespace SweetLib.Demo.Console
|
|||
Logger.Error("Error :(");
|
||||
|
||||
System.Console.ReadLine();
|
||||
|
||||
var f = Path.GetTempFileName();
|
||||
var ini = new IniFileStorer(f);
|
||||
System.Console.WriteLine(ini.ReadString("sec","key"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -32,5 +32,5 @@ using System.Runtime.InteropServices;
|
|||
// Sie können alle Werte angeben oder die standardmäßigen Build- und Revisionsnummern
|
||||
// übernehmen, indem Sie "*" eingeben:
|
||||
// [assembly: AssemblyVersion("1.0.*")]
|
||||
[assembly: AssemblyVersion("1.0.19.0")]
|
||||
[assembly: AssemblyFileVersion("1.0.19.0")]
|
||||
[assembly: AssemblyVersion("1.0.21.0")]
|
||||
[assembly: AssemblyFileVersion("1.0.21.0")]
|
||||
|
|
|
@ -1,9 +1,21 @@
|
|||
namespace SweetLib.Classes.Exception
|
||||
using SweetLib.Classes.Storer;
|
||||
|
||||
namespace SweetLib.Classes.Exception
|
||||
{
|
||||
/// <summary>
|
||||
/// Exception thrown by <see cref="RegistryStorer"/>.
|
||||
/// </summary>
|
||||
public class RegistryStorerException : System.Exception
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="RegistryStorerException"/>.
|
||||
/// </summary>
|
||||
public RegistryStorerException(){}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="RegistryStorerException"/>.
|
||||
/// </summary>
|
||||
/// <param name="message">Exception message.</param>
|
||||
public RegistryStorerException(string message):base(message) {}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,23 +1,80 @@
|
|||
namespace SweetLib.Classes.Storer
|
||||
{
|
||||
/// <summary>
|
||||
/// Interface, which provides several methods to store simple data.
|
||||
/// </summary>
|
||||
public interface IStorer
|
||||
{
|
||||
/// <summary>
|
||||
/// Reads a string value.
|
||||
/// </summary>
|
||||
/// <param name="section">Represents the section in which the data is stored.</param>
|
||||
/// <param name="key">Represents the key of the stored data.</param>
|
||||
/// <param name="defaultValue">Default value, if this value does not exist.</param>
|
||||
/// <returns>Value of the <see cref="key"/> in <see cref="section"/>.</returns>
|
||||
string ReadString(string section, string key, string defaultValue = "");
|
||||
|
||||
/// <summary>
|
||||
/// Reads an integer value.
|
||||
/// </summary>
|
||||
/// <param name="section">Represents the section in which the data is stored.</param>
|
||||
/// <param name="key">Represents the key of the stored data.</param>
|
||||
/// <param name="defaultValue">Default value, if this value does not exist.</param>
|
||||
/// <returns>Value of the <see cref="key"/> in <see cref="section"/>.</returns>
|
||||
int ReadInteger(string section, string key, int defaultValue = 0);
|
||||
|
||||
/// <summary>
|
||||
/// Reads a bool value.
|
||||
/// </summary>
|
||||
/// <param name="section">Represents the section in which the data is stored.</param>
|
||||
/// <param name="key">Represents the key of the stored data.</param>
|
||||
/// <param name="defaultValue">Default value, if this value does not exist.</param>
|
||||
/// <returns>Value of the <see cref="key"/> in <see cref="section"/>.</returns>
|
||||
bool ReadBool(string section, string key, bool defaultValue = false);
|
||||
|
||||
/// <summary>
|
||||
/// Checks, if a key exists inside a section.
|
||||
/// </summary>
|
||||
/// <param name="section">Represents the section in which the data is stored.</param>
|
||||
/// <param name="key">Represents the key of the stored data.</param>
|
||||
/// <returns>True, if <see cref="key"/> is found inside <see cref="section"/>.</returns>
|
||||
bool HasKey(string section, string key);
|
||||
|
||||
/// <summary>
|
||||
/// Writes a string value.
|
||||
/// </summary>
|
||||
/// <param name="section">Represents the section in which the data is stored.</param>
|
||||
/// <param name="key">Represents the key of the stored data.</param>
|
||||
/// <param name="value">Value to be stored.</param>
|
||||
void WriteString(string section, string key, string value);
|
||||
|
||||
/// <summary>
|
||||
/// Writes an integer value.
|
||||
/// </summary>
|
||||
/// <param name="section">Represents the section in which the data is stored.</param>
|
||||
/// <param name="key">Represents the key of the stored data.</param>
|
||||
/// <param name="value">Value to be stored.</param>
|
||||
void WriteInteger(string section, string key, int value);
|
||||
|
||||
/// <summary>
|
||||
/// Writes a bool value.
|
||||
/// </summary>
|
||||
/// <param name="section">Represents the section in which the data is stored.</param>
|
||||
/// <param name="key">Represents the key of the stored data.</param>
|
||||
/// <param name="value">Value to be stored.</param>
|
||||
void WriteBool(string section, string key, bool value);
|
||||
|
||||
/// <summary>
|
||||
/// Deletes a key inside a <see cref="section"/>.
|
||||
/// </summary>
|
||||
/// <param name="section">Represents the section in which the data is stored.</param>
|
||||
/// <param name="key">Represents the key to be deleted.</param>
|
||||
void DeleteKey(string section, string key);
|
||||
|
||||
/// <summary>
|
||||
/// Deletes a section with all its keys.
|
||||
/// </summary>
|
||||
/// <param name="section">Represents the section to be deleted.</param>
|
||||
void DeleteSection(string section);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,16 +3,26 @@ using System.Text;
|
|||
|
||||
namespace SweetLib.Classes.Storer
|
||||
{
|
||||
/// <summary>
|
||||
/// Implementation of an <see cref="IStorer"/> interface which stores the data inside an ini file.
|
||||
/// </summary>
|
||||
public class IniFileStorer : IStorer
|
||||
{
|
||||
/// <summary>
|
||||
/// Ini file path.
|
||||
/// </summary>
|
||||
public string FileName { get; }
|
||||
|
||||
[DllImport("kernel32", CharSet = CharSet.Unicode)]
|
||||
static extern long WritePrivateProfileString(string Section, string Key, string Value, string FilePath);
|
||||
private static extern long WritePrivateProfileString(string section, string key, string value, string filePath);
|
||||
|
||||
[DllImport("kernel32", CharSet = CharSet.Unicode)]
|
||||
static extern int GetPrivateProfileString(string Section, string Key, string Default, StringBuilder RetVal, int Size, string FilePath);
|
||||
private static extern int GetPrivateProfileString(string section, string key, string defaultValue, StringBuilder value, int size, string filePath);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of <see cref="IniFileStorer"/> with a specified file name.
|
||||
/// </summary>
|
||||
/// <param name="fileName">The file name of the ini file.</param>
|
||||
public IniFileStorer(string fileName)
|
||||
{
|
||||
FileName = fileName;
|
||||
|
|
|
@ -3,24 +3,45 @@ using SweetLib.Classes.Exception;
|
|||
|
||||
namespace SweetLib.Classes.Storer
|
||||
{
|
||||
/// <summary>
|
||||
/// Implementation of an <see cref="IStorer"/> interface which stores the data inside the registry.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Sections will be interpreted as subkeys on registry level.
|
||||
/// </remarks>
|
||||
public class RegistryStorer : IStorer
|
||||
{
|
||||
public RegistryKey BaseRegistryKey { get; }
|
||||
/// <summary>
|
||||
/// The base registry key in which will be operated.
|
||||
/// </summary>
|
||||
public RegistryKey OperatingRegistryKey { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of <see cref="RegistryStorer"/> with a specified application name.
|
||||
/// </summary>
|
||||
/// <param name="appName">The applications base name. This will be used as name for a sub key inside the software key below the base key.</param>
|
||||
/// <remarks>
|
||||
/// This will use current user as the base key.
|
||||
/// </remarks>
|
||||
public RegistryStorer(string appName) : this(Registry.CurrentUser, appName) { }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of <see cref="RegistryStorer"/> with a specified application name.
|
||||
/// </summary>
|
||||
/// <param name="baseRegistryKey">Provide a key of <see cref="Registry"/>, e.G. <i>Registry.CurrentUser</i>.</param>
|
||||
/// <param name="appName">The applications base name. This will be used as name for a sub key inside the software key below the base key.</param>
|
||||
public RegistryStorer(RegistryKey baseRegistryKey, string appName)
|
||||
{
|
||||
baseRegistryKey = baseRegistryKey.CreateSubKey("SOFTWARE");
|
||||
BaseRegistryKey = baseRegistryKey?.CreateSubKey(appName);
|
||||
OperatingRegistryKey = baseRegistryKey?.CreateSubKey(appName);
|
||||
|
||||
if (BaseRegistryKey == null)
|
||||
if (OperatingRegistryKey == null)
|
||||
throw new RegistryStorerException("Unable to create registriy key.");
|
||||
}
|
||||
|
||||
public string ReadString(string section, string key, string defaultValue = "")
|
||||
{
|
||||
var localRegKey = BaseRegistryKey.OpenSubKey(section);
|
||||
var localRegKey = OperatingRegistryKey.OpenSubKey(section);
|
||||
return (string)localRegKey?.GetValue(key.ToUpper());
|
||||
}
|
||||
|
||||
|
@ -45,7 +66,7 @@ namespace SweetLib.Classes.Storer
|
|||
|
||||
public void WriteString(string section, string key, string value)
|
||||
{
|
||||
var localRegKey = BaseRegistryKey.CreateSubKey(section);
|
||||
var localRegKey = OperatingRegistryKey.CreateSubKey(section);
|
||||
localRegKey?.SetValue(key.ToUpper(), value);
|
||||
}
|
||||
|
||||
|
@ -61,13 +82,13 @@ namespace SweetLib.Classes.Storer
|
|||
|
||||
public void DeleteKey(string section, string key)
|
||||
{
|
||||
var localRegKey = BaseRegistryKey.CreateSubKey(section);
|
||||
var localRegKey = OperatingRegistryKey.CreateSubKey(section);
|
||||
localRegKey?.DeleteValue(key);
|
||||
}
|
||||
|
||||
public void DeleteSection(string section)
|
||||
{
|
||||
BaseRegistryKey.DeleteSubKey(section);
|
||||
OperatingRegistryKey.DeleteSubKey(section);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -31,5 +31,5 @@ using System.Runtime.InteropServices;
|
|||
// Sie können alle Werte angeben oder die standardmäßigen Build- und Revisionsnummern
|
||||
// übernehmen, indem Sie "*" eingeben:
|
||||
// [assembly: AssemblyVersion("1.0.*")]
|
||||
[assembly: AssemblyVersion("1.0.21.0")]
|
||||
[assembly: AssemblyFileVersion("1.0.21.0")]
|
||||
[assembly: AssemblyVersion("0.1.23.0")]
|
||||
[assembly: AssemblyFileVersion("0.1.23.0")]
|
||||
|
|
|
@ -4,6 +4,9 @@ using SweetLib.Utils.Logger.Message;
|
|||
|
||||
namespace SweetLib.Utils.Logger
|
||||
{
|
||||
/// <summary>
|
||||
/// Enum which contains the several log levels.
|
||||
/// </summary>
|
||||
[Flags]
|
||||
public enum LogLevel
|
||||
{
|
||||
|
@ -17,22 +20,52 @@ namespace SweetLib.Utils.Logger
|
|||
All = Int32.MaxValue,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Global logger class providing several methods to log events by the application.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// As <see cref="DefaultLogMemory"/> a <see cref="ArchivableConsoleLogMemory"/> will be used. You can change this to any other implementation at any time while runtime.
|
||||
/// Default log levels are set as bitflags in <see cref="GlobalLogLevel"/>.
|
||||
/// </remarks>
|
||||
public static class Logger
|
||||
{
|
||||
/// <summary>
|
||||
/// The global log level. Only messages with the set <see cref="LogLevel"/> will be procedered.
|
||||
/// </summary>
|
||||
public static LogLevel GlobalLogLevel { get; set; } = LogLevel.Info | LogLevel.Warn | LogLevel.Error;
|
||||
|
||||
/// <summary>
|
||||
/// The default <see cref="ILogMemory"/> which will be used for any logging action, if no custom <see cref="ILogMemory"/> is set as parameter.
|
||||
/// </summary>
|
||||
public static ILogMemory DefaultLogMemory = new ArchivableConsoleLogMemory();
|
||||
|
||||
/// <summary>
|
||||
/// Will log a message into the global <see cref="DefaultLogMemory"/>.
|
||||
/// </summary>
|
||||
/// <param name="logLevel">The log level of this message.</param>
|
||||
/// <param name="message">The message to log.</param>
|
||||
public static void Log(LogLevel logLevel, string message)
|
||||
{
|
||||
Log(logLevel, message, DefaultLogMemory);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Will log a message into the provided <see cref="logMemory"/>.
|
||||
/// </summary>
|
||||
/// <param name="logLevel">The log level of this message.</param>
|
||||
/// /// <param name="message">The message to log.</param>
|
||||
/// <param name="logMemory">The <see cref="ILogMemory"/> to store the <see cref="message"/> into.</param>
|
||||
public static void Log(LogLevel logLevel, string message, ILogMemory logMemory)
|
||||
{
|
||||
Log(new LogMessage(logLevel, message), logMemory);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Will log a message into the provided <see cref="logMemory"/>.
|
||||
/// </summary>
|
||||
/// <param name="message">A <see cref="LogMessage"/> object to store.</param>
|
||||
/// <param name="logMemory">The <see cref="ILogMemory"/> to store the <see cref="message"/> into.</param>
|
||||
/// <remarks>In general use cases you should either use one of the <see cref="Log(SweetLib.Utils.Logger.LogLevel,string)"/> or <see cref="Log(SweetLib.Utils.Logger.LogLevel,string)"/> methods which will generate a call to this method.</remarks>
|
||||
public static void Log(LogMessage message, ILogMemory logMemory)
|
||||
{
|
||||
if (message == null)
|
||||
|
@ -47,26 +80,46 @@ namespace SweetLib.Utils.Logger
|
|||
logMemory.Remember(message);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Will log a message with the <see cref="LogLevel.Trace"/> log level.
|
||||
/// </summary>
|
||||
/// <param name="message">Message to log.</param>
|
||||
public static void Trace(string message)
|
||||
{
|
||||
Log(LogLevel.Trace, message);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Will log a message with the <see cref="LogLevel.Debug"/> log level.
|
||||
/// </summary>
|
||||
/// <param name="message">Message to log.</param>
|
||||
public static void Debug(string message)
|
||||
{
|
||||
Log(LogLevel.Debug, message);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Will log a message with the <see cref="LogLevel.Info"/> log level.
|
||||
/// </summary>
|
||||
/// <param name="message">Message to log.</param>
|
||||
public static void Info(string message)
|
||||
{
|
||||
Log(LogLevel.Info, message);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Will log a message with the <see cref="LogLevel.Warn"/> log level.
|
||||
/// </summary>
|
||||
/// <param name="message">Message to log.</param>
|
||||
public static void Warn(string message)
|
||||
{
|
||||
Log(LogLevel.Warn, message);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Will log a message with the <see cref="LogLevel.Error"/> log level.
|
||||
/// </summary>
|
||||
/// <param name="message">Message to log.</param>
|
||||
public static void Error(string message)
|
||||
{
|
||||
Log(LogLevel.Error, message);
|
||||
|
|
|
@ -2,12 +2,28 @@
|
|||
|
||||
namespace SweetLib.Utils.Logger.Memory
|
||||
{
|
||||
/// <summary>
|
||||
/// Interface for a class to store and proceed <see cref="LogMessage"/> objects.
|
||||
/// </summary>
|
||||
public interface ILogMemory
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds a <see cref="message"/> into the <see cref="ILogMemory"/>.
|
||||
/// </summary>
|
||||
/// <param name="message"><see cref="LogMessage"/> to be stored.</param>
|
||||
void Remember(LogMessage message);
|
||||
|
||||
/// <summary>
|
||||
/// Removes a <see cref="message"/> from the <see cref="ILogMemory"/>.
|
||||
/// </summary>
|
||||
/// <param name="message"><see cref="LogMessage"/> to be removed.</param>
|
||||
/// <remarks>This might not have any effect depending on the <see cref="ILogMemory"/> implementation.</remarks>
|
||||
void Forget(LogMessage message);
|
||||
|
||||
/// <summary>
|
||||
/// Saves all remembered <see cref="LogMessage"/> objects into a persistent file.
|
||||
/// </summary>
|
||||
/// <param name="fullFileName">File name to store the <see cref="LogMessage"/> objects.</param>
|
||||
void Archive(string fullFileName);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,16 +3,39 @@ using System.Globalization;
|
|||
|
||||
namespace SweetLib.Utils.Logger.Message
|
||||
{
|
||||
/// <summary>
|
||||
/// <see cref="LogMessage"/> contains all event log data which should be logged in as a single log message.
|
||||
/// </summary>
|
||||
public class LogMessage : IFormattable
|
||||
{
|
||||
/// <summary>
|
||||
/// The <see cref="LogLevel"/> of this event log.
|
||||
/// </summary>
|
||||
public LogLevel LogLevel { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The message of this event log.
|
||||
/// </summary>
|
||||
public string Message { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The date and time of this event log.
|
||||
/// </summary>
|
||||
public DateTime LogDateTime { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="LogMessage"/> instance. <see cref="DateTime.Now"/> will be the <see cref="LogDateTime"/>.
|
||||
/// </summary>
|
||||
/// <param name="logLevel">The log level of this event log.</param>
|
||||
/// <param name="message">The message of this event log.</param>
|
||||
public LogMessage(LogLevel logLevel, string message) : this(logLevel, message, DateTime.Now) { }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="LogMessage"/> instance.
|
||||
/// </summary>
|
||||
/// <param name="logLevel">The log level of this event log.</param>
|
||||
/// <param name="message">The message of this event log.</param>
|
||||
/// <param name="logDateTime">The <see cref="DateTime"/> of this event log.</param>
|
||||
public LogMessage(LogLevel logLevel, string message, DateTime logDateTime)
|
||||
{
|
||||
if (message == null)
|
||||
|
@ -26,12 +49,22 @@ namespace SweetLib.Utils.Logger.Message
|
|||
LogDateTime = logDateTime;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a formatted <see cref="string"/> of this event log. <see cref="LogMessageFormatter.DefaultFormatString"/> will be used to format this event log.
|
||||
/// </summary>
|
||||
/// <returns>A formated <see cref="string"/> of this event log.</returns>
|
||||
public override string ToString()
|
||||
{
|
||||
return ToString(LogMessageFormatter.DefaultFormatString, CultureInfo.CurrentCulture);
|
||||
}
|
||||
|
||||
public string ToString(string format, IFormatProvider formatProvider)
|
||||
/// <summary>
|
||||
/// Generates a formatted <see cref="string"/> of this event log with a given format.
|
||||
/// </summary>
|
||||
/// <param name="format">The format to be used. See <see cref="LogMessageFormatter"/> for more format information.</param>
|
||||
/// <param name="formatProvider">Optional, an <see cref="IFormatProvider"/> interface to be used while formatting if needed.</param>
|
||||
/// <returns>A formated <see cref="string"/> of this event log.</returns>
|
||||
public string ToString(string format, IFormatProvider formatProvider = null)
|
||||
{
|
||||
return LogMessageFormatter.Instance.Format(format, this, formatProvider);
|
||||
}
|
||||
|
|
|
@ -3,6 +3,10 @@ using System.Globalization;
|
|||
|
||||
namespace SweetLib.Utils.Logger.Message
|
||||
{
|
||||
/// <summary>
|
||||
/// A <see cref="ICustomFormatter"/> which is used to format <see cref="LogMessage"/> objects.
|
||||
/// </summary>
|
||||
/// <remarks>This class implements a singleton pattern.</remarks>
|
||||
public class LogMessageFormatter : ICustomFormatter
|
||||
{
|
||||
|
||||
|
@ -10,10 +14,16 @@ namespace SweetLib.Utils.Logger.Message
|
|||
|
||||
private static object Locker { get; } = new object();
|
||||
|
||||
public static LogMessageFormatter FormatterInstance { get; set; }
|
||||
/// <summary>
|
||||
/// Accesses the global instance of the <see cref="LogMessageFormatter"/>.
|
||||
/// </summary>
|
||||
private static LogMessageFormatter FormatterInstance { get; set; }
|
||||
|
||||
public static string DefaultFormatString { get; set; } = $"[{CultureInfo.CurrentCulture.DateTimeFormat.ShortDatePattern} - {CultureInfo.CurrentCulture.DateTimeFormat.LongTimePattern}] (LL): V";
|
||||
|
||||
/// <summary>
|
||||
/// The default format string which is used to format <see cref="LogMessage"/> objects, if no custom format string is provided.
|
||||
/// </summary>
|
||||
public static LogMessageFormatter Instance
|
||||
{
|
||||
get
|
||||
|
@ -32,10 +42,20 @@ namespace SweetLib.Utils.Logger.Message
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Formats a <see cref="LogMessage"/> object.
|
||||
/// </summary>
|
||||
/// <param name="format">The format string. If <see cref="null"/>, <see cref="DefaultFormatString"/> will be used.</param>
|
||||
/// <param name="arg">The <see cref="LogMessage"/> object to be formatted.</param>
|
||||
/// <param name="formatProvider">Optional, an <see cref="IFormatProvider"/> interface to be used while formatting if needed.</param>
|
||||
/// <returns>A formatted <see cref="string"/> of the <see cref="LogMessage"/>.</returns>
|
||||
/// <remarks>
|
||||
/// If <see cref="arg"/> is not a <see cref="LogMessage"/> object, it will either be returned the formatted string implemented by the type of <see cref="arg"/>, if <see cref="IFormattable"/> is implemented by it, or the <see cref="arg.ToString()"/> result."
|
||||
/// </remarks>
|
||||
public string Format(string format, object arg, IFormatProvider formatProvider)
|
||||
{
|
||||
if (format == null)
|
||||
throw new ArgumentNullException(nameof(format));
|
||||
format = DefaultFormatString;
|
||||
|
||||
if (arg == null)
|
||||
throw new ArgumentNullException(nameof(arg));
|
||||
|
|
Loading…
Reference in a new issue