浏览代码

Merge branch 'main' into AddLiveStatistics

Marcel Baumgartner 1 年之前
父节点
当前提交
b99e8b5476
共有 47 个文件被更改,包括 8462 次插入3386 次删除
  1. 8 0
      Moonlight/App/Configuration/ConfigV1.cs
  2. 10 0
      Moonlight/App/Helpers/Wings/WingsConsole.cs
  3. 6 9
      Moonlight/App/MalwareScans/FakePlayerPluginScan.cs
  4. 6 9
      Moonlight/App/MalwareScans/MinerJarScan.cs
  5. 35 0
      Moonlight/App/MalwareScans/MinerScan.cs
  6. 6 9
      Moonlight/App/MalwareScans/SelfBotCodeScan.cs
  7. 6 9
      Moonlight/App/MalwareScans/SelfBotScan.cs
  8. 1 1
      Moonlight/App/Models/Misc/MalwareScan.cs
  9. 5 5
      Moonlight/App/Services/Background/MalwareBackgroundScanService.cs
  10. 77 13
      Moonlight/App/Services/Files/StorageService.cs
  11. 91 0
      Moonlight/App/Services/IpVerificationService.cs
  12. 8 13
      Moonlight/App/Services/MalwareScanService.cs
  13. 4 6
      Moonlight/App/Services/ServerService.cs
  14. 0 1
      Moonlight/Dockerfile
  15. 1 0
      Moonlight/Moonlight.csproj
  16. 12 7
      Moonlight/Pages/_Layout.cshtml
  17. 5 2
      Moonlight/Program.cs
  18. 4 4
      Moonlight/Properties/launchSettings.json
  19. 22 0
      Moonlight/Shared/Components/Navigations/AdminDomainsNavigation.razor
  20. 3 26
      Moonlight/Shared/Layouts/DefaultLayout.razor
  21. 36 22
      Moonlight/Shared/Layouts/MainLayout.razor
  22. 47 46
      Moonlight/Shared/Views/Admin/Domains/Index.razor
  23. 40 33
      Moonlight/Shared/Views/Admin/Domains/Shared/Index.razor
  24. 109 33
      Moonlight/Shared/Views/Admin/Security/Malware.razor
  25. 2 2
      Moonlight/Shared/Views/Admin/Servers/New.razor
  26. 1 1
      Moonlight/Shared/Views/Server/Index.razor
  27. 0 1
      Moonlight/defaultstorage/configs/default_subscription.json
  28. 0 778
      Moonlight/defaultstorage/resources/lang/de_de.lang
  29. 0 567
      Moonlight/defaultstorage/resources/lang/en_us.lang
  30. 0 778
      Moonlight/defaultstorage/resources/lang/fr_fr.lang
  31. 0 778
      Moonlight/defaultstorage/resources/lang/ro_ro.lang
  32. 0 53
      Moonlight/defaultstorage/resources/mail/login.html
  33. 0 53
      Moonlight/defaultstorage/resources/mail/passwordChange.html
  34. 0 54
      Moonlight/defaultstorage/resources/mail/passwordReset.html
  35. 0 50
      Moonlight/defaultstorage/resources/mail/register.html
  36. 二进制
      Moonlight/defaultstorage/resources/public/background/main.jpg
  37. 0 14
      Moonlight/defaultstorage/resources/public/images/logo.svg
  38. 二进制
      Moonlight/defaultstorage/resources/public/images/logolong.png
  39. 0 9
      Moonlight/wwwroot/assets/css/snow.css
  40. 372 0
      Moonlight/wwwroot/assets/css/toastr.css
  41. 5 0
      Moonlight/wwwroot/assets/js/apexcharts.js
  42. 5 0
      Moonlight/wwwroot/assets/js/bootstrap.min.js
  43. 7522 0
      Moonlight/wwwroot/assets/js/draggable.bundle.js
  44. 1 0
      Moonlight/wwwroot/assets/js/jquery.min.js
  45. 4 0
      Moonlight/wwwroot/assets/js/popper.min.js
  46. 0 0
      Moonlight/wwwroot/assets/js/toastr.min.js
  47. 8 0
      Moonlight/wwwroot/assets/js/xterm-addon-fit.min.js

+ 8 - 0
Moonlight/App/Configuration/ConfigV1.cs

@@ -304,6 +304,14 @@ public class ConfigV1
         public int BlockIpDuration { get; set; } = 15;
 
         [JsonProperty("ReCaptcha")] public ReCaptchaData ReCaptcha { get; set; } = new();
+
+        [JsonProperty("BlockDatacenterIps")]
+        [Description("If this option is enabled, users with an ip from datacenters will not be able to access the panel")]
+        public bool BlockDatacenterIps { get; set; } = true;
+
+        [JsonProperty("AllowCloudflareIps")]
+        [Description("Allow cloudflare ips to bypass the datacenter ip check")]
+        public bool AllowCloudflareIps { get; set; } = false;
     }
 
     public class ReCaptchaData

+ 10 - 0
Moonlight/App/Helpers/Wings/WingsConsole.cs

@@ -218,6 +218,16 @@ public class WingsConsole : IDisposable
                         break;
 
                     case "install output":
+                        if (ServerState != ServerState.Installing)
+                        {
+                            // Because wings is sending "install output" events BEFORE
+                            // sending the "install started" event,
+                            // we need to set the install state here
+                            // See https://github.com/pterodactyl/panel/issues/4853
+                            // for more details
+                            await UpdateServerState(ServerState.Installing);
+                        }
+                        
                         foreach (var line in eventData.Args)
                         {
                             await SaveMessage(line);

+ 6 - 9
Moonlight/App/MalwareScans/FakePlayerPluginScan.cs

@@ -9,7 +9,7 @@ public class FakePlayerPluginScan : MalwareScan
     public override string Name => "Fake player plugin scan";
     public override string Description => "This scan is a simple fake player plugin scan provided by moonlight";
 
-    public override async Task<MalwareScanResult[]> Scan(Server server, IServiceProvider serviceProvider)
+    public override async Task<MalwareScanResult?> Scan(Server server, IServiceProvider serviceProvider)
     {
         var serverService = serviceProvider.GetRequiredService<ServerService>();
         var access = await serverService.CreateFileAccess(server, null!);
@@ -24,19 +24,16 @@ public class FakePlayerPluginScan : MalwareScan
             {
                 if (fileElement.Name.ToLower().Contains("fakeplayer"))
                 {
-                    return new[]
+                    return new()
                     {
-                        new MalwareScanResult
-                        {
-                            Title = "Fake player plugin",
-                            Description = $"Suspicious plugin file: {fileElement.Name}",
-                            Author = "Marcel Baumgartner"
-                        }
+                        Title = "Fake player plugin",
+                        Description = $"Suspicious plugin file: {fileElement.Name}",
+                        Author = "Marcel Baumgartner"
                     };
                 }
             }
         }
 
-        return Array.Empty<MalwareScanResult>();
+        return null;
     }
 }

+ 6 - 9
Moonlight/App/MalwareScans/MinerJarScan.cs

@@ -9,7 +9,7 @@ public class MinerJarScan : MalwareScan
     public override string Name => "Miner jar scan";
     public override string Description => "This scan is a simple miner jar scan provided by moonlight";
 
-    public override async Task<MalwareScanResult[]> Scan(Server server, IServiceProvider serviceProvider)
+    public override async Task<MalwareScanResult?> Scan(Server server, IServiceProvider serviceProvider)
     {
         var serverService = serviceProvider.GetRequiredService<ServerService>();
         var access = await serverService.CreateFileAccess(server, null!);
@@ -23,18 +23,15 @@ public class MinerJarScan : MalwareScan
 
             if (fileElements.Any(x => x.Name == "jdk" && !x.IsFile))
             {
-                return new[]
+                return new()
                 {
-                    new MalwareScanResult
-                    {
-                        Title = "Found Miner",
-                        Description = "Detected suspicious library directory which may contain a script for miners",
-                        Author = "Marcel Baumgartner"
-                    }
+                    Title = "Found Miner",
+                    Description = "Detected suspicious library directory which may contain a script for miners",
+                    Author = "Marcel Baumgartner"
                 };
             }
         }
 
-        return Array.Empty<MalwareScanResult>();
+        return null;
     }
 }

+ 35 - 0
Moonlight/App/MalwareScans/MinerScan.cs

@@ -0,0 +1,35 @@
+using Moonlight.App.Database.Entities;
+using Moonlight.App.Models.Misc;
+using Moonlight.App.Services;
+
+namespace Moonlight.App.MalwareScans;
+
+public class MinerScan : MalwareScan
+{
+    public override string Name => "Miner (NEZHA)";
+    public override string Description => "Probably a miner";
+    public override async Task<MalwareScanResult?> Scan(Server server, IServiceProvider serviceProvider)
+    {
+        var serverService = serviceProvider.GetRequiredService<ServerService>();
+
+        var access = await serverService.CreateFileAccess(server, null!);
+        var files = await access.Ls();
+
+        foreach (var file in files.Where(x => x.IsFile && (x.Name.EndsWith(".sh") || x.Name.EndsWith(".yml")) || x.Name == "bed"))
+        {
+            var content = await access.Read(file);
+
+            if (content.ToLower().Contains("nezha"))
+            {
+                return new()
+                {
+                    Title = "Miner",
+                    Description = "Miner start script (NEZHA)",
+                    Author = "Marcel Baumgartner"
+                };
+            }
+        }
+
+        return null;
+    }
+}

+ 6 - 9
Moonlight/App/MalwareScans/SelfBotCodeScan.cs

@@ -9,7 +9,7 @@ public class SelfBotCodeScan : MalwareScan
     public override string Name => "Selfbot code scan";
     public override string Description => "This scan is a simple selfbot code scan provided by moonlight";
     
-    public override async Task<MalwareScanResult[]> Scan(Server server, IServiceProvider serviceProvider)
+    public override async Task<MalwareScanResult?> Scan(Server server, IServiceProvider serviceProvider)
     {
         var serverService = serviceProvider.GetRequiredService<ServerService>();
         var access = await serverService.CreateFileAccess(server, null!);
@@ -21,18 +21,15 @@ public class SelfBotCodeScan : MalwareScan
 
             if (rawScript.Contains("https://discord.com/api") && !rawScript.Contains("https://discord.com/api/oauth2") && !rawScript.Contains("https://discord.com/api/webhook") || rawScript.Contains("https://rblxwild.com")) //TODO: Export to plugins, add regex for checking
             {
-                return new[]
+                return new MalwareScanResult
                 {
-                    new MalwareScanResult
-                    {
-                        Title = "Potential selfbot",
-                        Description = $"Suspicious script file: {script.Name}",
-                        Author = "Marcel Baumgartner"
-                    }
+                    Title = "Potential selfbot",
+                    Description = $"Suspicious script file: {script.Name}",
+                    Author = "Marcel Baumgartner"
                 };
             }
         }
 
-        return Array.Empty<MalwareScanResult>();
+        return null;
     }
 }

+ 6 - 9
Moonlight/App/MalwareScans/SelfBotScan.cs

@@ -9,7 +9,7 @@ public class SelfBotScan : MalwareScan
     public override string Name => "Selfbot Scan";
     public override string Description => "This scan is a simple selfbot scan provided by moonlight";
 
-    public override async Task<MalwareScanResult[]> Scan(Server server, IServiceProvider serviceProvider)
+    public override async Task<MalwareScanResult?> Scan(Server server, IServiceProvider serviceProvider)
     {
         var serverService = serviceProvider.GetRequiredService<ServerService>();
         var access = await serverService.CreateFileAccess(server, null!);
@@ -17,17 +17,14 @@ public class SelfBotScan : MalwareScan
 
         if (fileElements.Any(x => x.Name == "tokens.txt"))
         {
-            return new[]
+            return new MalwareScanResult
             {
-                new MalwareScanResult
-                {
-                    Title = "Found SelfBot",
-                    Description = "Detected suspicious 'tokens.txt' file which may contain tokens for a selfbot",
-                    Author = "Marcel Baumgartner"
-                }
+                Title = "Found SelfBot",
+                Description = "Detected suspicious 'tokens.txt' file which may contain tokens for a selfbot",
+                Author = "Marcel Baumgartner"
             };
         }
 
-        return Array.Empty<MalwareScanResult>();
+        return null;
     }
 }

+ 1 - 1
Moonlight/App/Models/Misc/MalwareScan.cs

@@ -6,5 +6,5 @@ public abstract class MalwareScan
 {
     public abstract string Name { get; }
     public abstract string Description { get; }
-    public abstract Task<MalwareScanResult[]> Scan(Server server, IServiceProvider serviceProvider);
+    public abstract Task<MalwareScanResult?> Scan(Server server, IServiceProvider serviceProvider);
 }

+ 5 - 5
Moonlight/App/Services/Background/MalwareBackgroundScanService.cs

@@ -15,7 +15,7 @@ public class MalwareBackgroundScanService
 
     public bool IsRunning => !ScanTask?.IsCompleted ?? false;
     public bool ScanAllServers { get; set; }
-    public readonly Dictionary<Server, MalwareScanResult[]> ScanResults;
+    public readonly Dictionary<Server, MalwareScanResult> ScanResults;
     public string Status { get; private set; } = "N/A";
 
     private Task? ScanTask;
@@ -70,16 +70,16 @@ public class MalwareBackgroundScanService
             Status = $"[{i} / {servers.Length}] Scanning server {server.Name}";
             await Event.Emit("malwareScan.status", IsRunning);
             
-            var results = await malwareScanService.Perform(server);
+            var result = await malwareScanService.Perform(server);
 
-            if (results.Any())
+            if (result != null)
             {
                 lock (ScanResults)
                 {
-                    ScanResults.Add(server, results);
+                    ScanResults.Add(server, result);
                 }
                 
-                await Event.Emit("malwareScan.result");
+                await Event.Emit("malwareScan.result", server);
             }
 
             i++;

+ 77 - 13
Moonlight/App/Services/Files/StorageService.cs

@@ -1,15 +1,11 @@
 using Moonlight.App.Helpers;
+using Octokit;
 
 namespace Moonlight.App.Services.Files;
 
 public class StorageService
 {
-    public StorageService()
-    {
-        EnsureCreated();
-    }
-    
-    public void EnsureCreated()
+    public async Task EnsureCreated()
     {
         Directory.CreateDirectory(PathBuilder.Dir("storage", "uploads"));
         Directory.CreateDirectory(PathBuilder.Dir("storage", "configs"));
@@ -17,13 +13,16 @@ public class StorageService
         Directory.CreateDirectory(PathBuilder.Dir("storage", "backups"));
         Directory.CreateDirectory(PathBuilder.Dir("storage", "logs"));
         Directory.CreateDirectory(PathBuilder.Dir("storage", "plugins"));
-        
-        if(IsEmpty(PathBuilder.Dir("storage", "resources")))
+
+        await UpdateResources();
+
+        return;
+        if (IsEmpty(PathBuilder.Dir("storage", "resources")))
         {
             Logger.Info("Default resources not found. Copying default resources");
-            
+
             CopyFilesRecursively(
-                PathBuilder.Dir("defaultstorage", "resources"), 
+                PathBuilder.Dir("defaultstorage", "resources"),
                 PathBuilder.Dir("storage", "resources")
             );
         }
@@ -31,18 +30,83 @@ public class StorageService
         if (IsEmpty(PathBuilder.Dir("storage", "configs")))
         {
             Logger.Info("Default configs not found. Copying default configs");
-            
+
             CopyFilesRecursively(
-                PathBuilder.Dir("defaultstorage", "configs"), 
+                PathBuilder.Dir("defaultstorage", "configs"),
                 PathBuilder.Dir("storage", "configs")
             );
         }
     }
 
+    private async Task UpdateResources()
+    {
+        try
+        {
+            Logger.Info("Checking resources");
+
+            var client = new GitHubClient(
+                new ProductHeaderValue("Moonlight-Panel"));
+
+            var user = "Moonlight-Panel";
+            var repo = "Resources";
+            var resourcesDir = PathBuilder.Dir("storage", "resources");
+
+            async Task CopyDirectory(string dirPath, string localDir)
+            {
+                IReadOnlyList<RepositoryContent> contents;
+
+                if (string.IsNullOrEmpty(dirPath))
+                    contents = await client.Repository.Content.GetAllContents(user, repo);
+                else
+                    contents = await client.Repository.Content.GetAllContents(user, repo, dirPath);
+
+                foreach (var content in contents)
+                {
+                    string localPath = Path.Combine(localDir, content.Name);
+
+                    if (content.Type == ContentType.File)
+                    {
+                        if (content.Name.EndsWith(".gitattributes"))
+                            continue;
+
+                        if (File.Exists(localPath) && !content.Name.EndsWith(".lang"))
+                            continue;
+
+                        if (content.Name.EndsWith(".lang") && File.Exists(localPath) &&
+                            new FileInfo(localPath).Length == content.Size)
+                            continue;
+
+                        var fileContent = await client.Repository.Content.GetRawContent(user, repo, content.Path);
+                        Directory.CreateDirectory(localDir); // Ensure the directory exists
+                        await File.WriteAllBytesAsync(localPath, fileContent);
+
+                        Logger.Debug($"Synced file '{content.Path}'");
+                    }
+                    else if (content.Type == ContentType.Dir)
+                    {
+                        await CopyDirectory(content.Path, localPath);
+                    }
+                }
+            }
+
+            await CopyDirectory("", resourcesDir);
+        }
+        catch (RateLimitExceededException)
+        {
+            Logger.Warn("Unable to sync resources due to your ip being rate-limited by github");
+        }
+        catch (Exception e)
+        {
+            Logger.Warn("Unable to sync resources");
+            Logger.Warn(e);
+        }
+    }
+
     private bool IsEmpty(string path)
     {
         return !Directory.EnumerateFileSystemEntries(path).Any();
     }
+
     private static void CopyFilesRecursively(string sourcePath, string targetPath)
     {
         //Now Create all of the directories
@@ -52,7 +116,7 @@ public class StorageService
         }
 
         //Copy all the files & Replaces any files with the same name
-        foreach (string newPath in Directory.GetFiles(sourcePath, "*.*",SearchOption.AllDirectories))
+        foreach (string newPath in Directory.GetFiles(sourcePath, "*.*", SearchOption.AllDirectories))
         {
             File.Copy(newPath, newPath.Replace(sourcePath, targetPath), true);
         }

+ 91 - 0
Moonlight/App/Services/IpVerificationService.cs

@@ -0,0 +1,91 @@
+using Moonlight.App.Helpers;
+using Whois.NET;
+
+namespace Moonlight.App.Services;
+
+public class IpVerificationService
+{
+    private readonly ConfigService ConfigService;
+
+    public IpVerificationService(ConfigService configService)
+    {
+        ConfigService = configService;
+    }
+
+    public async Task<bool> IsDatacenterOrVpn(string ip)
+    {
+        if (!ConfigService.Get().Moonlight.Security.BlockDatacenterIps)
+            return false;
+        
+        if (string.IsNullOrEmpty(ip))
+            return false;
+
+        var datacenterNames = new List<string>()
+        {
+            "amazon",
+            "aws",
+            "microsoft",
+            "azure",
+            "google",
+            "google cloud",
+            "gcp",
+            "digitalocean",
+            "linode",
+            "vultr",
+            "ovh",
+            "ovhcloud",
+            "alibaba",
+            "oracle",
+            "ibm cloud",
+            "bluehost",
+            "godaddy",
+            "rackpace",
+            "hetzner",
+            "tencent",
+            "scaleway",
+            "softlayer",
+            "dreamhost",
+            "a2 hosting",
+            "inmotion hosting",
+            "red hat openstack",
+            "kamatera",
+            "hostgator",
+            "siteground",
+            "greengeeks",
+            "liquidweb",
+            "joyent",
+            "aruba",
+            "interoute",
+            "fastcomet",
+            "rosehosting",
+            "lunarpages",
+            "fatcow",
+            "jelastic",
+            "datacamp"
+        };
+        
+        if(!ConfigService.Get().Moonlight.Security.AllowCloudflareIps)
+            datacenterNames.Add("cloudflare");
+
+        try
+        {
+            var response = await WhoisClient.QueryAsync(ip);
+            var responseText = response.Raw.ToLower();
+
+            foreach (var name in datacenterNames)
+            {
+                if (responseText.Contains(name))
+                {
+                    Logger.Debug(name);
+                    return true;
+                }
+            }
+        }
+        catch (Exception)
+        {
+            return false;
+        }
+
+        return false;
+    }
+}

+ 8 - 13
Moonlight/App/Services/MalwareScanService.cs

@@ -5,7 +5,7 @@ using Moonlight.App.Services.Plugins;
 
 namespace Moonlight.App.Services;
 
-public class MalwareScanService //TODO: Make this moddable using plugins
+public class MalwareScanService
 {
     private readonly PluginService PluginService;
     private readonly IServiceScopeFactory ServiceScopeFactory;
@@ -16,34 +16,29 @@ public class MalwareScanService //TODO: Make this moddable using plugins
         ServiceScopeFactory = serviceScopeFactory;
     }
 
-    public async Task<MalwareScanResult[]> Perform(Server server)
+    public async Task<MalwareScanResult?> Perform(Server server)
     {
         var defaultScans = new List<MalwareScan>
         {
             new SelfBotScan(),
             new MinerJarScan(),
             new SelfBotCodeScan(),
-            new FakePlayerPluginScan()
+            new FakePlayerPluginScan(),
+            new MinerScan()
         };
 
         var scans = await PluginService.BuildMalwareScans(defaultScans.ToArray());
-
-        var results = new List<MalwareScanResult>();
+        
         using var scope = ServiceScopeFactory.CreateScope();
         
         foreach (var scan in scans)
         {
             var result = await scan.Scan(server, scope.ServiceProvider);
 
-            if (result.Any())
-            {
-                foreach (var scanResult in result)
-                {
-                    results.Add(scanResult);
-                }
-            }
+            if (result != null)
+                return result;
         }
 
-        return results.ToArray();
+        return null;
     }
 }

+ 4 - 6
Moonlight/App/Services/ServerService.cs

@@ -113,19 +113,17 @@ public class ServerService
         if (ConfigService.Get().Moonlight.Security.MalwareCheckOnStart && signal == PowerSignal.Start ||
             signal == PowerSignal.Restart)
         {
-            var results = await new MalwareScanService(
+            var result = await new MalwareScanService(
                 PluginService,
                 ServiceScopeFactory
             ).Perform(server);
 
-            if (results.Any())
+            if (result != null)
             {
-                var resultText = string.Join(" ", results.Select(x => x.Title));
-
-                Logger.Warn($"Found malware on server {server.Uuid}. Results: " + resultText);
+                Logger.Warn($"Found malware on server {server.Uuid}. Result: " + result.Title);
 
                 throw new DisplayException(
-                    $"Unable to start server. Found following malware on this server: {resultText}. Please contact the support if you think this detection is a false positive",
+                    $"Unable to start server. Found following malware on this server: {result.Title}. Please contact the support if you think this detection is a false positive",
                     true);
             }
         }

+ 0 - 1
Moonlight/Dockerfile

@@ -22,5 +22,4 @@ COPY --from=publish /app/publish .
 RUN mkdir -p /app/storage
 RUN touch /app/storage/donttriggeranyerrors
 RUN rm -r /app/storage/*
-COPY "Moonlight/defaultstorage" "/app/defaultstorage"
 ENTRYPOINT ["dotnet", "Moonlight.dll"]

+ 1 - 0
Moonlight/Moonlight.csproj

@@ -55,6 +55,7 @@
     <PackageReference Include="SSH.NET" Version="2020.0.2" />
     <PackageReference Include="Stripe.net" Version="41.23.0-beta.1" />
     <PackageReference Include="UAParser" Version="3.1.47" />
+    <PackageReference Include="WhoisClient.NET" Version="5.0.0" />
     <PackageReference Include="XtermBlazor" Version="1.8.1" />
   </ItemGroup>
 

+ 12 - 7
Moonlight/Pages/_Layout.cshtml

@@ -1,4 +1,4 @@
-@using Microsoft.AspNetCore.Components.Web
+@using Microsoft.AspNetCore.Components.Web
 @using Moonlight.App.Extensions
 @using Moonlight.App.Repositories
 @using Moonlight.App.Services
@@ -41,7 +41,6 @@
 
     <link rel="stylesheet" type="text/css" href="/assets/css/style.bundle.css"/>
     <link rel="stylesheet" type="text/css" href="/assets/css/flashbang.css"/>
-    <link rel="stylesheet" type="text/css" href="/assets/css/snow.css"/>
     <link rel="stylesheet" type="text/css" href="/assets/css/utils.css"/>
     <link rel="stylesheet" type="text/css" href="/assets/css/boxicons.min.css"/>
     <link rel="stylesheet" type="text/css" href="/assets/css/blazor.css"/>
@@ -49,6 +48,7 @@
     <link rel="stylesheet" type="text/css" href="/_content/XtermBlazor/XtermBlazor.css"/>
     <link rel="stylesheet" type="text/css" href="/_content/BlazorMonaco/lib/monaco-editor/min/vs/editor/editor.main.css"/>
     <link rel="stylesheet" type="text/css" href="/_content/Blazor.ContextMenu/blazorContextMenu.min.css"/>
+    <link rel="stylesheet" type="text/css" href="/assets/css/toastr.css"/>
     
     <meta name="viewport" content="width=device-width, initial-scale=1"/>
     <base href="~/"/>
@@ -94,8 +94,8 @@
     </div>
 </div>
 
-<script src="https://cdn.jsdelivr.net/npm/@@popperjs/core@2.11.8/dist/umd/popper.min.js" crossorigin="anonymous"></script>
-<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.1/dist/js/bootstrap.min.js" crossorigin="anonymous"></script>
+<script src="/assets/js/popper.min.js"></script>
+<script src="/assets/js/bootstrap.min.js"></script>
 
 <script src="/_content/Blazor-ApexCharts/js/apex-charts.min.js"></script>
 <script src="/_content/Blazor-ApexCharts/js/blazor-apex-charts.js"></script>
@@ -105,12 +105,15 @@
 <script src="/_content/CurrieTechnologies.Razor.SweetAlert2/sweetAlert2.min.js"></script>
 <script src="/_content/Blazor.ContextMenu/blazorContextMenu.min.js"></script>
 
-<script src="https://cdn.jsdelivr.net/npm/@@shopify/draggable@1.0.0-beta.11/lib/draggable.bundle.js"></script>
+<script src="/assets/js/draggable.bundle.js"></script>
 
 <script src="https://www.google.com/recaptcha/api.js"></script>
 
-<script src="https://cdn.jsdelivr.net/npm/xterm-addon-fit@0.7.0/lib/xterm-addon-fit.min.js"></script>
-<script src="https://code.jquery.com/jquery-3.7.0.min.js"></script>
+<script src="/assets/js/xterm-addon-fit.min.js"></script>
+<script src="/assets/js/jquery.min.js"></script>
+<script src="/assets/js/toastr.min.js"></script>
+
+<script src="/assets/js/apexcharts.js"></script>
 
 <script src="/_content/BlazorMonaco/lib/monaco-editor/min/vs/loader.js"></script>
 <script>require.config({ paths: { 'vs': '/_content/BlazorMonaco/lib/monaco-editor/min/vs' } });</script>
@@ -123,6 +126,8 @@
 moonlight.loading.registerXterm();
 </script>
 
+<script src="_content/Blazor-ApexCharts/js/blazor-apex-charts.js"></script>
+
 <script src="/_framework/blazor.server.js"></script>
 
 </body>

+ 5 - 2
Moonlight/Program.cs

@@ -42,8 +42,10 @@ namespace Moonlight
     {
         public static async Task Main(string[] args)
         {
-            // This will also copy all default config files
-            var configService = new ConfigService(new StorageService());
+            var storageService = new StorageService();
+            await storageService.EnsureCreated();
+            
+            var configService = new ConfigService(storageService);
             var shouldUseSentry = configService
                 .Get()
                 .Moonlight.Sentry.Enable;
@@ -218,6 +220,7 @@ namespace Moonlight
             builder.Services.AddScoped<TicketClientService>();
             builder.Services.AddScoped<TicketAdminService>();
             builder.Services.AddScoped<MalwareScanService>();
+            builder.Services.AddSingleton<IpVerificationService>();
 
             builder.Services.AddScoped<SessionClientService>();
             builder.Services.AddSingleton<SessionServerService>();

+ 4 - 4
Moonlight/Properties/launchSettings.json

@@ -9,7 +9,7 @@
         "ASPNETCORE_ENVIRONMENT": "Development",
         "ML_DEBUG": "true"
       },
-      "applicationUrl": "http://moonlight.testy:5118;https://localhost:7118;http://localhost:5118",
+      "applicationUrl": "http://moonlight.owo:5118;https://localhost:7118;http://localhost:5118",
       "dotnetRunMessages": true
     },
     "Live DB": 
@@ -21,7 +21,7 @@
         "ML_DEBUG": "true",
         "ML_CONFIG_PATH": "storage\\configs\\live_config.json"
       },
-      "applicationUrl": "http://moonlight.testy:5118;https://localhost:7118;http://localhost:5118",
+      "applicationUrl": "http://moonlight.owo:5118;https://localhost:7118;http://localhost:5118",
       "dotnetRunMessages": true
     },
     "Dev DB 1":
@@ -33,7 +33,7 @@
         "ML_DEBUG": "true",
         "ML_CONFIG_PATH": "storage\\configs\\dev_1_config.json"
       },
-      "applicationUrl": "http://moonlight.testy:5118;https://localhost:7118;http://localhost:5118",
+      "applicationUrl": "http://moonlight.owo:5118;https://localhost:7118;http://localhost:5118",
       "dotnetRunMessages": true
     },
     "Dev DB 2":
@@ -45,7 +45,7 @@
         "ML_DEBUG": "true",
         "ML_CONFIG_PATH": "storage\\configs\\dev_2_config.json"
       },
-      "applicationUrl": "http://moonlight.testy:5118;https://localhost:7118;http://localhost:5118",
+      "applicationUrl": "http://moonlight.owo:5118;https://localhost:7118;http://localhost:5118",
       "dotnetRunMessages": true
     }
   }

+ 22 - 0
Moonlight/Shared/Components/Navigations/AdminDomainsNavigation.razor

@@ -0,0 +1,22 @@
+<div class="card mb-5 mb-xl-10">
+    <div class="card-body pt-0 pb-0">
+        <ul class="nav nav-stretch nav-line-tabs nav-line-tabs-2x border-transparent fs-5 fw-bold">
+            <li class="nav-item mt-2">
+                <a class="nav-link text-active-primary ms-0 me-10 py-5 @(Index == 0 ? "active" : "")" href="/admin/domains">
+                    <TL>Domains</TL>
+                </a>
+            </li>
+            <li class="nav-item mt-2">
+                <a class="nav-link text-active-primary ms-0 me-10 py-5 @(Index == 1 ? "active" : "")" href="/admin/domains/shared">
+                    <TL>Shared domains</TL>
+                </a>
+            </li>
+        </ul>
+    </div>
+</div>
+
+@code
+{
+    [Parameter]
+    public int Index { get; set; } = 0;
+}

+ 3 - 26
Moonlight/Shared/Layouts/DefaultLayout.razor

@@ -187,38 +187,15 @@ else
                 </span>
             </a>
         </div>
-        <div data-kt-menu-trigger="click" class="menu-item menu-accordion">
-            <span class="menu-link">
+        <div class="menu-item">
+            <a class="menu-link" href="/admin/domains">
                 <span class="menu-icon">
                     <i class="bx bx-purchase-tag"></i>
                 </span>
                 <span class="menu-title">
                     <TL>Domains</TL>
                 </span>
-                <span class="menu-arrow"></span>
-            </span>
-            <div class="menu-sub menu-sub-accordion">
-                <div class="menu-item">
-                    <a class="menu-link" href="/admin/domains/">
-                        <span class="menu-bullet">
-                            <span class="bullet bullet-dot"></span>
-                        </span>
-                        <span class="menu-title">
-                            <TL>Domains</TL>
-                        </span>
-                    </a>
-                </div>
-                <div class="menu-item">
-                    <a class="menu-link" href="/admin/domains/shared">
-                        <span class="menu-bullet">
-                            <span class="bullet bullet-dot"></span>
-                        </span>
-                        <span class="menu-title">
-                            <TL>Shared domains</TL>
-                        </span>
-                    </a>
-                </div>
-            </div>
+            </a>
         </div>
         <div class="menu-item">
             <a class="menu-link" href="/admin/support">

+ 36 - 22
Moonlight/Shared/Layouts/MainLayout.razor

@@ -22,6 +22,7 @@
 @inject DynamicBackgroundService DynamicBackgroundService
 @inject KeyListenerService KeyListenerService
 @inject ConfigService ConfigService
+@inject IpVerificationService IpVerificationService
 
 @{
     var uri = new Uri(NavigationManager.Uri);
@@ -46,9 +47,35 @@
 
     <DefaultLayout>
         <SoftErrorBoundary>
-            @if (!IsIpBanned)
+            @if (UserProcessed)
             {
-                if (UserProcessed)
+                if (IsIpBanned)
+                {
+                    <div class="modal d-block">
+                        <div class="modal-dialog modal-dialog-centered mw-900px">
+                            <div class="modal-content">
+                                <div class="pt-2 modal-body py-lg-10 px-lg-10">
+                                    <h2>@(SmartTranslateService.Translate("Your ip has been banned"))</h2>
+                                    <p class="mt-3 fw-normal fs-6">@(SmartTranslateService.Translate("Your ip address has been banned by an admin"))</p>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                }
+                else if (IsIpSuspicious)
+                {
+                    <div class="modal d-block">
+                        <div class="modal-dialog modal-dialog-centered mw-900px">
+                            <div class="modal-content">
+                                <div class="pt-2 modal-body py-lg-10 px-lg-10">
+                                    <h2>@(SmartTranslateService.Translate("Your ip his blocked. VPNs and Datacenter IPs are prohibited from accessing this site"))</h2>
+                                    <p class="mt-3 fw-normal fs-6">@(SmartTranslateService.Translate("Please disable your vpn or proxy and try it again"))</p>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                }
+                else
                 {
                     if (uri.LocalPath != "/login" &&
                         uri.LocalPath != "/passwordreset" &&
@@ -102,19 +129,6 @@
                         }
                     }
                 }
-                else
-                {
-                    <div class="modal d-block">
-                        <div class="modal-dialog modal-dialog-centered mw-900px">
-                            <div class="modal-content">
-                                <div class="pt-2 modal-body py-lg-10 px-lg-10">
-                                    <h2>@(SmartTranslateService.Translate("Authenticating"))...</h2>
-                                    <p class="mt-3 fw-normal fs-6">@(SmartTranslateService.Translate("Verifying token, loading user data"))</p>
-                                </div>
-                            </div>
-                        </div>
-                    </div>
-                }
             }
             else
             {
@@ -122,8 +136,8 @@
                     <div class="modal-dialog modal-dialog-centered mw-900px">
                         <div class="modal-content">
                             <div class="pt-2 modal-body py-lg-10 px-lg-10">
-                                <h2>@(SmartTranslateService.Translate("Your ip has been banned"))</h2>
-                                <p class="mt-3 fw-normal fs-6">@(SmartTranslateService.Translate("Your ip address has been banned by an admin"))</p>
+                                <h2>@(SmartTranslateService.Translate("Authenticating"))...</h2>
+                                <p class="mt-3 fw-normal fs-6">@(SmartTranslateService.Translate("Verifying token, loading user data"))</p>
                             </div>
                         </div>
                     </div>
@@ -137,6 +151,7 @@
 {
     private bool UserProcessed = false;
     private bool IsIpBanned = false;
+    private bool IsIpSuspicious = false;
 
     protected override async Task OnAfterRenderAsync(bool firstRender)
     {
@@ -146,11 +161,6 @@
             {
                 DynamicBackgroundService.OnBackgroundImageChanged += async (_, _) => { await InvokeAsync(StateHasChanged); };
 
-                IsIpBanned = await IpBanService.IsBanned();
-
-                if (IsIpBanned)
-                    await InvokeAsync(StateHasChanged);
-
                 await Event.On<Object>("ipBan.update", this, async _ =>
                 {
                     IsIpBanned = await IpBanService.IsBanned();
@@ -158,6 +168,10 @@
                 });
 
                 await IdentityService.Load();
+
+                IsIpBanned = await IpBanService.IsBanned();
+                IsIpSuspicious = await IpVerificationService.IsDatacenterOrVpn(IdentityService.Ip);
+
                 UserProcessed = true;
                 await InvokeAsync(StateHasChanged);
 

+ 47 - 46
Moonlight/Shared/Views/Admin/Domains/Index.razor

@@ -4,6 +4,7 @@
 @using Microsoft.EntityFrameworkCore
 @using BlazorTable
 @using Moonlight.App.Services
+@using Moonlight.Shared.Components.Navigations
 
 @inject DomainRepository DomainRepository
 @inject DomainService DomainService
@@ -11,56 +12,56 @@
 
 @attribute [PermissionRequired(nameof(Permissions.AdminDomains))]
 
+<AdminDomainsNavigation Index="0" />
+
 <LazyLoader @ref="LazyLoader" Load="Load">
-    <div class="row">
-        <div class="card">
-            <div class="card-header border-0 pt-5">
-                <h3 class="card-title align-items-start flex-column">
-                    <span class="card-label fw-bold fs-3 mb-1">
-                        <TL>Domains</TL>
-                    </span>
-                </h3>
-                <div class="card-toolbar">
-                    <a href="/admin/domains/new" class="btn btn-sm btn-light-success">
-                        <i class="bx bx-layer-plus"></i>
-                        <TL>New domain</TL>
-                    </a>
+    <div class="card">
+                <div class="card-header border-0 pt-5">
+                    <h3 class="card-title align-items-start flex-column">
+                        <span class="card-label fw-bold fs-3 mb-1">
+                            <TL>Domains</TL>
+                        </span>
+                    </h3>
+                    <div class="card-toolbar">
+                        <a href="/admin/domains/new" class="btn btn-sm btn-light-success">
+                            <i class="bx bx-layer-plus"></i>
+                            <TL>New domain</TL>
+                        </a>
+                    </div>
                 </div>
-            </div>
-            <div class="card-body pt-0">
-                <div class="table-responsive">
-                    <Table TableItem="Domain" Items="Domains" PageSize="25" TableClass="table table-row-bordered table-row-gray-100 align-middle gs-0 gy-3" TableHeadClass="fw-bold text-muted">
-                        <Column TableItem="Domain" Title="@(SmartTranslateService.Translate("Id"))" Field="@(x => x.Id)" Sortable="true" Filterable="true" Width="10%"/>
-                        <Column TableItem="Domain" Title="@(SmartTranslateService.Translate("Name"))" Field="@(x => x.Name)" Sortable="true" Filterable="true" Width="10%"/>
-                        <Column TableItem="Domain" Title="@(SmartTranslateService.Translate("Shared domain"))" Field="@(x => x.SharedDomain)" Sortable="true" Filterable="true" Width="10%">
-                            <Template>
-                                <span>@(context.SharedDomain.Name)</span>
-                            </Template>
-                        </Column>
-                        <Column TableItem="Domain" Title="@(SmartTranslateService.Translate("Owner"))" Field="@(x => x.Owner)" Sortable="true" Filterable="true" Width="10%">
-                            <Template>
-                                <a href="/admin/users/view/@(context.Owner.Id)">@(context.Owner.Email)</a>
-                            </Template>
-                        </Column>
-                        <Column TableItem="Domain" Title="@(SmartTranslateService.Translate("Manage"))" Field="@(x => x.Id)" Sortable="false" Filterable="false" Width="10%">
-                            <Template>
-                                <a href="/domain/@(context.Id)">Manage</a>
-                            </Template>
-                        </Column>
-                        <Column TableItem="Domain" Title="" Field="@(x => x.Id)" Sortable="false" Filterable="false" Width="10%">
-                            <Template>
-                                <DeleteButton Confirm="true"
-                                              AdditionalCssClasses="float-end"
-                                              OnClick="() => Delete(context)">
-                                </DeleteButton>
-                            </Template>
-                        </Column>
-                        <Pager ShowPageNumber="true" ShowTotalCount="true"/>
-                    </Table>
+                <div class="card-body pt-0">
+                    <div class="table-responsive">
+                        <Table TableItem="Domain" Items="Domains" PageSize="25" TableClass="table table-row-bordered table-row-gray-100 align-middle gs-0 gy-3" TableHeadClass="fw-bold text-muted">
+                            <Column TableItem="Domain" Title="@(SmartTranslateService.Translate("Id"))" Field="@(x => x.Id)" Sortable="true" Filterable="true" Width="10%"/>
+                            <Column TableItem="Domain" Title="@(SmartTranslateService.Translate("Name"))" Field="@(x => x.Name)" Sortable="true" Filterable="true" Width="10%"/>
+                            <Column TableItem="Domain" Title="@(SmartTranslateService.Translate("Shared domain"))" Field="@(x => x.SharedDomain)" Sortable="true" Filterable="true" Width="10%">
+                                <Template>
+                                    <span>@(context.SharedDomain.Name)</span>
+                                </Template>
+                            </Column>
+                            <Column TableItem="Domain" Title="@(SmartTranslateService.Translate("Owner"))" Field="@(x => x.Owner)" Sortable="true" Filterable="true" Width="10%">
+                                <Template>
+                                    <a href="/admin/users/view/@(context.Owner.Id)">@(context.Owner.Email)</a>
+                                </Template>
+                            </Column>
+                            <Column TableItem="Domain" Title="@(SmartTranslateService.Translate("Manage"))" Field="@(x => x.Id)" Sortable="false" Filterable="false" Width="10%">
+                                <Template>
+                                    <a href="/domain/@(context.Id)">Manage</a>
+                                </Template>
+                            </Column>
+                            <Column TableItem="Domain" Title="" Field="@(x => x.Id)" Sortable="false" Filterable="false" Width="10%">
+                                <Template>
+                                    <DeleteButton Confirm="true"
+                                                  AdditionalCssClasses="float-end"
+                                                  OnClick="() => Delete(context)">
+                                    </DeleteButton>
+                                </Template>
+                            </Column>
+                            <Pager ShowPageNumber="true" ShowTotalCount="true"/>
+                        </Table>
+                    </div>
                 </div>
             </div>
-        </div>
-    </div>
 </LazyLoader>
 
 @code

+ 40 - 33
Moonlight/Shared/Views/Admin/Domains/Shared/Index.razor

@@ -5,6 +5,7 @@
 @using Moonlight.App.Database.Entities
 @using Moonlight.App.Services.Interop
 @using BlazorTable
+@using Moonlight.Shared.Components.Navigations
 
 @inject SharedDomainRepository SharedDomainRepository
 @inject SmartTranslateService SmartTranslateService
@@ -14,37 +15,43 @@
 
 @attribute [PermissionRequired(nameof(Permissions.AdminSharedDomains))]
 
+<AdminDomainsNavigation Index="1" />
+
 <LazyLoader @ref="LazyLoader" Load="Load">
-                    <div class="card">
-                        <div class="card-header border-0 pt-5">
-                            <h3 class="card-title align-items-start flex-column">
-                                <span class="card-label fw-bold fs-3 mb-1">
-                                    <span><TL>Shared domains</TL></span>
-                                </span>
-                            </h3>
-                            <div class="card-toolbar">
-                                <a href="/admin/domains/shared/new" class="btn btn-sm btn-light-success">
-                                    <i class="bx bx-layer-plus"></i>
-                                    <span><TL>Add shared domain</TL></span>
-                                </a>
-                            </div>
-                        </div>
-                        <div class="card-body">
-                            <Table TableItem="SharedDomain" Items="SharedDomains" PageSize="25" TableClass="table table-row-bordered table-row-gray-100 align-middle gs-0 gy-3" TableHeadClass="fw-bold text-muted">
-                                <Column TableItem="SharedDomain" Title="@(SmartTranslateService.Translate("Id"))" Field="@(x => x.Id)" Sortable="true" Filterable="true" Width="10%"/>
-                                <Column TableItem="SharedDomain" Title="@(SmartTranslateService.Translate("Name"))" Field="@(x => x.Name)" Sortable="true" Filterable="true" Width="10%"/>
-                                <Column TableItem="SharedDomain" Title="" Field="@(x => x.Id)" Sortable="false" Filterable="false" Width="10%">
-                                    <Template>
-                                        <DeleteButton Confirm="true"
-                                                      AdditionalCssClasses="float-end"
-                                                      OnClick="() => Delete(context)">
-                                        </DeleteButton>
-                                    </Template>
-                                </Column>
-                            </Table>
-                        </div>
-                    </div>
-                </LazyLoader>
+    <div class="card">
+        <div class="card-header border-0 pt-5">
+            <h3 class="card-title align-items-start flex-column">
+                <span class="card-label fw-bold fs-3 mb-1">
+                    <span>
+                        <TL>Shared domains</TL>
+                    </span>
+                </span>
+            </h3>
+            <div class="card-toolbar">
+                <a href="/admin/domains/shared/new" class="btn btn-sm btn-light-success">
+                    <i class="bx bx-layer-plus"></i>
+                    <span>
+                        <TL>Add shared domain</TL>
+                    </span>
+                </a>
+            </div>
+        </div>
+        <div class="card-body">
+            <Table TableItem="SharedDomain" Items="SharedDomains" PageSize="25" TableClass="table table-row-bordered table-row-gray-100 align-middle gs-0 gy-3" TableHeadClass="fw-bold text-muted">
+                <Column TableItem="SharedDomain" Title="@(SmartTranslateService.Translate("Id"))" Field="@(x => x.Id)" Sortable="true" Filterable="true" Width="10%"/>
+                <Column TableItem="SharedDomain" Title="@(SmartTranslateService.Translate("Name"))" Field="@(x => x.Name)" Sortable="true" Filterable="true" Width="10%"/>
+                <Column TableItem="SharedDomain" Title="" Field="@(x => x.Id)" Sortable="false" Filterable="false" Width="10%">
+                    <Template>
+                        <DeleteButton Confirm="true"
+                                      AdditionalCssClasses="float-end"
+                                      OnClick="() => Delete(context)">
+                        </DeleteButton>
+                    </Template>
+                </Column>
+            </Table>
+        </div>
+    </div>
+</LazyLoader>
 
 @code
 {
@@ -72,12 +79,12 @@
         }
         catch (Exception e)
         {
-            //TODO: Add check if any domains are left
-            
+    //TODO: Add check if any domains are left
+
             await AlertService.Error(
                 SmartTranslateService.Translate("Error"),
                 SmartTranslateService.Translate("Something went wrong. Are any domains associated with this shared domain still there?")
-            );
+                );
         }
     }
 }

+ 109 - 33
Moonlight/Shared/Views/Admin/Security/Malware.razor

@@ -4,12 +4,23 @@
 @using Moonlight.App.Services.Background
 @using Moonlight.App.Services
 @using BlazorTable
+@using Microsoft.EntityFrameworkCore
+@using Moonlight.App.ApiClients.Wings
 @using Moonlight.App.Database.Entities
 @using Moonlight.App.Events
+@using Moonlight.App.Helpers
 @using Moonlight.App.Models.Misc
+@using Moonlight.App.Repositories
+@using Moonlight.App.Services.Interop
+@using Moonlight.App.Services.Sessions
 
 @inject MalwareBackgroundScanService MalwareBackgroundScanService
 @inject SmartTranslateService SmartTranslateService
+@inject ServerService ServerService
+@inject ToastService ToastService
+@inject SessionServerService SessionServerService
+@inject Repository<Server> ServerRepository
+@inject Repository<User> UserRepository
 @inject EventSystem Event
 
 @implements IDisposable
@@ -38,7 +49,7 @@
                 }
                 else
                 {
-                    <div class="mb-3">
+                    <div class="mb-5">
                         <div class="form-check">
                             <input class="form-check-input" type="checkbox" id="scanAllServers" @bind="MalwareBackgroundScanService.ScanAllServers">
                             <label class="form-check-label" for="scanAllServers">
@@ -48,8 +59,13 @@
                     </div>
 
                     <WButton Text="@(SmartTranslateService.Translate("Start scan"))"
-                             CssClasses="btn-success"
-                             OnClick="MalwareBackgroundScanService.Start">
+                             CssClasses="btn-success me-3"
+                             OnClick="Scan">
+                    </WButton>
+                    
+                    <WButton Text="@(SmartTranslateService.Translate("Purge page"))"
+                             CssClasses="btn-danger"
+                             OnClick="PurgeSelected">
                     </WButton>
                 }
             </div>
@@ -65,39 +81,14 @@
             <div class="card-body">
                 <LazyLoader @ref="LazyLoaderResults" Load="LoadResults">
                     <div class="table-responsive">
-                        <Table TableItem="Server" Items="ScanResults.Keys" PageSize="25" TableClass="table table-row-bordered table-row-gray-100 align-middle gs-0 gy-3" TableHeadClass="fw-bold text-muted">
+                        <Table @ref="Table" TableItem="Server" Items="ScanResults.Keys" PageSize="25" TableClass="table table-row-bordered table-row-gray-100 align-middle gs-0 gy-3" TableHeadClass="fw-bold text-muted">
                             <Column TableItem="Server" Title="@(SmartTranslateService.Translate("Server"))" Field="@(x => x.Id)" Sortable="false" Filterable="false">
                                 <Template>
                                     <a href="/server/@(context.Uuid)">@(context.Name)</a>
                                 </Template>
                             </Column>
-                            <Column TableItem="Server" Title="@(SmartTranslateService.Translate("Results"))" Field="@(x => x.Id)" Sortable="false" Filterable="false">
-                                <Template>
-                                    <div class="row">
-                                        @foreach (var result in ScanResults[context])
-                                        {
-                                            <div class="col-12 col-md-6 p-3">
-                                                <div class="accordion" id="scanResult@(result.GetHashCode())">
-                                                    <div class="accordion-item">
-                                                        <h2 class="accordion-header" id="scanResult-header@(result.GetHashCode())">
-                                                            <button class="accordion-button fs-4 fw-semibold collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#scanResult-body@(result.GetHashCode())" aria-expanded="false" aria-controls="scanResult-body@(result.GetHashCode())">
-                                                                <span>@(result.Title)</span>
-                                                            </button>
-                                                        </h2>
-                                                        <div id="scanResult-body@(result.GetHashCode())" class="accordion-collapse collapse" aria-labelledby="scanResult-header@(result.GetHashCode())" data-bs-parent="#scanResult">
-                                                            <div class="accordion-body">
-                                                                <p>
-                                                                    @(result.Description)
-                                                                </p>
-                                                            </div>
-                                                        </div>
-                                                    </div>
-                                                </div>
-                                            </div>
-                                        }
-                                    </div>
-                                </Template>
-                            </Column>
+                            <Column TableItem="Server" Title="@(SmartTranslateService.Translate("Title"))" Field="@(x => ScanResults[x].Title)" Sortable="false" Filterable="true" />
+                            <Column TableItem="Server" Title="@(SmartTranslateService.Translate("Description"))" Field="@(x => ScanResults[x].Description)" Sortable="false" Filterable="true" />
                             <Pager ShowPageNumber="true" ShowTotalCount="true"/>
                         </Table>
                     </div>
@@ -109,7 +100,9 @@
 
 @code
 {
-    private readonly Dictionary<Server, MalwareScanResult[]> ScanResults = new();
+    private readonly Dictionary<Server, MalwareScanResult> ScanResults = new();
+
+    private Table<Server> Table;
 
     private LazyLoader LazyLoaderResults;
 
@@ -117,7 +110,18 @@
     {
         await Event.On<Object>("malwareScan.status", this, async o => { await InvokeAsync(StateHasChanged); });
 
-        await Event.On<Object>("malwareScan.result", this, async o => { await LazyLoaderResults.Reload(); });
+        await Event.On<Server?>("malwareScan.result", this, async server =>
+        {
+            lock (MalwareBackgroundScanService.ScanResults)
+            {
+                if (server == null)
+                    return;
+                
+                ScanResults.Add(server, MalwareBackgroundScanService.ScanResults[server]);
+            }
+            
+            await InvokeAsync(StateHasChanged);
+        });
     }
 
     private Task LoadResults(LazyLoader arg)
@@ -140,4 +144,76 @@
         await Event.Off("malwareScan.status", this);
         await Event.Off("malwareScan.result", this);
     }
+
+    private async Task PurgeSelected()
+    {
+        int users = 0;
+        int servers = 0;
+        
+        int allServersCount = Table.FilteredItems.Count();
+        int position = 0;
+
+        await ToastService.CreateProcessToast("purgeProcess", "Purging");
+        
+        foreach (var item in Table.FilteredItems)
+        {
+            position++;
+            
+            if (item == null)
+                continue;
+
+            try
+            {
+                var server = ServerRepository.Get()
+                    .Include(x => x.Owner)
+                    .FirstOrDefault(x => x.Id == item.Id);
+                
+                if(server == null)
+                    continue;
+
+                await ToastService.UpdateProcessToast("purgeProcess", $"[{position}/{allServersCount}] {server.Name}");
+
+                ScanResults.Remove(item);
+
+                await InvokeAsync(StateHasChanged);
+
+                // Owner
+                
+                server.Owner.Status = UserStatus.Banned;
+                UserRepository.Update(server.Owner);
+                users++;
+
+                try
+                {
+                    await SessionServerService.ReloadUserSessions(server.Owner);
+                }
+                catch (Exception) {/* Ignored */}
+
+    // Server itself
+
+                await ServerService.SetPowerState(server, PowerSignal.Kill);
+                await ServerService.Delete(server);
+                servers++;
+            }
+            catch (Exception e)
+            {
+                Logger.Warn($"Error purging server: {item.Uuid}");
+                Logger.Warn(e);
+
+                await ToastService.Error(
+                    $"Failed to purge server '{item.Name}': {e.Message}"
+                );
+            }
+        }
+        
+        await ToastService.RemoveProcessToast("purgeProcess");
+        await ToastService.Success($"Successfully purged {servers} servers by {users} users");
+    }
+
+    private async Task Scan()
+    {
+        ScanResults.Clear();
+        await InvokeAsync(StateHasChanged);
+        await MalwareBackgroundScanService.Start();
+    }
 }

+ 2 - 2
Moonlight/Shared/Views/Admin/Servers/New.razor

@@ -229,7 +229,7 @@
     {
         try
         {
-            await ServerService.Create(Model.Name, Model.Cpu, Model.Memory, Model.Disk, Model.Owner, Model.Image, Model.Node, server =>
+            var newServer = await ServerService.Create(Model.Name, Model.Cpu, Model.Memory, Model.Disk, Model.Owner, Model.Image, Model.Node, server =>
             {
                 server.OverrideStartup = Model.OverrideStartup;
                 server.DockerImageIndex = Model.DockerImageIndex;
@@ -242,7 +242,7 @@
             });
 
             await ToastService.Success(SmartTranslateService.Translate("Server successfully created"));
-            NavigationManager.NavigateTo("/admin/servers");
+            NavigationManager.NavigateTo($"/server/{newServer.Uuid}");
         }
         catch (DisplayException e)
         {

+ 1 - 1
Moonlight/Shared/Views/Server/Index.razor

@@ -190,7 +190,7 @@
             .Include(x => x.Variables)
             .Include(x => x.MainAllocation)
             .Include(x => x.Owner)
-            .First(x => x.Uuid == uuid);
+            .FirstOrDefault(x => x.Uuid == uuid);
 
         if (CurrentServer != null)
         {

+ 0 - 1
Moonlight/defaultstorage/configs/default_subscription.json

@@ -1 +0,0 @@
-[]

+ 0 - 778
Moonlight/defaultstorage/resources/lang/de_de.lang

@@ -1,778 +0,0 @@
-Open support;Support Öffnen
-About us;Über uns
-Imprint;Impressum
-Privacy;Privacy
-Login;Einloggen
-Register;Registrieren
-Insert brand name...;Firmenname eingeben...
-Save and continue;Speichern und Fortfahren
-Saving;Speichern
-Configure basics;Grundlagen einstellen
-Brand name;Firmenname
-test;test
-Insert first name...;Vornamen eingeben...
-Insert last name...;Nachnamen eingeben...
-Insert email address...;E-Mail-Adresse eingeben...
-Add;Hinzufügen
-Adding...;Wird hinzugefügt...
-Add admin accounts;Admin Konto hinzufügen
-First name;Vorname
-Last name;Nachname
-Email address;E-Mail-Adresse
-Enter password;Password eingeben
-Next;Weiter
-Back;Zurück
-Configure features;Features konfigurieren
-Support chat;Chat Hilfe
-Finish;Fertigstellen
-Finalize installation;Installation Fertigstellen
-Moonlight basic settings successfully configured;Moonlight's Standard-Einstellungen wurden konfiguriert
-Ooops. This page is crashed;Ups. Die Seite ist abgestürzt.
-This page is crashed. The error has been reported to the moonlight team. Meanwhile you can try reloading the page;Diese Seite ist abgestürzt. Bitte versuche, sie neu zu laden.
-Setup complete;Einrichtung abgeschlossen
-It looks like this moonlight instance is ready to go;Diese Moonlight Instanz ist bereit
-User successfully created;Benutzer erfolgreich erstellt
-Ooops. Your moonlight client is crashed;Ups. Dein Moonlight ist abgestürzt.
-This error has been reported to the moonlight team;Dieser Fehler wurde dem Moonlight-Team mitgeteilt
-Sign In;Anmelden
-Sign in to start with moonlight;Anmelden, um mit Moonlight zu starten
-Sign in with Discord;Mit Discord Anmelden
-Or with email;Oder mit E-Mail
-Forgot password?;Password vergessen?
-Sign-in;Anmelden
-Not registered yet?;Noch nicht Registriert?
-Sign up;Registrieren
-Authenticating;Authentifizieren...
-Sign in with Google;Mit Google Anmelden
-Working;Bitte warten...
-Error;Fehler
-Email and password combination not found;E-Mail und Password-Kombination wurden nicht gefunden
-Email;E-mail
-Password;Password
-Account settings;Benutzer-Einstellungen
-Logout;Abmelden
-Dashboard;Dashboard
-Order;Bestellen
-Website;Webseite
-Database;Datenbank
-Domain;Domain
-Servers;Server
-Websites;Webseiten
-Databases;Datenbanken
-Domains;Domains
-Changelog;Änderungen
-Firstname;Vorname
-Lastname;Nachname
-Repeat password;Password wiederholen
-Sign Up;Anmelden
-Sign up to start with moonlight;Registrieren um mit Moonlight zu starten
-Sign up with Discord;Mit Discord Registrieren
-Sign up with Google;Mit Google Registrieren
-Sign-up;Registrieren
-Already registered?;Schon Registriert?
-Sign in;Registrieren
-Create something new;Etwas neues erstellen
-Create a gameserver;Einen Gameserver erstellen
-A new gameserver in just a few minutes;Ein neuer Gameserver in wenigen Minuten
-Create a database;Eine Datenbank erstellen
-A quick way to store your data and manage it from all around the world;Eine schnelle Möglichkeit, um deine Daten von überall auf der Welt zu verwalten
-Manage your services;Deine Dienste verwalten
-Manage your gameservers;Gameserver verwalten
-Adjust your gameservers;Deine Gameserver anpassen
-Manage your databases;Datenbanken verwalten
-Insert, delete and update the data in your databases;Daten in die Datenbank einfügen, entfernen und ändern
-Create a website;Eine Webseite erstellen
-Make your own websites with a webspace;Mit einem Webspace eine Webseite erstellen
-Create a domain;Eine Domain erstellen
-Make your servvices accessible throught your own domain;Mache deine Dienste mit einer Domain erreichbar
-Manage your websites;Deine Webseiten verwalten
-Modify the content of your websites;Den Inhalt deiner Webseiten verwalten
-Manage your domains;Deine Domains verwalten
-Add, edit and delete dns records;DNS-Records hinzufügen, entfernen oder bearbeiten
-Admin;Admin
-System;System
-Overview;Übersicht
-Manager;Manager
-Cleanup;Cleanup
-Nodes;Nodes
-Images;Images
-aaPanel;aaPanel
-Users;Benutzer
-Support;Hilfe
-Statistics;Statistiken
-No nodes found. Start with adding a new node;Keine Nodes gefunden. Ein neues Node hinzufügen
-Nodename;Nodename
-FQDN;FQDN
-Create;Erstellen
-Creating;Wird erstellt...
-Http port;Http Port
-Sftp port;Sftp Port
-Moonlight daemon port;Moonlight Daemon Port
-SSL;SSL
-CPU Usage;CPU Auslastung
-In %;In %
-Memory;Arbeitsspeicher
-Used / Available memory;Benutzter / Verfügbarer Arbeitsspeicher
-Storage;Speicherplatz
-Available storage;Verfügbarer Speicherplatz
-Add a new node;Ein neues Node hinzufügen
-Delete;Löschen
-Deleting;Wird gelöscht...
-Edit;Bearbeiten
-Token Id;Token Id
-Token;Token
-Save;Speichern
-Setup;Aufsetzen
-Open a ssh connection to your node and enter;Eine SSH verbindung zum Node hinzufügen und öffnen
-and paste the config below. Then press STRG+O and STRG+X to save;und die Config darunter einfügern. Dann STRG+O und STRG+X um zu Speichern
-Before configuring this node, install the daemon;Installiere den Daemon bevor du dieses Node konfigurierst
-Delete this node?;Dieses Node löschen?
-Do you really want to delete this node;Möchtest du dieses Node wirklich löschen?
-Yes;Ja
-No;Nein
-Status;Status
-Adding;Hinzufügen
-Port;Port
-Id;Id
-Manage;Verwalten
-Create new server;Neuen Server erstellen
-No servers found;Keine Server gefunden
-Server name;Server Name
-Cpu cores;CPU Kerne
-Disk;Speicherplatz
-Image;Image
-Override startup;Startup überschreiben
-Docker image;Docker Image
-CPU Cores (100% = 1 Core);CPU Kerne (100% = 1 Kern)
-Server successfully created;Server erfolgreich erstellt
-Name;Name
-Cores;Kerne
-Owner;Besitzer
-Value;Wert
-An unknown error occured;Ein unbekannter Fehler ist aufgetreten
-No allocation found;Keine Zuweisung gefunden
-Identifier;Identifier
-UuidIdentifier;UuidIdentifier
-Override startup command;Startup Befehl überschreiben
-Loading;Wird geladen...
-Offline;Offline
-Connecting;Verbiden...
-Start;Start
-Restart;Neu Starten
-Stop;Stoppen
-Shared IP;Geteilte IP
-Server ID;Server ID
-Cpu;CPU
-Console;Console
-Files;Dateien
-Backups;Backups
-Network;Netzwerk
-Plugins;Plugins
-Settings;Einstellungen
-Enter command;Befehl eingeben
-Execute;Ausführen
-Checking disk space;Speicherplatz überprüfen
-Updating config files;Konfigurations-Dateien werden geupdatet
-Checking file permissions;Datei-Rechte werden überprüft
-Downloading server image;Server Image wird heruntergeladen
-Downloaded server image;Server Image wurde heruntergeladen
-Starting;Startet
-Online;Online
-Kill;Kill
-Stopping;Stoppt
-Search files and folders;Ordner und Dateien durchsuchen
-Launch WinSCP;WinSCP starten
-New folder;Neuer Ordner
-Upload;Hochladen
-File name;Datei-Name
-File size;Datei-Größe
-Last modified;Zuletz geändert
-Cancel;Abbrechen
-Canceling;Wird Abbgebrochen
-Running;Läuft
-Loading backups;Backups werden geladen
-Started backup creation;Backup wird erstellt
-Backup is going to be created;Backup wird erstellt werden
-Rename;Umbenennen
-Move;Bewegen
-Archive;Archivieren
-Unarchive;Archivieren rückgängig machen
-Download;Herunterladen
-Starting download;Herunterladen wird gestartet
-Backup successfully created;Backup wurde erfolgreich erstellt
-Restore;Wiederherstellen
-Copy url;URL Kopieren
-Backup deletion started;Backup löschung wird gestartet
-Backup successfully deleted;Backup wurde erfolgreich gelöscht
-Primary;Primärer
-This feature is currently not available;Diese Funktion ist zur Zeit nicht verfügbar
-Send;Senden
-Sending;Wird gesendet
-Welcome to the support chat. Ask your question here and we will help you;Willkommen in der Chat Hilfe. Stelle hier deine Frage und wir helfen dir.
- minutes ago; Minuten
-just now;gerade eben
-less than a minute ago;weniger als eine Minute
-1 hour ago;vor einer Stunde
-1 minute ago;vor einer Minute
-Failed;Fehlgeschlagen
- hours ago; Stunden
-Open tickets;Tickets öffnen
-Actions;Aktionen
-No support ticket is currently open;Kein Support Ticket ist zurzeit offen.
-User information;Benutzer-Information
-Close ticket;Ticket schließen
-Closing;Wird geschlossen...
-The support team has been notified. Please be patient;Das Support-Team wurde benachrichtigt. Habe etwas gedult
-The ticket is now closed. Type a message to open it again;Das Ticket wurde geschlossen. Schreibe etwas, um es wieder zu öffnen
-1 day ago;vor einem Tag
-is typing;schreibt...
-are typing;schreiben
-No domains available;Keine Domains verfügbar
-Shared domains;Geteilte Domains
-Shared domain;Geteilte Domain
-Shared domain successfully deleted;Geteilte Domain wurde erfolgreich gelöscht
-Shared domain successfully added;Geteilte Domain wurde erfolgreich hinzugefügt
-Domain name;Domain Name
-DNS records for;DNS-Record für
-Fetching dns records;Es wird nach DNS-Records gesucht
-No dns records found;Keine DNS-Records gefunden
-Content;Inhalt
-Priority;Priorität
-Ttl;Ttl
-Enable cloudflare proxy;Cloudflare-Proxy benutzen
-CF Proxy;CF Proxy
- days ago; Tage
-Cancle;Abbrechen
-An unexpected error occured;Ein unbekannter Fehler ist aufgetreten
-Testy;Testy
-Error from cloudflare api;Fehler von der Cloudflare-API
-Profile;Profil
-No subscription available;Kein Abo verfügbar
-Buy;Kaufen
-Redirecting;Weiterleiten
-Apply;Anwenden
-Applying code;Code Anwenden
-Invalid subscription code;Unbekannter Abo-Code
-Cancel Subscription;Abo beenden
-Active until;Aktiv bis
-We will send you a notification upon subscription expiration;Wenn dein Abo endet, senden wir dir eine E-Mail
-This token has been already used;Dieser Token wurde schon benutzt
-New login for;Neue Anmeldung für
-No records found for this day;Für diesen Tag wurden keine Records gefunden
-Change;Ändern
-Changing;Wird geändert
-Minecraft version;Minecraft Version
-Build version;Build Version
-Server installation is currently running;Der Server wird installiert.
-Selected;Ausgewählt
-Move deleted;Gelöschtest Bewegen
-Delete selected;Ausgewähltes löschen
-Log level;Log Level
-Log message;Log Message
-Time;Zeit
-Version;Version
-You are running moonlight version;Du benutzt die Moonlight-Version
-Operating system;Betriebssystem
-Moonlight is running on;Moonlight läuft auf
-Memory usage;Arbeitsspeicher-Auslastung
-Moonlight is using;Moonlight benutzt
-of memory;des Arbeitsspeichers
-Cpu usage;CPU Auslastung
-Refresh;Neu Laden
-Send a message to all users;Eine Nachricht an alle Benutzer senden
-IP;IP
-URL;URL
-Device;Gerät
-Change url;URL Ändern
-Message;Nachricht
-Enter message;Nachricht eingeben
-Enter the message to send;Eine Nachricht zum senden eingeben
-Confirm;Bestätigen
-Are you sure?;Bist du dir sicher
-Enter url;URL eingeben
-An unknown error occured while starting backup deletion;Ein unbekannter Fehler ist während der Backuplöschung aufgetreten
-Success;erfolgreich
-Backup URL successfully copied to your clipboard;Die Backup URL wurde in deine Zwischenablage kopiert
-Backup restore started;Backup wiederherstellung gestartet
-Backup successfully restored;Das Backup wurde erfolgreich wiedergeherstellt
-Register for;Registrieren für
-Core;Kern
-Logs;Logs
-AuditLog;AuditLog
-SecurityLog;SecurityLog
-ErrorLog;ErrorLog
-Resources;Resourcen
-WinSCP cannot be launched here;WinSCP kann nicht gestartet werden
-Create a new folder;Neuen Ordner erstellen
-Enter a name;Einen Namen eingeben
-File upload complete;Dateiupload abgeschlossen
-New server;Neuer Server
-Sessions;Sitzungen
-New user;Neuer Benutzer
-Created at;Erstellt am
-Mail template not found;E-Mail template wurde nicht gefunden
-Missing admin permissions. This attempt has been logged ;Fehlende Admin-Rechte. Dieser Versuch wurde aufgezeichnet
-Address;Addresse
-City;Stadt
-State;Land
-Country;Staat
-Totp;Totp
-Discord;Discord
-Subscription;Abonnement
-None;None
-No user with this id found;Kein Benutzer mit dieser ID gefunden
-Back to list;Zurück zur liste
-New domain;Neue domain
-Reset password;Password wiederherstellen
-Password reset;Password wiederherstellung
-Reset the password of your account;Password deines Accounts zurücksetzen
-Wrong here?;Falsch hier?
-A user with this email can not be found;Ein Benutzer mit dieser E-Mail konnte nicht gefunden werden
-Password reset successfull. Check your mail;Password wiederherstellung erfolgreich. Überprüfe deine Email
-Discord bot;Discord Bot
-New image;Neues Image
-Description;Beschreibung
-Uuid;UUID
-Enter tag name;Tag Namen eingeben
-Remove;Entfernen
-No tags found;Keine Tags gefunden
-Enter docker image name;Docker Image Namen eingeben
-Tags;Tags
-Docker images;Docker Images
-Default image;Standard Image
-Startup command;Startup Befehl
-Install container;Install container
-Install entry;Install entry
-Configuration files;Konfigurationsdateien
-Startup detection;Startuperkennung
-Stop command;Stopp-Befehl
-Successfully saved image;Das Image wurde erfolgreich gespeichert
-No docker images found;Keine Docker Images gefunden
-Key;Schlüssel
-Default value;Standardwert
-Allocations;Zuweisung
-No variables found;Keine Variablen gefunden
-Successfully added image;Das Image wurde erfolgreich hinzugefügt
-Password change for;Password ändern für
-of;von
-New node;Neues Node
-Fqdn;Fqdn
-Cores used;Kerne genutzt
-used;benutzt
-5.15.90.1-microsoft-standard-WSL2 - amd64;5.15.90.1-microsoft-standard-WSL2 - amd64
-Host system information;Host System Information
-0;0
-Docker containers running;Laufende Docker Container
-details;Details
-1;1
-2;2
-DDos;DDos
-No ddos attacks found;Keine DDoS gefunden
-Node;Node
-Date;Datum
-DDos attack started;DDos Attacke gestartet
-packets;Pakete
-DDos attack stopped;DDos Attacke gestoppt
- packets; Pakete
-Stop all;Alle Stoppen
-Kill all;Allen Killen
-Network in;Network in
-Network out;Network out
-Kill all servers;Alle Server Killen
-Do you really want to kill all running servers?;Möchtest du wirklich alle laufenden Server Killen?
-Change power state for;Power-State ändern für
-to;zu
-Stop all servers;Alle Server stoppen
-Do you really want to stop all running servers?;Möchtest du wirklich alle laufenden Server Killen?
-Manage ;Verwalten 
-Manage user ;Benutzer verwalten 
-Reloading;Neu Laden...
-Update;Aktualisieren
-Updating;Wird Aktualisiert...
-Successfully updated user;Benutzer erfolgreich aktualisiert
-Discord id;Discord ID
-Discord username;Discord Benutzername
-Discord discriminator;Discord Tag
-The Name field is required.;Der Name dieses Feldes ist erforderlich
-An error occured while logging you in;Ein Fehler ist aufgetreten, als du dich angemeldet hast
-You need to enter an email address;Du musst eine E-Mail-Adresse angeben
-You need to enter a password;Du musst ein Password eingeben
-You need to enter a password with minimum 8 characters in lenght;Du musst ein Password eingeben, das mindestens 8 Buchstaben lang ist
-Proccessing;Wird verarbeitet...
-The FirstName field is required.;Das Vorname-Feld ist erforderlich
-The LastName field is required.;Das Nachname-Feld ist erforderlich
-The Address field is required.;Das Address-Feld ist erforderlich
-The City field is required.;Das Stadt-Feld ist erforderlich
-The State field is required.;Das Staat-Feld ist erforderlich
-The Country field is required.;Das Land-Feld ist erforderlich
-Street and house number requered;Straße und Hausnummer erforderlich
-Max lenght reached;Maximale Länge erreicht
-Server;Server
-stopped;gestoppt
-Cleanups;Cleanups
-executed;ausgeführt
-Used clanup;Cleanup benutzt
-Enable;Aktivieren
-Disable;Deaktivieren
-Addons;Add-ons
-Javascript version;Javascript Version
-Javascript file;Javascript Datei
-Select javascript file to execute on start;Javascript Datei zum starten auswählen
-Submit;Einreichen
-Processing;Wird verarbeitet...
-Go up;Nach oben gehen
-Running cleanup;Cleanup läuft
-servers;Servers
-Select folder to move the file(s) to;Ordner zum Bewegen der Dateien auswählen
-Paper version;Paper Version
-Join2Start;Join2Start
-Server reset;Server zurücksetzen
-Reset;Zurücksetzen
-Resetting;Wird zurückgesetzt
-Are you sure you want to reset this server?;Möchtest du diesen Server wirklich zurücksetzen?
-Are you sure? This cannot be undone;Bist du dir sicher? Dies kann nicht rückgängig gemacht werden
-Resetting server;Server wird zurückgesetzt...
-Deleted file;Datei gelöscht
-Reinstalling server;Server wird reinstalliert
-Uploading files;Dateien wurden hochgeladen
-complete;vollständig
-Upload complete;Upload komplett
-Security;Sicherheit
-Subscriptions;Abonnements
-2fa Code;2FA Code
-Your account is secured with 2fa;Dein Account wird mit 2-FA gesichert
-anyone write a fancy text here?;hier einen schönen Text schreiben?
-Activate 2fa;2-FA Aktivieren
-2fa apps;2-FA Apps
-Use an app like ;Benutze eine App wie 
-or;oder
-and scan the following QR Code;und scanne diesen QR-Code
-If you have trouble using the QR Code, select manual input in the app and enter your email and the following code:;Wenn du Probleme mit dem Scannen des Qr-Codes has, benutze doch die Manuelle Eingabe der App und gib deine E-Mail und den folgenden Code ein:
-Finish activation;Aktivierung fertig
-2fa Code requiered;2-FA Code erforderlich
-New password;Neues Password
-Secure your account;Deinen Account sichern
-2fa adds another layer of security to your account. You have to enter a 6 digit code in order to login.;2-FA fügt eine weitere Sicherheits-Schicht hinzu. Du musst einen 6-Ziffern-Code eingeben, um dich anzumelden.
-New subscription;Neues Abonnement
-You need to enter a name;Du musst einen Namen eingeben
-You need to enter a description;Du musst eine Beschreibung eigeben
-Add new limit;Ein neues Limit hinzufügen
-Create subscription;Abonnement erstellen
-Options;Optionen
-Amount;Betrag
-Do you really want to delete it?;Möchtes du es wirklich löschen?
-Loading your subscription;Dein Abonnement wird geladen
-Searching for deploy node;Suche Node zum Aufsetzen
-Searching for available images;Nach verfügbaren Images wird gesucht
-Server details;Server Details
-Configure your server;Deinen Server konfigurieren
-Default;Standard
-You reached the maximum amount of servers for every image of your subscription;Du hast die maximale Anzahl an Images von deinem Abonnement erreicht.
-Personal information;Prsönliche Informationen
-Enter code;Code eingeben
-Server rename;Server Umbenennen
-Create code;Code erstellen
-Save subscription;Abonnement speichern
-Enter your information;Informationen eingeben
-You need to enter your full name in order to use moonlight;Du musst deinen ganzen Namen eingeben, um Moonlight zu nutzen
-No node found;Kein Node gefunden
-No node found to deploy to found;Keine Node zum Aufsetzen gefunden
-Node offline;Node offline
-The node the server is running on is currently offline;Die Node, auf dem der Server grat läuft, ist offline
-Server not found;Server konnte nicht gefunden werden
-A server with that id cannot be found or you have no access for this server;Ein Server mit dieser ID konnte nicht gefunden werden
-Compress;Komprimieren
-Decompress;De-Komprimieren
-Moving;Bewegen...
-Compressing;Komprimieren...
-selected;Ausgewählt
-New website;Neue Webseite
-Plesk servers;Plesk Servers
-Base domain;Base Domain
-Plesk server;Plesk Server
-Ftp;FTP
-No SSL certificate found;Keine SSL-Zertifikate gefunden
-Ftp Host;FTP Host
-Ftp Port;FTP Port
-Ftp Username;FTP Username
-Ftp Password;FTP Password
-Use;Benutzen
-SSL Certificates;SSL Zertifikate
-SSL certificates;SSL Zertifikate
-Issue certificate;SSL-Zertifikat Ausgeben
-New plesk server;Neuer Plesk Server
-Api url;API URL
-Host system offline;Host System Offline
-The host system the website is running on is currently offline;Das Host System, auf dem diese Webseite läuft, ist offline
-No SSL certificates found;Keine SSL-Zertifikate gefunden
-No databases found for this website;Dieser Webseite konnten keine Datenbanken zugeordnet werden
-The name should be at least 8 characters long;Der Name sollte mindestens 8 Zeichen lang sein
-The name should only contain of lower case characters and numbers;Der Name sollte nur Kleinbuchstaben und Zahlen enthalten
-Error from plesk;Error von Plesk
-Host;Host
-Username;Benutzername
-SRV records cannot be updated thanks to the cloudflare api client. Please delete the record and create a new one;SRV Records können aufgrund von Cloudflare nicht geupdatet werden. Bitte lösche den Record und erstelle einen neuen.
-The User field is required.;Das Benutzer-Feld ist erforderlich
-You need to specify a owner;Du musst einen Server-Besitzer angeben
-You need to specify a image;You need to specify a image
-Api Url;API URL
-Api Key;Api Key
-Duration;Dauer
-Enter duration of subscription;Dauer des Abonnements eingeben
-Copied code to clipboard;Code in die Zwischenablage kopiert
-Invalid or expired subscription code;Ungültiger oder Abgelaufener Abo-Code
-Current subscription;Dein Abonnement
-You need to specify a server image;Du musst ein Image angeben
-CPU;CPU
-Hour;Stunde
-Day;Tag
-Month;Monat
-Year;Jahr
-All time;Für immer
-This function is not implemented;Diese Funktion wurde noch nicht hinzugefügt
-Domain details;Domain Details
-Configure your domain;Deine Domain konfigurieren
-You reached the maximum amount of domains in your subscription;Du hast das Maximum an Domains in deinem Abonnement erreicht
-You need to specify a shared domain;Du musst eine Shared-Domain angeben
-A domain with this name does already exist for this shared domain;Eine Domain mit diesem Name existiert bereits in dieser Shared-Domain
-The Email field is required.;Das E-Mail-Feld ist erforderlich
-The Password field is required.;Das Password-Feld ist erforderlich
-The ConfirmPassword field is required.;Das Password-Bestätigen-Feld ist erforderlich
-Passwords need to match;Die Passwörter müssen übereinstimmen
-Cleanup exception;Cleanup ausnahme
-No shared domain found;Keine Shared-Domain gefunden
-Searching for deploy plesk server;Suchen um den Plesk Server aufzusetzen
-No plesk server found;Kein Plesk Server gefunden
-No plesk server found to deploy to;Keinen Plesk Server zum Aufsetzen gefunden
-No node found to deploy to;Kein Node zum Aufsetzen
-Website details;Details der Webseite
-Configure your website;Konfiguriere deine Webseite
-The name cannot be longer that 32 characters;Der Name kann nicht länger als 32 Zeichen sein
-The name should only consist of lower case characters;Der Name sollte nur aus Kleinbuchstaben bestehen
-News;Neuigkeiten
-Title...;Titel...
-Enter text...;Text einfügen...
-Saving...;Wird gespeichert...
-Deleting...;Wird gelöscht...
-Delete post;Post löschen
-Do you really want to delete the post ";Post löschen? "
-You have no domains;Du hast keine Domains
-We were not able to find any domains associated with your account;Wir haben keine Domains, die mit deinem Account verbunden sind, gefunden
-You have no websites;Du hast keine Webseites
-We were not able to find any websites associated with your account;Wir haben keine Webseiten, die mit deinem Account verbunden sind, gefunden
-Guest;Gast
-You need a domain;Du brauchts eine Domain
-New post;Neuer Post
-New entry;Neuer Eintrag
-You have no servers;Du hast keine Server
-We were not able to find any servers associated with your account;Wir haben keine Server, die mit deinem Account verbunden sind, gefunden
-Error creating server on wings;Fehler bei der Erstellung des Servers auf Wings
-An unknown error occured while restoring a backup;Ein unbekannter Fehler ist während der Backup-Wiederherstellung aufgetreten
-Error from daemon;Fehler vom Daemon
-End;Ende
-Cloud panel;Cloud Panel
-Cloud panels;Cloud Panels
-New cloud panel;Neues cloud Panel
-You need to enter an api key;Du musst einen API-Key eigeben
-Webspaces;Webspaces
-New webspace;Neuer Webspace
-The uploaded file should not be bigger than 100MB;DIe Datei sollte nicht größer als 100MB sein
-An unknown error occured while uploading a file;Ein unbekannter Fehler ist während dem Datei-Hochladen aufgetreten
-No databases found for this webspace;Keine Datenbanken für diesen Webspace gefunden
-Sftp;SFTP
-Sftp Host;Sftp Host
-Sftp Port;Sftp Port
-Sftp Username;Sftp Benutzername
-Sftp Password;Sftp Password
-Lets Encrypt certificate successfully issued;Lets Encrypt Zertifikat erfolgreich erstellt
-Add shared domain;Shared Domain Hinzufügen
-Webspace;Webspace
-You reached the maximum amount of websites in your subscription;Du hast das Maximum an Webseiten in deinem Abonnement erreicht
-Searching for deploy web host;Suchen um den Webhost aufzusetzen
-Webspace details;Webspace Details
-Web host;Web host
-Configure your webspaces;Konfiguriere deine Webspaces
-You reached the maximum amount of webspaces in your subscription;Du hast das Maximum an Webspaces in deinem Abonnement erreicht
-Create a webspace;Einen Webspace erstellen
-Manage your webspaces;Deine Webspaces verwalten
-Modify the content of your webspaces;Den Inhalt deiner Webspaces verwalten
-Successfully updated password;Password erfolgreich geupdatet
-An unknown error occured while sending your message;Ein unbekannter Fehler ist während dem Senden von deiner Nachricht aufgetreten
-Open chats;Offene Chats
-No message sent yet;Keine Nachrichten gesendet
-Support ticket open;Support-Ticket geöffnet
-Support ticket closed;Support-Ticket geschlossen
-Your connection has been paused;Deine Verbindung wurde pausiert
-We paused your connection because of inactivity. The resume just focus the tab and wait a few seconds;Wir haben deine Verbindung aufgrund von inaktivität pausiert. Wechsle auf den Tab und warte ein paar Sekunden
-Failed to reconnect to the moonlight servers;Die Wiederverbindung zu den Moonlight-Servern ist gescheitert
-We were unable to reconnect to moonlight. Please refresh the page;Verbindung zu Moonlight fehlgeschlagen. Bitte aktualisiere die Seite
-Failed to reconnect to the moonlight servers. The connection has been rejected;Die Wiederverbindung zu den Moonlight-Servern ist fehlgeschlagen. Die Verbindung wurde abgelehnt
-We were unable to reconnect to moonlight. Most of the time this is caused by an update of moonlight. Please refresh the page;Verbindung zu Moonlight fehlgeschlagen. Meistens wird dies durch eine Aktualisierung von Moonlight verursacht. Bitte aktualisieren Sie die Seite
-Verifying token, loading user data;Token verifizieren, Benutzer-Daten laden
-Reload config;Konfiguration neu laden
-Successfully reloading configuration;Konfiguration wird neu geladen...
-Successfully reloaded configuration;Die Konfiguration wurde erfolgreich neu geladen
-Flows;Flows
-Add node;Node Hinzufügen
-Web system;Web System
-Servers with this image;Server mit diesem Image
-You need to specify a user;Du musst einen Benutzer angeben
-Import;Importieren
-Export;Exportieren
-Exporting;Wird exportiert
-Successfully imported image;Das Image wurde erfolgreich importiert.
-Forge version;Forge Version
-Fabric version;Fabric Version
-Fabric loader version;Fabric Loader Version
-Rate;Rate
-Hey, can i borrow you for a second?;Hey, kann ich dich mal kurz ausleihen?
-We want to improve our services and get a little bit of feedback how we are currently doing. Please leave us a rating;Da wir unsere Dienste ständig verbessern, möchten wir dich um Feedback bitten. Bitte Bewerte uns:
-Thanks for your rating;Danke für deine Bewertun
-It would be really kind of you rating us on a external platform as it will help our project very much;Es wäre wirklich nett, wenn du uns auf einer externen Plattform bewerten würdest, denn das würde unserem Projekt sehr helfen
-Close;Schließen
-Rating saved;Bewretung gespeichert
-Group;Gruppe
-Beta;Beta
-Create a new group;Eine neue Gruppe erstellen
-Download WinSCP;WinSCP herunterladen
-Show connection details;Verbindungsdetails anzeigen
-New;Neu
-New file;Neue Datei
-Connection details;Verbindungsdetails
-Malware;Malware
-Create a new file;Eine neue Datei erstellen
-Edit layout;Layout anpassen
-Uptime;Laufzeit
-Moonlight is online since;Moonlight ist online seit
-User;Benutzer
-Databases;Datenbanken
-Sitzungen;Sitzungen
-Active users;Aktive Nutzer
-Search for plugins;Nach Plugins suchen
-Search;Suchen
-Searching;Wird gesucht...
-Successfully installed gunshell;Gunshell wurde erfolgreich installiert
-Successfully installed fastasyncworldedit;Fastasyncworldedit wurde erfolgreich installiert
-Successfully installed minimotd;Minimotd wurde erfolgreich installiert
-Moonlight health;Moonlight Status
-Healthy;Healthy
-Successfully saved file;Die Datei wurde erfolgreich gespeichert
-Unsorted servers;Unsortierte Server
-Enter a new name;Einen neuen Namen eingeben
-Sign in with;Anmelden mit
-Make your services accessible through your own domain;Mache deine Dienste mit einer Domain verfügbar
-New group;Neue Gruppe
-Finish editing layout;Layout Fertigstellen
-Remove group;Gruppe löschen
-Hidden in edit mode;Im Edit-Mode ausgeblendet
-Enter your 2fa code here;Deinen 2FA Code hier eingeben
-Two factor authentication;Zwei Faktor Authentifikation
-Preferences;Preferenzen
-Streamer mode;Streamer Modus
-Scan the QR code and enter the code generated by the app you have scanned it in;Scanne den QR-Code mit der App
-Start scan;Scan Starten
-Results;Ergebnisse
-Scan in progress;Wird gescannt
-Debug;Debug
-Save changes;Änderungen speichern
-Delete domain;Domain löschen
-Python version;Python Version
-Python file;Python Datei
-Select python file to execute on start;Wähle die Python Datei aus, die beim Start ausgeführt wird
-You have no webspaces;Du hast keine Webspaces
-We were not able to find any webspaces associated with your account;Wir haben keine Webspaces gefunden, die mit deinem Account verbunden sind
-Backup download successfully started;Der Backup-Download wurde erfolgreich gestartet
-Error from cloud panel;Fehler vom Cloud Panel
-Error from wings;Fehler von Wings
-Remove link;Link entfernen
-Your account is linked to a discord account;Dein Account ist mit einem Discord Account verbunden
-You are able to use features like the discord bot of moonlight;Du kannst Features wie den Discord Bot von Moonlight benutzen
-The password should be at least 8 characters long;Dein Password sollte mindestens 8 Zeichen lang sein
-The name should only consist of lower case characters or numbers;Der Name sollte nur aus Kleinbuchstaben und Nummern bestehen
-The requested resource was not found;Die gewünschte Ressource wurde nicht gefunden
-We were not able to find the requested resource. This can have following reasons;Wir haben die gewünschte Ressource nicht gefunden. Das kann folgenden Grund haben
-The resource was deleted;Die Ressource wurde gelöscht
-You have to permission to access this resource;Du hast keine Rechte um auf diese Ressource zuzugreifen
-You may have entered invalid data;Du hast ungültige Daten angegeben
-A unknown bug occured;Ein unbekannter Bug ist aufgetreten
-An api was down and not proper handled;Eine API war offline und wurde nicht richtig behandelt
-A database with this name does already exist;Es gibt bereits eine Datenbank mit diesem Namen
-Successfully installed quickshop-hikari;Quickshop-Hikari wurde erfolgreich installiert
-You need to enter a valid domain;Du musst eine gültige Domain angeben
-2fa code;2FA Code
-This feature is not available for;Dieses Feature ist nicht verfügbar für
-Your account is currently not linked to discord;Dein Account ist nicht mit Discord verbunden
-To use features like the discord bot, link your moonlight account with your discord account;Um Features wie Moonlights Discord Bot nutzen zu können, verbinde deinen Account mit Discord
-Link account;Account verbinden
-Continue;Weiter
-Preparing;Wird vorbereitet
-Make sure you have installed one of the following apps on your smartphone and press continue;Stelle sicher dass du eine der folgenden Apps auf deinem Smartphone installiert hast und drücke auf Weiter
-The max lenght for the name is 32 characters;Die maximale Länge für den Namen beträgt 32 Zeichen
-Successfully installed chunky;Chunky wurde erfolgreich installiert
-Successfully installed huskhomes;Huskhomes wurde erfolgreich installiert
-Successfully installed simply-farming;Simple-Farming wurde erfolgreich installiert
-You need to specify a first name;Du musst einen Vornamen eingeben
-You need to specify a last name;Du musst einen Nachnamen angeben
-You need to specify a password;Du musst ein Password angeben
-Please solve the captcha;Bitte löse das Captcha
-The email is already in use;Diese Email wird bereits verwendet
-The dns records of your webspace do not point to the host system;Die DNS Records deines Webspaces zeigen nicht zum Hostsystem
-Scan complete;Scan komplett
-Currently scanning:;Zurzeit wird gescannt:
-Successfully installed dynmap;Dynmap wurde erfolgreich installiert
-Successfully installed squaremap;Squaremap wurde erfolgreich installiert
-No web host found;Kein Webhost gefunden
-No web host found to deploy to;Es wurde kein Webhost zum Aufsetzen gefunden
-Successfully installed sleeper;Sleeper wurde erfolgreich installiert
-You need to enter a domain;Du musst eine Domain angeben
-Enter a ip;Eine IP angeben
-Ip Bans;Ip Bans
-Ip;Ip
-Successfully installed simple-voice-chat;Simple-Voice-Chat wurde erfolgreich installiert
-Successfully installed smithing-table-fix;Smithing-Table-Fix wurde erfolgreich installiert
-Successfully installed justplayer-tpa;Justplayer-TPA wurde erfolgreich installiert
-Successfully installed ishop;iShop wurde erfolgreich installiert
-Successfully installed lifestealre;Lifestealre wurde erfolgreich installiert
-Successfully installed lifeswap;Lifeswap wurde erfolgreich installiert
-Java version;Java Version
-Jar file;JAR Datei
-Select jar to execute on start;Eine JAR Datei auswählen, die beim Start ausgeführt wird
-A website with this domain does already exist;Eine Webseite mit dieser Domain existiert bereits
-Successfully installed discordsrv;DiscordSrv wurde erfolgreich installiert
-Reinstall;Reinstallieren
-Reinstalling;Wird Reinstalliert
-Successfully installed freedomchat;Freedomchat wurde erfolgreich installiert
-Leave empty for the default background image;Für das Standard Hintergrundbild freilassen
-Background image url;URL zum Hintergrundbild
-of CPU used;von der CPU wird benutzt
-memory used;Speicher benutzt
-163;163
-172;172
-Sentry;Sentry
-Sentry is enabled;Sentry ist eingeschaltet
-Successfully installed mobis-homes;Mobis-Homes wurde erfolgreich installiert
-Your moonlight account is disabled;Dein Moonlight Account ist deaktiviert
-Your moonlight account is currently disabled. But dont worry your data is still saved;Dein Moonlight Account ist deaktiviert, aber keine Sorge, deine Daten sind noch gespeichert
-You need to specify a email address;Du musst eine Email Adresse angeben
-A user with that email does already exist;Ein Nutzer mit dieser Email Adresse existiert bereits
-Successfully installed buildmode;Buildmode wurde erfolgreich installiert
-Successfully installed plasmo-voice;Plasmo-Voice wurde erfolgreich installiert
-157;157
-174;174
-158;158
-Webspace not found;Webspace nicht gefunden
-A webspace with that id cannot be found or you have no access for this webspace;Ein Webspace mit dieser ID wurde nicht gefunden, oder du hast keinen Zugriff auf diesen Webspace
-No plugin download for your minecraft version found;Kein Plugin für deine Minecraft-Version gefunden
-Successfully installed gamemode-alias;Gamemode-Alias wurde erfolgreich installiert
-228;228
-User;Nutzer
-Send notification;Benachrichtigung senden
-Successfully saved changes;Änderungen erfolgreich gespeichert
-Archiving;Wird Archiviert
-Server is currently not archived;Dieser Server ist zurzeit nicht Archiviert
-Add allocation;Zuweisung hinzufügen
-231;231
-175;175
-Dotnet version;Dotnet Version
-Dll file;DLL Datei
-Select dll to execute on start;Wähle die DLL Datei aus, die beim Start ausgeführt wird 

+ 0 - 567
Moonlight/defaultstorage/resources/lang/en_us.lang

@@ -1,567 +0,0 @@
-Open support;Open support
-About us;About us
-Imprint;Imprint
-Privacy;Privacy
-Login;Login
-Register;Register
-Insert brand name...;Insert brand name...
-Save and continue;Save and continue
-Saving;Saving
-Configure basics;Configure basics
-Brand name;Brand name
-test;test
-Insert first name...;Insert first name...
-Insert last name...;Insert last name...
-Insert email address...;Insert email address...
-Add;Add
-Adding...;Adding...
-Add admin accounts;Add admin accounts
-First name;First name
-Last name;Last name
-Email address;Email address
-Enter password;Enter password
-Next;Next
-Back;Back
-Configure features;Configure features
-Support chat;Support chat
-Finish;Finish
-Finalize installation;Finalize installation
-Moonlight basic settings successfully configured;Moonlight basic settings successfully configured
-Ooops. This page is crashed;Ooops. This page crashed
-This page is crashed. The error has been reported to the moonlight team. Meanwhile you can try reloading the page;This page crashed. The error has been reported to the moonlight team. Meanwhile you can try reloading the page
-Setup complete;Setup complete
-It looks like this moonlight instance is ready to go;It looks like this moonlight instance is ready to go
-User successfully created;User successfully created
-Ooops. Your moonlight client is crashed;Ooops. Your moonlight client crashed
-This error has been reported to the moonlight team;This error has been reported to the moonlight team
-Sign In;Sign In
-Sign in to start with moonlight;Sign in to start with moonlight
-Sign in with Discord;Sign in with Discord
-Or with email;Or with email
-Forgot password?;Forgot password?
-Sign-in;Sign-in
-Not registered yet?;Not registered yet?
-Sign up;Sign up
-Authenticating;Authenticating
-Sign in with Google;Sign in with Google
-Working;Working
-Error;Error
-Email and password combination not found;Email and password combination not found
-Email;Email
-Password;Password
-Account settings;Account settings
-Logout;Logout
-Dashboard;Dashboard
-Order;Order
-Website;Website
-Database;Database
-Domain;Domain
-Servers;Servers
-Websites;Websites
-Databases;Databases
-Domains;Domains
-Changelog;Changelog
-Firstname;First Name
-Lastname;Last Name
-Repeat password;Repeat password
-Sign Up;Sign Up
-Sign up to start with moonlight;Sign up to start with moonlight
-Sign up with Discord;Sign up with Discord
-Sign up with Google;Sign up with Google
-Sign-up;Sign-up
-Already registered?;Already registered?
-Sign in;Sign in
-Create something new;Create something new
-Create a gameserver;Create a gameserver
-A new gameserver in just a few minutes;A new gameserver in just a few minutes
-Create a database;Create a database
-A quick way to store your data and manage it from all around the world;A quick way to store your data and manage it from all around the world
-Manage your services;Manage your services
-Manage your gameservers;Manage your gameservers
-Adjust your gameservers;Adjust your gameservers
-Manage your databases;Manage your databases
-Insert, delete and update the data in your databases;Insert, delete and update the data in your databases
-Create a website;Create a website
-Make your own websites with a webspace;Make your own websites with a webspace
-Create a domain;Create a domain
-Make your servvices accessible throught your own domain;Make your services accessible throught your own domain
-Manage your websites;Manage your websites
-Modify the content of your websites;Modify the content of your websites
-Manage your domains;Manage your domains
-Add, edit and delete dns records;Add, edit and delete dns records
-Admin;Admin
-System;System
-Overview;Overview
-Manager;Manager
-Cleanup;Cleanup
-Nodes;Nodes
-Images;Images
-aaPanel;aaPanel
-Users;Users
-Support;Support
-Statistics;Statistics
-No nodes found. Start with adding a new node;No nodes found. Start with adding a new node
-Nodename;Nodename
-FQDN;FQDN
-Create;Create
-Creating;Creating
-Http port;Http port
-Sftp port;Sftp port
-Moonlight daemon port;Moonlight daemon port
-SSL;SSL
-CPU Usage;CPU Usage
-In %;In %
-Memory;Memory
-Used / Available memory;Used / Available memory
-Storage;Storage
-Available storage;Available storage
-Add a new node;Add a new node
-Delete;Delete
-Deleting;Deleting
-Edit;Edit
-Token Id;Token Id
-Token;Token
-Save;Save
-Setup;Setup
-Open a ssh connection to your node and enter;Open a ssh connection to your node and enter
-and paste the config below. Then press STRG+O and STRG+X to save;and paste the config below. Then press STRG+O and STRG+X to save
-Before configuring this node, install the daemon;Before configuring this node, install the daemon
-Delete this node?;Delete this node?
-Do you really want to delete this node;Do you really want to delete this node
-Yes;Yes
-No;No
-Status;Status
-Adding;Adding
-Port;Port
-Id;Id
-Manage;Manage
-Create new server;Create new server
-No servers found;No servers found
-Server name;Server name
-Cpu cores;Cpu cores
-Disk;Disk
-Image;Image
-Override startup;Override startup
-Docker image;Docker image
-CPU Cores (100% = 1 Core);CPU Cores (100% = 1 Core)
-Server successfully created;Server successfully created
-Name;Name
-Cores;Cores
-Owner;Owner
-Value;Value
-An unknown error occured;An unknown error occured
-No allocation found;No allocation found
-Identifier;Identifier
-UuidIdentifier;UuidIdentifier
-Override startup command;Override startup command
-Loading;Loading
-Offline;Offline
-Connecting;Connecting
-Start;Start
-Restart;Restart
-Stop;Stop
-Shared IP;Shared IP
-Server ID;Server ID
-Cpu;Cpu
-Console;Console
-Files;Files
-Backups;Backups
-Network;Network
-Plugins;Plugins
-Settings;Settings
-Enter command;Enter command
-Execute;Execute
-Checking disk space;Checking disk space
-Updating config files;Updating config files
-Checking file permissions;Checking file permissions
-Downloading server image;Downloading server image
-Downloaded server image;Downloaded server image
-Starting;Starting
-Online;Online
-Kill;Kill
-Stopping;Stopping
-Search files and folders;Search files and folders
-Launch WinSCP;Launch WinSCP
-New folder;New folder
-Upload;Upload
-File name;File name
-File size;File size
-Last modified;Last modified
-Cancel;Cancel
-Canceling;Canceling
-Running;Running
-Loading backups;Loading backups
-Started backup creation;Started backup creation
-Backup is going to be created;Backup is going to be created
-Rename;Rename
-Move;Move
-Archive;Archive
-Unarchive;Unarchive
-Download;Download
-Starting download;Starting download
-Backup successfully created;Backup successfully created
-Restore;Restore
-Copy url;Copy url
-Backup deletion started;Backup deletion started
-Backup successfully deleted;Backup successfully deleted
-Primary;Primary
-This feature is currently not available;This feature is currently not available
-Send;Send
-Sending;Sending
-Welcome to the support chat. Ask your question here and we will help you;Welcome to the support chat. Ask your question here and we will help you
- minutes ago; minutes ago
-just now;just now
-less than a minute ago;less than a minute ago
-1 hour ago;1 hour ago
-1 minute ago;1 minute ago
-Failed;Failed
- hours ago; hours ago
-Open tickets;Open tickets
-Actions;Actions
-No support ticket is currently open;No support ticket is currently open
-User information;User information
-Close ticket;Close ticket
-Closing;Closing
-The support team has been notified. Please be patient;The support team has been notified. Please be patient
-The ticket is now closed. Type a message to open it again;The ticket is now closed. Type a message to open it again
-1 day ago;1 day ago
-is typing;is typing
-are typing;are typing
-No domains available;No domains available
-Shared domains;Shared domains
-Shared domain;Shared domain
-Shared domain successfully deleted;Shared domain successfully deleted
-Shared domain successfully added;Shared domain successfully added
-Domain name;Domain name
-DNS records for;DNS records for 
-Fetching dns records;Fetching dns records
-No dns records found;No dns records found
-Content;Content
-Priority;Priority
-Ttl;Ttl
-Enable cloudflare proxy;Enable cloudflare proxy
-CF Proxy;CF Proxy
- days ago; days ago
-Cancle;Cancle
-An unexpected error occured;An unexpected error occured
-Testy;Testy
-Error from cloudflare api;Error from cloudflare api
-Profile;Profile
-No subscription available;No subscription available
-Buy;Buy
-Redirecting;Redirecting
-Apply;Apply
-Applying code;Applying code
-Invalid subscription code;Invalid subscription code
-Cancel Subscription;Cancel Subscription
-Active until;Active until
-We will send you a notification upon subscription expiration;We will send you a notification upon subscription expiration
-This token has been already used;This token has been already used
-New login for;New login for
-No records found for this day;No records found for this day
-Change;Change
-Changing;Changing
-Minecraft version;Minecraft version
-Build version;Build version
-Server installation is currently running;Server installation is currently running
-Selected;Selected
-Move deleted;Move deleted
-Delete selected;Delete selected
-Log level;Log level
-Log message;Log message
-Time;Time
-Version;Version
-You are running moonlight version;You are running moonlight version
-Operating system;Operating system
-Moonlight is running on;Moonlight is running on
-Memory usage;Memory usage
-Moonlight is using;Moonlight is using
-of memory;of memory
-Cpu usage;Cpu usage
-Refresh;Refresh
-Send a message to all users;Send a message to all users
-IP;IP
-URL;URL
-Device;Device
-Change url;Change url
-Message;Message
-Enter message;Enter message
-Enter the message to send;Enter the message to send
-Confirm;Confirm
-Are you sure?;Are you sure?
-Enter url;Enter url
-An unknown error occured while starting backup deletion;An unknown error occured while starting backup deletion
-Success;Success
-Backup URL successfully copied to your clipboard;Backup URL successfully copied to your clipboard
-Backup restore started;Backup restore started
-Backup successfully restored;Backup successfully restored
-Register for;Register for
-Core;Core
-Logs;Logs
-AuditLog;AuditLog
-SecurityLog;SecurityLog
-ErrorLog;ErrorLog
-Resources;Resources
-WinSCP cannot be launched here;WinSCP cannot be launched here
-Create a new folder;Create a new folder
-Enter a name;Enter a name
-File upload complete;File upload complete
-New server;New server
-Sessions;Sessions
-New user;New user
-Created at;Created at
-Mail template not found;Mail template not found
-Missing admin permissions. This attempt has been logged ;)
-Address;Address
-City;City
-State;State
-Country;Country
-Totp;Totp
-Discord;Discord
-Subscription;Subscription
-None;None
-No user with this id found;No user with this id found
-Back to list;Back to list
-New domain;New domain
-Reset password;Reset password
-Password reset;Password reset
-Reset the password of your account;Reset the password of your account
-Wrong here?;Wrong here?
-A user with this email can not be found;A user with this email can not be found
-Passwort reset successfull. Check your mail;Passwort reset successfull. Check your mail
-Discord bot;Discord bot
-New image;New image
-Description;Description
-Uuid;Uuid
-Enter tag name;Enter tag name
-Remove;Remove
-No tags found;No tags found
-Enter docker image name;Enter docker image name
-Tags;Tags
-Docker images;Docker images
-Default image;Default image
-Startup command;Startup command
-Install container;Install container
-Install entry;Install entry
-Configuration files;Configuration files
-Startup detection;Startup detection
-Stop command;Stop command
-Successfully saved image;Successfully saved image
-No docker images found;No docker images found
-Key;Key
-Default value;Default value
-Allocations;Allocations
-No variables found;No variables found
-Successfully added image;Successfully added image
-Password change for;Password change for
-of;of
-New node;New node
-Fqdn;Fqdn
-Cores used;Cores used
-used;used
-5.15.90.1-microsoft-standard-WSL2 - amd64;5.15.90.1-microsoft-standard-WSL2 - amd64
-Host system information;Host system information
-0;0
-Docker containers running;Docker containers running
-details;details
-1;1
-2;2
-DDos;DDos
-No ddos attacks found;No ddos attacks found
-Node;Node
-Date;Date
-DDos attack started;DDos attack started
-packets;packets
-DDos attack stopped;DDos attack stopped
- packets; packets
-Stop all;Stop all
-Kill all;Kill all
-Network in;Network in
-Network out;Network out
-Kill all servers;Kill all servers
-Do you really want to kill all running servers?;Do you really want to kill all running servers?
-Change power state for;Change power state for
-to;to
-Stop all servers;Stop all servers
-Do you really want to stop all running servers?;Do you really want to stop all running servers?
-Manage ;Manage 
-Manage user ;Manage user 
-Reloading;Reloading
-Update;Update
-Updating;Updating
-Successfully updated user;Successfully updated user
-Discord id;Discord id
-Discord username;Discord username
-Discord discriminator;Discord discriminator
-The Name field is required.;The Name field is required.
-An error occured while logging you in;An error occured while logging you in
-You need to enter an email address;You need to enter an email address
-You need to enter a password;You need to enter a password
-You need to enter a password with minimum 8 characters in lenght;You need to enter a password with minimum 8 characters in length
-Proccessing;Proccessing
-The FirstName field is required.;The FirstName field is required.
-The LastName field is required.;The LastName field is required.
-The Address field is required.;The Address field is required.
-The City field is required.;The City field is required.
-The State field is required.;The State field is required.
-The Country field is required.;The Country field is required.
-Street and house number requered;Street and house number requered
-Max lenght reached;Max lenght reached
-Server;Server
-stopped;stopped
-Cleanups;Cleanups
-executed;executed
-Used clanup;Used clanup
-Enable;Enable
-Disabble;Disabble
-Disable;Disable
-Addons;Addons
-Javascript version;Javascript version
-Javascript file;Javascript file
-Select javascript file to execute on start;Select javascript file to execute on start
-Submit;Submit
-Processing;Processing
-Go up;Go up
-Running cleanup;Running cleanup
-servers;servers
-Select folder to move the file(s) to;Select folder to move the file(s) to
-Paper version;Paper version
-Join2Start;Join2Start
-Server reset;Server reset
-Reset;Reset
-Resetting;Resetting
-Are you sure you want to reset this server?;Are you sure you want to reset this server?
-Are you sure? This cannot be undone;Are you sure? This cannot be undone
-Resetting server;Resetting server
-Deleted file;Deleted file
-Reinstalling server;Reinstalling server
-Uploading files;Uploading files
-complete;complete
-Upload complete;Upload complete
-Security;Security
-Subscriptions;Subscriptions
-2fa Code;2fa Code
-Your account is secured with 2fa;Your account is secured with 2fa
-anyone write a fancy text here?;anyone write a fancy text here?
-Activate 2fa;Activate 2fa
-2fa apps;2fa apps
-Use an app like ;Use an app like 
-or;or
-and scan the following QR Code;and scan the following QR Code
-If you have trouble using the QR Code, select manual input in the app and enter your email and the following code:;If you have trouble using the QR Code, select manual input in the app and enter your email and the following code:
-Finish activation;Finish activation
-2fa Code requiered;2fa Code requiered
-New password;New password
-Secure your account;Secure your account
-2fa adds another layer of security to your account. You have to enter a 6 digit code in order to login.;2fa adds another layer of security to your account. You have to enter a 6 digit code in order to login.
-New subscription;New subscription
-You need to enter a name;You need to enter a name
-You need to enter a description;You need to enter a description
-Add new limit;Add new limit
-Create subscription;Create subscription
-Options;Options
-Amount;Amount
-Do you really want to delete it?;Do you really want to delete it?
-Loading your subscription;Loading your subscription
-Searching for deploy node;Searching for deploy node
-Searching for available images;Searching for available images
-Server details;Server details
-Configure your server;Configure your server
-Default;Default
-You reached the maximum amount of servers for every image of your subscription;You reached the maximum amount of servers for every image of your subscription
-Personal information;Personal information
-Enter code;Enter code
-Server rename;Server rename
-Create code;Create code
-Save subscription;Save subscription
-Enter your information;Enter your information
-You need to enter your full name in order to use moonlight;You need to enter your full name in order to use moonlight
-No node found;No node found
-No node found to deploy to found;No node found to deploy to found
-Node offline;Node offline
-The node the server is running on is currently offline;The node the server is running on is currently offline
-Server not found;Server not found
-A server with that id cannot be found or you have no access for this server;A server with that id cannot be found or you don't have access for this server
-Compress;Compress
-Decompress;Decompress
-Moving;Moving
-Compressing;Compressing
-selected;selected
-New website;New website
-Plesk servers;Plesk servers
-Base domain;Base domain
-Plesk server;Plesk server
-Ftp;Ftp
-No SSL certificate found;No SSL certificate found
-Ftp Host;Ftp Host
-Ftp Port;Ftp Port
-Ftp Username;Ftp Username
-Ftp Password;Ftp Password
-Use;Use
-SSL Certificates;SSL Certificates
-SSL certificates;SSL certificates
-Issue certificate;Issue certificate
-New plesk server;New plesk server
-Api url;Api url
-Host system offline;Host system offline
-The host system the website is running on is currently offline;The host system the website is running on is currently offline
-No SSL certificates found;No SSL certificates found
-No databases found for this website;No databases found for this website
-The name should be at least 8 characters long;The name should be at least 8 characters long
-The name should only contain of lower case characters and numbers;The name should only contain of lower case characters and numbers
-Error from plesk;Error from plesk
-Host;Host
-Username;Username
-SRV records cannot be updated thanks to the cloudflare api client. Please delete the record and create a new one;SRV records cannot be updated thanks to the cloudflare api client. Please delete the record and create a new one
-The User field is required.;The User field is required.
-You need to specify a owner;You need to specify a owner
-You need to specify a image;You need to specify a image
-Api Url;Api Url
-Api Key;Api Key
-Duration;Duration
-Enter duration of subscription;Enter duration of subscription
-Copied code to clipboard;Copied code to clipboard
-Invalid or expired subscription code;Invalid or expired subscription code
-Current subscription;Current subscription
-You need to specify a server image;You need to specify a server image
-CPU;CPU
-Hour;Hour
-Day;Day
-Month;Month
-Year;Year
-All time;All time
-This function is not implemented;This function is not implemented
-Domain details;Domain details
-Configure your domain;Configure your domain
-You reached the maximum amount of domains in your subscription;You reached the maximum amount of domains in your subscription
-You need to specify a shared domain;You need to specify a shared domain
-A domain with this name does already exist for this shared domain;A domain with this name does already exist for this shared domain
-The Email field is required.;The Email field is required.
-The Password field is required.;The Password field is required.
-The ConfirmPassword field is required.;The ConfirmPassword field is required.
-Passwords need to match;Passwords need to match
-Cleanup exception;Cleanup exception
-No shared domain found;No shared domain found
-Searching for deploy plesk server;Searching for deploy plesk server
-No plesk server found;No plesk server found
-No plesk server found to deploy to;No plesk server found to deploy to
-No node found to deploy to;No node found to deploy to
-Website details;Website details
-Configure your website;Configure your website
-The name cannot be longer that 32 characters;The name cannot be longer that 32 characters
-The name should only consist of lower case characters;The name should only consist of lower case characters
-News;News
-Title...;Title...
-Enter text...;Enter text...
-Saving...;Saving...
-Deleting...;Deleting...
-Delete post;Delete post
-Do you really want to delete the post ";Do you really want to delete the post "
-You have no domains;You don't have domains
-We were not able to find any domains associated with your account;We were not able to find any domains associated with your account
-You have no websites;You have no websites
-We were not able to find any websites associated with your account;We were not able to find any websites associated with your account
-Guest;Guest
-You need a domain;You need a domain
-New post;New post
-New entry;New entry

+ 0 - 778
Moonlight/defaultstorage/resources/lang/fr_fr.lang

@@ -1,778 +0,0 @@
-Open support;Assistance ouverte
-About us;À propos de nous
-Imprint;Imprimer
-Privacy;Confidentialité
-Login;Se connecter
-Register;S'enregistrer
-Insert brand name...;Insérer le nom de la marque...
-Save and continue;Sauvegarder et continuer
-Saving;Sauvegarde
-Configure basics;Configurer les bases
-Brand name;Nom de l'entreprise
-test;test
-Insert first name...;Insérer le prénom...
-Insert last name...;Insérer le nom de famille...
-Insert email address...;Insérer l'adresse e-mail...
-Add;Ajouter
-Adding...;Ajout...
-Add admin accounts;Ajouter des comptes administrateur
-First name;Prénom
-Last name;Nom de famille
-Email address;Adresse e-mail
-Enter password;Entrer le mot de passe
-Next;Suivant
-Back;Retour
-Configure features;Configurer les fonctionnalités
-Support chat;Chat d'assistance
-Finish;Finition
-Finalize installation;Finaliser l'installation
-Moonlight basic settings successfully configured;Paramètres de base Moonlight configurés avec succès
-Ooops. This page is crashed;Oups. Cette page est plantée.
-This page is crashed. The error has been reported to the moonlight team. Meanwhile you can try reloading the page;Cette page est plantée. L'erreur a été signalée à l'équipe Moonlight. En attendant, vous pouvez essayer de recharger la page
-Setup complete;Configuration terminée
-It looks like this moonlight instance is ready to go;Cette instance Moonlight est prête
-User successfully created;Utilisateur créé avec succès
-Ooops. Your moonlight client is crashed;Oups. Votre client moonlight est planté
-This error has been reported to the moonlight team;Cette erreur a été signalée à l'équipe Moonlight
-Sign In;Se connecter
-Sign in to start with moonlight;Connectez-vous pour commencer avec Moonlight
-Sign in with Discord;Connectez-vous avec Discord
-Or with email;Ou avec e-mail
-Forgot password?;Mot de passe oublié?
-Sign-in;Se connecter
-Not registered yet?;Pas encore inscrit?
-Sign up;S'inscrire
-Authenticating;Authentification...
-Sign in with Google;Connectez-vous avec Google
-Working;En fonctionnement...
-Error;Erreur
-Email and password combination not found;Combinaison e-mail et mot de passe introuvable
-Email;E-mail
-Password;Mot de passe
-Account settings;Paramètres du compte
-Logout;Se déconnecter
-Dashboard;Tableau de bord
-Order;Commande
-Website;Site internet
-Database;Base de données
-Domain;Domaine
-Servers;Serveurs
-Websites;Sites Internet
-Databases;Bases de données
-Domains;Domaines
-Changelog;Journal des modifications
-Firstname;Prénom
-Lastname;Nom de famille
-Repeat password;Répéter le mot de passe
-Sign Up;S'inscrire
-Sign up to start with moonlight;Inscrivez-vous pour commencer avec moonlight
-Sign up with Discord;Inscrivez-vous avec Discord
-Sign up with Google;S'inscrire avec Google
-Sign-up;S'inscrire
-Already registered?;Déjà enregistré?
-Sign in;Se connecter
-Create something new;Créer quelque chose de nouveau
-Create a gameserver;Créer un serveur de jeu
-A new gameserver in just a few minutes;Un nouveau serveur de jeu en quelques minutes
-Create a database;Créer une base de données
-A quick way to store your data and manage it from all around the world;Un moyen rapide de stocker vos données et de les gérer partout dans le monde
-Manage your services;Gérez vos prestations
-Manage your gameservers;Gérez vos serveurs de jeux
-Adjust your gameservers;Ajustez vos serveurs de jeu
-Manage your databases;Gérez vos bases de données
-Insert, delete and update the data in your databases;Insérer, supprimer et mettre à jour les données dans vos bases de données
-Create a website;Créer un site internet
-Make your own websites with a webspace;Créez vos propres sites internet avec un espace Web
-Create a domain;Créer un domaine
-Make your servvices accessible throught your own domain;Rendez vos services accessibles via votre propre domaine
-Manage your websites;Gérer vos sites internet
-Modify the content of your websites;Modifier le contenu de vos sites internet
-Manage your domains;Gérez vos domaines
-Add, edit and delete dns records;Ajouter, modifier et supprimer des enregistrements DNS
-Admin;Admin
-System;Système
-Overview;Aperçu
-Manager;Manager
-Cleanup;Nettoyer
-Nodes;Nœuds
-Images;Images
-aaPanel;aaPanel
-Users;Utilisateurs
-Support;Assistance
-Statistics;Statistiques
-No nodes found. Start with adding a new node;Aucun nœud trouvé. Commencez par ajouter un nouveau nœud
-Nodename;Nom du nœud
-FQDN;Nom de domaine complet
-Create;Créer
-Creating;Création...
-Http port;Port HTTP
-Sftp port;Port Sftp
-Moonlight daemon port;Port du daemon Moonlight
-SSL;SSL
-CPU Usage;L'utilisation du processeur
-In %;En %
-Memory;Mémoire
-Used / Available memory;Mémoire utilisée / disponible
-Storage;Stockage
-Available storage;Stockage disponible
-Add a new node;Ajouter un nouveau nœud
-Delete;Supprimer
-Deleting;Suppression...
-Edit;Modifier
-Token Id;Token Id
-Token;Token
-Save;Sauvegarder
-Setup;Configuration
-Open a ssh connection to your node and enter;Ouvrez une connexion ssh à votre nœud et entrez
-and paste the config below. Then press STRG+O and STRG+X to save;et collez la configuration ci-dessous. Appuyez ensuite sur CTRL+O et CTRL+X pour enregistrer
-Before configuring this node, install the daemon;Avant de configurer ce nœud, installez le deamon
-Delete this node?;Supprimer ce nœud ?
-Do you really want to delete this node;Voulez-vous vraiment supprimer ce nœud?
-Yes;Oui
-No;Non
-Status;Statut
-Adding;Ajout
-Port;Port
-Id;Id
-Manage;Gérer
-Create new server;Créer un nouveau serveur
-No servers found;Aucun serveur trouvé
-Server name;Nom du serveur
-Cpu cores;Cœurs de processeur
-Disk;Stockage
-Image;Image
-Override startup;Ignorer le démarrage
-Docker image;Image Docker
-CPU Cores (100% = 1 Core);Cœurs de processeur (100% = 1 cœur)
-Server successfully created;Serveur créé avec succès
-Name;Nom
-Cores;Noyaux
-Owner;Propriétaire
-Value;Valeur
-An unknown error occured;Une erreur inconnue s'est produite
-No allocation found;Aucune attribution trouvée
-Identifier;Identifiant
-UuidIdentifier;Identifiant Uuid
-Override startup command;Remplacer la commande de démarrage
-Loading;Chargement...
-Offline;Hors ligne
-Connecting;Connexion...
-Start;Démarrer
-Restart;Redémarrer
-Stop;Arrêt
-Shared IP;IP partagée
-Server ID;Identifiant du serveur
-Cpu;CPU
-Console;Console
-Files;Fichiers
-Backups;Sauvegardes
-Network;Réseau
-Plugins;Plugins
-Settings;Paramètres
-Enter command;Entrez la commande
-Execute;Exécuter
-Checking disk space;Vérification de l'espace disque
-Updating config files;Mise à jour des fichiers de configuration
-Checking file permissions;Vérification des autorisations de fichiers
-Downloading server image;Téléchargement de l'image du serveur
-Downloaded server image;Image de serveur téléchargée
-Starting;Démarrage
-Online;En ligne
-Kill;Kill
-Stopping;Arrêt
-Search files and folders;Rechercher des fichiers et des dossiers
-Launch WinSCP;Lancer WinSCP
-New folder;Nouveau dossier
-Upload;Upload
-File name;Nom de fichier
-File size;Taille du fichier
-Last modified;Dernière modification
-Cancel;Annuler
-Canceling;Annulation
-Running;En cours d'exécution
-Loading backups;Chargement des sauvegardes
-Started backup creation;Création de sauvegarde démarrée
-Backup is going to be created;La sauvegarde va être crée
-Rename;Renommer
-Move;Déplacer
-Archive;Archiver
-Unarchive;Désarchiver
-Download;Télécharger
-Starting download;Démarrage du téléchargement
-Backup successfully created;Sauvegarde crée avec succès
-Restore;Restaurer
-Copy url;Copier le lien
-Backup deletion started;La suppression de la sauvegarde a commencé
-Backup successfully deleted;Sauvegarde supprimée avec succès
-Primary;Primaire
-This feature is currently not available;Cette fonctionnalité n'est pas disponible actuellement
-Send;Envoyer
-Sending;Envoi en cours
-Welcome to the support chat. Ask your question here and we will help you;Bienvenue sur le chat d'assistance. Posez votre question ici et nous vous aiderons
- minutes ago; minutes
-just now;à l'instant
-less than a minute ago;il y a moins d'une minute
-1 hour ago;Il ya 1 heure
-1 minute ago;il y a 1 minute
-Failed;Échec
- hours ago; Heures
-Open tickets;Tickets ouverts
-Actions;Actions
-No support ticket is currently open;Aucun ticket d'assistance n'est actuellement ouvert
-User information;Informations de l'utilisateur
-Close ticket;Fermer le ticket
-Closing;Fermeture...
-The support team has been notified. Please be patient;L'équipe d'assistance a été prévenue. S'il vous plaît soyez patient
-The ticket is now closed. Type a message to open it again;Le ticket est maintenant fermé. Tapez un message pour l'ouvrir à nouveau
-1 day ago;il y a 1 jour
-is typing;est en train d'écrire...
-are typing;sont en train de taper
-No domains available;Aucun domaine disponible
-Shared domains;Domaines partagés
-Shared domain;Domaine partagé
-Shared domain successfully deleted;Domaine partagé supprimé avec succès
-Shared domain successfully added;Domaine partagé ajouté avec succès
-Domain name;Nom de domaine
-DNS records for;enregistrements DNS pour
-Fetching dns records;Récupération des enregistrements DNS
-No dns records found;Aucun enregistrement DNS trouvé
-Content;Contenu
-Priority;Priorité
-Ttl;TTL
-Enable cloudflare proxy;Activer le proxy cloudflare
-CF Proxy;Proxy CF
- days ago; Jours
-Cancle;Annuler
-An unexpected error occured;Une erreur inattendue s'est produite
-Testy;Testy
-Error from cloudflare api;Erreur de l'API cloudflare
-Profile;Profil
-No subscription available;Aucun abonnement disponible
-Buy;Acheter
-Redirecting;Redirection
-Apply;Appliquer
-Applying code;Appliquer le code
-Invalid subscription code;Code d'abonnement invalide
-Cancel Subscription;Annuler l'abonnement
-Active until;Actif jusqu'à
-We will send you a notification upon subscription expiration;Nous vous enverrons une notification à l'expiration de l'abonnement
-This token has been already used;Ce jeton a déjà été utilisé
-New login for;Nouvelle connexion pour
-No records found for this day;Aucun enregistrement trouvé pour ce jour
-Change;Changer
-Changing;En changement
-Minecraft version;Version Minecraft
-Build version;Version
-Server installation is currently running;L'installation du serveur est en cours
-Selected;Choisi
-Move deleted;Déplacement supprimé
-Delete selected;Supprimer sélectionnée
-Log level;Niveau de journalisation
-Log message;Message de journal
-Time;Temps
-Version;Version
-You are running moonlight version;Vous utilisez la version Moonlight
-Operating system;Système opérateur
-Moonlight is running on;Moonlight est en cours d'exécution
-Memory usage;Utilisation de la mémoire
-Moonlight is using;Moonlight est utilisé
-of memory;de mémoire
-Cpu usage;L'utilisation du processeur
-Refresh;Rafraîchir
-Send a message to all users;Envoyer un message à tous les utilisateurs
-IP;IP
-URL;URL
-Device;Appareil
-Change url;Modifier l'URL
-Message;Message
-Enter message;Entrez le message
-Enter the message to send;Entrez le message à envoyer
-Confirm;Confirmer
-Are you sure?;Etes-vous sûr?
-Enter url;Entrer l'URL
-An unknown error occured while starting backup deletion;Une erreur inconnue s'est produite lors du démarrage de la suppression de la sauvegarde
-Success;Succès
-Backup URL successfully copied to your clipboard;URL de sauvegarde copiée avec succès dans votre presse-papiers
-Backup restore started;La restauration de la sauvegarde a commencé
-Backup successfully restored;Sauvegarde restaurée avec succès
-Register for;S'inscrire à
-Core;Cœur
-Logs;Logs
-AuditLog;AuditLog
-SecurityLog;SecurityLog
-ErrorLog;ErrorLog
-Resources;Ressources
-WinSCP cannot be launched here;WinSCP ne peut pas être lancé ici
-Create a new folder;Créer un nouveau dossier
-Enter a name;Entrez un nom
-File upload complete;Téléchargement du fichier terminé
-New server;Nouveau serveur
-Sessions;Session
-New user;Nouvel utilisateur
-Created at;Créé à
-Mail template not found;Modèle de courrier introuvable
-Missing admin permissions. This attempt has been logged ;Autorisations d'administrateur manquantes. Cette tentative a été enregistrée
-Address;Adresse
-City;Ville
-State;État
-Country;Pays
-Totp;Totp
-Discord;Discord
-Subscription;Abonnement
-None;Aucun
-No user with this id found;Aucun utilisateur avec cet identifiant n'a été trouvé
-Back to list;Retour à la liste
-New domain;Nouveau domaine
-Reset password;Réinitialiser le mot de passe
-Password reset;Réinitialisation du mot de passe
-Reset the password of your account;Réinitialiser le mot de passe de votre compte
-Wrong here?;Faux ici?
-A user with this email can not be found;Un utilisateur avec cet e-mail est introuvable
-Password reset successfull. Check your mail;Réinitialisation du mot de passe réussie. Vérifier votre e-mail
-Discord bot;Discord Bot
-New image;Nouvelle image
-Description;Description
-Uuid;UUID
-Enter tag name;Entrez le nom de la balise
-Remove;Retirer
-No tags found;Aucun tag trouvé
-Enter docker image name;Entrez le nom de l'image Docker
-Tags;Mots clés
-Docker images;Images Docker
-Default image;Image par défaut
-Startup command;Commande de démarrage
-Install container;Installer le conteneur
-Install entry;Installer l'entrée
-Configuration files;Fichiers de configuration
-Startup detection;Détection de démarrage
-Stop command;Commande d'arrêt
-Successfully saved image;Image enregistrée avec succès
-No docker images found;Aucune image docker trouvée
-Key;Clé
-Default value;Valeur par défaut
-Allocations;Allocations
-No variables found;Aucune variable trouvée
-Successfully added image;Image ajoutée avec succès
-Password change for;Changement de mot de passe pour
-of;de
-New node;Nouveau nœud
-Fqdn;Fqdn
-Cores used;Cœurs utilisés
-used;utilisé
-5.15.90.1-microsoft-standard-WSL2 - amd64;5.15.90.1-microsoft-standard-WSL2 - amd64
-Host system information;Informations sur le système hôte
-0;0
-Docker containers running;Conteneurs Docker en cours d'exécution
-details;Détails
-1;1
-2;2
-DDos;DDos
-No ddos attacks found;Aucune attaque ddos trouvée
-Node;Nœud
-Date;Date
-DDos attack started;L'attaque DDos a commencé
-packets;paquets
-DDos attack stopped;Attaque DDos stoppée
- packets; paquets
-Stop all;Arrête tout
-Kill all;Tuer tous
-Network in;Réseau dans
-Network out;Sortie réseau
-Kill all servers;Tuez tous les serveurs
-Do you really want to kill all running servers?;Voulez-vous vraiment tuer tous les serveurs en cours d'exécution?
-Change power state for;Changer l'état de l'alimentation pour
-to;à
-Stop all servers;Arrêtez tous les serveurs
-Do you really want to stop all running servers?;Voulez-vous vraiment arrêter tous les serveurs en cours d'exécution?
-Manage ;Gérer 
-Manage user ;Gérer l'utilisateur 
-Reloading;Rechargement...
-Update;Mise à jour
-Updating;Mise à jour...
-Successfully updated user;Utilisateur mis à jour avec succès
-Discord id;Discord ID
-Discord username;Nom d'utilisateur Discord
-Discord discriminator;Discord Tag
-The Name field is required.;Il est requis de compléter le champ correspondant au nom.
-An error occured while logging you in;Une erreur s'est produite lors de votre connexion
-You need to enter an email address;Vous devez saisir une adresse e-mail
-You need to enter a password;Vous devez entrer un mot de passe
-You need to enter a password with minimum 8 characters in lenght;Vous devez saisir un mot de passe d'au moins 8 caractères
-Proccessing;Traitement...
-The FirstName field is required.;Le champ Prénom est obligatoire.
-The LastName field is required.;Le champ Nom est obligatoire.
-The Address field is required.;Le champ Adresse est obligatoire.
-The City field is required.;Le champ Ville est obligatoire.
-The State field is required.;Le champ État est obligatoire.
-The Country field is required.;Le champ Pays est obligatoire.
-Street and house number requered;Rue et numéro de maison requis
-Max lenght reached;Longueur max atteinte
-Server;Serveur
-stopped;arrêté
-Cleanups;Nettoyages
-executed;réalisé
-Used clanup;nettoyage utilisé
-Enable;Activer
-Disable;Désactiver
-Addons;Compléments
-Javascript version;Version Javascript
-Javascript file;Fichier Javascript
-Select javascript file to execute on start;Sélectionnez le fichier javascript à exécuter au démarrage
-Submit;Soumettre
-Processing;Traitement...
-Go up;monter
-Running cleanup;Nettoyage en cours
-servers;Serveurs
-Select folder to move the file(s) to;Sélectionnez le dossier dans lequel déplacer le(s) fichier(s)
-Paper version;Version paper
-Join2Start;Join2Start
-Server reset;Réinitialisation du serveur
-Reset;Réinitialiser
-Resetting;Réinitialisation...
-Are you sure you want to reset this server?;Voulez-vous vraiment réinitialiser ce serveur?
-Are you sure? This cannot be undone;Etes-vous sûr? cela ne peut être annulé
-Resetting server;Réinitialisation du serveur...
-Deleted file;Fichier supprimé
-Reinstalling server;Réinstallation du serveur
-Uploading files;Téléchargement des fichiers
-complete;complet
-Upload complete;Téléchargement complet
-Security;Sécurité
-Subscriptions;Abonnements
-2fa Code;Code 2FA
-Your account is secured with 2fa;Votre compte est sécurisé avec 2fa
-anyone write a fancy text here?;quelqu'un écrit un texte de fantaisie ici?
-Activate 2fa;Activer 2FA
-2fa apps;applications 2FA
-Use an app like ;Utilisez une application comme 
-or;ou
-and scan the following QR Code;et scannez le QR Code suivant
-If you have trouble using the QR Code, select manual input in the app and enter your email and the following code:;Si vous rencontrez des difficultés pour utiliser le QR Code, sélectionnez la saisie manuelle dans l'application et entrez votre email et le code suivant:
-Finish activation;Terminer l'activation
-2fa Code requiered;Code 2FA requis
-New password;Nouveau mot de passe
-Secure your account;Sécurisez votre compte
-2fa adds another layer of security to your account. You have to enter a 6 digit code in order to login.;2FA ajoute une autre couche de sécurité à votre compte. Vous devez entrer un code à 6 chiffres pour vous connecter.
-New subscription;Nouvel abonnement
-You need to enter a name;Vous devez entrer un nom
-You need to enter a description;Vous devez entrer une description
-Add new limit;Ajouter une nouvelle limite
-Create subscription;Créer un abonnement
-Options;Options
-Amount;Montant
-Do you really want to delete it?;Voulez-vous vraiment le supprimer?
-Loading your subscription;Chargement de votre abonnement
-Searching for deploy node;Recherche d'un nœud de déploiement
-Searching for available images;Recherche d'images disponibles
-Server details;Détails du serveur
-Configure your server;Configurez votre serveur
-Default;Défaut
-You reached the maximum amount of servers for every image of your subscription;Vous avez atteint le nombre maximum de serveurs pour chaque image de votre abonnement.
-Personal information;Informations personnelles
-Enter code;Entrez le code
-Server rename;Renommer le serveur
-Create code;créer un code
-Save subscription;Enregistrer l'abonnement
-Enter your information;Entrez vos informations
-You need to enter your full name in order to use moonlight;Vous devez entrer votre nom complet pour utiliser moonlight
-No node found;Aucun nœud trouvé
-No node found to deploy to found;Aucun noeud trouvé à monter
-Node offline;Nœud hors ligne
-The node the server is running on is currently offline;Le nœud sur lequel le serveur s'exécute est actuellement hors ligne
-Server not found;serveur introuvable
-A server with that id cannot be found or you have no access for this server;Un serveur avec cet identifiant est introuvable ou vous n'avez pas accès à ce serveur
-Compress;Compresser
-Decompress;Décompresser
-Moving;Déplacement...
-Compressing;Compression...
-selected;choisi
-New website;Nouveau site internet
-Plesk servers;Serveurs Plesk
-Base domain;Domaine de base
-Plesk server;Serveur Plesk
-Ftp;FTP
-No SSL certificate found;Aucun certificat SSL trouvé
-Ftp Host;Hôte FTP
-Ftp Port;Port FTP
-Ftp Username;Nom d'utilisateur FTP
-Ftp Password;Mot de passe FTP
-Use;Utiliser
-SSL Certificates;Certificats SSL
-SSL certificates;Certificats SSL
-Issue certificate;Délivrer un certificat
-New plesk server;Nouveau serveur plesk
-Api url;URL de l'API
-Host system offline;Système hôte hors ligne
-The host system the website is running on is currently offline;Le système hôte sur lequel le site internet s'exécute est actuellement hors ligne
-No SSL certificates found;Aucun certificat SSL trouvé
-No databases found for this website;Aucune base de données trouvée pour ce site internet
-The name should be at least 8 characters long;Le nom doit comporter au moins 8 caractères
-The name should only contain of lower case characters and numbers;Le nom ne doit contenir que des caractères minuscules et des chiffres
-Error from plesk;Erreur de plesk
-Host;Hôte
-Username;Nom d'utilisateur
-SRV records cannot be updated thanks to the cloudflare api client. Please delete the record and create a new one;Les enregistrements SRV ne peuvent pas être mis à jour grâce au client api cloudflare. Veuillez supprimer l'enregistrement et en créer un nouveau
-The User field is required.;Le champ Utilisateur est obligatoire.
-You need to specify a owner;Vous devez spécifier un propriétaire
-You need to specify a image;Vous devez spécifier une image
-Api Url;URL de l'API
-Api Key;Clé API
-Duration;Durée
-Enter duration of subscription;Saisir la durée de l'abonnement
-Copied code to clipboard;Code copié dans le presse-papiers
-Invalid or expired subscription code;Code d'abonnement invalide ou expiré
-Current subscription;Abonnement actuel
-You need to specify a server image;Vous devez spécifier une image de serveur
-CPU;CPU
-Hour;Heure
-Day;Jour
-Month;Mois
-Year;Année
-All time;Tout le temps
-This function is not implemented;Cette fonction n'est pas implémentée
-Domain details;Détails du domaine
-Configure your domain;Configurez votre domaine
-You reached the maximum amount of domains in your subscription;Vous avez atteint le nombre maximum de domaines dans votre abonnement
-You need to specify a shared domain;Vous devez spécifier un domaine partagé
-A domain with this name does already exist for this shared domain;Un domaine portant ce nom existe déjà pour ce domaine partagé
-The Email field is required.;Le champ E-mail est obligatoire.
-The Password field is required.;Le champ Mot de passe est obligatoire.
-The ConfirmPassword field is required.;Le champ Confirmer le mot de passe est obligatoire.
-Passwords need to match;Les mots de passe doivent correspondre
-Cleanup exception;Exception de nettoyage
-No shared domain found;Aucun domaine partagé trouvé
-Searching for deploy plesk server;Recherche de déploiement du serveur Plesk
-No plesk server found;Aucun serveur plesk trouvé
-No plesk server found to deploy to;Aucun serveur plesk trouvé pour le déploiement
-No node found to deploy to;Aucun nœud trouvé pour le déploiement
-Website details;Détails du site internet
-Configure your website;Configurez votre site internet
-The name cannot be longer that 32 characters;Le nom ne peut pas dépasser 32 caractères
-The name should only consist of lower case characters;Le nom ne doit être composé que de caractères minuscules
-News;Nouvelles
-Title...;Titre...
-Enter text...;Entrez du texte...
-Saving...;Sauvegarde...
-Deleting...;Suppression...
-Delete post;Supprimer le message
-Do you really want to delete the post ";Voulez-vous vraiment supprimer le message "
-You have no domains;Vous n'avez aucun domaine
-We were not able to find any domains associated with your account;Nous n'avons trouvé aucun domaine associé à votre compte
-You have no websites;Vous n'avez pas de sites internet
-We were not able to find any websites associated with your account;Nous n'avons trouvé aucun site internet associé à votre compte
-Guest;Invité
-You need a domain;Vous avez besoin d'un domaine
-New post;Nouveau poste
-New entry;Nouvelle entrée
-You have no servers;Vous n'avez pas de serveurs
-We were not able to find any servers associated with your account;Nous n'avons trouvé aucun serveur associé à votre compte
-Error creating server on wings;Erreur lors de la création du serveur sur les ailes
-An unknown error occured while restoring a backup;Une erreur inconnue s'est produite lors de la restauration d'une sauvegarde
-Error from daemon;Erreur du daemon
-End;Fin
-Cloud panel;Cloud panel
-Cloud panels;Cloud panels
-New cloud panel;Nouveau Panel cloud 
-You need to enter an api key;Vous devez entrer une clé API
-Webspaces;Espaces webs
-New webspace;Nouvel espace web
-The uploaded file should not be bigger than 100MB;Le fichier téléchargé ne doit pas dépasser 100 Mo
-An unknown error occured while uploading a file;Une erreur inconnue s'est produite lors du téléchargement d'un fichier
-No databases found for this webspace;Aucune base de données trouvée pour cet espace Web
-Sftp;SFTP
-Sftp Host;Hôte Sftp
-Sftp Port;Port Sftp
-Sftp Username;Nom d'utilisateur Sftp
-Sftp Password;Mot de passe Sftp
-Lets Encrypt certificate successfully issued;Certificat Lets Encrypt émis avec succès
-Add shared domain;Ajouter un domaine partagé
-Webspace;Espace web
-You reached the maximum amount of websites in your subscription;Vous avez atteint le nombre maximum de sites internet dans votre abonnement
-Searching for deploy web host;Recherche d'hébergeur web déployé
-Webspace details;Détails de l'espace Web
-Web host;Hébergeur
-Configure your webspaces;Configurez vos espaces web
-You reached the maximum amount of webspaces in your subscription;Vous avez atteint le nombre maximum d'espaces Web dans votre abonnement
-Create a webspace;Créer un espace Web
-Manage your webspaces;Gérez vos espaces web
-Modify the content of your webspaces;Modifier le contenu de vos espaces web
-Successfully updated password;Mot de passe mis à jour avec succès
-An unknown error occured while sending your message;Une erreur inconnue s'est produite lors de l'envoi de votre message
-Open chats;Ouvrir les messages
-No message sent yet;Aucun message envoyé pour le moment
-Support ticket open;Ticket d'assistance ouvert
-Support ticket closed;Ticket d'assistance fermé
-Your connection has been paused;Votre connexion a été interrompue
-We paused your connection because of inactivity. The resume just focus the tab and wait a few seconds;Nous avons suspendu votre connexion pour cause d'inactivité. Passez à l'onglet et attendez quelques secondes
-Failed to reconnect to the moonlight servers;Échec de la reconnexion aux serveurs Moonlight
-We were unable to reconnect to moonlight. Please refresh the page;Nous n'avons pas pu nous reconnecter à moonlight. Veuillez actualiser la page
-Failed to reconnect to the moonlight servers. The connection has been rejected;Échec de la reconnexion aux serveurs Moonlight. La connexion a été rejetée
-We were unable to reconnect to moonlight. Most of the time this is caused by an update of moonlight. Please refresh the page;Nous n'avons pas pu nous reconnecter à moonlight. La plupart du temps, cela est causé par une mise à jour du clair de lune. Veuillez actualiser la page
-Verifying token, loading user data;Vérification du jeton, chargement des données utilisateur
-Reload config;Recharger la configuration
-Successfully reloading configuration;Rechargement de la configuration réussi...
-Successfully reloaded configuration;Configuration rechargée avec succès
-Flows;Flux
-Add node;Ajouter un nœud
-Web system;Système Internet
-Servers with this image;Serveurs avec cette image
-You need to specify a user;Vous devez spécifier un utilisateur
-Import;Importer
-Export;Exporter
-Exporting;Exportation
-Successfully imported image;Image importée avec succès
-Forge version;Forge Version
-Fabric version;Fabric Version
-Fabric loader version;Fabric Loader Version
-Rate;Taux
-Hey, can i borrow you for a second?;Hey, je peux vous empruntez une seconde ?
-We want to improve our services and get a little bit of feedback how we are currently doing. Please leave us a rating;Nous voulons améliorer nos services et obtenir un peu de rétroaction sur la façon dont nous faisons actuellement. Merci de nous laisser une note:
-Thanks for your rating;Merci pour votre évaluation
-It would be really kind of you rating us on a external platform as it will help our project very much;Ce serait vraiment gentil de votre part de nous noter sur une plateforme externe car cela aidera beaucoup notre projet
-Close;Fermer
-Rating saved;Note enregistrée
-Group;Groupe
-Beta;Bêta
-Create a new group;Créer un nouveau groupe
-Download WinSCP;Télécharger WinSCP
-Show connection details;Afficher les détails de la connexion
-New;Nouveau
-New file;Nouveau fichier
-Connection details;Détails de connexion
-Malware;Logiciels malveillants
-Create a new file;Créer un nouveau fichier
-Edit layout;Modifier la mise en page
-Uptime;Disponibilité
-Moonlight is online since;Moonlight est en ligne depuis
-User;Utilisateur
-Databases;Bases de données
-Sitzungen;Sitzungen
-Active users;Utilisateurs actifs
-Search for plugins;Rechercher des plugins
-Search;Recherche
-Searching;Recherche...
-Successfully installed gunshell;Cartouche installée avec succès
-Successfully installed fastasyncworldedit;Fastasyncworldedit a été installé avec succès
-Successfully installed minimotd;Minimotd installé avec succès
-Moonlight health;Statut Moonlight
-Healthy;En bonne santé
-Successfully saved file;Fichier enregistré avec succès
-Unsorted servers;Serveurs non triés
-Enter a new name;Entrez un nouveau nom
-Sign in with;Se connecter avec
-Make your services accessible through your own domain;Rendez vos services accessibles via votre propre domaine
-New group;Nouveau groupe
-Finish editing layout;Terminer la modification de la mise en page
-Remove group;Supprimer le groupe
-Hidden in edit mode;Masqué en mode édition
-Enter your 2fa code here;Entrez votre code 2FA ici
-Two factor authentication;Authentification à deux facteurs
-Preferences;Préférences
-Streamer mode;Mode diffusion
-Scan the QR code and enter the code generated by the app you have scanned it in;Scannez le code QR et entrez le code généré par l'application dans laquelle vous l'avez scanné
-Start scan;Lancer l'analyse
-Results;Résultats
-Scan in progress;Analyse en cours
-Debug;Debug
-Save changes;Sauvegarder les modifications
-Delete domain;Supprimer le domaine
-Python version;Version Python 
-Python file;Fichier Python
-Select python file to execute on start;Sélectionnez le fichier python à exécuter au démarrage
-You have no webspaces;Vous n'avez pas d'espaces Web
-We were not able to find any webspaces associated with your account;Nous n'avons trouvé aucun espace Web associé à votre compte
-Backup download successfully started;Le téléchargement de la sauvegarde a démarré avec succès
-Error from cloud panel;Erreur du Cloud Panel
-Error from wings;Erreur des ailes
-Remove link;Supprimer le lien
-Your account is linked to a discord account;Votre compte est lié à un compte discord
-You are able to use features like the discord bot of moonlight;Vous pouvez utiliser des fonctionnalités telles que le bot discord de Moonlight
-The password should be at least 8 characters long;Le mot de passe doit comporter au moins 8 caractères
-The name should only consist of lower case characters or numbers;Le nom ne doit être composé que de caractères minuscules ou de chiffres
-The requested resource was not found;La ressource demandée n'a pas été trouvée
-We were not able to find the requested resource. This can have following reasons;Nous n'avons pas pu trouver la ressource demandée. Cela peut avoir les raisons suivantes
-The resource was deleted;La ressource a été supprimée
-You have to permission to access this resource;Vous devez être autorisé à accéder à cette ressource
-You may have entered invalid data;Vous avez peut-être entré des données invalides
-A unknown bug occured;Un bug inconnu s'est produit
-An api was down and not proper handled;Une API était en panne et n'était pas correctement gérée
-A database with this name does already exist;Une base de données portant ce nom existe déjà
-Successfully installed quickshop-hikari;Quickshop-hikari a été installé avec succès
-You need to enter a valid domain;Vous devez entrer un domaine valide
-2fa code;code 2FA
-This feature is not available for;Cette fonction n'est pas disponible pour
-Your account is currently not linked to discord;Votre compte n'est actuellement pas lié à discord
-To use features like the discord bot, link your moonlight account with your discord account;Pour utiliser des fonctionnalités telles que le bot Discord, liez votre compte Moonlight à votre compte Discord
-Link account;Lien de compte
-Continue;Continuer
-Preparing;Préparation
-Make sure you have installed one of the following apps on your smartphone and press continue;Assurez-vous d'avoir installé l'une des applications suivantes sur votre smartphone et appuyez sur continuer
-The max lenght for the name is 32 characters;La longueur maximale du nom est de 32 caractères
-Successfully installed chunky;Chunky installé avec succès
-Successfully installed huskhomes;Hulkhomes installés avec succès
-Successfully installed simply-farming;Simple-farming installé avec succès
-You need to specify a first name;Vous devez indiquer un prénom
-You need to specify a last name;Vous devez spécifier un nom de famille
-You need to specify a password;Vous devez spécifier un mot de passe
-Please solve the captcha;Merci de résoudre le captcha
-The email is already in use;L'e-mail est déjà utilisé
-The dns records of your webspace do not point to the host system;Les enregistrements DNS de votre espace Web ne pointent pas vers le système hôte
-Scan complete;Analyse terminée
-Currently scanning:;Analyse en cours:
-Successfully installed dynmap;Dynamap installé avec succès
-Successfully installed squaremap;Squaremap installé avec succès
-No web host found;Aucun hébergeur trouvé
-No web host found to deploy to;Aucun hébergeur trouvé pour le déploiement
-Successfully installed sleeper;Sleeper installé avec succès
-You need to enter a domain;Vous devez saisir un domaine
-Enter a ip;Entrez une ip
-Ip Bans;Ip Bans
-Ip;Ip
-Successfully installed simple-voice-chat;Simple-voice-chat installé avec succès
-Successfully installed smithing-table-fix;Smithing-table-fix a été installé avec succès
-Successfully installed justplayer-tpa;Justplayer-TPA installé avec succès
-Successfully installed ishop;iShop installé avec succès
-Successfully installed lifestealre;Lifestealre installé avec succès
-Successfully installed lifeswap;Lifeswap installé avec succès
-Java version;Java Version
-Jar file;Fichier Jar
-Select jar to execute on start;Sélectionnez le jar à exécuter au démarrage
-A website with this domain does already exist;Un site Web avec ce domaine existe déjà
-Successfully installed discordsrv;Discordsrv a été installé avec succès
-Reinstall;Réinstaller
-Reinstalling;Réinstallation
-Successfully installed freedomchat;Freedomchat installé avec succès
-Leave empty for the default background image;Laisser vide pour l'image d'arrière-plan par défaut
-Background image url;URL de l'image d'arrière-plan
-of CPU used;du processeur utilisé
-memory used;mémoire utilisée
-163;163
-172;172
-Sentry;Sentinelle
-Sentry is enabled;La sentinelle est activée
-Successfully installed mobis-homes;Mobis-homes installés avec succès
-Your moonlight account is disabled;Votre compte moonlight est désactivé
-Your moonlight account is currently disabled. But dont worry your data is still saved;Votre compte Moonlight est actuellement désactivé. Mais ne vous inquiétez pas, vos données sont toujours enregistrées
-You need to specify a email address;Vous devez indiquer une adresse e-mail
-A user with that email does already exist;Un utilisateur avec cet email existe déjà
-Successfully installed buildmode;buildmode installé avec succès
-Successfully installed plasmo-voice;Plasmo-Voice installé avec succès
-157;157
-174;174
-158;158
-Webspace not found;Espace Web introuvable
-A webspace with that id cannot be found or you have no access for this webspace;Un espace Web avec cet identifiant est introuvable ou vous n'avez pas accès à cet espace Web
-No plugin download for your minecraft version found;Aucun téléchargement de plugin pour votre version de minecraft trouvé
-Successfully installed gamemode-alias;Gamemode-alias installé avec succès
-228;228
-User;Utilisateur
-Send notification;Envoyer une notification
-Successfully saved changes;Modifications enregistrées avec succès
-Archiving;Archivage...
-Server is currently not archived;Le serveur n'est actuellement pas archivé
-Add allocation;Ajouter une allocation
-231;231
-175;175
-Dotnet version;Version Dotnet
-Dll file;Fichier DLL
-Select dll to execute on start;Sélectionnez le dll à exécuter au démarrage

+ 0 - 778
Moonlight/defaultstorage/resources/lang/ro_ro.lang

@@ -1,778 +0,0 @@
-Open support;Suport deschis
-About us;Despre noi
-Imprint;Impresum
-Privacy;Confidențialitate
-Login;Autentificare
-Register;Înregistrare
-Insert brand name...;Introduceți numele mărcii...
-Save and continue;Salvați și continuați
-Saving;Se salvează
-Configure basics;Configurare elemente de bază
-Brand name;Numele mărcii
-test;test
-Insert first name...;Introduceți prenumele...
-Insert last name...;Introduceți numele de familie...
-Insert email address...;Introduceți adresa de email...
-Add;Adăugați
-Adding...;Se adaugă...
-Add admin accounts;Adăugați conturi de administrator
-First name;Prenume
-Last name;Nume de familie
-Email address;Adresă de email
-Enter password;Introduceți parola
-Next;Următorul
-Back;Înapoi
-Configure features;Configurați funcționalitățile
-Support chat;Asistență prin chat
-Finish;Finalizare
-Finalize installation;Finalizați instalarea
-Moonlight basic settings successfully configured;Setările de bază Moonlight au fost configurate cu succes
-Ooops. This page is crashed;Hopa. Această pagină s-a blocat.
-This page is crashed. The error has been reported to the moonlight team. Meanwhile you can try reloading the page;Această pagină s-a blocat. Eroarea a fost raportată echipei Moonlight. Între timp, puteți încerca să reîncărcați pagina
-Setup complete;Configurare completă
-It looks like this moonlight instance is ready to go;Se pare că această instanță Moonlight este gata de utilizare
-User successfully created;Utilizator creat cu succes
-Ooops. Your moonlight client is crashed;Hopa. Clientul tău Moonlight s-a blocat.
-This error has been reported to the moonlight team;Această eroare a fost raportată echipei Moonlight
-Sign In;Autentificare
-Sign in to start with moonlight;Autentificați-vă pentru a începe cu Moonlight
-Sign in with Discord;Autentificare cu Discord
-Or with email;Sau cu email
-Forgot password?;Ați uitat parola?
-Sign-in;Autentificare
-Not registered yet?;Încă nu sunteți înregistrat?
-Sign up;Înregistrare
-Authenticating;Se autentifică...
-Sign in with Google;Autentificare cu Google
-Working;Se lucrează...
-Error;Eroare
-Email and password combination not found;Combinarea de email și parolă nu a fost găsită
-Email;Email
-Password;Parolă
-Account settings;Setări cont
-Logout;Deconectare
-Dashboard;Panou de control
-Order;Comandă
-Website;Website
-Database;Bază de date
-Domain;Domeniu
-Servers;Servere
-Websites;Site-uri web
-Databases;Baze de date
-Domains;Domenii
-Changelog;Jurnal de modificări
-Firstname;Prenume
-Lastname;Nume de familie
-Repeat password;Repetă parola
-Sign Up;Înregistrare
-Sign up to start with moonlight;Înregistrați-vă pentru a începe cu Moonlight
-Sign up with Discord;Înregistrare cu Discord
-Sign up with Google;Înregistrare cu Google
-Sign-up;Înregistrare
-Already registered?;Deja înregistrat?
-Sign in;Autentificare
-Create something new;Creați ceva nou
-Create a gameserver;Creați un server de jocuri
-A new gameserver in just a few minutes;Un server de jocuri nou în doar câteva minute
-Create a database;Creați o bază de date
-A quick way to store your data and manage it from all around the world;O modalitate rapidă de a stoca datele și de a le gestiona din întreaga lume
-Manage your services;Gestionați serviciile dumneavoastră
-Manage your gameservers;Gestionați serverele dumneavoastră de jocuri
-Adjust your gameservers;Reglați serverele dumneavoastră de jocuri
-Manage your databases;Gestionați bazele de date
-Insert, delete and update the data in your databases;Introduceți, ștergeți și actualizați datele din bazele de date
-Create a website;Creați un site web
-Make your own websites with a webspace;Creați propriile site-uri web cu un spațiu web
-Create a domain;Creați un domeniu
-Make your services accessible through your own domain;Faceți serviciile dumneavoastră accesibile prin propriul domeniu
-Manage your websites;Gestionați site-urile web
-Modify the content of your websites;Modificați conținutul site-urilor dumneavoastră
-Manage your domains;Gestionați domeniile dumneavoastră
-Add, edit, and delete DNS records;Adăugați, editați și ștergeți înregistrări DNS
-Admin;Administrator
-System;Sistem
-Overview;Prezentare generală
-Manager;Manager
-Cleanup;Curățare
-Nodes;Noduri
-Images;Imagini
-aaPanel;aaPanel
-Users;Utilizatori
-Support;Asistență
-Statistics;Statistici
-No nodes found. Start with adding a new node;Nu s-au găsit noduri. Începeți prin adăugarea unui nod nou
-Nodename;Nume nod
-FQDN;Nume complet de domeniu (FQDN)
-Create;Creare
-Creating;Se creează...
-Http port;Port HTTP
-Sftp port;Port SFTP
-Moonlight daemon port;Port daemon Moonlight
-SSL;SSL
-CPU Usage;Utilizare CPU
-In %;În %
-Memory;Memorie
-Used / Available memory;Memorie utilizată / Memorie disponibilă
-Storage;Stocare
-Available storage;Stocare disponibilă
-Add a new node;Adăugați un nod nou
-Delete;Ștergere
-Deleting;Se șterge...
-Edit;Editare
-Token Id;ID token
-Token;Token
-Save;Salvare
-Setup;Configurare
-Open a ssh connection to your node and enter;Deschideți o conexiune SSH la nodul dvs. și introduceți
-and paste the config below. Then press STRG+O and STRG+X to save;și lipiți configurația de mai jos. Apoi apăsați STRG+O și STRG+X pentru a salva
-Before configuring this node, install the daemon;Înainte de a configura acest nod, instalați daemonul
-Delete this node?;Doriți să ștergeți acest nod?
-Do you really want to delete this node;Doriți cu adevărat să ștergeți acest nod?
-Yes;Da
-No;Nu
-Status;Stare
-Adding;Se adaugă
-Port;Port
-Id;ID
-Manage;Administrare
-Create new server;Creați un server nou
-No servers found;Niciun server găsit
-Server name;Nume server
-Cpu cores;Nuclee CPU
-Disk;Disc
-Image;Imagine
-Override startup;Suprascrie pornirea
-Docker image;Imagine Docker
-CPU Cores (100% = 1 Core);Nuclee CPU (100% = 1 nucleu)
-Server successfully created;Server creat cu succes
-Name;Nume
-Cores;Nuclee
-Owner;Proprietar
-Value;Valoare
-An unknown error occurred;A apărut o eroare necunoscută
-No allocation found;Nicio alocare găsită
-Identifier;Identificator
-UuidIdentifier;UuidIdentifier
-Override startup command;Suprascrieți comanda de pornire
-Loading;Se încarcă...
-Offline;Deconectat
-Connecting;Conectare...
-Start;Pornire
-Restart;Repornire
-Stop;Oprire
-Shared IP;IP partajată
-Server ID;ID server
-Cpu;CPU
-Console;Consolă
-Files;Fișiere
-Backups;Copii de siguranță
-Network;Rețea
-Plugins;Plugin-uri
-Settings;Setări
-Enter command;Introduceți comanda
-Execute;Executare
-Checking disk space;Verificare spațiu pe disc
-Updating config files;Actualizare fișiere de configurație
-Checking file permissions;Verificare permisiuni de fișiere
-Downloading server image;Se descarcă imaginea serverului
-Downloaded server image;Imaginea serverului a fost descărcată
-Starting;Începere
-Online;Conectat
-Kill;Oprit forțat
-Stopping;Se oprește
-Search files and folders;Căutare fișiere și foldere
-Launch WinSCP;Lansați WinSCP
-New folder;Folder nou
-Upload;Încărcare
-File name;Nume fișier
-File size;Mărime fișier
-Last modified;Ultima modificare
-Cancel;Anulare
-Canceling;Se anulează
-Running;În execuție
-Loading backups;Se încarcă copiile de siguranță
-Started backup creation;Crearea copiei de siguranță a început
-Backup is going to be created;Se va crea o copie de siguranță
-Rename;Redenumire
-Move;Mutare
-Archive;Arhivare
-Unarchive;Dezarhivare
-Download;Descărcare
-Starting download;Începerea descărcării
-Backup successfully created;Copie de siguranță creată cu succes
-Restore;Restaurare
-Copy url;Copiere URL
-Backup deletion started;Se începe ștergerea copiei de siguranță
-Backup successfully deleted;Copie de siguranță ștearsă cu succes
-Primary;Primar
-This feature is currently not available;Această funcționalitate nu este disponibilă în prezent
-Send;Trimite
-Sending;Se trimite
-Welcome to the support chat. Ask your question here and we will help you;Bine ați venit la chatul de asistență. Puneți-vă întrebarea aici și vă vom ajuta
-minutes ago;minute în urmă
-just now;chiar acum
-less than a minute ago;mai puțin de o minută în urmă
-1 hour ago;acum 1 oră
-1 minute ago;acum 1 minut
-Failed;Eșuat
-hours ago;ore în urmă
-Open tickets;Deschideți tichete
-Actions;Acțiuni
-No support ticket is currently open;Nu există momentan niciun tichet de asistență deschis
-User information;Informații utilizator
-Close ticket;Închideți tichetul
-Closing;Se închide
-The support team has been notified. Please be patient;Echipa de suport a fost notificată. Vă rugăm să aveți răbdare
-The ticket is now closed. Type a message to open it again;Tichetul este acum închis. Tastați un mesaj pentru a-l deschide din nou
-1 day ago;acum 1 zi
-is typing;scrie...
-are typing;scriu...
-No domains available;Niciun domeniu disponibil
-Shared domains;Domenii partajate
-Shared domain;Domeniu partajat
-Shared domain successfully deleted;Domeniu partajat șters cu succes
-Shared domain successfully added;Domeniu partajat adăugat cu succes
-Domain name;Nume de domeniu
-DNS records for;Înregistrări DNS pentru
-Fetching dns records;Se preiau înregistrările DNS
-No dns records found;Nicio înregistrare DNS găsită
-Content;Conținut
-Priority;Prioritate
-Ttl;TTL (Timp de Viață)
-Enable cloudflare proxy;Activați proxy Cloudflare
-CF Proxy;Proxy CF
-days ago;zile în urmă
-Cancel;Anulare
-An unexpected error occurred;A apărut o eroare neașteptată
-Testy;Testy
-Error from cloudflare api;Eroare de la API Cloudflare
-Profile;Profil
-No subscription available;Nicio abonare disponibilă
-Buy;Cumpără
-Redirecting;Redirecționare
-Apply;Aplică
-Applying code;Se aplică codul
-Invalid subscription code;Cod de abonament invalid
-Cancel Subscription;Anulează abonamentul
-Active until;Activ până la
-We will send you a notification upon subscription expiration;Vă vom trimite o notificare la expirarea abonamentului
-This token has been already used;Acest token a fost deja utilizat
-New login for;Autentificare nouă pentru
-No records found for this day;Nu s-au găsit înregistrări pentru această zi
-Change;Schimbă
-Changing;Se schimbă
-Minecraft version;Versiune Minecraft
-Build version;Versiune build
-Server installation is currently running;Instalarea serverului este în desfășurare în prezent
-Selected;Selectat
-Move deleted;Mutarea ștergerii
-Delete selected;Ștergeți selecția
-Log level;Nivelul de jurnal
-Log message;Mesaj de jurnal
-Time;Timp
-Version;Versiune
-You are running moonlight version;Rulați versiunea Moonlight
-Operating system;Sistem de operare
-Moonlight is running on;Moonlight rulează pe
-Memory usage;Utilizare memorie
-Moonlight is using;Moonlight folosește
-of memory;din memorie
-Cpu usage;Utilizare CPU
-Refresh;Reîmprospătează
-Send a message to all users;Trimiteți un mesaj tuturor utilizatorilor
-IP;IP
-URL;URL
-Device;Dispozitiv
-Change url;Schimbați URL-ul
-Message;Mesaj
-Enter message;Introduceți mesajul
-Enter the message to send;Introduceți mesajul de trimis
-Confirm;Confirmă
-Are you sure?;Sunteți sigur?
-Enter url;Introduceți URL-ul
-An unknown error occured while starting backup deletion;A apărut o eroare necunoscută în timpul începerii ștergerii backup-ului
-Success;Succes
-Backup URL successfully copied to your clipboard;URL-ul de backup a fost copiat cu succes în clipboard
-Backup restore started;Restaurarea backup-ului a început
-Backup successfully restored;Backup-ul a fost restaurat cu succes
-Register for;Înregistrare pentru
-Core;Nucleu
-Logs;Jurnale
-AuditLog;Jurnal de audit
-SecurityLog;Jurnal de securitate
-ErrorLog;Jurnal de eroare
-Resources;Resurse
-WinSCP cannot be launched here;WinSCP nu poate fi lansat aici
-Create a new folder;Creați un folder nou
-Enter a name;Introduceți un nume
-File upload complete;Încărcarea fișierului a fost finalizată
-New server;Server nou
-Sessions;Sesiuni
-New user;Utilizator nou
-Created at;Creat la
-Mail template not found;Șablonul de e-mail nu a fost găsit
-Missing admin permissions. This attempt has been logged ;Lipsesc permisiunile de administrator. Acest încercare a fost înregistrată
-Address;Adresă
-City;Oraș
-State;Stat
-Country;Țară
-Totp;Totp
-Discord;Discord
-Subscription;Abonament
-None;Niciunul
-No user with this id found;Niciun utilizator cu această ID găsit
-Back to list;Înapoi la listă
-New domain;Domeniu nou
-Reset password;Resetare parolă
-Password reset;Resetare parolă
-Reset the password of your account;Resetați parola contului dvs.
-Wrong here?;Greșit aici?
-A user with this email can not be found;Un utilizator cu această adresă de e-mail nu poate fi găsit
-Password reset successfull. Check your mail;Resetarea parolei a fost efectuată cu succes. Verificați-vă poșta
-Discord bot;Bot Discord
-New image;Imagine nouă
-Description;Descriere
-Uuid;UUID
-Enter tag name;Introduceți numele etichetei
-Remove;Eliminați
-No tags found;Nu s-au găsit etichete
-Enter docker image name;Introduceți numele imaginii Docker
-Tags;Etichete
-Docker images;Imagini Docker
-Default image;Imagine implicită
-Startup command;Comandă de pornire
-Install container;Instalați containerul
-Install entry;Intrare de instalare
-Configuration files;Fișiere de configurare
-Startup detection;Detectare de pornire
-Stop command;Comandă de oprire
-Successfully saved image;Imaginea a fost salvată cu succes
-No docker images found;Nu s-au găsit imagini Docker
-Key;Cheie
-Default value;Valoare implicită
-Allocations;Alocări
-No variables found;Nu s-au găsit variabile
-Successfully added image;Imaginea a fost adăugată cu succes
-Password change for;Schimbarea parolei pentru
-of;al
-New node;Nod nou
-Fqdn;Nume de domeniu complet
-Cores used;Nuclee utilizate
-used;folosite
-5.15.90.1-microsoft-standard-WSL2 - amd64;5.15.90.1-microsoft-standard-WSL2 - amd64
-Host system information;Informații despre sistemul gazdă
-0;0
-Docker containers running;Containere Docker în execuție
-details;Detalii
-1;1
-2;2
-DDos;DDoS
-No ddos attacks found;Nu s-au găsit atacuri DDoS
-Node;Nod
-Date;Dată
-DDos attack started;Atac DDoS început
-packets;pachete
-DDos attack stopped;Atac DDoS oprit
-packets; pachete
-Stop all;Oprește totul
-Kill all;Omoară totul
-Network in;Rețea intrare
-Network out;Rețea ieșire
-Kill all servers;Omoară toate serverele
-Do you really want to kill all running servers?;Doriți cu adevărat să opriți toate serverele care rulează?
-Change power state for;Schimbați starea de alimentare pentru
-to;la
-Stop all servers;Oprește toate serverele
-Do you really want to stop all running servers?;Doriți cu adevărat să opriți toate serverele care rulează?
-Manage ;Gestionează
-Manage user ;Gestionează utilizatorul
-Reloading;Se reîncarcă...
-Update;Actualizare
-Updating;Se actualizează
-Successfully updated user;Utilizator actualizat cu succes
-Discord id;ID Discord
-Discord username;Nume de utilizator Discord
-Discord discriminator;Discord Discriminator
-The Name field is required.;Câmpul Nume este obligatoriu.
-An error occured while logging you in;A apărut o eroare în timpul autentificării
-You need to enter an email address;Trebuie să introduceți o adresă de email
-You need to enter a password;Trebuie să introduceți o parolă
-You need to enter a password with minimum 8 characters in lenght;Trebuie să introduceți o parolă cu minim 8 caractere în lungime
-Proccessing;Se procesează...
-The FirstName field is required.;Câmpul Prenume este obligatoriu.
-The LastName field is required.;Câmpul Nume este obligatoriu.
-The Address field is required.;Câmpul Adresă este obligatoriu.
-The City field is required.;Câmpul Oraș este obligatoriu.
-The State field is required.;Câmpul Stat este obligatoriu.
-The Country field is required.;Câmpul Țară este obligatoriu.
-Street and house number requered;Strada și numărul casei sunt necesare.
-Max lenght reached;A fost atinsă lungimea maximă
-Server;Server
-stopped;oprit
-Cleanups;Curățări
-executed;executat
-Used clanup;Curățare folosită
-Enable;Activare
-Disable;Dezactivare
-Addons;Add-on-uri
-Javascript version;Versiune JavaScript
-Javascript file;Fișier JavaScript
-Select javascript file to execute on start;Selectați fișierul JavaScript pentru a fi executat la pornire
-Submit;Trimite
-Processing;Se procesează...
-Go up;Urcați în sus
-Running cleanup;Curățarea în curs de rulare
-servers;servere
-Select folder to move the file(s) to;Selectați folderul în care să mutați fișierele
-Paper version;Versiune tipărită
-Join2Start;Join2Start
-Server reset;Resetare server
-Reset;Resetare
-Resetting;Se resetează...
-Are you sure you want to reset this server?;Sigur doriți să resetați acest server?
-Are you sure? This cannot be undone;Sunteți sigur? Aceasta nu poate fi anulată
-Resetting server;Se resetează serverul...
-Deleted file;Fișier șters
-Reinstalling server;Se reinstalează serverul
-Uploading files;Încărcare fișiere
-complete;complet
-Upload complete;Încărcare completă
-Security;Securitate
-Subscriptions;Abonamente
-2fa Code;Cod 2FA
-Your account is secured with 2fa;Contul dvs. este securizat cu 2FA
-anyone write a fancy text here?;cineva să scrie un text frumos aici?
-Activate 2fa;Activare 2FA
-2fa apps;Aplicații 2FA
-Use an app like ;Utilizați o aplicație precum
-or;sau
-and scan the following QR Code;și scanați codul QR următor
-If you have trouble using the QR Code, select manual input in the app and enter your email and the following code:;Dacă întâmpinați probleme la utilizarea codului QR, selectați introducerea manuală în aplicație și introduceți adresa dvs. de email și următorul cod:
-Finish activation;Finalizare activare
-2fa Code requiered;Cod 2FA necesar
-New password;Parolă nouă
-Secure your account;Securizați contul dvs.
-2fa adds another layer of security to your account. You have to enter a 6 digit code in order to login.;2FA adaugă un alt nivel de securitate contului dvs. Trebuie să introduceți un cod cu 6 cifre pentru a vă autentifica.
-New subscription;Abonament nou
-You need to enter a name;Trebuie să introduceți un nume
-You need to enter a description;Trebuie să introduceți o descriere
-Add new limit;Adăugați o limită nouă
-Create subscription;Creați abonament
-Options;Opțiuni
-Amount;Suma
-Do you really want to delete it?;Sigur doriți să ștergeți acesta?
-Loading your subscription;Se încarcă abonamentul dvs.
-Searching for deploy node;Căutare nod de implementare
-Searching for available images;Căutare imagini disponibile
-Server details;Detalii server
-Configure your server;Configurați serverul dvs.
-Default;Implicit
-You reached the maximum amount of servers for every image of your subscription;Ați atins cantitatea maximă de servere pentru fiecare imagine din abonamentul dvs.
-Personal information;Informații personale
-Enter code;Introduceți codul
-Server rename;Redenumire server
-Create code;Creați codul
-Save subscription;Salvați abonamentul
-Enter your information;Introduceți informațiile dvs.
-You need to enter your full name in order to use moonlight;Trebuie să introduceți numele complet pentru a utiliza Moonlight
-No node found;Niciun nod găsit
-No node found to deploy to found;Nu s-a găsit niciun nod pentru a fi implementat
-Node offline;Nodul este deconectat
-The node the server is running on is currently offline;Nodul pe care rulează serverul este momentan deconectat
-Server not found;Serverul nu a fost găsit
-A server with that id cannot be found or you have no access for this server;Un server cu această ID nu poate fi găsit sau nu aveți acces la acest server
-Compress;Comprimare
-Decompress;Dezcompresare
-Moving;Mutare...
-Compressing;Se comprimă...
-selected;selectat
-New website;Site web nou
-Plesk servers;Servere Plesk
-Base domain;Domeniu de bază
-Plesk server;Server Plesk
-Ftp;FTP
-No SSL certificate found;Nu s-a găsit niciun certificat SSL
-Ftp Host;Gazdă FTP
-Ftp Port;Port FTP
-Ftp Username;Nume utilizator FTP
-Ftp Password;Parolă FTP
-Use;Utilizați
-SSL Certificates;Certificate SSL
-SSL certificates;Certificate SSL
-Issue certificate;Emitere certificat
-New plesk server;Un nou server Plesk
-Api url;URL API
-Host system offline;Sistemul gazdă este offline
-The host system the website is running on is currently offline;Sistemul gazdă pe care rulează site-ul este în prezent offline
-No SSL certificates found;Nu s-au găsit certificate SSL
-No databases found for this website;Nu s-au găsit baze de date pentru acest site web
-The name should be at least 8 characters long;Numele trebuie să aibă cel puțin 8 caractere
-The name should only contain of lower case characters and numbers;Numele ar trebui să conțină doar litere mici și cifre
-Error from plesk;Eroare de la Plesk
-Host;Gazdă
-Username;Nume de utilizator
-SRV records cannot be updated thanks to the cloudflare api client. Please delete the record and create a new one;Înregistrările SRV nu pot fi actualizate datorită clientului API Cloudflare. Vă rugăm să ștergeți înregistrarea și să creați una nouă
-The User field is required.;Câmpul Utilizator este obligatoriu
-You need to specify an owner;Trebuie să specificați un proprietar
-You need to specify an image;Trebuie să specificați o imagine
-Api Url;URL API
-Api Key;Cheie API
-Duration;Durată
-Enter duration of subscription;Introduceți durata abonamentului
-Copied code to clipboard;Codul a fost copiat în clipboard
-Invalid or expired subscription code;Cod de abonament invalid sau expirat
-Current subscription;Abonament curent
-You need to specify a server image;Trebuie să specificați o imagine de server
-CPU;CPU
-Hour;Oră
-Day;Zi
-Month;Lună
-Year;An
-All time;Tot timpul
-This function is not implemented;Această funcție nu este implementată
-Domain details;Detalii domeniu
-Configure your domain;Configurați domeniul dvs.
-You reached the maximum amount of domains in your subscription;Ați atins numărul maxim de domenii în abonamentul dvs.
-You need to specify a shared domain;Trebuie să specificați un domeniu partajat
-A domain with this name already exists for this shared domain;Un domeniu cu acest nume există deja pentru acest domeniu partajat
-The Email field is required.;Câmpul Email este obligatoriu
-The Password field is required.;Câmpul Parolă este obligatoriu
-The ConfirmPassword field is required.;Câmpul Confirmare Parolă este obligatoriu
-Passwords need to match;Parolele trebuie să se potrivească
-Cleanup exception;Excepție la curățare
-No shared domain found;Nu s-a găsit niciun domeniu partajat
-Searching for deploy plesk server;Se caută pentru a implementa serverul Plesk
-No plesk server found;Nu s-a găsit niciun server Plesk
-No plesk server found to deploy to;Nu s-a găsit niciun server Plesk pentru a implementa
-No node found to deploy to;Nu s-a găsit niciun nod pentru a implementa
-Website details;Detalii site web
-Configure your website;Configurați site-ul dvs. web
-The name cannot be longer that 32 characters;Numele nu poate avea mai mult de 32 de caractere
-The name should only consist of lower case characters;Numele ar trebui să conțină doar litere mici
-News;Știri
-Title...;Titlu...
-Enter text...;Introduceți text...
-Saving...;Se salvează...
-Deleting...;Se șterge...
-Delete post;Ștergeți postarea
-Do you really want to delete the post ";Doriți cu adevărat să ștergeți postarea "
-You have no domains;Nu aveți domenii
-We were not able to find any domains associated with your account;Nu am putut găsi niciun domeniu asociat cu contul dvs.
-You have no websites;Nu aveți site-uri web
-We were not able to find any websites associated with your account;Nu am putut găsi niciun site web asociat cu contul dvs.
-Guest;Vizitator
-You need a domain;Aveți nevoie de un domeniu
-New post;Postare nouă
-New entry;Intrare nouă
-You have no servers;Nu aveți servere
-We were not able to find any servers associated with your account;Nu am putut găsi niciun server asociat cu contul dvs.
-Error creating server on wings;Eroare la crearea serverului pe Wings
-An unknown error occurred while restoring a backup;A apărut o eroare necunoscută în timpul restaurării unei copii de rezervă
-Error from daemon;Eroare de la daemon
-End;Sfârșit
-Cloud panel;Panou de control pentru cloud
-Cloud panels;Panouri de control pentru cloud
-New cloud panel;Panou de control pentru cloud nou
-You need to enter an api key;Trebuie să introduceți o cheie API
-Webspaces;Spații web
-New webspace;Spațiu web nou
-The uploaded file should not be bigger than 100MB;Fișierul încărcat nu trebuie să depășească 100MB
-An unknown error occurred while uploading a file;A apărut o eroare necunoscută în timpul încărcării unui fișier
-No databases found for this webspace;Nu s-au găsit baze de date pentru acest spațiu web
-Sftp;SFTP
-Sftp Host;Gazdă SFTP
-Sftp Port;Port SFTP
-Sftp Username;Nume de utilizator SFTP
-Sftp Password;Parolă SFTP
-Lets Encrypt certificate successfully issued;Certificatul Lets Encrypt a fost emis cu succes
-Add shared domain;Adăugați domeniu partajat
-Webspace;Spațiu web
-You reached the maximum amount of websites in your subscription;Ați atins numărul maxim de site-uri web în abonamentul dvs.
-Searching for deploy web host;Se caută pentru a implementa gazda web
-Webspace details;Detalii spațiu web
-Web host;Gazdă web
-Configure your webspaces;Configurați spațiile dvs. web
-You reached the maximum amount of webspaces in your subscription;Ați atins numărul maxim de spații web în abonamentul dvs.
-Create a webspace;Creați un spațiu web
-Manage your webspaces;Gestionați spațiile dvs. web
-Modify the content of your webspaces;Modificați conținutul spațiilor dvs. web
-Successfully updated password;Parola a fost actualizată cu succes
-An unknown error occurred while sending your message;A apărut o eroare necunoscută în timpul trimiterii mesajului dvs.
-Open chats;Deschideți conversații
-No message sent yet;Nu ați trimis încă niciun mesaj
-Support ticket open;Bilet de suport deschis
-Support ticket closed;Bilet de suport închis
-Your connection has been paused;Conexiunea dvs. a fost pusă în pauză
-We paused your connection because of inactivity. To resume, simply focus the tab and wait a few seconds;Am pus în pauză conexiunea dvs. din cauza inactivității. Pentru a relua, focalizați pur și simplu fila și așteptați câteva secunde
-Failed to reconnect to the moonlight servers;Nu s-a reușit reconectarea la serverele Moonlight
-We were unable to reconnect to moonlight. Please refresh the page;Nu am reușit să ne reconectăm la Moonlight. Vă rugăm să reîmprospătați pagina
-Failed to reconnect to the moonlight servers. The connection has been rejected;Nu s-a reușit reconectarea la serverele Moonlight. Conexiunea a fost respinsă
-We were unable to reconnect to moonlight. Most of the time this is caused by an update of moonlight. Please refresh the page;Nu am reușit să ne reconectăm la Moonlight. De cele mai multe ori, aceasta este cauzată de o actualizare a Moonlight. Vă rugăm să reîmprospătați pagina
-Verifying token, loading user data;Se verifică token-ul, se încarcă datele utilizatorului
-Reload config;Reîncarcă configurația
-Successfully reloading configuration;Configurația a fost reîncărcată cu succes
-Successfully reloaded configuration;Configurația a fost reîncărcată cu succes
-Flows;Fluxuri
-Add node;Adăugați nod
-Web system;Sistem web
-Servers with this image;Servere cu această imagine
-You need to specify a user;Trebuie să specificați un utilizator
-Import;Importați
-Export;Exportați
-Exporting;Se exportă
-Successfully imported image;Imaginea a fost importată cu succes
-Forge version;Versiune Forge
-Fabric version;Versiune Fabric
-Fabric loader version;Versiune Fabric Loader
-Rate;Evaluare
-Hey, can i borrow you for a second?;Hey, pot să te împrumut pentru o secundă?
-We want to improve our services and get a little bit of feedback how we are currently doing. Please leave us a rating;Vrem să îmbunătățim serviciile noastre și să obținem un pic de feedback despre modul în care facem în prezent. Vă rugăm să ne lăsați o evaluare
-Thanks for your rating;Mulțumim pentru evaluarea dvs.
-It would be really kind of you rating us on a external platform as it will help our project very much;Ar fi foarte amabil din partea dvs. să ne evaluați pe o platformă externă, deoarece va ajuta foarte mult proiectul nostru
-Close;Închide
-Rating saved;Evaluare salvată
-Group;Grup
-Beta;Beta
-Create a new group;Creați un grup nou
-Download WinSCP;Descărcați WinSCP
-Show connection details;Afișați detaliile de conexiune
-New;Nou
-New file;Fișier nou
-Connection details;Detalii de conexiune
-Malware;Program malware
-Create a new file;Creați un fișier nou
-Edit layout;Editați aspectul
-Uptime;Timp de funcționare
-Moonlight is online since;Moonlight este online de la
-User;Utilizator
-Databases;Baze de date
-Sesiuni;Sesiuni
-Active users;Utilizatori activi
-Search for plugins;Căutați plugin-uri
-Search;Căutare
-Searching;Se caută...
-Successfully installed gunshell;Gunshell a fost instalat cu succes
-Successfully installed fastasyncworldedit;FastAsyncWorldEdit a fost instalat cu succes
-Successfully installed minimotd;MiniMotd a fost instalat cu succes
-Moonlight health;Stare Moonlight
-Healthy;Sănătos
-Successfully saved file;Fișier salvat cu succes
-Unsorted servers;Servere nesortate
-Enter a new name;Introduceți un nume nou
-Sign in with;Conectare cu
-Make your services accessible through your own domain;Faceți serviciile dvs. accesibile prin propriul domeniu
-New group;Grup nou
-Finish editing layout;Finalizați editarea aspectului
-Remove group;Eliminați grupul
-Hidden in edit mode;Ascuns în modul de editare
-Enter your 2fa code here;Introduceți codul dvs. 2FA aici
-Two factor authentication;Autentificare în două factori
-Preferences;Preferințe
-Streamer mode;Mod streamer
-Scan the QR code and enter the code generated by the app you have scanned it in;Scanați codul QR și introduceți codul generat de aplicație
-Start scan;Începeți scanarea
-Results;Rezultate
-Scan in progress;Scanare în curs
-Debug;Depanare
-Save changes;Salvați modificările
-Delete domain;Ștergeți domeniul
-Python version;Versiune Python
-Python file;Fișier Python
-Select python file to execute on start;Selectați fișierul Python pentru a fi executat la pornire
-You have no webspaces;Nu aveți spații web
-We were not able to find any webspaces associated with your account;Nu am reușit să găsim spații web asociate contului dvs.
-Backup download successfully started;Descărcarea backup-ului a început cu succes
-Error from cloud panel;Eroare din panoul de control al norului
-Error from wings;Eroare din Wings
-Remove link;Elimină linkul
-Your account is linked to a discord account;Contul dvs. este legat de un cont Discord
-You are able to use features like the discord bot of moonlight;Puteți utiliza funcții precum botul Discord al Moonlight
-The password should be at least 8 characters long;Parola trebuie să aibă cel puțin 8 caractere
-The name should only consist of lower case characters or numbers;Numele ar trebui să conțină doar litere mici sau cifre
-The requested resource was not found;Resursa cerută nu a fost găsită
-We were not able to find the requested resource. This can have following reasons;Nu am putut găsi resursa solicitată. Acest lucru poate avea următoarele motive
-The resource was deleted;Resursa a fost ștearsă
-You have to permission to access this resource;Nu aveți permisiunea de a accesa această resursă
-You may have entered invalid data;Ați putut introduce date invalide
-A unknown bug occured;A apărut o eroare necunoscută
-An api was down and not proper handled;O interfață API a fost indisponibilă și nu a fost gestionată corespunzător
-A database with this name does already exist;O bază de date cu acest nume există deja
-Successfully installed quickshop-hikari;Quickshop-Hikari a fost instalat cu succes
-You need to enter a valid domain;Trebuie să introduceți un domeniu valid
-2fa code;Cod 2FA
-This feature is not available for;Această funcționalitate nu este disponibilă pentru
-Your account is currently not linked to discord;Contul dvs. nu este în prezent legat de Discord
-To use features like the discord bot, link your moonlight account with your discord account;Pentru a utiliza funcții precum botul Discord, legați-vă contul Moonlight de contul Discord
-Link account;Conectează contul
-Continue;Continuați
-Preparing;Pregătire
-Make sure you have installed one of the following apps on your smartphone and press continue;Asigurați-vă că ați instalat una dintre următoarele aplicații pe smartphone-ul dvs. și apăsați Continuare
-The max length for the name is 32 characters;Lungimea maximă a numelui este de 32 de caractere
-Successfully installed chunky;Chunky a fost instalat cu succes
-Successfully installed huskhomes;Huskhomes a fost instalat cu succes
-Successfully installed simply-farming;Simply-Farming a fost instalat cu succes
-You need to specify a first name;Trebuie să specificați un prenume
-You need to specify a last name;Trebuie să specificați un nume de familie
-You need to specify a password;Trebuie să specificați o parolă
-Please solve the captcha;Vă rugăm să rezolvați captcha
-The email is already in use;Adresa de email este deja folosită
-The dns records of your webspace do not point to the host system;Înregistrările DNS ale spațiului dvs. web nu se îndreaptă către sistemul gazdă
-Scan complete;Scanare completă
-Currently scanning:;În prezent se scanează:
-Successfully installed dynmap;Dynmap a fost instalat cu succes
-Successfully installed squaremap;Squaremap a fost instalat cu succes
-No web host found;Niciun gazdă web găsită
-No web host found to deploy to;Niciun gazdă web găsită pentru a implementa
-Successfully installed sleeper;Sleeper a fost instalat cu succes
-You need to enter a domain;Trebuie să introduceți un domeniu
-Enter a ip;Introduceți o adresă IP
-Ip Bans;Interdicții IP
-Ip;Adresă IP
-Successfully installed simple-voice-chat;Simple-Voice-Chat a fost instalat cu succes
-Successfully installed smithing-table-fix;Smithing-Table-Fix a fost instalat cu succes
-Successfully installed justplayer-tpa;Justplayer-TPA a fost instalat cu succes
-Successfully installed ishop;iShop a fost instalat cu succes
-Successfully installed lifestealre;Lifestealre a fost instalat cu succes
-Successfully installed lifeswap;Lifeswap a fost instalat cu succes
-Java version;Versiune Java
-Jar file;Fișier JAR
-Select jar to execute on start;Selectați fișierul JAR pentru a fi executat la pornire
-A website with this domain does already exist;Un website cu acest domeniu există deja
-Successfully installed discordsrv;DiscordSrv a fost instalat cu succes
-Reinstall;Reinstalați
-Reinstalling;Se reinstalează
-Successfully installed freedomchat;Freedomchat a fost instalat cu succes
-Leave empty for the default background image;Lăsați gol pentru imaginea de fundal implicită
-Background image url;URL-ul imaginii de fundal
-of CPU used;de CPU folosit
-memory used;memorie folosită
-163;163
-172;172
-Sentry;Sentry
-Sentry is enabled;Sentry este activat
-Successfully installed mobis-homes;Mobis-Homes a fost instalat cu succes
-Your moonlight account is disabled;Contul Moonlight este dezactivat
-Your moonlight account is currently disabled. But dont worry your data is still saved;Contul Moonlight este în prezent dezactivat. Dar nu vă faceți griji, datele dvs. sunt încă salvate
-You need to specify a email address;Trebuie să specificați o adresă de email
-A user with that email does already exist;Un utilizator cu această adresă de email există deja
-Successfully installed buildmode;Buildmode a fost instalat cu succes
-Successfully installed plasmo-voice;Plasmo-Voice a fost instalat cu succes
-157;157
-174;174
-158;158
-Webspace not found;Spațiu web nu găsit
-A webspace with that id cannot be found or you have no access for this webspace;Un spațiu web cu această ID nu poate fi găsit sau nu aveți acces la acest spațiu web
-No plugin download for your minecraft version found;Nu s-a găsit nicio descărcare de plugin pentru versiunea dvs. Minecraft
-Successfully installed gamemode-alias;Gamemode-Alias a fost instalat cu succes
-228;228
-User;Utilizator
-Send notification;Trimite notificare
-Successfully saved changes;Modificări salvate cu succes
-Archiving;Se arhivează
-Server is currently not archived;Serverul nu este arhivat în prezent
-Add allocation;Adaugă alocare
-231;231
-175;175
-Dotnet version;Versiune Dotnet
-Dll file;Fișier DLL
-Select dll to execute on start;Selectați fișierul DLL pentru a fi executat la pornire

+ 0 - 53
Moonlight/defaultstorage/resources/mail/login.html

@@ -1,53 +0,0 @@
-<!DOCTYPE html>
-<html lang="en">
-<head>
-    <meta charset="UTF-8">
-    <title>New moonlight login</title>
-</head>
-<body>
-<div style="background-color:#ffffff; padding: 45px 0 34px 0; border-radius: 24px; margin:40px auto; max-width: 600px;">
-    <table align="center" border="0" cellpadding="0" cellspacing="0" width="100%" height="auto"
-           style="border-collapse:collapse">
-        <tbody>
-        <tr>
-            <td align="center" valign="center" style="text-align:center; padding-bottom: 10px">
-                <div style="text-align:center; margin:0 15px 34px 15px">
-                    <div style="margin-bottom: 10px">
-                        <a href="https://endelon-hosting.de" rel="noopener" target="_blank">
-                            <img alt="Logo" src="https://moonlight.endelon-hosting.de/assets/media/logo/MoonFullText.png" style="height: 35px">
-                        </a>
-                    </div>
-                    <div style="font-size: 14px; font-weight: 500; margin-bottom: 27px; font-family:Arial,Helvetica,sans-serif;">
-                        <p style="margin-bottom:9px; color:#181C32; font-size: 22px; font-weight:700">Hey {{FirstName}}, there is a new login in your moonlight account</p>
-                        <p style="margin-bottom:2px; color:#7E8299">Here is all the data we collected</p>
-                        <p style="margin-bottom:2px; color:#7E8299">IP: {{Ip}}</p>
-                        <p style="margin-bottom:2px; color:#7E8299">Device: {{Device}}</p>
-                        <p style="margin-bottom:2px; color:#7E8299">Location: {{Location}}</p>
-                    </div>
-                    <a href="https://moonlight.endelon-hosting.de" target="_blank"
-                       style="background-color:#50cd89; border-radius:6px;display:inline-block; padding:11px 19px; color: #FFFFFF; font-size: 14px; font-weight:500;">Open Moonlight
-                    </a>
-                </div>
-            </td>
-        </tr>
-        <tr>
-            <td align="center" valign="center"
-                style="font-size: 13px; text-align:center; padding: 0 10px 10px 10px; font-weight: 500; color: #A1A5B7; font-family:Arial,Helvetica,sans-serif">
-                <p style="color:#181C32; font-size: 16px; font-weight: 600; margin-bottom:9px">You need help?</p>
-                <p style="margin-bottom:2px">We are happy to help!</p>
-                <p style="margin-bottom:4px">More information at
-                    <a href="https://endelon.link/support" rel="noopener" target="_blank" style="font-weight: 600">endelon.link/support</a>.
-                </p>
-            </td>
-        </tr>
-        <tr>
-            <td align="center" valign="center"
-                style="font-size: 13px; padding:0 15px; text-align:center; font-weight: 500; color: #A1A5B7;font-family:Arial,Helvetica,sans-serif">
-                <p>Copyright 2022 Endelon Hosting </p>
-            </td>
-        </tr>
-        </tbody>
-    </table>
-</div>
-</body>
-</html>

+ 0 - 53
Moonlight/defaultstorage/resources/mail/passwordChange.html

@@ -1,53 +0,0 @@
-<!DOCTYPE html>
-<html lang="en">
-<head>
-    <meta charset="UTF-8">
-    <title>Moonlight password change</title>
-</head>
-<body>
-<div style="background-color:#ffffff; padding: 45px 0 34px 0; border-radius: 24px; margin:40px auto; max-width: 600px;">
-    <table align="center" border="0" cellpadding="0" cellspacing="0" width="100%" height="auto"
-           style="border-collapse:collapse">
-        <tbody>
-        <tr>
-            <td align="center" valign="center" style="text-align:center; padding-bottom: 10px">
-                <div style="text-align:center; margin:0 15px 34px 15px">
-                    <div style="margin-bottom: 10px">
-                        <a href="https://endelon-hosting.de" rel="noopener" target="_blank">
-                            <img alt="Logo" src="https://moonlight.endelon-hosting.de/assets/media/logo/MoonFullText.png" style="height: 35px">
-                        </a>
-                    </div>
-                    <div style="font-size: 14px; font-weight: 500; margin-bottom: 27px; font-family:Arial,Helvetica,sans-serif;">
-                        <p style="margin-bottom:9px; color:#181C32; font-size: 22px; font-weight:700">Hey {{FirstName}}, your password has been changed</p>
-                        <p style="margin-bottom:2px; color:#7E8299">If this was not you please contact us. Also here is the data we collected.</p>
-                        <p style="margin-bottom:2px; color:#7E8299">IP: {{Ip}}</p>
-                        <p style="margin-bottom:2px; color:#7E8299">Device: {{Device}}</p>
-                        <p style="margin-bottom:2px; color:#7E8299">Location: {{Location}}</p>
-                    </div>
-                    <a href="https://moonlight.endelon-hosting.de" target="_blank"
-                       style="background-color:#50cd89; border-radius:6px;display:inline-block; padding:11px 19px; color: #FFFFFF; font-size: 14px; font-weight:500;">Open Moonlight
-                    </a>
-                </div>
-            </td>
-        </tr>
-        <tr>
-            <td align="center" valign="center"
-                style="font-size: 13px; text-align:center; padding: 0 10px 10px 10px; font-weight: 500; color: #A1A5B7; font-family:Arial,Helvetica,sans-serif">
-                <p style="color:#181C32; font-size: 16px; font-weight: 600; margin-bottom:9px">You need help?</p>
-                <p style="margin-bottom:2px">We are happy to help!</p>
-                <p style="margin-bottom:4px">More information at
-                    <a href="https://endelon.link/support" rel="noopener" target="_blank" style="font-weight: 600">endelon.link/support</a>.
-                </p>
-            </td>
-        </tr>
-        <tr>
-            <td align="center" valign="center"
-                style="font-size: 13px; padding:0 15px; text-align:center; font-weight: 500; color: #A1A5B7;font-family:Arial,Helvetica,sans-serif">
-                <p>Copyright 2023 Endelon Hosting </p>
-            </td>
-        </tr>
-        </tbody>
-    </table>
-</div>
-</body>
-</html>

+ 0 - 54
Moonlight/defaultstorage/resources/mail/passwordReset.html

@@ -1,54 +0,0 @@
-<!DOCTYPE html>
-<html lang="en">
-<head>
-    <meta charset="UTF-8">
-    <title>Moonlight password reset</title>
-</head>
-<body>
-<div style="background-color:#ffffff; padding: 45px 0 34px 0; border-radius: 24px; margin:40px auto; max-width: 600px;">
-    <table align="center" border="0" cellpadding="0" cellspacing="0" width="100%" height="auto"
-           style="border-collapse:collapse">
-        <tbody>
-        <tr>
-            <td align="center" valign="center" style="text-align:center; padding-bottom: 10px">
-                <div style="text-align:center; margin:0 15px 34px 15px">
-                    <div style="margin-bottom: 10px">
-                        <a href="https://endelon-hosting.de" rel="noopener" target="_blank">
-                            <img alt="Logo" src="https://moonlight.endelon-hosting.de/assets/media/logo/MoonFullText.png" style="height: 35px">
-                        </a>
-                    </div>
-                    <div style="font-size: 14px; font-weight: 500; margin-bottom: 27px; font-family:Arial,Helvetica,sans-serif;">
-                        <p style="margin-bottom:9px; color:#181C32; font-size: 22px; font-weight:700">Hey {{FirstName}}, your password has been resetted</p>
-                        <p style="margin-bottom:2px; color:#7E8299">Your new password is: <b>{{Password}}</b></p>
-                        <p style="margin-bottom:2px; color:#7E8299">If this was not you please contact us. Also here is the data we collected.</p>
-                        <p style="margin-bottom:2px; color:#7E8299">IP: {{Ip}}</p>
-                        <p style="margin-bottom:2px; color:#7E8299">Device: {{Device}}</p>
-                        <p style="margin-bottom:2px; color:#7E8299">Location: {{Location}}</p>
-                    </div>
-                    <a href="https://moonlight.endelon-hosting.de" target="_blank"
-                       style="background-color:#50cd89; border-radius:6px;display:inline-block; padding:11px 19px; color: #FFFFFF; font-size: 14px; font-weight:500;">Open Moonlight
-                    </a>
-                </div>
-            </td>
-        </tr>
-        <tr>
-            <td align="center" valign="center"
-                style="font-size: 13px; text-align:center; padding: 0 10px 10px 10px; font-weight: 500; color: #A1A5B7; font-family:Arial,Helvetica,sans-serif">
-                <p style="color:#181C32; font-size: 16px; font-weight: 600; margin-bottom:9px">You need help?</p>
-                <p style="margin-bottom:2px">We are happy to help!</p>
-                <p style="margin-bottom:4px">More information at
-                    <a href="https://endelon.link/support" rel="noopener" target="_blank" style="font-weight: 600">endelon.link/support</a>.
-                </p>
-            </td>
-        </tr>
-        <tr>
-            <td align="center" valign="center"
-                style="font-size: 13px; padding:0 15px; text-align:center; font-weight: 500; color: #A1A5B7;font-family:Arial,Helvetica,sans-serif">
-                <p>Copyright 2022 Endelon Hosting </p>
-            </td>
-        </tr>
-        </tbody>
-    </table>
-</div>
-</body>
-</html>

+ 0 - 50
Moonlight/defaultstorage/resources/mail/register.html

@@ -1,50 +0,0 @@
-<!DOCTYPE html>
-<html lang="en">
-<head>
-    <meta charset="UTF-8">
-    <title>Welcome</title>
-</head>
-<body>
-<div style="background-color:#ffffff; padding: 45px 0 34px 0; border-radius: 24px; margin:40px auto; max-width: 600px;">
-    <table align="center" border="0" cellpadding="0" cellspacing="0" width="100%" height="auto"
-           style="border-collapse:collapse">
-        <tbody>
-        <tr>
-            <td align="center" valign="center" style="text-align:center; padding-bottom: 10px">
-                <div style="text-align:center; margin:0 15px 34px 15px">
-                    <div style="margin-bottom: 10px">
-                        <a href="https://endelon-hosting.de" rel="noopener" target="_blank">
-                            <img alt="Logo" src="https://moonlight.endelon-hosting.de/assets/media/logo/MoonFullText.png" style="height: 35px">
-                        </a>
-                    </div>
-                    <div style="font-size: 14px; font-weight: 500; margin-bottom: 27px; font-family:Arial,Helvetica,sans-serif;">
-                        <p style="margin-bottom:9px; color:#181C32; font-size: 22px; font-weight:700">Hey {{FirstName}}, welcome to moonlight</p>
-                        <p style="margin-bottom:2px; color:#7E8299">We are happy to welcome you in ;)</p>
-                    </div>
-                    <a href="https://moonlight.endelon-hosting.de" target="_blank"
-                       style="background-color:#50cd89; border-radius:6px;display:inline-block; padding:11px 19px; color: #FFFFFF; font-size: 14px; font-weight:500;">Open Moonlight
-                    </a>
-                </div>
-            </td>
-        </tr>
-        <tr>
-            <td align="center" valign="center"
-                style="font-size: 13px; text-align:center; padding: 0 10px 10px 10px; font-weight: 500; color: #A1A5B7; font-family:Arial,Helvetica,sans-serif">
-                <p style="color:#181C32; font-size: 16px; font-weight: 600; margin-bottom:9px">You need help?</p>
-                <p style="margin-bottom:2px">We are happy to help!</p>
-                <p style="margin-bottom:4px">More information at
-                    <a href="https://endelon.link/support" rel="noopener" target="_blank" style="font-weight: 600">endelon.link/support</a>.
-                </p>
-            </td>
-        </tr>
-        <tr>
-            <td align="center" valign="center"
-                style="font-size: 13px; padding:0 15px; text-align:center; font-weight: 500; color: #A1A5B7;font-family:Arial,Helvetica,sans-serif">
-                <p>Copyright 2022 Endelon Hosting </p>
-            </td>
-        </tr>
-        </tbody>
-    </table>
-</div>
-</body>
-</html>

二进制
Moonlight/defaultstorage/resources/public/background/main.jpg


+ 0 - 14
Moonlight/defaultstorage/resources/public/images/logo.svg

@@ -1,14 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<svg width="256px" height="301px" viewBox="0 0 256 301" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid">
-    <defs>
-        <linearGradient x1="2.17771739%" y1="34.7938955%" x2="92.7221942%" y2="91.3419405%" id="linearGradient-1">
-            <stop stop-color="#41A7EF" offset="0%"></stop>
-            <stop stop-color="#813DDE" offset="54.2186236%"></stop>
-            <stop stop-color="#8F2EE2" offset="74.4988788%"></stop>
-            <stop stop-color="#A11CE6" offset="100%"></stop>
-        </linearGradient>
-    </defs>
-    <g>
-        <path d="M124.183681,101.699 C124.183681,66.515 136.256681,34.152 156.486681,8.525 C159.197681,5.092 156.787681,0.069 152.412681,0.012 C151.775681,0.004 151.136681,0 150.497681,0 C67.6206813,0 0.390681343,66.99 0.00168134279,149.775 C-0.386318657,232.369 66.4286813,300.195 149.019681,300.988 C189.884681,301.381 227.036681,285.484 254.376681,259.395 C257.519681,256.396 255.841681,251.082 251.548681,250.42 C179.413681,239.291 124.183681,176.949 124.183681,101.699" fill="url(#linearGradient-1)"></path>
-    </g>
-</svg>

二进制
Moonlight/defaultstorage/resources/public/images/logolong.png


+ 0 - 9
Moonlight/wwwroot/assets/css/snow.css

@@ -1,9 +0,0 @@
-.snow-canvas {
-    position: fixed;
-    top: 0;
-    left: 0;
-    right: 0;
-    bottom: 0;
-    z-index: -1;
-    pointer-events: none;
-}

+ 372 - 0
Moonlight/wwwroot/assets/css/toastr.css

@@ -0,0 +1,372 @@
+.toastr-title {
+    font-weight: bold;
+}
+
+.toastr-message {
+    -ms-word-wrap: break-word;
+    word-wrap: break-word;
+}
+
+.toastr-message a,
+.toastr-message label {
+    color: #FFFFFF;
+}
+
+.toastr-message a:hover {
+    color: #CCCCCC;
+    text-decoration: none;
+}
+
+.toastr-close-button {
+    position: relative;
+    right: -0.3em;
+    top: -0.3em;
+    float: right;
+    font-size: 20px;
+    font-weight: bold;
+    color: #FFFFFF;
+    -webkit-text-shadow: 0 1px 0 #ffffff;
+    text-shadow: 0 1px 0 #ffffff;
+    opacity: 0.8;
+    -ms-filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=80);
+    filter: alpha(opacity=80);
+    line-height: 1;
+}
+
+.toastr-close-button:hover,
+.toastr-close-button:focus {
+    color: #000000;
+    text-decoration: none;
+    cursor: pointer;
+    opacity: 0.4;
+    -ms-filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=40);
+    filter: alpha(opacity=40);
+}
+
+.rtl .toastr-close-button {
+    left: -0.3em;
+    float: left;
+    right: 0.3em;
+}
+
+/*Additional properties for button version
+ iOS requires the button element instead of an anchor tag.
+ If you want the anchor version, it requires `href="#"`.*/
+button.toastr-close-button {
+    padding: 0;
+    cursor: pointer;
+    background: transparent;
+    border: 0;
+    -webkit-appearance: none;
+}
+
+.toastr-top-center {
+    top: 0;
+    right: 0;
+    width: 100%;
+}
+
+.toastr-top-center-nav-margin {
+    top: 65px;
+    right: 0;
+    width: 100%;
+}
+
+.toastr-bottom-center {
+    bottom: 0;
+    right: 0;
+    width: 100%;
+}
+
+.toastr-top-full-width {
+    top: 0;
+    right: 0;
+    width: 100%;
+}
+
+.toastr-top-full-width-nav-margin {
+    top: 65px;
+    right: 0;
+    width: 100%;
+}
+
+.toastr-bottom-full-width {
+    bottom: 0;
+    right: 0;
+    width: 100%;
+}
+
+.toastr-top-left {
+    top: 12px;
+    left: 12px;
+}
+
+.toastr-top-left-nav-margin {
+    top: 65px;
+    left: 12px;
+}
+
+.toastr-top-right {
+    top: 12px;
+    right: 12px;
+}
+
+.toastr-top-right-nav-margin {
+    top: 65px;
+    right: 12px;
+}
+
+.toastr-bottom-right {
+    right: 12px;
+    bottom: 12px;
+}
+
+.toastr-bottom-left {
+    bottom: 12px;
+    left: 12px;
+}
+
+#toastr-container {
+    position: fixed;
+    z-index: 999999;
+    pointer-events: none;
+    /*overrides*/
+}
+
+#toastr-container * {
+    -moz-box-sizing: border-box;
+    -webkit-box-sizing: border-box;
+    box-sizing: border-box;
+}
+
+#toastr-container > div {
+    position: relative;
+    pointer-events: auto;
+    overflow: hidden;
+    margin: 0 0 6px;
+    padding: 15px 15px 15px 50px;
+    width: 300px;
+    -moz-border-radius: 3px 3px 3px 3px;
+    -webkit-border-radius: 3px 3px 3px 3px;
+    border-radius: 3px 3px 3px 3px;
+    background-position: 15px center;
+    background-repeat: no-repeat;
+    -moz-box-shadow: 0 0 12px #999999;
+    -webkit-box-shadow: 0 0 12px #999999;
+    box-shadow: 0 0 12px #999999;
+    color: #FFFFFF;
+    opacity: 0.8;
+    -ms-filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=80);
+    filter: alpha(opacity=80);
+}
+
+#toastr-container > div.rtl {
+    direction: rtl;
+    padding: 15px 50px 15px 15px;
+    background-position: right 15px center;
+}
+
+#toastr-container > div:hover {
+    -moz-box-shadow: 0 0 12px #000000;
+    -webkit-box-shadow: 0 0 12px #000000;
+    box-shadow: 0 0 12px #000000;
+    opacity: 1;
+    -ms-filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=100);
+    filter: alpha(opacity=100);
+    cursor: pointer;
+}
+
+#toastr-container > .toastr-info {
+    background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAGwSURBVEhLtZa9SgNBEMc9sUxxRcoUKSzSWIhXpFMhhYWFhaBg4yPYiWCXZxBLERsLRS3EQkEfwCKdjWJAwSKCgoKCcudv4O5YLrt7EzgXhiU3/4+b2ckmwVjJSpKkQ6wAi4gwhT+z3wRBcEz0yjSseUTrcRyfsHsXmD0AmbHOC9Ii8VImnuXBPglHpQ5wwSVM7sNnTG7Za4JwDdCjxyAiH3nyA2mtaTJufiDZ5dCaqlItILh1NHatfN5skvjx9Z38m69CgzuXmZgVrPIGE763Jx9qKsRozWYw6xOHdER+nn2KkO+Bb+UV5CBN6WC6QtBgbRVozrahAbmm6HtUsgtPC19tFdxXZYBOfkbmFJ1VaHA1VAHjd0pp70oTZzvR+EVrx2Ygfdsq6eu55BHYR8hlcki+n+kERUFG8BrA0BwjeAv2M8WLQBtcy+SD6fNsmnB3AlBLrgTtVW1c2QN4bVWLATaIS60J2Du5y1TiJgjSBvFVZgTmwCU+dAZFoPxGEEs8nyHC9Bwe2GvEJv2WXZb0vjdyFT4Cxk3e/kIqlOGoVLwwPevpYHT+00T+hWwXDf4AJAOUqWcDhbwAAAAASUVORK5CYII=") !important;
+}
+
+#toastr-container > .toastr-error {
+    background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAHOSURBVEhLrZa/SgNBEMZzh0WKCClSCKaIYOED+AAKeQQLG8HWztLCImBrYadgIdY+gIKNYkBFSwu7CAoqCgkkoGBI/E28PdbLZmeDLgzZzcx83/zZ2SSXC1j9fr+I1Hq93g2yxH4iwM1vkoBWAdxCmpzTxfkN2RcyZNaHFIkSo10+8kgxkXIURV5HGxTmFuc75B2RfQkpxHG8aAgaAFa0tAHqYFfQ7Iwe2yhODk8+J4C7yAoRTWI3w/4klGRgR4lO7Rpn9+gvMyWp+uxFh8+H+ARlgN1nJuJuQAYvNkEnwGFck18Er4q3egEc/oO+mhLdKgRyhdNFiacC0rlOCbhNVz4H9FnAYgDBvU3QIioZlJFLJtsoHYRDfiZoUyIxqCtRpVlANq0EU4dApjrtgezPFad5S19Wgjkc0hNVnuF4HjVA6C7QrSIbylB+oZe3aHgBsqlNqKYH48jXyJKMuAbiyVJ8KzaB3eRc0pg9VwQ4niFryI68qiOi3AbjwdsfnAtk0bCjTLJKr6mrD9g8iq/S/B81hguOMlQTnVyG40wAcjnmgsCNESDrjme7wfftP4P7SP4N3CJZdvzoNyGq2c/HWOXJGsvVg+RA/k2MC/wN6I2YA2Pt8GkAAAAASUVORK5CYII=") !important;
+}
+
+#toastr-container > .toastr-success {
+    background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAADsSURBVEhLY2AYBfQMgf///3P8+/evAIgvA/FsIF+BavYDDWMBGroaSMMBiE8VC7AZDrIFaMFnii3AZTjUgsUUWUDA8OdAH6iQbQEhw4HyGsPEcKBXBIC4ARhex4G4BsjmweU1soIFaGg/WtoFZRIZdEvIMhxkCCjXIVsATV6gFGACs4Rsw0EGgIIH3QJYJgHSARQZDrWAB+jawzgs+Q2UO49D7jnRSRGoEFRILcdmEMWGI0cm0JJ2QpYA1RDvcmzJEWhABhD/pqrL0S0CWuABKgnRki9lLseS7g2AlqwHWQSKH4oKLrILpRGhEQCw2LiRUIa4lwAAAABJRU5ErkJggg==") !important;
+}
+
+#toastr-container > .toastr-warning {
+    background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAGYSURBVEhL5ZSvTsNQFMbXZGICMYGYmJhAQIJAICYQPAACiSDB8AiICQQJT4CqQEwgJvYASAQCiZiYmJhAIBATCARJy+9rTsldd8sKu1M0+dLb057v6/lbq/2rK0mS/TRNj9cWNAKPYIJII7gIxCcQ51cvqID+GIEX8ASG4B1bK5gIZFeQfoJdEXOfgX4QAQg7kH2A65yQ87lyxb27sggkAzAuFhbbg1K2kgCkB1bVwyIR9m2L7PRPIhDUIXgGtyKw575yz3lTNs6X4JXnjV+LKM/m3MydnTbtOKIjtz6VhCBq4vSm3ncdrD2lk0VgUXSVKjVDJXJzijW1RQdsU7F77He8u68koNZTz8Oz5yGa6J3H3lZ0xYgXBK2QymlWWA+RWnYhskLBv2vmE+hBMCtbA7KX5drWyRT/2JsqZ2IvfB9Y4bWDNMFbJRFmC9E74SoS0CqulwjkC0+5bpcV1CZ8NMej4pjy0U+doDQsGyo1hzVJttIjhQ7GnBtRFN1UarUlH8F3xict+HY07rEzoUGPlWcjRFRr4/gChZgc3ZL2d8oAAAAASUVORK5CYII=") !important;
+}
+
+#toastr-container.toastr-top-center-nav-margin > div,
+#toastr-container.toastr-top-center > div,
+#toastr-container.toastr-bottom-center > div {
+    width: 300px;
+    margin-left: auto;
+    margin-right: auto;
+}
+
+#toastr-container.toastr-top-full-width-nav-margin > div,
+#toastr-container.toastr-top-full-width > div,
+#toastr-container.toastr-bottom-full-width > div {
+    width: 96%;
+    margin-left: auto;
+    margin-right: auto;
+}
+
+.toastr {
+    background-color: #030303;
+}
+
+.toastr-success {
+    background-color: #51A351;
+}
+
+.toastr-error {
+    background-color: #BD362F;
+}
+
+.toastr-info {
+    background-color: #2F96B4;
+}
+
+.toastr-warning {
+    background-color: #F89406;
+}
+
+.toastr-progress {
+    position: absolute;
+    left: 0;
+    bottom: 0;
+    height: 4px;
+    background-color: #000000;
+    opacity: 0.4;
+    -ms-filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=40);
+    filter: alpha(opacity=40);
+}
+
+/*Responsive Design*/
+@media all and (max-width: 240px) {
+    #toastr-container > div {
+        padding: 8px 8px 8px 50px;
+        width: 11em;
+    }
+    #toastr-container > div.rtl {
+        padding: 8px 50px 8px 8px;
+    }
+    #toastr-container .toastr-close-button {
+        right: -0.2em;
+        top: -0.2em;
+    }
+    #toastr-container .rtl .toastr-close-button {
+        left: -0.2em;
+        right: 0.2em;
+    }
+}
+@media all and (min-width: 241px) and (max-width: 480px) {
+    #toastr-container > div {
+        padding: 8px 8px 8px 50px;
+        width: 18em;
+    }
+    #toastr-container > div.rtl {
+        padding: 8px 50px 8px 8px;
+    }
+    #toastr-container .toastr-close-button {
+        right: -0.2em;
+        top: -0.2em;
+    }
+    #toastr-container .rtl .toastr-close-button {
+        left: -0.2em;
+        right: 0.2em;
+    }
+}
+@media all and (min-width: 481px) and (max-width: 768px) {
+    #toastr-container > div {
+        padding: 15px 15px 15px 50px;
+        width: 25em;
+    }
+    #toastr-container > div.rtl {
+        padding: 15px 50px 15px 15px;
+    }
+}
+
+.toastr {
+    background-position: 1.5rem center /*rtl:calc(100% - 1.5rem) center*/ !important;
+    box-shadow: var(--kt-dropdown-box-shadow) !important;
+    border-radius: 0.475rem !important;
+    border: 0 !important;
+    background-color: var(--kt-gray-100);
+    color: var(--kt-gray-700);
+    padding: 1.25rem 1.25rem 1.25rem 4.5rem !important;
+}
+.toastr .toastr-close-button {
+    outline: none !important;
+    font-size: 0;
+    width: 0.85rem;
+    height: 0.85rem;
+}
+.toastr .toastr-title {
+    font-size: 1.15rem;
+    font-weight: 500;
+}
+.toastr .toastr-title + .toastr-message {
+    margin-top: 0.25rem;
+}
+.toastr .toastr-message {
+    font-size: 1rem;
+    font-weight: 400;
+}
+.toastr.toastr-success {
+    background-color: var(--kt-success);
+    color: var(--kt-success-inverse);
+}
+.toastr.toastr-success .toastr-close-button {
+    mask-repeat: no-repeat;
+    mask-position: center;
+    -webkit-mask-repeat: no-repeat;
+    -webkit-mask-position: center;
+    background-color: var(--kt-success-inverse);
+    -webkit-mask-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='var%28--kt-success-inverse%29'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e");
+    mask-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='var%28--kt-success-inverse%29'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e");
+}
+.toastr.toastr-info {
+    background-color: var(--kt-info);
+    color: var(--kt-info-inverse);
+}
+.toastr.toastr-info .toastr-close-button {
+    mask-repeat: no-repeat;
+    mask-position: center;
+    -webkit-mask-repeat: no-repeat;
+    -webkit-mask-position: center;
+    background-color: var(--kt-info-inverse);
+    -webkit-mask-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='var%28--kt-info-inverse%29'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e");
+    mask-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='var%28--kt-info-inverse%29'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e");
+}
+.toastr.toastr-warning {
+    background-color: var(--kt-warning);
+    color: var(--kt-warning-inverse);
+}
+.toastr.toastr-warning .toastr-close-button {
+    mask-repeat: no-repeat;
+    mask-position: center;
+    -webkit-mask-repeat: no-repeat;
+    -webkit-mask-position: center;
+    background-color: var(--kt-warning-inverse);
+    -webkit-mask-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='var%28--kt-warning-inverse%29'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e");
+    mask-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='var%28--kt-warning-inverse%29'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e");
+}
+.toastr.toastr-error {
+    background-color: var(--kt-danger);
+    color: var(--kt-danger-inverse);
+}
+.toastr.toastr-error .toastr-close-button {
+    mask-repeat: no-repeat;
+    mask-position: center;
+    -webkit-mask-repeat: no-repeat;
+    -webkit-mask-position: center;
+    background-color: var(--kt-danger-inverse);
+    -webkit-mask-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='var%28--kt-danger-inverse%29'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e");
+    mask-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='var%28--kt-danger-inverse%29'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e");
+}
+
+.toastr-top-center {
+    top: 12px;
+}
+
+.toastr-bottom-center {
+    bottom: 12px;
+}

文件差异内容过多而无法显示
+ 5 - 0
Moonlight/wwwroot/assets/js/apexcharts.js


文件差异内容过多而无法显示
+ 5 - 0
Moonlight/wwwroot/assets/js/bootstrap.min.js


+ 7522 - 0
Moonlight/wwwroot/assets/js/draggable.bundle.js

@@ -0,0 +1,7522 @@
+(function webpackUniversalModuleDefinition(root, factory) {
+	if(typeof exports === 'object' && typeof module === 'object')
+		module.exports = factory();
+	else if(typeof define === 'function' && define.amd)
+		define("Draggable", [], factory);
+	else if(typeof exports === 'object')
+		exports["Draggable"] = factory();
+	else
+		root["Draggable"] = factory();
+})(window, function() {
+return /******/ (function(modules) { // webpackBootstrap
+/******/ 	// The module cache
+/******/ 	var installedModules = {};
+/******/
+/******/ 	// The require function
+/******/ 	function __webpack_require__(moduleId) {
+/******/
+/******/ 		// Check if module is in cache
+/******/ 		if(installedModules[moduleId]) {
+/******/ 			return installedModules[moduleId].exports;
+/******/ 		}
+/******/ 		// Create a new module (and put it into the cache)
+/******/ 		var module = installedModules[moduleId] = {
+/******/ 			i: moduleId,
+/******/ 			l: false,
+/******/ 			exports: {}
+/******/ 		};
+/******/
+/******/ 		// Execute the module function
+/******/ 		modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
+/******/
+/******/ 		// Flag the module as loaded
+/******/ 		module.l = true;
+/******/
+/******/ 		// Return the exports of the module
+/******/ 		return module.exports;
+/******/ 	}
+/******/
+/******/
+/******/ 	// expose the modules object (__webpack_modules__)
+/******/ 	__webpack_require__.m = modules;
+/******/
+/******/ 	// expose the module cache
+/******/ 	__webpack_require__.c = installedModules;
+/******/
+/******/ 	// define getter function for harmony exports
+/******/ 	__webpack_require__.d = function(exports, name, getter) {
+/******/ 		if(!__webpack_require__.o(exports, name)) {
+/******/ 			Object.defineProperty(exports, name, { enumerable: true, get: getter });
+/******/ 		}
+/******/ 	};
+/******/
+/******/ 	// define __esModule on exports
+/******/ 	__webpack_require__.r = function(exports) {
+/******/ 		if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
+/******/ 			Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
+/******/ 		}
+/******/ 		Object.defineProperty(exports, '__esModule', { value: true });
+/******/ 	};
+/******/
+/******/ 	// create a fake namespace object
+/******/ 	// mode & 1: value is a module id, require it
+/******/ 	// mode & 2: merge all properties of value into the ns
+/******/ 	// mode & 4: return value when already ns object
+/******/ 	// mode & 8|1: behave like require
+/******/ 	__webpack_require__.t = function(value, mode) {
+/******/ 		if(mode & 1) value = __webpack_require__(value);
+/******/ 		if(mode & 8) return value;
+/******/ 		if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
+/******/ 		var ns = Object.create(null);
+/******/ 		__webpack_require__.r(ns);
+/******/ 		Object.defineProperty(ns, 'default', { enumerable: true, value: value });
+/******/ 		if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));
+/******/ 		return ns;
+/******/ 	};
+/******/
+/******/ 	// getDefaultExport function for compatibility with non-harmony modules
+/******/ 	__webpack_require__.n = function(module) {
+/******/ 		var getter = module && module.__esModule ?
+/******/ 			function getDefault() { return module['default']; } :
+/******/ 			function getModuleExports() { return module; };
+/******/ 		__webpack_require__.d(getter, 'a', getter);
+/******/ 		return getter;
+/******/ 	};
+/******/
+/******/ 	// Object.prototype.hasOwnProperty.call
+/******/ 	__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
+/******/
+/******/ 	// __webpack_public_path__
+/******/ 	__webpack_require__.p = "";
+/******/
+/******/
+/******/ 	// Load entry module and return exports
+/******/ 	return __webpack_require__(__webpack_require__.s = 72);
+/******/ })
+/************************************************************************/
+/******/ ([
+/* 0 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+
+var _AbstractPlugin = __webpack_require__(66);
+
+var _AbstractPlugin2 = _interopRequireDefault(_AbstractPlugin);
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+exports.default = _AbstractPlugin2.default;
+
+/***/ }),
+/* 1 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+
+var _AbstractEvent = __webpack_require__(70);
+
+var _AbstractEvent2 = _interopRequireDefault(_AbstractEvent);
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+exports.default = _AbstractEvent2.default;
+
+/***/ }),
+/* 2 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+
+var _closest = __webpack_require__(57);
+
+Object.defineProperty(exports, 'closest', {
+  enumerable: true,
+  get: function () {
+    return _interopRequireDefault(_closest).default;
+  }
+});
+
+var _requestNextAnimationFrame = __webpack_require__(55);
+
+Object.defineProperty(exports, 'requestNextAnimationFrame', {
+  enumerable: true,
+  get: function () {
+    return _interopRequireDefault(_requestNextAnimationFrame).default;
+  }
+});
+
+var _distance = __webpack_require__(53);
+
+Object.defineProperty(exports, 'distance', {
+  enumerable: true,
+  get: function () {
+    return _interopRequireDefault(_distance).default;
+  }
+});
+
+var _touchCoords = __webpack_require__(51);
+
+Object.defineProperty(exports, 'touchCoords', {
+  enumerable: true,
+  get: function () {
+    return _interopRequireDefault(_touchCoords).default;
+  }
+});
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+/***/ }),
+/* 3 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+
+var _SensorEvent = __webpack_require__(46);
+
+Object.keys(_SensorEvent).forEach(function (key) {
+  if (key === "default" || key === "__esModule") return;
+  Object.defineProperty(exports, key, {
+    enumerable: true,
+    get: function () {
+      return _SensorEvent[key];
+    }
+  });
+});
+
+/***/ }),
+/* 4 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+
+var _Sensor = __webpack_require__(49);
+
+var _Sensor2 = _interopRequireDefault(_Sensor);
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+exports.default = _Sensor2.default;
+
+/***/ }),
+/* 5 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+
+var _DragEvent = __webpack_require__(14);
+
+Object.keys(_DragEvent).forEach(function (key) {
+  if (key === "default" || key === "__esModule") return;
+  Object.defineProperty(exports, key, {
+    enumerable: true,
+    get: function () {
+      return _DragEvent[key];
+    }
+  });
+});
+
+var _DraggableEvent = __webpack_require__(13);
+
+Object.keys(_DraggableEvent).forEach(function (key) {
+  if (key === "default" || key === "__esModule") return;
+  Object.defineProperty(exports, key, {
+    enumerable: true,
+    get: function () {
+      return _DraggableEvent[key];
+    }
+  });
+});
+
+var _Plugins = __webpack_require__(12);
+
+Object.keys(_Plugins).forEach(function (key) {
+  if (key === "default" || key === "__esModule") return;
+  Object.defineProperty(exports, key, {
+    enumerable: true,
+    get: function () {
+      return _Plugins[key];
+    }
+  });
+});
+
+var _Sensors = __webpack_require__(6);
+
+Object.keys(_Sensors).forEach(function (key) {
+  if (key === "default" || key === "__esModule") return;
+  Object.defineProperty(exports, key, {
+    enumerable: true,
+    get: function () {
+      return _Sensors[key];
+    }
+  });
+});
+
+var _Draggable = __webpack_require__(39);
+
+var _Draggable2 = _interopRequireDefault(_Draggable);
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+exports.default = _Draggable2.default;
+
+/***/ }),
+/* 6 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+
+var _Sensor = __webpack_require__(4);
+
+Object.defineProperty(exports, 'Sensor', {
+  enumerable: true,
+  get: function () {
+    return _interopRequireDefault(_Sensor).default;
+  }
+});
+
+var _MouseSensor = __webpack_require__(48);
+
+Object.defineProperty(exports, 'MouseSensor', {
+  enumerable: true,
+  get: function () {
+    return _interopRequireDefault(_MouseSensor).default;
+  }
+});
+
+var _TouchSensor = __webpack_require__(45);
+
+Object.defineProperty(exports, 'TouchSensor', {
+  enumerable: true,
+  get: function () {
+    return _interopRequireDefault(_TouchSensor).default;
+  }
+});
+
+var _DragSensor = __webpack_require__(43);
+
+Object.defineProperty(exports, 'DragSensor', {
+  enumerable: true,
+  get: function () {
+    return _interopRequireDefault(_DragSensor).default;
+  }
+});
+
+var _ForceTouchSensor = __webpack_require__(41);
+
+Object.defineProperty(exports, 'ForceTouchSensor', {
+  enumerable: true,
+  get: function () {
+    return _interopRequireDefault(_ForceTouchSensor).default;
+  }
+});
+
+var _SensorEvent = __webpack_require__(3);
+
+Object.keys(_SensorEvent).forEach(function (key) {
+  if (key === "default" || key === "__esModule") return;
+  Object.defineProperty(exports, key, {
+    enumerable: true,
+    get: function () {
+      return _SensorEvent[key];
+    }
+  });
+});
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+/***/ }),
+/* 7 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+
+var _SnappableEvent = __webpack_require__(20);
+
+Object.keys(_SnappableEvent).forEach(function (key) {
+  if (key === "default" || key === "__esModule") return;
+  Object.defineProperty(exports, key, {
+    enumerable: true,
+    get: function () {
+      return _SnappableEvent[key];
+    }
+  });
+});
+
+/***/ }),
+/* 8 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+
+var _CollidableEvent = __webpack_require__(25);
+
+Object.keys(_CollidableEvent).forEach(function (key) {
+  if (key === "default" || key === "__esModule") return;
+  Object.defineProperty(exports, key, {
+    enumerable: true,
+    get: function () {
+      return _CollidableEvent[key];
+    }
+  });
+});
+
+/***/ }),
+/* 9 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+
+var _SortableEvent = __webpack_require__(29);
+
+Object.keys(_SortableEvent).forEach(function (key) {
+  if (key === "default" || key === "__esModule") return;
+  Object.defineProperty(exports, key, {
+    enumerable: true,
+    get: function () {
+      return _SortableEvent[key];
+    }
+  });
+});
+
+/***/ }),
+/* 10 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+
+var _SwappableEvent = __webpack_require__(32);
+
+Object.keys(_SwappableEvent).forEach(function (key) {
+  if (key === "default" || key === "__esModule") return;
+  Object.defineProperty(exports, key, {
+    enumerable: true,
+    get: function () {
+      return _SwappableEvent[key];
+    }
+  });
+});
+
+/***/ }),
+/* 11 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+
+var _DroppableEvent = __webpack_require__(35);
+
+Object.keys(_DroppableEvent).forEach(function (key) {
+  if (key === "default" || key === "__esModule") return;
+  Object.defineProperty(exports, key, {
+    enumerable: true,
+    get: function () {
+      return _DroppableEvent[key];
+    }
+  });
+});
+
+/***/ }),
+/* 12 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+
+var _Announcement = __webpack_require__(68);
+
+Object.defineProperty(exports, 'Announcement', {
+  enumerable: true,
+  get: function () {
+    return _interopRequireDefault(_Announcement).default;
+  }
+});
+Object.defineProperty(exports, 'defaultAnnouncementOptions', {
+  enumerable: true,
+  get: function () {
+    return _Announcement.defaultOptions;
+  }
+});
+
+var _Focusable = __webpack_require__(65);
+
+Object.defineProperty(exports, 'Focusable', {
+  enumerable: true,
+  get: function () {
+    return _interopRequireDefault(_Focusable).default;
+  }
+});
+
+var _Mirror = __webpack_require__(63);
+
+Object.defineProperty(exports, 'Mirror', {
+  enumerable: true,
+  get: function () {
+    return _interopRequireDefault(_Mirror).default;
+  }
+});
+Object.defineProperty(exports, 'defaultMirrorOptions', {
+  enumerable: true,
+  get: function () {
+    return _Mirror.defaultOptions;
+  }
+});
+
+var _Scrollable = __webpack_require__(59);
+
+Object.defineProperty(exports, 'Scrollable', {
+  enumerable: true,
+  get: function () {
+    return _interopRequireDefault(_Scrollable).default;
+  }
+});
+Object.defineProperty(exports, 'defaultScrollableOptions', {
+  enumerable: true,
+  get: function () {
+    return _Scrollable.defaultOptions;
+  }
+});
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+/***/ }),
+/* 13 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+
+var _DraggableEvent = __webpack_require__(69);
+
+Object.keys(_DraggableEvent).forEach(function (key) {
+  if (key === "default" || key === "__esModule") return;
+  Object.defineProperty(exports, key, {
+    enumerable: true,
+    get: function () {
+      return _DraggableEvent[key];
+    }
+  });
+});
+
+/***/ }),
+/* 14 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+
+var _DragEvent = __webpack_require__(71);
+
+Object.keys(_DragEvent).forEach(function (key) {
+  if (key === "default" || key === "__esModule") return;
+  Object.defineProperty(exports, key, {
+    enumerable: true,
+    get: function () {
+      return _DragEvent[key];
+    }
+  });
+});
+
+/***/ }),
+/* 15 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+exports.defaultOptions = undefined;
+
+var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
+
+var _AbstractPlugin = __webpack_require__(0);
+
+var _AbstractPlugin2 = _interopRequireDefault(_AbstractPlugin);
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+const onSortableSorted = Symbol('onSortableSorted');
+const onSortableSort = Symbol('onSortableSort');
+
+/**
+ * SortAnimation default options
+ * @property {Object} defaultOptions
+ * @property {Number} defaultOptions.duration
+ * @property {String} defaultOptions.easingFunction
+ * @type {Object}
+ */
+const defaultOptions = exports.defaultOptions = {
+  duration: 150,
+  easingFunction: 'ease-in-out'
+};
+
+/**
+ * SortAnimation plugin adds sort animation for sortable
+ * @class SortAnimation
+ * @module SortAnimation
+ * @extends AbstractPlugin
+ */
+class SortAnimation extends _AbstractPlugin2.default {
+  /**
+   * SortAnimation constructor.
+   * @constructs SortAnimation
+   * @param {Draggable} draggable - Draggable instance
+   */
+  constructor(draggable) {
+    super(draggable);
+
+    /**
+     * SortAnimation options
+     * @property {Object} options
+     * @property {Number} defaultOptions.duration
+     * @property {String} defaultOptions.easingFunction
+     * @type {Object}
+     */
+    this.options = _extends({}, defaultOptions, this.getOptions());
+
+    /**
+     * Last animation frame
+     * @property {Number} lastAnimationFrame
+     * @type {Number}
+     */
+    this.lastAnimationFrame = null;
+    this.lastElements = [];
+
+    this[onSortableSorted] = this[onSortableSorted].bind(this);
+    this[onSortableSort] = this[onSortableSort].bind(this);
+  }
+
+  /**
+   * Attaches plugins event listeners
+   */
+  attach() {
+    this.draggable.on('sortable:sort', this[onSortableSort]);
+    this.draggable.on('sortable:sorted', this[onSortableSorted]);
+  }
+
+  /**
+   * Detaches plugins event listeners
+   */
+  detach() {
+    this.draggable.off('sortable:sort', this[onSortableSort]);
+    this.draggable.off('sortable:sorted', this[onSortableSorted]);
+  }
+
+  /**
+   * Returns options passed through draggable
+   * @return {Object}
+   */
+  getOptions() {
+    return this.draggable.options.sortAnimation || {};
+  }
+
+  /**
+   * Sortable sort handler
+   * @param {SortableSortEvent} sortableEvent
+   * @private
+   */
+  [onSortableSort]({ dragEvent }) {
+    const { sourceContainer } = dragEvent;
+    const elements = this.draggable.getDraggableElementsForContainer(sourceContainer);
+    this.lastElements = Array.from(elements).map(el => {
+      return {
+        domEl: el,
+        offsetTop: el.offsetTop,
+        offsetLeft: el.offsetLeft
+      };
+    });
+  }
+
+  /**
+   * Sortable sorted handler
+   * @param {SortableSortedEvent} sortableEvent
+   * @private
+   */
+  [onSortableSorted]({ oldIndex, newIndex }) {
+    if (oldIndex === newIndex) {
+      return;
+    }
+
+    const effectedElements = [];
+    let start;
+    let end;
+    let num;
+    if (oldIndex > newIndex) {
+      start = newIndex;
+      end = oldIndex - 1;
+      num = 1;
+    } else {
+      start = oldIndex + 1;
+      end = newIndex;
+      num = -1;
+    }
+
+    for (let i = start; i <= end; i++) {
+      const from = this.lastElements[i];
+      const to = this.lastElements[i + num];
+      effectedElements.push({ from, to });
+    }
+    cancelAnimationFrame(this.lastAnimationFrame);
+
+    // Can be done in a separate frame
+    this.lastAnimationFrame = requestAnimationFrame(() => {
+      effectedElements.forEach(element => animate(element, this.options));
+    });
+  }
+}
+
+exports.default = SortAnimation; /**
+                                  * Animates two elements
+                                  * @param {Object} element
+                                  * @param {Object} element.from
+                                  * @param {Object} element.to
+                                  * @param {Object} options
+                                  * @param {Number} options.duration
+                                  * @param {String} options.easingFunction
+                                  * @private
+                                  */
+
+function animate({ from, to }, { duration, easingFunction }) {
+  const domEl = from.domEl;
+  const x = from.offsetLeft - to.offsetLeft;
+  const y = from.offsetTop - to.offsetTop;
+
+  domEl.style.pointerEvents = 'none';
+  domEl.style.transform = `translate3d(${x}px, ${y}px, 0)`;
+
+  requestAnimationFrame(() => {
+    domEl.addEventListener('transitionend', resetElementOnTransitionEnd);
+    domEl.style.transition = `transform ${duration}ms ${easingFunction}`;
+    domEl.style.transform = '';
+  });
+}
+
+/**
+ * Resets animation style properties after animation has completed
+ * @param {Event} event
+ * @private
+ */
+function resetElementOnTransitionEnd(event) {
+  event.target.style.transition = '';
+  event.target.style.pointerEvents = '';
+  event.target.removeEventListener('transitionend', resetElementOnTransitionEnd);
+}
+
+/***/ }),
+/* 16 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+exports.defaultOptions = undefined;
+
+var _SortAnimation = __webpack_require__(15);
+
+var _SortAnimation2 = _interopRequireDefault(_SortAnimation);
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+exports.default = _SortAnimation2.default;
+exports.defaultOptions = _SortAnimation.defaultOptions;
+
+/***/ }),
+/* 17 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+exports.defaultOptions = undefined;
+
+var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
+
+var _AbstractPlugin = __webpack_require__(0);
+
+var _AbstractPlugin2 = _interopRequireDefault(_AbstractPlugin);
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+const onSortableSorted = Symbol('onSortableSorted');
+
+/**
+ * SwapAnimation default options
+ * @property {Object} defaultOptions
+ * @property {Number} defaultOptions.duration
+ * @property {String} defaultOptions.easingFunction
+ * @property {Boolean} defaultOptions.horizontal
+ * @type {Object}
+ */
+const defaultOptions = exports.defaultOptions = {
+  duration: 150,
+  easingFunction: 'ease-in-out',
+  horizontal: false
+};
+
+/**
+ * SwapAnimation plugin adds swap animations for sortable
+ * @class SwapAnimation
+ * @module SwapAnimation
+ * @extends AbstractPlugin
+ */
+class SwapAnimation extends _AbstractPlugin2.default {
+  /**
+   * SwapAnimation constructor.
+   * @constructs SwapAnimation
+   * @param {Draggable} draggable - Draggable instance
+   */
+  constructor(draggable) {
+    super(draggable);
+
+    /**
+     * SwapAnimation options
+     * @property {Object} options
+     * @property {Number} defaultOptions.duration
+     * @property {String} defaultOptions.easingFunction
+     * @type {Object}
+     */
+    this.options = _extends({}, defaultOptions, this.getOptions());
+
+    /**
+     * Last animation frame
+     * @property {Number} lastAnimationFrame
+     * @type {Number}
+     */
+    this.lastAnimationFrame = null;
+
+    this[onSortableSorted] = this[onSortableSorted].bind(this);
+  }
+
+  /**
+   * Attaches plugins event listeners
+   */
+  attach() {
+    this.draggable.on('sortable:sorted', this[onSortableSorted]);
+  }
+
+  /**
+   * Detaches plugins event listeners
+   */
+  detach() {
+    this.draggable.off('sortable:sorted', this[onSortableSorted]);
+  }
+
+  /**
+   * Returns options passed through draggable
+   * @return {Object}
+   */
+  getOptions() {
+    return this.draggable.options.swapAnimation || {};
+  }
+
+  /**
+   * Sortable sorted handler
+   * @param {SortableSortedEvent} sortableEvent
+   * @private
+   */
+  [onSortableSorted]({ oldIndex, newIndex, dragEvent }) {
+    const { source, over } = dragEvent;
+
+    cancelAnimationFrame(this.lastAnimationFrame);
+
+    // Can be done in a separate frame
+    this.lastAnimationFrame = requestAnimationFrame(() => {
+      if (oldIndex >= newIndex) {
+        animate(source, over, this.options);
+      } else {
+        animate(over, source, this.options);
+      }
+    });
+  }
+}
+
+exports.default = SwapAnimation; /**
+                                  * Animates two elements
+                                  * @param {HTMLElement} from
+                                  * @param {HTMLElement} to
+                                  * @param {Object} options
+                                  * @param {Number} options.duration
+                                  * @param {String} options.easingFunction
+                                  * @param {String} options.horizontal
+                                  * @private
+                                  */
+
+function animate(from, to, { duration, easingFunction, horizontal }) {
+  for (const element of [from, to]) {
+    element.style.pointerEvents = 'none';
+  }
+
+  if (horizontal) {
+    const width = from.offsetWidth;
+    from.style.transform = `translate3d(${width}px, 0, 0)`;
+    to.style.transform = `translate3d(-${width}px, 0, 0)`;
+  } else {
+    const height = from.offsetHeight;
+    from.style.transform = `translate3d(0, ${height}px, 0)`;
+    to.style.transform = `translate3d(0, -${height}px, 0)`;
+  }
+
+  requestAnimationFrame(() => {
+    for (const element of [from, to]) {
+      element.addEventListener('transitionend', resetElementOnTransitionEnd);
+      element.style.transition = `transform ${duration}ms ${easingFunction}`;
+      element.style.transform = '';
+    }
+  });
+}
+
+/**
+ * Resets animation style properties after animation has completed
+ * @param {Event} event
+ * @private
+ */
+function resetElementOnTransitionEnd(event) {
+  event.target.style.transition = '';
+  event.target.style.pointerEvents = '';
+  event.target.removeEventListener('transitionend', resetElementOnTransitionEnd);
+}
+
+/***/ }),
+/* 18 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+exports.defaultOptions = undefined;
+
+var _SwapAnimation = __webpack_require__(17);
+
+var _SwapAnimation2 = _interopRequireDefault(_SwapAnimation);
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+exports.default = _SwapAnimation2.default;
+exports.defaultOptions = _SwapAnimation.defaultOptions;
+
+/***/ }),
+/* 19 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+
+var _AbstractPlugin = __webpack_require__(0);
+
+var _AbstractPlugin2 = _interopRequireDefault(_AbstractPlugin);
+
+var _SnappableEvent = __webpack_require__(7);
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+const onDragStart = Symbol('onDragStart');
+const onDragStop = Symbol('onDragStop');
+const onDragOver = Symbol('onDragOver');
+const onDragOut = Symbol('onDragOut');
+const onMirrorCreated = Symbol('onMirrorCreated');
+const onMirrorDestroy = Symbol('onMirrorDestroy');
+
+/**
+ * Snappable plugin which snaps draggable elements into place
+ * @class Snappable
+ * @module Snappable
+ * @extends AbstractPlugin
+ */
+class Snappable extends _AbstractPlugin2.default {
+  /**
+   * Snappable constructor.
+   * @constructs Snappable
+   * @param {Draggable} draggable - Draggable instance
+   */
+  constructor(draggable) {
+    super(draggable);
+
+    /**
+     * Keeps track of the first source element
+     * @property {HTMLElement|null} firstSource
+     */
+    this.firstSource = null;
+
+    /**
+     * Keeps track of the mirror element
+     * @property {HTMLElement} mirror
+     */
+    this.mirror = null;
+
+    this[onDragStart] = this[onDragStart].bind(this);
+    this[onDragStop] = this[onDragStop].bind(this);
+    this[onDragOver] = this[onDragOver].bind(this);
+    this[onDragOut] = this[onDragOut].bind(this);
+    this[onMirrorCreated] = this[onMirrorCreated].bind(this);
+    this[onMirrorDestroy] = this[onMirrorDestroy].bind(this);
+  }
+
+  /**
+   * Attaches plugins event listeners
+   */
+  attach() {
+    this.draggable.on('drag:start', this[onDragStart]).on('drag:stop', this[onDragStop]).on('drag:over', this[onDragOver]).on('drag:out', this[onDragOut]).on('droppable:over', this[onDragOver]).on('droppable:out', this[onDragOut]).on('mirror:created', this[onMirrorCreated]).on('mirror:destroy', this[onMirrorDestroy]);
+  }
+
+  /**
+   * Detaches plugins event listeners
+   */
+  detach() {
+    this.draggable.off('drag:start', this[onDragStart]).off('drag:stop', this[onDragStop]).off('drag:over', this[onDragOver]).off('drag:out', this[onDragOut]).off('droppable:over', this[onDragOver]).off('droppable:out', this[onDragOut]).off('mirror:created', this[onMirrorCreated]).off('mirror:destroy', this[onMirrorDestroy]);
+  }
+
+  /**
+   * Drag start handler
+   * @private
+   * @param {DragStartEvent} event - Drag start event
+   */
+  [onDragStart](event) {
+    if (event.canceled()) {
+      return;
+    }
+
+    this.firstSource = event.source;
+  }
+
+  /**
+   * Drag stop handler
+   * @private
+   * @param {DragStopEvent} event - Drag stop event
+   */
+  [onDragStop]() {
+    this.firstSource = null;
+  }
+
+  /**
+   * Drag over handler
+   * @private
+   * @param {DragOverEvent|DroppableOverEvent} event - Drag over event
+   */
+  [onDragOver](event) {
+    if (event.canceled()) {
+      return;
+    }
+
+    const source = event.source || event.dragEvent.source;
+
+    if (source === this.firstSource) {
+      this.firstSource = null;
+      return;
+    }
+
+    const snapInEvent = new _SnappableEvent.SnapInEvent({
+      dragEvent: event,
+      snappable: event.over || event.droppable
+    });
+
+    this.draggable.trigger(snapInEvent);
+
+    if (snapInEvent.canceled()) {
+      return;
+    }
+
+    if (this.mirror) {
+      this.mirror.style.display = 'none';
+    }
+
+    source.classList.remove(this.draggable.getClassNameFor('source:dragging'));
+    source.classList.add(this.draggable.getClassNameFor('source:placed'));
+
+    // Need to cancel this in drag out
+    setTimeout(() => {
+      source.classList.remove(this.draggable.getClassNameFor('source:placed'));
+    }, this.draggable.options.placedTimeout);
+  }
+
+  /**
+   * Drag out handler
+   * @private
+   * @param {DragOutEvent|DroppableOutEvent} event - Drag out event
+   */
+  [onDragOut](event) {
+    if (event.canceled()) {
+      return;
+    }
+
+    const source = event.source || event.dragEvent.source;
+
+    const snapOutEvent = new _SnappableEvent.SnapOutEvent({
+      dragEvent: event,
+      snappable: event.over || event.droppable
+    });
+
+    this.draggable.trigger(snapOutEvent);
+
+    if (snapOutEvent.canceled()) {
+      return;
+    }
+
+    if (this.mirror) {
+      this.mirror.style.display = '';
+    }
+
+    source.classList.add(this.draggable.getClassNameFor('source:dragging'));
+  }
+
+  /**
+   * Mirror created handler
+   * @param {MirrorCreatedEvent} mirrorEvent
+   * @private
+   */
+  [onMirrorCreated]({ mirror }) {
+    this.mirror = mirror;
+  }
+
+  /**
+   * Mirror destroy handler
+   * @param {MirrorDestroyEvent} mirrorEvent
+   * @private
+   */
+  [onMirrorDestroy]() {
+    this.mirror = null;
+  }
+}
+exports.default = Snappable;
+
+/***/ }),
+/* 20 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+exports.SnapOutEvent = exports.SnapInEvent = exports.SnapEvent = undefined;
+
+var _AbstractEvent = __webpack_require__(1);
+
+var _AbstractEvent2 = _interopRequireDefault(_AbstractEvent);
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+/**
+ * Base snap event
+ * @class SnapEvent
+ * @module SnapEvent
+ * @extends AbstractEvent
+ */
+class SnapEvent extends _AbstractEvent2.default {
+
+  /**
+   * Drag event that triggered this snap event
+   * @property dragEvent
+   * @type {DragEvent}
+   * @readonly
+   */
+  get dragEvent() {
+    return this.data.dragEvent;
+  }
+
+  /**
+   * Snappable element
+   * @property snappable
+   * @type {HTMLElement}
+   * @readonly
+   */
+  get snappable() {
+    return this.data.snappable;
+  }
+}
+
+exports.SnapEvent = SnapEvent; /**
+                                * Snap in event
+                                * @class SnapInEvent
+                                * @module SnapInEvent
+                                * @extends SnapEvent
+                                */
+
+SnapEvent.type = 'snap';
+class SnapInEvent extends SnapEvent {}
+
+exports.SnapInEvent = SnapInEvent; /**
+                                    * Snap out event
+                                    * @class SnapOutEvent
+                                    * @module SnapOutEvent
+                                    * @extends SnapEvent
+                                    */
+
+SnapInEvent.type = 'snap:in';
+SnapInEvent.cancelable = true;
+class SnapOutEvent extends SnapEvent {}
+exports.SnapOutEvent = SnapOutEvent;
+SnapOutEvent.type = 'snap:out';
+SnapOutEvent.cancelable = true;
+
+/***/ }),
+/* 21 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+
+var _SnappableEvent = __webpack_require__(7);
+
+Object.keys(_SnappableEvent).forEach(function (key) {
+  if (key === "default" || key === "__esModule") return;
+  Object.defineProperty(exports, key, {
+    enumerable: true,
+    get: function () {
+      return _SnappableEvent[key];
+    }
+  });
+});
+
+var _Snappable = __webpack_require__(19);
+
+var _Snappable2 = _interopRequireDefault(_Snappable);
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+exports.default = _Snappable2.default;
+
+/***/ }),
+/* 22 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+exports.defaultOptions = undefined;
+
+var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
+
+var _AbstractPlugin = __webpack_require__(0);
+
+var _AbstractPlugin2 = _interopRequireDefault(_AbstractPlugin);
+
+var _utils = __webpack_require__(2);
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+const onMirrorCreated = Symbol('onMirrorCreated');
+const onMirrorDestroy = Symbol('onMirrorDestroy');
+const onDragOver = Symbol('onDragOver');
+const resize = Symbol('resize');
+
+/**
+ * ResizeMirror default options
+ * @property {Object} defaultOptions
+ * @type {Object}
+ */
+const defaultOptions = exports.defaultOptions = {};
+
+/**
+ * The ResizeMirror plugin resizes the mirror element to the dimensions of the draggable element that the mirror is hovering over
+ * @class ResizeMirror
+ * @module ResizeMirror
+ * @extends AbstractPlugin
+ */
+class ResizeMirror extends _AbstractPlugin2.default {
+  /**
+   * ResizeMirror constructor.
+   * @constructs ResizeMirror
+   * @param {Draggable} draggable - Draggable instance
+   */
+  constructor(draggable) {
+    super(draggable);
+
+    /**
+     * ResizeMirror options
+     * @property {Object} options
+     * @type {Object}
+     */
+    this.options = _extends({}, defaultOptions, this.getOptions());
+
+    /**
+     * ResizeMirror remembers the last width when resizing the mirror
+     * to avoid additional writes to the DOM
+     * @property {number} lastWidth
+     */
+    this.lastWidth = 0;
+
+    /**
+     * ResizeMirror remembers the last height when resizing the mirror
+     * to avoid additional writes to the DOM
+     * @property {number} lastHeight
+     */
+    this.lastHeight = 0;
+
+    /**
+     * Keeps track of the mirror element
+     * @property {HTMLElement} mirror
+     */
+    this.mirror = null;
+
+    this[onMirrorCreated] = this[onMirrorCreated].bind(this);
+    this[onMirrorDestroy] = this[onMirrorDestroy].bind(this);
+    this[onDragOver] = this[onDragOver].bind(this);
+  }
+
+  /**
+   * Attaches plugins event listeners
+   */
+  attach() {
+    this.draggable.on('mirror:created', this[onMirrorCreated]).on('drag:over', this[onDragOver]).on('drag:over:container', this[onDragOver]);
+  }
+
+  /**
+   * Detaches plugins event listeners
+   */
+  detach() {
+    this.draggable.off('mirror:created', this[onMirrorCreated]).off('mirror:destroy', this[onMirrorDestroy]).off('drag:over', this[onDragOver]).off('drag:over:container', this[onDragOver]);
+  }
+
+  /**
+   * Returns options passed through draggable
+   * @return {Object}
+   */
+  getOptions() {
+    return this.draggable.options.resizeMirror || {};
+  }
+
+  /**
+   * Mirror created handler
+   * @param {MirrorCreatedEvent} mirrorEvent
+   * @private
+   */
+  [onMirrorCreated]({ mirror }) {
+    this.mirror = mirror;
+  }
+
+  /**
+   * Mirror destroy handler
+   * @param {MirrorDestroyEvent} mirrorEvent
+   * @private
+   */
+  [onMirrorDestroy]() {
+    this.mirror = null;
+  }
+
+  /**
+   * Drag over handler
+   * @param {DragOverEvent | DragOverContainer} dragEvent
+   * @private
+   */
+  [onDragOver](dragEvent) {
+    this[resize](dragEvent);
+  }
+
+  /**
+   * Resize function for
+   * @param {DragOverEvent | DragOverContainer} dragEvent
+   * @private
+   */
+  [resize]({ overContainer, over }) {
+    requestAnimationFrame(() => {
+      if (!this.mirror.parentNode) {
+        return;
+      }
+
+      if (this.mirror.parentNode !== overContainer) {
+        overContainer.appendChild(this.mirror);
+      }
+
+      const overElement = over || this.draggable.getDraggableElementsForContainer(overContainer)[0];
+
+      if (!overElement) {
+        return;
+      }
+
+      (0, _utils.requestNextAnimationFrame)(() => {
+        const overRect = overElement.getBoundingClientRect();
+
+        if (this.lastHeight === overRect.height && this.lastWidth === overRect.width) {
+          return;
+        }
+
+        this.mirror.style.width = `${overRect.width}px`;
+        this.mirror.style.height = `${overRect.height}px`;
+
+        this.lastWidth = overRect.width;
+        this.lastHeight = overRect.height;
+      });
+    });
+  }
+}
+exports.default = ResizeMirror;
+
+/***/ }),
+/* 23 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+exports.defaultOptions = undefined;
+
+var _ResizeMirror = __webpack_require__(22);
+
+var _ResizeMirror2 = _interopRequireDefault(_ResizeMirror);
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+exports.default = _ResizeMirror2.default;
+exports.defaultOptions = _ResizeMirror.defaultOptions;
+
+/***/ }),
+/* 24 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+
+var _AbstractPlugin = __webpack_require__(0);
+
+var _AbstractPlugin2 = _interopRequireDefault(_AbstractPlugin);
+
+var _utils = __webpack_require__(2);
+
+var _CollidableEvent = __webpack_require__(8);
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+const onDragMove = Symbol('onDragMove');
+const onDragStop = Symbol('onDragStop');
+const onRequestAnimationFrame = Symbol('onRequestAnimationFrame');
+
+/**
+ * Collidable plugin which detects colliding elements while dragging
+ * @class Collidable
+ * @module Collidable
+ * @extends AbstractPlugin
+ */
+class Collidable extends _AbstractPlugin2.default {
+  /**
+   * Collidable constructor.
+   * @constructs Collidable
+   * @param {Draggable} draggable - Draggable instance
+   */
+  constructor(draggable) {
+    super(draggable);
+
+    /**
+     * Keeps track of currently colliding elements
+     * @property {HTMLElement|null} currentlyCollidingElement
+     * @type {HTMLElement|null}
+     */
+    this.currentlyCollidingElement = null;
+
+    /**
+     * Keeps track of currently colliding elements
+     * @property {HTMLElement|null} lastCollidingElement
+     * @type {HTMLElement|null}
+     */
+    this.lastCollidingElement = null;
+
+    /**
+     * Animation frame for finding colliding elements
+     * @property {Number|null} currentAnimationFrame
+     * @type {Number|null}
+     */
+    this.currentAnimationFrame = null;
+
+    this[onDragMove] = this[onDragMove].bind(this);
+    this[onDragStop] = this[onDragStop].bind(this);
+    this[onRequestAnimationFrame] = this[onRequestAnimationFrame].bind(this);
+  }
+
+  /**
+   * Attaches plugins event listeners
+   */
+  attach() {
+    this.draggable.on('drag:move', this[onDragMove]).on('drag:stop', this[onDragStop]);
+  }
+
+  /**
+   * Detaches plugins event listeners
+   */
+  detach() {
+    this.draggable.off('drag:move', this[onDragMove]).off('drag:stop', this[onDragStop]);
+  }
+
+  /**
+   * Returns current collidables based on `collidables` option
+   * @return {HTMLElement[]}
+   */
+  getCollidables() {
+    const collidables = this.draggable.options.collidables;
+
+    if (typeof collidables === 'string') {
+      return Array.prototype.slice.call(document.querySelectorAll(collidables));
+    } else if (collidables instanceof NodeList || collidables instanceof Array) {
+      return Array.prototype.slice.call(collidables);
+    } else if (collidables instanceof HTMLElement) {
+      return [collidables];
+    } else if (typeof collidables === 'function') {
+      return collidables();
+    } else {
+      return [];
+    }
+  }
+
+  /**
+   * Drag move handler
+   * @private
+   * @param {DragMoveEvent} event - Drag move event
+   */
+  [onDragMove](event) {
+    const target = event.sensorEvent.target;
+
+    this.currentAnimationFrame = requestAnimationFrame(this[onRequestAnimationFrame](target));
+
+    if (this.currentlyCollidingElement) {
+      event.cancel();
+    }
+
+    const collidableInEvent = new _CollidableEvent.CollidableInEvent({
+      dragEvent: event,
+      collidingElement: this.currentlyCollidingElement
+    });
+
+    const collidableOutEvent = new _CollidableEvent.CollidableOutEvent({
+      dragEvent: event,
+      collidingElement: this.lastCollidingElement
+    });
+
+    const enteringCollidable = Boolean(this.currentlyCollidingElement && this.lastCollidingElement !== this.currentlyCollidingElement);
+    const leavingCollidable = Boolean(!this.currentlyCollidingElement && this.lastCollidingElement);
+
+    if (enteringCollidable) {
+      if (this.lastCollidingElement) {
+        this.draggable.trigger(collidableOutEvent);
+      }
+
+      this.draggable.trigger(collidableInEvent);
+    } else if (leavingCollidable) {
+      this.draggable.trigger(collidableOutEvent);
+    }
+
+    this.lastCollidingElement = this.currentlyCollidingElement;
+  }
+
+  /**
+   * Drag stop handler
+   * @private
+   * @param {DragStopEvent} event - Drag stop event
+   */
+  [onDragStop](event) {
+    const lastCollidingElement = this.currentlyCollidingElement || this.lastCollidingElement;
+    const collidableOutEvent = new _CollidableEvent.CollidableOutEvent({
+      dragEvent: event,
+      collidingElement: lastCollidingElement
+    });
+
+    if (lastCollidingElement) {
+      this.draggable.trigger(collidableOutEvent);
+    }
+
+    this.lastCollidingElement = null;
+    this.currentlyCollidingElement = null;
+  }
+
+  /**
+   * Animation frame function
+   * @private
+   * @param {HTMLElement} target - Current move target
+   * @return {Function}
+   */
+  [onRequestAnimationFrame](target) {
+    return () => {
+      const collidables = this.getCollidables();
+      this.currentlyCollidingElement = (0, _utils.closest)(target, element => collidables.includes(element));
+    };
+  }
+}
+exports.default = Collidable;
+
+/***/ }),
+/* 25 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+exports.CollidableOutEvent = exports.CollidableInEvent = exports.CollidableEvent = undefined;
+
+var _AbstractEvent = __webpack_require__(1);
+
+var _AbstractEvent2 = _interopRequireDefault(_AbstractEvent);
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+/**
+ * Base collidable event
+ * @class CollidableEvent
+ * @module CollidableEvent
+ * @extends AbstractEvent
+ */
+class CollidableEvent extends _AbstractEvent2.default {
+
+  /**
+   * Drag event that triggered this colliable event
+   * @property dragEvent
+   * @type {DragEvent}
+   * @readonly
+   */
+  get dragEvent() {
+    return this.data.dragEvent;
+  }
+}
+
+exports.CollidableEvent = CollidableEvent; /**
+                                            * Collidable in event
+                                            * @class CollidableInEvent
+                                            * @module CollidableInEvent
+                                            * @extends CollidableEvent
+                                            */
+
+CollidableEvent.type = 'collidable';
+class CollidableInEvent extends CollidableEvent {
+
+  /**
+   * Element you are currently colliding with
+   * @property collidingElement
+   * @type {HTMLElement}
+   * @readonly
+   */
+  get collidingElement() {
+    return this.data.collidingElement;
+  }
+}
+
+exports.CollidableInEvent = CollidableInEvent; /**
+                                                * Collidable out event
+                                                * @class CollidableOutEvent
+                                                * @module CollidableOutEvent
+                                                * @extends CollidableEvent
+                                                */
+
+CollidableInEvent.type = 'collidable:in';
+class CollidableOutEvent extends CollidableEvent {
+
+  /**
+   * Element you were previously colliding with
+   * @property collidingElement
+   * @type {HTMLElement}
+   * @readonly
+   */
+  get collidingElement() {
+    return this.data.collidingElement;
+  }
+}
+exports.CollidableOutEvent = CollidableOutEvent;
+CollidableOutEvent.type = 'collidable:out';
+
+/***/ }),
+/* 26 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+
+var _CollidableEvent = __webpack_require__(8);
+
+Object.keys(_CollidableEvent).forEach(function (key) {
+  if (key === "default" || key === "__esModule") return;
+  Object.defineProperty(exports, key, {
+    enumerable: true,
+    get: function () {
+      return _CollidableEvent[key];
+    }
+  });
+});
+
+var _Collidable = __webpack_require__(24);
+
+var _Collidable2 = _interopRequireDefault(_Collidable);
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+exports.default = _Collidable2.default;
+
+/***/ }),
+/* 27 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+
+var _Collidable = __webpack_require__(26);
+
+Object.defineProperty(exports, 'Collidable', {
+  enumerable: true,
+  get: function () {
+    return _interopRequireDefault(_Collidable).default;
+  }
+});
+
+var _ResizeMirror = __webpack_require__(23);
+
+Object.defineProperty(exports, 'ResizeMirror', {
+  enumerable: true,
+  get: function () {
+    return _interopRequireDefault(_ResizeMirror).default;
+  }
+});
+Object.defineProperty(exports, 'defaultResizeMirrorOptions', {
+  enumerable: true,
+  get: function () {
+    return _ResizeMirror.defaultOptions;
+  }
+});
+
+var _Snappable = __webpack_require__(21);
+
+Object.defineProperty(exports, 'Snappable', {
+  enumerable: true,
+  get: function () {
+    return _interopRequireDefault(_Snappable).default;
+  }
+});
+
+var _SwapAnimation = __webpack_require__(18);
+
+Object.defineProperty(exports, 'SwapAnimation', {
+  enumerable: true,
+  get: function () {
+    return _interopRequireDefault(_SwapAnimation).default;
+  }
+});
+Object.defineProperty(exports, 'defaultSwapAnimationOptions', {
+  enumerable: true,
+  get: function () {
+    return _SwapAnimation.defaultOptions;
+  }
+});
+
+var _SortAnimation = __webpack_require__(16);
+
+Object.defineProperty(exports, 'SortAnimation', {
+  enumerable: true,
+  get: function () {
+    return _interopRequireDefault(_SortAnimation).default;
+  }
+});
+Object.defineProperty(exports, 'defaultSortAnimationOptions', {
+  enumerable: true,
+  get: function () {
+    return _SortAnimation.defaultOptions;
+  }
+});
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+/***/ }),
+/* 28 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+
+var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
+
+var _Draggable = __webpack_require__(5);
+
+var _Draggable2 = _interopRequireDefault(_Draggable);
+
+var _SortableEvent = __webpack_require__(9);
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+const onDragStart = Symbol('onDragStart');
+const onDragOverContainer = Symbol('onDragOverContainer');
+const onDragOver = Symbol('onDragOver');
+const onDragStop = Symbol('onDragStop');
+
+/**
+ * Returns announcement message when a Draggable element has been sorted with another Draggable element
+ * or moved into a new container
+ * @param {SortableSortedEvent} sortableEvent
+ * @return {String}
+ */
+function onSortableSortedDefaultAnnouncement({ dragEvent }) {
+  const sourceText = dragEvent.source.textContent.trim() || dragEvent.source.id || 'sortable element';
+
+  if (dragEvent.over) {
+    const overText = dragEvent.over.textContent.trim() || dragEvent.over.id || 'sortable element';
+    const isFollowing = dragEvent.source.compareDocumentPosition(dragEvent.over) & Node.DOCUMENT_POSITION_FOLLOWING;
+
+    if (isFollowing) {
+      return `Placed ${sourceText} after ${overText}`;
+    } else {
+      return `Placed ${sourceText} before ${overText}`;
+    }
+  } else {
+    // need to figure out how to compute container name
+    return `Placed ${sourceText} into a different container`;
+  }
+}
+
+/**
+ * @const {Object} defaultAnnouncements
+ * @const {Function} defaultAnnouncements['sortable:sorted']
+ */
+const defaultAnnouncements = {
+  'sortable:sorted': onSortableSortedDefaultAnnouncement
+};
+
+/**
+ * Sortable is built on top of Draggable and allows sorting of draggable elements. Sortable will keep
+ * track of the original index and emits the new index as you drag over draggable elements.
+ * @class Sortable
+ * @module Sortable
+ * @extends Draggable
+ */
+class Sortable extends _Draggable2.default {
+  /**
+   * Sortable constructor.
+   * @constructs Sortable
+   * @param {HTMLElement[]|NodeList|HTMLElement} containers - Sortable containers
+   * @param {Object} options - Options for Sortable
+   */
+  constructor(containers = [], options = {}) {
+    super(containers, _extends({}, options, {
+      announcements: _extends({}, defaultAnnouncements, options.announcements || {})
+    }));
+
+    /**
+     * start index of source on drag start
+     * @property startIndex
+     * @type {Number}
+     */
+    this.startIndex = null;
+
+    /**
+     * start container on drag start
+     * @property startContainer
+     * @type {HTMLElement}
+     * @default null
+     */
+    this.startContainer = null;
+
+    this[onDragStart] = this[onDragStart].bind(this);
+    this[onDragOverContainer] = this[onDragOverContainer].bind(this);
+    this[onDragOver] = this[onDragOver].bind(this);
+    this[onDragStop] = this[onDragStop].bind(this);
+
+    this.on('drag:start', this[onDragStart]).on('drag:over:container', this[onDragOverContainer]).on('drag:over', this[onDragOver]).on('drag:stop', this[onDragStop]);
+  }
+
+  /**
+   * Destroys Sortable instance.
+   */
+  destroy() {
+    super.destroy();
+
+    this.off('drag:start', this[onDragStart]).off('drag:over:container', this[onDragOverContainer]).off('drag:over', this[onDragOver]).off('drag:stop', this[onDragStop]);
+  }
+
+  /**
+   * Returns true index of element within its container during drag operation, i.e. excluding mirror and original source
+   * @param {HTMLElement} element - An element
+   * @return {Number}
+   */
+  index(element) {
+    return this.getDraggableElementsForContainer(element.parentNode).indexOf(element);
+  }
+
+  /**
+   * Drag start handler
+   * @private
+   * @param {DragStartEvent} event - Drag start event
+   */
+  [onDragStart](event) {
+    this.startContainer = event.source.parentNode;
+    this.startIndex = this.index(event.source);
+
+    const sortableStartEvent = new _SortableEvent.SortableStartEvent({
+      dragEvent: event,
+      startIndex: this.startIndex,
+      startContainer: this.startContainer
+    });
+
+    this.trigger(sortableStartEvent);
+
+    if (sortableStartEvent.canceled()) {
+      event.cancel();
+    }
+  }
+
+  /**
+   * Drag over container handler
+   * @private
+   * @param {DragOverContainerEvent} event - Drag over container event
+   */
+  [onDragOverContainer](event) {
+    if (event.canceled()) {
+      return;
+    }
+
+    const { source, over, overContainer } = event;
+    const oldIndex = this.index(source);
+
+    const sortableSortEvent = new _SortableEvent.SortableSortEvent({
+      dragEvent: event,
+      currentIndex: oldIndex,
+      source,
+      over
+    });
+
+    this.trigger(sortableSortEvent);
+
+    if (sortableSortEvent.canceled()) {
+      return;
+    }
+
+    const children = this.getDraggableElementsForContainer(overContainer);
+    const moves = move({ source, over, overContainer, children });
+
+    if (!moves) {
+      return;
+    }
+
+    const { oldContainer, newContainer } = moves;
+    const newIndex = this.index(event.source);
+
+    const sortableSortedEvent = new _SortableEvent.SortableSortedEvent({
+      dragEvent: event,
+      oldIndex,
+      newIndex,
+      oldContainer,
+      newContainer
+    });
+
+    this.trigger(sortableSortedEvent);
+  }
+
+  /**
+   * Drag over handler
+   * @private
+   * @param {DragOverEvent} event - Drag over event
+   */
+  [onDragOver](event) {
+    if (event.over === event.originalSource || event.over === event.source) {
+      return;
+    }
+
+    const { source, over, overContainer } = event;
+    const oldIndex = this.index(source);
+
+    const sortableSortEvent = new _SortableEvent.SortableSortEvent({
+      dragEvent: event,
+      currentIndex: oldIndex,
+      source,
+      over
+    });
+
+    this.trigger(sortableSortEvent);
+
+    if (sortableSortEvent.canceled()) {
+      return;
+    }
+
+    const children = this.getDraggableElementsForContainer(overContainer);
+    const moves = move({ source, over, overContainer, children });
+
+    if (!moves) {
+      return;
+    }
+
+    const { oldContainer, newContainer } = moves;
+    const newIndex = this.index(source);
+
+    const sortableSortedEvent = new _SortableEvent.SortableSortedEvent({
+      dragEvent: event,
+      oldIndex,
+      newIndex,
+      oldContainer,
+      newContainer
+    });
+
+    this.trigger(sortableSortedEvent);
+  }
+
+  /**
+   * Drag stop handler
+   * @private
+   * @param {DragStopEvent} event - Drag stop event
+   */
+  [onDragStop](event) {
+    const sortableStopEvent = new _SortableEvent.SortableStopEvent({
+      dragEvent: event,
+      oldIndex: this.startIndex,
+      newIndex: this.index(event.source),
+      oldContainer: this.startContainer,
+      newContainer: event.source.parentNode
+    });
+
+    this.trigger(sortableStopEvent);
+
+    this.startIndex = null;
+    this.startContainer = null;
+  }
+}
+
+exports.default = Sortable;
+function index(element) {
+  return Array.prototype.indexOf.call(element.parentNode.children, element);
+}
+
+function move({ source, over, overContainer, children }) {
+  const emptyOverContainer = !children.length;
+  const differentContainer = source.parentNode !== overContainer;
+  const sameContainer = over && !differentContainer;
+
+  if (emptyOverContainer) {
+    return moveInsideEmptyContainer(source, overContainer);
+  } else if (sameContainer) {
+    return moveWithinContainer(source, over);
+  } else if (differentContainer) {
+    return moveOutsideContainer(source, over, overContainer);
+  } else {
+    return null;
+  }
+}
+
+function moveInsideEmptyContainer(source, overContainer) {
+  const oldContainer = source.parentNode;
+
+  overContainer.appendChild(source);
+
+  return { oldContainer, newContainer: overContainer };
+}
+
+function moveWithinContainer(source, over) {
+  const oldIndex = index(source);
+  const newIndex = index(over);
+
+  if (oldIndex < newIndex) {
+    source.parentNode.insertBefore(source, over.nextElementSibling);
+  } else {
+    source.parentNode.insertBefore(source, over);
+  }
+
+  return { oldContainer: source.parentNode, newContainer: source.parentNode };
+}
+
+function moveOutsideContainer(source, over, overContainer) {
+  const oldContainer = source.parentNode;
+
+  if (over) {
+    over.parentNode.insertBefore(source, over);
+  } else {
+    // need to figure out proper position
+    overContainer.appendChild(source);
+  }
+
+  return { oldContainer, newContainer: source.parentNode };
+}
+
+/***/ }),
+/* 29 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+exports.SortableStopEvent = exports.SortableSortedEvent = exports.SortableSortEvent = exports.SortableStartEvent = exports.SortableEvent = undefined;
+
+var _AbstractEvent = __webpack_require__(1);
+
+var _AbstractEvent2 = _interopRequireDefault(_AbstractEvent);
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+/**
+ * Base sortable event
+ * @class SortableEvent
+ * @module SortableEvent
+ * @extends AbstractEvent
+ */
+class SortableEvent extends _AbstractEvent2.default {
+
+  /**
+   * Original drag event that triggered this sortable event
+   * @property dragEvent
+   * @type {DragEvent}
+   * @readonly
+   */
+  get dragEvent() {
+    return this.data.dragEvent;
+  }
+}
+
+exports.SortableEvent = SortableEvent; /**
+                                        * Sortable start event
+                                        * @class SortableStartEvent
+                                        * @module SortableStartEvent
+                                        * @extends SortableEvent
+                                        */
+
+SortableEvent.type = 'sortable';
+class SortableStartEvent extends SortableEvent {
+
+  /**
+   * Start index of source on sortable start
+   * @property startIndex
+   * @type {Number}
+   * @readonly
+   */
+  get startIndex() {
+    return this.data.startIndex;
+  }
+
+  /**
+   * Start container on sortable start
+   * @property startContainer
+   * @type {HTMLElement}
+   * @readonly
+   */
+  get startContainer() {
+    return this.data.startContainer;
+  }
+}
+
+exports.SortableStartEvent = SortableStartEvent; /**
+                                                  * Sortable sort event
+                                                  * @class SortableSortEvent
+                                                  * @module SortableSortEvent
+                                                  * @extends SortableEvent
+                                                  */
+
+SortableStartEvent.type = 'sortable:start';
+SortableStartEvent.cancelable = true;
+class SortableSortEvent extends SortableEvent {
+
+  /**
+   * Index of current draggable element
+   * @property currentIndex
+   * @type {Number}
+   * @readonly
+   */
+  get currentIndex() {
+    return this.data.currentIndex;
+  }
+
+  /**
+   * Draggable element you are hovering over
+   * @property over
+   * @type {HTMLElement}
+   * @readonly
+   */
+  get over() {
+    return this.data.over;
+  }
+
+  /**
+   * Draggable container element you are hovering over
+   * @property overContainer
+   * @type {HTMLElement}
+   * @readonly
+   */
+  get overContainer() {
+    return this.data.dragEvent.overContainer;
+  }
+}
+
+exports.SortableSortEvent = SortableSortEvent; /**
+                                                * Sortable sorted event
+                                                * @class SortableSortedEvent
+                                                * @module SortableSortedEvent
+                                                * @extends SortableEvent
+                                                */
+
+SortableSortEvent.type = 'sortable:sort';
+SortableSortEvent.cancelable = true;
+class SortableSortedEvent extends SortableEvent {
+
+  /**
+   * Index of last sorted event
+   * @property oldIndex
+   * @type {Number}
+   * @readonly
+   */
+  get oldIndex() {
+    return this.data.oldIndex;
+  }
+
+  /**
+   * New index of this sorted event
+   * @property newIndex
+   * @type {Number}
+   * @readonly
+   */
+  get newIndex() {
+    return this.data.newIndex;
+  }
+
+  /**
+   * Old container of draggable element
+   * @property oldContainer
+   * @type {HTMLElement}
+   * @readonly
+   */
+  get oldContainer() {
+    return this.data.oldContainer;
+  }
+
+  /**
+   * New container of draggable element
+   * @property newContainer
+   * @type {HTMLElement}
+   * @readonly
+   */
+  get newContainer() {
+    return this.data.newContainer;
+  }
+}
+
+exports.SortableSortedEvent = SortableSortedEvent; /**
+                                                    * Sortable stop event
+                                                    * @class SortableStopEvent
+                                                    * @module SortableStopEvent
+                                                    * @extends SortableEvent
+                                                    */
+
+SortableSortedEvent.type = 'sortable:sorted';
+class SortableStopEvent extends SortableEvent {
+
+  /**
+   * Original index on sortable start
+   * @property oldIndex
+   * @type {Number}
+   * @readonly
+   */
+  get oldIndex() {
+    return this.data.oldIndex;
+  }
+
+  /**
+   * New index of draggable element
+   * @property newIndex
+   * @type {Number}
+   * @readonly
+   */
+  get newIndex() {
+    return this.data.newIndex;
+  }
+
+  /**
+   * Original container of draggable element
+   * @property oldContainer
+   * @type {HTMLElement}
+   * @readonly
+   */
+  get oldContainer() {
+    return this.data.oldContainer;
+  }
+
+  /**
+   * New container of draggable element
+   * @property newContainer
+   * @type {HTMLElement}
+   * @readonly
+   */
+  get newContainer() {
+    return this.data.newContainer;
+  }
+}
+exports.SortableStopEvent = SortableStopEvent;
+SortableStopEvent.type = 'sortable:stop';
+
+/***/ }),
+/* 30 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+
+var _SortableEvent = __webpack_require__(9);
+
+Object.keys(_SortableEvent).forEach(function (key) {
+  if (key === "default" || key === "__esModule") return;
+  Object.defineProperty(exports, key, {
+    enumerable: true,
+    get: function () {
+      return _SortableEvent[key];
+    }
+  });
+});
+
+var _Sortable = __webpack_require__(28);
+
+var _Sortable2 = _interopRequireDefault(_Sortable);
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+exports.default = _Sortable2.default;
+
+/***/ }),
+/* 31 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+
+var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
+
+var _Draggable = __webpack_require__(5);
+
+var _Draggable2 = _interopRequireDefault(_Draggable);
+
+var _SwappableEvent = __webpack_require__(10);
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+const onDragStart = Symbol('onDragStart');
+const onDragOver = Symbol('onDragOver');
+const onDragStop = Symbol('onDragStop');
+
+/**
+ * Returns an announcement message when the Draggable element is swapped with another draggable element
+ * @param {SwappableSwappedEvent} swappableEvent
+ * @return {String}
+ */
+function onSwappableSwappedDefaultAnnouncement({ dragEvent, swappedElement }) {
+  const sourceText = dragEvent.source.textContent.trim() || dragEvent.source.id || 'swappable element';
+  const overText = swappedElement.textContent.trim() || swappedElement.id || 'swappable element';
+
+  return `Swapped ${sourceText} with ${overText}`;
+}
+
+/**
+ * @const {Object} defaultAnnouncements
+ * @const {Function} defaultAnnouncements['swappabled:swapped']
+ */
+const defaultAnnouncements = {
+  'swappabled:swapped': onSwappableSwappedDefaultAnnouncement
+};
+
+/**
+ * Swappable is built on top of Draggable and allows swapping of draggable elements.
+ * Order is irrelevant to Swappable.
+ * @class Swappable
+ * @module Swappable
+ * @extends Draggable
+ */
+class Swappable extends _Draggable2.default {
+  /**
+   * Swappable constructor.
+   * @constructs Swappable
+   * @param {HTMLElement[]|NodeList|HTMLElement} containers - Swappable containers
+   * @param {Object} options - Options for Swappable
+   */
+  constructor(containers = [], options = {}) {
+    super(containers, _extends({}, options, {
+      announcements: _extends({}, defaultAnnouncements, options.announcements || {})
+    }));
+
+    /**
+     * Last draggable element that was dragged over
+     * @property lastOver
+     * @type {HTMLElement}
+     */
+    this.lastOver = null;
+
+    this[onDragStart] = this[onDragStart].bind(this);
+    this[onDragOver] = this[onDragOver].bind(this);
+    this[onDragStop] = this[onDragStop].bind(this);
+
+    this.on('drag:start', this[onDragStart]).on('drag:over', this[onDragOver]).on('drag:stop', this[onDragStop]);
+  }
+
+  /**
+   * Destroys Swappable instance.
+   */
+  destroy() {
+    super.destroy();
+
+    this.off('drag:start', this._onDragStart).off('drag:over', this._onDragOver).off('drag:stop', this._onDragStop);
+  }
+
+  /**
+   * Drag start handler
+   * @private
+   * @param {DragStartEvent} event - Drag start event
+   */
+  [onDragStart](event) {
+    const swappableStartEvent = new _SwappableEvent.SwappableStartEvent({
+      dragEvent: event
+    });
+
+    this.trigger(swappableStartEvent);
+
+    if (swappableStartEvent.canceled()) {
+      event.cancel();
+    }
+  }
+
+  /**
+   * Drag over handler
+   * @private
+   * @param {DragOverEvent} event - Drag over event
+   */
+  [onDragOver](event) {
+    if (event.over === event.originalSource || event.over === event.source || event.canceled()) {
+      return;
+    }
+
+    const swappableSwapEvent = new _SwappableEvent.SwappableSwapEvent({
+      dragEvent: event,
+      over: event.over,
+      overContainer: event.overContainer
+    });
+
+    this.trigger(swappableSwapEvent);
+
+    if (swappableSwapEvent.canceled()) {
+      return;
+    }
+
+    // swap originally swapped element back
+    if (this.lastOver && this.lastOver !== event.over) {
+      swap(this.lastOver, event.source);
+    }
+
+    if (this.lastOver === event.over) {
+      this.lastOver = null;
+    } else {
+      this.lastOver = event.over;
+    }
+
+    swap(event.source, event.over);
+
+    const swappableSwappedEvent = new _SwappableEvent.SwappableSwappedEvent({
+      dragEvent: event,
+      swappedElement: event.over
+    });
+
+    this.trigger(swappableSwappedEvent);
+  }
+
+  /**
+   * Drag stop handler
+   * @private
+   * @param {DragStopEvent} event - Drag stop event
+   */
+  [onDragStop](event) {
+    const swappableStopEvent = new _SwappableEvent.SwappableStopEvent({
+      dragEvent: event
+    });
+
+    this.trigger(swappableStopEvent);
+    this.lastOver = null;
+  }
+}
+
+exports.default = Swappable;
+function withTempElement(callback) {
+  const tmpElement = document.createElement('div');
+  callback(tmpElement);
+  tmpElement.parentNode.removeChild(tmpElement);
+}
+
+function swap(source, over) {
+  const overParent = over.parentNode;
+  const sourceParent = source.parentNode;
+
+  withTempElement(tmpElement => {
+    sourceParent.insertBefore(tmpElement, source);
+    overParent.insertBefore(source, over);
+    sourceParent.insertBefore(over, tmpElement);
+  });
+}
+
+/***/ }),
+/* 32 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+exports.SwappableStopEvent = exports.SwappableSwappedEvent = exports.SwappableSwapEvent = exports.SwappableStartEvent = exports.SwappableEvent = undefined;
+
+var _AbstractEvent = __webpack_require__(1);
+
+var _AbstractEvent2 = _interopRequireDefault(_AbstractEvent);
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+/**
+ * Base swappable event
+ * @class SwappableEvent
+ * @module SwappableEvent
+ * @extends AbstractEvent
+ */
+class SwappableEvent extends _AbstractEvent2.default {
+
+  /**
+   * Original drag event that triggered this swappable event
+   * @property dragEvent
+   * @type {DragEvent}
+   * @readonly
+   */
+  get dragEvent() {
+    return this.data.dragEvent;
+  }
+}
+
+exports.SwappableEvent = SwappableEvent; /**
+                                          * Swappable start event
+                                          * @class SwappableStartEvent
+                                          * @module SwappableStartEvent
+                                          * @extends SwappableEvent
+                                          */
+
+SwappableEvent.type = 'swappable';
+class SwappableStartEvent extends SwappableEvent {}
+
+exports.SwappableStartEvent = SwappableStartEvent; /**
+                                                    * Swappable swap event
+                                                    * @class SwappableSwapEvent
+                                                    * @module SwappableSwapEvent
+                                                    * @extends SwappableEvent
+                                                    */
+
+SwappableStartEvent.type = 'swappable:start';
+SwappableStartEvent.cancelable = true;
+class SwappableSwapEvent extends SwappableEvent {
+
+  /**
+   * Draggable element you are over
+   * @property over
+   * @type {HTMLElement}
+   * @readonly
+   */
+  get over() {
+    return this.data.over;
+  }
+
+  /**
+   * Draggable container you are over
+   * @property overContainer
+   * @type {HTMLElement}
+   * @readonly
+   */
+  get overContainer() {
+    return this.data.overContainer;
+  }
+}
+
+exports.SwappableSwapEvent = SwappableSwapEvent; /**
+                                                  * Swappable swapped event
+                                                  * @class SwappableSwappedEvent
+                                                  * @module SwappableSwappedEvent
+                                                  * @extends SwappableEvent
+                                                  */
+
+SwappableSwapEvent.type = 'swappable:swap';
+SwappableSwapEvent.cancelable = true;
+class SwappableSwappedEvent extends SwappableEvent {
+
+  /**
+   * The draggable element that you swapped with
+   * @property swappedElement
+   * @type {HTMLElement}
+   * @readonly
+   */
+  get swappedElement() {
+    return this.data.swappedElement;
+  }
+}
+
+exports.SwappableSwappedEvent = SwappableSwappedEvent; /**
+                                                        * Swappable stop event
+                                                        * @class SwappableStopEvent
+                                                        * @module SwappableStopEvent
+                                                        * @extends SwappableEvent
+                                                        */
+
+SwappableSwappedEvent.type = 'swappable:swapped';
+class SwappableStopEvent extends SwappableEvent {}
+exports.SwappableStopEvent = SwappableStopEvent;
+SwappableStopEvent.type = 'swappable:stop';
+
+/***/ }),
+/* 33 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+
+var _SwappableEvent = __webpack_require__(10);
+
+Object.keys(_SwappableEvent).forEach(function (key) {
+  if (key === "default" || key === "__esModule") return;
+  Object.defineProperty(exports, key, {
+    enumerable: true,
+    get: function () {
+      return _SwappableEvent[key];
+    }
+  });
+});
+
+var _Swappable = __webpack_require__(31);
+
+var _Swappable2 = _interopRequireDefault(_Swappable);
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+exports.default = _Swappable2.default;
+
+/***/ }),
+/* 34 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+
+var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
+
+var _utils = __webpack_require__(2);
+
+var _Draggable = __webpack_require__(5);
+
+var _Draggable2 = _interopRequireDefault(_Draggable);
+
+var _DroppableEvent = __webpack_require__(11);
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+const onDragStart = Symbol('onDragStart');
+const onDragMove = Symbol('onDragMove');
+const onDragStop = Symbol('onDragStop');
+const dropInDropzone = Symbol('dropInDropZone');
+const returnToOriginalDropzone = Symbol('returnToOriginalDropzone');
+const closestDropzone = Symbol('closestDropzone');
+const getDropzones = Symbol('getDropzones');
+
+/**
+ * Returns an announcement message when the Draggable element is dropped into a dropzone element
+ * @param {DroppableDroppedEvent} droppableEvent
+ * @return {String}
+ */
+function onDroppableDroppedDefaultAnnouncement({ dragEvent, dropzone }) {
+  const sourceText = dragEvent.source.textContent.trim() || dragEvent.source.id || 'draggable element';
+  const dropzoneText = dropzone.textContent.trim() || dropzone.id || 'droppable element';
+
+  return `Dropped ${sourceText} into ${dropzoneText}`;
+}
+
+/**
+ * Returns an announcement message when the Draggable element has returned to its original dropzone element
+ * @param {DroppableReturnedEvent} droppableEvent
+ * @return {String}
+ */
+function onDroppableReturnedDefaultAnnouncement({ dragEvent, dropzone }) {
+  const sourceText = dragEvent.source.textContent.trim() || dragEvent.source.id || 'draggable element';
+  const dropzoneText = dropzone.textContent.trim() || dropzone.id || 'droppable element';
+
+  return `Returned ${sourceText} from ${dropzoneText}`;
+}
+
+/**
+ * @const {Object} defaultAnnouncements
+ * @const {Function} defaultAnnouncements['droppable:dropped']
+ * @const {Function} defaultAnnouncements['droppable:returned']
+ */
+const defaultAnnouncements = {
+  'droppable:dropped': onDroppableDroppedDefaultAnnouncement,
+  'droppable:returned': onDroppableReturnedDefaultAnnouncement
+};
+
+const defaultClasses = {
+  'droppable:active': 'draggable-dropzone--active',
+  'droppable:occupied': 'draggable-dropzone--occupied'
+};
+
+const defaultOptions = {
+  dropzone: '.draggable-droppable'
+};
+
+/**
+ * Droppable is built on top of Draggable and allows dropping draggable elements
+ * into dropzone element
+ * @class Droppable
+ * @module Droppable
+ * @extends Draggable
+ */
+class Droppable extends _Draggable2.default {
+  /**
+   * Droppable constructor.
+   * @constructs Droppable
+   * @param {HTMLElement[]|NodeList|HTMLElement} containers - Droppable containers
+   * @param {Object} options - Options for Droppable
+   */
+  constructor(containers = [], options = {}) {
+    super(containers, _extends({}, defaultOptions, options, {
+      classes: _extends({}, defaultClasses, options.classes || {}),
+      announcements: _extends({}, defaultAnnouncements, options.announcements || {})
+    }));
+
+    /**
+     * All dropzone elements on drag start
+     * @property dropzones
+     * @type {HTMLElement[]}
+     */
+    this.dropzones = null;
+
+    /**
+     * Last dropzone element that the source was dropped into
+     * @property lastDropzone
+     * @type {HTMLElement}
+     */
+    this.lastDropzone = null;
+
+    /**
+     * Initial dropzone element that the source was drag from
+     * @property initialDropzone
+     * @type {HTMLElement}
+     */
+    this.initialDropzone = null;
+
+    this[onDragStart] = this[onDragStart].bind(this);
+    this[onDragMove] = this[onDragMove].bind(this);
+    this[onDragStop] = this[onDragStop].bind(this);
+
+    this.on('drag:start', this[onDragStart]).on('drag:move', this[onDragMove]).on('drag:stop', this[onDragStop]);
+  }
+
+  /**
+   * Destroys Droppable instance.
+   */
+  destroy() {
+    super.destroy();
+
+    this.off('drag:start', this[onDragStart]).off('drag:move', this[onDragMove]).off('drag:stop', this[onDragStop]);
+  }
+
+  /**
+   * Drag start handler
+   * @private
+   * @param {DragStartEvent} event - Drag start event
+   */
+  [onDragStart](event) {
+    if (event.canceled()) {
+      return;
+    }
+
+    this.dropzones = [...this[getDropzones]()];
+    const dropzone = (0, _utils.closest)(event.sensorEvent.target, this.options.dropzone);
+
+    if (!dropzone) {
+      event.cancel();
+      return;
+    }
+
+    const droppableStartEvent = new _DroppableEvent.DroppableStartEvent({
+      dragEvent: event,
+      dropzone
+    });
+
+    this.trigger(droppableStartEvent);
+
+    if (droppableStartEvent.canceled()) {
+      event.cancel();
+      return;
+    }
+
+    this.initialDropzone = dropzone;
+
+    for (const dropzoneElement of this.dropzones) {
+      if (dropzoneElement.classList.contains(this.getClassNameFor('droppable:occupied'))) {
+        continue;
+      }
+
+      dropzoneElement.classList.add(this.getClassNameFor('droppable:active'));
+    }
+  }
+
+  /**
+   * Drag move handler
+   * @private
+   * @param {DragMoveEvent} event - Drag move event
+   */
+  [onDragMove](event) {
+    if (event.canceled()) {
+      return;
+    }
+
+    const dropzone = this[closestDropzone](event.sensorEvent.target);
+    const overEmptyDropzone = dropzone && !dropzone.classList.contains(this.getClassNameFor('droppable:occupied'));
+
+    if (overEmptyDropzone && this[dropInDropzone](event, dropzone)) {
+      this.lastDropzone = dropzone;
+    } else if ((!dropzone || dropzone === this.initialDropzone) && this.lastDropzone) {
+      this[returnToOriginalDropzone](event);
+      this.lastDropzone = null;
+    }
+  }
+
+  /**
+   * Drag stop handler
+   * @private
+   * @param {DragStopEvent} event - Drag stop event
+   */
+  [onDragStop](event) {
+    const droppableStopEvent = new _DroppableEvent.DroppableStopEvent({
+      dragEvent: event,
+      dropzone: this.lastDropzone || this.initialDropzone
+    });
+
+    this.trigger(droppableStopEvent);
+
+    const occupiedClass = this.getClassNameFor('droppable:occupied');
+
+    for (const dropzone of this.dropzones) {
+      dropzone.classList.remove(this.getClassNameFor('droppable:active'));
+    }
+
+    if (this.lastDropzone && this.lastDropzone !== this.initialDropzone) {
+      this.initialDropzone.classList.remove(occupiedClass);
+    }
+
+    this.dropzones = null;
+    this.lastDropzone = null;
+    this.initialDropzone = null;
+  }
+
+  /**
+   * Drops a draggable element into a dropzone element
+   * @private
+   * @param {DragMoveEvent} event - Drag move event
+   * @param {HTMLElement} dropzone - Dropzone element to drop draggable into
+   */
+  [dropInDropzone](event, dropzone) {
+    const droppableDroppedEvent = new _DroppableEvent.DroppableDroppedEvent({
+      dragEvent: event,
+      dropzone
+    });
+
+    this.trigger(droppableDroppedEvent);
+
+    if (droppableDroppedEvent.canceled()) {
+      return false;
+    }
+
+    const occupiedClass = this.getClassNameFor('droppable:occupied');
+
+    if (this.lastDropzone) {
+      this.lastDropzone.classList.remove(occupiedClass);
+    }
+
+    dropzone.appendChild(event.source);
+    dropzone.classList.add(occupiedClass);
+
+    return true;
+  }
+
+  /**
+   * Moves the previously dropped element back into its original dropzone
+   * @private
+   * @param {DragMoveEvent} event - Drag move event
+   */
+  [returnToOriginalDropzone](event) {
+    const droppableReturnedEvent = new _DroppableEvent.DroppableReturnedEvent({
+      dragEvent: event,
+      dropzone: this.lastDropzone
+    });
+
+    this.trigger(droppableReturnedEvent);
+
+    if (droppableReturnedEvent.canceled()) {
+      return;
+    }
+
+    this.initialDropzone.appendChild(event.source);
+    this.lastDropzone.classList.remove(this.getClassNameFor('droppable:occupied'));
+  }
+
+  /**
+   * Returns closest dropzone element for even target
+   * @private
+   * @param {HTMLElement} target - Event target
+   * @return {HTMLElement|null}
+   */
+  [closestDropzone](target) {
+    if (!this.dropzones) {
+      return null;
+    }
+
+    return (0, _utils.closest)(target, this.dropzones);
+  }
+
+  /**
+   * Returns all current dropzone elements for this draggable instance
+   * @private
+   * @return {NodeList|HTMLElement[]|Array}
+   */
+  [getDropzones]() {
+    const dropzone = this.options.dropzone;
+
+    if (typeof dropzone === 'string') {
+      return document.querySelectorAll(dropzone);
+    } else if (dropzone instanceof NodeList || dropzone instanceof Array) {
+      return dropzone;
+    } else if (typeof dropzone === 'function') {
+      return dropzone();
+    } else {
+      return [];
+    }
+  }
+}
+exports.default = Droppable;
+
+/***/ }),
+/* 35 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+exports.DroppableStopEvent = exports.DroppableReturnedEvent = exports.DroppableDroppedEvent = exports.DroppableStartEvent = exports.DroppableEvent = undefined;
+
+var _AbstractEvent = __webpack_require__(1);
+
+var _AbstractEvent2 = _interopRequireDefault(_AbstractEvent);
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+/**
+ * Base droppable event
+ * @class DroppableEvent
+ * @module DroppableEvent
+ * @extends AbstractEvent
+ */
+class DroppableEvent extends _AbstractEvent2.default {
+
+  /**
+   * Original drag event that triggered this droppable event
+   * @property dragEvent
+   * @type {DragEvent}
+   * @readonly
+   */
+  get dragEvent() {
+    return this.data.dragEvent;
+  }
+}
+
+exports.DroppableEvent = DroppableEvent; /**
+                                          * Droppable start event
+                                          * @class DroppableStartEvent
+                                          * @module DroppableStartEvent
+                                          * @extends DroppableEvent
+                                          */
+
+DroppableEvent.type = 'droppable';
+class DroppableStartEvent extends DroppableEvent {
+
+  /**
+   * The initial dropzone element of the currently dragging draggable element
+   * @property dropzone
+   * @type {HTMLElement}
+   * @readonly
+   */
+  get dropzone() {
+    return this.data.dropzone;
+  }
+}
+
+exports.DroppableStartEvent = DroppableStartEvent; /**
+                                                    * Droppable dropped event
+                                                    * @class DroppableDroppedEvent
+                                                    * @module DroppableDroppedEvent
+                                                    * @extends DroppableEvent
+                                                    */
+
+DroppableStartEvent.type = 'droppable:start';
+DroppableStartEvent.cancelable = true;
+class DroppableDroppedEvent extends DroppableEvent {
+
+  /**
+   * The dropzone element you dropped the draggable element into
+   * @property dropzone
+   * @type {HTMLElement}
+   * @readonly
+   */
+  get dropzone() {
+    return this.data.dropzone;
+  }
+}
+
+exports.DroppableDroppedEvent = DroppableDroppedEvent; /**
+                                                        * Droppable returned event
+                                                        * @class DroppableReturnedEvent
+                                                        * @module DroppableReturnedEvent
+                                                        * @extends DroppableEvent
+                                                        */
+
+DroppableDroppedEvent.type = 'droppable:dropped';
+DroppableDroppedEvent.cancelable = true;
+class DroppableReturnedEvent extends DroppableEvent {
+
+  /**
+   * The dropzone element you dragged away from
+   * @property dropzone
+   * @type {HTMLElement}
+   * @readonly
+   */
+  get dropzone() {
+    return this.data.dropzone;
+  }
+}
+
+exports.DroppableReturnedEvent = DroppableReturnedEvent; /**
+                                                          * Droppable stop event
+                                                          * @class DroppableStopEvent
+                                                          * @module DroppableStopEvent
+                                                          * @extends DroppableEvent
+                                                          */
+
+DroppableReturnedEvent.type = 'droppable:returned';
+DroppableReturnedEvent.cancelable = true;
+class DroppableStopEvent extends DroppableEvent {
+
+  /**
+   * The final dropzone element of the draggable element
+   * @property dropzone
+   * @type {HTMLElement}
+   * @readonly
+   */
+  get dropzone() {
+    return this.data.dropzone;
+  }
+}
+exports.DroppableStopEvent = DroppableStopEvent;
+DroppableStopEvent.type = 'droppable:stop';
+DroppableStopEvent.cancelable = true;
+
+/***/ }),
+/* 36 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+
+var _DroppableEvent = __webpack_require__(11);
+
+Object.keys(_DroppableEvent).forEach(function (key) {
+  if (key === "default" || key === "__esModule") return;
+  Object.defineProperty(exports, key, {
+    enumerable: true,
+    get: function () {
+      return _DroppableEvent[key];
+    }
+  });
+});
+
+var _Droppable = __webpack_require__(34);
+
+var _Droppable2 = _interopRequireDefault(_Droppable);
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+exports.default = _Droppable2.default;
+
+/***/ }),
+/* 37 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+/**
+ * The Emitter is a simple emitter class that provides you with `on()`, `off()` and `trigger()` methods
+ * @class Emitter
+ * @module Emitter
+ */
+class Emitter {
+  constructor() {
+    this.callbacks = {};
+  }
+
+  /**
+   * Registers callbacks by event name
+   * @param {String} type
+   * @param {...Function} callbacks
+   */
+  on(type, ...callbacks) {
+    if (!this.callbacks[type]) {
+      this.callbacks[type] = [];
+    }
+
+    this.callbacks[type].push(...callbacks);
+
+    return this;
+  }
+
+  /**
+   * Unregisters callbacks by event name
+   * @param {String} type
+   * @param {Function} callback
+   */
+  off(type, callback) {
+    if (!this.callbacks[type]) {
+      return null;
+    }
+
+    const copy = this.callbacks[type].slice(0);
+
+    for (let i = 0; i < copy.length; i++) {
+      if (callback === copy[i]) {
+        this.callbacks[type].splice(i, 1);
+      }
+    }
+
+    return this;
+  }
+
+  /**
+   * Triggers event callbacks by event object
+   * @param {AbstractEvent} event
+   */
+  trigger(event) {
+    if (!this.callbacks[event.type]) {
+      return null;
+    }
+
+    const callbacks = [...this.callbacks[event.type]];
+    const caughtErrors = [];
+
+    for (let i = callbacks.length - 1; i >= 0; i--) {
+      const callback = callbacks[i];
+
+      try {
+        callback(event);
+      } catch (error) {
+        caughtErrors.push(error);
+      }
+    }
+
+    if (caughtErrors.length) {
+      /* eslint-disable no-console */
+      console.error(`Draggable caught errors while triggering '${event.type}'`, caughtErrors);
+      /* eslint-disable no-console */
+    }
+
+    return this;
+  }
+}
+exports.default = Emitter;
+
+/***/ }),
+/* 38 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+
+var _Emitter = __webpack_require__(37);
+
+var _Emitter2 = _interopRequireDefault(_Emitter);
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+exports.default = _Emitter2.default;
+
+/***/ }),
+/* 39 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+exports.defaultOptions = undefined;
+
+var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
+
+var _utils = __webpack_require__(2);
+
+var _Plugins = __webpack_require__(12);
+
+var _Emitter = __webpack_require__(38);
+
+var _Emitter2 = _interopRequireDefault(_Emitter);
+
+var _Sensors = __webpack_require__(6);
+
+var _DraggableEvent = __webpack_require__(13);
+
+var _DragEvent = __webpack_require__(14);
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+const onDragStart = Symbol('onDragStart');
+const onDragMove = Symbol('onDragMove');
+const onDragStop = Symbol('onDragStop');
+const onDragPressure = Symbol('onDragPressure');
+
+/**
+ * @const {Object} defaultAnnouncements
+ * @const {Function} defaultAnnouncements['drag:start']
+ * @const {Function} defaultAnnouncements['drag:stop']
+ */
+const defaultAnnouncements = {
+  'drag:start': event => `Picked up ${event.source.textContent.trim() || event.source.id || 'draggable element'}`,
+  'drag:stop': event => `Released ${event.source.textContent.trim() || event.source.id || 'draggable element'}`
+};
+
+const defaultClasses = {
+  'container:dragging': 'draggable-container--is-dragging',
+  'source:dragging': 'draggable-source--is-dragging',
+  'source:placed': 'draggable-source--placed',
+  'container:placed': 'draggable-container--placed',
+  'body:dragging': 'draggable--is-dragging',
+  'draggable:over': 'draggable--over',
+  'container:over': 'draggable-container--over',
+  'source:original': 'draggable--original',
+  mirror: 'draggable-mirror'
+};
+
+const defaultOptions = exports.defaultOptions = {
+  draggable: '.draggable-source',
+  handle: null,
+  delay: 100,
+  distance: 0,
+  placedTimeout: 800,
+  plugins: [],
+  sensors: [],
+  exclude: {
+    plugins: [],
+    sensors: []
+  }
+};
+
+/**
+ * This is the core draggable library that does the heavy lifting
+ * @class Draggable
+ * @module Draggable
+ */
+class Draggable {
+
+  /**
+   * Draggable constructor.
+   * @constructs Draggable
+   * @param {HTMLElement[]|NodeList|HTMLElement} containers - Draggable containers
+   * @param {Object} options - Options for draggable
+   */
+
+  /**
+   * Default plugins draggable uses
+   * @static
+   * @property {Object} Plugins
+   * @property {Announcement} Plugins.Announcement
+   * @property {Focusable} Plugins.Focusable
+   * @property {Mirror} Plugins.Mirror
+   * @property {Scrollable} Plugins.Scrollable
+   * @type {Object}
+   */
+  constructor(containers = [document.body], options = {}) {
+    /**
+     * Draggable containers
+     * @property containers
+     * @type {HTMLElement[]}
+     */
+    if (containers instanceof NodeList || containers instanceof Array) {
+      this.containers = [...containers];
+    } else if (containers instanceof HTMLElement) {
+      this.containers = [containers];
+    } else {
+      throw new Error('Draggable containers are expected to be of type `NodeList`, `HTMLElement[]` or `HTMLElement`');
+    }
+
+    this.options = _extends({}, defaultOptions, options, {
+      classes: _extends({}, defaultClasses, options.classes || {}),
+      announcements: _extends({}, defaultAnnouncements, options.announcements || {}),
+      exclude: {
+        plugins: options.exclude && options.exclude.plugins || [],
+        sensors: options.exclude && options.exclude.sensors || []
+      }
+    });
+
+    /**
+     * Draggables event emitter
+     * @property emitter
+     * @type {Emitter}
+     */
+    this.emitter = new _Emitter2.default();
+
+    /**
+     * Current drag state
+     * @property dragging
+     * @type {Boolean}
+     */
+    this.dragging = false;
+
+    /**
+     * Active plugins
+     * @property plugins
+     * @type {Plugin[]}
+     */
+    this.plugins = [];
+
+    /**
+     * Active sensors
+     * @property sensors
+     * @type {Sensor[]}
+     */
+    this.sensors = [];
+
+    this[onDragStart] = this[onDragStart].bind(this);
+    this[onDragMove] = this[onDragMove].bind(this);
+    this[onDragStop] = this[onDragStop].bind(this);
+    this[onDragPressure] = this[onDragPressure].bind(this);
+
+    document.addEventListener('drag:start', this[onDragStart], true);
+    document.addEventListener('drag:move', this[onDragMove], true);
+    document.addEventListener('drag:stop', this[onDragStop], true);
+    document.addEventListener('drag:pressure', this[onDragPressure], true);
+
+    const defaultPlugins = Object.values(Draggable.Plugins).filter(Plugin => !this.options.exclude.plugins.includes(Plugin));
+    const defaultSensors = Object.values(Draggable.Sensors).filter(sensor => !this.options.exclude.sensors.includes(sensor));
+
+    this.addPlugin(...[...defaultPlugins, ...this.options.plugins]);
+    this.addSensor(...[...defaultSensors, ...this.options.sensors]);
+
+    const draggableInitializedEvent = new _DraggableEvent.DraggableInitializedEvent({
+      draggable: this
+    });
+
+    this.on('mirror:created', ({ mirror }) => this.mirror = mirror);
+    this.on('mirror:destroy', () => this.mirror = null);
+
+    this.trigger(draggableInitializedEvent);
+  }
+
+  /**
+   * Destroys Draggable instance. This removes all internal event listeners and
+   * deactivates sensors and plugins
+   */
+
+
+  /**
+   * Default sensors draggable uses
+   * @static
+   * @property {Object} Sensors
+   * @property {MouseSensor} Sensors.MouseSensor
+   * @property {TouchSensor} Sensors.TouchSensor
+   * @type {Object}
+   */
+  destroy() {
+    document.removeEventListener('drag:start', this[onDragStart], true);
+    document.removeEventListener('drag:move', this[onDragMove], true);
+    document.removeEventListener('drag:stop', this[onDragStop], true);
+    document.removeEventListener('drag:pressure', this[onDragPressure], true);
+
+    const draggableDestroyEvent = new _DraggableEvent.DraggableDestroyEvent({
+      draggable: this
+    });
+
+    this.trigger(draggableDestroyEvent);
+
+    this.removePlugin(...this.plugins.map(plugin => plugin.constructor));
+    this.removeSensor(...this.sensors.map(sensor => sensor.constructor));
+  }
+
+  /**
+   * Adds plugin to this draggable instance. This will end up calling the attach method of the plugin
+   * @param {...typeof Plugin} plugins - Plugins that you want attached to draggable
+   * @return {Draggable}
+   * @example draggable.addPlugin(CustomA11yPlugin, CustomMirrorPlugin)
+   */
+  addPlugin(...plugins) {
+    const activePlugins = plugins.map(Plugin => new Plugin(this));
+
+    activePlugins.forEach(plugin => plugin.attach());
+    this.plugins = [...this.plugins, ...activePlugins];
+
+    return this;
+  }
+
+  /**
+   * Removes plugins that are already attached to this draggable instance. This will end up calling
+   * the detach method of the plugin
+   * @param {...typeof Plugin} plugins - Plugins that you want detached from draggable
+   * @return {Draggable}
+   * @example draggable.removePlugin(MirrorPlugin, CustomMirrorPlugin)
+   */
+  removePlugin(...plugins) {
+    const removedPlugins = this.plugins.filter(plugin => plugins.includes(plugin.constructor));
+
+    removedPlugins.forEach(plugin => plugin.detach());
+    this.plugins = this.plugins.filter(plugin => !plugins.includes(plugin.constructor));
+
+    return this;
+  }
+
+  /**
+   * Adds sensors to this draggable instance. This will end up calling the attach method of the sensor
+   * @param {...typeof Sensor} sensors - Sensors that you want attached to draggable
+   * @return {Draggable}
+   * @example draggable.addSensor(ForceTouchSensor, CustomSensor)
+   */
+  addSensor(...sensors) {
+    const activeSensors = sensors.map(Sensor => new Sensor(this.containers, this.options));
+
+    activeSensors.forEach(sensor => sensor.attach());
+    this.sensors = [...this.sensors, ...activeSensors];
+
+    return this;
+  }
+
+  /**
+   * Removes sensors that are already attached to this draggable instance. This will end up calling
+   * the detach method of the sensor
+   * @param {...typeof Sensor} sensors - Sensors that you want attached to draggable
+   * @return {Draggable}
+   * @example draggable.removeSensor(TouchSensor, DragSensor)
+   */
+  removeSensor(...sensors) {
+    const removedSensors = this.sensors.filter(sensor => sensors.includes(sensor.constructor));
+
+    removedSensors.forEach(sensor => sensor.detach());
+    this.sensors = this.sensors.filter(sensor => !sensors.includes(sensor.constructor));
+
+    return this;
+  }
+
+  /**
+   * Adds container to this draggable instance
+   * @param {...HTMLElement} containers - Containers you want to add to draggable
+   * @return {Draggable}
+   * @example draggable.addContainer(document.body)
+   */
+  addContainer(...containers) {
+    this.containers = [...this.containers, ...containers];
+    this.sensors.forEach(sensor => sensor.addContainer(...containers));
+    return this;
+  }
+
+  /**
+   * Removes container from this draggable instance
+   * @param {...HTMLElement} containers - Containers you want to remove from draggable
+   * @return {Draggable}
+   * @example draggable.removeContainer(document.body)
+   */
+  removeContainer(...containers) {
+    this.containers = this.containers.filter(container => !containers.includes(container));
+    this.sensors.forEach(sensor => sensor.removeContainer(...containers));
+    return this;
+  }
+
+  /**
+   * Adds listener for draggable events
+   * @param {String} type - Event name
+   * @param {...Function} callbacks - Event callbacks
+   * @return {Draggable}
+   * @example draggable.on('drag:start', (dragEvent) => dragEvent.cancel());
+   */
+  on(type, ...callbacks) {
+    this.emitter.on(type, ...callbacks);
+    return this;
+  }
+
+  /**
+   * Removes listener from draggable
+   * @param {String} type - Event name
+   * @param {Function} callback - Event callback
+   * @return {Draggable}
+   * @example draggable.off('drag:start', handlerFunction);
+   */
+  off(type, callback) {
+    this.emitter.off(type, callback);
+    return this;
+  }
+
+  /**
+   * Triggers draggable event
+   * @param {AbstractEvent} event - Event instance
+   * @return {Draggable}
+   * @example draggable.trigger(event);
+   */
+  trigger(event) {
+    this.emitter.trigger(event);
+    return this;
+  }
+
+  /**
+   * Returns class name for class identifier
+   * @param {String} name - Name of class identifier
+   * @return {String|null}
+   */
+  getClassNameFor(name) {
+    return this.options.classes[name];
+  }
+
+  /**
+   * Returns true if this draggable instance is currently dragging
+   * @return {Boolean}
+   */
+  isDragging() {
+    return Boolean(this.dragging);
+  }
+
+  /**
+   * Returns all draggable elements
+   * @return {HTMLElement[]}
+   */
+  getDraggableElements() {
+    return this.containers.reduce((current, container) => {
+      return [...current, ...this.getDraggableElementsForContainer(container)];
+    }, []);
+  }
+
+  /**
+   * Returns draggable elements for a given container, excluding the mirror and
+   * original source element if present
+   * @param {HTMLElement} container
+   * @return {HTMLElement[]}
+   */
+  getDraggableElementsForContainer(container) {
+    const allDraggableElements = container.querySelectorAll(this.options.draggable);
+
+    return [...allDraggableElements].filter(childElement => {
+      return childElement !== this.originalSource && childElement !== this.mirror;
+    });
+  }
+
+  /**
+   * Drag start handler
+   * @private
+   * @param {Event} event - DOM Drag event
+   */
+  [onDragStart](event) {
+    const sensorEvent = getSensorEvent(event);
+    const { target, container } = sensorEvent;
+
+    if (!this.containers.includes(container)) {
+      return;
+    }
+
+    if (this.options.handle && target && !(0, _utils.closest)(target, this.options.handle)) {
+      sensorEvent.cancel();
+      return;
+    }
+
+    // Find draggable source element
+    this.originalSource = (0, _utils.closest)(target, this.options.draggable);
+    this.sourceContainer = container;
+
+    if (!this.originalSource) {
+      sensorEvent.cancel();
+      return;
+    }
+
+    if (this.lastPlacedSource && this.lastPlacedContainer) {
+      clearTimeout(this.placedTimeoutID);
+      this.lastPlacedSource.classList.remove(this.getClassNameFor('source:placed'));
+      this.lastPlacedContainer.classList.remove(this.getClassNameFor('container:placed'));
+    }
+
+    this.source = this.originalSource.cloneNode(true);
+    this.originalSource.parentNode.insertBefore(this.source, this.originalSource);
+    this.originalSource.style.display = 'none';
+
+    const dragEvent = new _DragEvent.DragStartEvent({
+      source: this.source,
+      originalSource: this.originalSource,
+      sourceContainer: container,
+      sensorEvent
+    });
+
+    this.trigger(dragEvent);
+
+    this.dragging = !dragEvent.canceled();
+
+    if (dragEvent.canceled()) {
+      this.source.parentNode.removeChild(this.source);
+      this.originalSource.style.display = null;
+      return;
+    }
+
+    this.originalSource.classList.add(this.getClassNameFor('source:original'));
+    this.source.classList.add(this.getClassNameFor('source:dragging'));
+    this.sourceContainer.classList.add(this.getClassNameFor('container:dragging'));
+    document.body.classList.add(this.getClassNameFor('body:dragging'));
+    applyUserSelect(document.body, 'none');
+
+    requestAnimationFrame(() => {
+      const oldSensorEvent = getSensorEvent(event);
+      const newSensorEvent = oldSensorEvent.clone({ target: this.source });
+
+      this[onDragMove](_extends({}, event, {
+        detail: newSensorEvent
+      }));
+    });
+  }
+
+  /**
+   * Drag move handler
+   * @private
+   * @param {Event} event - DOM Drag event
+   */
+  [onDragMove](event) {
+    if (!this.dragging) {
+      return;
+    }
+
+    const sensorEvent = getSensorEvent(event);
+    const { container } = sensorEvent;
+    let target = sensorEvent.target;
+
+    const dragMoveEvent = new _DragEvent.DragMoveEvent({
+      source: this.source,
+      originalSource: this.originalSource,
+      sourceContainer: container,
+      sensorEvent
+    });
+
+    this.trigger(dragMoveEvent);
+
+    if (dragMoveEvent.canceled()) {
+      sensorEvent.cancel();
+    }
+
+    target = (0, _utils.closest)(target, this.options.draggable);
+    const withinCorrectContainer = (0, _utils.closest)(sensorEvent.target, this.containers);
+    const overContainer = sensorEvent.overContainer || withinCorrectContainer;
+    const isLeavingContainer = this.currentOverContainer && overContainer !== this.currentOverContainer;
+    const isLeavingDraggable = this.currentOver && target !== this.currentOver;
+    const isOverContainer = overContainer && this.currentOverContainer !== overContainer;
+    const isOverDraggable = withinCorrectContainer && target && this.currentOver !== target;
+
+    if (isLeavingDraggable) {
+      const dragOutEvent = new _DragEvent.DragOutEvent({
+        source: this.source,
+        originalSource: this.originalSource,
+        sourceContainer: container,
+        sensorEvent,
+        over: this.currentOver
+      });
+
+      this.currentOver.classList.remove(this.getClassNameFor('draggable:over'));
+      this.currentOver = null;
+
+      this.trigger(dragOutEvent);
+    }
+
+    if (isLeavingContainer) {
+      const dragOutContainerEvent = new _DragEvent.DragOutContainerEvent({
+        source: this.source,
+        originalSource: this.originalSource,
+        sourceContainer: container,
+        sensorEvent,
+        overContainer: this.currentOverContainer
+      });
+
+      this.currentOverContainer.classList.remove(this.getClassNameFor('container:over'));
+      this.currentOverContainer = null;
+
+      this.trigger(dragOutContainerEvent);
+    }
+
+    if (isOverContainer) {
+      overContainer.classList.add(this.getClassNameFor('container:over'));
+
+      const dragOverContainerEvent = new _DragEvent.DragOverContainerEvent({
+        source: this.source,
+        originalSource: this.originalSource,
+        sourceContainer: container,
+        sensorEvent,
+        overContainer
+      });
+
+      this.currentOverContainer = overContainer;
+
+      this.trigger(dragOverContainerEvent);
+    }
+
+    if (isOverDraggable) {
+      target.classList.add(this.getClassNameFor('draggable:over'));
+
+      const dragOverEvent = new _DragEvent.DragOverEvent({
+        source: this.source,
+        originalSource: this.originalSource,
+        sourceContainer: container,
+        sensorEvent,
+        overContainer,
+        over: target
+      });
+
+      this.currentOver = target;
+
+      this.trigger(dragOverEvent);
+    }
+  }
+
+  /**
+   * Drag stop handler
+   * @private
+   * @param {Event} event - DOM Drag event
+   */
+  [onDragStop](event) {
+    if (!this.dragging) {
+      return;
+    }
+
+    this.dragging = false;
+
+    const dragStopEvent = new _DragEvent.DragStopEvent({
+      source: this.source,
+      originalSource: this.originalSource,
+      sensorEvent: event.sensorEvent,
+      sourceContainer: this.sourceContainer
+    });
+
+    this.trigger(dragStopEvent);
+
+    this.source.parentNode.insertBefore(this.originalSource, this.source);
+    this.source.parentNode.removeChild(this.source);
+    this.originalSource.style.display = '';
+
+    this.source.classList.remove(this.getClassNameFor('source:dragging'));
+    this.originalSource.classList.remove(this.getClassNameFor('source:original'));
+    this.originalSource.classList.add(this.getClassNameFor('source:placed'));
+    this.sourceContainer.classList.add(this.getClassNameFor('container:placed'));
+    this.sourceContainer.classList.remove(this.getClassNameFor('container:dragging'));
+    document.body.classList.remove(this.getClassNameFor('body:dragging'));
+    applyUserSelect(document.body, '');
+
+    if (this.currentOver) {
+      this.currentOver.classList.remove(this.getClassNameFor('draggable:over'));
+    }
+
+    if (this.currentOverContainer) {
+      this.currentOverContainer.classList.remove(this.getClassNameFor('container:over'));
+    }
+
+    this.lastPlacedSource = this.originalSource;
+    this.lastPlacedContainer = this.sourceContainer;
+
+    this.placedTimeoutID = setTimeout(() => {
+      if (this.lastPlacedSource) {
+        this.lastPlacedSource.classList.remove(this.getClassNameFor('source:placed'));
+      }
+
+      if (this.lastPlacedContainer) {
+        this.lastPlacedContainer.classList.remove(this.getClassNameFor('container:placed'));
+      }
+
+      this.lastPlacedSource = null;
+      this.lastPlacedContainer = null;
+    }, this.options.placedTimeout);
+
+    this.source = null;
+    this.originalSource = null;
+    this.currentOverContainer = null;
+    this.currentOver = null;
+    this.sourceContainer = null;
+  }
+
+  /**
+   * Drag pressure handler
+   * @private
+   * @param {Event} event - DOM Drag event
+   */
+  [onDragPressure](event) {
+    if (!this.dragging) {
+      return;
+    }
+
+    const sensorEvent = getSensorEvent(event);
+    const source = this.source || (0, _utils.closest)(sensorEvent.originalEvent.target, this.options.draggable);
+
+    const dragPressureEvent = new _DragEvent.DragPressureEvent({
+      sensorEvent,
+      source,
+      pressure: sensorEvent.pressure
+    });
+
+    this.trigger(dragPressureEvent);
+  }
+}
+
+exports.default = Draggable;
+Draggable.Plugins = { Announcement: _Plugins.Announcement, Focusable: _Plugins.Focusable, Mirror: _Plugins.Mirror, Scrollable: _Plugins.Scrollable };
+Draggable.Sensors = { MouseSensor: _Sensors.MouseSensor, TouchSensor: _Sensors.TouchSensor };
+function getSensorEvent(event) {
+  return event.detail;
+}
+
+function applyUserSelect(element, value) {
+  element.style.webkitUserSelect = value;
+  element.style.mozUserSelect = value;
+  element.style.msUserSelect = value;
+  element.style.oUserSelect = value;
+  element.style.userSelect = value;
+}
+
+/***/ }),
+/* 40 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+
+var _Sensor = __webpack_require__(4);
+
+var _Sensor2 = _interopRequireDefault(_Sensor);
+
+var _SensorEvent = __webpack_require__(3);
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+const onMouseForceWillBegin = Symbol('onMouseForceWillBegin');
+const onMouseForceDown = Symbol('onMouseForceDown');
+const onMouseDown = Symbol('onMouseDown');
+const onMouseForceChange = Symbol('onMouseForceChange');
+const onMouseMove = Symbol('onMouseMove');
+const onMouseUp = Symbol('onMouseUp');
+const onMouseForceGlobalChange = Symbol('onMouseForceGlobalChange');
+
+/**
+ * This sensor picks up native force touch events and dictates drag operations
+ * @class ForceTouchSensor
+ * @module ForceTouchSensor
+ * @extends Sensor
+ */
+class ForceTouchSensor extends _Sensor2.default {
+  /**
+   * ForceTouchSensor constructor.
+   * @constructs ForceTouchSensor
+   * @param {HTMLElement[]|NodeList|HTMLElement} containers - Containers
+   * @param {Object} options - Options
+   */
+  constructor(containers = [], options = {}) {
+    super(containers, options);
+
+    /**
+     * Draggable element needs to be remembered to unset the draggable attribute after drag operation has completed
+     * @property mightDrag
+     * @type {Boolean}
+     */
+    this.mightDrag = false;
+
+    this[onMouseForceWillBegin] = this[onMouseForceWillBegin].bind(this);
+    this[onMouseForceDown] = this[onMouseForceDown].bind(this);
+    this[onMouseDown] = this[onMouseDown].bind(this);
+    this[onMouseForceChange] = this[onMouseForceChange].bind(this);
+    this[onMouseMove] = this[onMouseMove].bind(this);
+    this[onMouseUp] = this[onMouseUp].bind(this);
+  }
+
+  /**
+   * Attaches sensors event listeners to the DOM
+   */
+  attach() {
+    for (const container of this.containers) {
+      container.addEventListener('webkitmouseforcewillbegin', this[onMouseForceWillBegin], false);
+      container.addEventListener('webkitmouseforcedown', this[onMouseForceDown], false);
+      container.addEventListener('mousedown', this[onMouseDown], true);
+      container.addEventListener('webkitmouseforcechanged', this[onMouseForceChange], false);
+    }
+
+    document.addEventListener('mousemove', this[onMouseMove]);
+    document.addEventListener('mouseup', this[onMouseUp]);
+  }
+
+  /**
+   * Detaches sensors event listeners to the DOM
+   */
+  detach() {
+    for (const container of this.containers) {
+      container.removeEventListener('webkitmouseforcewillbegin', this[onMouseForceWillBegin], false);
+      container.removeEventListener('webkitmouseforcedown', this[onMouseForceDown], false);
+      container.removeEventListener('mousedown', this[onMouseDown], true);
+      container.removeEventListener('webkitmouseforcechanged', this[onMouseForceChange], false);
+    }
+
+    document.removeEventListener('mousemove', this[onMouseMove]);
+    document.removeEventListener('mouseup', this[onMouseUp]);
+  }
+
+  /**
+   * Mouse force will begin handler
+   * @private
+   * @param {Event} event - Mouse force will begin event
+   */
+  [onMouseForceWillBegin](event) {
+    event.preventDefault();
+    this.mightDrag = true;
+  }
+
+  /**
+   * Mouse force down handler
+   * @private
+   * @param {Event} event - Mouse force down event
+   */
+  [onMouseForceDown](event) {
+    if (this.dragging) {
+      return;
+    }
+
+    const target = document.elementFromPoint(event.clientX, event.clientY);
+    const container = event.currentTarget;
+
+    const dragStartEvent = new _SensorEvent.DragStartSensorEvent({
+      clientX: event.clientX,
+      clientY: event.clientY,
+      target,
+      container,
+      originalEvent: event
+    });
+
+    this.trigger(container, dragStartEvent);
+
+    this.currentContainer = container;
+    this.dragging = !dragStartEvent.canceled();
+    this.mightDrag = false;
+  }
+
+  /**
+   * Mouse up handler
+   * @private
+   * @param {Event} event - Mouse up event
+   */
+  [onMouseUp](event) {
+    if (!this.dragging) {
+      return;
+    }
+
+    const dragStopEvent = new _SensorEvent.DragStopSensorEvent({
+      clientX: event.clientX,
+      clientY: event.clientY,
+      target: null,
+      container: this.currentContainer,
+      originalEvent: event
+    });
+
+    this.trigger(this.currentContainer, dragStopEvent);
+
+    this.currentContainer = null;
+    this.dragging = false;
+    this.mightDrag = false;
+  }
+
+  /**
+   * Mouse down handler
+   * @private
+   * @param {Event} event - Mouse down event
+   */
+  [onMouseDown](event) {
+    if (!this.mightDrag) {
+      return;
+    }
+
+    // Need workaround for real click
+    // Cancel potential drag events
+    event.stopPropagation();
+    event.stopImmediatePropagation();
+    event.preventDefault();
+  }
+
+  /**
+   * Mouse move handler
+   * @private
+   * @param {Event} event - Mouse force will begin event
+   */
+  [onMouseMove](event) {
+    if (!this.dragging) {
+      return;
+    }
+
+    const target = document.elementFromPoint(event.clientX, event.clientY);
+
+    const dragMoveEvent = new _SensorEvent.DragMoveSensorEvent({
+      clientX: event.clientX,
+      clientY: event.clientY,
+      target,
+      container: this.currentContainer,
+      originalEvent: event
+    });
+
+    this.trigger(this.currentContainer, dragMoveEvent);
+  }
+
+  /**
+   * Mouse force change handler
+   * @private
+   * @param {Event} event - Mouse force change event
+   */
+  [onMouseForceChange](event) {
+    if (this.dragging) {
+      return;
+    }
+
+    const target = event.target;
+    const container = event.currentTarget;
+
+    const dragPressureEvent = new _SensorEvent.DragPressureSensorEvent({
+      pressure: event.webkitForce,
+      clientX: event.clientX,
+      clientY: event.clientY,
+      target,
+      container,
+      originalEvent: event
+    });
+
+    this.trigger(container, dragPressureEvent);
+  }
+
+  /**
+   * Mouse force global change handler
+   * @private
+   * @param {Event} event - Mouse force global change event
+   */
+  [onMouseForceGlobalChange](event) {
+    if (!this.dragging) {
+      return;
+    }
+
+    const target = event.target;
+
+    const dragPressureEvent = new _SensorEvent.DragPressureSensorEvent({
+      pressure: event.webkitForce,
+      clientX: event.clientX,
+      clientY: event.clientY,
+      target,
+      container: this.currentContainer,
+      originalEvent: event
+    });
+
+    this.trigger(this.currentContainer, dragPressureEvent);
+  }
+}
+exports.default = ForceTouchSensor;
+
+/***/ }),
+/* 41 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+
+var _ForceTouchSensor = __webpack_require__(40);
+
+var _ForceTouchSensor2 = _interopRequireDefault(_ForceTouchSensor);
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+exports.default = _ForceTouchSensor2.default;
+
+/***/ }),
+/* 42 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+
+var _utils = __webpack_require__(2);
+
+var _Sensor = __webpack_require__(4);
+
+var _Sensor2 = _interopRequireDefault(_Sensor);
+
+var _SensorEvent = __webpack_require__(3);
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+const onMouseDown = Symbol('onMouseDown');
+const onMouseUp = Symbol('onMouseUp');
+const onDragStart = Symbol('onDragStart');
+const onDragOver = Symbol('onDragOver');
+const onDragEnd = Symbol('onDragEnd');
+const onDrop = Symbol('onDrop');
+const reset = Symbol('reset');
+
+/**
+ * This sensor picks up native browser drag events and dictates drag operations
+ * @class DragSensor
+ * @module DragSensor
+ * @extends Sensor
+ */
+class DragSensor extends _Sensor2.default {
+  /**
+   * DragSensor constructor.
+   * @constructs DragSensor
+   * @param {HTMLElement[]|NodeList|HTMLElement} containers - Containers
+   * @param {Object} options - Options
+   */
+  constructor(containers = [], options = {}) {
+    super(containers, options);
+
+    /**
+     * Mouse down timer which will end up setting the draggable attribute, unless canceled
+     * @property mouseDownTimeout
+     * @type {Number}
+     */
+    this.mouseDownTimeout = null;
+
+    /**
+     * Draggable element needs to be remembered to unset the draggable attribute after drag operation has completed
+     * @property draggableElement
+     * @type {HTMLElement}
+     */
+    this.draggableElement = null;
+
+    /**
+     * Native draggable element could be links or images, their draggable state will be disabled during drag operation
+     * @property nativeDraggableElement
+     * @type {HTMLElement}
+     */
+    this.nativeDraggableElement = null;
+
+    this[onMouseDown] = this[onMouseDown].bind(this);
+    this[onMouseUp] = this[onMouseUp].bind(this);
+    this[onDragStart] = this[onDragStart].bind(this);
+    this[onDragOver] = this[onDragOver].bind(this);
+    this[onDragEnd] = this[onDragEnd].bind(this);
+    this[onDrop] = this[onDrop].bind(this);
+  }
+
+  /**
+   * Attaches sensors event listeners to the DOM
+   */
+  attach() {
+    document.addEventListener('mousedown', this[onMouseDown], true);
+  }
+
+  /**
+   * Detaches sensors event listeners to the DOM
+   */
+  detach() {
+    document.removeEventListener('mousedown', this[onMouseDown], true);
+  }
+
+  /**
+   * Drag start handler
+   * @private
+   * @param {Event} event - Drag start event
+   */
+  [onDragStart](event) {
+    // Need for firefox. "text" key is needed for IE
+    event.dataTransfer.setData('text', '');
+    event.dataTransfer.effectAllowed = this.options.type;
+
+    const target = document.elementFromPoint(event.clientX, event.clientY);
+    this.currentContainer = (0, _utils.closest)(event.target, this.containers);
+
+    if (!this.currentContainer) {
+      return;
+    }
+
+    const dragStartEvent = new _SensorEvent.DragStartSensorEvent({
+      clientX: event.clientX,
+      clientY: event.clientY,
+      target,
+      container: this.currentContainer,
+      originalEvent: event
+    });
+
+    // Workaround
+    setTimeout(() => {
+      this.trigger(this.currentContainer, dragStartEvent);
+
+      if (dragStartEvent.canceled()) {
+        this.dragging = false;
+      } else {
+        this.dragging = true;
+      }
+    }, 0);
+  }
+
+  /**
+   * Drag over handler
+   * @private
+   * @param {Event} event - Drag over event
+   */
+  [onDragOver](event) {
+    if (!this.dragging) {
+      return;
+    }
+
+    const target = document.elementFromPoint(event.clientX, event.clientY);
+    const container = this.currentContainer;
+
+    const dragMoveEvent = new _SensorEvent.DragMoveSensorEvent({
+      clientX: event.clientX,
+      clientY: event.clientY,
+      target,
+      container,
+      originalEvent: event
+    });
+
+    this.trigger(container, dragMoveEvent);
+
+    if (!dragMoveEvent.canceled()) {
+      event.preventDefault();
+      event.dataTransfer.dropEffect = this.options.type;
+    }
+  }
+
+  /**
+   * Drag end handler
+   * @private
+   * @param {Event} event - Drag end event
+   */
+  [onDragEnd](event) {
+    if (!this.dragging) {
+      return;
+    }
+
+    document.removeEventListener('mouseup', this[onMouseUp], true);
+
+    const target = document.elementFromPoint(event.clientX, event.clientY);
+    const container = this.currentContainer;
+
+    const dragStopEvent = new _SensorEvent.DragStopSensorEvent({
+      clientX: event.clientX,
+      clientY: event.clientY,
+      target,
+      container,
+      originalEvent: event
+    });
+
+    this.trigger(container, dragStopEvent);
+
+    this.dragging = false;
+    this.startEvent = null;
+
+    this[reset]();
+  }
+
+  /**
+   * Drop handler
+   * @private
+   * @param {Event} event - Drop event
+   */
+  [onDrop](event) {
+    // eslint-disable-line class-methods-use-this
+    event.preventDefault();
+  }
+
+  /**
+   * Mouse down handler
+   * @private
+   * @param {Event} event - Mouse down event
+   */
+  [onMouseDown](event) {
+    // Firefox bug for inputs within draggables https://bugzilla.mozilla.org/show_bug.cgi?id=739071
+    if (event.target && (event.target.form || event.target.contenteditable)) {
+      return;
+    }
+
+    const nativeDraggableElement = (0, _utils.closest)(event.target, element => element.draggable);
+
+    if (nativeDraggableElement) {
+      nativeDraggableElement.draggable = false;
+      this.nativeDraggableElement = nativeDraggableElement;
+    }
+
+    document.addEventListener('mouseup', this[onMouseUp], true);
+    document.addEventListener('dragstart', this[onDragStart], false);
+    document.addEventListener('dragover', this[onDragOver], false);
+    document.addEventListener('dragend', this[onDragEnd], false);
+    document.addEventListener('drop', this[onDrop], false);
+
+    const target = (0, _utils.closest)(event.target, this.options.draggable);
+
+    if (!target) {
+      return;
+    }
+
+    this.startEvent = event;
+
+    this.mouseDownTimeout = setTimeout(() => {
+      target.draggable = true;
+      this.draggableElement = target;
+    }, this.options.delay);
+  }
+
+  /**
+   * Mouse up handler
+   * @private
+   * @param {Event} event - Mouse up event
+   */
+  [onMouseUp]() {
+    this[reset]();
+  }
+
+  /**
+   * Mouse up handler
+   * @private
+   * @param {Event} event - Mouse up event
+   */
+  [reset]() {
+    clearTimeout(this.mouseDownTimeout);
+
+    document.removeEventListener('mouseup', this[onMouseUp], true);
+    document.removeEventListener('dragstart', this[onDragStart], false);
+    document.removeEventListener('dragover', this[onDragOver], false);
+    document.removeEventListener('dragend', this[onDragEnd], false);
+    document.removeEventListener('drop', this[onDrop], false);
+
+    if (this.nativeDraggableElement) {
+      this.nativeDraggableElement.draggable = true;
+      this.nativeDraggableElement = null;
+    }
+
+    if (this.draggableElement) {
+      this.draggableElement.draggable = false;
+      this.draggableElement = null;
+    }
+  }
+}
+exports.default = DragSensor;
+
+/***/ }),
+/* 43 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+
+var _DragSensor = __webpack_require__(42);
+
+var _DragSensor2 = _interopRequireDefault(_DragSensor);
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+exports.default = _DragSensor2.default;
+
+/***/ }),
+/* 44 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+
+var _utils = __webpack_require__(2);
+
+var _Sensor = __webpack_require__(4);
+
+var _Sensor2 = _interopRequireDefault(_Sensor);
+
+var _SensorEvent = __webpack_require__(3);
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+const onTouchStart = Symbol('onTouchStart');
+const onTouchEnd = Symbol('onTouchEnd');
+const onTouchMove = Symbol('onTouchMove');
+const startDrag = Symbol('startDrag');
+const onDistanceChange = Symbol('onDistanceChange');
+
+/**
+ * Prevents scrolling when set to true
+ * @var {Boolean} preventScrolling
+ */
+let preventScrolling = false;
+
+// WebKit requires cancelable `touchmove` events to be added as early as possible
+window.addEventListener('touchmove', event => {
+  if (!preventScrolling) {
+    return;
+  }
+
+  // Prevent scrolling
+  event.preventDefault();
+}, { passive: false });
+
+/**
+ * This sensor picks up native browser touch events and dictates drag operations
+ * @class TouchSensor
+ * @module TouchSensor
+ * @extends Sensor
+ */
+class TouchSensor extends _Sensor2.default {
+  /**
+   * TouchSensor constructor.
+   * @constructs TouchSensor
+   * @param {HTMLElement[]|NodeList|HTMLElement} containers - Containers
+   * @param {Object} options - Options
+   */
+  constructor(containers = [], options = {}) {
+    super(containers, options);
+
+    /**
+     * Closest scrollable container so accidental scroll can cancel long touch
+     * @property currentScrollableParent
+     * @type {HTMLElement}
+     */
+    this.currentScrollableParent = null;
+
+    /**
+     * TimeoutID for managing delay
+     * @property tapTimeout
+     * @type {Number}
+     */
+    this.tapTimeout = null;
+
+    /**
+     * touchMoved indicates if touch has moved during tapTimeout
+     * @property touchMoved
+     * @type {Boolean}
+     */
+    this.touchMoved = false;
+
+    this[onTouchStart] = this[onTouchStart].bind(this);
+    this[onTouchEnd] = this[onTouchEnd].bind(this);
+    this[onTouchMove] = this[onTouchMove].bind(this);
+    this[startDrag] = this[startDrag].bind(this);
+    this[onDistanceChange] = this[onDistanceChange].bind(this);
+  }
+
+  /**
+   * Attaches sensors event listeners to the DOM
+   */
+  attach() {
+    document.addEventListener('touchstart', this[onTouchStart]);
+  }
+
+  /**
+   * Detaches sensors event listeners to the DOM
+   */
+  detach() {
+    document.removeEventListener('touchstart', this[onTouchStart]);
+  }
+
+  /**
+   * Touch start handler
+   * @private
+   * @param {Event} event - Touch start event
+   */
+  [onTouchStart](event) {
+    const container = (0, _utils.closest)(event.target, this.containers);
+
+    if (!container) {
+      return;
+    }
+    const { distance = 0, delay = 0 } = this.options;
+    const { pageX, pageY } = (0, _utils.touchCoords)(event);
+
+    Object.assign(this, { pageX, pageY });
+    this.onTouchStartAt = Date.now();
+    this.startEvent = event;
+    this.currentContainer = container;
+
+    document.addEventListener('touchend', this[onTouchEnd]);
+    document.addEventListener('touchcancel', this[onTouchEnd]);
+    document.addEventListener('touchmove', this[onDistanceChange]);
+    container.addEventListener('contextmenu', onContextMenu);
+
+    if (distance) {
+      preventScrolling = true;
+    }
+
+    this.tapTimeout = window.setTimeout(() => {
+      this[onDistanceChange]({ touches: [{ pageX: this.pageX, pageY: this.pageY }] });
+    }, delay);
+  }
+
+  /**
+   * Start the drag
+   * @private
+   */
+  [startDrag]() {
+    const startEvent = this.startEvent;
+    const container = this.currentContainer;
+    const touch = (0, _utils.touchCoords)(startEvent);
+
+    const dragStartEvent = new _SensorEvent.DragStartSensorEvent({
+      clientX: touch.pageX,
+      clientY: touch.pageY,
+      target: startEvent.target,
+      container,
+      originalEvent: startEvent
+    });
+
+    this.trigger(this.currentContainer, dragStartEvent);
+
+    this.dragging = !dragStartEvent.canceled();
+
+    if (this.dragging) {
+      document.addEventListener('touchmove', this[onTouchMove]);
+    }
+    preventScrolling = this.dragging;
+  }
+
+  /**
+   * Touch move handler prior to drag start.
+   * @private
+   * @param {Event} event - Touch move event
+   */
+  [onDistanceChange](event) {
+    const { delay, distance } = this.options;
+    const { startEvent } = this;
+    const start = (0, _utils.touchCoords)(startEvent);
+    const current = (0, _utils.touchCoords)(event);
+    const timeElapsed = Date.now() - this.onTouchStartAt;
+    const distanceTravelled = (0, _utils.distance)(start.pageX, start.pageY, current.pageX, current.pageY);
+
+    Object.assign(this, current);
+    if (timeElapsed >= delay && distanceTravelled >= distance) {
+      window.clearTimeout(this.tapTimeout);
+      document.removeEventListener('touchmove', this[onDistanceChange]);
+      this[startDrag]();
+    }
+  }
+
+  /**
+   * Mouse move handler while dragging
+   * @private
+   * @param {Event} event - Touch move event
+   */
+  [onTouchMove](event) {
+    if (!this.dragging) {
+      return;
+    }
+    const { pageX, pageY } = (0, _utils.touchCoords)(event);
+    const target = document.elementFromPoint(pageX - window.scrollX, pageY - window.scrollY);
+
+    const dragMoveEvent = new _SensorEvent.DragMoveSensorEvent({
+      clientX: pageX,
+      clientY: pageY,
+      target,
+      container: this.currentContainer,
+      originalEvent: event
+    });
+
+    this.trigger(this.currentContainer, dragMoveEvent);
+  }
+
+  /**
+   * Touch end handler
+   * @private
+   * @param {Event} event - Touch end event
+   */
+  [onTouchEnd](event) {
+    clearTimeout(this.tapTimeout);
+    preventScrolling = false;
+
+    document.removeEventListener('touchend', this[onTouchEnd]);
+    document.removeEventListener('touchcancel', this[onTouchEnd]);
+    document.removeEventListener('touchmove', this[onDistanceChange]);
+
+    if (this.currentContainer) {
+      this.currentContainer.removeEventListener('contextmenu', onContextMenu);
+    }
+
+    if (!this.dragging) {
+      return;
+    }
+
+    document.removeEventListener('touchmove', this[onTouchMove]);
+
+    const { pageX, pageY } = (0, _utils.touchCoords)(event);
+    const target = document.elementFromPoint(pageX - window.scrollX, pageY - window.scrollY);
+
+    event.preventDefault();
+
+    const dragStopEvent = new _SensorEvent.DragStopSensorEvent({
+      clientX: pageX,
+      clientY: pageY,
+      target,
+      container: this.currentContainer,
+      originalEvent: event
+    });
+
+    this.trigger(this.currentContainer, dragStopEvent);
+
+    this.currentContainer = null;
+    this.dragging = false;
+    this.startEvent = null;
+  }
+}
+
+exports.default = TouchSensor;
+function onContextMenu(event) {
+  event.preventDefault();
+  event.stopPropagation();
+}
+
+/***/ }),
+/* 45 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+
+var _TouchSensor = __webpack_require__(44);
+
+var _TouchSensor2 = _interopRequireDefault(_TouchSensor);
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+exports.default = _TouchSensor2.default;
+
+/***/ }),
+/* 46 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+exports.DragPressureSensorEvent = exports.DragStopSensorEvent = exports.DragMoveSensorEvent = exports.DragStartSensorEvent = exports.SensorEvent = undefined;
+
+var _AbstractEvent = __webpack_require__(1);
+
+var _AbstractEvent2 = _interopRequireDefault(_AbstractEvent);
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+/**
+ * Base sensor event
+ * @class SensorEvent
+ * @module SensorEvent
+ * @extends AbstractEvent
+ */
+class SensorEvent extends _AbstractEvent2.default {
+  /**
+   * Original browser event that triggered a sensor
+   * @property originalEvent
+   * @type {Event}
+   * @readonly
+   */
+  get originalEvent() {
+    return this.data.originalEvent;
+  }
+
+  /**
+   * Normalized clientX for both touch and mouse events
+   * @property clientX
+   * @type {Number}
+   * @readonly
+   */
+  get clientX() {
+    return this.data.clientX;
+  }
+
+  /**
+   * Normalized clientY for both touch and mouse events
+   * @property clientY
+   * @type {Number}
+   * @readonly
+   */
+  get clientY() {
+    return this.data.clientY;
+  }
+
+  /**
+   * Normalized target for both touch and mouse events
+   * Returns the element that is behind cursor or touch pointer
+   * @property target
+   * @type {HTMLElement}
+   * @readonly
+   */
+  get target() {
+    return this.data.target;
+  }
+
+  /**
+   * Container that initiated the sensor
+   * @property container
+   * @type {HTMLElement}
+   * @readonly
+   */
+  get container() {
+    return this.data.container;
+  }
+
+  /**
+   * Trackpad pressure
+   * @property pressure
+   * @type {Number}
+   * @readonly
+   */
+  get pressure() {
+    return this.data.pressure;
+  }
+}
+
+exports.SensorEvent = SensorEvent; /**
+                                    * Drag start sensor event
+                                    * @class DragStartSensorEvent
+                                    * @module DragStartSensorEvent
+                                    * @extends SensorEvent
+                                    */
+
+class DragStartSensorEvent extends SensorEvent {}
+
+exports.DragStartSensorEvent = DragStartSensorEvent; /**
+                                                      * Drag move sensor event
+                                                      * @class DragMoveSensorEvent
+                                                      * @module DragMoveSensorEvent
+                                                      * @extends SensorEvent
+                                                      */
+
+DragStartSensorEvent.type = 'drag:start';
+class DragMoveSensorEvent extends SensorEvent {}
+
+exports.DragMoveSensorEvent = DragMoveSensorEvent; /**
+                                                    * Drag stop sensor event
+                                                    * @class DragStopSensorEvent
+                                                    * @module DragStopSensorEvent
+                                                    * @extends SensorEvent
+                                                    */
+
+DragMoveSensorEvent.type = 'drag:move';
+class DragStopSensorEvent extends SensorEvent {}
+
+exports.DragStopSensorEvent = DragStopSensorEvent; /**
+                                                    * Drag pressure sensor event
+                                                    * @class DragPressureSensorEvent
+                                                    * @module DragPressureSensorEvent
+                                                    * @extends SensorEvent
+                                                    */
+
+DragStopSensorEvent.type = 'drag:stop';
+class DragPressureSensorEvent extends SensorEvent {}
+exports.DragPressureSensorEvent = DragPressureSensorEvent;
+DragPressureSensorEvent.type = 'drag:pressure';
+
+/***/ }),
+/* 47 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+
+var _utils = __webpack_require__(2);
+
+var _Sensor = __webpack_require__(4);
+
+var _Sensor2 = _interopRequireDefault(_Sensor);
+
+var _SensorEvent = __webpack_require__(3);
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+const onContextMenuWhileDragging = Symbol('onContextMenuWhileDragging');
+const onMouseDown = Symbol('onMouseDown');
+const onMouseMove = Symbol('onMouseMove');
+const onMouseUp = Symbol('onMouseUp');
+const startDrag = Symbol('startDrag');
+const onDistanceChange = Symbol('onDistanceChange');
+
+/**
+ * This sensor picks up native browser mouse events and dictates drag operations
+ * @class MouseSensor
+ * @module MouseSensor
+ * @extends Sensor
+ */
+class MouseSensor extends _Sensor2.default {
+  /**
+   * MouseSensor constructor.
+   * @constructs MouseSensor
+   * @param {HTMLElement[]|NodeList|HTMLElement} containers - Containers
+   * @param {Object} options - Options
+   */
+  constructor(containers = [], options = {}) {
+    super(containers, options);
+
+    /**
+     * Mouse down timer which will end up triggering the drag start operation
+     * @property mouseDownTimeout
+     * @type {Number}
+     */
+    this.mouseDownTimeout = null;
+
+    this[onContextMenuWhileDragging] = this[onContextMenuWhileDragging].bind(this);
+    this[onMouseDown] = this[onMouseDown].bind(this);
+    this[onMouseMove] = this[onMouseMove].bind(this);
+    this[onMouseUp] = this[onMouseUp].bind(this);
+    this[startDrag] = this[startDrag].bind(this);
+    this[onDistanceChange] = this[onDistanceChange].bind(this);
+  }
+
+  /**
+   * Attaches sensors event listeners to the DOM
+   */
+  attach() {
+    document.addEventListener('mousedown', this[onMouseDown], true);
+  }
+
+  /**
+   * Detaches sensors event listeners to the DOM
+   */
+  detach() {
+    document.removeEventListener('mousedown', this[onMouseDown], true);
+  }
+
+  /**
+   * Mouse down handler
+   * @private
+   * @param {Event} event - Mouse down event
+   */
+  [onMouseDown](event) {
+    if (event.button !== 0 || event.ctrlKey || event.metaKey) {
+      return;
+    }
+    const container = (0, _utils.closest)(event.target, this.containers);
+
+    if (!container) {
+      return;
+    }
+
+    const { delay = 0 } = this.options;
+    const { pageX, pageY } = event;
+
+    Object.assign(this, { pageX, pageY });
+    this.onMouseDownAt = Date.now();
+    this.startEvent = event;
+
+    this.currentContainer = container;
+    document.addEventListener('mouseup', this[onMouseUp]);
+    document.addEventListener('dragstart', preventNativeDragStart);
+    document.addEventListener('mousemove', this[onDistanceChange]);
+
+    this.mouseDownTimeout = window.setTimeout(() => {
+      this[onDistanceChange]({ pageX: this.pageX, pageY: this.pageY });
+    }, delay);
+  }
+
+  /**
+   * Start the drag
+   * @private
+   */
+  [startDrag]() {
+    const startEvent = this.startEvent;
+    const container = this.currentContainer;
+
+    const dragStartEvent = new _SensorEvent.DragStartSensorEvent({
+      clientX: startEvent.clientX,
+      clientY: startEvent.clientY,
+      target: startEvent.target,
+      container,
+      originalEvent: startEvent
+    });
+
+    this.trigger(this.currentContainer, dragStartEvent);
+
+    this.dragging = !dragStartEvent.canceled();
+
+    if (this.dragging) {
+      document.addEventListener('contextmenu', this[onContextMenuWhileDragging], true);
+      document.addEventListener('mousemove', this[onMouseMove]);
+    }
+  }
+
+  /**
+   * Detect change in distance, starting drag when both
+   * delay and distance requirements are met
+   * @private
+   * @param {Event} event - Mouse move event
+   */
+  [onDistanceChange](event) {
+    const { pageX, pageY } = event;
+    const { delay, distance } = this.options;
+    const { startEvent } = this;
+
+    Object.assign(this, { pageX, pageY });
+
+    if (!this.currentContainer) {
+      return;
+    }
+
+    const timeElapsed = Date.now() - this.onMouseDownAt;
+    const distanceTravelled = (0, _utils.distance)(startEvent.pageX, startEvent.pageY, pageX, pageY) || 0;
+
+    if (timeElapsed >= delay && distanceTravelled >= distance) {
+      window.clearTimeout(this.mouseDownTimeout);
+      document.removeEventListener('mousemove', this[onDistanceChange]);
+      this[startDrag]();
+    }
+  }
+
+  /**
+   * Mouse move handler
+   * @private
+   * @param {Event} event - Mouse move event
+   */
+  [onMouseMove](event) {
+    if (!this.dragging) {
+      return;
+    }
+
+    const target = document.elementFromPoint(event.clientX, event.clientY);
+
+    const dragMoveEvent = new _SensorEvent.DragMoveSensorEvent({
+      clientX: event.clientX,
+      clientY: event.clientY,
+      target,
+      container: this.currentContainer,
+      originalEvent: event
+    });
+
+    this.trigger(this.currentContainer, dragMoveEvent);
+  }
+
+  /**
+   * Mouse up handler
+   * @private
+   * @param {Event} event - Mouse up event
+   */
+  [onMouseUp](event) {
+    clearTimeout(this.mouseDownTimeout);
+
+    if (event.button !== 0) {
+      return;
+    }
+
+    document.removeEventListener('mouseup', this[onMouseUp]);
+    document.removeEventListener('dragstart', preventNativeDragStart);
+    document.removeEventListener('mousemove', this[onDistanceChange]);
+
+    if (!this.dragging) {
+      return;
+    }
+
+    const target = document.elementFromPoint(event.clientX, event.clientY);
+
+    const dragStopEvent = new _SensorEvent.DragStopSensorEvent({
+      clientX: event.clientX,
+      clientY: event.clientY,
+      target,
+      container: this.currentContainer,
+      originalEvent: event
+    });
+
+    this.trigger(this.currentContainer, dragStopEvent);
+
+    document.removeEventListener('contextmenu', this[onContextMenuWhileDragging], true);
+    document.removeEventListener('mousemove', this[onMouseMove]);
+
+    this.currentContainer = null;
+    this.dragging = false;
+    this.startEvent = null;
+  }
+
+  /**
+   * Context menu handler
+   * @private
+   * @param {Event} event - Context menu event
+   */
+  [onContextMenuWhileDragging](event) {
+    event.preventDefault();
+  }
+}
+
+exports.default = MouseSensor;
+function preventNativeDragStart(event) {
+  event.preventDefault();
+}
+
+/***/ }),
+/* 48 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+
+var _MouseSensor = __webpack_require__(47);
+
+var _MouseSensor2 = _interopRequireDefault(_MouseSensor);
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+exports.default = _MouseSensor2.default;
+
+/***/ }),
+/* 49 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+
+var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
+
+/**
+ * Base sensor class. Extend from this class to create a new or custom sensor
+ * @class Sensor
+ * @module Sensor
+ */
+class Sensor {
+  /**
+   * Sensor constructor.
+   * @constructs Sensor
+   * @param {HTMLElement[]|NodeList|HTMLElement} containers - Containers
+   * @param {Object} options - Options
+   */
+  constructor(containers = [], options = {}) {
+    /**
+     * Current containers
+     * @property containers
+     * @type {HTMLElement[]}
+     */
+    this.containers = [...containers];
+
+    /**
+     * Current options
+     * @property options
+     * @type {Object}
+     */
+    this.options = _extends({}, options);
+
+    /**
+     * Current drag state
+     * @property dragging
+     * @type {Boolean}
+     */
+    this.dragging = false;
+
+    /**
+     * Current container
+     * @property currentContainer
+     * @type {HTMLElement}
+     */
+    this.currentContainer = null;
+
+    /**
+     * The event of the initial sensor down
+     * @property startEvent
+     * @type {Event}
+     */
+    this.startEvent = null;
+  }
+
+  /**
+   * Attaches sensors event listeners to the DOM
+   * @return {Sensor}
+   */
+  attach() {
+    return this;
+  }
+
+  /**
+   * Detaches sensors event listeners to the DOM
+   * @return {Sensor}
+   */
+  detach() {
+    return this;
+  }
+
+  /**
+   * Adds container to this sensor instance
+   * @param {...HTMLElement} containers - Containers you want to add to this sensor
+   * @example draggable.addContainer(document.body)
+   */
+  addContainer(...containers) {
+    this.containers = [...this.containers, ...containers];
+  }
+
+  /**
+   * Removes container from this sensor instance
+   * @param {...HTMLElement} containers - Containers you want to remove from this sensor
+   * @example draggable.removeContainer(document.body)
+   */
+  removeContainer(...containers) {
+    this.containers = this.containers.filter(container => !containers.includes(container));
+  }
+
+  /**
+   * Triggers event on target element
+   * @param {HTMLElement} element - Element to trigger event on
+   * @param {SensorEvent} sensorEvent - Sensor event to trigger
+   */
+  trigger(element, sensorEvent) {
+    const event = document.createEvent('Event');
+    event.detail = sensorEvent;
+    event.initEvent(sensorEvent.type, true, true);
+    element.dispatchEvent(event);
+    this.lastEvent = sensorEvent;
+
+    return sensorEvent;
+  }
+}
+exports.default = Sensor;
+
+/***/ }),
+/* 50 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+exports.default = touchCoords;
+/**
+ * Returns the first touch event found in touches or changedTouches of a touch events.
+ * @param {TouchEvent} event a touch event
+ * @return {Touch} a touch object
+ */
+function touchCoords(event = {}) {
+  const { touches, changedTouches } = event;
+  return touches && touches[0] || changedTouches && changedTouches[0];
+}
+
+/***/ }),
+/* 51 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+
+var _touchCoords = __webpack_require__(50);
+
+var _touchCoords2 = _interopRequireDefault(_touchCoords);
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+exports.default = _touchCoords2.default;
+
+/***/ }),
+/* 52 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+exports.default = distance;
+/**
+ * Returns the distance between two points
+ * @param  {Number} x1 The X position of the first point
+ * @param  {Number} y1 The Y position of the first point
+ * @param  {Number} x2 The X position of the second point
+ * @param  {Number} y2 The Y position of the second point
+ * @return {Number}
+ */
+function distance(x1, y1, x2, y2) {
+  return Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2);
+}
+
+/***/ }),
+/* 53 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+
+var _distance = __webpack_require__(52);
+
+var _distance2 = _interopRequireDefault(_distance);
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+exports.default = _distance2.default;
+
+/***/ }),
+/* 54 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+exports.default = requestNextAnimationFrame;
+function requestNextAnimationFrame(callback) {
+  return requestAnimationFrame(() => {
+    requestAnimationFrame(callback);
+  });
+}
+
+/***/ }),
+/* 55 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+
+var _requestNextAnimationFrame = __webpack_require__(54);
+
+var _requestNextAnimationFrame2 = _interopRequireDefault(_requestNextAnimationFrame);
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+exports.default = _requestNextAnimationFrame2.default;
+
+/***/ }),
+/* 56 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+exports.default = closest;
+const matchFunction = Element.prototype.matches || Element.prototype.webkitMatchesSelector || Element.prototype.mozMatchesSelector || Element.prototype.msMatchesSelector;
+
+/**
+ * Get the closest parent element of a given element that matches the given
+ * selector string or matching function
+ *
+ * @param {Element} element The child element to find a parent of
+ * @param {String|Function} selector The string or function to use to match
+ *     the parent element
+ * @return {Element|null}
+ */
+function closest(element, value) {
+  if (!element) {
+    return null;
+  }
+
+  const selector = value;
+  const callback = value;
+  const nodeList = value;
+  const singleElement = value;
+
+  const isSelector = Boolean(typeof value === 'string');
+  const isFunction = Boolean(typeof value === 'function');
+  const isNodeList = Boolean(value instanceof NodeList || value instanceof Array);
+  const isElement = Boolean(value instanceof HTMLElement);
+
+  function conditionFn(currentElement) {
+    if (!currentElement) {
+      return currentElement;
+    } else if (isSelector) {
+      return matchFunction.call(currentElement, selector);
+    } else if (isNodeList) {
+      return [...nodeList].includes(currentElement);
+    } else if (isElement) {
+      return singleElement === currentElement;
+    } else if (isFunction) {
+      return callback(currentElement);
+    } else {
+      return null;
+    }
+  }
+
+  let current = element;
+
+  do {
+    current = current.correspondingUseElement || current.correspondingElement || current;
+
+    if (conditionFn(current)) {
+      return current;
+    }
+
+    current = current.parentNode;
+  } while (current && current !== document.body && current !== document);
+
+  return null;
+}
+
+/***/ }),
+/* 57 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+
+var _closest = __webpack_require__(56);
+
+var _closest2 = _interopRequireDefault(_closest);
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+exports.default = _closest2.default;
+
+/***/ }),
+/* 58 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+exports.defaultOptions = exports.scroll = exports.onDragStop = exports.onDragMove = exports.onDragStart = undefined;
+
+var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
+
+var _AbstractPlugin = __webpack_require__(0);
+
+var _AbstractPlugin2 = _interopRequireDefault(_AbstractPlugin);
+
+var _utils = __webpack_require__(2);
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+const onDragStart = exports.onDragStart = Symbol('onDragStart');
+const onDragMove = exports.onDragMove = Symbol('onDragMove');
+const onDragStop = exports.onDragStop = Symbol('onDragStop');
+const scroll = exports.scroll = Symbol('scroll');
+
+/**
+ * Scrollable default options
+ * @property {Object} defaultOptions
+ * @property {Number} defaultOptions.speed
+ * @property {Number} defaultOptions.sensitivity
+ * @property {HTMLElement[]} defaultOptions.scrollableElements
+ * @type {Object}
+ */
+const defaultOptions = exports.defaultOptions = {
+  speed: 6,
+  sensitivity: 50,
+  scrollableElements: []
+};
+
+/**
+ * Scrollable plugin which scrolls the closest scrollable parent
+ * @class Scrollable
+ * @module Scrollable
+ * @extends AbstractPlugin
+ */
+class Scrollable extends _AbstractPlugin2.default {
+  /**
+   * Scrollable constructor.
+   * @constructs Scrollable
+   * @param {Draggable} draggable - Draggable instance
+   */
+  constructor(draggable) {
+    super(draggable);
+
+    /**
+     * Scrollable options
+     * @property {Object} options
+     * @property {Number} options.speed
+     * @property {Number} options.sensitivity
+     * @property {HTMLElement[]} options.scrollableElements
+     * @type {Object}
+     */
+    this.options = _extends({}, defaultOptions, this.getOptions());
+
+    /**
+     * Keeps current mouse position
+     * @property {Object} currentMousePosition
+     * @property {Number} currentMousePosition.clientX
+     * @property {Number} currentMousePosition.clientY
+     * @type {Object|null}
+     */
+    this.currentMousePosition = null;
+
+    /**
+     * Scroll animation frame
+     * @property scrollAnimationFrame
+     * @type {Number|null}
+     */
+    this.scrollAnimationFrame = null;
+
+    /**
+     * Closest scrollable element
+     * @property scrollableElement
+     * @type {HTMLElement|null}
+     */
+    this.scrollableElement = null;
+
+    /**
+     * Animation frame looking for the closest scrollable element
+     * @property findScrollableElementFrame
+     * @type {Number|null}
+     */
+    this.findScrollableElementFrame = null;
+
+    this[onDragStart] = this[onDragStart].bind(this);
+    this[onDragMove] = this[onDragMove].bind(this);
+    this[onDragStop] = this[onDragStop].bind(this);
+    this[scroll] = this[scroll].bind(this);
+  }
+
+  /**
+   * Attaches plugins event listeners
+   */
+  attach() {
+    this.draggable.on('drag:start', this[onDragStart]).on('drag:move', this[onDragMove]).on('drag:stop', this[onDragStop]);
+  }
+
+  /**
+   * Detaches plugins event listeners
+   */
+  detach() {
+    this.draggable.off('drag:start', this[onDragStart]).off('drag:move', this[onDragMove]).off('drag:stop', this[onDragStop]);
+  }
+
+  /**
+   * Returns options passed through draggable
+   * @return {Object}
+   */
+  getOptions() {
+    return this.draggable.options.scrollable || {};
+  }
+
+  /**
+   * Returns closest scrollable elements by element
+   * @param {HTMLElement} target
+   * @return {HTMLElement}
+   */
+  getScrollableElement(target) {
+    if (this.hasDefinedScrollableElements()) {
+      return (0, _utils.closest)(target, this.options.scrollableElements) || document.documentElement;
+    } else {
+      return closestScrollableElement(target);
+    }
+  }
+
+  /**
+   * Returns true if at least one scrollable element have been defined via options
+   * @param {HTMLElement} target
+   * @return {Boolean}
+   */
+  hasDefinedScrollableElements() {
+    return Boolean(this.options.scrollableElements.length !== 0);
+  }
+
+  /**
+   * Drag start handler. Finds closest scrollable parent in separate frame
+   * @param {DragStartEvent} dragEvent
+   * @private
+   */
+  [onDragStart](dragEvent) {
+    this.findScrollableElementFrame = requestAnimationFrame(() => {
+      this.scrollableElement = this.getScrollableElement(dragEvent.source);
+    });
+  }
+
+  /**
+   * Drag move handler. Remembers mouse position and initiates scrolling
+   * @param {DragMoveEvent} dragEvent
+   * @private
+   */
+  [onDragMove](dragEvent) {
+    this.findScrollableElementFrame = requestAnimationFrame(() => {
+      this.scrollableElement = this.getScrollableElement(dragEvent.sensorEvent.target);
+    });
+
+    if (!this.scrollableElement) {
+      return;
+    }
+
+    const sensorEvent = dragEvent.sensorEvent;
+    const scrollOffset = { x: 0, y: 0 };
+
+    if ('ontouchstart' in window) {
+      scrollOffset.y = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0;
+      scrollOffset.x = window.pageXOffset || document.documentElement.scrollLeft || document.body.scrollLeft || 0;
+    }
+
+    this.currentMousePosition = {
+      clientX: sensorEvent.clientX - scrollOffset.x,
+      clientY: sensorEvent.clientY - scrollOffset.y
+    };
+
+    this.scrollAnimationFrame = requestAnimationFrame(this[scroll]);
+  }
+
+  /**
+   * Drag stop handler. Cancels scroll animations and resets state
+   * @private
+   */
+  [onDragStop]() {
+    cancelAnimationFrame(this.scrollAnimationFrame);
+    cancelAnimationFrame(this.findScrollableElementFrame);
+
+    this.scrollableElement = null;
+    this.scrollAnimationFrame = null;
+    this.findScrollableElementFrame = null;
+    this.currentMousePosition = null;
+  }
+
+  /**
+   * Scroll function that does the heavylifting
+   * @private
+   */
+  [scroll]() {
+    if (!this.scrollableElement || !this.currentMousePosition) {
+      return;
+    }
+
+    cancelAnimationFrame(this.scrollAnimationFrame);
+
+    const { speed, sensitivity } = this.options;
+
+    const rect = this.scrollableElement.getBoundingClientRect();
+    const bottomCutOff = rect.bottom > window.innerHeight;
+    const topCutOff = rect.top < 0;
+    const cutOff = topCutOff || bottomCutOff;
+
+    const documentScrollingElement = getDocumentScrollingElement();
+    const scrollableElement = this.scrollableElement;
+    const clientX = this.currentMousePosition.clientX;
+    const clientY = this.currentMousePosition.clientY;
+
+    if (scrollableElement !== document.body && scrollableElement !== document.documentElement && !cutOff) {
+      const { offsetHeight, offsetWidth } = scrollableElement;
+
+      if (rect.top + offsetHeight - clientY < sensitivity) {
+        scrollableElement.scrollTop += speed;
+      } else if (clientY - rect.top < sensitivity) {
+        scrollableElement.scrollTop -= speed;
+      }
+
+      if (rect.left + offsetWidth - clientX < sensitivity) {
+        scrollableElement.scrollLeft += speed;
+      } else if (clientX - rect.left < sensitivity) {
+        scrollableElement.scrollLeft -= speed;
+      }
+    } else {
+      const { innerHeight, innerWidth } = window;
+
+      if (clientY < sensitivity) {
+        documentScrollingElement.scrollTop -= speed;
+      } else if (innerHeight - clientY < sensitivity) {
+        documentScrollingElement.scrollTop += speed;
+      }
+
+      if (clientX < sensitivity) {
+        documentScrollingElement.scrollLeft -= speed;
+      } else if (innerWidth - clientX < sensitivity) {
+        documentScrollingElement.scrollLeft += speed;
+      }
+    }
+
+    this.scrollAnimationFrame = requestAnimationFrame(this[scroll]);
+  }
+}
+
+exports.default = Scrollable; /**
+                               * Returns true if the passed element has overflow
+                               * @param {HTMLElement} element
+                               * @return {Boolean}
+                               * @private
+                               */
+
+function hasOverflow(element) {
+  const overflowRegex = /(auto|scroll)/;
+  const computedStyles = getComputedStyle(element, null);
+
+  const overflow = computedStyles.getPropertyValue('overflow') + computedStyles.getPropertyValue('overflow-y') + computedStyles.getPropertyValue('overflow-x');
+
+  return overflowRegex.test(overflow);
+}
+
+/**
+ * Returns true if the passed element is statically positioned
+ * @param {HTMLElement} element
+ * @return {Boolean}
+ * @private
+ */
+function isStaticallyPositioned(element) {
+  const position = getComputedStyle(element).getPropertyValue('position');
+  return position === 'static';
+}
+
+/**
+ * Finds closest scrollable element
+ * @param {HTMLElement} element
+ * @return {HTMLElement}
+ * @private
+ */
+function closestScrollableElement(element) {
+  if (!element) {
+    return getDocumentScrollingElement();
+  }
+
+  const position = getComputedStyle(element).getPropertyValue('position');
+  const excludeStaticParents = position === 'absolute';
+
+  const scrollableElement = (0, _utils.closest)(element, parent => {
+    if (excludeStaticParents && isStaticallyPositioned(parent)) {
+      return false;
+    }
+    return hasOverflow(parent);
+  });
+
+  if (position === 'fixed' || !scrollableElement) {
+    return getDocumentScrollingElement();
+  } else {
+    return scrollableElement;
+  }
+}
+
+/**
+ * Returns element that scrolls document
+ * @return {HTMLElement}
+ * @private
+ */
+function getDocumentScrollingElement() {
+  return document.scrollingElement || document.documentElement;
+}
+
+/***/ }),
+/* 59 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+exports.defaultOptions = undefined;
+
+var _Scrollable = __webpack_require__(58);
+
+var _Scrollable2 = _interopRequireDefault(_Scrollable);
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+exports.default = _Scrollable2.default;
+exports.defaultOptions = _Scrollable.defaultOptions;
+
+/***/ }),
+/* 60 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+exports.MirrorDestroyEvent = exports.MirrorMoveEvent = exports.MirrorAttachedEvent = exports.MirrorCreatedEvent = exports.MirrorCreateEvent = exports.MirrorEvent = undefined;
+
+var _AbstractEvent = __webpack_require__(1);
+
+var _AbstractEvent2 = _interopRequireDefault(_AbstractEvent);
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+/**
+ * Base mirror event
+ * @class MirrorEvent
+ * @module MirrorEvent
+ * @extends AbstractEvent
+ */
+class MirrorEvent extends _AbstractEvent2.default {
+  /**
+   * Draggables source element
+   * @property source
+   * @type {HTMLElement}
+   * @readonly
+   */
+  get source() {
+    return this.data.source;
+  }
+
+  /**
+   * Draggables original source element
+   * @property originalSource
+   * @type {HTMLElement}
+   * @readonly
+   */
+  get originalSource() {
+    return this.data.originalSource;
+  }
+
+  /**
+   * Draggables source container element
+   * @property sourceContainer
+   * @type {HTMLElement}
+   * @readonly
+   */
+  get sourceContainer() {
+    return this.data.sourceContainer;
+  }
+
+  /**
+   * Sensor event
+   * @property sensorEvent
+   * @type {SensorEvent}
+   * @readonly
+   */
+  get sensorEvent() {
+    return this.data.sensorEvent;
+  }
+
+  /**
+   * Drag event
+   * @property dragEvent
+   * @type {DragEvent}
+   * @readonly
+   */
+  get dragEvent() {
+    return this.data.dragEvent;
+  }
+
+  /**
+   * Original event that triggered sensor event
+   * @property originalEvent
+   * @type {Event}
+   * @readonly
+   */
+  get originalEvent() {
+    if (this.sensorEvent) {
+      return this.sensorEvent.originalEvent;
+    }
+
+    return null;
+  }
+}
+
+exports.MirrorEvent = MirrorEvent; /**
+                                    * Mirror create event
+                                    * @class MirrorCreateEvent
+                                    * @module MirrorCreateEvent
+                                    * @extends MirrorEvent
+                                    */
+
+class MirrorCreateEvent extends MirrorEvent {}
+
+exports.MirrorCreateEvent = MirrorCreateEvent; /**
+                                                * Mirror created event
+                                                * @class MirrorCreatedEvent
+                                                * @module MirrorCreatedEvent
+                                                * @extends MirrorEvent
+                                                */
+
+MirrorCreateEvent.type = 'mirror:create';
+class MirrorCreatedEvent extends MirrorEvent {
+
+  /**
+   * Draggables mirror element
+   * @property mirror
+   * @type {HTMLElement}
+   * @readonly
+   */
+  get mirror() {
+    return this.data.mirror;
+  }
+}
+
+exports.MirrorCreatedEvent = MirrorCreatedEvent; /**
+                                                  * Mirror attached event
+                                                  * @class MirrorAttachedEvent
+                                                  * @module MirrorAttachedEvent
+                                                  * @extends MirrorEvent
+                                                  */
+
+MirrorCreatedEvent.type = 'mirror:created';
+class MirrorAttachedEvent extends MirrorEvent {
+
+  /**
+   * Draggables mirror element
+   * @property mirror
+   * @type {HTMLElement}
+   * @readonly
+   */
+  get mirror() {
+    return this.data.mirror;
+  }
+}
+
+exports.MirrorAttachedEvent = MirrorAttachedEvent; /**
+                                                    * Mirror move event
+                                                    * @class MirrorMoveEvent
+                                                    * @module MirrorMoveEvent
+                                                    * @extends MirrorEvent
+                                                    */
+
+MirrorAttachedEvent.type = 'mirror:attached';
+class MirrorMoveEvent extends MirrorEvent {
+
+  /**
+   * Draggables mirror element
+   * @property mirror
+   * @type {HTMLElement}
+   * @readonly
+   */
+  get mirror() {
+    return this.data.mirror;
+  }
+
+  /**
+   * Sensor has exceeded mirror's threshold on x axis
+   * @type {Boolean}
+   * @readonly
+   */
+  get passedThreshX() {
+    return this.data.passedThreshX;
+  }
+
+  /**
+   * Sensor has exceeded mirror's threshold on y axis
+   * @type {Boolean}
+   * @readonly
+   */
+  get passedThreshY() {
+    return this.data.passedThreshY;
+  }
+}
+
+exports.MirrorMoveEvent = MirrorMoveEvent; /**
+                                            * Mirror destroy event
+                                            * @class MirrorDestroyEvent
+                                            * @module MirrorDestroyEvent
+                                            * @extends MirrorEvent
+                                            */
+
+MirrorMoveEvent.type = 'mirror:move';
+MirrorMoveEvent.cancelable = true;
+class MirrorDestroyEvent extends MirrorEvent {
+
+  /**
+   * Draggables mirror element
+   * @property mirror
+   * @type {HTMLElement}
+   * @readonly
+   */
+  get mirror() {
+    return this.data.mirror;
+  }
+}
+exports.MirrorDestroyEvent = MirrorDestroyEvent;
+MirrorDestroyEvent.type = 'mirror:destroy';
+MirrorDestroyEvent.cancelable = true;
+
+/***/ }),
+/* 61 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+
+var _MirrorEvent = __webpack_require__(60);
+
+Object.keys(_MirrorEvent).forEach(function (key) {
+  if (key === "default" || key === "__esModule") return;
+  Object.defineProperty(exports, key, {
+    enumerable: true,
+    get: function () {
+      return _MirrorEvent[key];
+    }
+  });
+});
+
+/***/ }),
+/* 62 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+exports.defaultOptions = exports.getAppendableContainer = exports.onScroll = exports.onMirrorMove = exports.onMirrorCreated = exports.onDragStop = exports.onDragMove = exports.onDragStart = undefined;
+
+var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
+
+var _AbstractPlugin = __webpack_require__(0);
+
+var _AbstractPlugin2 = _interopRequireDefault(_AbstractPlugin);
+
+var _MirrorEvent = __webpack_require__(61);
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+function _objectWithoutProperties(obj, keys) { var target = {}; for (var i in obj) { if (keys.indexOf(i) >= 0) continue; if (!Object.prototype.hasOwnProperty.call(obj, i)) continue; target[i] = obj[i]; } return target; }
+
+const onDragStart = exports.onDragStart = Symbol('onDragStart');
+const onDragMove = exports.onDragMove = Symbol('onDragMove');
+const onDragStop = exports.onDragStop = Symbol('onDragStop');
+const onMirrorCreated = exports.onMirrorCreated = Symbol('onMirrorCreated');
+const onMirrorMove = exports.onMirrorMove = Symbol('onMirrorMove');
+const onScroll = exports.onScroll = Symbol('onScroll');
+const getAppendableContainer = exports.getAppendableContainer = Symbol('getAppendableContainer');
+
+/**
+ * Mirror default options
+ * @property {Object} defaultOptions
+ * @property {Boolean} defaultOptions.constrainDimensions
+ * @property {Boolean} defaultOptions.xAxis
+ * @property {Boolean} defaultOptions.yAxis
+ * @property {null} defaultOptions.cursorOffsetX
+ * @property {null} defaultOptions.cursorOffsetY
+ * @type {Object}
+ */
+const defaultOptions = exports.defaultOptions = {
+  constrainDimensions: false,
+  xAxis: true,
+  yAxis: true,
+  cursorOffsetX: null,
+  cursorOffsetY: null,
+  thresholdX: null,
+  thresholdY: null
+};
+
+/**
+ * Mirror plugin which controls the mirror positioning while dragging
+ * @class Mirror
+ * @module Mirror
+ * @extends AbstractPlugin
+ */
+class Mirror extends _AbstractPlugin2.default {
+  /**
+   * Mirror constructor.
+   * @constructs Mirror
+   * @param {Draggable} draggable - Draggable instance
+   */
+  constructor(draggable) {
+    super(draggable);
+
+    /**
+     * Mirror options
+     * @property {Object} options
+     * @property {Boolean} options.constrainDimensions
+     * @property {Boolean} options.xAxis
+     * @property {Boolean} options.yAxis
+     * @property {Number|null} options.cursorOffsetX
+     * @property {Number|null} options.cursorOffsetY
+     * @property {String|HTMLElement|Function} options.appendTo
+     * @type {Object}
+     */
+    this.options = _extends({}, defaultOptions, this.getOptions());
+
+    /**
+     * Scroll offset for touch devices because the mirror is positioned fixed
+     * @property {Object} scrollOffset
+     * @property {Number} scrollOffset.x
+     * @property {Number} scrollOffset.y
+     */
+    this.scrollOffset = { x: 0, y: 0 };
+
+    /**
+     * Initial scroll offset for touch devices because the mirror is positioned fixed
+     * @property {Object} scrollOffset
+     * @property {Number} scrollOffset.x
+     * @property {Number} scrollOffset.y
+     */
+    this.initialScrollOffset = {
+      x: window.scrollX,
+      y: window.scrollY
+    };
+
+    this[onDragStart] = this[onDragStart].bind(this);
+    this[onDragMove] = this[onDragMove].bind(this);
+    this[onDragStop] = this[onDragStop].bind(this);
+    this[onMirrorCreated] = this[onMirrorCreated].bind(this);
+    this[onMirrorMove] = this[onMirrorMove].bind(this);
+    this[onScroll] = this[onScroll].bind(this);
+  }
+
+  /**
+   * Attaches plugins event listeners
+   */
+  attach() {
+    this.draggable.on('drag:start', this[onDragStart]).on('drag:move', this[onDragMove]).on('drag:stop', this[onDragStop]).on('mirror:created', this[onMirrorCreated]).on('mirror:move', this[onMirrorMove]);
+  }
+
+  /**
+   * Detaches plugins event listeners
+   */
+  detach() {
+    this.draggable.off('drag:start', this[onDragStart]).off('drag:move', this[onDragMove]).off('drag:stop', this[onDragStop]).off('mirror:created', this[onMirrorCreated]).off('mirror:move', this[onMirrorMove]);
+  }
+
+  /**
+   * Returns options passed through draggable
+   * @return {Object}
+   */
+  getOptions() {
+    return this.draggable.options.mirror || {};
+  }
+
+  [onDragStart](dragEvent) {
+    if (dragEvent.canceled()) {
+      return;
+    }
+
+    if ('ontouchstart' in window) {
+      document.addEventListener('scroll', this[onScroll], true);
+    }
+
+    this.initialScrollOffset = {
+      x: window.scrollX,
+      y: window.scrollY
+    };
+
+    const { source, originalSource, sourceContainer, sensorEvent } = dragEvent;
+
+    // Last sensor position of mirror move
+    this.lastMirrorMovedClient = {
+      x: sensorEvent.clientX,
+      y: sensorEvent.clientY
+    };
+
+    const mirrorCreateEvent = new _MirrorEvent.MirrorCreateEvent({
+      source,
+      originalSource,
+      sourceContainer,
+      sensorEvent,
+      dragEvent
+    });
+
+    this.draggable.trigger(mirrorCreateEvent);
+
+    if (isNativeDragEvent(sensorEvent) || mirrorCreateEvent.canceled()) {
+      return;
+    }
+
+    const appendableContainer = this[getAppendableContainer](source) || sourceContainer;
+    this.mirror = source.cloneNode(true);
+
+    const mirrorCreatedEvent = new _MirrorEvent.MirrorCreatedEvent({
+      source,
+      originalSource,
+      sourceContainer,
+      sensorEvent,
+      dragEvent,
+      mirror: this.mirror
+    });
+
+    const mirrorAttachedEvent = new _MirrorEvent.MirrorAttachedEvent({
+      source,
+      originalSource,
+      sourceContainer,
+      sensorEvent,
+      dragEvent,
+      mirror: this.mirror
+    });
+
+    this.draggable.trigger(mirrorCreatedEvent);
+    appendableContainer.appendChild(this.mirror);
+    this.draggable.trigger(mirrorAttachedEvent);
+  }
+
+  [onDragMove](dragEvent) {
+    if (!this.mirror || dragEvent.canceled()) {
+      return;
+    }
+
+    const { source, originalSource, sourceContainer, sensorEvent } = dragEvent;
+
+    let passedThreshX = true;
+    let passedThreshY = true;
+
+    if (this.options.thresholdX || this.options.thresholdY) {
+      const { x: lastX, y: lastY } = this.lastMirrorMovedClient;
+
+      if (Math.abs(lastX - sensorEvent.clientX) < this.options.thresholdX) {
+        passedThreshX = false;
+      } else {
+        this.lastMirrorMovedClient.x = sensorEvent.clientX;
+      }
+
+      if (Math.abs(lastY - sensorEvent.clientY) < this.options.thresholdY) {
+        passedThreshY = false;
+      } else {
+        this.lastMirrorMovedClient.y = sensorEvent.clientY;
+      }
+
+      if (!passedThreshX && !passedThreshY) {
+        return;
+      }
+    }
+
+    const mirrorMoveEvent = new _MirrorEvent.MirrorMoveEvent({
+      source,
+      originalSource,
+      sourceContainer,
+      sensorEvent,
+      dragEvent,
+      mirror: this.mirror,
+      passedThreshX,
+      passedThreshY
+    });
+
+    this.draggable.trigger(mirrorMoveEvent);
+  }
+
+  [onDragStop](dragEvent) {
+    if ('ontouchstart' in window) {
+      document.removeEventListener('scroll', this[onScroll], true);
+    }
+
+    this.initialScrollOffset = { x: 0, y: 0 };
+    this.scrollOffset = { x: 0, y: 0 };
+
+    if (!this.mirror) {
+      return;
+    }
+
+    const { source, sourceContainer, sensorEvent } = dragEvent;
+
+    const mirrorDestroyEvent = new _MirrorEvent.MirrorDestroyEvent({
+      source,
+      mirror: this.mirror,
+      sourceContainer,
+      sensorEvent,
+      dragEvent
+    });
+
+    this.draggable.trigger(mirrorDestroyEvent);
+
+    if (!mirrorDestroyEvent.canceled()) {
+      this.mirror.parentNode.removeChild(this.mirror);
+    }
+  }
+
+  [onScroll]() {
+    this.scrollOffset = {
+      x: window.scrollX - this.initialScrollOffset.x,
+      y: window.scrollY - this.initialScrollOffset.y
+    };
+  }
+
+  /**
+   * Mirror created handler
+   * @param {MirrorCreatedEvent} mirrorEvent
+   * @return {Promise}
+   * @private
+   */
+  [onMirrorCreated]({ mirror, source, sensorEvent }) {
+    const mirrorClass = this.draggable.getClassNameFor('mirror');
+
+    const setState = (_ref) => {
+      let { mirrorOffset, initialX, initialY } = _ref,
+          args = _objectWithoutProperties(_ref, ['mirrorOffset', 'initialX', 'initialY']);
+
+      this.mirrorOffset = mirrorOffset;
+      this.initialX = initialX;
+      this.initialY = initialY;
+      this.lastMovedX = initialX;
+      this.lastMovedY = initialY;
+      return _extends({ mirrorOffset, initialX, initialY }, args);
+    };
+
+    mirror.style.display = 'none';
+
+    const initialState = {
+      mirror,
+      source,
+      sensorEvent,
+      mirrorClass,
+      scrollOffset: this.scrollOffset,
+      options: this.options,
+      passedThreshX: true,
+      passedThreshY: true
+    };
+
+    return Promise.resolve(initialState)
+    // Fix reflow here
+    .then(computeMirrorDimensions).then(calculateMirrorOffset).then(resetMirror).then(addMirrorClasses).then(positionMirror({ initial: true })).then(removeMirrorID).then(setState);
+  }
+
+  /**
+   * Mirror move handler
+   * @param {MirrorMoveEvent} mirrorEvent
+   * @return {Promise|null}
+   * @private
+   */
+  [onMirrorMove](mirrorEvent) {
+    if (mirrorEvent.canceled()) {
+      return null;
+    }
+
+    const setState = (_ref2) => {
+      let { lastMovedX, lastMovedY } = _ref2,
+          args = _objectWithoutProperties(_ref2, ['lastMovedX', 'lastMovedY']);
+
+      this.lastMovedX = lastMovedX;
+      this.lastMovedY = lastMovedY;
+
+      return _extends({ lastMovedX, lastMovedY }, args);
+    };
+
+    const initialState = {
+      mirror: mirrorEvent.mirror,
+      sensorEvent: mirrorEvent.sensorEvent,
+      mirrorOffset: this.mirrorOffset,
+      options: this.options,
+      initialX: this.initialX,
+      initialY: this.initialY,
+      scrollOffset: this.scrollOffset,
+      passedThreshX: mirrorEvent.passedThreshX,
+      passedThreshY: mirrorEvent.passedThreshY,
+      lastMovedX: this.lastMovedX,
+      lastMovedY: this.lastMovedY
+    };
+
+    return Promise.resolve(initialState).then(positionMirror({ raf: true })).then(setState);
+  }
+
+  /**
+   * Returns appendable container for mirror based on the appendTo option
+   * @private
+   * @param {Object} options
+   * @param {HTMLElement} options.source - Current source
+   * @return {HTMLElement}
+   */
+  [getAppendableContainer](source) {
+    const appendTo = this.options.appendTo;
+
+    if (typeof appendTo === 'string') {
+      return document.querySelector(appendTo);
+    } else if (appendTo instanceof HTMLElement) {
+      return appendTo;
+    } else if (typeof appendTo === 'function') {
+      return appendTo(source);
+    } else {
+      return source.parentNode;
+    }
+  }
+}
+
+exports.default = Mirror; /**
+                           * Computes mirror dimensions based on the source element
+                           * Adds sourceRect to state
+                           * @param {Object} state
+                           * @param {HTMLElement} state.source
+                           * @return {Promise}
+                           * @private
+                           */
+
+function computeMirrorDimensions(_ref3) {
+  let { source } = _ref3,
+      args = _objectWithoutProperties(_ref3, ['source']);
+
+  return withPromise(resolve => {
+    const sourceRect = source.getBoundingClientRect();
+    resolve(_extends({ source, sourceRect }, args));
+  });
+}
+
+/**
+ * Calculates mirror offset
+ * Adds mirrorOffset to state
+ * @param {Object} state
+ * @param {SensorEvent} state.sensorEvent
+ * @param {DOMRect} state.sourceRect
+ * @return {Promise}
+ * @private
+ */
+function calculateMirrorOffset(_ref4) {
+  let { sensorEvent, sourceRect, options } = _ref4,
+      args = _objectWithoutProperties(_ref4, ['sensorEvent', 'sourceRect', 'options']);
+
+  return withPromise(resolve => {
+    const top = options.cursorOffsetY === null ? sensorEvent.clientY - sourceRect.top : options.cursorOffsetY;
+    const left = options.cursorOffsetX === null ? sensorEvent.clientX - sourceRect.left : options.cursorOffsetX;
+
+    const mirrorOffset = { top, left };
+
+    resolve(_extends({ sensorEvent, sourceRect, mirrorOffset, options }, args));
+  });
+}
+
+/**
+ * Applys mirror styles
+ * @param {Object} state
+ * @param {HTMLElement} state.mirror
+ * @param {HTMLElement} state.source
+ * @param {Object} state.options
+ * @return {Promise}
+ * @private
+ */
+function resetMirror(_ref5) {
+  let { mirror, source, options } = _ref5,
+      args = _objectWithoutProperties(_ref5, ['mirror', 'source', 'options']);
+
+  return withPromise(resolve => {
+    let offsetHeight;
+    let offsetWidth;
+
+    if (options.constrainDimensions) {
+      const computedSourceStyles = getComputedStyle(source);
+      offsetHeight = computedSourceStyles.getPropertyValue('height');
+      offsetWidth = computedSourceStyles.getPropertyValue('width');
+    }
+
+    mirror.style.display = null;
+    mirror.style.position = 'fixed';
+    mirror.style.pointerEvents = 'none';
+    mirror.style.top = 0;
+    mirror.style.left = 0;
+    mirror.style.margin = 0;
+
+    if (options.constrainDimensions) {
+      mirror.style.height = offsetHeight;
+      mirror.style.width = offsetWidth;
+    }
+
+    resolve(_extends({ mirror, source, options }, args));
+  });
+}
+
+/**
+ * Applys mirror class on mirror element
+ * @param {Object} state
+ * @param {HTMLElement} state.mirror
+ * @param {String} state.mirrorClass
+ * @return {Promise}
+ * @private
+ */
+function addMirrorClasses(_ref6) {
+  let { mirror, mirrorClass } = _ref6,
+      args = _objectWithoutProperties(_ref6, ['mirror', 'mirrorClass']);
+
+  return withPromise(resolve => {
+    mirror.classList.add(mirrorClass);
+    resolve(_extends({ mirror, mirrorClass }, args));
+  });
+}
+
+/**
+ * Removes source ID from cloned mirror element
+ * @param {Object} state
+ * @param {HTMLElement} state.mirror
+ * @return {Promise}
+ * @private
+ */
+function removeMirrorID(_ref7) {
+  let { mirror } = _ref7,
+      args = _objectWithoutProperties(_ref7, ['mirror']);
+
+  return withPromise(resolve => {
+    mirror.removeAttribute('id');
+    delete mirror.id;
+    resolve(_extends({ mirror }, args));
+  });
+}
+
+/**
+ * Positions mirror with translate3d
+ * @param {Object} state
+ * @param {HTMLElement} state.mirror
+ * @param {SensorEvent} state.sensorEvent
+ * @param {Object} state.mirrorOffset
+ * @param {Number} state.initialY
+ * @param {Number} state.initialX
+ * @param {Object} state.options
+ * @return {Promise}
+ * @private
+ */
+function positionMirror({ withFrame = false, initial = false } = {}) {
+  return (_ref8) => {
+    let {
+      mirror,
+      sensorEvent,
+      mirrorOffset,
+      initialY,
+      initialX,
+      scrollOffset,
+      options,
+      passedThreshX,
+      passedThreshY,
+      lastMovedX,
+      lastMovedY
+    } = _ref8,
+        args = _objectWithoutProperties(_ref8, ['mirror', 'sensorEvent', 'mirrorOffset', 'initialY', 'initialX', 'scrollOffset', 'options', 'passedThreshX', 'passedThreshY', 'lastMovedX', 'lastMovedY']);
+
+    return withPromise(resolve => {
+      const result = _extends({
+        mirror,
+        sensorEvent,
+        mirrorOffset,
+        options
+      }, args);
+
+      if (mirrorOffset) {
+        const x = passedThreshX ? Math.round((sensorEvent.clientX - mirrorOffset.left - scrollOffset.x) / (options.thresholdX || 1)) * (options.thresholdX || 1) : Math.round(lastMovedX);
+        const y = passedThreshY ? Math.round((sensorEvent.clientY - mirrorOffset.top - scrollOffset.y) / (options.thresholdY || 1)) * (options.thresholdY || 1) : Math.round(lastMovedY);
+
+        if (options.xAxis && options.yAxis || initial) {
+          mirror.style.transform = `translate3d(${x}px, ${y}px, 0)`;
+        } else if (options.xAxis && !options.yAxis) {
+          mirror.style.transform = `translate3d(${x}px, ${initialY}px, 0)`;
+        } else if (options.yAxis && !options.xAxis) {
+          mirror.style.transform = `translate3d(${initialX}px, ${y}px, 0)`;
+        }
+
+        if (initial) {
+          result.initialX = x;
+          result.initialY = y;
+        }
+
+        result.lastMovedX = x;
+        result.lastMovedY = y;
+      }
+
+      resolve(result);
+    }, { frame: withFrame });
+  };
+}
+
+/**
+ * Wraps functions in promise with potential animation frame option
+ * @param {Function} callback
+ * @param {Object} options
+ * @param {Boolean} options.raf
+ * @return {Promise}
+ * @private
+ */
+function withPromise(callback, { raf = false } = {}) {
+  return new Promise((resolve, reject) => {
+    if (raf) {
+      requestAnimationFrame(() => {
+        callback(resolve, reject);
+      });
+    } else {
+      callback(resolve, reject);
+    }
+  });
+}
+
+/**
+ * Returns true if the sensor event was triggered by a native browser drag event
+ * @param {SensorEvent} sensorEvent
+ */
+function isNativeDragEvent(sensorEvent) {
+  return (/^drag/.test(sensorEvent.originalEvent.type)
+  );
+}
+
+/***/ }),
+/* 63 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+exports.defaultOptions = undefined;
+
+var _Mirror = __webpack_require__(62);
+
+var _Mirror2 = _interopRequireDefault(_Mirror);
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+exports.default = _Mirror2.default;
+exports.defaultOptions = _Mirror.defaultOptions;
+
+/***/ }),
+/* 64 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+
+var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
+
+var _AbstractPlugin = __webpack_require__(0);
+
+var _AbstractPlugin2 = _interopRequireDefault(_AbstractPlugin);
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+const onInitialize = Symbol('onInitialize');
+const onDestroy = Symbol('onDestroy');
+
+/**
+ * Focusable default options
+ * @property {Object} defaultOptions
+ * @type {Object}
+ */
+const defaultOptions = {};
+
+/**
+ * Focusable plugin
+ * @class Focusable
+ * @module Focusable
+ * @extends AbstractPlugin
+ */
+class Focusable extends _AbstractPlugin2.default {
+  /**
+   * Focusable constructor.
+   * @constructs Focusable
+   * @param {Draggable} draggable - Draggable instance
+   */
+  constructor(draggable) {
+    super(draggable);
+
+    /**
+     * Focusable options
+     * @property {Object} options
+     * @type {Object}
+     */
+    this.options = _extends({}, defaultOptions, this.getOptions());
+
+    this[onInitialize] = this[onInitialize].bind(this);
+    this[onDestroy] = this[onDestroy].bind(this);
+  }
+
+  /**
+   * Attaches listeners to draggable
+   */
+  attach() {
+    this.draggable.on('draggable:initialize', this[onInitialize]).on('draggable:destroy', this[onDestroy]);
+  }
+
+  /**
+   * Detaches listeners from draggable
+   */
+  detach() {
+    this.draggable.off('draggable:initialize', this[onInitialize]).off('draggable:destroy', this[onDestroy]);
+
+    // Remove modified elements when detach
+    this[onDestroy]();
+  }
+
+  /**
+   * Returns options passed through draggable
+   * @return {Object}
+   */
+  getOptions() {
+    return this.draggable.options.focusable || {};
+  }
+
+  /**
+   * Returns draggable containers and elements
+   * @return {HTMLElement[]}
+   */
+  getElements() {
+    return [...this.draggable.containers, ...this.draggable.getDraggableElements()];
+  }
+
+  /**
+   * Intialize handler
+   * @private
+   */
+  [onInitialize]() {
+    // Can wait until the next best frame is available
+    requestAnimationFrame(() => {
+      this.getElements().forEach(element => decorateElement(element));
+    });
+  }
+
+  /**
+   * Destroy handler
+   * @private
+   */
+  [onDestroy]() {
+    // Can wait until the next best frame is available
+    requestAnimationFrame(() => {
+      this.getElements().forEach(element => stripElement(element));
+    });
+  }
+}
+
+exports.default = Focusable; /**
+                              * Keeps track of all the elements that are missing tabindex attributes
+                              * so they can be reset when draggable gets destroyed
+                              * @const {HTMLElement[]} elementsWithMissingTabIndex
+                              */
+
+const elementsWithMissingTabIndex = [];
+
+/**
+ * Decorates element with tabindex attributes
+ * @param {HTMLElement} element
+ * @return {Object}
+ * @private
+ */
+function decorateElement(element) {
+  const hasMissingTabIndex = Boolean(!element.getAttribute('tabindex') && element.tabIndex === -1);
+
+  if (hasMissingTabIndex) {
+    elementsWithMissingTabIndex.push(element);
+    element.tabIndex = 0;
+  }
+}
+
+/**
+ * Removes elements tabindex attributes
+ * @param {HTMLElement} element
+ * @private
+ */
+function stripElement(element) {
+  const tabIndexElementPosition = elementsWithMissingTabIndex.indexOf(element);
+
+  if (tabIndexElementPosition !== -1) {
+    element.tabIndex = -1;
+    elementsWithMissingTabIndex.splice(tabIndexElementPosition, 1);
+  }
+}
+
+/***/ }),
+/* 65 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+
+var _Focusable = __webpack_require__(64);
+
+var _Focusable2 = _interopRequireDefault(_Focusable);
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+exports.default = _Focusable2.default;
+
+/***/ }),
+/* 66 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+/**
+ * All draggable plugins inherit from this class.
+ * @abstract
+ * @class AbstractPlugin
+ * @module AbstractPlugin
+ */
+class AbstractPlugin {
+  /**
+   * AbstractPlugin constructor.
+   * @constructs AbstractPlugin
+   * @param {Draggable} draggable - Draggable instance
+   */
+  constructor(draggable) {
+    /**
+     * Draggable instance
+     * @property draggable
+     * @type {Draggable}
+     */
+    this.draggable = draggable;
+  }
+
+  /**
+   * Override to add listeners
+   * @abstract
+   */
+  attach() {
+    throw new Error('Not Implemented');
+  }
+
+  /**
+   * Override to remove listeners
+   * @abstract
+   */
+  detach() {
+    throw new Error('Not Implemented');
+  }
+}
+exports.default = AbstractPlugin;
+
+/***/ }),
+/* 67 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+exports.defaultOptions = undefined;
+
+var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
+
+var _AbstractPlugin = __webpack_require__(0);
+
+var _AbstractPlugin2 = _interopRequireDefault(_AbstractPlugin);
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+const onInitialize = Symbol('onInitialize');
+const onDestroy = Symbol('onDestroy');
+const announceEvent = Symbol('announceEvent');
+const announceMessage = Symbol('announceMessage');
+
+const ARIA_RELEVANT = 'aria-relevant';
+const ARIA_ATOMIC = 'aria-atomic';
+const ARIA_LIVE = 'aria-live';
+const ROLE = 'role';
+
+/**
+ * Announcement default options
+ * @property {Object} defaultOptions
+ * @property {Number} defaultOptions.expire
+ * @type {Object}
+ */
+const defaultOptions = exports.defaultOptions = {
+  expire: 7000
+};
+
+/**
+ * Announcement plugin
+ * @class Announcement
+ * @module Announcement
+ * @extends AbstractPlugin
+ */
+class Announcement extends _AbstractPlugin2.default {
+  /**
+   * Announcement constructor.
+   * @constructs Announcement
+   * @param {Draggable} draggable - Draggable instance
+   */
+  constructor(draggable) {
+    super(draggable);
+
+    /**
+     * Plugin options
+     * @property options
+     * @type {Object}
+     */
+    this.options = _extends({}, defaultOptions, this.getOptions());
+
+    /**
+     * Original draggable trigger method. Hack until we have onAll or on('all')
+     * @property originalTriggerMethod
+     * @type {Function}
+     */
+    this.originalTriggerMethod = this.draggable.trigger;
+
+    this[onInitialize] = this[onInitialize].bind(this);
+    this[onDestroy] = this[onDestroy].bind(this);
+  }
+
+  /**
+   * Attaches listeners to draggable
+   */
+  attach() {
+    this.draggable.on('draggable:initialize', this[onInitialize]);
+  }
+
+  /**
+   * Detaches listeners from draggable
+   */
+  detach() {
+    this.draggable.off('draggable:destroy', this[onDestroy]);
+  }
+
+  /**
+   * Returns passed in options
+   */
+  getOptions() {
+    return this.draggable.options.announcements || {};
+  }
+
+  /**
+   * Announces event
+   * @private
+   * @param {AbstractEvent} event
+   */
+  [announceEvent](event) {
+    const message = this.options[event.type];
+
+    if (message && typeof message === 'string') {
+      this[announceMessage](message);
+    }
+
+    if (message && typeof message === 'function') {
+      this[announceMessage](message(event));
+    }
+  }
+
+  /**
+   * Announces message to screen reader
+   * @private
+   * @param {String} message
+   */
+  [announceMessage](message) {
+    announce(message, { expire: this.options.expire });
+  }
+
+  /**
+   * Initialize hander
+   * @private
+   */
+  [onInitialize]() {
+    // Hack until there is an api for listening for all events
+    this.draggable.trigger = event => {
+      try {
+        this[announceEvent](event);
+      } finally {
+        // Ensure that original trigger is called
+        this.originalTriggerMethod.call(this.draggable, event);
+      }
+    };
+  }
+
+  /**
+   * Destroy hander
+   * @private
+   */
+  [onDestroy]() {
+    this.draggable.trigger = this.originalTriggerMethod;
+  }
+}
+
+exports.default = Announcement; /**
+                                 * @const {HTMLElement} liveRegion
+                                 */
+
+const liveRegion = createRegion();
+
+/**
+ * Announces message via live region
+ * @param {String} message
+ * @param {Object} options
+ * @param {Number} options.expire
+ */
+function announce(message, { expire }) {
+  const element = document.createElement('div');
+
+  element.textContent = message;
+  liveRegion.appendChild(element);
+
+  return setTimeout(() => {
+    liveRegion.removeChild(element);
+  }, expire);
+}
+
+/**
+ * Creates region element
+ * @return {HTMLElement}
+ */
+function createRegion() {
+  const element = document.createElement('div');
+
+  element.setAttribute('id', 'draggable-live-region');
+  element.setAttribute(ARIA_RELEVANT, 'additions');
+  element.setAttribute(ARIA_ATOMIC, 'true');
+  element.setAttribute(ARIA_LIVE, 'assertive');
+  element.setAttribute(ROLE, 'log');
+
+  element.style.position = 'fixed';
+  element.style.width = '1px';
+  element.style.height = '1px';
+  element.style.top = '-1px';
+  element.style.overflow = 'hidden';
+
+  return element;
+}
+
+// Append live region element as early as possible
+document.addEventListener('DOMContentLoaded', () => {
+  document.body.appendChild(liveRegion);
+});
+
+/***/ }),
+/* 68 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+exports.defaultOptions = undefined;
+
+var _Announcement = __webpack_require__(67);
+
+var _Announcement2 = _interopRequireDefault(_Announcement);
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+exports.default = _Announcement2.default;
+exports.defaultOptions = _Announcement.defaultOptions;
+
+/***/ }),
+/* 69 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+exports.DraggableDestroyEvent = exports.DraggableInitializedEvent = exports.DraggableEvent = undefined;
+
+var _AbstractEvent = __webpack_require__(1);
+
+var _AbstractEvent2 = _interopRequireDefault(_AbstractEvent);
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+/**
+ * Base draggable event
+ * @class DraggableEvent
+ * @module DraggableEvent
+ * @extends AbstractEvent
+ */
+class DraggableEvent extends _AbstractEvent2.default {
+
+  /**
+   * Draggable instance
+   * @property draggable
+   * @type {Draggable}
+   * @readonly
+   */
+  get draggable() {
+    return this.data.draggable;
+  }
+}
+
+exports.DraggableEvent = DraggableEvent; /**
+                                          * Draggable initialized event
+                                          * @class DraggableInitializedEvent
+                                          * @module DraggableInitializedEvent
+                                          * @extends DraggableEvent
+                                          */
+
+DraggableEvent.type = 'draggable';
+class DraggableInitializedEvent extends DraggableEvent {}
+
+exports.DraggableInitializedEvent = DraggableInitializedEvent; /**
+                                                                * Draggable destory event
+                                                                * @class DraggableInitializedEvent
+                                                                * @module DraggableDestroyEvent
+                                                                * @extends DraggableDestroyEvent
+                                                                */
+
+DraggableInitializedEvent.type = 'draggable:initialize';
+class DraggableDestroyEvent extends DraggableEvent {}
+exports.DraggableDestroyEvent = DraggableDestroyEvent;
+DraggableDestroyEvent.type = 'draggable:destroy';
+
+/***/ }),
+/* 70 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+
+var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
+
+const canceled = Symbol('canceled');
+
+/**
+ * All events fired by draggable inherit this class. You can call `cancel()` to
+ * cancel a specific event or you can check if an event has been canceled by
+ * calling `canceled()`.
+ * @abstract
+ * @class AbstractEvent
+ * @module AbstractEvent
+ */
+class AbstractEvent {
+
+  /**
+   * AbstractEvent constructor.
+   * @constructs AbstractEvent
+   * @param {object} data - Event data
+   */
+
+  /**
+   * Event type
+   * @static
+   * @abstract
+   * @property type
+   * @type {String}
+   */
+  constructor(data) {
+    this[canceled] = false;
+    this.data = data;
+  }
+
+  /**
+   * Read-only type
+   * @abstract
+   * @return {String}
+   */
+
+
+  /**
+   * Event cancelable
+   * @static
+   * @abstract
+   * @property cancelable
+   * @type {Boolean}
+   */
+  get type() {
+    return this.constructor.type;
+  }
+
+  /**
+   * Read-only cancelable
+   * @abstract
+   * @return {Boolean}
+   */
+  get cancelable() {
+    return this.constructor.cancelable;
+  }
+
+  /**
+   * Cancels the event instance
+   * @abstract
+   */
+  cancel() {
+    this[canceled] = true;
+  }
+
+  /**
+   * Check if event has been canceled
+   * @abstract
+   * @return {Boolean}
+   */
+  canceled() {
+    return Boolean(this[canceled]);
+  }
+
+  /**
+   * Returns new event instance with existing event data.
+   * This method allows for overriding of event data.
+   * @param {Object} data
+   * @return {AbstractEvent}
+   */
+  clone(data) {
+    return new this.constructor(_extends({}, this.data, data));
+  }
+}
+exports.default = AbstractEvent;
+AbstractEvent.type = 'event';
+AbstractEvent.cancelable = false;
+
+/***/ }),
+/* 71 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+exports.DragStopEvent = exports.DragPressureEvent = exports.DragOutContainerEvent = exports.DragOverContainerEvent = exports.DragOutEvent = exports.DragOverEvent = exports.DragMoveEvent = exports.DragStartEvent = exports.DragEvent = undefined;
+
+var _AbstractEvent = __webpack_require__(1);
+
+var _AbstractEvent2 = _interopRequireDefault(_AbstractEvent);
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+/**
+ * Base drag event
+ * @class DragEvent
+ * @module DragEvent
+ * @extends AbstractEvent
+ */
+class DragEvent extends _AbstractEvent2.default {
+
+  /**
+   * Draggables source element
+   * @property source
+   * @type {HTMLElement}
+   * @readonly
+   */
+  get source() {
+    return this.data.source;
+  }
+
+  /**
+   * Draggables original source element
+   * @property originalSource
+   * @type {HTMLElement}
+   * @readonly
+   */
+  get originalSource() {
+    return this.data.originalSource;
+  }
+
+  /**
+   * Draggables mirror element
+   * @property mirror
+   * @type {HTMLElement}
+   * @readonly
+   */
+  get mirror() {
+    return this.data.mirror;
+  }
+
+  /**
+   * Draggables source container element
+   * @property sourceContainer
+   * @type {HTMLElement}
+   * @readonly
+   */
+  get sourceContainer() {
+    return this.data.sourceContainer;
+  }
+
+  /**
+   * Sensor event
+   * @property sensorEvent
+   * @type {SensorEvent}
+   * @readonly
+   */
+  get sensorEvent() {
+    return this.data.sensorEvent;
+  }
+
+  /**
+   * Original event that triggered sensor event
+   * @property originalEvent
+   * @type {Event}
+   * @readonly
+   */
+  get originalEvent() {
+    if (this.sensorEvent) {
+      return this.sensorEvent.originalEvent;
+    }
+
+    return null;
+  }
+}
+
+exports.DragEvent = DragEvent; /**
+                                * Drag start event
+                                * @class DragStartEvent
+                                * @module DragStartEvent
+                                * @extends DragEvent
+                                */
+
+DragEvent.type = 'drag';
+class DragStartEvent extends DragEvent {}
+
+exports.DragStartEvent = DragStartEvent; /**
+                                          * Drag move event
+                                          * @class DragMoveEvent
+                                          * @module DragMoveEvent
+                                          * @extends DragEvent
+                                          */
+
+DragStartEvent.type = 'drag:start';
+DragStartEvent.cancelable = true;
+class DragMoveEvent extends DragEvent {}
+
+exports.DragMoveEvent = DragMoveEvent; /**
+                                        * Drag over event
+                                        * @class DragOverEvent
+                                        * @module DragOverEvent
+                                        * @extends DragEvent
+                                        */
+
+DragMoveEvent.type = 'drag:move';
+class DragOverEvent extends DragEvent {
+
+  /**
+   * Draggable container you are over
+   * @property overContainer
+   * @type {HTMLElement}
+   * @readonly
+   */
+  get overContainer() {
+    return this.data.overContainer;
+  }
+
+  /**
+   * Draggable element you are over
+   * @property over
+   * @type {HTMLElement}
+   * @readonly
+   */
+  get over() {
+    return this.data.over;
+  }
+}
+
+exports.DragOverEvent = DragOverEvent; /**
+                                        * Drag out event
+                                        * @class DragOutEvent
+                                        * @module DragOutEvent
+                                        * @extends DragEvent
+                                        */
+
+DragOverEvent.type = 'drag:over';
+DragOverEvent.cancelable = true;
+class DragOutEvent extends DragEvent {
+
+  /**
+   * Draggable container you are over
+   * @property overContainer
+   * @type {HTMLElement}
+   * @readonly
+   */
+  get overContainer() {
+    return this.data.overContainer;
+  }
+
+  /**
+   * Draggable element you left
+   * @property over
+   * @type {HTMLElement}
+   * @readonly
+   */
+  get over() {
+    return this.data.over;
+  }
+}
+
+exports.DragOutEvent = DragOutEvent; /**
+                                      * Drag over container event
+                                      * @class DragOverContainerEvent
+                                      * @module DragOverContainerEvent
+                                      * @extends DragEvent
+                                      */
+
+DragOutEvent.type = 'drag:out';
+class DragOverContainerEvent extends DragEvent {
+
+  /**
+   * Draggable container you are over
+   * @property overContainer
+   * @type {HTMLElement}
+   * @readonly
+   */
+  get overContainer() {
+    return this.data.overContainer;
+  }
+}
+
+exports.DragOverContainerEvent = DragOverContainerEvent; /**
+                                                          * Drag out container event
+                                                          * @class DragOutContainerEvent
+                                                          * @module DragOutContainerEvent
+                                                          * @extends DragEvent
+                                                          */
+
+DragOverContainerEvent.type = 'drag:over:container';
+class DragOutContainerEvent extends DragEvent {
+
+  /**
+   * Draggable container you left
+   * @property overContainer
+   * @type {HTMLElement}
+   * @readonly
+   */
+  get overContainer() {
+    return this.data.overContainer;
+  }
+}
+
+exports.DragOutContainerEvent = DragOutContainerEvent; /**
+                                                        * Drag pressure event
+                                                        * @class DragPressureEvent
+                                                        * @module DragPressureEvent
+                                                        * @extends DragEvent
+                                                        */
+
+DragOutContainerEvent.type = 'drag:out:container';
+class DragPressureEvent extends DragEvent {
+
+  /**
+   * Pressure applied on draggable element
+   * @property pressure
+   * @type {Number}
+   * @readonly
+   */
+  get pressure() {
+    return this.data.pressure;
+  }
+}
+
+exports.DragPressureEvent = DragPressureEvent; /**
+                                                * Drag stop event
+                                                * @class DragStopEvent
+                                                * @module DragStopEvent
+                                                * @extends DragEvent
+                                                */
+
+DragPressureEvent.type = 'drag:pressure';
+class DragStopEvent extends DragEvent {}
+exports.DragStopEvent = DragStopEvent;
+DragStopEvent.type = 'drag:stop';
+
+/***/ }),
+/* 72 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+exports.Plugins = exports.Sensors = exports.Sortable = exports.Swappable = exports.Droppable = exports.Draggable = exports.BasePlugin = exports.BaseEvent = undefined;
+
+var _Draggable = __webpack_require__(5);
+
+Object.defineProperty(exports, 'Draggable', {
+  enumerable: true,
+  get: function () {
+    return _interopRequireDefault(_Draggable).default;
+  }
+});
+
+var _Droppable = __webpack_require__(36);
+
+Object.defineProperty(exports, 'Droppable', {
+  enumerable: true,
+  get: function () {
+    return _interopRequireDefault(_Droppable).default;
+  }
+});
+
+var _Swappable = __webpack_require__(33);
+
+Object.defineProperty(exports, 'Swappable', {
+  enumerable: true,
+  get: function () {
+    return _interopRequireDefault(_Swappable).default;
+  }
+});
+
+var _Sortable = __webpack_require__(30);
+
+Object.defineProperty(exports, 'Sortable', {
+  enumerable: true,
+  get: function () {
+    return _interopRequireDefault(_Sortable).default;
+  }
+});
+
+var _AbstractEvent = __webpack_require__(1);
+
+var _AbstractEvent2 = _interopRequireDefault(_AbstractEvent);
+
+var _AbstractPlugin = __webpack_require__(0);
+
+var _AbstractPlugin2 = _interopRequireDefault(_AbstractPlugin);
+
+var _Sensors = __webpack_require__(6);
+
+var Sensors = _interopRequireWildcard(_Sensors);
+
+var _Plugins = __webpack_require__(27);
+
+var Plugins = _interopRequireWildcard(_Plugins);
+
+function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } }
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+exports.BaseEvent = _AbstractEvent2.default;
+exports.BasePlugin = _AbstractPlugin2.default;
+exports.Sensors = Sensors;
+exports.Plugins = Plugins;
+
+/***/ })
+/******/ ]);
+});

文件差异内容过多而无法显示
+ 1 - 0
Moonlight/wwwroot/assets/js/jquery.min.js


文件差异内容过多而无法显示
+ 4 - 0
Moonlight/wwwroot/assets/js/popper.min.js


文件差异内容过多而无法显示
+ 0 - 0
Moonlight/wwwroot/assets/js/toastr.min.js


+ 8 - 0
Moonlight/wwwroot/assets/js/xterm-addon-fit.min.js

@@ -0,0 +1,8 @@
+/**
+ * Skipped minification because the original files appears to be already minified.
+ * Original file: /npm/xterm-addon-fit@0.7.0/lib/xterm-addon-fit.js
+ *
+ * Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files
+ */
+!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.FitAddon=t():e.FitAddon=t()}(self,(function(){return(()=>{"use strict";var e={};return(()=>{var t=e;Object.defineProperty(t,"__esModule",{value:!0}),t.FitAddon=void 0,t.FitAddon=class{constructor(){}activate(e){this._terminal=e}dispose(){}fit(){const e=this.proposeDimensions();if(!e||!this._terminal||isNaN(e.cols)||isNaN(e.rows))return;const t=this._terminal._core;this._terminal.rows===e.rows&&this._terminal.cols===e.cols||(t._renderService.clear(),this._terminal.resize(e.cols,e.rows))}proposeDimensions(){if(!this._terminal)return;if(!this._terminal.element||!this._terminal.element.parentElement)return;const e=this._terminal._core,t=e._renderService.dimensions;if(0===t.css.cell.width||0===t.css.cell.height)return;const r=0===this._terminal.options.scrollback?0:e.viewport.scrollBarWidth,i=window.getComputedStyle(this._terminal.element.parentElement),o=parseInt(i.getPropertyValue("height")),s=Math.max(0,parseInt(i.getPropertyValue("width"))),n=window.getComputedStyle(this._terminal.element),l=o-(parseInt(n.getPropertyValue("padding-top"))+parseInt(n.getPropertyValue("padding-bottom"))),a=s-(parseInt(n.getPropertyValue("padding-right"))+parseInt(n.getPropertyValue("padding-left")))-r;return{cols:Math.max(2,Math.floor(a/t.css.cell.width)),rows:Math.max(1,Math.floor(l/t.css.cell.height))}}}})(),e})()}));
+//# sourceMappingURL=xterm-addon-fit.js.map

部分文件因为文件数量过多而无法显示