mirror of
https://github.com/chibicitiberiu/drumkit.git
synced 2024-02-24 10:53:32 +00:00
Build 130105
This commit is contained in:
179
DrumKit/Managers/DataManager.cs
Normal file
179
DrumKit/Managers/DataManager.cs
Normal file
@ -0,0 +1,179 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Xml.Serialization;
|
||||
using System.IO;
|
||||
using Windows.Storage;
|
||||
|
||||
namespace DrumKit
|
||||
{
|
||||
static class DataManager
|
||||
{
|
||||
public static AppSettings Settings { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Checks if this is the first time application was launched.
|
||||
/// </summary>
|
||||
public static async Task<bool> IsFirstLaunch()
|
||||
{
|
||||
// See if 'installed.xml' exists
|
||||
var folder = ApplicationData.Current.RoamingFolder;
|
||||
var files = await folder.GetFilesAsync();
|
||||
|
||||
return files.Count(x => x.Name == "installed.xml") == 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copies the content of the source folder into the destination folder recursively.
|
||||
/// </summary>
|
||||
//private static async Task CopyFolder(StorageFolder source, StorageFolder dest)
|
||||
//{
|
||||
// // Copy folders recursively
|
||||
// var folders = await source.GetFoldersAsync();
|
||||
|
||||
// foreach (var i in folders)
|
||||
// {
|
||||
// var newfolder = await dest.CreateFolderAsync(i.Name, CreationCollisionOption.OpenIfExists);
|
||||
// await CopyFolder(i, newfolder);
|
||||
// }
|
||||
|
||||
// // Copy files
|
||||
// var files = await source.GetFilesAsync();
|
||||
|
||||
// foreach (var i in files)
|
||||
// await i.CopyAsync(dest);
|
||||
//}
|
||||
|
||||
/// <summary>
|
||||
/// Installs the assets at first launch.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
private static async Task InstallAssets()
|
||||
{
|
||||
// Read content of 'ApplicationData'
|
||||
var reader = new DrumKit.Archiving.TarballReader();
|
||||
await reader.Unpack(new Uri("ms-appx:///Assets/ApplicationData.tar"), ApplicationData.Current.RoamingFolder);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates the 'installed.xml' file.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
private static async Task MarkInstalled()
|
||||
{
|
||||
// Open stream
|
||||
StorageFile file = await ApplicationData.Current.RoamingFolder.CreateFileAsync("installed.xml");
|
||||
var stream = await file.OpenAsync(FileAccessMode.ReadWrite);
|
||||
var iostream = stream.AsStream();
|
||||
|
||||
// Generate xml
|
||||
var writer = System.Xml.XmlWriter.Create(iostream, new System.Xml.XmlWriterSettings() { Async = true, CloseOutput = true });
|
||||
|
||||
writer.WriteStartDocument();
|
||||
writer.WriteStartElement("drumkit");
|
||||
writer.WriteString(DateTime.UtcNow.ToString());
|
||||
writer.WriteEndElement();
|
||||
writer.WriteEndDocument();
|
||||
|
||||
// Cleanup
|
||||
await writer.FlushAsync();
|
||||
writer.Dispose();
|
||||
iostream.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resets everything to factory settings.
|
||||
/// The application must be reinitialized after (or closed).
|
||||
/// </summary>
|
||||
public static async Task FactoryReset()
|
||||
{
|
||||
await ApplicationData.Current.ClearAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads the settings file.
|
||||
/// </summary>
|
||||
public static async Task LoadSettings()
|
||||
{
|
||||
// If all else fails, default settings
|
||||
Settings = new AppSettings();
|
||||
|
||||
// Get settings file
|
||||
var files = await ApplicationData.Current.RoamingFolder.GetFilesAsync();
|
||||
var sf = files.FirstOrDefault(x => x.Name == "settings.xml");
|
||||
|
||||
// File found
|
||||
if (sf != null)
|
||||
{
|
||||
// Open file
|
||||
var fstream = await sf.OpenReadAsync();
|
||||
var fstream_net = fstream.AsStream();
|
||||
|
||||
// Deserialize
|
||||
XmlSerializer s = new XmlSerializer(Settings.GetType());
|
||||
var settings = s.Deserialize(fstream_net) as AppSettings;
|
||||
|
||||
// All good
|
||||
if (settings != null)
|
||||
Settings = settings;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Loads the settings file.
|
||||
/// </summary>
|
||||
public static async Task SaveSettings()
|
||||
{
|
||||
// Get settings file
|
||||
var file = await ApplicationData.Current.RoamingFolder.CreateFileAsync("settings.xml", CreationCollisionOption.ReplaceExisting);
|
||||
|
||||
// Open file
|
||||
var fstream = await file.OpenAsync(FileAccessMode.ReadWrite);
|
||||
var fstream_net = fstream.AsStream();
|
||||
|
||||
// Serialize
|
||||
XmlSerializer s = new XmlSerializer(Settings.GetType());
|
||||
s.Serialize(fstream_net, Settings);
|
||||
|
||||
// Cleanup
|
||||
await fstream_net.FlushAsync();
|
||||
fstream_net.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the application (prepares the application at first launch, loads settings and drums).
|
||||
/// </summary>
|
||||
public static async Task Initialize()
|
||||
{
|
||||
// Is this the first time the user launches the application?
|
||||
if (await IsFirstLaunch())
|
||||
{
|
||||
// Clean up any junk
|
||||
await FactoryReset();
|
||||
|
||||
// Copy local assets to app data
|
||||
await InstallAssets();
|
||||
|
||||
// Generate 'installed.xml' file
|
||||
await MarkInstalled();
|
||||
}
|
||||
|
||||
// Load settings
|
||||
await LoadSettings();
|
||||
|
||||
// Load drum packages
|
||||
}
|
||||
|
||||
public static async Task Close()
|
||||
{
|
||||
// Save settings
|
||||
await SaveSettings();
|
||||
|
||||
// Save modified layout & stuff
|
||||
|
||||
}
|
||||
}
|
||||
}
|
120
DrumKit/Managers/DrumsManager.cs
Normal file
120
DrumKit/Managers/DrumsManager.cs
Normal file
@ -0,0 +1,120 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Xml.Serialization;
|
||||
using System.IO;
|
||||
using Windows.Storage;
|
||||
|
||||
namespace DrumKit
|
||||
{
|
||||
static class DrumsManager
|
||||
{
|
||||
private static StorageFolder currentDrumkitKey = null;
|
||||
private static int currentDrumkitLayoutIndex = -1;
|
||||
|
||||
public static Dictionary<StorageFolder, Drumkit> AvailableDrumkits { get; private set; }
|
||||
public static DrumkitLayoutCollection CurrentDrumkitLayouts { get; private set; }
|
||||
public static DrumkitConfig CurrentDrumkitConfig { get; private set; }
|
||||
|
||||
public static Drumkit CurrentDrumkit
|
||||
{
|
||||
get {
|
||||
return (currentDrumkitKey == null) ? null : AvailableDrumkits[currentDrumkitKey];
|
||||
}
|
||||
}
|
||||
|
||||
public static StorageFolder CurrentDrumkitLocation
|
||||
{
|
||||
get {
|
||||
return currentDrumkitKey;
|
||||
}
|
||||
}
|
||||
|
||||
public static DrumkitLayout CurrentDrumkitLayout
|
||||
{
|
||||
get {
|
||||
return (currentDrumkitLayoutIndex == -1) ? null : CurrentDrumkitLayouts.Items[currentDrumkitLayoutIndex];
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<object> DeserializeFile(StorageFile file, Type type)
|
||||
{
|
||||
// Open manifest file
|
||||
var stream = await file.OpenReadAsync();
|
||||
var iostream = stream.AsStream();
|
||||
|
||||
// Deserialize
|
||||
XmlSerializer serializer = new XmlSerializer(type);
|
||||
return serializer.Deserialize(iostream);
|
||||
}
|
||||
|
||||
private static async Task<Drumkit> LoadDrumkit(StorageFolder f)
|
||||
{
|
||||
// Open manifest file
|
||||
var manifest = await f.GetFileAsync("drumkit.xml");
|
||||
object dk = await DeserializeFile(manifest, typeof(Drumkit));
|
||||
return dk as Drumkit;
|
||||
}
|
||||
|
||||
private static async Task FindDrumkits()
|
||||
{
|
||||
// Reset list
|
||||
AvailableDrumkits = new Dictionary<StorageFolder, Drumkit>();
|
||||
|
||||
// Get 'drumkits' folder content
|
||||
var folder = await ApplicationData.Current.RoamingFolder.GetFolderAsync("Drumkits");
|
||||
var kits = await folder.GetFoldersAsync();
|
||||
|
||||
// Load each drumkit
|
||||
foreach (var i in kits)
|
||||
{
|
||||
Drumkit kit = await LoadDrumkit(i);
|
||||
if (kit != null)
|
||||
AvailableDrumkits.Add(i, kit);
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task LoadCurrentDrumkit(string name)
|
||||
{
|
||||
// Get it from the list
|
||||
var current = AvailableDrumkits.FirstOrDefault(x => x.Value.Name == name);
|
||||
|
||||
// Doesn't exist? The default should at least exist.
|
||||
if (current.Equals(default(KeyValuePair<StorageFolder, Drumkit>)))
|
||||
current = AvailableDrumkits.FirstOrDefault(x => x.Value.Name == "Default");
|
||||
|
||||
// Not even default? Get any kit
|
||||
if (current.Equals(default(KeyValuePair<StorageFolder, Drumkit>)))
|
||||
current = AvailableDrumkits.FirstOrDefault();
|
||||
|
||||
// No drumkit? This is a serious problem
|
||||
if (current.Equals(default(KeyValuePair<StorageFolder, Drumkit>)))
|
||||
throw new Exception("No drumkits available.");
|
||||
|
||||
currentDrumkitKey = current.Key;
|
||||
|
||||
// Load layout and configuration
|
||||
StorageFile layout = await current.Key.GetFileAsync(current.Value.LayoutFilePath);
|
||||
CurrentDrumkitLayouts = await DeserializeFile(layout, typeof(DrumkitLayoutCollection)) as DrumkitLayoutCollection;
|
||||
|
||||
StorageFile config = await current.Key.GetFileAsync(current.Value.ConfigFilePath);
|
||||
CurrentDrumkitConfig = await DeserializeFile(config, typeof(DrumkitConfig)) as DrumkitConfig;
|
||||
}
|
||||
|
||||
public static async Task Initialize(AppSettings settings)
|
||||
{
|
||||
// Load drumkits
|
||||
await FindDrumkits();
|
||||
|
||||
// Load current drumkit
|
||||
await LoadCurrentDrumkit(settings.CurrentKit);
|
||||
}
|
||||
|
||||
public static void SetLayout()
|
||||
{
|
||||
currentDrumkitLayoutIndex = 0;
|
||||
}
|
||||
}
|
||||
}
|
147
DrumKit/Managers/SoundManager.cs
Normal file
147
DrumKit/Managers/SoundManager.cs
Normal file
@ -0,0 +1,147 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using SharpDX;
|
||||
using SharpDX.XAudio2;
|
||||
using SharpDX.Multimedia;
|
||||
using System.IO;
|
||||
using Windows.Storage;
|
||||
|
||||
namespace DrumKit
|
||||
{
|
||||
static class SoundManager
|
||||
{
|
||||
#region Data types
|
||||
private class MyWave
|
||||
{
|
||||
public AudioBuffer Buffer { get; set; }
|
||||
public uint[] DecodedPacketsInfo { get; set; }
|
||||
public WaveFormat WaveFormat { get; set; }
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Private attributes
|
||||
private static XAudio2 xaudio = null;
|
||||
private static MasteringVoice mvoice = null;
|
||||
private static Dictionary<string, MyWave> sounds = null;
|
||||
private static SoundPool soundPool = null;
|
||||
#endregion
|
||||
|
||||
#region Initialization
|
||||
/// <summary>
|
||||
/// Initializes the sound manager
|
||||
/// </summary>
|
||||
public static void Initialize()
|
||||
{
|
||||
xaudio = new XAudio2();
|
||||
xaudio.StartEngine();
|
||||
|
||||
mvoice = new MasteringVoice(xaudio);
|
||||
sounds = new Dictionary<string, MyWave>();
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Unload
|
||||
/// <summary>
|
||||
/// Unloads all the sounds
|
||||
/// </summary>
|
||||
public static void UnloadAll()
|
||||
{
|
||||
if (sounds == null)
|
||||
return;
|
||||
|
||||
sounds.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unloads the sounds related to a drum.
|
||||
/// </summary>
|
||||
/// <param name="drum">The drum which will be unloaded.</param>
|
||||
public static void UnloadDrum(Drum drum)
|
||||
{
|
||||
foreach (var i in drum.Sounds)
|
||||
sounds.Remove(drum.Id + i.Intensity.ToString());
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Load
|
||||
/// <summary>
|
||||
/// Adds a sound to the dictionary
|
||||
/// </summary>
|
||||
/// <param name="key">A key associated with the sound</param>
|
||||
/// <param name="file">The file which will be loaded</param>
|
||||
private static async Task AddSound(string key, StorageFile file)
|
||||
{
|
||||
MyWave wave = new MyWave();
|
||||
|
||||
// Load file
|
||||
var stream = await file.OpenReadAsync();
|
||||
var iostream = stream.AsStream();
|
||||
var soundStream = new SoundStream(iostream);
|
||||
var buffer = new AudioBuffer() {
|
||||
Stream = soundStream,
|
||||
AudioBytes = (int)soundStream.Length,
|
||||
Flags = BufferFlags.EndOfStream
|
||||
};
|
||||
iostream.Dispose();
|
||||
|
||||
// Set up information
|
||||
wave.Buffer = buffer;
|
||||
wave.DecodedPacketsInfo = soundStream.DecodedPacketsInfo;
|
||||
wave.WaveFormat = soundStream.Format;
|
||||
|
||||
// Now we can initialize the soundpool
|
||||
if (soundPool == null)
|
||||
soundPool = new SoundPool(xaudio, wave.WaveFormat);
|
||||
|
||||
// Add to sound list
|
||||
sounds.Add(key, wave);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads the sounds associated with a drum
|
||||
/// </summary>
|
||||
public static async Task LoadDrum(Drum drum, StorageFolder root)
|
||||
{
|
||||
// Load each sound
|
||||
foreach (var i in drum.Sounds)
|
||||
{
|
||||
var file = await IOHelper.GetFileRelativeAsync(root, i.Source);
|
||||
await AddSound(drum.Id + i.Intensity.ToString(), file);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads every drum in a drumkit.
|
||||
/// </summary>
|
||||
public static async Task LoadDrumkit(Drumkit kit, StorageFolder root)
|
||||
{
|
||||
// Load each sound
|
||||
foreach (var i in kit.Drums)
|
||||
await LoadDrum(i, root);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Play
|
||||
/// <summary>
|
||||
/// Plays a sound
|
||||
/// </summary>
|
||||
/// <param name="drum_id">The id of the drum</param>
|
||||
/// <param name="intensity">The intensity</param>
|
||||
public static void Play(string drum_id, int intensity)
|
||||
{
|
||||
// Get wave info
|
||||
MyWave info = null;
|
||||
|
||||
if (!sounds.TryGetValue(drum_id + intensity.ToString(), out info))
|
||||
return;
|
||||
|
||||
// Play
|
||||
soundPool.PlayBuffer(info.Buffer, info.DecodedPacketsInfo);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
183
DrumKit/Managers/UIManager.cs
Normal file
183
DrumKit/Managers/UIManager.cs
Normal file
@ -0,0 +1,183 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Windows.System;
|
||||
using Windows.UI.Xaml;
|
||||
using Windows.UI.Xaml.Controls;
|
||||
using Windows.UI.Xaml.Controls.Primitives;
|
||||
|
||||
namespace DrumKit
|
||||
{
|
||||
static class UIManager
|
||||
{
|
||||
private static Dictionary<string, DrumUI> drums;
|
||||
private static Dictionary<VirtualKey, string> keymap;
|
||||
|
||||
public static Canvas TheCanvas { get; private set; }
|
||||
|
||||
#region Initialization
|
||||
/// <summary>
|
||||
/// Initializes the ui manager
|
||||
/// </summary>
|
||||
public static void Initialize()
|
||||
{
|
||||
drums = new Dictionary<string, DrumUI>();
|
||||
keymap = new Dictionary<VirtualKey, string>();
|
||||
|
||||
TheCanvas = new Canvas();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads the ui stuff for drumkit
|
||||
/// </summary>
|
||||
public static async Task ReloadDrumkit()
|
||||
{
|
||||
// Delete previous
|
||||
drums.Clear();
|
||||
keymap.Clear();
|
||||
|
||||
// Load drums
|
||||
foreach (var i in DrumsManager.CurrentDrumkit.Drums)
|
||||
{
|
||||
DrumUI drumui = new DrumUI();
|
||||
await drumui.InitializeDrum(i, DrumsManager.CurrentDrumkitLocation);
|
||||
drumui.PointerPressed += HandlerDrumPointerPressed;
|
||||
drumui.DragDelta += HandlerDrumMoved;
|
||||
|
||||
TheCanvas.Children.Add(drumui.Element);
|
||||
drums.Add(i.Id, drumui);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the layout.
|
||||
/// </summary>
|
||||
public static void ReloadLayout()
|
||||
{
|
||||
DrumUI drum;
|
||||
|
||||
foreach (var i in DrumsManager.CurrentDrumkitLayout.Drums)
|
||||
if (drums.TryGetValue(i.TargetId, out drum))
|
||||
drum.UpdateLayout(i, TheCanvas.ActualWidth, TheCanvas.ActualHeight);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the configuration.
|
||||
/// </summary>
|
||||
public static async Task ReloadConfig()
|
||||
{
|
||||
foreach (var i in DrumsManager.CurrentDrumkitConfig.Drums)
|
||||
{
|
||||
// Enabled and not loaded
|
||||
if (i.IsEnabled && !drums.ContainsKey(i.TargetId))
|
||||
{
|
||||
Drum drum = DrumsManager.CurrentDrumkit.Drums.FirstOrDefault(x => x.Id == i.TargetId);
|
||||
if (drum != null)
|
||||
{
|
||||
DrumUI drumui = new DrumUI();
|
||||
await drumui.InitializeDrum(drum, DrumsManager.CurrentDrumkitLocation);
|
||||
drumui.PointerPressed += HandlerDrumPointerPressed;
|
||||
drumui.DragDelta += HandlerDrumMoved;
|
||||
|
||||
drums.Add(i.TargetId, drumui);
|
||||
}
|
||||
}
|
||||
|
||||
// Disabled and loaded
|
||||
else if (!i.IsEnabled && drums.ContainsKey(i.TargetId))
|
||||
{
|
||||
TheCanvas.Children.Remove(drums[i.TargetId].Element);
|
||||
drums.Remove(i.TargetId);
|
||||
}
|
||||
|
||||
// Keyboard mapping
|
||||
if (!keymap.ContainsKey(i.Key))
|
||||
keymap.Add(i.Key, i.TargetId);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Event handlers
|
||||
/// <summary>
|
||||
/// Handles drum hit using mouse/touchpad
|
||||
/// </summary>
|
||||
private static void HandlerDrumPointerPressed(object sender, Windows.UI.Xaml.Input.PointerRoutedEventArgs e)
|
||||
{
|
||||
var button = sender as FrameworkElement;
|
||||
var tag = button.Tag as string;
|
||||
|
||||
if (tag != null)
|
||||
HandlerDrumClickedCommon(tag);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles drum hit using keyboard
|
||||
/// </summary>
|
||||
public static void HandlerKeyDown(Windows.UI.Core.CoreWindow sender, Windows.UI.Core.KeyEventArgs args)
|
||||
{
|
||||
string drum;
|
||||
|
||||
// Key repeat or something
|
||||
if (args.KeyStatus.WasKeyDown)
|
||||
return;
|
||||
|
||||
// If key in key map, perform "click"
|
||||
if (keymap.TryGetValue(args.VirtualKey, out drum))
|
||||
HandlerDrumClickedCommon(drum);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles drum hit.
|
||||
/// </summary>
|
||||
private static void HandlerDrumClickedCommon(string drum_id)
|
||||
{
|
||||
SoundManager.Play(drum_id, 0);
|
||||
|
||||
if (DataManager.Settings.Animations)
|
||||
drums[drum_id].Hit();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles drum movement.
|
||||
/// </summary>
|
||||
private static void HandlerDrumMoved(object sender, Windows.UI.Xaml.Controls.Primitives.DragDeltaEventArgs e)
|
||||
{
|
||||
var thumb = sender as Thumb;
|
||||
var tag = thumb.Tag as string;
|
||||
int drum_index = -1;
|
||||
|
||||
if (tag != null)
|
||||
drum_index = DrumsManager.CurrentDrumkitLayout.Drums.FindIndex(x => x.TargetId == tag);
|
||||
|
||||
if (drum_index >= 0)
|
||||
{
|
||||
double delta_x = e.HorizontalChange / TheCanvas.ActualWidth;
|
||||
double delta_y = e.VerticalChange / TheCanvas.ActualHeight;
|
||||
|
||||
if (double.IsInfinity(delta_x) || double.IsInfinity(delta_y) || double.IsNaN(delta_x) || double.IsNaN(delta_y))
|
||||
return;
|
||||
|
||||
DrumsManager.CurrentDrumkitLayout.Drums[drum_index].X += delta_x;
|
||||
DrumsManager.CurrentDrumkitLayout.Drums[drum_index].Y += delta_y;
|
||||
|
||||
drums[tag].UpdateLayout(DrumsManager.CurrentDrumkitLayout.Drums[drum_index], TheCanvas.ActualWidth, TheCanvas.ActualHeight);
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
public static void EnterEdit()
|
||||
{
|
||||
foreach (var i in drums)
|
||||
i.Value.EnableEdit();
|
||||
}
|
||||
|
||||
public static void ExitEdit()
|
||||
{
|
||||
foreach (var i in drums)
|
||||
i.Value.DisableEdit();
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user