diff --git a/Assets/External/websocket-sharp.meta b/Assets/External/websocket-sharp.meta new file mode 100644 index 00000000..f9fed47b --- /dev/null +++ b/Assets/External/websocket-sharp.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 0b3c479ab08f10f42aaa9bcbad102a3a +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/External/websocket-sharp/AssemblyInfo.cs b/Assets/External/websocket-sharp/AssemblyInfo.cs new file mode 100644 index 00000000..90c5fa53 --- /dev/null +++ b/Assets/External/websocket-sharp/AssemblyInfo.cs @@ -0,0 +1,26 @@ +using System.Reflection; +using System.Runtime.CompilerServices; + +// Information about this assembly is defined by the following attributes. +// Change them to the values specific to your project. + +[assembly: AssemblyTitle("websocket-sharp")] +[assembly: AssemblyDescription("A C# implementation of the WebSocket protocol client and server")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("websocket-sharp.dll")] +[assembly: AssemblyCopyright("sta.blockhead")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// The assembly version has the format "{Major}.{Minor}.{Build}.{Revision}". +// The form "{Major}.{Minor}.*" will automatically update the build and revision, +// and "{Major}.{Minor}.{Build}.*" will update just the revision. + +[assembly: AssemblyVersion("1.0.2.0")] + +// The following attributes are used to specify the signing key for the assembly, +// if desired. See the Mono documentation for more information about signing. + +//[assembly: AssemblyDelaySign(false)] +//[assembly: AssemblyKeyFile("")] diff --git a/Assets/External/websocket-sharp/AssemblyInfo.cs.meta b/Assets/External/websocket-sharp/AssemblyInfo.cs.meta new file mode 100644 index 00000000..293e9191 --- /dev/null +++ b/Assets/External/websocket-sharp/AssemblyInfo.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 39fd29427d0e18849a32d93c78615e9f \ No newline at end of file diff --git a/Assets/External/websocket-sharp/ByteOrder.cs b/Assets/External/websocket-sharp/ByteOrder.cs new file mode 100644 index 00000000..317f462e --- /dev/null +++ b/Assets/External/websocket-sharp/ByteOrder.cs @@ -0,0 +1,47 @@ +#region License +/* + * ByteOrder.cs + * + * The MIT License + * + * Copyright (c) 2012-2015 sta.blockhead + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#endregion + +using System; + +namespace WebSocketSharp +{ + /// + /// Specifies the byte order. + /// + public enum ByteOrder + { + /// + /// Specifies Little-endian. + /// + Little, + /// + /// Specifies Big-endian. + /// + Big + } +} diff --git a/Assets/External/websocket-sharp/ByteOrder.cs.meta b/Assets/External/websocket-sharp/ByteOrder.cs.meta new file mode 100644 index 00000000..61c0e0a0 --- /dev/null +++ b/Assets/External/websocket-sharp/ByteOrder.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 87c28a16b5a541a46b5c738f453e85ea \ No newline at end of file diff --git a/Assets/External/websocket-sharp/CloseEventArgs.cs b/Assets/External/websocket-sharp/CloseEventArgs.cs new file mode 100644 index 00000000..8aa46e2d --- /dev/null +++ b/Assets/External/websocket-sharp/CloseEventArgs.cs @@ -0,0 +1,118 @@ +#region License +/* + * CloseEventArgs.cs + * + * The MIT License + * + * Copyright (c) 2012-2024 sta.blockhead + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#endregion + +using System; + +namespace WebSocketSharp +{ + /// + /// Represents the event data for the event. + /// + /// + /// + /// The close event occurs when the WebSocket connection has been closed. + /// + /// + /// If you would like to get the reason for the connection close, + /// you should access the or + /// property. + /// + /// + public class CloseEventArgs : EventArgs + { + #region Private Fields + + private PayloadData _payloadData; + private bool _wasClean; + + #endregion + + #region Internal Constructors + + internal CloseEventArgs (PayloadData payloadData, bool clean) + { + _payloadData = payloadData; + _wasClean = clean; + } + + #endregion + + #region Public Properties + + /// + /// Gets the status code for the connection close. + /// + /// + /// + /// A that represents the status code for + /// the connection close. + /// + /// + /// 1005 (no status) if not present. + /// + /// + public ushort Code { + get { + return _payloadData.Code; + } + } + + /// + /// Gets the reason for the connection close. + /// + /// + /// + /// A that represents the reason for + /// the connection close. + /// + /// + /// An empty string if not present. + /// + /// + public string Reason { + get { + return _payloadData.Reason; + } + } + + /// + /// Gets a value indicating whether the connection has been closed cleanly. + /// + /// + /// true if the connection has been closed cleanly; otherwise, + /// false. + /// + public bool WasClean { + get { + return _wasClean; + } + } + + #endregion + } +} diff --git a/Assets/External/websocket-sharp/CloseEventArgs.cs.meta b/Assets/External/websocket-sharp/CloseEventArgs.cs.meta new file mode 100644 index 00000000..587f6a9a --- /dev/null +++ b/Assets/External/websocket-sharp/CloseEventArgs.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 1d2b6502961bce247ab494e4a6d127c2 \ No newline at end of file diff --git a/Assets/External/websocket-sharp/CloseStatusCode.cs b/Assets/External/websocket-sharp/CloseStatusCode.cs new file mode 100644 index 00000000..81f3317a --- /dev/null +++ b/Assets/External/websocket-sharp/CloseStatusCode.cs @@ -0,0 +1,120 @@ +#region License +/* + * CloseStatusCode.cs + * + * The MIT License + * + * Copyright (c) 2012-2016 sta.blockhead + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#endregion + +using System; + +namespace WebSocketSharp +{ + /// + /// Indicates the status code for the WebSocket connection close. + /// + /// + /// + /// The values of this enumeration are defined in + /// + /// Section 7.4 of RFC 6455. + /// + /// + /// "Reserved value" cannot be sent as a status code in + /// closing handshake by an endpoint. + /// + /// + public enum CloseStatusCode : ushort + { + /// + /// Equivalent to close status 1000. Indicates normal close. + /// + Normal = 1000, + /// + /// Equivalent to close status 1001. Indicates that an endpoint is + /// going away. + /// + Away = 1001, + /// + /// Equivalent to close status 1002. Indicates that an endpoint is + /// terminating the connection due to a protocol error. + /// + ProtocolError = 1002, + /// + /// Equivalent to close status 1003. Indicates that an endpoint is + /// terminating the connection because it has received a type of + /// data that it cannot accept. + /// + UnsupportedData = 1003, + /// + /// Equivalent to close status 1004. Still undefined. A Reserved value. + /// + Undefined = 1004, + /// + /// Equivalent to close status 1005. Indicates that no status code was + /// actually present. A Reserved value. + /// + NoStatus = 1005, + /// + /// Equivalent to close status 1006. Indicates that the connection was + /// closed abnormally. A Reserved value. + /// + Abnormal = 1006, + /// + /// Equivalent to close status 1007. Indicates that an endpoint is + /// terminating the connection because it has received a message that + /// contains data that is not consistent with the type of the message. + /// + InvalidData = 1007, + /// + /// Equivalent to close status 1008. Indicates that an endpoint is + /// terminating the connection because it has received a message that + /// violates its policy. + /// + PolicyViolation = 1008, + /// + /// Equivalent to close status 1009. Indicates that an endpoint is + /// terminating the connection because it has received a message that + /// is too big to process. + /// + TooBig = 1009, + /// + /// Equivalent to close status 1010. Indicates that a client is + /// terminating the connection because it has expected the server to + /// negotiate one or more extension, but the server did not return + /// them in the handshake response. + /// + MandatoryExtension = 1010, + /// + /// Equivalent to close status 1011. Indicates that a server is + /// terminating the connection because it has encountered an unexpected + /// condition that prevented it from fulfilling the request. + /// + ServerError = 1011, + /// + /// Equivalent to close status 1015. Indicates that the connection was + /// closed due to a failure to perform a TLS handshake. A Reserved value. + /// + TlsHandshakeFailure = 1015 + } +} diff --git a/Assets/External/websocket-sharp/CloseStatusCode.cs.meta b/Assets/External/websocket-sharp/CloseStatusCode.cs.meta new file mode 100644 index 00000000..e6461507 --- /dev/null +++ b/Assets/External/websocket-sharp/CloseStatusCode.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: e8dc83dc35b69e140853c4e18b52b4a8 \ No newline at end of file diff --git a/Assets/External/websocket-sharp/CompressionMethod.cs b/Assets/External/websocket-sharp/CompressionMethod.cs new file mode 100644 index 00000000..42ab230a --- /dev/null +++ b/Assets/External/websocket-sharp/CompressionMethod.cs @@ -0,0 +1,52 @@ +#region License +/* + * CompressionMethod.cs + * + * The MIT License + * + * Copyright (c) 2013-2017 sta.blockhead + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#endregion + +using System; + +namespace WebSocketSharp +{ + /// + /// Specifies the method for compression. + /// + /// + /// The methods are defined in + /// + /// Compression Extensions for WebSocket. + /// + public enum CompressionMethod : byte + { + /// + /// Specifies no compression. + /// + None, + /// + /// Specifies DEFLATE. + /// + Deflate + } +} diff --git a/Assets/External/websocket-sharp/CompressionMethod.cs.meta b/Assets/External/websocket-sharp/CompressionMethod.cs.meta new file mode 100644 index 00000000..ba3f3df5 --- /dev/null +++ b/Assets/External/websocket-sharp/CompressionMethod.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: f388d1e288415ad45b4bf734a6232341 \ No newline at end of file diff --git a/Assets/External/websocket-sharp/ErrorEventArgs.cs b/Assets/External/websocket-sharp/ErrorEventArgs.cs new file mode 100644 index 00000000..c02d0e00 --- /dev/null +++ b/Assets/External/websocket-sharp/ErrorEventArgs.cs @@ -0,0 +1,115 @@ +#region License +/* + * ErrorEventArgs.cs + * + * The MIT License + * + * Copyright (c) 2012-2022 sta.blockhead + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#endregion + +#region Contributors +/* + * Contributors: + * - Frank Razenberg + */ +#endregion + +using System; + +namespace WebSocketSharp +{ + /// + /// Represents the event data for the event. + /// + /// + /// + /// The error event occurs when the interface + /// gets an error. + /// + /// + /// If you would like to get the error message, you should access + /// the property. + /// + /// + /// If the error is due to an exception, you can get it by accessing + /// the property. + /// + /// + public class ErrorEventArgs : EventArgs + { + #region Private Fields + + private Exception _exception; + private string _message; + + #endregion + + #region Internal Constructors + + internal ErrorEventArgs (string message) + : this (message, null) + { + } + + internal ErrorEventArgs (string message, Exception exception) + { + _message = message; + _exception = exception; + } + + #endregion + + #region Public Properties + + /// + /// Gets the exception that caused the error. + /// + /// + /// + /// An instance that represents + /// the cause of the error. + /// + /// + /// if not present. + /// + /// + public Exception Exception { + get { + return _exception; + } + } + + /// + /// Gets the error message. + /// + /// + /// A that represents the error message. + /// + public string Message { + get { + return _message; + } + } + + #endregion + } +} diff --git a/Assets/External/websocket-sharp/ErrorEventArgs.cs.meta b/Assets/External/websocket-sharp/ErrorEventArgs.cs.meta new file mode 100644 index 00000000..4c9e2286 --- /dev/null +++ b/Assets/External/websocket-sharp/ErrorEventArgs.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 58b5642169f4cce4d8717bd4c8070768 \ No newline at end of file diff --git a/Assets/External/websocket-sharp/Ext.cs b/Assets/External/websocket-sharp/Ext.cs new file mode 100644 index 00000000..2540f803 --- /dev/null +++ b/Assets/External/websocket-sharp/Ext.cs @@ -0,0 +1,1902 @@ +#region License +/* + * Ext.cs + * + * Some parts of this code are derived from Mono (http://www.mono-project.com): + * - The GetStatusDescription method is derived from HttpListenerResponse.cs (System.Net) + * - The MaybeUri method is derived from Uri.cs (System) + * - The isPredefinedScheme method is derived from Uri.cs (System) + * + * The MIT License + * + * Copyright (c) 2001 Garrett Rooney + * Copyright (c) 2003 Ian MacLean + * Copyright (c) 2003 Ben Maurer + * Copyright (c) 2003, 2005, 2009 Novell, Inc. (http://www.novell.com) + * Copyright (c) 2009 Stephane Delcroix + * Copyright (c) 2010-2025 sta.blockhead + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#endregion + +#region Contributors +/* + * Contributors: + * - Liryna + * - Nikola Kovacevic + * - Chris Swiedler + */ +#endregion + +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.IO; +using System.IO.Compression; +using System.Net.Sockets; +using System.Security.Cryptography.X509Certificates; +using System.Text; +using WebSocketSharp.Net; +using WebSocketSharp.Net.WebSockets; +using WebSocketSharp.Server; + +namespace WebSocketSharp +{ + /// + /// Provides a set of static methods for websocket-sharp. + /// + public static class Ext + { + #region Private Fields + + private static readonly byte[] _last = new byte[] { 0x00 }; + private static readonly int _maxRetry = 5; + private const string _tspecials = "()<>@,;:\\\"/[]?={} \t"; + + #endregion + + #region Private Methods + + private static byte[] compress (this byte[] data) + { + if (data.LongLength == 0) + return data; + + using (var input = new MemoryStream (data)) + return input.compressToArray (); + } + + private static MemoryStream compress (this Stream stream) + { + var ret = new MemoryStream (); + + if (stream.Length == 0) + return ret; + + stream.Position = 0; + + var mode = CompressionMode.Compress; + + using (var ds = new DeflateStream (ret, mode, true)) { + stream.CopyTo (ds, 1024); + ds.Close (); // BFINAL set to 1. + ret.Write (_last, 0, 1); + + ret.Position = 0; + + return ret; + } + } + + private static byte[] compressToArray (this Stream stream) + { + using (var output = stream.compress ()) { + output.Close (); + + return output.ToArray (); + } + } + + private static byte[] decompress (this byte[] data) + { + if (data.LongLength == 0) + return data; + + using (var input = new MemoryStream (data)) + return input.decompressToArray (); + } + + private static MemoryStream decompress (this Stream stream) + { + var ret = new MemoryStream (); + + if (stream.Length == 0) + return ret; + + stream.Position = 0; + + var mode = CompressionMode.Decompress; + + using (var ds = new DeflateStream (stream, mode, true)) { + ds.CopyTo (ret, 1024); + + ret.Position = 0; + + return ret; + } + } + + private static byte[] decompressToArray (this Stream stream) + { + using (var output = stream.decompress ()) { + output.Close (); + + return output.ToArray (); + } + } + + private static bool isPredefinedScheme (this string value) + { + var c = value[0]; + + if (c == 'h') + return value == "http" || value == "https"; + + if (c == 'w') + return value == "ws" || value == "wss"; + + if (c == 'f') + return value == "file" || value == "ftp"; + + if (c == 'g') + return value == "gopher"; + + if (c == 'm') + return value == "mailto"; + + if (c == 'n') { + c = value[1]; + + return c == 'e' + ? value == "news" || value == "net.pipe" || value == "net.tcp" + : value == "nntp"; + } + + return false; + } + + #endregion + + #region Internal Methods + + internal static byte[] Append (this ushort code, string reason) + { + var codeAsBytes = code.ToByteArray (ByteOrder.Big); + + if (reason == null || reason.Length == 0) + return codeAsBytes; + + var buff = new List (codeAsBytes); + var reasonAsBytes = Encoding.UTF8.GetBytes (reason); + + buff.AddRange (reasonAsBytes); + + return buff.ToArray (); + } + + internal static byte[] Compress ( + this byte[] data, + CompressionMethod method + ) + { + return method == CompressionMethod.Deflate ? data.compress () : data; + } + + internal static Stream Compress ( + this Stream stream, + CompressionMethod method + ) + { + return method == CompressionMethod.Deflate ? stream.compress () : stream; + } + + internal static bool Contains (this string value, params char[] anyOf) + { + return anyOf != null && anyOf.Length > 0 + ? value.IndexOfAny (anyOf) > -1 + : false; + } + + internal static bool Contains ( + this NameValueCollection collection, + string name + ) + { + return collection[name] != null; + } + + internal static bool Contains ( + this NameValueCollection collection, + string name, + string value, + StringComparison comparisonTypeForValue + ) + { + var val = collection[name]; + + if (val == null) + return false; + + foreach (var elm in val.Split (',')) { + if (elm.Trim ().Equals (value, comparisonTypeForValue)) + return true; + } + + return false; + } + + internal static bool Contains ( + this IEnumerable source, + Func condition + ) + { + foreach (T elm in source) { + if (condition (elm)) + return true; + } + + return false; + } + + internal static bool ContainsTwice (this string[] values) + { + var len = values.Length; + var end = len - 1; + + Func seek = null; + seek = idx => { + if (idx == end) + return false; + + var val = values[idx]; + + for (var i = idx + 1; i < len; i++) { + if (values[i] == val) + return true; + } + + return seek (++idx); + }; + + return seek (0); + } + + internal static T[] Copy (this T[] sourceArray, int length) + { + var dest = new T[length]; + + Array.Copy (sourceArray, 0, dest, 0, length); + + return dest; + } + + internal static T[] Copy (this T[] sourceArray, long length) + { + var dest = new T[length]; + + Array.Copy (sourceArray, 0, dest, 0, length); + + return dest; + } + + internal static void CopyTo ( + this Stream sourceStream, + Stream destinationStream, + int bufferLength + ) + { + var buff = new byte[bufferLength]; + + while (true) { + var nread = sourceStream.Read (buff, 0, bufferLength); + + if (nread <= 0) + break; + + destinationStream.Write (buff, 0, nread); + } + } + + internal static void CopyToAsync ( + this Stream sourceStream, + Stream destinationStream, + int bufferLength, + Action completed, + Action error + ) + { + var buff = new byte[bufferLength]; + + AsyncCallback callback = null; + callback = + ar => { + try { + var nread = sourceStream.EndRead (ar); + + if (nread <= 0) { + if (completed != null) + completed (); + + return; + } + + destinationStream.Write (buff, 0, nread); + + sourceStream.BeginRead (buff, 0, bufferLength, callback, null); + } + catch (Exception ex) { + if (error != null) + error (ex); + } + }; + + try { + sourceStream.BeginRead (buff, 0, bufferLength, callback, null); + } + catch (Exception ex) { + if (error != null) + error (ex); + } + } + + internal static byte[] Decompress ( + this byte[] data, + CompressionMethod method + ) + { + return method == CompressionMethod.Deflate ? data.decompress () : data; + } + + internal static Stream Decompress ( + this Stream stream, + CompressionMethod method + ) + { + return method == CompressionMethod.Deflate + ? stream.decompress () + : stream; + } + + internal static byte[] DecompressToArray ( + this Stream stream, + CompressionMethod method + ) + { + return method == CompressionMethod.Deflate + ? stream.decompressToArray () + : stream.ToByteArray (); + } + + internal static void Emit ( + this EventHandler eventHandler, + object sender, + EventArgs e + ) + { + if (eventHandler == null) + return; + + eventHandler (sender, e); + } + + internal static void Emit ( + this EventHandler eventHandler, + object sender, + TEventArgs e + ) + where TEventArgs : EventArgs + { + if (eventHandler == null) + return; + + eventHandler (sender, e); + } + + internal static string GetAbsolutePath (this Uri uri) + { + if (uri.IsAbsoluteUri) + return uri.AbsolutePath; + + var original = uri.OriginalString; + + if (original[0] != '/') + return null; + + var idx = original.IndexOfAny (new[] { '?', '#' }); + + return idx > 0 ? original.Substring (0, idx) : original; + } + + internal static CookieCollection GetCookies ( + this NameValueCollection headers, + bool response + ) + { + var name = response ? "Set-Cookie" : "Cookie"; + var val = headers[name]; + + return val != null + ? CookieCollection.Parse (val, response) + : new CookieCollection (); + } + + internal static string GetDnsSafeHost (this Uri uri, bool bracketIPv6) + { + return bracketIPv6 && uri.HostNameType == UriHostNameType.IPv6 + ? uri.Host + : uri.DnsSafeHost; + } + + internal static string GetErrorMessage (this ushort code) + { + switch (code) { + case 1002: + return "A protocol error has occurred."; + case 1003: + return "Unsupported data has been received."; + case 1006: + return "An abnormal error has occurred."; + case 1007: + return "Invalid data has been received."; + case 1008: + return "A policy violation has occurred."; + case 1009: + return "A too big message has been received."; + case 1010: + return "The client did not receive expected extension(s)."; + case 1011: + return "The server got an internal error."; + case 1015: + return "An error has occurred during a TLS handshake."; + default: + return String.Empty; + } + } + + internal static string GetErrorMessage (this CloseStatusCode code) + { + return ((ushort) code).GetErrorMessage (); + } + + internal static string GetName (this string nameAndValue, char separator) + { + var idx = nameAndValue.IndexOf (separator); + + return idx > 0 ? nameAndValue.Substring (0, idx).Trim () : null; + } + + internal static string GetUTF8DecodedString (this byte[] bytes) + { + try { + return Encoding.UTF8.GetString (bytes); + } + catch { + return null; + } + } + + internal static byte[] GetUTF8EncodedBytes (this string s) + { + try { + return Encoding.UTF8.GetBytes (s); + } + catch { + return null; + } + } + + internal static string GetValue (this string nameAndValue, char separator) + { + return nameAndValue.GetValue (separator, false); + } + + internal static string GetValue ( + this string nameAndValue, + char separator, + bool unquote + ) + { + var idx = nameAndValue.IndexOf (separator); + + if (idx < 0 || idx == nameAndValue.Length - 1) + return null; + + var val = nameAndValue.Substring (idx + 1).Trim (); + + return unquote ? val.Unquote () : val; + } + + internal static bool IsCompressionExtension ( + this string value, + CompressionMethod method + ) + { + var extStr = method.ToExtensionString (); + var compType = StringComparison.Ordinal; + + return value.StartsWith (extStr, compType); + } + + internal static bool IsDefined (this CloseStatusCode code) + { + return Enum.IsDefined (typeof (CloseStatusCode), code); + } + + internal static bool IsEqualTo ( + this int value, + char c, + Action beforeComparing + ) + { + beforeComparing (value); + + return value == c - 0; + } + + internal static bool IsHttpMethod (this string value) + { + return value == "GET" + || value == "HEAD" + || value == "POST" + || value == "PUT" + || value == "DELETE" + || value == "CONNECT" + || value == "OPTIONS" + || value == "TRACE"; + } + + internal static bool IsPortNumber (this int value) + { + return value > 0 && value < 65536; + } + + internal static bool IsReserved (this CloseStatusCode code) + { + return ((ushort) code).IsReservedStatusCode (); + } + + internal static bool IsReservedStatusCode (this ushort code) + { + return code == 1004 + || code == 1005 + || code == 1006 + || code == 1015; + } + + internal static bool IsSupportedOpcode (this int opcode) + { + return Enum.IsDefined (typeof (Opcode), opcode); + } + + internal static bool IsText (this string value) + { + var len = value.Length; + + for (var i = 0; i < len; i++) { + var c = value[i]; + + if (c < 0x20) { + if ("\r\n\t".IndexOf (c) == -1) + return false; + + if (c == '\n') { + i++; + + if (i == len) + break; + + c = value[i]; + + if (" \t".IndexOf (c) == -1) + return false; + } + + continue; + } + + if (c == 0x7f) + return false; + } + + return true; + } + + internal static bool IsToken (this string value) + { + foreach (var c in value) { + if (c < 0x20) + return false; + + if (c > 0x7e) + return false; + + if (_tspecials.IndexOf (c) > -1) + return false; + } + + return true; + } + + internal static bool KeepsAlive ( + this NameValueCollection headers, + Version version + ) + { + var compType = StringComparison.OrdinalIgnoreCase; + + return version > HttpVersion.Version10 + ? !headers.Contains ("Connection", "close", compType) + : headers.Contains ("Connection", "keep-alive", compType); + } + + internal static bool MaybeUri (this string value) + { + var idx = value.IndexOf (':'); + + if (idx < 2 || idx > 9) + return false; + + var schm = value.Substring (0, idx); + + return schm.isPredefinedScheme (); + } + + internal static string Quote (this string value) + { + var fmt = "\"{0}\""; + var val = value.Replace ("\"", "\\\""); + + return String.Format (fmt, val); + } + + internal static byte[] ReadBytes (this Stream stream, int length) + { + var ret = new byte[length]; + + var offset = 0; + var retry = 0; + + while (length > 0) { + var nread = stream.Read (ret, offset, length); + + if (nread <= 0) { + if (retry < _maxRetry) { + retry++; + + continue; + } + + return ret.SubArray (0, offset); + } + + retry = 0; + + offset += nread; + length -= nread; + } + + return ret; + } + + internal static byte[] ReadBytes ( + this Stream stream, + long length, + int bufferLength + ) + { + using (var dest = new MemoryStream ()) { + var buff = new byte[bufferLength]; + var retry = 0; + + while (length > 0) { + if (length < bufferLength) + bufferLength = (int) length; + + var nread = stream.Read (buff, 0, bufferLength); + + if (nread <= 0) { + if (retry < _maxRetry) { + retry++; + + continue; + } + + break; + } + + retry = 0; + + dest.Write (buff, 0, nread); + + length -= nread; + } + + dest.Close (); + + return dest.ToArray (); + } + } + + internal static void ReadBytesAsync ( + this Stream stream, + int length, + Action completed, + Action error + ) + { + var ret = new byte[length]; + + var offset = 0; + var retry = 0; + + AsyncCallback callback = null; + callback = + ar => { + try { + var nread = stream.EndRead (ar); + + if (nread <= 0) { + if (retry < _maxRetry) { + retry++; + + stream.BeginRead (ret, offset, length, callback, null); + + return; + } + + if (completed != null) + completed (ret.SubArray (0, offset)); + + return; + } + + if (nread == length) { + if (completed != null) + completed (ret); + + return; + } + + retry = 0; + + offset += nread; + length -= nread; + + stream.BeginRead (ret, offset, length, callback, null); + } + catch (Exception ex) { + if (error != null) + error (ex); + } + }; + + try { + stream.BeginRead (ret, offset, length, callback, null); + } + catch (Exception ex) { + if (error != null) + error (ex); + } + } + + internal static void ReadBytesAsync ( + this Stream stream, + long length, + int bufferLength, + Action completed, + Action error + ) + { + var dest = new MemoryStream (); + + var buff = new byte[bufferLength]; + var retry = 0; + + Action read = null; + read = + len => { + if (len < bufferLength) + bufferLength = (int) len; + + stream.BeginRead ( + buff, + 0, + bufferLength, + ar => { + try { + var nread = stream.EndRead (ar); + + if (nread <= 0) { + if (retry < _maxRetry) { + retry++; + + read (len); + + return; + } + + if (completed != null) { + dest.Close (); + + var ret = dest.ToArray (); + + completed (ret); + } + + dest.Dispose (); + + return; + } + + dest.Write (buff, 0, nread); + + if (nread == len) { + if (completed != null) { + dest.Close (); + + var ret = dest.ToArray (); + + completed (ret); + } + + dest.Dispose (); + + return; + } + + retry = 0; + + read (len - nread); + } + catch (Exception ex) { + dest.Dispose (); + + if (error != null) + error (ex); + } + }, + null + ); + }; + + try { + read (length); + } + catch (Exception ex) { + dest.Dispose (); + + if (error != null) + error (ex); + } + } + + internal static T[] Reverse (this T[] array) + { + var len = array.LongLength; + var ret = new T[len]; + + var end = len - 1; + + for (long i = 0; i <= end; i++) + ret[i] = array[end - i]; + + return ret; + } + + internal static IEnumerable SplitHeaderValue ( + this string value, + params char[] separators + ) + { + var len = value.Length; + var end = len - 1; + + var buff = new StringBuilder (32); + var escaped = false; + var quoted = false; + + for (var i = 0; i <= end; i++) { + var c = value[i]; + + buff.Append (c); + + if (c == '"') { + if (escaped) { + escaped = false; + + continue; + } + + quoted = !quoted; + + continue; + } + + if (c == '\\') { + if (i == end) + break; + + if (value[i + 1] == '"') + escaped = true; + + continue; + } + + if (Array.IndexOf (separators, c) > -1) { + if (quoted) + continue; + + buff.Length -= 1; + + yield return buff.ToString (); + + buff.Length = 0; + + continue; + } + } + + yield return buff.ToString (); + } + + internal static byte[] ToByteArray (this Stream stream) + { + stream.Position = 0; + + using (var buff = new MemoryStream ()) { + stream.CopyTo (buff, 1024); + buff.Close (); + + return buff.ToArray (); + } + } + + internal static byte[] ToByteArray (this ushort value, ByteOrder order) + { + var ret = BitConverter.GetBytes (value); + + if (!order.IsHostOrder ()) + Array.Reverse (ret); + + return ret; + } + + internal static byte[] ToByteArray (this ulong value, ByteOrder order) + { + var ret = BitConverter.GetBytes (value); + + if (!order.IsHostOrder ()) + Array.Reverse (ret); + + return ret; + } + + internal static CompressionMethod ToCompressionMethod (this string value) + { + var methods = Enum.GetValues (typeof (CompressionMethod)); + + foreach (CompressionMethod method in methods) { + if (method.ToExtensionString () == value) + return method; + } + + return CompressionMethod.None; + } + + internal static string ToExtensionString ( + this CompressionMethod method, + params string[] parameters + ) + { + if (method == CompressionMethod.None) + return String.Empty; + + var name = method.ToString ().ToLower (); + var ename = String.Format ("permessage-{0}", name); + + if (parameters == null || parameters.Length == 0) + return ename; + + var eparams = parameters.ToString ("; "); + + return String.Format ("{0}; {1}", ename, eparams); + } + + internal static int ToInt32 (this string numericString) + { + return Int32.Parse (numericString); + } + + internal static System.Net.IPAddress ToIPAddress (this string value) + { + if (value == null || value.Length == 0) + return null; + + System.Net.IPAddress addr; + + if (System.Net.IPAddress.TryParse (value, out addr)) + return addr; + + try { + var addrs = System.Net.Dns.GetHostAddresses (value); + + return addrs[0]; + } + catch { + return null; + } + } + + internal static List ToList ( + this IEnumerable source + ) + { + return new List (source); + } + + internal static string ToString ( + this System.Net.IPAddress address, + bool bracketIPv6 + ) + { + return bracketIPv6 + && address.AddressFamily == AddressFamily.InterNetworkV6 + ? String.Format ("[{0}]", address) + : address.ToString (); + } + + internal static ushort ToUInt16 (this byte[] source, ByteOrder sourceOrder) + { + var val = source.ToHostOrder (sourceOrder); + + return BitConverter.ToUInt16 (val, 0); + } + + internal static ulong ToUInt64 (this byte[] source, ByteOrder sourceOrder) + { + var val = source.ToHostOrder (sourceOrder); + + return BitConverter.ToUInt64 (val, 0); + } + + internal static Version ToVersion (this string versionString) + { + return new Version (versionString); + } + + internal static IEnumerable TrimEach ( + this IEnumerable source + ) + { + foreach (var elm in source) + yield return elm.Trim (); + } + + internal static string TrimSlashFromEnd (this string value) + { + var ret = value.TrimEnd ('/'); + + return ret.Length > 0 ? ret : "/"; + } + + internal static string TrimSlashOrBackslashFromEnd (this string value) + { + var ret = value.TrimEnd ('/', '\\'); + + return ret.Length > 0 ? ret : value[0].ToString (); + } + + internal static bool TryCreateVersion ( + this string versionString, + out Version result + ) + { + result = null; + + try { + result = new Version (versionString); + } + catch { + return false; + } + + return true; + } + + internal static bool TryCreateWebSocketUri ( + this string uriString, + out Uri result, + out string message + ) + { + result = null; + message = null; + + var uri = uriString.ToUri (); + + if (uri == null) { + message = "An invalid URI string."; + + return false; + } + + if (!uri.IsAbsoluteUri) { + message = "A relative URI."; + + return false; + } + + var schm = uri.Scheme; + var valid = schm == "ws" || schm == "wss"; + + if (!valid) { + message = "The scheme part is not \"ws\" or \"wss\"."; + + return false; + } + + var port = uri.Port; + + if (port == 0) { + message = "The port part is zero."; + + return false; + } + + if (uri.Fragment.Length > 0) { + message = "It includes the fragment component."; + + return false; + } + + if (port == -1) { + port = schm == "ws" ? 80 : 443; + uriString = String.Format ( + "{0}://{1}:{2}{3}", + schm, + uri.Host, + port, + uri.PathAndQuery + ); + + result = new Uri (uriString); + } + else { + result = uri; + } + + return true; + } + + internal static bool TryGetUTF8DecodedString ( + this byte[] bytes, + out string s + ) + { + s = null; + + try { + s = Encoding.UTF8.GetString (bytes); + } + catch { + return false; + } + + return true; + } + + internal static bool TryGetUTF8EncodedBytes ( + this string s, + out byte[] bytes + ) + { + bytes = null; + + try { + bytes = Encoding.UTF8.GetBytes (s); + } + catch { + return false; + } + + return true; + } + + internal static bool TryOpenRead ( + this FileInfo fileInfo, + out FileStream fileStream + ) + { + fileStream = null; + + try { + fileStream = fileInfo.OpenRead (); + } + catch { + return false; + } + + return true; + } + + internal static string Unquote (this string value) + { + var first = value.IndexOf ('"'); + + if (first == -1) + return value; + + var last = value.LastIndexOf ('"'); + + if (last == first) + return value; + + var len = last - first - 1; + + return len > 0 + ? value.Substring (first + 1, len).Replace ("\\\"", "\"") + : String.Empty; + } + + internal static bool Upgrades ( + this NameValueCollection headers, + string protocol + ) + { + var compType = StringComparison.OrdinalIgnoreCase; + + return headers.Contains ("Upgrade", protocol, compType) + && headers.Contains ("Connection", "Upgrade", compType); + } + + internal static string UrlDecode (this string value, Encoding encoding) + { + return value.IndexOfAny (new[] { '%', '+' }) > -1 + ? HttpUtility.UrlDecode (value, encoding) + : value; + } + + internal static string UrlEncode (this string value, Encoding encoding) + { + return HttpUtility.UrlEncode (value, encoding); + } + + internal static void WriteBytes ( + this Stream stream, + byte[] bytes, + int bufferLength + ) + { + using (var src = new MemoryStream (bytes)) + src.CopyTo (stream, bufferLength); + } + + internal static void WriteBytesAsync ( + this Stream stream, + byte[] bytes, + int bufferLength, + Action completed, + Action error + ) + { + var src = new MemoryStream (bytes); + + src.CopyToAsync ( + stream, + bufferLength, + () => { + if (completed != null) + completed (); + + src.Dispose (); + }, + ex => { + src.Dispose (); + + if (error != null) + error (ex); + } + ); + } + + #endregion + + #region Public Methods + + /// + /// Gets the description of the specified HTTP status code. + /// + /// + /// A that represents the description of + /// the HTTP status code. + /// + /// + /// + /// One of the enum values. + /// + /// + /// It specifies the HTTP status code. + /// + /// + public static string GetDescription (this HttpStatusCode code) + { + return ((int) code).GetStatusDescription (); + } + + /// + /// Gets the description of the specified HTTP status code. + /// + /// + /// + /// A that represents the description of + /// the HTTP status code. + /// + /// + /// An empty string if the description is not present. + /// + /// + /// + /// An that specifies the HTTP status code. + /// + public static string GetStatusDescription (this int code) + { + switch (code) { + case 100: return "Continue"; + case 101: return "Switching Protocols"; + case 102: return "Processing"; + case 200: return "OK"; + case 201: return "Created"; + case 202: return "Accepted"; + case 203: return "Non-Authoritative Information"; + case 204: return "No Content"; + case 205: return "Reset Content"; + case 206: return "Partial Content"; + case 207: return "Multi-Status"; + case 300: return "Multiple Choices"; + case 301: return "Moved Permanently"; + case 302: return "Found"; + case 303: return "See Other"; + case 304: return "Not Modified"; + case 305: return "Use Proxy"; + case 307: return "Temporary Redirect"; + case 400: return "Bad Request"; + case 401: return "Unauthorized"; + case 402: return "Payment Required"; + case 403: return "Forbidden"; + case 404: return "Not Found"; + case 405: return "Method Not Allowed"; + case 406: return "Not Acceptable"; + case 407: return "Proxy Authentication Required"; + case 408: return "Request Timeout"; + case 409: return "Conflict"; + case 410: return "Gone"; + case 411: return "Length Required"; + case 412: return "Precondition Failed"; + case 413: return "Request Entity Too Large"; + case 414: return "Request-Uri Too Long"; + case 415: return "Unsupported Media Type"; + case 416: return "Requested Range Not Satisfiable"; + case 417: return "Expectation Failed"; + case 422: return "Unprocessable Entity"; + case 423: return "Locked"; + case 424: return "Failed Dependency"; + case 500: return "Internal Server Error"; + case 501: return "Not Implemented"; + case 502: return "Bad Gateway"; + case 503: return "Service Unavailable"; + case 504: return "Gateway Timeout"; + case 505: return "Http Version Not Supported"; + case 507: return "Insufficient Storage"; + } + + return String.Empty; + } + + /// + /// Determines whether the specified ushort is in the range of + /// the status code for the WebSocket connection close. + /// + /// + /// + /// The ranges are the following: + /// + /// + /// + /// + /// 1000-2999: These numbers are reserved for definition by + /// the WebSocket protocol. + /// + /// + /// + /// + /// 3000-3999: These numbers are reserved for use by libraries, + /// frameworks, and applications. + /// + /// + /// + /// + /// 4000-4999: These numbers are reserved for private use. + /// + /// + /// + /// + /// + /// true if is in the range of + /// the status code for the close; otherwise, false. + /// + /// + /// A to test. + /// + public static bool IsCloseStatusCode (this ushort value) + { + return value > 999 && value < 5000; + } + + /// + /// Determines whether the specified string is enclosed in + /// the specified character. + /// + /// + /// true if is enclosed in + /// ; otherwise, false. + /// + /// + /// A to test. + /// + /// + /// A to find. + /// + public static bool IsEnclosedIn (this string value, char c) + { + if (value == null) + return false; + + var len = value.Length; + + return len > 1 ? value[0] == c && value[len - 1] == c : false; + } + + /// + /// Determines whether the specified byte order is host (this computer + /// architecture) byte order. + /// + /// + /// true if is host byte order; otherwise, + /// false. + /// + /// + /// One of the enum values to test. + /// + public static bool IsHostOrder (this ByteOrder order) + { + // true: !(true ^ true) or !(false ^ false) + // false: !(true ^ false) or !(false ^ true) + return !(BitConverter.IsLittleEndian ^ (order == ByteOrder.Little)); + } + + /// + /// Determines whether the specified IP address is a local IP address. + /// + /// + /// This local means NOT REMOTE for the current host. + /// + /// + /// true if is a local IP address; + /// otherwise, false. + /// + /// + /// A to test. + /// + /// + /// is . + /// + public static bool IsLocal (this System.Net.IPAddress address) + { + if (address == null) + throw new ArgumentNullException ("address"); + + if (address.Equals (System.Net.IPAddress.Any)) + return true; + + if (address.Equals (System.Net.IPAddress.Loopback)) + return true; + + if (Socket.OSSupportsIPv6) { + if (address.Equals (System.Net.IPAddress.IPv6Any)) + return true; + + if (address.Equals (System.Net.IPAddress.IPv6Loopback)) + return true; + } + + var name = System.Net.Dns.GetHostName (); + var addrs = System.Net.Dns.GetHostAddresses (name); + + foreach (var addr in addrs) { + if (address.Equals (addr)) + return true; + } + + return false; + } + + /// + /// Determines whether the specified string is or + /// an empty string. + /// + /// + /// true if is or + /// an empty string; otherwise, false. + /// + /// + /// A to test. + /// + public static bool IsNullOrEmpty (this string value) + { + return value == null || value.Length == 0; + } + + /// + /// Retrieves a sub-array from the specified array. A sub-array starts at + /// the specified index in the array. + /// + /// + /// An array of T that receives a sub-array. + /// + /// + /// An array of T from which to retrieve a sub-array. + /// + /// + /// An that specifies the zero-based index in the array + /// at which retrieving starts. + /// + /// + /// An that specifies the number of elements to retrieve. + /// + /// + /// The type of elements in the array. + /// + /// + /// is . + /// + /// + /// + /// is less than zero. + /// + /// + /// -or- + /// + /// + /// is greater than the end of the array. + /// + /// + /// -or- + /// + /// + /// is less than zero. + /// + /// + /// -or- + /// + /// + /// is greater than the number of elements from + /// to the end of the array. + /// + /// + public static T[] SubArray (this T[] array, int startIndex, int length) + { + if (array == null) + throw new ArgumentNullException ("array"); + + var len = array.Length; + + if (len == 0) { + if (startIndex != 0) + throw new ArgumentOutOfRangeException ("startIndex"); + + if (length != 0) + throw new ArgumentOutOfRangeException ("length"); + + return array; + } + + if (startIndex < 0 || startIndex >= len) + throw new ArgumentOutOfRangeException ("startIndex"); + + if (length < 0 || length > len - startIndex) + throw new ArgumentOutOfRangeException ("length"); + + if (length == 0) + return new T[0]; + + if (length == len) + return array; + + var ret = new T[length]; + + Array.Copy (array, startIndex, ret, 0, length); + + return ret; + } + + /// + /// Retrieves a sub-array from the specified array. A sub-array starts at + /// the specified index in the array. + /// + /// + /// An array of T that receives a sub-array. + /// + /// + /// An array of T from which to retrieve a sub-array. + /// + /// + /// A that specifies the zero-based index in the array + /// at which retrieving starts. + /// + /// + /// A that specifies the number of elements to retrieve. + /// + /// + /// The type of elements in the array. + /// + /// + /// is . + /// + /// + /// + /// is less than zero. + /// + /// + /// -or- + /// + /// + /// is greater than the end of the array. + /// + /// + /// -or- + /// + /// + /// is less than zero. + /// + /// + /// -or- + /// + /// + /// is greater than the number of elements from + /// to the end of the array. + /// + /// + public static T[] SubArray (this T[] array, long startIndex, long length) + { + if (array == null) + throw new ArgumentNullException ("array"); + + var len = array.LongLength; + + if (len == 0) { + if (startIndex != 0) + throw new ArgumentOutOfRangeException ("startIndex"); + + if (length != 0) + throw new ArgumentOutOfRangeException ("length"); + + return array; + } + + if (startIndex < 0 || startIndex >= len) + throw new ArgumentOutOfRangeException ("startIndex"); + + if (length < 0 || length > len - startIndex) + throw new ArgumentOutOfRangeException ("length"); + + if (length == 0) + return new T[0]; + + if (length == len) + return array; + + var ret = new T[length]; + + Array.Copy (array, startIndex, ret, 0, length); + + return ret; + } + + /// + /// Executes the specified delegate times. + /// + /// + /// An that specifies the number of times to execute. + /// + /// + /// + /// An Action<int> delegate to execute. + /// + /// + /// The parameter is the zero-based count of iteration. + /// + /// + public static void Times (this int n, Action action) + { + if (n <= 0) + return; + + if (action == null) + return; + + for (int i = 0; i < n; i++) + action (i); + } + + /// + /// Executes the specified delegate times. + /// + /// + /// A that specifies the number of times to execute. + /// + /// + /// + /// An Action<long> delegate to execute. + /// + /// + /// The parameter is the zero-based count of iteration. + /// + /// + public static void Times (this long n, Action action) + { + if (n <= 0) + return; + + if (action == null) + return; + + for (long i = 0; i < n; i++) + action (i); + } + + /// + /// Converts the order of elements in the specified byte array to + /// host (this computer architecture) byte order. + /// + /// + /// + /// An array of converted from + /// . + /// + /// + /// if the number of elements in + /// it is less than 2 or is + /// same as host byte order. + /// + /// + /// + /// An array of to convert. + /// + /// + /// + /// One of the enum values. + /// + /// + /// It specifies the order of elements in . + /// + /// + /// + /// is . + /// + public static byte[] ToHostOrder (this byte[] source, ByteOrder sourceOrder) + { + if (source == null) + throw new ArgumentNullException ("source"); + + if (source.Length < 2) + return source; + + if (sourceOrder.IsHostOrder ()) + return source; + + return source.Reverse (); + } + + /// + /// Converts the specified array to a string. + /// + /// + /// + /// A converted by concatenating each element of + /// across . + /// + /// + /// An empty string if is an empty array. + /// + /// + /// + /// An array of T to convert. + /// + /// + /// A used to separate each element of + /// . + /// + /// + /// The type of elements in . + /// + /// + /// is . + /// + public static string ToString (this T[] array, string separator) + { + if (array == null) + throw new ArgumentNullException ("array"); + + var len = array.Length; + + if (len == 0) + return String.Empty; + + var buff = new StringBuilder (64); + var end = len - 1; + + for (var i = 0; i < end; i++) + buff.AppendFormat ("{0}{1}", array[i], separator); + + buff.AppendFormat ("{0}", array[end]); + + return buff.ToString (); + } + + /// + /// Converts the specified string to a . + /// + /// + /// + /// A converted from . + /// + /// + /// if the conversion has failed. + /// + /// + /// + /// A to convert. + /// + public static Uri ToUri (this string value) + { + if (value == null || value.Length == 0) + return null; + + var kind = value.MaybeUri () ? UriKind.Absolute : UriKind.Relative; + Uri ret; + + Uri.TryCreate (value, kind, out ret); + + return ret; + } + + #endregion + } +} diff --git a/Assets/External/websocket-sharp/Ext.cs.meta b/Assets/External/websocket-sharp/Ext.cs.meta new file mode 100644 index 00000000..1aa38545 --- /dev/null +++ b/Assets/External/websocket-sharp/Ext.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 6c78a6051f0f6ab44806b2b480347ea4 \ No newline at end of file diff --git a/Assets/External/websocket-sharp/Fin.cs b/Assets/External/websocket-sharp/Fin.cs new file mode 100644 index 00000000..36622d7e --- /dev/null +++ b/Assets/External/websocket-sharp/Fin.cs @@ -0,0 +1,52 @@ +#region License +/* + * Fin.cs + * + * The MIT License + * + * Copyright (c) 2012-2025 sta.blockhead + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#endregion + +using System; + +namespace WebSocketSharp +{ + /// + /// Indicates whether a WebSocket frame is the final frame of a message. + /// + /// + /// The values of this enumeration are defined in + /// + /// Section 5.2 of RFC 6455. + /// + internal enum Fin + { + /// + /// Equivalent to numeric value 0. Indicates more frames of a message follow. + /// + More = 0x0, + /// + /// Equivalent to numeric value 1. Indicates the final frame of a message. + /// + Final = 0x1 + } +} diff --git a/Assets/External/websocket-sharp/Fin.cs.meta b/Assets/External/websocket-sharp/Fin.cs.meta new file mode 100644 index 00000000..49e742df --- /dev/null +++ b/Assets/External/websocket-sharp/Fin.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 8581c072d74d61c469dc413061917da4 \ No newline at end of file diff --git a/Assets/External/websocket-sharp/HttpBase.cs b/Assets/External/websocket-sharp/HttpBase.cs new file mode 100644 index 00000000..c4a244f4 --- /dev/null +++ b/Assets/External/websocket-sharp/HttpBase.cs @@ -0,0 +1,317 @@ +#region License +/* + * HttpBase.cs + * + * The MIT License + * + * Copyright (c) 2012-2024 sta.blockhead + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#endregion + +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; +using WebSocketSharp.Net; + +namespace WebSocketSharp +{ + internal abstract class HttpBase + { + #region Private Fields + + private NameValueCollection _headers; + private static readonly int _maxMessageHeaderLength; + private string _messageBody; + private byte[] _messageBodyData; + private Version _version; + + #endregion + + #region Protected Fields + + protected static readonly string CrLf; + protected static readonly string CrLfHt; + protected static readonly string CrLfSp; + + #endregion + + #region Static Constructor + + static HttpBase () + { + _maxMessageHeaderLength = 8192; + + CrLf = "\r\n"; + CrLfHt = "\r\n\t"; + CrLfSp = "\r\n "; + } + + #endregion + + #region Protected Constructors + + protected HttpBase (Version version, NameValueCollection headers) + { + _version = version; + _headers = headers; + } + + #endregion + + #region Internal Properties + + internal byte[] MessageBodyData { + get { + return _messageBodyData; + } + } + + #endregion + + #region Protected Properties + + protected string HeaderSection { + get { + var buff = new StringBuilder (64); + + var fmt = "{0}: {1}{2}"; + + foreach (var key in _headers.AllKeys) + buff.AppendFormat (fmt, key, _headers[key], CrLf); + + buff.Append (CrLf); + + return buff.ToString (); + } + } + + #endregion + + #region Public Properties + + public bool HasMessageBody { + get { + return _messageBodyData != null; + } + } + + public NameValueCollection Headers { + get { + return _headers; + } + } + + public string MessageBody { + get { + if (_messageBody == null) + _messageBody = getMessageBody (); + + return _messageBody; + } + } + + public abstract string MessageHeader { get; } + + public Version ProtocolVersion { + get { + return _version; + } + } + + #endregion + + #region Private Methods + + private string getMessageBody () + { + if (_messageBodyData == null || _messageBodyData.LongLength == 0) + return String.Empty; + + var contentType = _headers["Content-Type"]; + + var enc = contentType != null && contentType.Length > 0 + ? HttpUtility.GetEncoding (contentType) + : Encoding.UTF8; + + return enc.GetString (_messageBodyData); + } + + private static byte[] readMessageBodyFrom (Stream stream, string length) + { + long len; + + if (!Int64.TryParse (length, out len)) { + var msg = "It could not be parsed."; + + throw new ArgumentException (msg, "length"); + } + + if (len < 0) { + var msg = "Less than zero."; + + throw new ArgumentOutOfRangeException ("length", msg); + } + + return len > 1024 + ? stream.ReadBytes (len, 1024) + : len > 0 + ? stream.ReadBytes ((int) len) + : null; + } + + private static string[] readMessageHeaderFrom (Stream stream) + { + var buff = new List (); + var cnt = 0; + Action add = + i => { + if (i == -1) { + var msg = "The header could not be read from the data stream."; + + throw new EndOfStreamException (msg); + } + + buff.Add ((byte) i); + + cnt++; + }; + + var end = false; + + do { + end = stream.ReadByte ().IsEqualTo ('\r', add) + && stream.ReadByte ().IsEqualTo ('\n', add) + && stream.ReadByte ().IsEqualTo ('\r', add) + && stream.ReadByte ().IsEqualTo ('\n', add); + + if (cnt > _maxMessageHeaderLength) { + var msg = "The length of the header is greater than the max length."; + + throw new InvalidOperationException (msg); + } + } + while (!end); + + var bytes = buff.ToArray (); + + return Encoding.UTF8.GetString (bytes) + .Replace (CrLfSp, " ") + .Replace (CrLfHt, " ") + .Split (new[] { CrLf }, StringSplitOptions.RemoveEmptyEntries); + } + + #endregion + + #region Internal Methods + + internal void WriteTo (Stream stream) + { + var bytes = ToByteArray (); + + stream.Write (bytes, 0, bytes.Length); + } + + #endregion + + #region Protected Methods + + protected static T Read ( + Stream stream, + Func parser, + int millisecondsTimeout + ) + where T : HttpBase + { + T ret = null; + + var timeout = false; + var timer = new Timer ( + state => { + timeout = true; + + stream.Close (); + }, + null, + millisecondsTimeout, + -1 + ); + + Exception exception = null; + + try { + var header = readMessageHeaderFrom (stream); + ret = parser (header); + + var contentLen = ret.Headers["Content-Length"]; + + if (contentLen != null && contentLen.Length > 0) + ret._messageBodyData = readMessageBodyFrom (stream, contentLen); + } + catch (Exception ex) { + exception = ex; + } + finally { + timer.Change (-1, -1); + timer.Dispose (); + } + + if (timeout) { + var msg = "A timeout has occurred."; + + throw new WebSocketException (msg); + } + + if (exception != null) { + var msg = "An exception has occurred."; + + throw new WebSocketException (msg, exception); + } + + return ret; + } + + #endregion + + #region Public Methods + + public byte[] ToByteArray () + { + var headerData = Encoding.UTF8.GetBytes (MessageHeader); + + return _messageBodyData != null + ? headerData.Concat (_messageBodyData).ToArray () + : headerData; + } + + public override string ToString () + { + return _messageBodyData != null + ? MessageHeader + MessageBody + : MessageHeader; + } + + #endregion + } +} diff --git a/Assets/External/websocket-sharp/HttpBase.cs.meta b/Assets/External/websocket-sharp/HttpBase.cs.meta new file mode 100644 index 00000000..55ff373a --- /dev/null +++ b/Assets/External/websocket-sharp/HttpBase.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 01336e40da8ed304fa4f40db4660af86 \ No newline at end of file diff --git a/Assets/External/websocket-sharp/HttpRequest.cs b/Assets/External/websocket-sharp/HttpRequest.cs new file mode 100644 index 00000000..dd51d010 --- /dev/null +++ b/Assets/External/websocket-sharp/HttpRequest.cs @@ -0,0 +1,253 @@ +#region License +/* + * HttpRequest.cs + * + * The MIT License + * + * Copyright (c) 2012-2024 sta.blockhead + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#endregion + +#region Contributors +/* + * Contributors: + * - David Burhans + */ +#endregion + +using System; +using System.Collections.Specialized; +using System.IO; +using System.Text; +using WebSocketSharp.Net; + +namespace WebSocketSharp +{ + internal class HttpRequest : HttpBase + { + #region Private Fields + + private CookieCollection _cookies; + private string _method; + private string _target; + + #endregion + + #region Private Constructors + + private HttpRequest ( + string method, + string target, + Version version, + NameValueCollection headers + ) + : base (version, headers) + { + _method = method; + _target = target; + } + + #endregion + + #region Internal Constructors + + internal HttpRequest (string method, string target) + : this (method, target, HttpVersion.Version11, new NameValueCollection ()) + { + Headers["User-Agent"] = "websocket-sharp/1.0"; + } + + #endregion + + #region Internal Properties + + internal string RequestLine { + get { + var fmt = "{0} {1} HTTP/{2}{3}"; + + return String.Format (fmt, _method, _target, ProtocolVersion, CrLf); + } + } + + #endregion + + #region Public Properties + + public AuthenticationResponse AuthenticationResponse { + get { + var val = Headers["Authorization"]; + + return val != null && val.Length > 0 + ? AuthenticationResponse.Parse (val) + : null; + } + } + + public CookieCollection Cookies { + get { + if (_cookies == null) + _cookies = Headers.GetCookies (false); + + return _cookies; + } + } + + public string HttpMethod { + get { + return _method; + } + } + + public bool IsWebSocketRequest { + get { + return _method == "GET" + && ProtocolVersion > HttpVersion.Version10 + && Headers.Upgrades ("websocket"); + } + } + + public override string MessageHeader { + get { + return RequestLine + HeaderSection; + } + } + + public string RequestTarget { + get { + return _target; + } + } + + #endregion + + #region Internal Methods + + internal static HttpRequest CreateConnectRequest (Uri targetUri) + { + var fmt = "{0}:{1}"; + var host = targetUri.DnsSafeHost; + var port = targetUri.Port; + var authority = String.Format (fmt, host, port); + + var ret = new HttpRequest ("CONNECT", authority); + + ret.Headers["Host"] = port != 80 ? authority : host; + + return ret; + } + + internal static HttpRequest CreateWebSocketHandshakeRequest (Uri targetUri) + { + var ret = new HttpRequest ("GET", targetUri.PathAndQuery); + + var headers = ret.Headers; + + var port = targetUri.Port; + var schm = targetUri.Scheme; + var isDefaultPort = (port == 80 && schm == "ws") + || (port == 443 && schm == "wss"); + + headers["Host"] = !isDefaultPort + ? targetUri.Authority + : targetUri.DnsSafeHost; + + headers["Upgrade"] = "websocket"; + headers["Connection"] = "Upgrade"; + + return ret; + } + + internal HttpResponse GetResponse (Stream stream, int millisecondsTimeout) + { + WriteTo (stream); + + return HttpResponse.ReadResponse (stream, millisecondsTimeout); + } + + internal static HttpRequest Parse (string[] messageHeader) + { + var len = messageHeader.Length; + + if (len == 0) { + var msg = "An empty request header."; + + throw new ArgumentException (msg); + } + + var rlParts = messageHeader[0].Split (new[] { ' ' }, 3); + + if (rlParts.Length != 3) { + var msg = "It includes an invalid request line."; + + throw new ArgumentException (msg); + } + + var method = rlParts[0]; + var target = rlParts[1]; + var ver = rlParts[2].Substring (5).ToVersion (); + + var headers = new WebHeaderCollection (); + + for (var i = 1; i < len; i++) + headers.InternalSet (messageHeader[i], false); + + return new HttpRequest (method, target, ver, headers); + } + + internal static HttpRequest ReadRequest ( + Stream stream, + int millisecondsTimeout + ) + { + return Read (stream, Parse, millisecondsTimeout); + } + + #endregion + + #region Public Methods + + public void SetCookies (CookieCollection cookies) + { + if (cookies == null || cookies.Count == 0) + return; + + var buff = new StringBuilder (64); + + foreach (var cookie in cookies.Sorted) { + if (cookie.Expired) + continue; + + buff.AppendFormat ("{0}; ", cookie); + } + + var len = buff.Length; + + if (len <= 2) + return; + + buff.Length = len - 2; + + Headers["Cookie"] = buff.ToString (); + } + + #endregion + } +} diff --git a/Assets/External/websocket-sharp/HttpRequest.cs.meta b/Assets/External/websocket-sharp/HttpRequest.cs.meta new file mode 100644 index 00000000..4f6ac424 --- /dev/null +++ b/Assets/External/websocket-sharp/HttpRequest.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: f38d7bd29af589740a8ee756fd3dce99 \ No newline at end of file diff --git a/Assets/External/websocket-sharp/HttpResponse.cs b/Assets/External/websocket-sharp/HttpResponse.cs new file mode 100644 index 00000000..fb2f9d31 --- /dev/null +++ b/Assets/External/websocket-sharp/HttpResponse.cs @@ -0,0 +1,274 @@ +#region License +/* + * HttpResponse.cs + * + * The MIT License + * + * Copyright (c) 2012-2024 sta.blockhead + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#endregion + +using System; +using System.Collections.Specialized; +using System.IO; +using System.Text; +using WebSocketSharp.Net; + +namespace WebSocketSharp +{ + internal class HttpResponse : HttpBase + { + #region Private Fields + + private int _code; + private string _reason; + + #endregion + + #region Private Constructors + + private HttpResponse ( + int code, + string reason, + Version version, + NameValueCollection headers + ) + : base (version, headers) + { + _code = code; + _reason = reason; + } + + #endregion + + #region Internal Constructors + + internal HttpResponse (int code) + : this (code, code.GetStatusDescription ()) + { + } + + internal HttpResponse (HttpStatusCode code) + : this ((int) code) + { + } + + internal HttpResponse (int code, string reason) + : this ( + code, + reason, + HttpVersion.Version11, + new NameValueCollection () + ) + { + Headers["Server"] = "websocket-sharp/1.0"; + } + + internal HttpResponse (HttpStatusCode code, string reason) + : this ((int) code, reason) + { + } + + #endregion + + #region Internal Properties + + internal string StatusLine { + get { + return _reason != null + ? String.Format ( + "HTTP/{0} {1} {2}{3}", + ProtocolVersion, + _code, + _reason, + CrLf + ) + : String.Format ( + "HTTP/{0} {1}{2}", + ProtocolVersion, + _code, + CrLf + ); + } + } + + #endregion + + #region Public Properties + + public bool CloseConnection { + get { + var compType = StringComparison.OrdinalIgnoreCase; + + return Headers.Contains ("Connection", "close", compType); + } + } + + public CookieCollection Cookies { + get { + return Headers.GetCookies (true); + } + } + + public bool IsProxyAuthenticationRequired { + get { + return _code == 407; + } + } + + public bool IsRedirect { + get { + return _code == 301 || _code == 302; + } + } + + public bool IsSuccess { + get { + return _code >= 200 && _code <= 299; + } + } + + public bool IsUnauthorized { + get { + return _code == 401; + } + } + + public bool IsWebSocketResponse { + get { + return ProtocolVersion > HttpVersion.Version10 + && _code == 101 + && Headers.Upgrades ("websocket"); + } + } + + public override string MessageHeader { + get { + return StatusLine + HeaderSection; + } + } + + public string Reason { + get { + return _reason; + } + } + + public int StatusCode { + get { + return _code; + } + } + + #endregion + + #region Internal Methods + + internal static HttpResponse CreateCloseResponse (HttpStatusCode code) + { + var ret = new HttpResponse (code); + + ret.Headers["Connection"] = "close"; + + return ret; + } + + internal static HttpResponse CreateUnauthorizedResponse (string challenge) + { + var ret = new HttpResponse (HttpStatusCode.Unauthorized); + + ret.Headers["WWW-Authenticate"] = challenge; + + return ret; + } + + internal static HttpResponse CreateWebSocketHandshakeResponse () + { + var ret = new HttpResponse (HttpStatusCode.SwitchingProtocols); + + var headers = ret.Headers; + + headers["Upgrade"] = "websocket"; + headers["Connection"] = "Upgrade"; + + return ret; + } + + internal static HttpResponse Parse (string[] messageHeader) + { + var len = messageHeader.Length; + + if (len == 0) { + var msg = "An empty response header."; + + throw new ArgumentException (msg); + } + + var slParts = messageHeader[0].Split (new[] { ' ' }, 3); + var plen = slParts.Length; + + if (plen < 2) { + var msg = "It includes an invalid status line."; + + throw new ArgumentException (msg); + } + + var code = slParts[1].ToInt32 (); + var reason = plen == 3 ? slParts[2] : null; + var ver = slParts[0].Substring (5).ToVersion (); + + var headers = new WebHeaderCollection (); + + for (var i = 1; i < len; i++) + headers.InternalSet (messageHeader[i], true); + + return new HttpResponse (code, reason, ver, headers); + } + + internal static HttpResponse ReadResponse ( + Stream stream, + int millisecondsTimeout + ) + { + return Read (stream, Parse, millisecondsTimeout); + } + + #endregion + + #region Public Methods + + public void SetCookies (CookieCollection cookies) + { + if (cookies == null || cookies.Count == 0) + return; + + var headers = Headers; + + foreach (var cookie in cookies.Sorted) { + var val = cookie.ToResponseString (); + + headers.Add ("Set-Cookie", val); + } + } + + #endregion + } +} diff --git a/Assets/External/websocket-sharp/HttpResponse.cs.meta b/Assets/External/websocket-sharp/HttpResponse.cs.meta new file mode 100644 index 00000000..8dfa4717 --- /dev/null +++ b/Assets/External/websocket-sharp/HttpResponse.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: f3f90e85d078fa341af27ca4d173c389 \ No newline at end of file diff --git a/Assets/External/websocket-sharp/LogData.cs b/Assets/External/websocket-sharp/LogData.cs new file mode 100644 index 00000000..bb3492a9 --- /dev/null +++ b/Assets/External/websocket-sharp/LogData.cs @@ -0,0 +1,159 @@ +#region License +/* + * LogData.cs + * + * The MIT License + * + * Copyright (c) 2013-2024 sta.blockhead + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#endregion + +using System; +using System.Diagnostics; +using System.Text; + +namespace WebSocketSharp +{ + /// + /// Represents a log data used by the class. + /// + public class LogData + { + #region Private Fields + + private StackFrame _caller; + private DateTime _date; + private LogLevel _level; + private string _message; + + #endregion + + #region Internal Constructors + + internal LogData (LogLevel level, StackFrame caller, string message) + { + _level = level; + _caller = caller; + _message = message ?? String.Empty; + + _date = DateTime.Now; + } + + #endregion + + #region Public Properties + + /// + /// Gets the information of the logging method caller. + /// + /// + /// A that provides the information of + /// the logging method caller. + /// + public StackFrame Caller { + get { + return _caller; + } + } + + /// + /// Gets the date and time when the log data was created. + /// + /// + /// A that represents the date and time when + /// the log data was created. + /// + public DateTime Date { + get { + return _date; + } + } + + /// + /// Gets the logging level of the log data. + /// + /// + /// + /// One of the enum values. + /// + /// + /// It represents the logging level of the log data. + /// + /// + public LogLevel Level { + get { + return _level; + } + } + + /// + /// Gets the message of the log data. + /// + /// + /// A that represents the message of the log data. + /// + public string Message { + get { + return _message; + } + } + + #endregion + + #region Public Methods + + /// + /// Returns a string that represents the current instance. + /// + /// + /// A that represents the current instance. + /// + public override string ToString () + { + var date = String.Format ("[{0}]", _date); + var level = String.Format ("{0,-5}", _level.ToString ().ToUpper ()); + + var method = _caller.GetMethod (); + var type = method.DeclaringType; +#if DEBUG + var num = _caller.GetFileLineNumber (); + var caller = String.Format ("{0}.{1}:{2}", type.Name, method.Name, num); +#else + var caller = String.Format ("{0}.{1}", type.Name, method.Name); +#endif + var msgs = _message.Replace ("\r\n", "\n").TrimEnd ('\n').Split ('\n'); + + if (msgs.Length <= 1) + return String.Format ("{0} {1} {2} {3}", date, level, caller, _message); + + var buff = new StringBuilder (64); + + buff.AppendFormat ("{0} {1} {2}\n\n", date, level, caller); + + foreach (var msg in msgs) + buff.AppendFormat (" {0}\n", msg); + + return buff.ToString (); + } + + #endregion + } +} diff --git a/Assets/External/websocket-sharp/LogData.cs.meta b/Assets/External/websocket-sharp/LogData.cs.meta new file mode 100644 index 00000000..c8e27a29 --- /dev/null +++ b/Assets/External/websocket-sharp/LogData.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 7baa1e0e1d5589a4cb5d7990896ccf5e \ No newline at end of file diff --git a/Assets/External/websocket-sharp/LogLevel.cs b/Assets/External/websocket-sharp/LogLevel.cs new file mode 100644 index 00000000..5ff1d8fe --- /dev/null +++ b/Assets/External/websocket-sharp/LogLevel.cs @@ -0,0 +1,67 @@ +#region License +/* + * LogLevel.cs + * + * The MIT License + * + * Copyright (c) 2013-2022 sta.blockhead + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#endregion + +using System; + +namespace WebSocketSharp +{ + /// + /// Specifies the logging level. + /// + public enum LogLevel + { + /// + /// Specifies the bottom logging level. + /// + Trace, + /// + /// Specifies the 2nd logging level from the bottom. + /// + Debug, + /// + /// Specifies the 3rd logging level from the bottom. + /// + Info, + /// + /// Specifies the 3rd logging level from the top. + /// + Warn, + /// + /// Specifies the 2nd logging level from the top. + /// + Error, + /// + /// Specifies the top logging level. + /// + Fatal, + /// + /// Specifies not to output logs. + /// + None + } +} diff --git a/Assets/External/websocket-sharp/LogLevel.cs.meta b/Assets/External/websocket-sharp/LogLevel.cs.meta new file mode 100644 index 00000000..96b38137 --- /dev/null +++ b/Assets/External/websocket-sharp/LogLevel.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 8684c3e3443c55b47b0a2ffe1d14909c \ No newline at end of file diff --git a/Assets/External/websocket-sharp/Logger.cs b/Assets/External/websocket-sharp/Logger.cs new file mode 100644 index 00000000..a280ce43 --- /dev/null +++ b/Assets/External/websocket-sharp/Logger.cs @@ -0,0 +1,345 @@ +#region License +/* + * Logger.cs + * + * The MIT License + * + * Copyright (c) 2013-2024 sta.blockhead + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#endregion + +using System; +using System.Diagnostics; +using System.IO; + +namespace WebSocketSharp +{ + /// + /// Provides a set of methods and properties for logging. + /// + /// + /// + /// If you output a log with lower than the current logging level, + /// it cannot be outputted. + /// + /// + /// The default output method writes a log to the standard output + /// stream and the text file if it has a valid path. + /// + /// + /// If you would like to use the custom output method, you should + /// specify it with the constructor or the + /// property. + /// + /// + public class Logger + { + #region Private Fields + + private volatile string _file; + private volatile LogLevel _level; + private Action _output; + private object _sync; + + #endregion + + #region Public Constructors + + /// + /// Initializes a new instance of the class. + /// + /// + /// This constructor initializes the logging level with the Error level. + /// + public Logger () + : this (LogLevel.Error, null, null) + { + } + + /// + /// Initializes a new instance of the class with + /// the specified logging level. + /// + /// + /// One of the enum values that specifies + /// the logging level. + /// + public Logger (LogLevel level) + : this (level, null, null) + { + } + + /// + /// Initializes a new instance of the class with + /// the specified logging level, path to the log file, and delegate + /// used to output a log. + /// + /// + /// One of the enum values that specifies + /// the logging level. + /// + /// + /// A that specifies the path to the log file. + /// + /// + /// An that specifies + /// the delegate used to output a log. + /// + public Logger (LogLevel level, string file, Action output) + { + _level = level; + _file = file; + _output = output ?? defaultOutput; + + _sync = new object (); + } + + #endregion + + #region Public Properties + + /// + /// Gets or sets the path to the log file. + /// + /// + /// A that represents the path to the log file if any. + /// + public string File { + get { + return _file; + } + + set { + lock (_sync) + _file = value; + } + } + + /// + /// Gets or sets the current logging level. + /// + /// + /// A log with lower than the value of this property cannot be outputted. + /// + /// + /// + /// One of the enum values. + /// + /// + /// It represents the current logging level. + /// + /// + public LogLevel Level { + get { + return _level; + } + + set { + lock (_sync) + _level = value; + } + } + + /// + /// Gets or sets the delegate used to output a log. + /// + /// + /// + /// An delegate. + /// + /// + /// It represents the delegate called when the logger outputs a log. + /// + /// + /// The string parameter passed to the delegate is the value of + /// the property. + /// + /// + /// If the value to set is , the default + /// output method is set. + /// + /// + public Action Output { + get { + return _output; + } + + set { + lock (_sync) + _output = value ?? defaultOutput; + } + } + + #endregion + + #region Private Methods + + private static void defaultOutput (LogData data, string path) + { + var val = data.ToString (); + + Console.WriteLine (val); + + if (path != null && path.Length > 0) + writeToFile (val, path); + } + + private void output (string message, LogLevel level) + { + lock (_sync) { + if (_level > level) + return; + + try { + var data = new LogData (level, new StackFrame (2, true), message); + + _output (data, _file); + } + catch (Exception ex) { + var data = new LogData ( + LogLevel.Fatal, + new StackFrame (0, true), + ex.Message + ); + + Console.WriteLine (data.ToString ()); + } + } + } + + private static void writeToFile (string value, string path) + { + using (var writer = new StreamWriter (path, true)) + using (var syncWriter = TextWriter.Synchronized (writer)) + syncWriter.WriteLine (value); + } + + #endregion + + #region Public Methods + + /// + /// Outputs the specified message as a log with the Debug level. + /// + /// + /// If the current logging level is higher than the Debug level, + /// this method does not output the message as a log. + /// + /// + /// A that specifies the message to output. + /// + public void Debug (string message) + { + if (_level > LogLevel.Debug) + return; + + output (message, LogLevel.Debug); + } + + /// + /// Outputs the specified message as a log with the Error level. + /// + /// + /// If the current logging level is higher than the Error level, + /// this method does not output the message as a log. + /// + /// + /// A that specifies the message to output. + /// + public void Error (string message) + { + if (_level > LogLevel.Error) + return; + + output (message, LogLevel.Error); + } + + /// + /// Outputs the specified message as a log with the Fatal level. + /// + /// + /// A that specifies the message to output. + /// + public void Fatal (string message) + { + if (_level > LogLevel.Fatal) + return; + + output (message, LogLevel.Fatal); + } + + /// + /// Outputs the specified message as a log with the Info level. + /// + /// + /// If the current logging level is higher than the Info level, + /// this method does not output the message as a log. + /// + /// + /// A that specifies the message to output. + /// + public void Info (string message) + { + if (_level > LogLevel.Info) + return; + + output (message, LogLevel.Info); + } + + /// + /// Outputs the specified message as a log with the Trace level. + /// + /// + /// If the current logging level is higher than the Trace level, + /// this method does not output the message as a log. + /// + /// + /// A that specifies the message to output. + /// + public void Trace (string message) + { + if (_level > LogLevel.Trace) + return; + + output (message, LogLevel.Trace); + } + + /// + /// Outputs the specified message as a log with the Warn level. + /// + /// + /// If the current logging level is higher than the Warn level, + /// this method does not output the message as a log. + /// + /// + /// A that specifies the message to output. + /// + public void Warn (string message) + { + if (_level > LogLevel.Warn) + return; + + output (message, LogLevel.Warn); + } + + #endregion + } +} diff --git a/Assets/External/websocket-sharp/Logger.cs.meta b/Assets/External/websocket-sharp/Logger.cs.meta new file mode 100644 index 00000000..3194b864 --- /dev/null +++ b/Assets/External/websocket-sharp/Logger.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 3af5c1735c57c5944acd190ccf689365 \ No newline at end of file diff --git a/Assets/External/websocket-sharp/Mask.cs b/Assets/External/websocket-sharp/Mask.cs new file mode 100644 index 00000000..2958f5a8 --- /dev/null +++ b/Assets/External/websocket-sharp/Mask.cs @@ -0,0 +1,52 @@ +#region License +/* + * Mask.cs + * + * The MIT License + * + * Copyright (c) 2012-2025 sta.blockhead + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#endregion + +using System; + +namespace WebSocketSharp +{ + /// + /// Indicates whether the payload data of a WebSocket frame is masked. + /// + /// + /// The values of this enumeration are defined in + /// + /// Section 5.2 of RFC 6455. + /// + internal enum Mask + { + /// + /// Equivalent to numeric value 0. Indicates not masked. + /// + Off = 0x0, + /// + /// Equivalent to numeric value 1. Indicates masked. + /// + On = 0x1 + } +} diff --git a/Assets/External/websocket-sharp/Mask.cs.meta b/Assets/External/websocket-sharp/Mask.cs.meta new file mode 100644 index 00000000..a7399cb5 --- /dev/null +++ b/Assets/External/websocket-sharp/Mask.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: e96ce8210fac01d43aab00d3ee6e24d9 \ No newline at end of file diff --git a/Assets/External/websocket-sharp/MessageEventArgs.cs b/Assets/External/websocket-sharp/MessageEventArgs.cs new file mode 100644 index 00000000..63add90f --- /dev/null +++ b/Assets/External/websocket-sharp/MessageEventArgs.cs @@ -0,0 +1,192 @@ +#region License +/* + * MessageEventArgs.cs + * + * The MIT License + * + * Copyright (c) 2012-2022 sta.blockhead + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#endregion + +using System; + +namespace WebSocketSharp +{ + /// + /// Represents the event data for the event. + /// + /// + /// + /// The message event occurs when the interface + /// receives a message or a ping if the + /// property is set to true. + /// + /// + /// If you would like to get the message data, you should access + /// the or property. + /// + /// + public class MessageEventArgs : EventArgs + { + #region Private Fields + + private string _data; + private bool _dataSet; + private Opcode _opcode; + private byte[] _rawData; + + #endregion + + #region Internal Constructors + + internal MessageEventArgs (WebSocketFrame frame) + { + _opcode = frame.Opcode; + _rawData = frame.PayloadData.ApplicationData; + } + + internal MessageEventArgs (Opcode opcode, byte[] rawData) + { + if ((ulong) rawData.LongLength > PayloadData.MaxLength) + throw new WebSocketException (CloseStatusCode.TooBig); + + _opcode = opcode; + _rawData = rawData; + } + + #endregion + + #region Internal Properties + + /// + /// Gets the opcode for the message. + /// + /// + /// , , + /// or . + /// + internal Opcode Opcode { + get { + return _opcode; + } + } + + #endregion + + #region Public Properties + + /// + /// Gets the message data as a . + /// + /// + /// + /// A that represents the message data + /// if the message type is text or ping. + /// + /// + /// if the message type is binary or + /// the message data could not be UTF-8-decoded. + /// + /// + public string Data { + get { + setData (); + + return _data; + } + } + + /// + /// Gets a value indicating whether the message type is binary. + /// + /// + /// true if the message type is binary; otherwise, false. + /// + public bool IsBinary { + get { + return _opcode == Opcode.Binary; + } + } + + /// + /// Gets a value indicating whether the message type is ping. + /// + /// + /// true if the message type is ping; otherwise, false. + /// + public bool IsPing { + get { + return _opcode == Opcode.Ping; + } + } + + /// + /// Gets a value indicating whether the message type is text. + /// + /// + /// true if the message type is text; otherwise, false. + /// + public bool IsText { + get { + return _opcode == Opcode.Text; + } + } + + /// + /// Gets the message data as an array of . + /// + /// + /// An array of that represents the message data. + /// + public byte[] RawData { + get { + setData (); + + return _rawData; + } + } + + #endregion + + #region Private Methods + + private void setData () + { + if (_dataSet) + return; + + if (_opcode == Opcode.Binary) { + _dataSet = true; + + return; + } + + string data; + + if (_rawData.TryGetUTF8DecodedString (out data)) + _data = data; + + _dataSet = true; + } + + #endregion + } +} diff --git a/Assets/External/websocket-sharp/MessageEventArgs.cs.meta b/Assets/External/websocket-sharp/MessageEventArgs.cs.meta new file mode 100644 index 00000000..f87be3b6 --- /dev/null +++ b/Assets/External/websocket-sharp/MessageEventArgs.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: b9ecfe7845aca0f47b82eb3bf34eeebc \ No newline at end of file diff --git a/Assets/External/websocket-sharp/Net.meta b/Assets/External/websocket-sharp/Net.meta new file mode 100644 index 00000000..385160bc --- /dev/null +++ b/Assets/External/websocket-sharp/Net.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: c24f6cb4d39821c41a0119a5c2052211 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/External/websocket-sharp/Net/AuthenticationChallenge.cs b/Assets/External/websocket-sharp/Net/AuthenticationChallenge.cs new file mode 100644 index 00000000..72674080 --- /dev/null +++ b/Assets/External/websocket-sharp/Net/AuthenticationChallenge.cs @@ -0,0 +1,280 @@ +#region License +/* + * AuthenticationChallenge.cs + * + * The MIT License + * + * Copyright (c) 2013-2024 sta.blockhead + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#endregion + +using System; +using System.Collections.Specialized; +using System.Text; + +namespace WebSocketSharp.Net +{ + internal class AuthenticationChallenge + { + #region Private Fields + + private NameValueCollection _parameters; + private AuthenticationSchemes _scheme; + + #endregion + + #region Private Constructors + + private AuthenticationChallenge ( + AuthenticationSchemes scheme, + NameValueCollection parameters + ) + { + _scheme = scheme; + _parameters = parameters; + } + + #endregion + + #region Internal Constructors + + internal AuthenticationChallenge ( + AuthenticationSchemes scheme, + string realm + ) + : this (scheme, new NameValueCollection ()) + { + _parameters["realm"] = realm; + + if (scheme == AuthenticationSchemes.Digest) { + _parameters["nonce"] = CreateNonceValue (); + _parameters["algorithm"] = "MD5"; + _parameters["qop"] = "auth"; + } + } + + #endregion + + #region Internal Properties + + internal NameValueCollection Parameters { + get { + return _parameters; + } + } + + #endregion + + #region Public Properties + + public string Algorithm { + get { + return _parameters["algorithm"]; + } + } + + public string Domain { + get { + return _parameters["domain"]; + } + } + + public string Nonce { + get { + return _parameters["nonce"]; + } + } + + public string Opaque { + get { + return _parameters["opaque"]; + } + } + + public string Qop { + get { + return _parameters["qop"]; + } + } + + public string Realm { + get { + return _parameters["realm"]; + } + } + + public AuthenticationSchemes Scheme { + get { + return _scheme; + } + } + + public string Stale { + get { + return _parameters["stale"]; + } + } + + #endregion + + #region Internal Methods + + internal static AuthenticationChallenge CreateBasicChallenge (string realm) + { + return new AuthenticationChallenge (AuthenticationSchemes.Basic, realm); + } + + internal static AuthenticationChallenge CreateDigestChallenge (string realm) + { + return new AuthenticationChallenge (AuthenticationSchemes.Digest, realm); + } + + internal static string CreateNonceValue () + { + var rand = new Random (); + var bytes = new byte[16]; + + rand.NextBytes (bytes); + + var buff = new StringBuilder (32); + + foreach (var b in bytes) + buff.Append (b.ToString ("x2")); + + return buff.ToString (); + } + + internal static AuthenticationChallenge Parse (string value) + { + var chal = value.Split (new[] { ' ' }, 2); + + if (chal.Length != 2) + return null; + + var schm = chal[0].ToLower (); + + if (schm == "basic") { + var parameters = ParseParameters (chal[1]); + + return new AuthenticationChallenge ( + AuthenticationSchemes.Basic, + parameters + ); + } + + if (schm == "digest") { + var parameters = ParseParameters (chal[1]); + + return new AuthenticationChallenge ( + AuthenticationSchemes.Digest, + parameters + ); + } + + return null; + } + + internal static NameValueCollection ParseParameters (string value) + { + var ret = new NameValueCollection (); + + foreach (var param in value.SplitHeaderValue (',')) { + var i = param.IndexOf ('='); + + var name = i > 0 ? param.Substring (0, i).Trim () : null; + var val = i < 0 + ? param.Trim ().Trim ('"') + : i < param.Length - 1 + ? param.Substring (i + 1).Trim ().Trim ('"') + : String.Empty; + + ret.Add (name, val); + } + + return ret; + } + + internal string ToBasicString () + { + return String.Format ("Basic realm=\"{0}\"", _parameters["realm"]); + } + + internal string ToDigestString () + { + var buff = new StringBuilder (128); + + var domain = _parameters["domain"]; + var realm = _parameters["realm"]; + var nonce = _parameters["nonce"]; + + if (domain != null) { + buff.AppendFormat ( + "Digest realm=\"{0}\", domain=\"{1}\", nonce=\"{2}\"", + realm, + domain, + nonce + ); + } + else { + buff.AppendFormat ("Digest realm=\"{0}\", nonce=\"{1}\"", realm, nonce); + } + + var opaque = _parameters["opaque"]; + + if (opaque != null) + buff.AppendFormat (", opaque=\"{0}\"", opaque); + + var stale = _parameters["stale"]; + + if (stale != null) + buff.AppendFormat (", stale={0}", stale); + + var algo = _parameters["algorithm"]; + + if (algo != null) + buff.AppendFormat (", algorithm={0}", algo); + + var qop = _parameters["qop"]; + + if (qop != null) + buff.AppendFormat (", qop=\"{0}\"", qop); + + return buff.ToString (); + } + + #endregion + + #region Public Methods + + public override string ToString () + { + if (_scheme == AuthenticationSchemes.Basic) + return ToBasicString (); + + if (_scheme == AuthenticationSchemes.Digest) + return ToDigestString (); + + return String.Empty; + } + + #endregion + } +} diff --git a/Assets/External/websocket-sharp/Net/AuthenticationChallenge.cs.meta b/Assets/External/websocket-sharp/Net/AuthenticationChallenge.cs.meta new file mode 100644 index 00000000..db5ab960 --- /dev/null +++ b/Assets/External/websocket-sharp/Net/AuthenticationChallenge.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 84c0bed85decb0b479b4e5b420f1285a \ No newline at end of file diff --git a/Assets/External/websocket-sharp/Net/AuthenticationResponse.cs b/Assets/External/websocket-sharp/Net/AuthenticationResponse.cs new file mode 100644 index 00000000..28fbd223 --- /dev/null +++ b/Assets/External/websocket-sharp/Net/AuthenticationResponse.cs @@ -0,0 +1,464 @@ +#region License +/* + * AuthenticationResponse.cs + * + * The ParseBasicCredentials method is derived from HttpListenerContext.cs + * (System.Net) of Mono (http://www.mono-project.com). + * + * The MIT License + * + * Copyright (c) 2005 Novell, Inc. (http://www.novell.com) + * Copyright (c) 2013-2024 sta.blockhead + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#endregion + +using System; +using System.Collections.Specialized; +using System.Security.Cryptography; +using System.Security.Principal; +using System.Text; + +namespace WebSocketSharp.Net +{ + internal class AuthenticationResponse + { + #region Private Fields + + private uint _nonceCount; + private NameValueCollection _parameters; + private AuthenticationSchemes _scheme; + + #endregion + + #region Private Constructors + + private AuthenticationResponse ( + AuthenticationSchemes scheme, + NameValueCollection parameters + ) + { + _scheme = scheme; + _parameters = parameters; + } + + #endregion + + #region Internal Constructors + + internal AuthenticationResponse (NetworkCredential credentials) + : this ( + AuthenticationSchemes.Basic, + new NameValueCollection (), + credentials, + 0 + ) + { + } + + internal AuthenticationResponse ( + AuthenticationChallenge challenge, + NetworkCredential credentials, + uint nonceCount + ) + : this (challenge.Scheme, challenge.Parameters, credentials, nonceCount) + { + } + + internal AuthenticationResponse ( + AuthenticationSchemes scheme, + NameValueCollection parameters, + NetworkCredential credentials, + uint nonceCount + ) + : this (scheme, parameters) + { + _parameters["username"] = credentials.Username; + _parameters["password"] = credentials.Password; + _parameters["uri"] = credentials.Domain; + _nonceCount = nonceCount; + + if (scheme == AuthenticationSchemes.Digest) + initAsDigest (); + } + + #endregion + + #region Internal Properties + + internal uint NonceCount { + get { + return _nonceCount < UInt32.MaxValue ? _nonceCount : 0; + } + } + + internal NameValueCollection Parameters { + get { + return _parameters; + } + } + + #endregion + + #region Public Properties + + public string Algorithm { + get { + return _parameters["algorithm"]; + } + } + + public string Cnonce { + get { + return _parameters["cnonce"]; + } + } + + public string Nc { + get { + return _parameters["nc"]; + } + } + + public string Nonce { + get { + return _parameters["nonce"]; + } + } + + public string Opaque { + get { + return _parameters["opaque"]; + } + } + + public string Password { + get { + return _parameters["password"]; + } + } + + public string Qop { + get { + return _parameters["qop"]; + } + } + + public string Realm { + get { + return _parameters["realm"]; + } + } + + public string Response { + get { + return _parameters["response"]; + } + } + + public AuthenticationSchemes Scheme { + get { + return _scheme; + } + } + + public string Uri { + get { + return _parameters["uri"]; + } + } + + public string UserName { + get { + return _parameters["username"]; + } + } + + #endregion + + #region Private Methods + + private static string createA1 ( + string username, + string password, + string realm + ) + { + return String.Format ("{0}:{1}:{2}", username, realm, password); + } + + private static string createA1 ( + string username, + string password, + string realm, + string nonce, + string cnonce + ) + { + var a1 = createA1 (username, password, realm); + + return String.Format ("{0}:{1}:{2}", hash (a1), nonce, cnonce); + } + + private static string createA2 (string method, string uri) + { + return String.Format ("{0}:{1}", method, uri); + } + + private static string createA2 (string method, string uri, string entity) + { + return String.Format ("{0}:{1}:{2}", method, uri, hash (entity)); + } + + private static string hash (string value) + { + var buff = new StringBuilder (64); + + var md5 = MD5.Create (); + var bytes = Encoding.UTF8.GetBytes (value); + var res = md5.ComputeHash (bytes); + + foreach (var b in res) + buff.Append (b.ToString ("x2")); + + return buff.ToString (); + } + + private void initAsDigest () + { + var qops = _parameters["qop"]; + + if (qops != null) { + var hasAuth = qops.Split (',').Contains ( + qop => qop.Trim ().ToLower () == "auth" + ); + + if (hasAuth) { + _parameters["qop"] = "auth"; + _parameters["cnonce"] = AuthenticationChallenge.CreateNonceValue (); + _parameters["nc"] = String.Format ("{0:x8}", ++_nonceCount); + } + else { + _parameters["qop"] = null; + } + } + + _parameters["method"] = "GET"; + _parameters["response"] = CreateRequestDigest (_parameters); + } + + #endregion + + #region Internal Methods + + internal static string CreateRequestDigest (NameValueCollection parameters) + { + var uname = parameters["username"]; + var passwd = parameters["password"]; + var realm = parameters["realm"]; + var nonce = parameters["nonce"]; + var uri = parameters["uri"]; + var algo = parameters["algorithm"]; + var qop = parameters["qop"]; + var cnonce = parameters["cnonce"]; + var nc = parameters["nc"]; + var method = parameters["method"]; + + var a1 = algo != null && algo.ToLower () == "md5-sess" + ? createA1 (uname, passwd, realm, nonce, cnonce) + : createA1 (uname, passwd, realm); + + var a2 = qop != null && qop.ToLower () == "auth-int" + ? createA2 (method, uri, parameters["entity"]) + : createA2 (method, uri); + + var secret = hash (a1); + var data = qop != null + ? String.Format ( + "{0}:{1}:{2}:{3}:{4}", + nonce, + nc, + cnonce, + qop, + hash (a2) + ) + : String.Format ("{0}:{1}", nonce, hash (a2)); + + var keyed = String.Format ("{0}:{1}", secret, data); + + return hash (keyed); + } + + internal static AuthenticationResponse Parse (string value) + { + try { + var cred = value.Split (new[] { ' ' }, 2); + + if (cred.Length != 2) + return null; + + var schm = cred[0].ToLower (); + + if (schm == "basic") { + var parameters = ParseBasicCredentials (cred[1]); + + return new AuthenticationResponse ( + AuthenticationSchemes.Basic, + parameters + ); + } + + if (schm == "digest") { + var parameters = AuthenticationChallenge.ParseParameters (cred[1]); + + return new AuthenticationResponse ( + AuthenticationSchemes.Digest, + parameters + ); + } + + return null; + } + catch { + return null; + } + } + + internal static NameValueCollection ParseBasicCredentials (string value) + { + var ret = new NameValueCollection (); + + // Decode the basic-credentials (a Base64 encoded string). + + var bytes = Convert.FromBase64String (value); + var userPass = Encoding.UTF8.GetString (bytes); + + // The format is [\]:. + + var idx = userPass.IndexOf (':'); + var uname = userPass.Substring (0, idx); + var passwd = idx < userPass.Length - 1 + ? userPass.Substring (idx + 1) + : String.Empty; + + // Check if exists. + + idx = uname.IndexOf ('\\'); + + if (idx > -1) + uname = uname.Substring (idx + 1); + + ret["username"] = uname; + ret["password"] = passwd; + + return ret; + } + + internal string ToBasicString () + { + var uname = _parameters["username"]; + var passwd = _parameters["password"]; + var userPass = String.Format ("{0}:{1}", uname, passwd); + + var bytes = Encoding.UTF8.GetBytes (userPass); + var cred = Convert.ToBase64String (bytes); + + return "Basic " + cred; + } + + internal string ToDigestString () + { + var buff = new StringBuilder (256); + + var uname = _parameters["username"]; + var realm = _parameters["realm"]; + var nonce = _parameters["nonce"]; + var uri = _parameters["uri"]; + var res = _parameters["response"]; + + buff.AppendFormat ( + "Digest username=\"{0}\", realm=\"{1}\", nonce=\"{2}\", uri=\"{3}\", response=\"{4}\"", + uname, + realm, + nonce, + uri, + res + ); + + var opaque = _parameters["opaque"]; + + if (opaque != null) + buff.AppendFormat (", opaque=\"{0}\"", opaque); + + var algo = _parameters["algorithm"]; + + if (algo != null) + buff.AppendFormat (", algorithm={0}", algo); + + var qop = _parameters["qop"]; + + if (qop != null) { + var cnonce = _parameters["cnonce"]; + var nc = _parameters["nc"]; + + buff.AppendFormat ( + ", qop={0}, cnonce=\"{1}\", nc={2}", + qop, + cnonce, + nc + ); + } + + return buff.ToString (); + } + + #endregion + + #region Public Methods + + public IIdentity ToIdentity () + { + if (_scheme == AuthenticationSchemes.Basic) { + var uname = _parameters["username"]; + var passwd = _parameters["password"]; + + return new HttpBasicIdentity (uname, passwd); + } + + if (_scheme == AuthenticationSchemes.Digest) + return new HttpDigestIdentity (_parameters); + + return null; + } + + public override string ToString () + { + if (_scheme == AuthenticationSchemes.Basic) + return ToBasicString (); + + if (_scheme == AuthenticationSchemes.Digest) + return ToDigestString (); + + return String.Empty; + } + + #endregion + } +} diff --git a/Assets/External/websocket-sharp/Net/AuthenticationResponse.cs.meta b/Assets/External/websocket-sharp/Net/AuthenticationResponse.cs.meta new file mode 100644 index 00000000..c25cc51b --- /dev/null +++ b/Assets/External/websocket-sharp/Net/AuthenticationResponse.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: d47f8aed92f4d1d499acfb1f91908f39 \ No newline at end of file diff --git a/Assets/External/websocket-sharp/Net/AuthenticationSchemes.cs b/Assets/External/websocket-sharp/Net/AuthenticationSchemes.cs new file mode 100644 index 00000000..ab7721a1 --- /dev/null +++ b/Assets/External/websocket-sharp/Net/AuthenticationSchemes.cs @@ -0,0 +1,66 @@ +#region License +/* + * AuthenticationSchemes.cs + * + * This code is derived from AuthenticationSchemes.cs (System.Net) of Mono + * (http://www.mono-project.com). + * + * The MIT License + * + * Copyright (c) 2005 Novell, Inc. (http://www.novell.com) + * Copyright (c) 2012-2016 sta.blockhead + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#endregion + +#region Authors +/* + * Authors: + * - Atsushi Enomoto + */ +#endregion + +using System; + +namespace WebSocketSharp.Net +{ + /// + /// Specifies the scheme for authentication. + /// + public enum AuthenticationSchemes + { + /// + /// No authentication is allowed. + /// + None, + /// + /// Specifies digest authentication. + /// + Digest = 1, + /// + /// Specifies basic authentication. + /// + Basic = 8, + /// + /// Specifies anonymous authentication. + /// + Anonymous = 0x8000 + } +} diff --git a/Assets/External/websocket-sharp/Net/AuthenticationSchemes.cs.meta b/Assets/External/websocket-sharp/Net/AuthenticationSchemes.cs.meta new file mode 100644 index 00000000..b8871de7 --- /dev/null +++ b/Assets/External/websocket-sharp/Net/AuthenticationSchemes.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: d5a2c71d0b1e8a64ebd26b4420e70d3c \ No newline at end of file diff --git a/Assets/External/websocket-sharp/Net/Chunk.cs b/Assets/External/websocket-sharp/Net/Chunk.cs new file mode 100644 index 00000000..9ed28f86 --- /dev/null +++ b/Assets/External/websocket-sharp/Net/Chunk.cs @@ -0,0 +1,93 @@ +#region License +/* + * Chunk.cs + * + * This code is derived from ChunkStream.cs (System.Net) of Mono + * (http://www.mono-project.com). + * + * The MIT License + * + * Copyright (c) 2003 Ximian, Inc (http://www.ximian.com) + * Copyright (c) 2014-2021 sta.blockhead + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#endregion + +#region Authors +/* + * Authors: + * - Gonzalo Paniagua Javier + */ +#endregion + +using System; + +namespace WebSocketSharp.Net +{ + internal class Chunk + { + #region Private Fields + + private byte[] _data; + private int _offset; + + #endregion + + #region Public Constructors + + public Chunk (byte[] data) + { + _data = data; + } + + #endregion + + #region Public Properties + + public int ReadLeft { + get { + return _data.Length - _offset; + } + } + + #endregion + + #region Public Methods + + public int Read (byte[] buffer, int offset, int count) + { + var left = _data.Length - _offset; + + if (left == 0) + return 0; + + if (count > left) + count = left; + + Buffer.BlockCopy (_data, _offset, buffer, offset, count); + + _offset += count; + + return count; + } + + #endregion + } +} diff --git a/Assets/External/websocket-sharp/Net/Chunk.cs.meta b/Assets/External/websocket-sharp/Net/Chunk.cs.meta new file mode 100644 index 00000000..42a01b78 --- /dev/null +++ b/Assets/External/websocket-sharp/Net/Chunk.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 14a0fcae05dbbdc4ea0cdca9bc630d0f \ No newline at end of file diff --git a/Assets/External/websocket-sharp/Net/ChunkStream.cs b/Assets/External/websocket-sharp/Net/ChunkStream.cs new file mode 100644 index 00000000..3de4374d --- /dev/null +++ b/Assets/External/websocket-sharp/Net/ChunkStream.cs @@ -0,0 +1,429 @@ +#region License +/* + * ChunkStream.cs + * + * This code is derived from ChunkStream.cs (System.Net) of Mono + * (http://www.mono-project.com). + * + * The MIT License + * + * Copyright (c) 2003 Ximian, Inc (http://www.ximian.com) + * Copyright (c) 2012-2023 sta.blockhead + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#endregion + +#region Authors +/* + * Authors: + * - Gonzalo Paniagua Javier + */ +#endregion + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Net; +using System.Text; + +namespace WebSocketSharp.Net +{ + internal class ChunkStream + { + #region Private Fields + + private int _chunkRead; + private int _chunkSize; + private List _chunks; + private int _count; + private byte[] _endBuffer; + private bool _gotIt; + private WebHeaderCollection _headers; + private int _offset; + private StringBuilder _saved; + private bool _sawCr; + private InputChunkState _state; + private int _trailerState; + + #endregion + + #region Public Constructors + + public ChunkStream (WebHeaderCollection headers) + { + _headers = headers; + + _chunkSize = -1; + _chunks = new List (); + _saved = new StringBuilder (); + } + + #endregion + + #region Internal Properties + + internal int Count { + get { + return _count; + } + } + + internal byte[] EndBuffer { + get { + return _endBuffer; + } + } + + internal int Offset { + get { + return _offset; + } + } + + #endregion + + #region Public Properties + + public WebHeaderCollection Headers { + get { + return _headers; + } + } + + public bool WantsMore { + get { + return _state < InputChunkState.End; + } + } + + #endregion + + #region Private Methods + + private int read (byte[] buffer, int offset, int count) + { + var nread = 0; + var cnt = _chunks.Count; + + for (var i = 0; i < cnt; i++) { + var chunk = _chunks[i]; + + if (chunk == null) + continue; + + if (chunk.ReadLeft == 0) { + _chunks[i] = null; + + continue; + } + + nread += chunk.Read (buffer, offset + nread, count - nread); + + if (nread == count) + break; + } + + return nread; + } + + private InputChunkState seekCrLf (byte[] buffer, ref int offset, int length) + { + if (!_sawCr) { + if (buffer[offset++] != 13) + throwProtocolViolation ("CR is expected."); + + _sawCr = true; + + if (offset == length) + return InputChunkState.DataEnded; + } + + if (buffer[offset++] != 10) + throwProtocolViolation ("LF is expected."); + + return InputChunkState.None; + } + + private InputChunkState setChunkSize ( + byte[] buffer, + ref int offset, + int length + ) + { + byte b = 0; + + while (offset < length) { + b = buffer[offset++]; + + if (_sawCr) { + if (b != 10) + throwProtocolViolation ("LF is expected."); + + break; + } + + if (b == 13) { + _sawCr = true; + + continue; + } + + if (b == 10) + throwProtocolViolation ("LF is unexpected."); + + if (_gotIt) + continue; + + if (b == 32 || b == 59) { // SP or ';' + _gotIt = true; + + continue; + } + + _saved.Append ((char) b); + } + + if (_saved.Length > 20) + throwProtocolViolation ("The chunk size is too big."); + + if (b != 10) + return InputChunkState.None; + + var s = _saved.ToString (); + + try { + _chunkSize = Int32.Parse (s, NumberStyles.HexNumber); + } + catch { + throwProtocolViolation ("The chunk size cannot be parsed."); + } + + _chunkRead = 0; + + if (_chunkSize == 0) { + _trailerState = 2; + + return InputChunkState.Trailer; + } + + return InputChunkState.Data; + } + + private InputChunkState setTrailer ( + byte[] buffer, + ref int offset, + int length + ) + { + while (offset < length) { + if (_trailerState == 4) // CR LF CR LF + break; + + var b = buffer[offset++]; + + _saved.Append ((char) b); + + if (_trailerState == 1 || _trailerState == 3) { // CR or CR LF CR + if (b != 10) + throwProtocolViolation ("LF is expected."); + + _trailerState++; + + continue; + } + + if (b == 13) { + _trailerState++; + + continue; + } + + if (b == 10) + throwProtocolViolation ("LF is unexpected."); + + _trailerState = 0; + } + + var len = _saved.Length; + + if (len > 4196) + throwProtocolViolation ("The trailer is too long."); + + if (_trailerState < 4) + return InputChunkState.Trailer; + + if (len == 2) + return InputChunkState.End; + + _saved.Length = len - 2; + + var val = _saved.ToString (); + var reader = new StringReader (val); + + while (true) { + var line = reader.ReadLine (); + + if (line == null || line.Length == 0) + break; + + _headers.Add (line); + } + + return InputChunkState.End; + } + + private static void throwProtocolViolation (string message) + { + throw new WebException ( + message, + null, + WebExceptionStatus.ServerProtocolViolation, + null + ); + } + + private void write (byte[] buffer, int offset, int length) + { + if (_state == InputChunkState.End) + throwProtocolViolation ("The chunks were ended."); + + if (_state == InputChunkState.None) { + _state = setChunkSize (buffer, ref offset, length); + + if (_state == InputChunkState.None) + return; + + _saved.Length = 0; + _sawCr = false; + _gotIt = false; + } + + if (_state == InputChunkState.Data) { + if (offset >= length) + return; + + _state = writeData (buffer, ref offset, length); + + if (_state == InputChunkState.Data) + return; + } + + if (_state == InputChunkState.DataEnded) { + if (offset >= length) + return; + + _state = seekCrLf (buffer, ref offset, length); + + if (_state == InputChunkState.DataEnded) + return; + + _sawCr = false; + } + + if (_state == InputChunkState.Trailer) { + if (offset >= length) + return; + + _state = setTrailer (buffer, ref offset, length); + + if (_state == InputChunkState.Trailer) + return; + + _saved.Length = 0; + } + + if (_state == InputChunkState.End) { + _endBuffer = buffer; + _offset = offset; + _count = length - offset; + + return; + } + + if (offset >= length) + return; + + write (buffer, offset, length); + } + + private InputChunkState writeData ( + byte[] buffer, + ref int offset, + int length + ) + { + var cnt = length - offset; + var left = _chunkSize - _chunkRead; + + if (cnt > left) + cnt = left; + + var data = new byte[cnt]; + + Buffer.BlockCopy (buffer, offset, data, 0, cnt); + + var chunk = new Chunk (data); + + _chunks.Add (chunk); + + offset += cnt; + _chunkRead += cnt; + + return _chunkRead == _chunkSize + ? InputChunkState.DataEnded + : InputChunkState.Data; + } + + #endregion + + #region Internal Methods + + internal void ResetChunkStore () + { + _chunkRead = 0; + _chunkSize = -1; + + _chunks.Clear (); + } + + #endregion + + #region Public Methods + + public int Read (byte[] buffer, int offset, int count) + { + if (count <= 0) + return 0; + + return read (buffer, offset, count); + } + + public void Write (byte[] buffer, int offset, int count) + { + if (count <= 0) + return; + + write (buffer, offset, offset + count); + } + + #endregion + } +} diff --git a/Assets/External/websocket-sharp/Net/ChunkStream.cs.meta b/Assets/External/websocket-sharp/Net/ChunkStream.cs.meta new file mode 100644 index 00000000..b972d7eb --- /dev/null +++ b/Assets/External/websocket-sharp/Net/ChunkStream.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 1f64dddc3b0d0f049af97629c42dca44 \ No newline at end of file diff --git a/Assets/External/websocket-sharp/Net/ChunkedRequestStream.cs b/Assets/External/websocket-sharp/Net/ChunkedRequestStream.cs new file mode 100644 index 00000000..f4a58392 --- /dev/null +++ b/Assets/External/websocket-sharp/Net/ChunkedRequestStream.cs @@ -0,0 +1,283 @@ +#region License +/* + * ChunkedRequestStream.cs + * + * This code is derived from ChunkedInputStream.cs (System.Net) of Mono + * (http://www.mono-project.com). + * + * The MIT License + * + * Copyright (c) 2005 Novell, Inc. (http://www.novell.com) + * Copyright (c) 2012-2023 sta.blockhead + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#endregion + +#region Authors +/* + * Authors: + * - Gonzalo Paniagua Javier + */ +#endregion + +using System; +using System.IO; + +namespace WebSocketSharp.Net +{ + internal class ChunkedRequestStream : RequestStream + { + #region Private Fields + + private static readonly int _bufferLength; + private HttpListenerContext _context; + private ChunkStream _decoder; + private bool _disposed; + private bool _noMoreData; + + #endregion + + #region Static Constructor + + static ChunkedRequestStream () + { + _bufferLength = 8192; + } + + #endregion + + #region Internal Constructors + + internal ChunkedRequestStream ( + Stream innerStream, + byte[] initialBuffer, + int offset, + int count, + HttpListenerContext context + ) + : base (innerStream, initialBuffer, offset, count, -1) + { + _context = context; + + _decoder = new ChunkStream ( + (WebHeaderCollection) context.Request.Headers + ); + } + + #endregion + + #region Internal Properties + + internal bool HasRemainingBuffer { + get { + return _decoder.Count + Count > 0; + } + } + + internal byte[] RemainingBuffer { + get { + using (var buff = new MemoryStream ()) { + var cnt = _decoder.Count; + + if (cnt > 0) + buff.Write (_decoder.EndBuffer, _decoder.Offset, cnt); + + cnt = Count; + + if (cnt > 0) + buff.Write (InitialBuffer, Offset, cnt); + + buff.Close (); + + return buff.ToArray (); + } + } + } + + #endregion + + #region Private Methods + + private void onRead (IAsyncResult asyncResult) + { + var rstate = (ReadBufferState) asyncResult.AsyncState; + var ares = rstate.AsyncResult; + + try { + var nread = base.EndRead (asyncResult); + + _decoder.Write (ares.Buffer, ares.Offset, nread); + + nread = _decoder.Read (rstate.Buffer, rstate.Offset, rstate.Count); + + rstate.Offset += nread; + rstate.Count -= nread; + + if (rstate.Count == 0 || !_decoder.WantsMore || nread == 0) { + _noMoreData = !_decoder.WantsMore && nread == 0; + + ares.Count = rstate.InitialCount - rstate.Count; + + ares.Complete (); + + return; + } + + base.BeginRead (ares.Buffer, ares.Offset, ares.Count, onRead, rstate); + } + catch (Exception ex) { + _context.ErrorMessage = "I/O operation aborted"; + + _context.SendError (); + + ares.Complete (ex); + } + } + + #endregion + + #region Public Methods + + public override IAsyncResult BeginRead ( + byte[] buffer, + int offset, + int count, + AsyncCallback callback, + object state + ) + { + if (_disposed) + throw new ObjectDisposedException (ObjectName); + + if (buffer == null) + throw new ArgumentNullException ("buffer"); + + if (offset < 0) { + var msg = "A negative value."; + + throw new ArgumentOutOfRangeException ("offset", msg); + } + + if (count < 0) { + var msg = "A negative value."; + + throw new ArgumentOutOfRangeException ("count", msg); + } + + var len = buffer.Length; + + if (offset + count > len) { + var msg = "The sum of offset and count is greater than the length of buffer."; + + throw new ArgumentException (msg); + } + + var ares = new HttpStreamAsyncResult (callback, state); + + if (_noMoreData) { + ares.Complete (); + + return ares; + } + + var nread = _decoder.Read (buffer, offset, count); + + offset += nread; + count -= nread; + + if (count == 0) { + ares.Count = nread; + + ares.Complete (); + + return ares; + } + + if (!_decoder.WantsMore) { + _noMoreData = nread == 0; + + ares.Count = nread; + + ares.Complete (); + + return ares; + } + + ares.Buffer = new byte[_bufferLength]; + ares.Offset = 0; + ares.Count = _bufferLength; + + var rstate = new ReadBufferState (buffer, offset, count, ares); + + rstate.InitialCount += nread; + + base.BeginRead (ares.Buffer, ares.Offset, ares.Count, onRead, rstate); + + return ares; + } + + public override void Close () + { + if (_disposed) + return; + + base.Close (); + + _disposed = true; + } + + public override int EndRead (IAsyncResult asyncResult) + { + if (_disposed) + throw new ObjectDisposedException (ObjectName); + + if (asyncResult == null) + throw new ArgumentNullException ("asyncResult"); + + var ares = asyncResult as HttpStreamAsyncResult; + + if (ares == null) { + var msg = "A wrong IAsyncResult instance."; + + throw new ArgumentException (msg, "asyncResult"); + } + + if (!ares.IsCompleted) + ares.AsyncWaitHandle.WaitOne (); + + if (ares.HasException) { + var msg = "The I/O operation has been aborted."; + + throw new HttpListenerException (995, msg); + } + + return ares.Count; + } + + public override int Read (byte[] buffer, int offset, int count) + { + var ares = BeginRead (buffer, offset, count, null, null); + + return EndRead (ares); + } + + #endregion + } +} diff --git a/Assets/External/websocket-sharp/Net/ChunkedRequestStream.cs.meta b/Assets/External/websocket-sharp/Net/ChunkedRequestStream.cs.meta new file mode 100644 index 00000000..87030b2c --- /dev/null +++ b/Assets/External/websocket-sharp/Net/ChunkedRequestStream.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 1580b5e2a58b77b45b5b49739b05e897 \ No newline at end of file diff --git a/Assets/External/websocket-sharp/Net/ClientSslConfiguration.cs b/Assets/External/websocket-sharp/Net/ClientSslConfiguration.cs new file mode 100644 index 00000000..33438a93 --- /dev/null +++ b/Assets/External/websocket-sharp/Net/ClientSslConfiguration.cs @@ -0,0 +1,311 @@ +#region License +/* + * ClientSslConfiguration.cs + * + * The MIT License + * + * Copyright (c) 2014 liryna + * Copyright (c) 2014-2024 sta.blockhead + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#endregion + +#region Authors +/* + * Authors: + * - Liryna + */ +#endregion + +using System; +using System.Net.Security; +using System.Security.Authentication; +using System.Security.Cryptography.X509Certificates; + +namespace WebSocketSharp.Net +{ + /// + /// Stores the parameters for an instance used by + /// a client. + /// + public class ClientSslConfiguration + { + #region Private Fields + + private bool _checkCertRevocation; + private LocalCertificateSelectionCallback _clientCertSelectionCallback; + private X509CertificateCollection _clientCerts; + private SslProtocols _enabledSslProtocols; + private RemoteCertificateValidationCallback _serverCertValidationCallback; + private string _targetHost; + + #endregion + + #region Public Constructors + + /// + /// Initializes a new instance of the + /// class with the specified target host name. + /// + /// + /// A that specifies the name of the server that + /// will share a secure connection with the client. + /// + /// + /// is an empty string. + /// + /// + /// is . + /// + public ClientSslConfiguration (string targetHost) + { + if (targetHost == null) + throw new ArgumentNullException ("targetHost"); + + if (targetHost.Length == 0) + throw new ArgumentException ("An empty string.", "targetHost"); + + _targetHost = targetHost; + + _enabledSslProtocols = SslProtocols.None; + } + + /// + /// Initializes a new instance of the + /// class copying from the specified configuration. + /// + /// + /// A from which to copy. + /// + /// + /// is . + /// + public ClientSslConfiguration (ClientSslConfiguration configuration) + { + if (configuration == null) + throw new ArgumentNullException ("configuration"); + + _checkCertRevocation = configuration._checkCertRevocation; + _clientCertSelectionCallback = configuration._clientCertSelectionCallback; + _clientCerts = configuration._clientCerts; + _enabledSslProtocols = configuration._enabledSslProtocols; + _serverCertValidationCallback = configuration._serverCertValidationCallback; + _targetHost = configuration._targetHost; + } + + #endregion + + #region Public Properties + + /// + /// Gets or sets a value indicating whether the certificate revocation + /// list is checked during authentication. + /// + /// + /// + /// true if the certificate revocation list is checked during + /// authentication; otherwise, false. + /// + /// + /// The default value is false. + /// + /// + public bool CheckCertificateRevocation { + get { + return _checkCertRevocation; + } + + set { + _checkCertRevocation = value; + } + } + + /// + /// Gets or sets the collection of the certificates from which to select + /// one to supply to the server. + /// + /// + /// + /// A that contains + /// the certificates from which to select. + /// + /// + /// if not present. + /// + /// + /// The default value is . + /// + /// + public X509CertificateCollection ClientCertificates { + get { + return _clientCerts; + } + + set { + _clientCerts = value; + } + } + + /// + /// Gets or sets the callback used to select the certificate to supply to + /// the server. + /// + /// + /// No certificate is supplied if the callback returns . + /// + /// + /// + /// A delegate. + /// + /// + /// It represents the delegate called when the client selects + /// the certificate. + /// + /// + /// The default value invokes a method that only returns + /// . + /// + /// + public LocalCertificateSelectionCallback ClientCertificateSelectionCallback { + get { + if (_clientCertSelectionCallback == null) + _clientCertSelectionCallback = defaultSelectClientCertificate; + + return _clientCertSelectionCallback; + } + + set { + _clientCertSelectionCallback = value; + } + } + + /// + /// Gets or sets the enabled versions of the SSL/TLS protocols. + /// + /// + /// + /// Any of the enum values. + /// + /// + /// It represents the enabled versions of the SSL/TLS protocols. + /// + /// + /// The default value is . + /// + /// + public SslProtocols EnabledSslProtocols { + get { + return _enabledSslProtocols; + } + + set { + _enabledSslProtocols = value; + } + } + + /// + /// Gets or sets the callback used to validate the certificate supplied by + /// the server. + /// + /// + /// The certificate is valid if the callback returns true. + /// + /// + /// + /// A delegate. + /// + /// + /// It represents the delegate called when the client validates + /// the certificate. + /// + /// + /// The default value invokes a method that only returns true. + /// + /// + public RemoteCertificateValidationCallback ServerCertificateValidationCallback { + get { + if (_serverCertValidationCallback == null) + _serverCertValidationCallback = defaultValidateServerCertificate; + + return _serverCertValidationCallback; + } + + set { + _serverCertValidationCallback = value; + } + } + + /// + /// Gets or sets the target host name. + /// + /// + /// A that represents the name of the server that + /// will share a secure connection with the client. + /// + /// + /// The value specified for a set operation is an empty string. + /// + /// + /// The value specified for a set operation is . + /// + public string TargetHost { + get { + return _targetHost; + } + + set { + if (value == null) + throw new ArgumentNullException ("value"); + + if (value.Length == 0) + throw new ArgumentException ("An empty string.", "value"); + + _targetHost = value; + } + } + + #endregion + + #region Private Methods + + private static X509Certificate defaultSelectClientCertificate ( + object sender, + string targetHost, + X509CertificateCollection clientCertificates, + X509Certificate serverCertificate, + string[] acceptableIssuers + ) + { + return null; + } + + private static bool defaultValidateServerCertificate ( + object sender, + X509Certificate certificate, + X509Chain chain, + SslPolicyErrors sslPolicyErrors + ) + { + return true; + } + + #endregion + } +} diff --git a/Assets/External/websocket-sharp/Net/ClientSslConfiguration.cs.meta b/Assets/External/websocket-sharp/Net/ClientSslConfiguration.cs.meta new file mode 100644 index 00000000..2bd8464f --- /dev/null +++ b/Assets/External/websocket-sharp/Net/ClientSslConfiguration.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 60a0fec9d60bbcd428164ca7e1522355 \ No newline at end of file diff --git a/Assets/External/websocket-sharp/Net/Cookie.cs b/Assets/External/websocket-sharp/Net/Cookie.cs new file mode 100644 index 00000000..149b5041 --- /dev/null +++ b/Assets/External/websocket-sharp/Net/Cookie.cs @@ -0,0 +1,1032 @@ +#region License +/* + * Cookie.cs + * + * This code is derived from Cookie.cs (System.Net) of Mono + * (http://www.mono-project.com). + * + * The MIT License + * + * Copyright (c) 2004,2009 Novell, Inc. (http://www.novell.com) + * Copyright (c) 2012-2024 sta.blockhead + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#endregion + +#region Authors +/* + * Authors: + * - Lawrence Pit + * - Gonzalo Paniagua Javier + * - Daniel Nauck + * - Sebastien Pouliot + */ +#endregion + +using System; +using System.Globalization; +using System.Text; + +namespace WebSocketSharp.Net +{ + /// + /// Provides a set of methods and properties used to manage an HTTP cookie. + /// + /// + /// + /// This class refers to the following specifications: + /// + /// + /// + /// + /// + /// Netscape specification + /// + /// + /// + /// + /// RFC 2109 + /// + /// + /// + /// + /// RFC 2965 + /// + /// + /// + /// + /// RFC 6265 + /// + /// + /// + /// + /// This class cannot be inherited. + /// + /// + [Serializable] + public sealed class Cookie + { + #region Private Fields + + private string _comment; + private Uri _commentUri; + private bool _discard; + private string _domain; + private static readonly int[] _emptyPorts; + private DateTime _expires; + private bool _httpOnly; + private string _name; + private string _path; + private string _port; + private int[] _ports; + private static readonly char[] _reservedCharsForValue; + private string _sameSite; + private bool _secure; + private DateTime _timeStamp; + private string _value; + private int _version; + + #endregion + + #region Static Constructor + + static Cookie () + { + _emptyPorts = new int[0]; + _reservedCharsForValue = new[] { ';', ',' }; + } + + #endregion + + #region Internal Constructors + + internal Cookie () + { + init (String.Empty, String.Empty, String.Empty, String.Empty); + } + + #endregion + + #region Public Constructors + + /// + /// Initializes a new instance of the class with + /// the specified name and value. + /// + /// + /// + /// A that specifies the name of the cookie. + /// + /// + /// The name must be a token defined in + /// + /// RFC 2616. + /// + /// + /// + /// A that specifies the value of the cookie. + /// + /// + /// + /// is an empty string. + /// + /// + /// -or- + /// + /// + /// starts with a dollar sign. + /// + /// + /// -or- + /// + /// + /// contains an invalid character. + /// + /// + /// -or- + /// + /// + /// is a string not enclosed in double quotes + /// although it contains a reserved character. + /// + /// + /// + /// is . + /// + public Cookie (string name, string value) + : this (name, value, String.Empty, String.Empty) + { + } + + /// + /// Initializes a new instance of the class with + /// the specified name, value, and path. + /// + /// + /// + /// A that specifies the name of the cookie. + /// + /// + /// The name must be a token defined in + /// + /// RFC 2616. + /// + /// + /// + /// A that specifies the value of the cookie. + /// + /// + /// A that specifies the value of the Path + /// attribute of the cookie. + /// + /// + /// + /// is an empty string. + /// + /// + /// -or- + /// + /// + /// starts with a dollar sign. + /// + /// + /// -or- + /// + /// + /// contains an invalid character. + /// + /// + /// -or- + /// + /// + /// is a string not enclosed in double quotes + /// although it contains a reserved character. + /// + /// + /// + /// is . + /// + public Cookie (string name, string value, string path) + : this (name, value, path, String.Empty) + { + } + + /// + /// Initializes a new instance of the class with + /// the specified name, value, path, and domain. + /// + /// + /// + /// A that specifies the name of the cookie. + /// + /// + /// The name must be a token defined in + /// + /// RFC 2616. + /// + /// + /// + /// A that specifies the value of the cookie. + /// + /// + /// A that specifies the value of the Path + /// attribute of the cookie. + /// + /// + /// A that specifies the value of the Domain + /// attribute of the cookie. + /// + /// + /// + /// is an empty string. + /// + /// + /// -or- + /// + /// + /// starts with a dollar sign. + /// + /// + /// -or- + /// + /// + /// contains an invalid character. + /// + /// + /// -or- + /// + /// + /// is a string not enclosed in double quotes + /// although it contains a reserved character. + /// + /// + /// + /// is . + /// + public Cookie (string name, string value, string path, string domain) + { + if (name == null) + throw new ArgumentNullException ("name"); + + if (name.Length == 0) + throw new ArgumentException ("An empty string.", "name"); + + if (name[0] == '$') { + var msg = "It starts with a dollar sign."; + + throw new ArgumentException (msg, "name"); + } + + if (!name.IsToken ()) { + var msg = "It contains an invalid character."; + + throw new ArgumentException (msg, "name"); + } + + if (value == null) + value = String.Empty; + + if (value.Contains (_reservedCharsForValue)) { + if (!value.IsEnclosedIn ('"')) { + var msg = "A string not enclosed in double quotes."; + + throw new ArgumentException (msg, "value"); + } + } + + init (name, value, path ?? String.Empty, domain ?? String.Empty); + } + + #endregion + + #region Internal Properties + + internal bool ExactDomain { + get { + return _domain.Length == 0 || _domain[0] != '.'; + } + } + + internal int MaxAge { + get { + if (_expires == DateTime.MinValue) + return 0; + + var expires = _expires.Kind != DateTimeKind.Local + ? _expires.ToLocalTime () + : _expires; + + var span = expires - DateTime.Now; + + return span > TimeSpan.Zero + ? (int) span.TotalSeconds + : 0; + } + + set { + _expires = value > 0 + ? DateTime.Now.AddSeconds ((double) value) + : DateTime.Now; + } + } + + internal int[] Ports { + get { + return _ports ?? _emptyPorts; + } + } + + internal string SameSite { + get { + return _sameSite; + } + + set { + _sameSite = value; + } + } + + #endregion + + #region Public Properties + + /// + /// Gets the value of the Comment attribute of the cookie. + /// + /// + /// + /// A that represents the comment to document + /// intended use of the cookie. + /// + /// + /// if not present. + /// + /// + /// The default value is . + /// + /// + public string Comment { + get { + return _comment; + } + + internal set { + _comment = value; + } + } + + /// + /// Gets the value of the CommentURL attribute of the cookie. + /// + /// + /// + /// A that represents the URI that provides + /// the comment to document intended use of the cookie. + /// + /// + /// if not present. + /// + /// + /// The default value is . + /// + /// + public Uri CommentUri { + get { + return _commentUri; + } + + internal set { + _commentUri = value; + } + } + + /// + /// Gets a value indicating whether the client discards the cookie + /// unconditionally when the client terminates. + /// + /// + /// + /// true if the client discards the cookie unconditionally + /// when the client terminates; otherwise, false. + /// + /// + /// The default value is false. + /// + /// + public bool Discard { + get { + return _discard; + } + + internal set { + _discard = value; + } + } + + /// + /// Gets or sets the value of the Domain attribute of the cookie. + /// + /// + /// + /// A that represents the domain name that + /// the cookie is valid for. + /// + /// + /// An empty string if not necessary. + /// + /// + public string Domain { + get { + return _domain; + } + + set { + _domain = value ?? String.Empty; + } + } + + /// + /// Gets or sets a value indicating whether the cookie has expired. + /// + /// + /// + /// true if the cookie has expired; otherwise, false. + /// + /// + /// The default value is false. + /// + /// + public bool Expired { + get { + return _expires != DateTime.MinValue && _expires <= DateTime.Now; + } + + set { + _expires = value ? DateTime.Now : DateTime.MinValue; + } + } + + /// + /// Gets or sets the value of the Expires attribute of the cookie. + /// + /// + /// + /// A that represents the date and time that + /// the cookie expires on. + /// + /// + /// if not necessary. + /// + /// + /// The default value is . + /// + /// + public DateTime Expires { + get { + return _expires; + } + + set { + _expires = value; + } + } + + /// + /// Gets or sets a value indicating whether non-HTTP APIs can access + /// the cookie. + /// + /// + /// + /// true if non-HTTP APIs cannot access the cookie; otherwise, + /// false. + /// + /// + /// The default value is false. + /// + /// + public bool HttpOnly { + get { + return _httpOnly; + } + + set { + _httpOnly = value; + } + } + + /// + /// Gets or sets the name of the cookie. + /// + /// + /// + /// A that represents the name of the cookie. + /// + /// + /// The name must be a token defined in + /// + /// RFC 2616. + /// + /// + /// + /// + /// The value specified for a set operation is an empty string. + /// + /// + /// -or- + /// + /// + /// The value specified for a set operation starts with a dollar sign. + /// + /// + /// -or- + /// + /// + /// The value specified for a set operation contains an invalid character. + /// + /// + /// + /// The value specified for a set operation is . + /// + public string Name { + get { + return _name; + } + + set { + if (value == null) + throw new ArgumentNullException ("value"); + + if (value.Length == 0) + throw new ArgumentException ("An empty string.", "value"); + + if (value[0] == '$') { + var msg = "It starts with a dollar sign."; + + throw new ArgumentException (msg, "value"); + } + + if (!value.IsToken ()) { + var msg = "It contains an invalid character."; + + throw new ArgumentException (msg, "value"); + } + + _name = value; + } + } + + /// + /// Gets or sets the value of the Path attribute of the cookie. + /// + /// + /// A that represents the subset of URI on + /// the origin server that the cookie applies to. + /// + public string Path { + get { + return _path; + } + + set { + _path = value ?? String.Empty; + } + } + + /// + /// Gets the value of the Port attribute of the cookie. + /// + /// + /// + /// A that represents the list of TCP ports + /// that the cookie applies to. + /// + /// + /// if not present. + /// + /// + /// The default value is . + /// + /// + public string Port { + get { + return _port; + } + + internal set { + int[] ports; + + if (!tryCreatePorts (value, out ports)) + return; + + _ports = ports; + _port = value; + } + } + + /// + /// Gets or sets a value indicating whether the security level of + /// the cookie is secure. + /// + /// + /// When this property is true, the cookie may be included in + /// the request only if the request is transmitted over HTTPS. + /// + /// + /// + /// true if the security level of the cookie is secure; + /// otherwise, false. + /// + /// + /// The default value is false. + /// + /// + public bool Secure { + get { + return _secure; + } + + set { + _secure = value; + } + } + + /// + /// Gets the time when the cookie was issued. + /// + /// + /// A that represents the time when + /// the cookie was issued. + /// + public DateTime TimeStamp { + get { + return _timeStamp; + } + } + + /// + /// Gets or sets the value of the cookie. + /// + /// + /// A that represents the value of the cookie. + /// + /// + /// The value specified for a set operation is a string not enclosed in + /// double quotes although it contains a reserved character. + /// + public string Value { + get { + return _value; + } + + set { + if (value == null) + value = String.Empty; + + if (value.Contains (_reservedCharsForValue)) { + if (!value.IsEnclosedIn ('"')) { + var msg = "A string not enclosed in double quotes."; + + throw new ArgumentException (msg, "value"); + } + } + + _value = value; + } + } + + /// + /// Gets the value of the Version attribute of the cookie. + /// + /// + /// + /// An that represents the version of HTTP state + /// management that the cookie conforms to. + /// + /// + /// 0 or 1. + /// + /// + /// 0 if not present. + /// + /// + /// The default value is 0. + /// + /// + public int Version { + get { + return _version; + } + + internal set { + if (value < 0 || value > 1) + return; + + _version = value; + } + } + + #endregion + + #region Private Methods + + private static int hash (int i, int j, int k, int l, int m) + { + return i + ^ (j << 13 | j >> 19) + ^ (k << 26 | k >> 6) + ^ (l << 7 | l >> 25) + ^ (m << 20 | m >> 12); + } + + private void init (string name, string value, string path, string domain) + { + _name = name; + _value = value; + _path = path; + _domain = domain; + + _expires = DateTime.MinValue; + _timeStamp = DateTime.Now; + } + + private string toResponseStringVersion0 () + { + var buff = new StringBuilder (64); + + buff.AppendFormat ("{0}={1}", _name, _value); + + if (_expires != DateTime.MinValue) { + var expires = _expires + .ToUniversalTime () + .ToString ( + "ddd, dd'-'MMM'-'yyyy HH':'mm':'ss 'GMT'", + CultureInfo.CreateSpecificCulture ("en-US") + ); + + buff.AppendFormat ("; Expires={0}", expires); + } + + if (!_path.IsNullOrEmpty ()) + buff.AppendFormat ("; Path={0}", _path); + + if (!_domain.IsNullOrEmpty ()) + buff.AppendFormat ("; Domain={0}", _domain); + + if (!_sameSite.IsNullOrEmpty ()) + buff.AppendFormat ("; SameSite={0}", _sameSite); + + if (_secure) + buff.Append ("; Secure"); + + if (_httpOnly) + buff.Append ("; HttpOnly"); + + return buff.ToString (); + } + + private string toResponseStringVersion1 () + { + var buff = new StringBuilder (64); + + buff.AppendFormat ("{0}={1}; Version={2}", _name, _value, _version); + + if (_expires != DateTime.MinValue) + buff.AppendFormat ("; Max-Age={0}", MaxAge); + + if (!_path.IsNullOrEmpty ()) + buff.AppendFormat ("; Path={0}", _path); + + if (!_domain.IsNullOrEmpty ()) + buff.AppendFormat ("; Domain={0}", _domain); + + if (_port != null) { + if (_port != "\"\"") + buff.AppendFormat ("; Port={0}", _port); + else + buff.Append ("; Port"); + } + + if (_comment != null) { + var comment = HttpUtility.UrlEncode (_comment); + + buff.AppendFormat ("; Comment={0}", comment); + } + + if (_commentUri != null) { + var url = _commentUri.OriginalString; + + buff.AppendFormat ( + "; CommentURL={0}", + !url.IsToken () ? url.Quote () : url + ); + } + + if (_discard) + buff.Append ("; Discard"); + + if (_secure) + buff.Append ("; Secure"); + + return buff.ToString (); + } + + private static bool tryCreatePorts (string value, out int[] result) + { + result = null; + + var arr = value.Trim ('"').Split (','); + var len = arr.Length; + var res = new int[len]; + + for (var i = 0; i < len; i++) { + var s = arr[i].Trim (); + + if (s.Length == 0) { + res[i] = Int32.MinValue; + + continue; + } + + if (!Int32.TryParse (s, out res[i])) + return false; + } + + result = res; + + return true; + } + + #endregion + + #region Internal Methods + + internal bool EqualsWithoutValue (Cookie cookie) + { + var caseSensitive = StringComparison.InvariantCulture; + var caseInsensitive = StringComparison.InvariantCultureIgnoreCase; + + return _name.Equals (cookie._name, caseInsensitive) + && _path.Equals (cookie._path, caseSensitive) + && _domain.Equals (cookie._domain, caseInsensitive) + && _version == cookie._version; + } + + internal bool EqualsWithoutValueAndVersion (Cookie cookie) + { + var caseSensitive = StringComparison.InvariantCulture; + var caseInsensitive = StringComparison.InvariantCultureIgnoreCase; + + return _name.Equals (cookie._name, caseInsensitive) + && _path.Equals (cookie._path, caseSensitive) + && _domain.Equals (cookie._domain, caseInsensitive); + } + + internal string ToRequestString (Uri uri) + { + if (_name.Length == 0) + return String.Empty; + + if (_version == 0) + return String.Format ("{0}={1}", _name, _value); + + var buff = new StringBuilder (64); + + buff.AppendFormat ("$Version={0}; {1}={2}", _version, _name, _value); + + if (!_path.IsNullOrEmpty ()) + buff.AppendFormat ("; $Path={0}", _path); + else if (uri != null) + buff.AppendFormat ("; $Path={0}", uri.GetAbsolutePath ()); + else + buff.Append ("; $Path=/"); + + if (!_domain.IsNullOrEmpty ()) { + if (uri == null || uri.Host != _domain) + buff.AppendFormat ("; $Domain={0}", _domain); + } + + if (_port != null) { + if (_port != "\"\"") + buff.AppendFormat ("; $Port={0}", _port); + else + buff.Append ("; $Port"); + } + + return buff.ToString (); + } + + internal string ToResponseString () + { + if (_name.Length == 0) + return String.Empty; + + if (_version == 0) + return toResponseStringVersion0 (); + + return toResponseStringVersion1 (); + } + + internal static bool TryCreate ( + string name, + string value, + out Cookie result + ) + { + result = null; + + try { + result = new Cookie (name, value); + } + catch { + return false; + } + + return true; + } + + #endregion + + #region Public Methods + + /// + /// Determines whether the current cookie instance is equal to + /// the specified instance. + /// + /// + /// + /// An instance to compare with + /// the current cookie instance. + /// + /// + /// An reference to a instance. + /// + /// + /// + /// true if the current cookie instance is equal to + /// ; otherwise, false. + /// + public override bool Equals (object comparand) + { + var cookie = comparand as Cookie; + + if (cookie == null) + return false; + + var caseSensitive = StringComparison.InvariantCulture; + var caseInsensitive = StringComparison.InvariantCultureIgnoreCase; + + return _name.Equals (cookie._name, caseInsensitive) + && _value.Equals (cookie._value, caseSensitive) + && _path.Equals (cookie._path, caseSensitive) + && _domain.Equals (cookie._domain, caseInsensitive) + && _version == cookie._version; + } + + /// + /// Gets a hash code for the current cookie instance. + /// + /// + /// An that represents the hash code. + /// + public override int GetHashCode () + { + var i = StringComparer.InvariantCultureIgnoreCase.GetHashCode (_name); + var j = _value.GetHashCode (); + var k = _path.GetHashCode (); + var l = StringComparer.InvariantCultureIgnoreCase.GetHashCode (_domain); + var m = _version; + + return hash (i, j, k, l, m); + } + + /// + /// Returns a string that represents the current cookie instance. + /// + /// + /// A that is suitable for the Cookie request header. + /// + public override string ToString () + { + return ToRequestString (null); + } + + #endregion + } +} diff --git a/Assets/External/websocket-sharp/Net/Cookie.cs.meta b/Assets/External/websocket-sharp/Net/Cookie.cs.meta new file mode 100644 index 00000000..3a9da13d --- /dev/null +++ b/Assets/External/websocket-sharp/Net/Cookie.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 5ff89aabc6475184fa85a0cb73e53bcd \ No newline at end of file diff --git a/Assets/External/websocket-sharp/Net/CookieCollection.cs b/Assets/External/websocket-sharp/Net/CookieCollection.cs new file mode 100644 index 00000000..9a8ce641 --- /dev/null +++ b/Assets/External/websocket-sharp/Net/CookieCollection.cs @@ -0,0 +1,882 @@ +#region License +/* + * CookieCollection.cs + * + * This code is derived from CookieCollection.cs (System.Net) of Mono + * (http://www.mono-project.com). + * + * The MIT License + * + * Copyright (c) 2004,2009 Novell, Inc. (http://www.novell.com) + * Copyright (c) 2012-2024 sta.blockhead + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#endregion + +#region Authors +/* + * Authors: + * - Lawrence Pit + * - Gonzalo Paniagua Javier + * - Sebastien Pouliot + */ +#endregion + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Globalization; +using System.Text; + +namespace WebSocketSharp.Net +{ + /// + /// Provides a collection of instances of the class. + /// + [Serializable] + public class CookieCollection : ICollection + { + #region Private Fields + + private List _list; + private bool _readOnly; + private object _sync; + + #endregion + + #region Public Constructors + + /// + /// Initializes a new instance of the class. + /// + public CookieCollection () + { + _list = new List (); + _sync = ((ICollection) _list).SyncRoot; + } + + #endregion + + #region Internal Properties + + internal IList List { + get { + return _list; + } + } + + internal IEnumerable Sorted { + get { + var list = new List (_list); + + if (list.Count > 1) + list.Sort (compareForSorted); + + return list; + } + } + + #endregion + + #region Public Properties + + /// + /// Gets the number of cookies in the collection. + /// + /// + /// An that represents the number of cookies in + /// the collection. + /// + public int Count { + get { + return _list.Count; + } + } + + /// + /// Gets a value indicating whether the collection is read-only. + /// + /// + /// + /// true if the collection is read-only; otherwise, false. + /// + /// + /// The default value is false. + /// + /// + public bool IsReadOnly { + get { + return _readOnly; + } + + internal set { + _readOnly = value; + } + } + + /// + /// Gets a value indicating whether the access to the collection is + /// thread safe. + /// + /// + /// + /// true if the access to the collection is thread safe; + /// otherwise, false. + /// + /// + /// The default value is false. + /// + /// + public bool IsSynchronized { + get { + return false; + } + } + + /// + /// Gets the cookie at the specified index from the collection. + /// + /// + /// A at the specified index in the collection. + /// + /// + /// An that specifies the zero-based index of the cookie + /// to find. + /// + /// + /// is out of allowable range for the collection. + /// + public Cookie this[int index] { + get { + if (index < 0 || index >= _list.Count) + throw new ArgumentOutOfRangeException ("index"); + + return _list[index]; + } + } + + /// + /// Gets the cookie with the specified name from the collection. + /// + /// + /// + /// A with the specified name in the collection. + /// + /// + /// if not found. + /// + /// + /// + /// A that specifies the name of the cookie to find. + /// + /// + /// is . + /// + public Cookie this[string name] { + get { + if (name == null) + throw new ArgumentNullException ("name"); + + var caseInsensitive = StringComparison.InvariantCultureIgnoreCase; + + foreach (var cookie in Sorted) { + if (cookie.Name.Equals (name, caseInsensitive)) + return cookie; + } + + return null; + } + } + + /// + /// Gets an object used to synchronize access to the collection. + /// + /// + /// An used to synchronize access to the collection. + /// + public object SyncRoot { + get { + return _sync; + } + } + + #endregion + + #region Private Methods + + private void add (Cookie cookie) + { + var idx = search (cookie); + + if (idx == -1) { + _list.Add (cookie); + + return; + } + + _list[idx] = cookie; + } + + private static int compareForSort (Cookie x, Cookie y) + { + return (x.Name.Length + x.Value.Length) + - (y.Name.Length + y.Value.Length); + } + + private static int compareForSorted (Cookie x, Cookie y) + { + var ret = x.Version - y.Version; + + if (ret != 0) + return ret; + + ret = x.Name.CompareTo (y.Name); + + if (ret != 0) + return ret; + + return y.Path.Length - x.Path.Length; + } + + private static CookieCollection parseRequest (string value) + { + var ret = new CookieCollection (); + + Cookie cookie = null; + var ver = 0; + var caseInsensitive = StringComparison.InvariantCultureIgnoreCase; + + var pairs = value.SplitHeaderValue (',', ';').ToList (); + + for (var i = 0; i < pairs.Count; i++) { + var pair = pairs[i].Trim (); + + if (pair.Length == 0) + continue; + + var idx = pair.IndexOf ('='); + + if (idx == -1) { + if (cookie == null) + continue; + + if (pair.Equals ("$port", caseInsensitive)) { + cookie.Port = "\"\""; + + continue; + } + + continue; + } + + if (idx == 0) { + if (cookie != null) { + ret.add (cookie); + + cookie = null; + } + + continue; + } + + var name = pair.Substring (0, idx).TrimEnd (' '); + var val = idx < pair.Length - 1 + ? pair.Substring (idx + 1).TrimStart (' ') + : String.Empty; + + if (name.Equals ("$version", caseInsensitive)) { + if (val.Length == 0) + continue; + + var s = val.Unquote (); + + int num; + + if (!Int32.TryParse (s, out num)) + continue; + + ver = num; + + continue; + } + + if (name.Equals ("$path", caseInsensitive)) { + if (cookie == null) + continue; + + if (val.Length == 0) + continue; + + cookie.Path = val; + + continue; + } + + if (name.Equals ("$domain", caseInsensitive)) { + if (cookie == null) + continue; + + if (val.Length == 0) + continue; + + cookie.Domain = val; + + continue; + } + + if (name.Equals ("$port", caseInsensitive)) { + if (cookie == null) + continue; + + if (val.Length == 0) + continue; + + cookie.Port = val; + + continue; + } + + if (cookie != null) + ret.add (cookie); + + if (!Cookie.TryCreate (name, val, out cookie)) + continue; + + if (ver != 0) + cookie.Version = ver; + } + + if (cookie != null) + ret.add (cookie); + + return ret; + } + + private static CookieCollection parseResponse (string value) + { + var ret = new CookieCollection (); + + Cookie cookie = null; + var caseInsensitive = StringComparison.InvariantCultureIgnoreCase; + + var pairs = value.SplitHeaderValue (',', ';').ToList (); + + for (var i = 0; i < pairs.Count; i++) { + var pair = pairs[i].Trim (); + + if (pair.Length == 0) + continue; + + var idx = pair.IndexOf ('='); + + if (idx == -1) { + if (cookie == null) + continue; + + if (pair.Equals ("port", caseInsensitive)) { + cookie.Port = "\"\""; + + continue; + } + + if (pair.Equals ("discard", caseInsensitive)) { + cookie.Discard = true; + + continue; + } + + if (pair.Equals ("secure", caseInsensitive)) { + cookie.Secure = true; + + continue; + } + + if (pair.Equals ("httponly", caseInsensitive)) { + cookie.HttpOnly = true; + + continue; + } + + continue; + } + + if (idx == 0) { + if (cookie != null) { + ret.add (cookie); + + cookie = null; + } + + continue; + } + + var name = pair.Substring (0, idx).TrimEnd (' '); + var val = idx < pair.Length - 1 + ? pair.Substring (idx + 1).TrimStart (' ') + : String.Empty; + + if (name.Equals ("version", caseInsensitive)) { + if (cookie == null) + continue; + + if (val.Length == 0) + continue; + + var s = val.Unquote (); + + int num; + + if (!Int32.TryParse (s, out num)) + continue; + + cookie.Version = num; + + continue; + } + + if (name.Equals ("expires", caseInsensitive)) { + if (val.Length == 0) + continue; + + if (i == pairs.Count - 1) + break; + + i++; + + if (cookie == null) + continue; + + if (cookie.Expires != DateTime.MinValue) + continue; + + var buff = new StringBuilder (val, 32); + + buff.AppendFormat (", {0}", pairs[i].Trim ()); + + var s = buff.ToString (); + var fmts = new[] { "ddd, dd'-'MMM'-'yyyy HH':'mm':'ss 'GMT'", "r" }; + var provider = CultureInfo.CreateSpecificCulture ("en-US"); + var style = DateTimeStyles.AdjustToUniversal + | DateTimeStyles.AssumeUniversal; + + DateTime expires; + + var done = DateTime.TryParseExact ( + s, + fmts, + provider, + style, + out expires + ); + + if (!done) + continue; + + cookie.Expires = expires.ToLocalTime (); + + continue; + } + + if (name.Equals ("max-age", caseInsensitive)) { + if (cookie == null) + continue; + + if (val.Length == 0) + continue; + + var s = val.Unquote (); + + int maxAge; + + if (!Int32.TryParse (s, out maxAge)) + continue; + + cookie.MaxAge = maxAge; + + continue; + } + + if (name.Equals ("path", caseInsensitive)) { + if (cookie == null) + continue; + + if (val.Length == 0) + continue; + + cookie.Path = val; + + continue; + } + + if (name.Equals ("domain", caseInsensitive)) { + if (cookie == null) + continue; + + if (val.Length == 0) + continue; + + cookie.Domain = val; + + continue; + } + + if (name.Equals ("port", caseInsensitive)) { + if (cookie == null) + continue; + + if (val.Length == 0) + continue; + + cookie.Port = val; + + continue; + } + + if (name.Equals ("comment", caseInsensitive)) { + if (cookie == null) + continue; + + if (val.Length == 0) + continue; + + cookie.Comment = urlDecode (val, Encoding.UTF8); + + continue; + } + + if (name.Equals ("commenturl", caseInsensitive)) { + if (cookie == null) + continue; + + if (val.Length == 0) + continue; + + cookie.CommentUri = val.Unquote ().ToUri (); + + continue; + } + + if (name.Equals ("samesite", caseInsensitive)) { + if (cookie == null) + continue; + + if (val.Length == 0) + continue; + + cookie.SameSite = val.Unquote (); + + continue; + } + + if (cookie != null) + ret.add (cookie); + + Cookie.TryCreate (name, val, out cookie); + } + + if (cookie != null) + ret.add (cookie); + + return ret; + } + + private int search (Cookie cookie) + { + for (var i = _list.Count - 1; i >= 0; i--) { + if (_list[i].EqualsWithoutValue (cookie)) + return i; + } + + return -1; + } + + private static string urlDecode (string s, Encoding encoding) + { + if (s.IndexOfAny (new[] { '%', '+' }) == -1) + return s; + + try { + return HttpUtility.UrlDecode (s, encoding); + } + catch { + return null; + } + } + + #endregion + + #region Internal Methods + + internal static CookieCollection Parse (string value, bool response) + { + try { + return response ? parseResponse (value) : parseRequest (value); + } + catch (Exception ex) { + throw new CookieException ("It could not be parsed.", ex); + } + } + + internal void SetOrRemove (Cookie cookie) + { + var idx = search (cookie); + + if (idx == -1) { + if (cookie.Expired) + return; + + _list.Add (cookie); + + return; + } + + if (cookie.Expired) { + _list.RemoveAt (idx); + + return; + } + + _list[idx] = cookie; + } + + internal void SetOrRemove (CookieCollection cookies) + { + foreach (var cookie in cookies._list) + SetOrRemove (cookie); + } + + internal void Sort () + { + if (_list.Count < 2) + return; + + _list.Sort (compareForSort); + } + + #endregion + + #region Public Methods + + /// + /// Adds the specified cookie to the collection. + /// + /// + /// A to add. + /// + /// + /// is . + /// + /// + /// The collection is read-only. + /// + public void Add (Cookie cookie) + { + if (_readOnly) { + var msg = "The collection is read-only."; + + throw new InvalidOperationException (msg); + } + + if (cookie == null) + throw new ArgumentNullException ("cookie"); + + add (cookie); + } + + /// + /// Adds the specified cookies to the collection. + /// + /// + /// A that contains the cookies to add. + /// + /// + /// is . + /// + /// + /// The collection is read-only. + /// + public void Add (CookieCollection cookies) + { + if (_readOnly) { + var msg = "The collection is read-only."; + + throw new InvalidOperationException (msg); + } + + if (cookies == null) + throw new ArgumentNullException ("cookies"); + + foreach (var cookie in cookies._list) + add (cookie); + } + + /// + /// Removes all cookies from the collection. + /// + /// + /// The collection is read-only. + /// + public void Clear () + { + if (_readOnly) { + var msg = "The collection is read-only."; + + throw new InvalidOperationException (msg); + } + + _list.Clear (); + } + + /// + /// Determines whether the collection contains the specified cookie. + /// + /// + /// true if the cookie is found in the collection; otherwise, + /// false. + /// + /// + /// A to find. + /// + /// + /// is . + /// + public bool Contains (Cookie cookie) + { + if (cookie == null) + throw new ArgumentNullException ("cookie"); + + return search (cookie) > -1; + } + + /// + /// Copies the elements of the collection to the specified array, + /// starting at the specified index. + /// + /// + /// An array of that specifies the destination of + /// the elements copied from the collection. + /// + /// + /// An that specifies the zero-based index in + /// the array at which copying starts. + /// + /// + /// The space from to the end of + /// is not enough to copy to. + /// + /// + /// is . + /// + /// + /// is less than zero. + /// + public void CopyTo (Cookie[] array, int index) + { + if (array == null) + throw new ArgumentNullException ("array"); + + if (index < 0) { + var msg = "Less than zero."; + + throw new ArgumentOutOfRangeException ("index", msg); + } + + if (array.Length - index < _list.Count) { + var msg = "The available space of the array is not enough to copy to."; + + throw new ArgumentException (msg); + } + + _list.CopyTo (array, index); + } + + /// + /// Gets the enumerator that iterates through the collection. + /// + /// + /// An + /// instance that can be used to iterate through the collection. + /// + public IEnumerator GetEnumerator () + { + return _list.GetEnumerator (); + } + + /// + /// Removes the specified cookie from the collection. + /// + /// + /// + /// true if the cookie is successfully removed; otherwise, + /// false. + /// + /// + /// false if the cookie is not found in the collection. + /// + /// + /// + /// A to remove. + /// + /// + /// is . + /// + /// + /// The collection is read-only. + /// + public bool Remove (Cookie cookie) + { + if (_readOnly) { + var msg = "The collection is read-only."; + + throw new InvalidOperationException (msg); + } + + if (cookie == null) + throw new ArgumentNullException ("cookie"); + + var idx = search (cookie); + + if (idx == -1) + return false; + + _list.RemoveAt (idx); + + return true; + } + + #endregion + + #region Explicit Interface Implementations + + /// + /// Gets the enumerator that iterates through the collection. + /// + /// + /// An instance that can be used to iterate + /// through the collection. + /// + IEnumerator IEnumerable.GetEnumerator () + { + return _list.GetEnumerator (); + } + + #endregion + } +} diff --git a/Assets/External/websocket-sharp/Net/CookieCollection.cs.meta b/Assets/External/websocket-sharp/Net/CookieCollection.cs.meta new file mode 100644 index 00000000..75bac1c7 --- /dev/null +++ b/Assets/External/websocket-sharp/Net/CookieCollection.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 9324917a904d21243a764a5afcb4dde4 \ No newline at end of file diff --git a/Assets/External/websocket-sharp/Net/CookieException.cs b/Assets/External/websocket-sharp/Net/CookieException.cs new file mode 100644 index 00000000..3c9ab3f9 --- /dev/null +++ b/Assets/External/websocket-sharp/Net/CookieException.cs @@ -0,0 +1,169 @@ +#region License +/* + * CookieException.cs + * + * This code is derived from CookieException.cs (System.Net) of Mono + * (http://www.mono-project.com). + * + * The MIT License + * + * Copyright (c) 2012-2024 sta.blockhead + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#endregion + +#region Authors +/* + * Authors: + * - Lawrence Pit + */ +#endregion + +using System; +using System.Runtime.Serialization; +using System.Security.Permissions; + +namespace WebSocketSharp.Net +{ + /// + /// The exception that is thrown when a gets an error. + /// + [Serializable] + public class CookieException : FormatException, ISerializable + { + #region Internal Constructors + + internal CookieException (string message) + : base (message) + { + } + + internal CookieException (string message, Exception innerException) + : base (message, innerException) + { + } + + #endregion + + #region Protected Constructors + + /// + /// Initializes a new instance of the class + /// with the specified serialized data. + /// + /// + /// A that contains the serialized + /// object data. + /// + /// + /// A that specifies the source for + /// the deserialization. + /// + /// + /// is . + /// + protected CookieException ( + SerializationInfo serializationInfo, + StreamingContext streamingContext + ) + : base (serializationInfo, streamingContext) + { + } + + #endregion + + #region Public Constructors + + /// + /// Initializes a new instance of the class. + /// + public CookieException () + : base () + { + } + + #endregion + + #region Public Methods + + /// + /// Populates the specified instance with + /// the data needed to serialize the current instance. + /// + /// + /// A that holds the serialized object data. + /// + /// + /// A that specifies the destination for + /// the serialization. + /// + /// + /// is . + /// + [ + SecurityPermission ( + SecurityAction.LinkDemand, + Flags = SecurityPermissionFlag.SerializationFormatter + ) + ] + public override void GetObjectData ( + SerializationInfo serializationInfo, + StreamingContext streamingContext + ) + { + base.GetObjectData (serializationInfo, streamingContext); + } + + #endregion + + #region Explicit Interface Implementation + + /// + /// Populates the specified instance with + /// the data needed to serialize the current instance. + /// + /// + /// A that holds the serialized object data. + /// + /// + /// A that specifies the destination for + /// the serialization. + /// + /// + /// is . + /// + [ + SecurityPermission ( + SecurityAction.LinkDemand, + Flags = SecurityPermissionFlag.SerializationFormatter, + SerializationFormatter = true + ) + ] + void ISerializable.GetObjectData ( + SerializationInfo serializationInfo, + StreamingContext streamingContext + ) + { + base.GetObjectData (serializationInfo, streamingContext); + } + + #endregion + } +} diff --git a/Assets/External/websocket-sharp/Net/CookieException.cs.meta b/Assets/External/websocket-sharp/Net/CookieException.cs.meta new file mode 100644 index 00000000..55a5956c --- /dev/null +++ b/Assets/External/websocket-sharp/Net/CookieException.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 00c4066ad16ffd145815887061f4c68d \ No newline at end of file diff --git a/Assets/External/websocket-sharp/Net/EndPointListener.cs b/Assets/External/websocket-sharp/Net/EndPointListener.cs new file mode 100644 index 00000000..3c8b3653 --- /dev/null +++ b/Assets/External/websocket-sharp/Net/EndPointListener.cs @@ -0,0 +1,604 @@ +#region License +/* + * EndPointListener.cs + * + * This code is derived from EndPointListener.cs (System.Net) of Mono + * (http://www.mono-project.com). + * + * The MIT License + * + * Copyright (c) 2005 Novell, Inc. (http://www.novell.com) + * Copyright (c) 2012-2023 sta.blockhead + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#endregion + +#region Authors +/* + * Authors: + * - Gonzalo Paniagua Javier + */ +#endregion + +#region Contributors +/* + * Contributors: + * - Liryna + * - Nicholas Devenish + */ +#endregion + +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Net.Sockets; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; +using System.Threading; + +namespace WebSocketSharp.Net +{ + internal sealed class EndPointListener + { + #region Private Fields + + private List _all; // host == '+' + private Dictionary _connections; + private object _connectionsSync; + private static readonly string _defaultCertFolderPath; + private IPEndPoint _endpoint; + private List _prefixes; + private bool _secure; + private Socket _socket; + private ServerSslConfiguration _sslConfig; + private List _unhandled; // host == '*' + + #endregion + + #region Static Constructor + + static EndPointListener () + { + _defaultCertFolderPath = Environment.GetFolderPath ( + Environment.SpecialFolder.ApplicationData + ); + } + + #endregion + + #region Internal Constructors + + internal EndPointListener ( + IPEndPoint endpoint, + bool secure, + string certificateFolderPath, + ServerSslConfiguration sslConfig, + bool reuseAddress + ) + { + _endpoint = endpoint; + + if (secure) { + var cert = getCertificate ( + endpoint.Port, + certificateFolderPath, + sslConfig.ServerCertificate + ); + + if (cert == null) { + var msg = "No server certificate could be found."; + + throw new ArgumentException (msg); + } + + _secure = true; + _sslConfig = new ServerSslConfiguration (sslConfig); + _sslConfig.ServerCertificate = cert; + } + + _prefixes = new List (); + _connections = new Dictionary (); + _connectionsSync = ((ICollection) _connections).SyncRoot; + + _socket = new Socket ( + endpoint.Address.AddressFamily, + SocketType.Stream, + ProtocolType.Tcp + ); + + if (reuseAddress) { + _socket.SetSocketOption ( + SocketOptionLevel.Socket, + SocketOptionName.ReuseAddress, + true + ); + } + + _socket.Bind (endpoint); + _socket.Listen (500); + _socket.BeginAccept (onAccept, this); + } + + #endregion + + #region Public Properties + + public IPAddress Address { + get { + return _endpoint.Address; + } + } + + public bool IsSecure { + get { + return _secure; + } + } + + public int Port { + get { + return _endpoint.Port; + } + } + + public ServerSslConfiguration SslConfiguration { + get { + return _sslConfig; + } + } + + #endregion + + #region Private Methods + + private static void addSpecial ( + List prefixes, + HttpListenerPrefix prefix + ) + { + var path = prefix.Path; + + foreach (var pref in prefixes) { + if (pref.Path == path) { + var msg = "The prefix is already in use."; + + throw new HttpListenerException (87, msg); + } + } + + prefixes.Add (prefix); + } + + private void clearConnections () + { + HttpConnection[] conns = null; + + lock (_connectionsSync) { + var cnt = _connections.Count; + + if (cnt == 0) + return; + + conns = new HttpConnection[cnt]; + + _connections.Values.CopyTo (conns, 0); + _connections.Clear (); + } + + foreach (var conn in conns) + conn.Close (true); + } + + private static RSACryptoServiceProvider createRSAFromFile (string path) + { + var rsa = new RSACryptoServiceProvider (); + + var key = File.ReadAllBytes (path); + + rsa.ImportCspBlob (key); + + return rsa; + } + + private static X509Certificate2 getCertificate ( + int port, + string folderPath, + X509Certificate2 defaultCertificate + ) + { + if (folderPath == null || folderPath.Length == 0) + folderPath = _defaultCertFolderPath; + + try { + var cer = Path.Combine (folderPath, String.Format ("{0}.cer", port)); + var key = Path.Combine (folderPath, String.Format ("{0}.key", port)); + + var exists = File.Exists (cer) && File.Exists (key); + + if (!exists) + return defaultCertificate; + + var cert = new X509Certificate2 (cer); + + cert.PrivateKey = createRSAFromFile (key); + + return cert; + } + catch { + return defaultCertificate; + } + } + + private void leaveIfNoPrefix () + { + if (_prefixes.Count > 0) + return; + + var prefs = _unhandled; + + if (prefs != null && prefs.Count > 0) + return; + + prefs = _all; + + if (prefs != null && prefs.Count > 0) + return; + + Close (); + } + + private static void onAccept (IAsyncResult asyncResult) + { + var lsnr = (EndPointListener) asyncResult.AsyncState; + + Socket sock = null; + + try { + sock = lsnr._socket.EndAccept (asyncResult); + } + catch (ObjectDisposedException) { + return; + } + catch (Exception) { + // TODO: Logging. + } + + try { + lsnr._socket.BeginAccept (onAccept, lsnr); + } + catch (Exception) { + // TODO: Logging. + + if (sock != null) + sock.Close (); + + return; + } + + if (sock == null) + return; + + processAccepted (sock, lsnr); + } + + private static void processAccepted ( + Socket socket, + EndPointListener listener + ) + { + HttpConnection conn = null; + + try { + conn = new HttpConnection (socket, listener); + } + catch (Exception) { + // TODO: Logging. + + socket.Close (); + + return; + } + + lock (listener._connectionsSync) + listener._connections.Add (conn, conn); + + conn.BeginReadRequest (); + } + + private static bool removeSpecial ( + List prefixes, + HttpListenerPrefix prefix + ) + { + var path = prefix.Path; + var cnt = prefixes.Count; + + for (var i = 0; i < cnt; i++) { + if (prefixes[i].Path == path) { + prefixes.RemoveAt (i); + + return true; + } + } + + return false; + } + + private static HttpListener searchHttpListenerFromSpecial ( + string path, + List prefixes + ) + { + if (prefixes == null) + return null; + + HttpListener ret = null; + + var bestLen = -1; + + foreach (var pref in prefixes) { + var prefPath = pref.Path; + var len = prefPath.Length; + + if (len < bestLen) + continue; + + var match = path.StartsWith (prefPath, StringComparison.Ordinal); + + if (!match) + continue; + + bestLen = len; + ret = pref.Listener; + } + + return ret; + } + + #endregion + + #region Internal Methods + + internal static bool CertificateExists (int port, string folderPath) + { + if (folderPath == null || folderPath.Length == 0) + folderPath = _defaultCertFolderPath; + + var cer = Path.Combine (folderPath, String.Format ("{0}.cer", port)); + var key = Path.Combine (folderPath, String.Format ("{0}.key", port)); + + return File.Exists (cer) && File.Exists (key); + } + + internal void RemoveConnection (HttpConnection connection) + { + lock (_connectionsSync) + _connections.Remove (connection); + } + + internal bool TrySearchHttpListener (Uri uri, out HttpListener listener) + { + listener = null; + + if (uri == null) + return false; + + var host = uri.Host; + var dns = Uri.CheckHostName (host) == UriHostNameType.Dns; + var port = uri.Port.ToString (); + var path = HttpUtility.UrlDecode (uri.AbsolutePath); + + if (path[path.Length - 1] != '/') + path += "/"; + + if (host != null && host.Length > 0) { + var prefs = _prefixes; + var bestLen = -1; + + foreach (var pref in prefs) { + if (dns) { + var prefHost = pref.Host; + var prefDns = Uri.CheckHostName (prefHost) == UriHostNameType.Dns; + + if (prefDns) { + if (prefHost != host) + continue; + } + } + + if (pref.Port != port) + continue; + + var prefPath = pref.Path; + var len = prefPath.Length; + + if (len < bestLen) + continue; + + var match = path.StartsWith (prefPath, StringComparison.Ordinal); + + if (!match) + continue; + + bestLen = len; + listener = pref.Listener; + } + + if (bestLen != -1) + return true; + } + + listener = searchHttpListenerFromSpecial (path, _unhandled); + + if (listener != null) + return true; + + listener = searchHttpListenerFromSpecial (path, _all); + + return listener != null; + } + + #endregion + + #region Public Methods + + public void AddPrefix (HttpListenerPrefix prefix) + { + List current, future; + + if (prefix.Host == "*") { + do { + current = _unhandled; + future = current != null + ? new List (current) + : new List (); + + addSpecial (future, prefix); + } + while ( + Interlocked.CompareExchange (ref _unhandled, future, current) + != current + ); + + return; + } + + if (prefix.Host == "+") { + do { + current = _all; + future = current != null + ? new List (current) + : new List (); + + addSpecial (future, prefix); + } + while ( + Interlocked.CompareExchange (ref _all, future, current) + != current + ); + + return; + } + + do { + current = _prefixes; + + var idx = current.IndexOf (prefix); + + if (idx > -1) { + if (current[idx].Listener != prefix.Listener) { + var fmt = "There is another listener for {0}."; + var msg = String.Format (fmt, prefix); + + throw new HttpListenerException (87, msg); + } + + return; + } + + future = new List (current); + + future.Add (prefix); + } + while ( + Interlocked.CompareExchange (ref _prefixes, future, current) + != current + ); + } + + public void Close () + { + _socket.Close (); + + clearConnections (); + EndPointManager.RemoveEndPoint (_endpoint); + } + + public void RemovePrefix (HttpListenerPrefix prefix) + { + List current, future; + + if (prefix.Host == "*") { + do { + current = _unhandled; + + if (current == null) + break; + + future = new List (current); + + if (!removeSpecial (future, prefix)) + break; + } + while ( + Interlocked.CompareExchange (ref _unhandled, future, current) + != current + ); + + leaveIfNoPrefix (); + + return; + } + + if (prefix.Host == "+") { + do { + current = _all; + + if (current == null) + break; + + future = new List (current); + + if (!removeSpecial (future, prefix)) + break; + } + while ( + Interlocked.CompareExchange (ref _all, future, current) + != current + ); + + leaveIfNoPrefix (); + + return; + } + + do { + current = _prefixes; + + if (!current.Contains (prefix)) + break; + + future = new List (current); + + future.Remove (prefix); + } + while ( + Interlocked.CompareExchange (ref _prefixes, future, current) + != current + ); + + leaveIfNoPrefix (); + } + + #endregion + } +} diff --git a/Assets/External/websocket-sharp/Net/EndPointListener.cs.meta b/Assets/External/websocket-sharp/Net/EndPointListener.cs.meta new file mode 100644 index 00000000..1700336d --- /dev/null +++ b/Assets/External/websocket-sharp/Net/EndPointListener.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 9545c19e60d2db24db67dfb6448ffb6b \ No newline at end of file diff --git a/Assets/External/websocket-sharp/Net/EndPointManager.cs b/Assets/External/websocket-sharp/Net/EndPointManager.cs new file mode 100644 index 00000000..ac4582b2 --- /dev/null +++ b/Assets/External/websocket-sharp/Net/EndPointManager.cs @@ -0,0 +1,261 @@ +#region License +/* + * EndPointManager.cs + * + * This code is derived from EndPointManager.cs (System.Net) of Mono + * (http://www.mono-project.com). + * + * The MIT License + * + * Copyright (c) 2005 Novell, Inc. (http://www.novell.com) + * Copyright (c) 2012-2020 sta.blockhead + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#endregion + +#region Authors +/* + * Authors: + * - Gonzalo Paniagua Javier + */ +#endregion + +#region Contributors +/* + * Contributors: + * - Liryna + */ +#endregion + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Net; + +namespace WebSocketSharp.Net +{ + internal sealed class EndPointManager + { + #region Private Fields + + private static readonly Dictionary _endpoints; + + #endregion + + #region Static Constructor + + static EndPointManager () + { + _endpoints = new Dictionary (); + } + + #endregion + + #region Private Constructors + + private EndPointManager () + { + } + + #endregion + + #region Private Methods + + private static void addPrefix (string uriPrefix, HttpListener listener) + { + var pref = new HttpListenerPrefix (uriPrefix, listener); + + var addr = convertToIPAddress (pref.Host); + + if (addr == null) { + var msg = "The URI prefix includes an invalid host."; + + throw new HttpListenerException (87, msg); + } + + if (!addr.IsLocal ()) { + var msg = "The URI prefix includes an invalid host."; + + throw new HttpListenerException (87, msg); + } + + int port; + + if (!Int32.TryParse (pref.Port, out port)) { + var msg = "The URI prefix includes an invalid port."; + + throw new HttpListenerException (87, msg); + } + + if (!port.IsPortNumber ()) { + var msg = "The URI prefix includes an invalid port."; + + throw new HttpListenerException (87, msg); + } + + var path = pref.Path; + + if (path.IndexOf ('%') != -1) { + var msg = "The URI prefix includes an invalid path."; + + throw new HttpListenerException (87, msg); + } + + if (path.IndexOf ("//", StringComparison.Ordinal) != -1) { + var msg = "The URI prefix includes an invalid path."; + + throw new HttpListenerException (87, msg); + } + + var endpoint = new IPEndPoint (addr, port); + + EndPointListener lsnr; + + if (_endpoints.TryGetValue (endpoint, out lsnr)) { + if (lsnr.IsSecure ^ pref.IsSecure) { + var msg = "The URI prefix includes an invalid scheme."; + + throw new HttpListenerException (87, msg); + } + } + else { + lsnr = new EndPointListener ( + endpoint, + pref.IsSecure, + listener.CertificateFolderPath, + listener.SslConfiguration, + listener.ReuseAddress + ); + + _endpoints.Add (endpoint, lsnr); + } + + lsnr.AddPrefix (pref); + } + + private static IPAddress convertToIPAddress (string hostname) + { + if (hostname == "*") + return IPAddress.Any; + + if (hostname == "+") + return IPAddress.Any; + + return hostname.ToIPAddress (); + } + + private static void removePrefix (string uriPrefix, HttpListener listener) + { + var pref = new HttpListenerPrefix (uriPrefix, listener); + + var addr = convertToIPAddress (pref.Host); + + if (addr == null) + return; + + if (!addr.IsLocal ()) + return; + + int port; + + if (!Int32.TryParse (pref.Port, out port)) + return; + + if (!port.IsPortNumber ()) + return; + + var path = pref.Path; + + if (path.IndexOf ('%') != -1) + return; + + if (path.IndexOf ("//", StringComparison.Ordinal) != -1) + return; + + var endpoint = new IPEndPoint (addr, port); + + EndPointListener lsnr; + + if (!_endpoints.TryGetValue (endpoint, out lsnr)) + return; + + if (lsnr.IsSecure ^ pref.IsSecure) + return; + + lsnr.RemovePrefix (pref); + } + + #endregion + + #region Internal Methods + + internal static bool RemoveEndPoint (IPEndPoint endpoint) + { + lock (((ICollection) _endpoints).SyncRoot) + return _endpoints.Remove (endpoint); + } + + #endregion + + #region Public Methods + + public static void AddListener (HttpListener listener) + { + var added = new List (); + + lock (((ICollection) _endpoints).SyncRoot) { + try { + foreach (var pref in listener.Prefixes) { + addPrefix (pref, listener); + added.Add (pref); + } + } + catch { + foreach (var pref in added) + removePrefix (pref, listener); + + throw; + } + } + } + + public static void AddPrefix (string uriPrefix, HttpListener listener) + { + lock (((ICollection) _endpoints).SyncRoot) + addPrefix (uriPrefix, listener); + } + + public static void RemoveListener (HttpListener listener) + { + lock (((ICollection) _endpoints).SyncRoot) { + foreach (var pref in listener.Prefixes) + removePrefix (pref, listener); + } + } + + public static void RemovePrefix (string uriPrefix, HttpListener listener) + { + lock (((ICollection) _endpoints).SyncRoot) + removePrefix (uriPrefix, listener); + } + + #endregion + } +} diff --git a/Assets/External/websocket-sharp/Net/EndPointManager.cs.meta b/Assets/External/websocket-sharp/Net/EndPointManager.cs.meta new file mode 100644 index 00000000..76021b40 --- /dev/null +++ b/Assets/External/websocket-sharp/Net/EndPointManager.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 420d942fd863ffb48a7a0a77a21417ea \ No newline at end of file diff --git a/Assets/External/websocket-sharp/Net/HttpBasicIdentity.cs b/Assets/External/websocket-sharp/Net/HttpBasicIdentity.cs new file mode 100644 index 00000000..d26b29f6 --- /dev/null +++ b/Assets/External/websocket-sharp/Net/HttpBasicIdentity.cs @@ -0,0 +1,82 @@ +#region License +/* + * HttpBasicIdentity.cs + * + * This code is derived from HttpListenerBasicIdentity.cs (System.Net) of + * Mono (http://www.mono-project.com). + * + * The MIT License + * + * Copyright (c) 2005 Novell, Inc. (http://www.novell.com) + * Copyright (c) 2014-2017 sta.blockhead + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#endregion + +#region Authors +/* + * Authors: + * - Gonzalo Paniagua Javier + */ +#endregion + +using System; +using System.Security.Principal; + +namespace WebSocketSharp.Net +{ + /// + /// Holds the username and password from an HTTP Basic authentication attempt. + /// + public class HttpBasicIdentity : GenericIdentity + { + #region Private Fields + + private string _password; + + #endregion + + #region Internal Constructors + + internal HttpBasicIdentity (string username, string password) + : base (username, "Basic") + { + _password = password; + } + + #endregion + + #region Public Properties + + /// + /// Gets the password from a basic authentication attempt. + /// + /// + /// A that represents the password. + /// + public virtual string Password { + get { + return _password; + } + } + + #endregion + } +} diff --git a/Assets/External/websocket-sharp/Net/HttpBasicIdentity.cs.meta b/Assets/External/websocket-sharp/Net/HttpBasicIdentity.cs.meta new file mode 100644 index 00000000..04dae55a --- /dev/null +++ b/Assets/External/websocket-sharp/Net/HttpBasicIdentity.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 8488c11ffdda6dc4cb98b844ec000b25 \ No newline at end of file diff --git a/Assets/External/websocket-sharp/Net/HttpConnection.cs b/Assets/External/websocket-sharp/Net/HttpConnection.cs new file mode 100644 index 00000000..e51efb11 --- /dev/null +++ b/Assets/External/websocket-sharp/Net/HttpConnection.cs @@ -0,0 +1,653 @@ +#region License +/* + * HttpConnection.cs + * + * This code is derived from HttpConnection.cs (System.Net) of Mono + * (http://www.mono-project.com). + * + * The MIT License + * + * Copyright (c) 2005 Novell, Inc. (http://www.novell.com) + * Copyright (c) 2012-2025 sta.blockhead + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#endregion + +#region Authors +/* + * Authors: + * - Gonzalo Paniagua Javier + */ +#endregion + +#region Contributors +/* + * Contributors: + * - Liryna + * - Rohan Singh + */ +#endregion + +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Net.Security; +using System.Net.Sockets; +using System.Text; +using System.Threading; + +namespace WebSocketSharp.Net +{ + internal sealed class HttpConnection + { + #region Private Fields + + private int _attempts; + private byte[] _buffer; + private static readonly int _bufferLength; + private HttpListenerContext _context; + private StringBuilder _currentLine; + private EndPointListener _endPointListener; + private InputState _inputState; + private RequestStream _inputStream; + private bool _isSecure; + private LineState _lineState; + private EndPoint _localEndPoint; + private static readonly int _maxInputLength; + private ResponseStream _outputStream; + private int _position; + private EndPoint _remoteEndPoint; + private MemoryStream _requestBuffer; + private int _reuses; + private Socket _socket; + private Stream _stream; + private object _sync; + private int _timeout; + private Dictionary _timeoutCanceled; + private Timer _timer; + + #endregion + + #region Static Constructor + + static HttpConnection () + { + _bufferLength = 8192; + _maxInputLength = 32768; + } + + #endregion + + #region Internal Constructors + + internal HttpConnection (Socket socket, EndPointListener listener) + { + _socket = socket; + _endPointListener = listener; + + var netStream = new NetworkStream (socket, false); + + if (listener.IsSecure) { + var sslConf = listener.SslConfiguration; + var sslStream = new SslStream ( + netStream, + false, + sslConf.ClientCertificateValidationCallback + ); + + sslStream.AuthenticateAsServer ( + sslConf.ServerCertificate, + sslConf.ClientCertificateRequired, + sslConf.EnabledSslProtocols, + sslConf.CheckCertificateRevocation + ); + + _isSecure = true; + _stream = sslStream; + } + else { + _stream = netStream; + } + + _buffer = new byte[_bufferLength]; + _localEndPoint = socket.LocalEndPoint; + _remoteEndPoint = socket.RemoteEndPoint; + _sync = new object (); + _timeoutCanceled = new Dictionary (); + _timer = new Timer (onTimeout, this, Timeout.Infinite, Timeout.Infinite); + + // 90k ms for first request, 15k ms from then on. + init (new MemoryStream (), 90000); + } + + #endregion + + #region Public Properties + + public bool IsClosed { + get { + return _socket == null; + } + } + + public bool IsLocal { + get { + return ((IPEndPoint) _remoteEndPoint).Address.IsLocal (); + } + } + + public bool IsSecure { + get { + return _isSecure; + } + } + + public IPEndPoint LocalEndPoint { + get { + return (IPEndPoint) _localEndPoint; + } + } + + public IPEndPoint RemoteEndPoint { + get { + return (IPEndPoint) _remoteEndPoint; + } + } + + public int Reuses { + get { + return _reuses; + } + } + + public Socket Socket { + get { + return _socket; + } + } + + public Stream Stream { + get { + return _stream; + } + } + + #endregion + + #region Private Methods + + private void close () + { + lock (_sync) { + if (_socket == null) + return; + + disposeTimer (); + disposeRequestBuffer (); + disposeStream (); + closeSocket (); + } + + _context.Unregister (); + _endPointListener.RemoveConnection (this); + } + + private void closeSocket () + { + try { + _socket.Shutdown (SocketShutdown.Both); + } + catch { + } + + _socket.Close (); + + _socket = null; + } + + private static MemoryStream createRequestBuffer ( + RequestStream inputStream + ) + { + var ret = new MemoryStream (); + + if (inputStream is ChunkedRequestStream) { + var crs = (ChunkedRequestStream) inputStream; + + if (crs.HasRemainingBuffer) { + var buff = crs.RemainingBuffer; + + ret.Write (buff, 0, buff.Length); + } + + return ret; + } + + var cnt = inputStream.Count; + + if (cnt > 0) + ret.Write (inputStream.InitialBuffer, inputStream.Offset, cnt); + + return ret; + } + + private void disposeRequestBuffer () + { + if (_requestBuffer == null) + return; + + _requestBuffer.Dispose (); + + _requestBuffer = null; + } + + private void disposeStream () + { + if (_stream == null) + return; + + _stream.Dispose (); + + _stream = null; + } + + private void disposeTimer () + { + if (_timer == null) + return; + + try { + _timer.Change (Timeout.Infinite, Timeout.Infinite); + } + catch { + } + + _timer.Dispose (); + + _timer = null; + } + + private void init (MemoryStream requestBuffer, int timeout) + { + _requestBuffer = requestBuffer; + _timeout = timeout; + + _context = new HttpListenerContext (this); + _currentLine = new StringBuilder (64); + _inputState = InputState.RequestLine; + _inputStream = null; + _lineState = LineState.None; + _outputStream = null; + _position = 0; + } + + private static void onRead (IAsyncResult asyncResult) + { + var conn = (HttpConnection) asyncResult.AsyncState; + var current = conn._attempts; + + if (conn._socket == null) + return; + + lock (conn._sync) { + if (conn._socket == null) + return; + + conn._timer.Change (Timeout.Infinite, Timeout.Infinite); + conn._timeoutCanceled[current] = true; + + var nread = 0; + + try { + nread = conn._stream.EndRead (asyncResult); + } + catch (Exception) { + // TODO: Logging. + + conn.close (); + + return; + } + + if (nread <= 0) { + conn.close (); + + return; + } + + conn._requestBuffer.Write (conn._buffer, 0, nread); + + if (conn.processRequestBuffer ()) + return; + + conn.BeginReadRequest (); + } + } + + private static void onTimeout (object state) + { + var conn = (HttpConnection) state; + var current = conn._attempts; + + if (conn._socket == null) + return; + + lock (conn._sync) { + if (conn._socket == null) + return; + + if (conn._timeoutCanceled[current]) + return; + + conn._context.SendError (408); + } + } + + private bool processInput (byte[] data, int length) + { + // This method returns a bool: + // - true Done processing + // - false Need more input + + var req = _context.Request; + + try { + while (true) { + int nread; + var line = readLineFrom (data, _position, length, out nread); + + _position += nread; + + if (line == null) + break; + + if (line.Length == 0) { + if (_inputState == InputState.RequestLine) + continue; + + if (_position > _maxInputLength) + _context.ErrorMessage = "Headers too long"; + + return true; + } + + if (_inputState == InputState.RequestLine) { + req.SetRequestLine (line); + + _inputState = InputState.Headers; + } + else { + req.AddHeader (line); + } + + if (_context.HasErrorMessage) + return true; + } + } + catch (Exception) { + // TODO: Logging. + + _context.ErrorMessage = "Processing failure"; + + return true; + } + + if (_position >= _maxInputLength) { + _context.ErrorMessage = "Headers too long"; + + return true; + } + + return false; + } + + private bool processRequestBuffer () + { + // This method returns a bool: + // - true Done processing + // - false Need more write + + var data = _requestBuffer.GetBuffer (); + var len = (int) _requestBuffer.Length; + + if (!processInput (data, len)) + return false; + + var req = _context.Request; + + if (!_context.HasErrorMessage) + req.FinishInitialization (); + + if (_context.HasErrorMessage) { + _context.SendError (); + + return true; + } + + var uri = req.Url; + HttpListener httplsnr; + + if (!_endPointListener.TrySearchHttpListener (uri, out httplsnr)) { + _context.SendError (404); + + return true; + } + + httplsnr.RegisterContext (_context); + + return true; + } + + private string readLineFrom ( + byte[] buffer, + int offset, + int length, + out int nread + ) + { + nread = 0; + + for (var i = offset; i < length; i++) { + nread++; + + var b = buffer[i]; + + if (b == 13) { + _lineState = LineState.Cr; + + continue; + } + + if (b == 10) { + _lineState = LineState.Lf; + + break; + } + + _currentLine.Append ((char) b); + } + + if (_lineState != LineState.Lf) + return null; + + var ret = _currentLine.ToString (); + + _currentLine.Length = 0; + _lineState = LineState.None; + + return ret; + } + + private MemoryStream takeOverRequestBuffer () + { + if (_inputStream != null) + return createRequestBuffer (_inputStream); + + var ret = new MemoryStream (); + + var buff = _requestBuffer.GetBuffer (); + var len = (int) _requestBuffer.Length; + var cnt = len - _position; + + if (cnt > 0) + ret.Write (buff, _position, cnt); + + disposeRequestBuffer (); + + return ret; + } + + #endregion + + #region Internal Methods + + internal void BeginReadRequest () + { + _attempts++; + + _timeoutCanceled.Add (_attempts, false); + _timer.Change (_timeout, Timeout.Infinite); + + try { + _stream.BeginRead (_buffer, 0, _bufferLength, onRead, this); + } + catch (Exception) { + // TODO: Logging. + + close (); + } + } + + internal void Close (bool force) + { + if (_socket == null) + return; + + lock (_sync) { + if (_socket == null) + return; + + if (force) { + if (_outputStream != null) + _outputStream.Close (true); + + close (); + + return; + } + + GetResponseStream ().Close (false); + + if (_context.Response.CloseConnection) { + close (); + + return; + } + + if (!_context.Request.FlushInput ()) { + close (); + + return; + } + + _context.Unregister (); + + _reuses++; + + var buff = takeOverRequestBuffer (); + var len = buff.Length; + + init (buff, 15000); + + if (len > 0) { + if (processRequestBuffer ()) + return; + } + + BeginReadRequest (); + } + } + + #endregion + + #region Public Methods + + public void Close () + { + Close (false); + } + + public RequestStream GetRequestStream (long contentLength, bool chunked) + { + lock (_sync) { + if (_socket == null) + return null; + + if (_inputStream != null) + return _inputStream; + + var buff = _requestBuffer.GetBuffer (); + var len = (int) _requestBuffer.Length; + var cnt = len - _position; + + _inputStream = chunked + ? new ChunkedRequestStream ( + _stream, + buff, + _position, + cnt, + _context + ) + : new RequestStream ( + _stream, + buff, + _position, + cnt, + contentLength + ); + + disposeRequestBuffer (); + + return _inputStream; + } + } + + public ResponseStream GetResponseStream () + { + lock (_sync) { + if (_socket == null) + return null; + + if (_outputStream != null) + return _outputStream; + + var lsnr = _context.Listener; + var ignore = lsnr != null ? lsnr.IgnoreWriteExceptions : true; + + _outputStream = new ResponseStream (_stream, _context.Response, ignore); + + return _outputStream; + } + } + + #endregion + } +} diff --git a/Assets/External/websocket-sharp/Net/HttpConnection.cs.meta b/Assets/External/websocket-sharp/Net/HttpConnection.cs.meta new file mode 100644 index 00000000..8573846e --- /dev/null +++ b/Assets/External/websocket-sharp/Net/HttpConnection.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 08ab2f95b185d50488a0fe48f5dd5189 \ No newline at end of file diff --git a/Assets/External/websocket-sharp/Net/HttpDigestIdentity.cs b/Assets/External/websocket-sharp/Net/HttpDigestIdentity.cs new file mode 100644 index 00000000..e2863aa9 --- /dev/null +++ b/Assets/External/websocket-sharp/Net/HttpDigestIdentity.cs @@ -0,0 +1,192 @@ +#region License +/* + * HttpDigestIdentity.cs + * + * The MIT License + * + * Copyright (c) 2014-2024 sta.blockhead + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#endregion + +using System; +using System.Collections.Specialized; +using System.Security.Principal; + +namespace WebSocketSharp.Net +{ + /// + /// Holds the username and other parameters from an HTTP Digest + /// authentication attempt. + /// + public class HttpDigestIdentity : GenericIdentity + { + #region Private Fields + + private NameValueCollection _parameters; + + #endregion + + #region Internal Constructors + + internal HttpDigestIdentity (NameValueCollection parameters) + : base (parameters["username"], "Digest") + { + _parameters = parameters; + } + + #endregion + + #region Public Properties + + /// + /// Gets the algorithm parameter from a digest authentication attempt. + /// + /// + /// A that represents the algorithm parameter. + /// + public string Algorithm { + get { + return _parameters["algorithm"]; + } + } + + /// + /// Gets the cnonce parameter from a digest authentication attempt. + /// + /// + /// A that represents the cnonce parameter. + /// + public string Cnonce { + get { + return _parameters["cnonce"]; + } + } + + /// + /// Gets the nc parameter from a digest authentication attempt. + /// + /// + /// A that represents the nc parameter. + /// + public string Nc { + get { + return _parameters["nc"]; + } + } + + /// + /// Gets the nonce parameter from a digest authentication attempt. + /// + /// + /// A that represents the nonce parameter. + /// + public string Nonce { + get { + return _parameters["nonce"]; + } + } + + /// + /// Gets the opaque parameter from a digest authentication attempt. + /// + /// + /// A that represents the opaque parameter. + /// + public string Opaque { + get { + return _parameters["opaque"]; + } + } + + /// + /// Gets the qop parameter from a digest authentication attempt. + /// + /// + /// A that represents the qop parameter. + /// + public string Qop { + get { + return _parameters["qop"]; + } + } + + /// + /// Gets the realm parameter from a digest authentication attempt. + /// + /// + /// A that represents the realm parameter. + /// + public string Realm { + get { + return _parameters["realm"]; + } + } + + /// + /// Gets the response parameter from a digest authentication attempt. + /// + /// + /// A that represents the response parameter. + /// + public string Response { + get { + return _parameters["response"]; + } + } + + /// + /// Gets the uri parameter from a digest authentication attempt. + /// + /// + /// A that represents the uri parameter. + /// + public string Uri { + get { + return _parameters["uri"]; + } + } + + #endregion + + #region Internal Methods + + internal bool IsValid ( + string password, + string realm, + string method, + string entity + ) + { + var parameters = new NameValueCollection (_parameters); + + parameters["password"] = password; + parameters["realm"] = realm; + parameters["method"] = method; + parameters["entity"] = entity; + + var expectedDigest = AuthenticationResponse.CreateRequestDigest (parameters); + + return _parameters["response"] == expectedDigest; + } + + #endregion + } +} diff --git a/Assets/External/websocket-sharp/Net/HttpDigestIdentity.cs.meta b/Assets/External/websocket-sharp/Net/HttpDigestIdentity.cs.meta new file mode 100644 index 00000000..58d2b565 --- /dev/null +++ b/Assets/External/websocket-sharp/Net/HttpDigestIdentity.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 7640330cbf592d14184f90eee16b8094 \ No newline at end of file diff --git a/Assets/External/websocket-sharp/Net/HttpHeaderInfo.cs b/Assets/External/websocket-sharp/Net/HttpHeaderInfo.cs new file mode 100644 index 00000000..2b77e3d9 --- /dev/null +++ b/Assets/External/websocket-sharp/Net/HttpHeaderInfo.cs @@ -0,0 +1,128 @@ +#region License +/* + * HttpHeaderInfo.cs + * + * The MIT License + * + * Copyright (c) 2013-2020 sta.blockhead + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#endregion + +using System; + +namespace WebSocketSharp.Net +{ + internal class HttpHeaderInfo + { + #region Private Fields + + private string _headerName; + private HttpHeaderType _headerType; + + #endregion + + #region Internal Constructors + + internal HttpHeaderInfo (string headerName, HttpHeaderType headerType) + { + _headerName = headerName; + _headerType = headerType; + } + + #endregion + + #region Internal Properties + + internal bool IsMultiValueInRequest { + get { + var headerType = _headerType & HttpHeaderType.MultiValueInRequest; + + return headerType == HttpHeaderType.MultiValueInRequest; + } + } + + internal bool IsMultiValueInResponse { + get { + var headerType = _headerType & HttpHeaderType.MultiValueInResponse; + + return headerType == HttpHeaderType.MultiValueInResponse; + } + } + + #endregion + + #region Public Properties + + public string HeaderName { + get { + return _headerName; + } + } + + public HttpHeaderType HeaderType { + get { + return _headerType; + } + } + + public bool IsRequest { + get { + var headerType = _headerType & HttpHeaderType.Request; + + return headerType == HttpHeaderType.Request; + } + } + + public bool IsResponse { + get { + var headerType = _headerType & HttpHeaderType.Response; + + return headerType == HttpHeaderType.Response; + } + } + + #endregion + + #region Public Methods + + public bool IsMultiValue (bool response) + { + var headerType = _headerType & HttpHeaderType.MultiValue; + + if (headerType != HttpHeaderType.MultiValue) + return response ? IsMultiValueInResponse : IsMultiValueInRequest; + + return response ? IsResponse : IsRequest; + } + + public bool IsRestricted (bool response) + { + var headerType = _headerType & HttpHeaderType.Restricted; + + if (headerType != HttpHeaderType.Restricted) + return false; + + return response ? IsResponse : IsRequest; + } + + #endregion + } +} diff --git a/Assets/External/websocket-sharp/Net/HttpHeaderInfo.cs.meta b/Assets/External/websocket-sharp/Net/HttpHeaderInfo.cs.meta new file mode 100644 index 00000000..71762326 --- /dev/null +++ b/Assets/External/websocket-sharp/Net/HttpHeaderInfo.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 658e30bfa268d7e40a400018e371487b \ No newline at end of file diff --git a/Assets/External/websocket-sharp/Net/HttpHeaderType.cs b/Assets/External/websocket-sharp/Net/HttpHeaderType.cs new file mode 100644 index 00000000..113fb63b --- /dev/null +++ b/Assets/External/websocket-sharp/Net/HttpHeaderType.cs @@ -0,0 +1,44 @@ +#region License +/* + * HttpHeaderType.cs + * + * The MIT License + * + * Copyright (c) 2013-2014 sta.blockhead + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#endregion + +using System; + +namespace WebSocketSharp.Net +{ + [Flags] + internal enum HttpHeaderType + { + Unspecified = 0, + Request = 1, + Response = 1 << 1, + Restricted = 1 << 2, + MultiValue = 1 << 3, + MultiValueInRequest = 1 << 4, + MultiValueInResponse = 1 << 5 + } +} diff --git a/Assets/External/websocket-sharp/Net/HttpHeaderType.cs.meta b/Assets/External/websocket-sharp/Net/HttpHeaderType.cs.meta new file mode 100644 index 00000000..f0437edd --- /dev/null +++ b/Assets/External/websocket-sharp/Net/HttpHeaderType.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: f1d86fad644dba148ae08c1ab9e638ee \ No newline at end of file diff --git a/Assets/External/websocket-sharp/Net/HttpListener.cs b/Assets/External/websocket-sharp/Net/HttpListener.cs new file mode 100644 index 00000000..44b964c0 --- /dev/null +++ b/Assets/External/websocket-sharp/Net/HttpListener.cs @@ -0,0 +1,1028 @@ +#region License +/* + * HttpListener.cs + * + * This code is derived from HttpListener.cs (System.Net) of Mono + * (http://www.mono-project.com). + * + * The MIT License + * + * Copyright (c) 2005 Novell, Inc. (http://www.novell.com) + * Copyright (c) 2012-2024 sta.blockhead + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#endregion + +#region Authors +/* + * Authors: + * - Gonzalo Paniagua Javier + */ +#endregion + +#region Contributors +/* + * Contributors: + * - Liryna + */ +#endregion + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Security.Cryptography.X509Certificates; +using System.Security.Principal; +using System.Threading; + +// TODO: Logging. +namespace WebSocketSharp.Net +{ + /// + /// Provides a simple, programmatically controlled HTTP listener. + /// + /// + /// + /// The listener supports HTTP/1.1 version request and response. + /// + /// + /// And the listener allows to accept WebSocket handshake requests. + /// + /// + /// This class cannot be inherited. + /// + /// + public sealed class HttpListener : IDisposable + { + #region Private Fields + + private AuthenticationSchemes _authSchemes; + private Func _authSchemeSelector; + private string _certFolderPath; + private Queue _contextQueue; + private LinkedList _contextRegistry; + private object _contextRegistrySync; + private static readonly string _defaultRealm; + private bool _disposed; + private bool _ignoreWriteExceptions; + private volatile bool _isListening; + private Logger _log; + private HttpListenerPrefixCollection _prefixes; + private string _realm; + private bool _reuseAddress; + private ServerSslConfiguration _sslConfig; + private object _sync; + private Func _userCredFinder; + private Queue _waitQueue; + + #endregion + + #region Static Constructor + + static HttpListener () + { + _defaultRealm = "SECRET AREA"; + } + + #endregion + + #region Public Constructors + + /// + /// Initializes a new instance of the class. + /// + public HttpListener () + { + _authSchemes = AuthenticationSchemes.Anonymous; + _contextQueue = new Queue (); + _contextRegistry = new LinkedList (); + _contextRegistrySync = ((ICollection) _contextRegistry).SyncRoot; + _log = new Logger (); + _prefixes = new HttpListenerPrefixCollection (this); + _sync = new object (); + _waitQueue = new Queue (); + } + + #endregion + + #region Internal Properties + + internal string ObjectName { + get { + return GetType ().ToString (); + } + } + + internal bool ReuseAddress { + get { + return _reuseAddress; + } + + set { + _reuseAddress = value; + } + } + + #endregion + + #region Public Properties + + /// + /// Gets or sets the scheme used to authenticate the clients. + /// + /// + /// + /// One of the + /// enum values. + /// + /// + /// It represents the scheme used to authenticate the clients. + /// + /// + /// The default value is + /// . + /// + /// + /// + /// This listener has been closed. + /// + public AuthenticationSchemes AuthenticationSchemes { + get { + if (_disposed) + throw new ObjectDisposedException (ObjectName); + + return _authSchemes; + } + + set { + if (_disposed) + throw new ObjectDisposedException (ObjectName); + + _authSchemes = value; + } + } + + /// + /// Gets or sets the delegate called to determine the scheme used to + /// authenticate the clients. + /// + /// + /// + /// If this property is set, the listener uses the authentication + /// scheme selected by the delegate for each request. + /// + /// + /// Or if this property is not set, the listener uses the value of + /// the property + /// as the authentication scheme for all requests. + /// + /// + /// + /// + /// A + /// delegate. + /// + /// + /// It represents the delegate called when the listener selects + /// an authentication scheme. + /// + /// + /// if not necessary. + /// + /// + /// The default value is . + /// + /// + /// + /// This listener has been closed. + /// + public Func AuthenticationSchemeSelector { + get { + if (_disposed) + throw new ObjectDisposedException (ObjectName); + + return _authSchemeSelector; + } + + set { + if (_disposed) + throw new ObjectDisposedException (ObjectName); + + _authSchemeSelector = value; + } + } + + /// + /// Gets or sets the path to the folder in which stores the certificate + /// files used to authenticate the server on the secure connection. + /// + /// + /// + /// This property represents the path to the folder in which stores + /// the certificate files associated with each port number of added + /// URI prefixes. + /// + /// + /// A set of the certificate files is a pair of <port number>.cer + /// (DER) and <port number>.key (DER, RSA Private Key). + /// + /// + /// If this property is or an empty string, + /// the result of the + /// with the method is used as + /// the default path. + /// + /// + /// + /// + /// A that represents the path to the folder + /// in which stores the certificate files. + /// + /// + /// The default value is . + /// + /// + /// + /// This listener has been closed. + /// + public string CertificateFolderPath { + get { + if (_disposed) + throw new ObjectDisposedException (ObjectName); + + return _certFolderPath; + } + + set { + if (_disposed) + throw new ObjectDisposedException (ObjectName); + + _certFolderPath = value; + } + } + + /// + /// Gets or sets a value indicating whether the listener returns + /// exceptions that occur when sending the response to the client. + /// + /// + /// + /// true if the listener should not return those exceptions; + /// otherwise, false. + /// + /// + /// The default value is false. + /// + /// + /// + /// This listener has been closed. + /// + public bool IgnoreWriteExceptions { + get { + if (_disposed) + throw new ObjectDisposedException (ObjectName); + + return _ignoreWriteExceptions; + } + + set { + if (_disposed) + throw new ObjectDisposedException (ObjectName); + + _ignoreWriteExceptions = value; + } + } + + /// + /// Gets a value indicating whether the listener has been started. + /// + /// + /// true if the listener has been started; otherwise, false. + /// + public bool IsListening { + get { + return _isListening; + } + } + + /// + /// Gets a value indicating whether the listener can be used with + /// the current operating system. + /// + /// + /// true. + /// + public static bool IsSupported { + get { + return true; + } + } + + /// + /// Gets the logging functions. + /// + /// + /// + /// The default logging level is . + /// + /// + /// If you would like to change it, you should set the Log.Level + /// property to any of the enum values. + /// + /// + /// + /// A that provides the logging functions. + /// + /// + /// This listener has been closed. + /// + public Logger Log { + get { + if (_disposed) + throw new ObjectDisposedException (ObjectName); + + return _log; + } + } + + /// + /// Gets the URI prefixes handled by the listener. + /// + /// + /// A that contains the URI + /// prefixes. + /// + /// + /// This listener has been closed. + /// + public HttpListenerPrefixCollection Prefixes { + get { + if (_disposed) + throw new ObjectDisposedException (ObjectName); + + return _prefixes; + } + } + + /// + /// Gets or sets the name of the realm associated with the listener. + /// + /// + /// If this property is or an empty string, + /// "SECRET AREA" is used as the name of the realm. + /// + /// + /// + /// A that represents the name of the realm. + /// + /// + /// The default value is . + /// + /// + /// + /// This listener has been closed. + /// + public string Realm { + get { + if (_disposed) + throw new ObjectDisposedException (ObjectName); + + return _realm; + } + + set { + if (_disposed) + throw new ObjectDisposedException (ObjectName); + + _realm = value; + } + } + + /// + /// Gets the configuration for secure connection. + /// + /// + /// A that represents the + /// configuration used to provide secure connections. + /// + /// + /// This listener has been closed. + /// + public ServerSslConfiguration SslConfiguration { + get { + if (_disposed) + throw new ObjectDisposedException (ObjectName); + + if (_sslConfig == null) + _sslConfig = new ServerSslConfiguration (); + + return _sslConfig; + } + } + + /// + /// Gets or sets a value indicating whether, when NTLM authentication is used, + /// the authentication information of first request is used to authenticate + /// additional requests on the same connection. + /// + /// + /// This property is not currently supported and always throws + /// a . + /// + /// + /// true if the authentication information of first request is used; + /// otherwise, false. + /// + /// + /// Any use of this property. + /// + public bool UnsafeConnectionNtlmAuthentication { + get { + throw new NotSupportedException (); + } + + set { + throw new NotSupportedException (); + } + } + + /// + /// Gets or sets the delegate called to find the credentials for + /// an identity used to authenticate a client. + /// + /// + /// + /// A + /// delegate. + /// + /// + /// It represents the delegate called when the listener finds + /// the credentials used to authenticate a client. + /// + /// + /// It must return if the credentials + /// are not found. + /// + /// + /// if not necessary. + /// + /// + /// The default value is . + /// + /// + /// + /// This listener has been closed. + /// + public Func UserCredentialsFinder { + get { + if (_disposed) + throw new ObjectDisposedException (ObjectName); + + return _userCredFinder; + } + + set { + if (_disposed) + throw new ObjectDisposedException (ObjectName); + + _userCredFinder = value; + } + } + + #endregion + + #region Private Methods + + private bool authenticateClient (HttpListenerContext context) + { + var schm = selectAuthenticationScheme (context.Request); + + if (schm == AuthenticationSchemes.Anonymous) + return true; + + if (schm == AuthenticationSchemes.None) { + var msg = "Authentication not allowed"; + + context.SendError (403, msg); + + return false; + } + + var realm = getRealm (); + + if (!context.SetUser (schm, realm, _userCredFinder)) { + context.SendAuthenticationChallenge (schm, realm); + + return false; + } + + return true; + } + + private HttpListenerAsyncResult beginGetContext ( + AsyncCallback callback, + object state + ) + { + lock (_contextRegistrySync) { + if (!_isListening) { + var msg = "The method is canceled."; + + throw new HttpListenerException (995, msg); + } + + var ares = new HttpListenerAsyncResult (callback, state, _log); + + if (_contextQueue.Count == 0) { + _waitQueue.Enqueue (ares); + + return ares; + } + + var ctx = _contextQueue.Dequeue (); + + ares.Complete (ctx, true); + + return ares; + } + } + + private void cleanupContextQueue (bool force) + { + if (_contextQueue.Count == 0) + return; + + if (force) { + _contextQueue.Clear (); + + return; + } + + var ctxs = _contextQueue.ToArray (); + + _contextQueue.Clear (); + + foreach (var ctx in ctxs) + ctx.SendError (503); + } + + private void cleanupContextRegistry () + { + var cnt = _contextRegistry.Count; + + if (cnt == 0) + return; + + var ctxs = new HttpListenerContext[cnt]; + + lock (_contextRegistrySync) { + _contextRegistry.CopyTo (ctxs, 0); + _contextRegistry.Clear (); + } + + foreach (var ctx in ctxs) + ctx.Connection.Close (true); + } + + private void cleanupWaitQueue (string message) + { + if (_waitQueue.Count == 0) + return; + + var aress = _waitQueue.ToArray (); + + _waitQueue.Clear (); + + foreach (var ares in aress) { + var ex = new HttpListenerException (995, message); + + ares.Complete (ex); + } + } + + private void close (bool force) + { + lock (_sync) { + if (_disposed) + return; + + lock (_contextRegistrySync) { + if (!_isListening) { + _disposed = true; + + return; + } + + _isListening = false; + } + + cleanupContextQueue (force); + cleanupContextRegistry (); + + var msg = "The listener is closed."; + + cleanupWaitQueue (msg); + + EndPointManager.RemoveListener (this); + + _disposed = true; + } + } + + private string getRealm () + { + var realm = _realm; + + return realm != null && realm.Length > 0 ? realm : _defaultRealm; + } + + private bool registerContext (HttpListenerContext context) + { + if (!_isListening) + return false; + + lock (_contextRegistrySync) { + if (!_isListening) + return false; + + context.Listener = this; + + _contextRegistry.AddLast (context); + + if (_waitQueue.Count == 0) { + _contextQueue.Enqueue (context); + + return true; + } + + var ares = _waitQueue.Dequeue (); + + ares.Complete (context, false); + + return true; + } + } + + private AuthenticationSchemes selectAuthenticationScheme ( + HttpListenerRequest request + ) + { + var selector = _authSchemeSelector; + + if (selector == null) + return _authSchemes; + + try { + return selector (request); + } + catch { + return AuthenticationSchemes.None; + } + } + + #endregion + + #region Internal Methods + + internal void CheckDisposed () + { + if (_disposed) + throw new ObjectDisposedException (ObjectName); + } + + internal bool RegisterContext (HttpListenerContext context) + { + if (!authenticateClient (context)) + return false; + + if (!registerContext (context)) { + context.SendError (503); + + return false; + } + + return true; + } + + internal void UnregisterContext (HttpListenerContext context) + { + lock (_contextRegistrySync) + _contextRegistry.Remove (context); + } + + #endregion + + #region Public Methods + + /// + /// Shuts down the listener immediately. + /// + public void Abort () + { + if (_disposed) + return; + + close (true); + } + + /// + /// Begins getting an incoming request asynchronously. + /// + /// + /// + /// This asynchronous operation must be ended by calling + /// the method. + /// + /// + /// Typically, the method is called by + /// . + /// + /// + /// + /// An instance that represents the status of + /// the asynchronous operation. + /// + /// + /// + /// An delegate. + /// + /// + /// It specifies the delegate called when the asynchronous operation is + /// complete. + /// + /// + /// + /// An that specifies a user defined object to pass to + /// . + /// + /// + /// This method is canceled. + /// + /// + /// + /// This listener has not been started or is currently stopped. + /// + /// + /// -or- + /// + /// + /// This listener has no URI prefix on which listens. + /// + /// + /// + /// This listener has been closed. + /// + public IAsyncResult BeginGetContext (AsyncCallback callback, object state) + { + if (_disposed) + throw new ObjectDisposedException (ObjectName); + + if (!_isListening) { + var msg = "The listener has not been started."; + + throw new InvalidOperationException (msg); + } + + if (_prefixes.Count == 0) { + var msg = "The listener has no URI prefix on which listens."; + + throw new InvalidOperationException (msg); + } + + return beginGetContext (callback, state); + } + + /// + /// Shuts down the listener. + /// + public void Close () + { + if (_disposed) + return; + + close (false); + } + + /// + /// Ends an asynchronous operation to get an incoming request. + /// + /// + /// This method ends an asynchronous operation started by calling + /// the method. + /// + /// + /// A that represents a request. + /// + /// + /// An instance obtained by calling + /// the method. + /// + /// + /// was not obtained by calling + /// the method. + /// + /// + /// is . + /// + /// + /// This method is canceled. + /// + /// + /// + /// This listener has not been started or is currently stopped. + /// + /// + /// -or- + /// + /// + /// This method was already called for . + /// + /// + /// + /// This listener has been closed. + /// + public HttpListenerContext EndGetContext (IAsyncResult asyncResult) + { + if (_disposed) + throw new ObjectDisposedException (ObjectName); + + if (!_isListening) { + var msg = "The listener has not been started."; + + throw new InvalidOperationException (msg); + } + + if (asyncResult == null) + throw new ArgumentNullException ("asyncResult"); + + var ares = asyncResult as HttpListenerAsyncResult; + + if (ares == null) { + var msg = "A wrong IAsyncResult instance."; + + throw new ArgumentException (msg, "asyncResult"); + } + + lock (ares.SyncRoot) { + if (ares.EndCalled) { + var msg = "This IAsyncResult instance cannot be reused."; + + throw new InvalidOperationException (msg); + } + + ares.EndCalled = true; + } + + if (!ares.IsCompleted) + ares.AsyncWaitHandle.WaitOne (); + + return ares.Context; + } + + /// + /// Gets an incoming request. + /// + /// + /// This method waits for an incoming request and returns when + /// a request is received. + /// + /// + /// A that represents a request. + /// + /// + /// This method is canceled. + /// + /// + /// + /// This listener has not been started or is currently stopped. + /// + /// + /// -or- + /// + /// + /// This listener has no URI prefix on which listens. + /// + /// + /// + /// This listener has been closed. + /// + public HttpListenerContext GetContext () + { + if (_disposed) + throw new ObjectDisposedException (ObjectName); + + if (!_isListening) { + var msg = "The listener has not been started."; + + throw new InvalidOperationException (msg); + } + + if (_prefixes.Count == 0) { + var msg = "The listener has no URI prefix on which listens."; + + throw new InvalidOperationException (msg); + } + + var ares = beginGetContext (null, null); + + ares.EndCalled = true; + + if (!ares.IsCompleted) + ares.AsyncWaitHandle.WaitOne (); + + return ares.Context; + } + + /// + /// Starts receiving incoming requests. + /// + /// + /// This listener has been closed. + /// + public void Start () + { + if (_disposed) + throw new ObjectDisposedException (ObjectName); + + lock (_sync) { + if (_disposed) + throw new ObjectDisposedException (ObjectName); + + lock (_contextRegistrySync) { + if (_isListening) + return; + + EndPointManager.AddListener (this); + + _isListening = true; + } + } + } + + /// + /// Stops receiving incoming requests. + /// + /// + /// This listener has been closed. + /// + public void Stop () + { + if (_disposed) + throw new ObjectDisposedException (ObjectName); + + lock (_sync) { + if (_disposed) + throw new ObjectDisposedException (ObjectName); + + lock (_contextRegistrySync) { + if (!_isListening) + return; + + _isListening = false; + } + + cleanupContextQueue (false); + cleanupContextRegistry (); + + var msg = "The listener is stopped."; + + cleanupWaitQueue (msg); + + EndPointManager.RemoveListener (this); + } + } + + #endregion + + #region Explicit Interface Implementations + + /// + /// Releases all resources used by the listener. + /// + void IDisposable.Dispose () + { + if (_disposed) + return; + + close (true); + } + + #endregion + } +} diff --git a/Assets/External/websocket-sharp/Net/HttpListener.cs.meta b/Assets/External/websocket-sharp/Net/HttpListener.cs.meta new file mode 100644 index 00000000..48efbd0d --- /dev/null +++ b/Assets/External/websocket-sharp/Net/HttpListener.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: bbec4555545c5a740b476dbc0ef4f52a \ No newline at end of file diff --git a/Assets/External/websocket-sharp/Net/HttpListenerAsyncResult.cs b/Assets/External/websocket-sharp/Net/HttpListenerAsyncResult.cs new file mode 100644 index 00000000..e4742d77 --- /dev/null +++ b/Assets/External/websocket-sharp/Net/HttpListenerAsyncResult.cs @@ -0,0 +1,202 @@ +#region License +/* + * HttpListenerAsyncResult.cs + * + * This code is derived from ListenerAsyncResult.cs (System.Net) of Mono + * (http://www.mono-project.com). + * + * The MIT License + * + * Copyright (c) 2005 Ximian, Inc. (http://www.ximian.com) + * Copyright (c) 2012-2024 sta.blockhead + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#endregion + +#region Authors +/* + * Authors: + * - Gonzalo Paniagua Javier + */ +#endregion + +#region Contributors +/* + * Contributors: + * - Nicholas Devenish + */ +#endregion + +using System; +using System.Threading; + +namespace WebSocketSharp.Net +{ + internal class HttpListenerAsyncResult : IAsyncResult + { + #region Private Fields + + private AsyncCallback _callback; + private bool _completed; + private bool _completedSynchronously; + private HttpListenerContext _context; + private bool _endCalled; + private Exception _exception; + private Logger _log; + private object _state; + private object _sync; + private ManualResetEvent _waitHandle; + + #endregion + + #region Internal Constructors + + internal HttpListenerAsyncResult ( + AsyncCallback callback, + object state, + Logger log + ) + { + _callback = callback; + _state = state; + _log = log; + + _sync = new object (); + } + + #endregion + + #region Internal Properties + + internal HttpListenerContext Context + { + get { + if (_exception != null) + throw _exception; + + return _context; + } + } + + internal bool EndCalled { + get { + return _endCalled; + } + + set { + _endCalled = value; + } + } + + internal object SyncRoot { + get { + return _sync; + } + } + + #endregion + + #region Public Properties + + public object AsyncState { + get { + return _state; + } + } + + public WaitHandle AsyncWaitHandle { + get { + lock (_sync) { + if (_waitHandle == null) + _waitHandle = new ManualResetEvent (_completed); + + return _waitHandle; + } + } + } + + public bool CompletedSynchronously { + get { + return _completedSynchronously; + } + } + + public bool IsCompleted { + get { + lock (_sync) + return _completed; + } + } + + #endregion + + #region Private Methods + + private void complete () + { + lock (_sync) { + _completed = true; + + if (_waitHandle != null) + _waitHandle.Set (); + } + + if (_callback == null) + return; + + ThreadPool.QueueUserWorkItem ( + state => { + try { + _callback (this); + } + catch (Exception ex) { + _log.Error (ex.Message); + _log.Debug (ex.ToString ()); + } + }, + null + ); + } + + #endregion + + #region Internal Methods + + internal void Complete (Exception exception) + { + _exception = exception; + + complete (); + } + + internal void Complete ( + HttpListenerContext context, + bool completedSynchronously + ) + { + _context = context; + _completedSynchronously = completedSynchronously; + + complete (); + } + + #endregion + } +} diff --git a/Assets/External/websocket-sharp/Net/HttpListenerAsyncResult.cs.meta b/Assets/External/websocket-sharp/Net/HttpListenerAsyncResult.cs.meta new file mode 100644 index 00000000..9d71db8a --- /dev/null +++ b/Assets/External/websocket-sharp/Net/HttpListenerAsyncResult.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: d785cd8b82ce78b4fab65e7330d18fe3 \ No newline at end of file diff --git a/Assets/External/websocket-sharp/Net/HttpListenerContext.cs b/Assets/External/websocket-sharp/Net/HttpListenerContext.cs new file mode 100644 index 00000000..82ea0176 --- /dev/null +++ b/Assets/External/websocket-sharp/Net/HttpListenerContext.cs @@ -0,0 +1,452 @@ +#region License +/* + * HttpListenerContext.cs + * + * This code is derived from HttpListenerContext.cs (System.Net) of Mono + * (http://www.mono-project.com). + * + * The MIT License + * + * Copyright (c) 2005 Novell, Inc. (http://www.novell.com) + * Copyright (c) 2012-2024 sta.blockhead + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#endregion + +#region Authors +/* + * Authors: + * - Gonzalo Paniagua Javier + */ +#endregion + +using System; +using System.Security.Principal; +using System.Text; +using WebSocketSharp.Net.WebSockets; + +namespace WebSocketSharp.Net +{ + /// + /// Provides the access to the HTTP request and response objects used by + /// the class. + /// + /// + /// This class cannot be inherited. + /// + public sealed class HttpListenerContext + { + #region Private Fields + + private HttpConnection _connection; + private string _errorMessage; + private int _errorStatusCode; + private HttpListener _listener; + private HttpListenerRequest _request; + private HttpListenerResponse _response; + private IPrincipal _user; + private HttpListenerWebSocketContext _websocketContext; + + #endregion + + #region Internal Constructors + + internal HttpListenerContext (HttpConnection connection) + { + _connection = connection; + + _errorStatusCode = 400; + _request = new HttpListenerRequest (this); + _response = new HttpListenerResponse (this); + } + + #endregion + + #region Internal Properties + + internal HttpConnection Connection { + get { + return _connection; + } + } + + internal string ErrorMessage { + get { + return _errorMessage; + } + + set { + _errorMessage = value; + } + } + + internal int ErrorStatusCode { + get { + return _errorStatusCode; + } + + set { + _errorStatusCode = value; + } + } + + internal bool HasErrorMessage { + get { + return _errorMessage != null; + } + } + + internal HttpListener Listener { + get { + return _listener; + } + + set { + _listener = value; + } + } + + #endregion + + #region Public Properties + + /// + /// Gets the HTTP request object that represents a client request. + /// + /// + /// A that represents the client request. + /// + public HttpListenerRequest Request { + get { + return _request; + } + } + + /// + /// Gets the HTTP response object used to send a response to the client. + /// + /// + /// A that represents a response to + /// the client request. + /// + public HttpListenerResponse Response { + get { + return _response; + } + } + + /// + /// Gets the client information. + /// + /// + /// + /// A instance that represents identity, + /// authentication, and security roles for the client. + /// + /// + /// if the client is not authenticated. + /// + /// + public IPrincipal User { + get { + return _user; + } + } + + #endregion + + #region Private Methods + + private static string createErrorContent ( + int statusCode, + string statusDescription, + string message + ) + { + return message != null && message.Length > 0 + ? String.Format ( + "

{0} {1} ({2})

", + statusCode, + statusDescription, + message + ) + : String.Format ( + "

{0} {1}

", + statusCode, + statusDescription + ); + } + + #endregion + + #region Internal Methods + + internal HttpListenerWebSocketContext GetWebSocketContext (string protocol) + { + _websocketContext = new HttpListenerWebSocketContext (this, protocol); + + return _websocketContext; + } + + internal void SendAuthenticationChallenge ( + AuthenticationSchemes scheme, + string realm + ) + { + _response.StatusCode = 401; + + var val = new AuthenticationChallenge (scheme, realm).ToString (); + + _response.Headers.InternalSet ("WWW-Authenticate", val, true); + + _response.Close (); + } + + internal void SendError () + { + try { + _response.StatusCode = _errorStatusCode; + _response.ContentType = "text/html"; + + var content = createErrorContent ( + _errorStatusCode, + _response.StatusDescription, + _errorMessage + ); + + var enc = Encoding.UTF8; + var entity = enc.GetBytes (content); + + _response.ContentEncoding = enc; + _response.ContentLength64 = entity.LongLength; + + _response.Close (entity, true); + } + catch { + _connection.Close (true); + } + } + + internal void SendError (int statusCode) + { + _errorStatusCode = statusCode; + + SendError (); + } + + internal void SendError (int statusCode, string message) + { + _errorStatusCode = statusCode; + _errorMessage = message; + + SendError (); + } + + internal bool SetUser ( + AuthenticationSchemes scheme, + string realm, + Func credentialsFinder + ) + { + var user = HttpUtility.CreateUser ( + _request.Headers["Authorization"], + scheme, + realm, + _request.HttpMethod, + credentialsFinder + ); + + if (user == null) + return false; + + if (!user.Identity.IsAuthenticated) + return false; + + _user = user; + + return true; + } + + internal void Unregister () + { + if (_listener == null) + return; + + _listener.UnregisterContext (this); + } + + #endregion + + #region Public Methods + + /// + /// Accepts a WebSocket connection. + /// + /// + /// A that represents + /// the WebSocket handshake request. + /// + /// + /// + /// A that specifies the name of the subprotocol + /// supported on the WebSocket connection. + /// + /// + /// if not necessary. + /// + /// + /// + /// + /// is an empty string. + /// + /// + /// -or- + /// + /// + /// contains an invalid character. + /// + /// + /// + /// + /// This method has already been done. + /// + /// + /// -or- + /// + /// + /// The client request is not a WebSocket handshake request. + /// + /// + public HttpListenerWebSocketContext AcceptWebSocket (string protocol) + { + return AcceptWebSocket (protocol, null); + } + + /// + /// Accepts a WebSocket connection with initializing the WebSocket + /// interface. + /// + /// + /// A that represents + /// the WebSocket handshake request. + /// + /// + /// + /// A that specifies the name of the subprotocol + /// supported on the WebSocket connection. + /// + /// + /// if not necessary. + /// + /// + /// + /// + /// An delegate. + /// + /// + /// It specifies the delegate called when a new WebSocket instance is + /// initialized. + /// + /// + /// + /// + /// is an empty string. + /// + /// + /// -or- + /// + /// + /// contains an invalid character. + /// + /// + /// -or- + /// + /// + /// caused an exception. + /// + /// + /// + /// + /// This method has already been done. + /// + /// + /// -or- + /// + /// + /// The client request is not a WebSocket handshake request. + /// + /// + public HttpListenerWebSocketContext AcceptWebSocket ( + string protocol, + Action initializer + ) + { + if (_websocketContext != null) { + var msg = "The method has already been done."; + + throw new InvalidOperationException (msg); + } + + if (!_request.IsWebSocketRequest) { + var msg = "The request is not a WebSocket handshake request."; + + throw new InvalidOperationException (msg); + } + + if (protocol != null) { + if (protocol.Length == 0) { + var msg = "An empty string."; + + throw new ArgumentException (msg, "protocol"); + } + + if (!protocol.IsToken ()) { + var msg = "It contains an invalid character."; + + throw new ArgumentException (msg, "protocol"); + } + } + + var ret = GetWebSocketContext (protocol); + + var ws = ret.WebSocket; + + if (initializer != null) { + try { + initializer (ws); + } + catch (Exception ex) { + if (ws.ReadyState == WebSocketState.New) + _websocketContext = null; + + var msg = "It caused an exception."; + + throw new ArgumentException (msg, "initializer", ex); + } + } + + ws.Accept (); + + return ret; + } + + #endregion + } +} diff --git a/Assets/External/websocket-sharp/Net/HttpListenerContext.cs.meta b/Assets/External/websocket-sharp/Net/HttpListenerContext.cs.meta new file mode 100644 index 00000000..8451eee8 --- /dev/null +++ b/Assets/External/websocket-sharp/Net/HttpListenerContext.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: a273b442ff77f394d94e4c5c941c48d7 \ No newline at end of file diff --git a/Assets/External/websocket-sharp/Net/HttpListenerException.cs b/Assets/External/websocket-sharp/Net/HttpListenerException.cs new file mode 100644 index 00000000..dec858d5 --- /dev/null +++ b/Assets/External/websocket-sharp/Net/HttpListenerException.cs @@ -0,0 +1,140 @@ +#region License +/* + * HttpListenerException.cs + * + * This code is derived from HttpListenerException.cs (System.Net) of Mono + * (http://www.mono-project.com). + * + * The MIT License + * + * Copyright (c) 2005 Novell, Inc. (http://www.novell.com) + * Copyright (c) 2012-2024 sta.blockhead + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#endregion + +#region Authors +/* + * Authors: + * - Gonzalo Paniagua Javier + */ +#endregion + +using System; +using System.ComponentModel; +using System.Runtime.Serialization; + +namespace WebSocketSharp.Net +{ + /// + /// The exception that is thrown when an error occurs processing + /// an HTTP request. + /// + [Serializable] + public class HttpListenerException : Win32Exception + { + #region Protected Constructors + + /// + /// Initializes a new instance of the + /// class with the specified serialized data. + /// + /// + /// A that contains the serialized + /// object data. + /// + /// + /// A that specifies the source for + /// the deserialization. + /// + /// + /// is . + /// + protected HttpListenerException ( + SerializationInfo serializationInfo, + StreamingContext streamingContext + ) + : base (serializationInfo, streamingContext) + { + } + + #endregion + + #region Public Constructors + + /// + /// Initializes a new instance of the + /// class. + /// + public HttpListenerException () + { + } + + /// + /// Initializes a new instance of the + /// class with the specified error code. + /// + /// + /// An that specifies the error code. + /// + public HttpListenerException (int errorCode) + : base (errorCode) + { + } + + /// + /// Initializes a new instance of the + /// class with the specified error code and message. + /// + /// + /// An that specifies the error code. + /// + /// + /// A that specifies the message. + /// + public HttpListenerException (int errorCode, string message) + : base (errorCode, message) + { + } + + #endregion + + #region Public Properties + + /// + /// Gets the error code that identifies the error that occurred. + /// + /// + /// + /// An that represents the error code. + /// + /// + /// It is any of the Win32 error codes. + /// + /// + public override int ErrorCode { + get { + return NativeErrorCode; + } + } + + #endregion + } +} diff --git a/Assets/External/websocket-sharp/Net/HttpListenerException.cs.meta b/Assets/External/websocket-sharp/Net/HttpListenerException.cs.meta new file mode 100644 index 00000000..346f3aa5 --- /dev/null +++ b/Assets/External/websocket-sharp/Net/HttpListenerException.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 07dd63c18a8033d40a377a29d705ff58 \ No newline at end of file diff --git a/Assets/External/websocket-sharp/Net/HttpListenerPrefix.cs b/Assets/External/websocket-sharp/Net/HttpListenerPrefix.cs new file mode 100644 index 00000000..aba9d1bf --- /dev/null +++ b/Assets/External/websocket-sharp/Net/HttpListenerPrefix.cs @@ -0,0 +1,243 @@ +#region License +/* + * HttpListenerPrefix.cs + * + * This code is derived from ListenerPrefix.cs (System.Net) of Mono + * (http://www.mono-project.com). + * + * The MIT License + * + * Copyright (c) 2005 Novell, Inc. (http://www.novell.com) + * Copyright (c) 2012-2024 sta.blockhead + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#endregion + +#region Authors +/* + * Authors: + * - Gonzalo Paniagua Javier + * - Oleg Mihailik + */ +#endregion + +using System; + +namespace WebSocketSharp.Net +{ + internal sealed class HttpListenerPrefix + { + #region Private Fields + + private string _host; + private bool _isSecure; + private HttpListener _listener; + private string _original; + private string _path; + private string _port; + private string _prefix; + private string _scheme; + + #endregion + + #region Internal Constructors + + internal HttpListenerPrefix (string uriPrefix, HttpListener listener) + { + _original = uriPrefix; + _listener = listener; + + parse (uriPrefix); + } + + #endregion + + #region Public Properties + + public string Host { + get { + return _host; + } + } + + public bool IsSecure { + get { + return _isSecure; + } + } + + public HttpListener Listener { + get { + return _listener; + } + } + + public string Original { + get { + return _original; + } + } + + public string Path { + get { + return _path; + } + } + + public string Port { + get { + return _port; + } + } + + public string Scheme { + get { + return _scheme; + } + } + + #endregion + + #region Private Methods + + private void parse (string uriPrefix) + { + var compType = StringComparison.Ordinal; + + _isSecure = uriPrefix.StartsWith ("https", compType); + _scheme = _isSecure ? "https" : "http"; + + var hostStartIdx = uriPrefix.IndexOf (':') + 3; + + var len = uriPrefix.Length; + var rootIdx = uriPrefix + .IndexOf ('/', hostStartIdx + 1, len - hostStartIdx - 1); + + var colonIdx = uriPrefix + .LastIndexOf (':', rootIdx - 1, rootIdx - hostStartIdx - 1); + + var hasPort = uriPrefix[rootIdx - 1] != ']' && colonIdx > hostStartIdx; + + if (hasPort) { + _host = uriPrefix.Substring (hostStartIdx, colonIdx - hostStartIdx); + _port = uriPrefix.Substring (colonIdx + 1, rootIdx - colonIdx - 1); + } + else { + _host = uriPrefix.Substring (hostStartIdx, rootIdx - hostStartIdx); + _port = _isSecure ? "443" : "80"; + } + + _path = uriPrefix.Substring (rootIdx); + + var fmt = "{0}://{1}:{2}{3}"; + + _prefix = String.Format (fmt, _scheme, _host, _port, _path); + } + + #endregion + + #region Public Methods + + public static void CheckPrefix (string uriPrefix) + { + if (uriPrefix == null) + throw new ArgumentNullException ("uriPrefix"); + + var len = uriPrefix.Length; + + if (len == 0) { + var msg = "An empty string."; + + throw new ArgumentException (msg, "uriPrefix"); + } + + var compType = StringComparison.Ordinal; + var isHttpSchm = uriPrefix.StartsWith ("http://", compType) + || uriPrefix.StartsWith ("https://", compType); + + if (!isHttpSchm) { + var msg = "The scheme is not http or https."; + + throw new ArgumentException (msg, "uriPrefix"); + } + + var endIdx = len - 1; + + if (uriPrefix[endIdx] != '/') { + var msg = "It ends without a forward slash."; + + throw new ArgumentException (msg, "uriPrefix"); + } + + var hostStartIdx = uriPrefix.IndexOf (':') + 3; + + if (hostStartIdx >= endIdx) { + var msg = "No host is specified."; + + throw new ArgumentException (msg, "uriPrefix"); + } + + if (uriPrefix[hostStartIdx] == ':') { + var msg = "No host is specified."; + + throw new ArgumentException (msg, "uriPrefix"); + } + + var rootIdx = uriPrefix.IndexOf ('/', hostStartIdx, len - hostStartIdx); + + if (rootIdx == hostStartIdx) { + var msg = "No host is specified."; + + throw new ArgumentException (msg, "uriPrefix"); + } + + if (uriPrefix[rootIdx - 1] == ':') { + var msg = "No port is specified."; + + throw new ArgumentException (msg, "uriPrefix"); + } + + if (rootIdx == endIdx - 1) { + var msg = "No path is specified."; + + throw new ArgumentException (msg, "uriPrefix"); + } + } + + public override bool Equals (object obj) + { + var pref = obj as HttpListenerPrefix; + + return pref != null && _prefix.Equals (pref._prefix); + } + + public override int GetHashCode () + { + return _prefix.GetHashCode (); + } + + public override string ToString () + { + return _prefix; + } + + #endregion + } +} diff --git a/Assets/External/websocket-sharp/Net/HttpListenerPrefix.cs.meta b/Assets/External/websocket-sharp/Net/HttpListenerPrefix.cs.meta new file mode 100644 index 00000000..95df4db5 --- /dev/null +++ b/Assets/External/websocket-sharp/Net/HttpListenerPrefix.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 9da8d497d19f7f745826de72945261d6 \ No newline at end of file diff --git a/Assets/External/websocket-sharp/Net/HttpListenerPrefixCollection.cs b/Assets/External/websocket-sharp/Net/HttpListenerPrefixCollection.cs new file mode 100644 index 00000000..3f5c44a2 --- /dev/null +++ b/Assets/External/websocket-sharp/Net/HttpListenerPrefixCollection.cs @@ -0,0 +1,295 @@ +#region License +/* + * HttpListenerPrefixCollection.cs + * + * This code is derived from HttpListenerPrefixCollection.cs (System.Net) of Mono + * (http://www.mono-project.com). + * + * The MIT License + * + * Copyright (c) 2005 Novell, Inc. (http://www.novell.com) + * Copyright (c) 2012-2024 sta.blockhead + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#endregion + +#region Authors +/* + * Authors: + * - Gonzalo Paniagua Javier + */ +#endregion + +using System; +using System.Collections; +using System.Collections.Generic; + +namespace WebSocketSharp.Net +{ + /// + /// Provides a collection used to store the URI prefixes for a instance of + /// the class. + /// + /// + /// The instance responds to the request which + /// has a requested URI that the prefixes most closely match. + /// + public class HttpListenerPrefixCollection : ICollection + { + #region Private Fields + + private HttpListener _listener; + private List _prefixes; + + #endregion + + #region Internal Constructors + + internal HttpListenerPrefixCollection (HttpListener listener) + { + _listener = listener; + + _prefixes = new List (); + } + + #endregion + + #region Public Properties + + /// + /// Gets the number of prefixes in the collection. + /// + /// + /// An that represents the number of prefixes. + /// + public int Count { + get { + return _prefixes.Count; + } + } + + /// + /// Gets a value indicating whether the access to the collection is + /// read-only. + /// + /// + /// Always returns false. + /// + public bool IsReadOnly { + get { + return false; + } + } + + /// + /// Gets a value indicating whether the access to the collection is + /// synchronized. + /// + /// + /// Always returns false. + /// + public bool IsSynchronized { + get { + return false; + } + } + + #endregion + + #region Public Methods + + /// + /// Adds the specified URI prefix to the collection. + /// + /// + /// + /// A that specifies the URI prefix to add. + /// + /// + /// It must be a well-formed URI prefix with http or https scheme, + /// and must end with a forward slash (/). + /// + /// + /// + /// is invalid. + /// + /// + /// is . + /// + /// + /// The instance associated with this + /// collection is closed. + /// + public void Add (string uriPrefix) + { + _listener.CheckDisposed (); + + HttpListenerPrefix.CheckPrefix (uriPrefix); + + if (_prefixes.Contains (uriPrefix)) + return; + + if (_listener.IsListening) + EndPointManager.AddPrefix (uriPrefix, _listener); + + _prefixes.Add (uriPrefix); + } + + /// + /// Removes all URI prefixes from the collection. + /// + /// + /// The instance associated with this + /// collection is closed. + /// + public void Clear () + { + _listener.CheckDisposed (); + + if (_listener.IsListening) + EndPointManager.RemoveListener (_listener); + + _prefixes.Clear (); + } + + /// + /// Returns a value indicating whether the collection contains the + /// specified URI prefix. + /// + /// + /// true if the collection contains the URI prefix; otherwise, + /// false. + /// + /// + /// A that specifies the URI prefix to test. + /// + /// + /// is . + /// + /// + /// The instance associated with this + /// collection is closed. + /// + public bool Contains (string uriPrefix) + { + _listener.CheckDisposed (); + + if (uriPrefix == null) + throw new ArgumentNullException ("uriPrefix"); + + return _prefixes.Contains (uriPrefix); + } + + /// + /// Copies the contents of the collection to the specified array of string. + /// + /// + /// An array of that specifies the destination of + /// the URI prefix strings copied from the collection. + /// + /// + /// An that specifies the zero-based index in + /// the array at which copying begins. + /// + /// + /// The space from to the end of + /// is not enough to copy to. + /// + /// + /// is . + /// + /// + /// is less than zero. + /// + /// + /// The instance associated with this + /// collection is closed. + /// + public void CopyTo (string[] array, int offset) + { + _listener.CheckDisposed (); + + _prefixes.CopyTo (array, offset); + } + + /// + /// Gets the enumerator that iterates through the collection. + /// + /// + /// An + /// instance that can be used to iterate through the collection. + /// + public IEnumerator GetEnumerator () + { + return _prefixes.GetEnumerator (); + } + + /// + /// Removes the specified URI prefix from the collection. + /// + /// + /// true if the URI prefix is successfully removed; otherwise, + /// false. + /// + /// + /// A that specifies the URI prefix to remove. + /// + /// + /// is . + /// + /// + /// The instance associated with this + /// collection is closed. + /// + public bool Remove (string uriPrefix) + { + _listener.CheckDisposed (); + + if (uriPrefix == null) + throw new ArgumentNullException ("uriPrefix"); + + if (!_prefixes.Contains (uriPrefix)) + return false; + + if (_listener.IsListening) + EndPointManager.RemovePrefix (uriPrefix, _listener); + + return _prefixes.Remove (uriPrefix); + } + + #endregion + + #region Explicit Interface Implementations + + /// + /// Gets the enumerator that iterates through the collection. + /// + /// + /// An instance that can be used to iterate + /// through the collection. + /// + IEnumerator IEnumerable.GetEnumerator () + { + return _prefixes.GetEnumerator (); + } + + #endregion + } +} diff --git a/Assets/External/websocket-sharp/Net/HttpListenerPrefixCollection.cs.meta b/Assets/External/websocket-sharp/Net/HttpListenerPrefixCollection.cs.meta new file mode 100644 index 00000000..f0a9ba80 --- /dev/null +++ b/Assets/External/websocket-sharp/Net/HttpListenerPrefixCollection.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 9940f054c5d5b5240a1491e9c6c82159 \ No newline at end of file diff --git a/Assets/External/websocket-sharp/Net/HttpListenerRequest.cs b/Assets/External/websocket-sharp/Net/HttpListenerRequest.cs new file mode 100644 index 00000000..9c7b1a16 --- /dev/null +++ b/Assets/External/websocket-sharp/Net/HttpListenerRequest.cs @@ -0,0 +1,943 @@ +#region License +/* + * HttpListenerRequest.cs + * + * This code is derived from HttpListenerRequest.cs (System.Net) of Mono + * (http://www.mono-project.com). + * + * The MIT License + * + * Copyright (c) 2005 Novell, Inc. (http://www.novell.com) + * Copyright (c) 2012-2024 sta.blockhead + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#endregion + +#region Authors +/* + * Authors: + * - Gonzalo Paniagua Javier + */ +#endregion + +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Globalization; +using System.IO; +using System.Security.Cryptography.X509Certificates; +using System.Text; + +namespace WebSocketSharp.Net +{ + /// + /// Represents an incoming HTTP request to a + /// instance. + /// + /// + /// This class cannot be inherited. + /// + public sealed class HttpListenerRequest + { + #region Private Fields + + private static readonly byte[] _100continue; + private string[] _acceptTypes; + private bool _chunked; + private HttpConnection _connection; + private Encoding _contentEncoding; + private long _contentLength; + private HttpListenerContext _context; + private CookieCollection _cookies; + private static readonly Encoding _defaultEncoding; + private WebHeaderCollection _headers; + private string _httpMethod; + private Stream _inputStream; + private Version _protocolVersion; + private NameValueCollection _queryString; + private string _rawUrl; + private Guid _requestTraceIdentifier; + private Uri _url; + private Uri _urlReferrer; + private bool _urlSet; + private string _userHostName; + private string[] _userLanguages; + + #endregion + + #region Static Constructor + + static HttpListenerRequest () + { + _100continue = Encoding.ASCII.GetBytes ("HTTP/1.1 100 Continue\r\n\r\n"); + _defaultEncoding = Encoding.UTF8; + } + + #endregion + + #region Internal Constructors + + internal HttpListenerRequest (HttpListenerContext context) + { + _context = context; + + _connection = context.Connection; + _contentLength = -1; + _headers = new WebHeaderCollection (); + _requestTraceIdentifier = Guid.NewGuid (); + } + + #endregion + + #region Public Properties + + /// + /// Gets the media types that are acceptable for the client. + /// + /// + /// + /// An array of that contains the names of + /// the media types specified in the value of the Accept header. + /// + /// + /// if the header is not present. + /// + /// + public string[] AcceptTypes { + get { + var val = _headers["Accept"]; + + if (val == null) + return null; + + if (_acceptTypes == null) { + _acceptTypes = val + .SplitHeaderValue (',') + .TrimEach () + .ToList () + .ToArray (); + } + + return _acceptTypes; + } + } + + /// + /// Gets an error code that identifies a problem with the certificate + /// provided by the client. + /// + /// + /// An that represents an error code. + /// + /// + /// This property is not supported. + /// + public int ClientCertificateError { + get { + throw new NotSupportedException (); + } + } + + /// + /// Gets the encoding for the entity body data included in the request. + /// + /// + /// + /// A converted from the charset value of the + /// Content-Type header. + /// + /// + /// if the charset value is not available. + /// + /// + public Encoding ContentEncoding { + get { + if (_contentEncoding == null) + _contentEncoding = getContentEncoding (); + + return _contentEncoding; + } + } + + /// + /// Gets the length in bytes of the entity body data included in the + /// request. + /// + /// + /// + /// A converted from the value of the Content-Length + /// header. + /// + /// + /// -1 if the header is not present. + /// + /// + public long ContentLength64 { + get { + return _contentLength; + } + } + + /// + /// Gets the media type of the entity body data included in the request. + /// + /// + /// + /// A that represents the value of the Content-Type + /// header. + /// + /// + /// if the header is not present. + /// + /// + public string ContentType { + get { + return _headers["Content-Type"]; + } + } + + /// + /// Gets the HTTP cookies included in the request. + /// + /// + /// + /// A that contains the cookies. + /// + /// + /// An empty collection if not included. + /// + /// + public CookieCollection Cookies { + get { + if (_cookies == null) + _cookies = _headers.GetCookies (false); + + return _cookies; + } + } + + /// + /// Gets a value indicating whether the request has the entity body data. + /// + /// + /// true if the request has the entity body data; otherwise, + /// false. + /// + public bool HasEntityBody { + get { + return _contentLength > 0 || _chunked; + } + } + + /// + /// Gets the HTTP headers included in the request. + /// + /// + /// A that contains the headers. + /// + public NameValueCollection Headers { + get { + return _headers; + } + } + + /// + /// Gets the HTTP method specified by the client. + /// + /// + /// A that represents the HTTP method specified in + /// the request line. + /// + public string HttpMethod { + get { + return _httpMethod; + } + } + + /// + /// Gets a stream that contains the entity body data included in + /// the request. + /// + /// + /// + /// A that contains the entity body data. + /// + /// + /// if the entity body data is not available. + /// + /// + public Stream InputStream { + get { + if (_inputStream == null) { + _inputStream = _contentLength > 0 || _chunked + ? _connection + .GetRequestStream (_contentLength, _chunked) + : Stream.Null; + } + + return _inputStream; + } + } + + /// + /// Gets a value indicating whether the client is authenticated. + /// + /// + /// true if the client is authenticated; otherwise, false. + /// + public bool IsAuthenticated { + get { + return _context.User != null; + } + } + + /// + /// Gets a value indicating whether the request is sent from the + /// local computer. + /// + /// + /// true if the request is sent from the same computer as + /// the server; otherwise, false. + /// + public bool IsLocal { + get { + return _connection.IsLocal; + } + } + + /// + /// Gets a value indicating whether a secure connection is used to send + /// the request. + /// + /// + /// true if the connection is secure; otherwise, false. + /// + public bool IsSecureConnection { + get { + return _connection.IsSecure; + } + } + + /// + /// Gets a value indicating whether the request is a WebSocket handshake + /// request. + /// + /// + /// true if the request is a WebSocket handshake request; otherwise, + /// false. + /// + public bool IsWebSocketRequest { + get { + return _httpMethod == "GET" && _headers.Upgrades ("websocket"); + } + } + + /// + /// Gets a value indicating whether a persistent connection is requested. + /// + /// + /// true if the request specifies that the connection is kept open; + /// otherwise, false. + /// + public bool KeepAlive { + get { + return _headers.KeepsAlive (_protocolVersion); + } + } + + /// + /// Gets the endpoint to which the request is sent. + /// + /// + /// A that represents the server + /// IP address and port number. + /// + public System.Net.IPEndPoint LocalEndPoint { + get { + return _connection.LocalEndPoint; + } + } + + /// + /// Gets the HTTP version specified by the client. + /// + /// + /// A that represents the HTTP version specified in + /// the request line. + /// + public Version ProtocolVersion { + get { + return _protocolVersion; + } + } + + /// + /// Gets the query string included in the request. + /// + /// + /// + /// A that contains the query + /// parameters. + /// + /// + /// Each query parameter is decoded in UTF-8. + /// + /// + /// An empty collection if not included. + /// + /// + public NameValueCollection QueryString { + get { + if (_queryString == null) { + var url = Url; + var query = url != null ? url.Query : null; + + _queryString = QueryStringCollection.Parse (query, _defaultEncoding); + } + + return _queryString; + } + } + + /// + /// Gets the raw URL specified by the client. + /// + /// + /// A that represents the request target specified in + /// the request line. + /// + public string RawUrl { + get { + return _rawUrl; + } + } + + /// + /// Gets the endpoint from which the request is sent. + /// + /// + /// A that represents the client + /// IP address and port number. + /// + public System.Net.IPEndPoint RemoteEndPoint { + get { + return _connection.RemoteEndPoint; + } + } + + /// + /// Gets the trace identifier of the request. + /// + /// + /// A that represents the trace identifier. + /// + public Guid RequestTraceIdentifier { + get { + return _requestTraceIdentifier; + } + } + + /// + /// Gets the URL requested by the client. + /// + /// + /// + /// A that represents the URL parsed from the request. + /// + /// + /// if the URL cannot be parsed. + /// + /// + public Uri Url { + get { + if (!_urlSet) { + _url = HttpUtility + .CreateRequestUrl ( + _rawUrl, + _userHostName, + IsWebSocketRequest, + IsSecureConnection + ); + + _urlSet = true; + } + + return _url; + } + } + + /// + /// Gets the URI of the resource from which the requested URL was obtained. + /// + /// + /// + /// A that represents the value of the Referer header. + /// + /// + /// if the header value is not available. + /// + /// + public Uri UrlReferrer { + get { + var val = _headers["Referer"]; + + if (val == null) + return null; + + if (_urlReferrer == null) + _urlReferrer = val.ToUri (); + + return _urlReferrer; + } + } + + /// + /// Gets the user agent from which the request is originated. + /// + /// + /// + /// A that represents the value of the User-Agent + /// header. + /// + /// + /// if the header is not present. + /// + /// + public string UserAgent { + get { + return _headers["User-Agent"]; + } + } + + /// + /// Gets the IP address and port number to which the request is sent. + /// + /// + /// A that represents the server IP address and + /// port number. + /// + public string UserHostAddress { + get { + return _connection.LocalEndPoint.ToString (); + } + } + + /// + /// Gets the server host name requested by the client. + /// + /// + /// + /// A that represents the value of the Host header. + /// + /// + /// It includes the port number if provided. + /// + /// + public string UserHostName { + get { + return _userHostName; + } + } + + /// + /// Gets the natural languages that are acceptable for the client. + /// + /// + /// + /// An array of that contains the names of the + /// natural languages specified in the value of the Accept-Language + /// header. + /// + /// + /// if the header is not present. + /// + /// + public string[] UserLanguages { + get { + var val = _headers["Accept-Language"]; + + if (val == null) + return null; + + if (_userLanguages == null) + _userLanguages = val.Split (',').TrimEach ().ToList ().ToArray (); + + return _userLanguages; + } + } + + #endregion + + #region Private Methods + + private Encoding getContentEncoding () + { + var val = _headers["Content-Type"]; + + if (val == null) + return _defaultEncoding; + + Encoding ret; + + return HttpUtility.TryGetEncoding (val, out ret) + ? ret + : _defaultEncoding; + } + + #endregion + + #region Internal Methods + + internal void AddHeader (string headerField) + { + var start = headerField[0]; + + if (start == ' ' || start == '\t') { + _context.ErrorMessage = "Invalid header field"; + + return; + } + + var colon = headerField.IndexOf (':'); + + if (colon < 1) { + _context.ErrorMessage = "Invalid header field"; + + return; + } + + var name = headerField.Substring (0, colon).Trim (); + + if (name.Length == 0 || !name.IsToken ()) { + _context.ErrorMessage = "Invalid header name"; + + return; + } + + var val = colon < headerField.Length - 1 + ? headerField.Substring (colon + 1).Trim () + : String.Empty; + + _headers.InternalSet (name, val, false); + + var lower = name.ToLower (CultureInfo.InvariantCulture); + + if (lower == "host") { + if (_userHostName != null) { + _context.ErrorMessage = "Invalid Host header"; + + return; + } + + if (val.Length == 0) { + _context.ErrorMessage = "Invalid Host header"; + + return; + } + + _userHostName = val; + + return; + } + + if (lower == "content-length") { + if (_contentLength > -1) { + _context.ErrorMessage = "Invalid Content-Length header"; + + return; + } + + long len; + + if (!Int64.TryParse (val, out len)) { + _context.ErrorMessage = "Invalid Content-Length header"; + + return; + } + + if (len < 0) { + _context.ErrorMessage = "Invalid Content-Length header"; + + return; + } + + _contentLength = len; + + return; + } + } + + internal void FinishInitialization () + { + if (_userHostName == null) { + _context.ErrorMessage = "Host header required"; + + return; + } + + var transferEnc = _headers["Transfer-Encoding"]; + + if (transferEnc != null) { + var compType = StringComparison.OrdinalIgnoreCase; + + if (!transferEnc.Equals ("chunked", compType)) { + _context.ErrorStatusCode = 501; + _context.ErrorMessage = "Invalid Transfer-Encoding header"; + + return; + } + + _chunked = true; + } + + if (_httpMethod == "POST" || _httpMethod == "PUT") { + if (_contentLength == -1 && !_chunked) { + _context.ErrorStatusCode = 411; + _context.ErrorMessage = "Content-Length header required"; + + return; + } + + if (_contentLength == 0 && !_chunked) { + _context.ErrorStatusCode = 411; + _context.ErrorMessage = "Invalid Content-Length header"; + + return; + } + } + + var expect = _headers["Expect"]; + + if (expect != null) { + var compType = StringComparison.OrdinalIgnoreCase; + + if (!expect.Equals ("100-continue", compType)) { + _context.ErrorStatusCode = 417; + _context.ErrorMessage = "Invalid Expect header"; + + return; + } + + var output = _connection.GetResponseStream (); + + output.InternalWrite (_100continue, 0, _100continue.Length); + } + } + + internal bool FlushInput () + { + var input = InputStream; + + if (input == Stream.Null) + return true; + + var len = 2048; + + if (_contentLength > 0 && _contentLength < len) + len = (int) _contentLength; + + var buff = new byte[len]; + + while (true) { + try { + var ares = input.BeginRead (buff, 0, len, null, null); + + if (!ares.IsCompleted) { + var timeout = 100; + + if (!ares.AsyncWaitHandle.WaitOne (timeout)) + return false; + } + + if (input.EndRead (ares) <= 0) + return true; + } + catch { + return false; + } + } + } + + internal bool IsUpgradeRequest (string protocol) + { + return _headers.Upgrades (protocol); + } + + internal void SetRequestLine (string requestLine) + { + var parts = requestLine.Split (new[] { ' ' }, 3); + + if (parts.Length < 3) { + _context.ErrorMessage = "Invalid request line (parts)"; + + return; + } + + var method = parts[0]; + + if (method.Length == 0) { + _context.ErrorMessage = "Invalid request line (method)"; + + return; + } + + if (!method.IsHttpMethod ()) { + _context.ErrorStatusCode = 501; + _context.ErrorMessage = "Invalid request line (method)"; + + return; + } + + var target = parts[1]; + + if (target.Length == 0) { + _context.ErrorMessage = "Invalid request line (target)"; + + return; + } + + var rawVer = parts[2]; + + if (rawVer.Length != 8) { + _context.ErrorMessage = "Invalid request line (version)"; + + return; + } + + if (!rawVer.StartsWith ("HTTP/", StringComparison.Ordinal)) { + _context.ErrorMessage = "Invalid request line (version)"; + + return; + } + + Version ver; + + if (!rawVer.Substring (5).TryCreateVersion (out ver)) { + _context.ErrorMessage = "Invalid request line (version)"; + + return; + } + + if (ver != HttpVersion.Version11) { + _context.ErrorStatusCode = 505; + _context.ErrorMessage = "Invalid request line (version)"; + + return; + } + + _httpMethod = method; + _rawUrl = target; + _protocolVersion = ver; + } + + #endregion + + #region Public Methods + + /// + /// Begins getting the certificate provided by the client asynchronously. + /// + /// + /// An instance that represents the status of + /// the asynchronous operation. + /// + /// + /// + /// An delegate. + /// + /// + /// It specifies the delegate called when the asynchronous operation is + /// complete. + /// + /// + /// + /// An that specifies a user defined object to pass to + /// . + /// + /// + /// This method is not supported. + /// + public IAsyncResult BeginGetClientCertificate ( + AsyncCallback requestCallback, + object state + ) + { + throw new NotSupportedException (); + } + + /// + /// Ends an asynchronous operation to get the certificate provided by + /// the client. + /// + /// + /// A that represents an X.509 certificate + /// provided by the client. + /// + /// + /// An instance obtained by calling + /// the method. + /// + /// + /// This method is not supported. + /// + public X509Certificate2 EndGetClientCertificate (IAsyncResult asyncResult) + { + throw new NotSupportedException (); + } + + /// + /// Gets the certificate provided by the client. + /// + /// + /// A that represents an X.509 certificate + /// provided by the client. + /// + /// + /// This method is not supported. + /// + public X509Certificate2 GetClientCertificate () + { + throw new NotSupportedException (); + } + + /// + /// Returns a string that represents the current instance. + /// + /// + /// A that contains the request line and headers + /// included in the request. + /// + public override string ToString () + { + var buff = new StringBuilder (64); + + var fmt = "{0} {1} HTTP/{2}\r\n"; + var headers = _headers.ToString (); + + buff + .AppendFormat (fmt, _httpMethod, _rawUrl, _protocolVersion) + .Append (headers); + + return buff.ToString (); + } + + #endregion + } +} diff --git a/Assets/External/websocket-sharp/Net/HttpListenerRequest.cs.meta b/Assets/External/websocket-sharp/Net/HttpListenerRequest.cs.meta new file mode 100644 index 00000000..15f73d28 --- /dev/null +++ b/Assets/External/websocket-sharp/Net/HttpListenerRequest.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 74456f8199f4b2c4aae6bd8089faf59a \ No newline at end of file diff --git a/Assets/External/websocket-sharp/Net/HttpListenerResponse.cs b/Assets/External/websocket-sharp/Net/HttpListenerResponse.cs new file mode 100644 index 00000000..500d4282 --- /dev/null +++ b/Assets/External/websocket-sharp/Net/HttpListenerResponse.cs @@ -0,0 +1,1201 @@ +#region License +/* + * HttpListenerResponse.cs + * + * This code is derived from HttpListenerResponse.cs (System.Net) of Mono + * (http://www.mono-project.com). + * + * The MIT License + * + * Copyright (c) 2005 Novell, Inc. (http://www.novell.com) + * Copyright (c) 2012-2024 sta.blockhead + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#endregion + +#region Authors +/* + * Authors: + * - Gonzalo Paniagua Javier + */ +#endregion + +#region Contributors +/* + * Contributors: + * - Nicholas Devenish + */ +#endregion + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Text; + +namespace WebSocketSharp.Net +{ + /// + /// Represents an HTTP response to an HTTP request received by + /// a instance. + /// + /// + /// This class cannot be inherited. + /// + public sealed class HttpListenerResponse : IDisposable + { + #region Private Fields + + private bool _closeConnection; + private Encoding _contentEncoding; + private long _contentLength; + private string _contentType; + private HttpListenerContext _context; + private CookieCollection _cookies; + private static readonly string _defaultProductName; + private bool _disposed; + private WebHeaderCollection _headers; + private bool _headersSent; + private bool _keepAlive; + private ResponseStream _outputStream; + private Uri _redirectLocation; + private bool _sendChunked; + private int _statusCode; + private string _statusDescription; + private Version _version; + + #endregion + + #region Static Constructor + + static HttpListenerResponse () + { + _defaultProductName = "websocket-sharp/1.0"; + } + + #endregion + + #region Internal Constructors + + internal HttpListenerResponse (HttpListenerContext context) + { + _context = context; + + _keepAlive = true; + _statusCode = 200; + _statusDescription = "OK"; + _version = HttpVersion.Version11; + } + + #endregion + + #region Internal Properties + + internal bool CloseConnection { + get { + return _closeConnection; + } + + set { + _closeConnection = value; + } + } + + internal WebHeaderCollection FullHeaders { + get { + var headers = new WebHeaderCollection (HttpHeaderType.Response, true); + + if (_headers != null) + headers.Add (_headers); + + if (_contentType != null) { + var val = createContentTypeHeaderText (_contentType, _contentEncoding); + + headers.InternalSet ("Content-Type", val, true); + } + + if (headers["Server"] == null) + headers.InternalSet ("Server", _defaultProductName, true); + + if (headers["Date"] == null) { + var val = DateTime.UtcNow.ToString ("r", CultureInfo.InvariantCulture); + + headers.InternalSet ("Date", val, true); + } + + if (_sendChunked) { + headers.InternalSet ("Transfer-Encoding", "chunked", true); + } + else { + var val = _contentLength.ToString (CultureInfo.InvariantCulture); + + headers.InternalSet ("Content-Length", val, true); + } + + /* + * Apache forces closing the connection for these status codes: + * - 400 Bad Request + * - 408 Request Timeout + * - 411 Length Required + * - 413 Request Entity Too Large + * - 414 Request-Uri Too Long + * - 500 Internal Server Error + * - 503 Service Unavailable + */ + + var reuses = _context.Connection.Reuses; + var closeConn = !_context.Request.KeepAlive + || !_keepAlive + || reuses >= 100 + || _statusCode == 400 + || _statusCode == 408 + || _statusCode == 411 + || _statusCode == 413 + || _statusCode == 414 + || _statusCode == 500 + || _statusCode == 503; + + if (closeConn) { + headers.InternalSet ("Connection", "close", true); + } + else { + var fmt = "timeout=15,max={0}"; + var max = 100 - reuses; + var val = String.Format (fmt, max); + + headers.InternalSet ("Keep-Alive", val, true); + } + + if (_redirectLocation != null) + headers.InternalSet ("Location", _redirectLocation.AbsoluteUri, true); + + if (_cookies != null) { + foreach (var cookie in _cookies) { + var val = cookie.ToResponseString (); + + headers.InternalSet ("Set-Cookie", val, true); + } + } + + return headers; + } + } + + internal bool HeadersSent { + get { + return _headersSent; + } + + set { + _headersSent = value; + } + } + + internal string ObjectName { + get { + return GetType ().ToString (); + } + } + + internal string StatusLine { + get { + var fmt = "HTTP/{0} {1} {2}\r\n"; + + return String.Format (fmt, _version, _statusCode, _statusDescription); + } + } + + #endregion + + #region Public Properties + + /// + /// Gets or sets the encoding for the entity body data included in + /// the response. + /// + /// + /// + /// A that represents the encoding for + /// the entity body data. + /// + /// + /// if no encoding is specified. + /// + /// + /// The default value is . + /// + /// + /// + /// The response is already being sent. + /// + /// + /// This instance is closed. + /// + public Encoding ContentEncoding { + get { + return _contentEncoding; + } + + set { + if (_disposed) + throw new ObjectDisposedException (ObjectName); + + if (_headersSent) { + var msg = "The response is already being sent."; + + throw new InvalidOperationException (msg); + } + + _contentEncoding = value; + } + } + + /// + /// Gets or sets the number of bytes in the entity body data included in + /// the response. + /// + /// + /// + /// A that represents the number of bytes in + /// the entity body data. + /// + /// + /// It is used for the value of the Content-Length header. + /// + /// + /// The default value is zero. + /// + /// + /// + /// The value specified for a set operation is less than zero. + /// + /// + /// The response is already being sent. + /// + /// + /// This instance is closed. + /// + public long ContentLength64 { + get { + return _contentLength; + } + + set { + if (_disposed) + throw new ObjectDisposedException (ObjectName); + + if (_headersSent) { + var msg = "The response is already being sent."; + + throw new InvalidOperationException (msg); + } + + if (value < 0) { + var msg = "Less than zero."; + + throw new ArgumentOutOfRangeException (msg, "value"); + } + + _contentLength = value; + } + } + + /// + /// Gets or sets the media type of the entity body included in + /// the response. + /// + /// + /// + /// A that represents the media type of + /// the entity body. + /// + /// + /// It is used for the value of the Content-Type header. + /// + /// + /// if no media type is specified. + /// + /// + /// The default value is . + /// + /// + /// + /// + /// The value specified for a set operation is an empty string. + /// + /// + /// -or- + /// + /// + /// The value specified for a set operation contains + /// an invalid character. + /// + /// + /// + /// The response is already being sent. + /// + /// + /// This instance is closed. + /// + public string ContentType { + get { + return _contentType; + } + + set { + if (_disposed) + throw new ObjectDisposedException (ObjectName); + + if (_headersSent) { + var msg = "The response is already being sent."; + + throw new InvalidOperationException (msg); + } + + if (value == null) { + _contentType = null; + + return; + } + + if (value.Length == 0) + throw new ArgumentException ("An empty string.", "value"); + + if (!isValidForContentType (value)) { + var msg = "It contains an invalid character."; + + throw new ArgumentException (msg, "value"); + } + + _contentType = value; + } + } + + /// + /// Gets or sets the collection of the HTTP cookies sent with the response. + /// + /// + /// A that contains the cookies sent with + /// the response. + /// + public CookieCollection Cookies { + get { + if (_cookies == null) + _cookies = new CookieCollection (); + + return _cookies; + } + + set { + _cookies = value; + } + } + + /// + /// Gets or sets the collection of the HTTP headers sent to the client. + /// + /// + /// A that contains the headers sent to + /// the client. + /// + /// + /// The value specified for a set operation is not valid for a response. + /// + public WebHeaderCollection Headers { + get { + if (_headers == null) + _headers = new WebHeaderCollection (HttpHeaderType.Response, false); + + return _headers; + } + + set { + if (value == null) { + _headers = null; + + return; + } + + if (value.State != HttpHeaderType.Response) { + var msg = "The value is not valid for a response."; + + throw new InvalidOperationException (msg); + } + + _headers = value; + } + } + + /// + /// Gets or sets a value indicating whether the server requests + /// a persistent connection. + /// + /// + /// + /// true if the server requests a persistent connection; + /// otherwise, false. + /// + /// + /// The default value is true. + /// + /// + /// + /// The response is already being sent. + /// + /// + /// This instance is closed. + /// + public bool KeepAlive { + get { + return _keepAlive; + } + + set { + if (_disposed) + throw new ObjectDisposedException (ObjectName); + + if (_headersSent) { + var msg = "The response is already being sent."; + + throw new InvalidOperationException (msg); + } + + _keepAlive = value; + } + } + + /// + /// Gets a stream instance to which the entity body data can be written. + /// + /// + /// A instance to which the entity body data can be + /// written. + /// + /// + /// This instance is closed. + /// + public Stream OutputStream { + get { + if (_disposed) + throw new ObjectDisposedException (ObjectName); + + if (_outputStream == null) + _outputStream = _context.Connection.GetResponseStream (); + + return _outputStream; + } + } + + /// + /// Gets the HTTP version used for the response. + /// + /// + /// + /// A that represents the HTTP version used for + /// the response. + /// + /// + /// Always returns same as 1.1. + /// + /// + public Version ProtocolVersion { + get { + return _version; + } + } + + /// + /// Gets or sets the URL to which the client is redirected to locate + /// a requested resource. + /// + /// + /// + /// A that represents the absolute URL for + /// the redirect location. + /// + /// + /// It is used for the value of the Location header. + /// + /// + /// if no redirect location is specified. + /// + /// + /// The default value is . + /// + /// + /// + /// + /// The value specified for a set operation is an empty string. + /// + /// + /// -or- + /// + /// + /// The value specified for a set operation is not an absolute URL. + /// + /// + /// + /// The response is already being sent. + /// + /// + /// This instance is closed. + /// + public string RedirectLocation { + get { + return _redirectLocation != null + ? _redirectLocation.OriginalString + : null; + } + + set { + if (_disposed) + throw new ObjectDisposedException (ObjectName); + + if (_headersSent) { + var msg = "The response is already being sent."; + + throw new InvalidOperationException (msg); + } + + if (value == null) { + _redirectLocation = null; + + return; + } + + if (value.Length == 0) + throw new ArgumentException ("An empty string.", "value"); + + Uri uri; + + if (!Uri.TryCreate (value, UriKind.Absolute, out uri)) { + var msg = "Not an absolute URL."; + + throw new ArgumentException (msg, "value"); + } + + _redirectLocation = uri; + } + } + + /// + /// Gets or sets a value indicating whether the response uses the chunked + /// transfer encoding. + /// + /// + /// + /// true if the response uses the chunked transfer encoding; + /// otherwise, false. + /// + /// + /// The default value is false. + /// + /// + /// + /// The response is already being sent. + /// + /// + /// This instance is closed. + /// + public bool SendChunked { + get { + return _sendChunked; + } + + set { + if (_disposed) + throw new ObjectDisposedException (ObjectName); + + if (_headersSent) { + var msg = "The response is already being sent."; + + throw new InvalidOperationException (msg); + } + + _sendChunked = value; + } + } + + /// + /// Gets or sets the HTTP status code returned to the client. + /// + /// + /// + /// An that represents the HTTP status code for + /// the response to the request. + /// + /// + /// The default value is 200. It indicates that the request has + /// succeeded. + /// + /// + /// + /// The response is already being sent. + /// + /// + /// This instance is closed. + /// + /// + /// + /// The value specified for a set operation is invalid. + /// + /// + /// Valid values are between 100 and 999 inclusive. + /// + /// + public int StatusCode { + get { + return _statusCode; + } + + set { + if (_disposed) + throw new ObjectDisposedException (ObjectName); + + if (_headersSent) { + var msg = "The response is already being sent."; + + throw new InvalidOperationException (msg); + } + + if (value < 100 || value > 999) { + var msg = "A value is not between 100 and 999 inclusive."; + + throw new System.Net.ProtocolViolationException (msg); + } + + _statusCode = value; + _statusDescription = value.GetStatusDescription (); + } + } + + /// + /// Gets or sets the description of the HTTP status code returned to + /// the client. + /// + /// + /// + /// A that represents the description of + /// the HTTP status code for the response to the request. + /// + /// + /// The default value is + /// the + /// RFC 2616 description for the + /// property value. + /// + /// + /// An empty string if an RFC 2616 description does not exist. + /// + /// + /// + /// The value specified for a set operation contains an invalid character. + /// + /// + /// The value specified for a set operation is . + /// + /// + /// The response is already being sent. + /// + /// + /// This instance is closed. + /// + public string StatusDescription { + get { + return _statusDescription; + } + + set { + if (_disposed) + throw new ObjectDisposedException (ObjectName); + + if (_headersSent) { + var msg = "The response is already being sent."; + + throw new InvalidOperationException (msg); + } + + if (value == null) + throw new ArgumentNullException ("value"); + + if (value.Length == 0) { + _statusDescription = _statusCode.GetStatusDescription (); + + return; + } + + if (!isValidForStatusDescription (value)) { + var msg = "It contains an invalid character."; + + throw new ArgumentException (msg, "value"); + } + + _statusDescription = value; + } + } + + #endregion + + #region Private Methods + + private bool canSetCookie (Cookie cookie) + { + var res = findCookie (cookie).ToList (); + + if (res.Count == 0) + return true; + + var ver = cookie.Version; + + foreach (var c in res) { + if (c.Version == ver) + return true; + } + + return false; + } + + private void close (bool force) + { + _disposed = true; + + _context.Connection.Close (force); + } + + private void close (byte[] responseEntity, int bufferLength, bool willBlock) + { + if (willBlock) { + OutputStream.WriteBytes (responseEntity, bufferLength); + close (false); + + return; + } + + OutputStream.WriteBytesAsync ( + responseEntity, + bufferLength, + () => close (false), + null + ); + } + + private static string createContentTypeHeaderText ( + string value, + Encoding encoding + ) + { + if (value.Contains ("charset=")) + return value; + + if (encoding == null) + return value; + + var fmt = "{0}; charset={1}"; + + return String.Format (fmt, value, encoding.WebName); + } + + private IEnumerable findCookie (Cookie cookie) + { + if (_cookies == null || _cookies.Count == 0) + yield break; + + foreach (var c in _cookies) { + if (c.EqualsWithoutValueAndVersion (cookie)) + yield return c; + } + } + + private static bool isValidForContentType (string value) + { + foreach (var c in value) { + if (c < 0x20) + return false; + + if (c > 0x7e) + return false; + + if ("()<>@:\\[]?{}".IndexOf (c) > -1) + return false; + } + + return true; + } + + private static bool isValidForStatusDescription (string value) + { + foreach (var c in value) { + if (c < 0x20) + return false; + + if (c > 0x7e) + return false; + } + + return true; + } + + #endregion + + #region Public Methods + + /// + /// Closes the connection to the client without sending a response. + /// + public void Abort () + { + if (_disposed) + return; + + close (true); + } + + /// + /// Appends an HTTP cookie to the cookies sent with the response. + /// + /// + /// A to append. + /// + /// + /// is . + /// + public void AppendCookie (Cookie cookie) + { + Cookies.Add (cookie); + } + + /// + /// Appends an HTTP header with the specified name and value to + /// the headers for the response. + /// + /// + /// A that specifies the name of the header to + /// append. + /// + /// + /// A that specifies the value of the header to + /// append. + /// + /// + /// + /// is an empty string. + /// + /// + /// -or- + /// + /// + /// is a string of spaces. + /// + /// + /// -or- + /// + /// + /// contains an invalid character. + /// + /// + /// -or- + /// + /// + /// contains an invalid character. + /// + /// + /// -or- + /// + /// + /// is a restricted header name. + /// + /// + /// + /// is . + /// + /// + /// The length of is greater than 65,535 + /// characters. + /// + /// + /// The current headers do not allow the header. + /// + public void AppendHeader (string name, string value) + { + Headers.Add (name, value); + } + + /// + /// Sends the response to the client and releases the resources used by + /// this instance. + /// + public void Close () + { + if (_disposed) + return; + + close (false); + } + + /// + /// Sends the response with the specified entity body data to the client + /// and releases the resources used by this instance. + /// + /// + /// An array of that contains the entity body data. + /// + /// + /// A : true if this method blocks execution while + /// flushing the stream to the client; otherwise, false. + /// + /// + /// is . + /// + /// + /// This instance is closed. + /// + public void Close (byte[] responseEntity, bool willBlock) + { + if (_disposed) + throw new ObjectDisposedException (ObjectName); + + if (responseEntity == null) + throw new ArgumentNullException ("responseEntity"); + + var len = responseEntity.LongLength; + + if (len > Int32.MaxValue) { + close (responseEntity, 1024, willBlock); + + return; + } + + var stream = OutputStream; + + if (willBlock) { + stream.Write (responseEntity, 0, (int) len); + close (false); + + return; + } + + stream.BeginWrite ( + responseEntity, + 0, + (int) len, + ar => { + stream.EndWrite (ar); + close (false); + }, + null + ); + } + + /// + /// Copies some properties from the specified response instance to + /// this instance. + /// + /// + /// A to copy. + /// + /// + /// is . + /// + public void CopyFrom (HttpListenerResponse templateResponse) + { + if (templateResponse == null) + throw new ArgumentNullException ("templateResponse"); + + var headers = templateResponse._headers; + + if (headers != null) { + if (_headers != null) + _headers.Clear (); + + Headers.Add (headers); + } + else { + _headers = null; + } + + _contentLength = templateResponse._contentLength; + _statusCode = templateResponse._statusCode; + _statusDescription = templateResponse._statusDescription; + _keepAlive = templateResponse._keepAlive; + _version = templateResponse._version; + } + + /// + /// Configures the response to redirect the client's request to + /// the specified URL. + /// + /// + /// This method sets the property to + /// , the property to + /// 302, and the property to "Found". + /// + /// + /// A that specifies the absolute URL to which + /// the client is redirected to locate a requested resource. + /// + /// + /// + /// is an empty string. + /// + /// + /// -or- + /// + /// + /// is not an absolute URL. + /// + /// + /// + /// is . + /// + /// + /// The response is already being sent. + /// + /// + /// This instance is closed. + /// + public void Redirect (string url) + { + if (_disposed) + throw new ObjectDisposedException (ObjectName); + + if (_headersSent) { + var msg = "The response is already being sent."; + + throw new InvalidOperationException (msg); + } + + if (url == null) + throw new ArgumentNullException ("url"); + + if (url.Length == 0) + throw new ArgumentException ("An empty string.", "url"); + + Uri uri; + + if (!Uri.TryCreate (url, UriKind.Absolute, out uri)) { + var msg = "Not an absolute URL."; + + throw new ArgumentException (msg, "url"); + } + + _redirectLocation = uri; + _statusCode = 302; + _statusDescription = "Found"; + } + + /// + /// Adds or updates an HTTP cookie in the cookies sent with the response. + /// + /// + /// A to set. + /// + /// + /// already exists in the cookies but + /// it cannot be updated. + /// + /// + /// is . + /// + public void SetCookie (Cookie cookie) + { + if (cookie == null) + throw new ArgumentNullException ("cookie"); + + if (!canSetCookie (cookie)) { + var msg = "It cannot be updated."; + + throw new ArgumentException (msg, "cookie"); + } + + Cookies.Add (cookie); + } + + /// + /// Adds or updates an HTTP header with the specified name and value in + /// the headers for the response. + /// + /// + /// A that specifies the name of the header to set. + /// + /// + /// A that specifies the value of the header to set. + /// + /// + /// + /// is an empty string. + /// + /// + /// -or- + /// + /// + /// is a string of spaces. + /// + /// + /// -or- + /// + /// + /// contains an invalid character. + /// + /// + /// -or- + /// + /// + /// contains an invalid character. + /// + /// + /// -or- + /// + /// + /// is a restricted header name. + /// + /// + /// + /// is . + /// + /// + /// The length of is greater than 65,535 + /// characters. + /// + /// + /// The current headers do not allow the header. + /// + public void SetHeader (string name, string value) + { + Headers.Set (name, value); + } + + #endregion + + #region Explicit Interface Implementations + + /// + /// Releases all resources used by this instance. + /// + void IDisposable.Dispose () + { + if (_disposed) + return; + + close (true); + } + + #endregion + } +} diff --git a/Assets/External/websocket-sharp/Net/HttpListenerResponse.cs.meta b/Assets/External/websocket-sharp/Net/HttpListenerResponse.cs.meta new file mode 100644 index 00000000..507186e0 --- /dev/null +++ b/Assets/External/websocket-sharp/Net/HttpListenerResponse.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 33da38aab487ebd449ffa8848b25d89f \ No newline at end of file diff --git a/Assets/External/websocket-sharp/Net/HttpRequestHeader.cs b/Assets/External/websocket-sharp/Net/HttpRequestHeader.cs new file mode 100644 index 00000000..aab3497f --- /dev/null +++ b/Assets/External/websocket-sharp/Net/HttpRequestHeader.cs @@ -0,0 +1,233 @@ +#region License +/* + * HttpRequestHeader.cs + * + * This code is derived from HttpRequestHeader.cs (System.Net) of Mono + * (http://www.mono-project.com). + * + * The MIT License + * + * Copyright (c) 2005 Novell, Inc. (http://www.novell.com) + * Copyright (c) 2014-2020 sta.blockhead + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#endregion + +#region Authors +/* + * Authors: + * - Gonzalo Paniagua Javier + */ +#endregion + +namespace WebSocketSharp.Net +{ + /// + /// Indicates the HTTP header that may be specified in a client request. + /// + /// + /// The headers of this enumeration are defined in + /// RFC 2616 or + /// RFC 6455. + /// + public enum HttpRequestHeader + { + /// + /// Indicates the Cache-Control header. + /// + CacheControl, + /// + /// Indicates the Connection header. + /// + Connection, + /// + /// Indicates the Date header. + /// + Date, + /// + /// Indicates the Keep-Alive header. + /// + KeepAlive, + /// + /// Indicates the Pragma header. + /// + Pragma, + /// + /// Indicates the Trailer header. + /// + Trailer, + /// + /// Indicates the Transfer-Encoding header. + /// + TransferEncoding, + /// + /// Indicates the Upgrade header. + /// + Upgrade, + /// + /// Indicates the Via header. + /// + Via, + /// + /// Indicates the Warning header. + /// + Warning, + /// + /// Indicates the Allow header. + /// + Allow, + /// + /// Indicates the Content-Length header. + /// + ContentLength, + /// + /// Indicates the Content-Type header. + /// + ContentType, + /// + /// Indicates the Content-Encoding header. + /// + ContentEncoding, + /// + /// Indicates the Content-Language header. + /// + ContentLanguage, + /// + /// Indicates the Content-Location header. + /// + ContentLocation, + /// + /// Indicates the Content-MD5 header. + /// + ContentMd5, + /// + /// Indicates the Content-Range header. + /// + ContentRange, + /// + /// Indicates the Expires header. + /// + Expires, + /// + /// Indicates the Last-Modified header. + /// + LastModified, + /// + /// Indicates the Accept header. + /// + Accept, + /// + /// Indicates the Accept-Charset header. + /// + AcceptCharset, + /// + /// Indicates the Accept-Encoding header. + /// + AcceptEncoding, + /// + /// Indicates the Accept-Language header. + /// + AcceptLanguage, + /// + /// Indicates the Authorization header. + /// + Authorization, + /// + /// Indicates the Cookie header. + /// + Cookie, + /// + /// Indicates the Expect header. + /// + Expect, + /// + /// Indicates the From header. + /// + From, + /// + /// Indicates the Host header. + /// + Host, + /// + /// Indicates the If-Match header. + /// + IfMatch, + /// + /// Indicates the If-Modified-Since header. + /// + IfModifiedSince, + /// + /// Indicates the If-None-Match header. + /// + IfNoneMatch, + /// + /// Indicates the If-Range header. + /// + IfRange, + /// + /// Indicates the If-Unmodified-Since header. + /// + IfUnmodifiedSince, + /// + /// Indicates the Max-Forwards header. + /// + MaxForwards, + /// + /// Indicates the Proxy-Authorization header. + /// + ProxyAuthorization, + /// + /// Indicates the Referer header. + /// + Referer, + /// + /// Indicates the Range header. + /// + Range, + /// + /// Indicates the TE header. + /// + Te, + /// + /// Indicates the Translate header. + /// + Translate, + /// + /// Indicates the User-Agent header. + /// + UserAgent, + /// + /// Indicates the Sec-WebSocket-Key header. + /// + SecWebSocketKey, + /// + /// Indicates the Sec-WebSocket-Extensions header. + /// + SecWebSocketExtensions, + /// + /// Indicates the Sec-WebSocket-Protocol header. + /// + SecWebSocketProtocol, + /// + /// Indicates the Sec-WebSocket-Version header. + /// + SecWebSocketVersion + } +} diff --git a/Assets/External/websocket-sharp/Net/HttpRequestHeader.cs.meta b/Assets/External/websocket-sharp/Net/HttpRequestHeader.cs.meta new file mode 100644 index 00000000..caba7e1d --- /dev/null +++ b/Assets/External/websocket-sharp/Net/HttpRequestHeader.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: ffdc665e7f1df3b41bf94a12cb43defd \ No newline at end of file diff --git a/Assets/External/websocket-sharp/Net/HttpResponseHeader.cs b/Assets/External/websocket-sharp/Net/HttpResponseHeader.cs new file mode 100644 index 00000000..d32afe6c --- /dev/null +++ b/Assets/External/websocket-sharp/Net/HttpResponseHeader.cs @@ -0,0 +1,189 @@ +#region License +/* + * HttpResponseHeader.cs + * + * This code is derived from HttpResponseHeader.cs (System.Net) of Mono + * (http://www.mono-project.com). + * + * The MIT License + * + * Copyright (c) 2005 Novell, Inc. (http://www.novell.com) + * Copyright (c) 2014-2020 sta.blockhead + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#endregion + +#region Authors +/* + * Authors: + * - Gonzalo Paniagua Javier + */ +#endregion + +namespace WebSocketSharp.Net +{ + /// + /// Indicates the HTTP header that can be specified in a server response. + /// + /// + /// The headers of this enumeration are defined in + /// RFC 2616 or + /// RFC 6455. + /// + public enum HttpResponseHeader + { + /// + /// Indicates the Cache-Control header. + /// + CacheControl, + /// + /// Indicates the Connection header. + /// + Connection, + /// + /// Indicates the Date header. + /// + Date, + /// + /// Indicates the Keep-Alive header. + /// + KeepAlive, + /// + /// Indicates the Pragma header. + /// + Pragma, + /// + /// Indicates the Trailer header. + /// + Trailer, + /// + /// Indicates the Transfer-Encoding header. + /// + TransferEncoding, + /// + /// Indicates the Upgrade header. + /// + Upgrade, + /// + /// Indicates the Via header. + /// + Via, + /// + /// Indicates the Warning header. + /// + Warning, + /// + /// Indicates the Allow header. + /// + Allow, + /// + /// Indicates the Content-Length header. + /// + ContentLength, + /// + /// Indicates the Content-Type header. + /// + ContentType, + /// + /// Indicates the Content-Encoding header. + /// + ContentEncoding, + /// + /// Indicates the Content-Language header. + /// + ContentLanguage, + /// + /// Indicates the Content-Location header. + /// + ContentLocation, + /// + /// Indicates the Content-MD5 header. + /// + ContentMd5, + /// + /// Indicates the Content-Range header. + /// + ContentRange, + /// + /// Indicates the Expires header. + /// + Expires, + /// + /// Indicates the Last-Modified header. + /// + LastModified, + /// + /// Indicates the Accept-Ranges header. + /// + AcceptRanges, + /// + /// Indicates the Age header. + /// + Age, + /// + /// Indicates the ETag header. + /// + ETag, + /// + /// Indicates the Location header. + /// + Location, + /// + /// Indicates the Proxy-Authenticate header. + /// + ProxyAuthenticate, + /// + /// Indicates the Retry-After header. + /// + RetryAfter, + /// + /// Indicates the Server header. + /// + Server, + /// + /// Indicates the Set-Cookie header. + /// + SetCookie, + /// + /// Indicates the Vary header. + /// + Vary, + /// + /// Indicates the WWW-Authenticate header. + /// + WwwAuthenticate, + /// + /// Indicates the Sec-WebSocket-Extensions header. + /// + SecWebSocketExtensions, + /// + /// Indicates the Sec-WebSocket-Accept header. + /// + SecWebSocketAccept, + /// + /// Indicates the Sec-WebSocket-Protocol header. + /// + SecWebSocketProtocol, + /// + /// Indicates the Sec-WebSocket-Version header. + /// + SecWebSocketVersion + } +} diff --git a/Assets/External/websocket-sharp/Net/HttpResponseHeader.cs.meta b/Assets/External/websocket-sharp/Net/HttpResponseHeader.cs.meta new file mode 100644 index 00000000..d88b3e1f --- /dev/null +++ b/Assets/External/websocket-sharp/Net/HttpResponseHeader.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 273615a0fa5cbad40806a4188f12a253 \ No newline at end of file diff --git a/Assets/External/websocket-sharp/Net/HttpStatusCode.cs b/Assets/External/websocket-sharp/Net/HttpStatusCode.cs new file mode 100644 index 00000000..b773a496 --- /dev/null +++ b/Assets/External/websocket-sharp/Net/HttpStatusCode.cs @@ -0,0 +1,346 @@ +#region License +/* + * HttpStatusCode.cs + * + * This code is derived from HttpStatusCode.cs (System.Net) of Mono + * (http://www.mono-project.com). + * + * It was automatically generated from ECMA CLI XML Library Specification. + * Generator: libgen.xsl [1.0; (C) Sergey Chaban (serge@wildwestsoftware.com)] + * Created: Wed, 5 Sep 2001 06:32:05 UTC + * Source file: AllTypes.xml + * URL: http://msdn.microsoft.com/net/ecma/AllTypes.xml + * + * The MIT License + * + * Copyright (c) 2001 Ximian, Inc. (http://www.ximian.com) + * Copyright (c) 2012-2020 sta.blockhead + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#endregion + +namespace WebSocketSharp.Net +{ + /// + /// Indicates the HTTP status code that can be specified in a server response. + /// + /// + /// The values of this enumeration are defined in + /// RFC 2616. + /// + public enum HttpStatusCode + { + /// + /// Equivalent to status code 100. Indicates that the client should continue + /// with its request. + /// + Continue = 100, + /// + /// Equivalent to status code 101. Indicates that the server is switching + /// the HTTP version or protocol on the connection. + /// + SwitchingProtocols = 101, + /// + /// Equivalent to status code 200. Indicates that the client's request has + /// succeeded. + /// + OK = 200, + /// + /// Equivalent to status code 201. Indicates that the client's request has + /// been fulfilled and resulted in a new resource being created. + /// + Created = 201, + /// + /// Equivalent to status code 202. Indicates that the client's request has + /// been accepted for processing, but the processing has not been completed. + /// + Accepted = 202, + /// + /// Equivalent to status code 203. Indicates that the returned metainformation + /// is from a local or a third-party copy instead of the origin server. + /// + NonAuthoritativeInformation = 203, + /// + /// Equivalent to status code 204. Indicates that the server has fulfilled + /// the client's request but does not need to return an entity-body. + /// + NoContent = 204, + /// + /// Equivalent to status code 205. Indicates that the server has fulfilled + /// the client's request, and the user agent should reset the document view + /// which caused the request to be sent. + /// + ResetContent = 205, + /// + /// Equivalent to status code 206. Indicates that the server has fulfilled + /// the partial GET request for the resource. + /// + PartialContent = 206, + /// + /// + /// Equivalent to status code 300. Indicates that the requested resource + /// corresponds to any of multiple representations. + /// + /// + /// MultipleChoices is a synonym for Ambiguous. + /// + /// + MultipleChoices = 300, + /// + /// + /// Equivalent to status code 300. Indicates that the requested resource + /// corresponds to any of multiple representations. + /// + /// + /// Ambiguous is a synonym for MultipleChoices. + /// + /// + Ambiguous = 300, + /// + /// + /// Equivalent to status code 301. Indicates that the requested resource + /// has been assigned a new permanent URI and any future references to + /// this resource should use one of the returned URIs. + /// + /// + /// MovedPermanently is a synonym for Moved. + /// + /// + MovedPermanently = 301, + /// + /// + /// Equivalent to status code 301. Indicates that the requested resource + /// has been assigned a new permanent URI and any future references to + /// this resource should use one of the returned URIs. + /// + /// + /// Moved is a synonym for MovedPermanently. + /// + /// + Moved = 301, + /// + /// + /// Equivalent to status code 302. Indicates that the requested resource + /// is located temporarily under a different URI. + /// + /// + /// Found is a synonym for Redirect. + /// + /// + Found = 302, + /// + /// + /// Equivalent to status code 302. Indicates that the requested resource + /// is located temporarily under a different URI. + /// + /// + /// Redirect is a synonym for Found. + /// + /// + Redirect = 302, + /// + /// + /// Equivalent to status code 303. Indicates that the response to + /// the request can be found under a different URI and should be + /// retrieved using a GET method on that resource. + /// + /// + /// SeeOther is a synonym for RedirectMethod. + /// + /// + SeeOther = 303, + /// + /// + /// Equivalent to status code 303. Indicates that the response to + /// the request can be found under a different URI and should be + /// retrieved using a GET method on that resource. + /// + /// + /// RedirectMethod is a synonym for SeeOther. + /// + /// + RedirectMethod = 303, + /// + /// Equivalent to status code 304. Indicates that the client has performed + /// a conditional GET request and access is allowed, but the document has + /// not been modified. + /// + NotModified = 304, + /// + /// Equivalent to status code 305. Indicates that the requested resource + /// must be accessed through the proxy given by the Location field. + /// + UseProxy = 305, + /// + /// Equivalent to status code 306. This status code was used in a previous + /// version of the specification, is no longer used, and is reserved for + /// future use. + /// + Unused = 306, + /// + /// + /// Equivalent to status code 307. Indicates that the requested resource + /// is located temporarily under a different URI. + /// + /// + /// TemporaryRedirect is a synonym for RedirectKeepVerb. + /// + /// + TemporaryRedirect = 307, + /// + /// + /// Equivalent to status code 307. Indicates that the requested resource + /// is located temporarily under a different URI. + /// + /// + /// RedirectKeepVerb is a synonym for TemporaryRedirect. + /// + /// + RedirectKeepVerb = 307, + /// + /// Equivalent to status code 400. Indicates that the client's request could + /// not be understood by the server due to malformed syntax. + /// + BadRequest = 400, + /// + /// Equivalent to status code 401. Indicates that the client's request + /// requires user authentication. + /// + Unauthorized = 401, + /// + /// Equivalent to status code 402. This status code is reserved for future + /// use. + /// + PaymentRequired = 402, + /// + /// Equivalent to status code 403. Indicates that the server understood + /// the client's request but is refusing to fulfill it. + /// + Forbidden = 403, + /// + /// Equivalent to status code 404. Indicates that the server has not found + /// anything matching the request URI. + /// + NotFound = 404, + /// + /// Equivalent to status code 405. Indicates that the method specified + /// in the request line is not allowed for the resource identified by + /// the request URI. + /// + MethodNotAllowed = 405, + /// + /// Equivalent to status code 406. Indicates that the server does not + /// have the appropriate resource to respond to the Accept headers in + /// the client's request. + /// + NotAcceptable = 406, + /// + /// Equivalent to status code 407. Indicates that the client must first + /// authenticate itself with the proxy. + /// + ProxyAuthenticationRequired = 407, + /// + /// Equivalent to status code 408. Indicates that the client did not produce + /// a request within the time that the server was prepared to wait. + /// + RequestTimeout = 408, + /// + /// Equivalent to status code 409. Indicates that the client's request could + /// not be completed due to a conflict on the server. + /// + Conflict = 409, + /// + /// Equivalent to status code 410. Indicates that the requested resource is + /// no longer available at the server and no forwarding address is known. + /// + Gone = 410, + /// + /// Equivalent to status code 411. Indicates that the server refuses to + /// accept the client's request without a defined Content-Length. + /// + LengthRequired = 411, + /// + /// Equivalent to status code 412. Indicates that the precondition given in + /// one or more of the request headers evaluated to false when it was tested + /// on the server. + /// + PreconditionFailed = 412, + /// + /// Equivalent to status code 413. Indicates that the entity of the client's + /// request is larger than the server is willing or able to process. + /// + RequestEntityTooLarge = 413, + /// + /// Equivalent to status code 414. Indicates that the request URI is longer + /// than the server is willing to interpret. + /// + RequestUriTooLong = 414, + /// + /// Equivalent to status code 415. Indicates that the entity of the client's + /// request is in a format not supported by the requested resource for the + /// requested method. + /// + UnsupportedMediaType = 415, + /// + /// Equivalent to status code 416. Indicates that none of the range + /// specifier values in a Range request header overlap the current + /// extent of the selected resource. + /// + RequestedRangeNotSatisfiable = 416, + /// + /// Equivalent to status code 417. Indicates that the expectation given in + /// an Expect request header could not be met by the server. + /// + ExpectationFailed = 417, + /// + /// Equivalent to status code 500. Indicates that the server encountered + /// an unexpected condition which prevented it from fulfilling the client's + /// request. + /// + InternalServerError = 500, + /// + /// Equivalent to status code 501. Indicates that the server does not + /// support the functionality required to fulfill the client's request. + /// + NotImplemented = 501, + /// + /// Equivalent to status code 502. Indicates that a gateway or proxy server + /// received an invalid response from the upstream server. + /// + BadGateway = 502, + /// + /// Equivalent to status code 503. Indicates that the server is currently + /// unable to handle the client's request due to a temporary overloading + /// or maintenance of the server. + /// + ServiceUnavailable = 503, + /// + /// Equivalent to status code 504. Indicates that a gateway or proxy server + /// did not receive a timely response from the upstream server or some other + /// auxiliary server. + /// + GatewayTimeout = 504, + /// + /// Equivalent to status code 505. Indicates that the server does not + /// support the HTTP version used in the client's request. + /// + HttpVersionNotSupported = 505, + } +} diff --git a/Assets/External/websocket-sharp/Net/HttpStatusCode.cs.meta b/Assets/External/websocket-sharp/Net/HttpStatusCode.cs.meta new file mode 100644 index 00000000..164b0d0d --- /dev/null +++ b/Assets/External/websocket-sharp/Net/HttpStatusCode.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: b0ab739b61e8c01498c39a4ab5289949 \ No newline at end of file diff --git a/Assets/External/websocket-sharp/Net/HttpStreamAsyncResult.cs b/Assets/External/websocket-sharp/Net/HttpStreamAsyncResult.cs new file mode 100644 index 00000000..09447ea2 --- /dev/null +++ b/Assets/External/websocket-sharp/Net/HttpStreamAsyncResult.cs @@ -0,0 +1,201 @@ +#region License +/* + * HttpStreamAsyncResult.cs + * + * This code is derived from HttpStreamAsyncResult.cs (System.Net) of Mono + * (http://www.mono-project.com). + * + * The MIT License + * + * Copyright (c) 2005 Novell, Inc. (http://www.novell.com) + * Copyright (c) 2012-2021 sta.blockhead + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#endregion + +#region Authors +/* + * Authors: + * - Gonzalo Paniagua Javier + */ +#endregion + +using System; +using System.Threading; + +namespace WebSocketSharp.Net +{ + internal class HttpStreamAsyncResult : IAsyncResult + { + #region Private Fields + + private byte[] _buffer; + private AsyncCallback _callback; + private bool _completed; + private int _count; + private Exception _exception; + private int _offset; + private object _state; + private object _sync; + private int _syncRead; + private ManualResetEvent _waitHandle; + + #endregion + + #region Internal Constructors + + internal HttpStreamAsyncResult (AsyncCallback callback, object state) + { + _callback = callback; + _state = state; + + _sync = new object (); + } + + #endregion + + #region Internal Properties + + internal byte[] Buffer { + get { + return _buffer; + } + + set { + _buffer = value; + } + } + + internal int Count { + get { + return _count; + } + + set { + _count = value; + } + } + + internal Exception Exception { + get { + return _exception; + } + } + + internal bool HasException { + get { + return _exception != null; + } + } + + internal int Offset { + get { + return _offset; + } + + set { + _offset = value; + } + } + + internal int SyncRead { + get { + return _syncRead; + } + + set { + _syncRead = value; + } + } + + #endregion + + #region Public Properties + + public object AsyncState { + get { + return _state; + } + } + + public WaitHandle AsyncWaitHandle { + get { + lock (_sync) { + if (_waitHandle == null) + _waitHandle = new ManualResetEvent (_completed); + + return _waitHandle; + } + } + } + + public bool CompletedSynchronously { + get { + return _syncRead == _count; + } + } + + public bool IsCompleted { + get { + lock (_sync) + return _completed; + } + } + + #endregion + + #region Internal Methods + + internal void Complete () + { + lock (_sync) { + if (_completed) + return; + + _completed = true; + + if (_waitHandle != null) + _waitHandle.Set (); + + if (_callback != null) + _callback.BeginInvoke (this, ar => _callback.EndInvoke (ar), null); + } + } + + internal void Complete (Exception exception) + { + lock (_sync) { + if (_completed) + return; + + _completed = true; + _exception = exception; + + if (_waitHandle != null) + _waitHandle.Set (); + + if (_callback != null) + _callback.BeginInvoke (this, ar => _callback.EndInvoke (ar), null); + } + } + + #endregion + } +} diff --git a/Assets/External/websocket-sharp/Net/HttpStreamAsyncResult.cs.meta b/Assets/External/websocket-sharp/Net/HttpStreamAsyncResult.cs.meta new file mode 100644 index 00000000..519974e4 --- /dev/null +++ b/Assets/External/websocket-sharp/Net/HttpStreamAsyncResult.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 7b214852a23637e46bd25b72af883f25 \ No newline at end of file diff --git a/Assets/External/websocket-sharp/Net/HttpUtility.cs b/Assets/External/websocket-sharp/Net/HttpUtility.cs new file mode 100644 index 00000000..b7db7b92 --- /dev/null +++ b/Assets/External/websocket-sharp/Net/HttpUtility.cs @@ -0,0 +1,1237 @@ +#region License +/* + * HttpUtility.cs + * + * This code is derived from HttpUtility.cs (System.Net) of Mono + * (http://www.mono-project.com). + * + * The MIT License + * + * Copyright (c) 2005-2009 Novell, Inc. (http://www.novell.com) + * Copyright (c) 2012-2024 sta.blockhead + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#endregion + +#region Authors +/* + * Authors: + * - Patrik Torstensson + * - Wictor Wilén (decode/encode functions) + * - Tim Coleman + * - Gonzalo Paniagua Javier + */ +#endregion + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Globalization; +using System.IO; +using System.Security.Principal; +using System.Text; + +namespace WebSocketSharp.Net +{ + internal static class HttpUtility + { + #region Private Fields + + private static Dictionary _entities; + private static char[] _hexChars; + private static object _sync; + + #endregion + + #region Static Constructor + + static HttpUtility () + { + _hexChars = "0123456789ABCDEF".ToCharArray (); + _sync = new object (); + } + + #endregion + + #region Private Methods + + private static Dictionary getEntities () + { + lock (_sync) { + if (_entities == null) + initEntities (); + + return _entities; + } + } + + private static int getNumber (char c) + { + if (c >= '0' && c <= '9') + return c - '0'; + + if (c >= 'A' && c <= 'F') + return c - 'A' + 10; + + if (c >= 'a' && c <= 'f') + return c - 'a' + 10; + + return -1; + } + + private static int getNumber (byte[] bytes, int offset, int count) + { + var ret = 0; + + var end = offset + count - 1; + + for (var i = offset; i <= end; i++) { + var c = (char) bytes[i]; + var n = getNumber (c); + + if (n == -1) + return -1; + + ret = (ret << 4) + n; + } + + return ret; + } + + private static int getNumber (string s, int offset, int count) + { + var ret = 0; + + var end = offset + count - 1; + + for (var i = offset; i <= end; i++) { + var c = s[i]; + var n = getNumber (c); + + if (n == -1) + return -1; + + ret = (ret << 4) + n; + } + + return ret; + } + + private static string htmlDecode (string s) + { + var buff = new StringBuilder (); + + // 0: None + // 1: Right after '&' + // 2: Between '&' and ';' but no NCR + // 3: '#' found after '&' and getting numbers + // 4: 'x' found after '#' and getting numbers + var state = 0; + + var reference = new StringBuilder (); + var num = 0; + + foreach (var c in s) { + if (state == 0) { + if (c == '&') { + reference.Append ('&'); + + state = 1; + + continue; + } + + buff.Append (c); + + continue; + } + + if (c == '&') { + buff.Append (reference.ToString ()); + + reference.Length = 0; + + reference.Append ('&'); + + state = 1; + + continue; + } + + reference.Append (c); + + if (state == 1) { + if (c == ';') { + buff.Append (reference.ToString ()); + + reference.Length = 0; + state = 0; + + continue; + } + + num = 0; + state = c == '#' ? 3 : 2; + + continue; + } + + if (state == 2) { + if (c == ';') { + var entity = reference.ToString (); + var name = entity.Substring (1, entity.Length - 2); + + var entities = getEntities (); + + if (entities.ContainsKey (name)) + buff.Append (entities[name]); + else + buff.Append (entity); + + reference.Length = 0; + state = 0; + + continue; + } + + continue; + } + + if (state == 3) { + if (c == ';') { + if (reference.Length > 3 && num < 65536) + buff.Append ((char) num); + else + buff.Append (reference.ToString ()); + + reference.Length = 0; + state = 0; + + continue; + } + + if (c == 'x') { + state = reference.Length == 3 ? 4 : 2; + + continue; + } + + if (!isNumeric (c)) { + state = 2; + + continue; + } + + num = num * 10 + (c - '0'); + + continue; + } + + if (state == 4) { + if (c == ';') { + if (reference.Length > 4 && num < 65536) + buff.Append ((char) num); + else + buff.Append (reference.ToString ()); + + reference.Length = 0; + state = 0; + + continue; + } + + var n = getNumber (c); + + if (n == -1) { + state = 2; + + continue; + } + + num = (num << 4) + n; + } + } + + if (reference.Length > 0) + buff.Append (reference.ToString ()); + + return buff.ToString (); + } + + /// + /// Converts the specified string to an HTML-encoded string. + /// + /// + /// + /// This method starts encoding with a NCR from the character code 160 + /// but does not stop at the character code 255. + /// + /// + /// One reason is the unicode characters < and > that + /// look like < and >. + /// + /// + /// + /// A that represents an encoded string. + /// + /// + /// A to encode. + /// + /// + /// A : true if encodes without a NCR; + /// otherwise, false. + /// + private static string htmlEncode (string s, bool minimal) + { + var buff = new StringBuilder (); + + foreach (var c in s) { + if (c == '"') { + buff.Append ("""); + + continue; + } + + if (c == '&') { + buff.Append ("&"); + + continue; + } + + if (c == '<') { + buff.Append ("<"); + + continue; + } + + if (c == '>') { + buff.Append (">"); + + continue; + } + + if (c > 159) { + if (!minimal) { + var val = String.Format ("&#{0};", (int) c); + + buff.Append (val); + + continue; + } + } + + buff.Append (c); + } + + return buff.ToString (); + } + + /// + /// Initializes the _entities field. + /// + /// + /// This method builds a dictionary of HTML character entity references. + /// This dictionary comes from the HTML 4.01 W3C recommendation. + /// + private static void initEntities () + { + _entities = new Dictionary (); + + _entities.Add ("nbsp", '\u00A0'); + _entities.Add ("iexcl", '\u00A1'); + _entities.Add ("cent", '\u00A2'); + _entities.Add ("pound", '\u00A3'); + _entities.Add ("curren", '\u00A4'); + _entities.Add ("yen", '\u00A5'); + _entities.Add ("brvbar", '\u00A6'); + _entities.Add ("sect", '\u00A7'); + _entities.Add ("uml", '\u00A8'); + _entities.Add ("copy", '\u00A9'); + _entities.Add ("ordf", '\u00AA'); + _entities.Add ("laquo", '\u00AB'); + _entities.Add ("not", '\u00AC'); + _entities.Add ("shy", '\u00AD'); + _entities.Add ("reg", '\u00AE'); + _entities.Add ("macr", '\u00AF'); + _entities.Add ("deg", '\u00B0'); + _entities.Add ("plusmn", '\u00B1'); + _entities.Add ("sup2", '\u00B2'); + _entities.Add ("sup3", '\u00B3'); + _entities.Add ("acute", '\u00B4'); + _entities.Add ("micro", '\u00B5'); + _entities.Add ("para", '\u00B6'); + _entities.Add ("middot", '\u00B7'); + _entities.Add ("cedil", '\u00B8'); + _entities.Add ("sup1", '\u00B9'); + _entities.Add ("ordm", '\u00BA'); + _entities.Add ("raquo", '\u00BB'); + _entities.Add ("frac14", '\u00BC'); + _entities.Add ("frac12", '\u00BD'); + _entities.Add ("frac34", '\u00BE'); + _entities.Add ("iquest", '\u00BF'); + _entities.Add ("Agrave", '\u00C0'); + _entities.Add ("Aacute", '\u00C1'); + _entities.Add ("Acirc", '\u00C2'); + _entities.Add ("Atilde", '\u00C3'); + _entities.Add ("Auml", '\u00C4'); + _entities.Add ("Aring", '\u00C5'); + _entities.Add ("AElig", '\u00C6'); + _entities.Add ("Ccedil", '\u00C7'); + _entities.Add ("Egrave", '\u00C8'); + _entities.Add ("Eacute", '\u00C9'); + _entities.Add ("Ecirc", '\u00CA'); + _entities.Add ("Euml", '\u00CB'); + _entities.Add ("Igrave", '\u00CC'); + _entities.Add ("Iacute", '\u00CD'); + _entities.Add ("Icirc", '\u00CE'); + _entities.Add ("Iuml", '\u00CF'); + _entities.Add ("ETH", '\u00D0'); + _entities.Add ("Ntilde", '\u00D1'); + _entities.Add ("Ograve", '\u00D2'); + _entities.Add ("Oacute", '\u00D3'); + _entities.Add ("Ocirc", '\u00D4'); + _entities.Add ("Otilde", '\u00D5'); + _entities.Add ("Ouml", '\u00D6'); + _entities.Add ("times", '\u00D7'); + _entities.Add ("Oslash", '\u00D8'); + _entities.Add ("Ugrave", '\u00D9'); + _entities.Add ("Uacute", '\u00DA'); + _entities.Add ("Ucirc", '\u00DB'); + _entities.Add ("Uuml", '\u00DC'); + _entities.Add ("Yacute", '\u00DD'); + _entities.Add ("THORN", '\u00DE'); + _entities.Add ("szlig", '\u00DF'); + _entities.Add ("agrave", '\u00E0'); + _entities.Add ("aacute", '\u00E1'); + _entities.Add ("acirc", '\u00E2'); + _entities.Add ("atilde", '\u00E3'); + _entities.Add ("auml", '\u00E4'); + _entities.Add ("aring", '\u00E5'); + _entities.Add ("aelig", '\u00E6'); + _entities.Add ("ccedil", '\u00E7'); + _entities.Add ("egrave", '\u00E8'); + _entities.Add ("eacute", '\u00E9'); + _entities.Add ("ecirc", '\u00EA'); + _entities.Add ("euml", '\u00EB'); + _entities.Add ("igrave", '\u00EC'); + _entities.Add ("iacute", '\u00ED'); + _entities.Add ("icirc", '\u00EE'); + _entities.Add ("iuml", '\u00EF'); + _entities.Add ("eth", '\u00F0'); + _entities.Add ("ntilde", '\u00F1'); + _entities.Add ("ograve", '\u00F2'); + _entities.Add ("oacute", '\u00F3'); + _entities.Add ("ocirc", '\u00F4'); + _entities.Add ("otilde", '\u00F5'); + _entities.Add ("ouml", '\u00F6'); + _entities.Add ("divide", '\u00F7'); + _entities.Add ("oslash", '\u00F8'); + _entities.Add ("ugrave", '\u00F9'); + _entities.Add ("uacute", '\u00FA'); + _entities.Add ("ucirc", '\u00FB'); + _entities.Add ("uuml", '\u00FC'); + _entities.Add ("yacute", '\u00FD'); + _entities.Add ("thorn", '\u00FE'); + _entities.Add ("yuml", '\u00FF'); + _entities.Add ("fnof", '\u0192'); + _entities.Add ("Alpha", '\u0391'); + _entities.Add ("Beta", '\u0392'); + _entities.Add ("Gamma", '\u0393'); + _entities.Add ("Delta", '\u0394'); + _entities.Add ("Epsilon", '\u0395'); + _entities.Add ("Zeta", '\u0396'); + _entities.Add ("Eta", '\u0397'); + _entities.Add ("Theta", '\u0398'); + _entities.Add ("Iota", '\u0399'); + _entities.Add ("Kappa", '\u039A'); + _entities.Add ("Lambda", '\u039B'); + _entities.Add ("Mu", '\u039C'); + _entities.Add ("Nu", '\u039D'); + _entities.Add ("Xi", '\u039E'); + _entities.Add ("Omicron", '\u039F'); + _entities.Add ("Pi", '\u03A0'); + _entities.Add ("Rho", '\u03A1'); + _entities.Add ("Sigma", '\u03A3'); + _entities.Add ("Tau", '\u03A4'); + _entities.Add ("Upsilon", '\u03A5'); + _entities.Add ("Phi", '\u03A6'); + _entities.Add ("Chi", '\u03A7'); + _entities.Add ("Psi", '\u03A8'); + _entities.Add ("Omega", '\u03A9'); + _entities.Add ("alpha", '\u03B1'); + _entities.Add ("beta", '\u03B2'); + _entities.Add ("gamma", '\u03B3'); + _entities.Add ("delta", '\u03B4'); + _entities.Add ("epsilon", '\u03B5'); + _entities.Add ("zeta", '\u03B6'); + _entities.Add ("eta", '\u03B7'); + _entities.Add ("theta", '\u03B8'); + _entities.Add ("iota", '\u03B9'); + _entities.Add ("kappa", '\u03BA'); + _entities.Add ("lambda", '\u03BB'); + _entities.Add ("mu", '\u03BC'); + _entities.Add ("nu", '\u03BD'); + _entities.Add ("xi", '\u03BE'); + _entities.Add ("omicron", '\u03BF'); + _entities.Add ("pi", '\u03C0'); + _entities.Add ("rho", '\u03C1'); + _entities.Add ("sigmaf", '\u03C2'); + _entities.Add ("sigma", '\u03C3'); + _entities.Add ("tau", '\u03C4'); + _entities.Add ("upsilon", '\u03C5'); + _entities.Add ("phi", '\u03C6'); + _entities.Add ("chi", '\u03C7'); + _entities.Add ("psi", '\u03C8'); + _entities.Add ("omega", '\u03C9'); + _entities.Add ("thetasym", '\u03D1'); + _entities.Add ("upsih", '\u03D2'); + _entities.Add ("piv", '\u03D6'); + _entities.Add ("bull", '\u2022'); + _entities.Add ("hellip", '\u2026'); + _entities.Add ("prime", '\u2032'); + _entities.Add ("Prime", '\u2033'); + _entities.Add ("oline", '\u203E'); + _entities.Add ("frasl", '\u2044'); + _entities.Add ("weierp", '\u2118'); + _entities.Add ("image", '\u2111'); + _entities.Add ("real", '\u211C'); + _entities.Add ("trade", '\u2122'); + _entities.Add ("alefsym", '\u2135'); + _entities.Add ("larr", '\u2190'); + _entities.Add ("uarr", '\u2191'); + _entities.Add ("rarr", '\u2192'); + _entities.Add ("darr", '\u2193'); + _entities.Add ("harr", '\u2194'); + _entities.Add ("crarr", '\u21B5'); + _entities.Add ("lArr", '\u21D0'); + _entities.Add ("uArr", '\u21D1'); + _entities.Add ("rArr", '\u21D2'); + _entities.Add ("dArr", '\u21D3'); + _entities.Add ("hArr", '\u21D4'); + _entities.Add ("forall", '\u2200'); + _entities.Add ("part", '\u2202'); + _entities.Add ("exist", '\u2203'); + _entities.Add ("empty", '\u2205'); + _entities.Add ("nabla", '\u2207'); + _entities.Add ("isin", '\u2208'); + _entities.Add ("notin", '\u2209'); + _entities.Add ("ni", '\u220B'); + _entities.Add ("prod", '\u220F'); + _entities.Add ("sum", '\u2211'); + _entities.Add ("minus", '\u2212'); + _entities.Add ("lowast", '\u2217'); + _entities.Add ("radic", '\u221A'); + _entities.Add ("prop", '\u221D'); + _entities.Add ("infin", '\u221E'); + _entities.Add ("ang", '\u2220'); + _entities.Add ("and", '\u2227'); + _entities.Add ("or", '\u2228'); + _entities.Add ("cap", '\u2229'); + _entities.Add ("cup", '\u222A'); + _entities.Add ("int", '\u222B'); + _entities.Add ("there4", '\u2234'); + _entities.Add ("sim", '\u223C'); + _entities.Add ("cong", '\u2245'); + _entities.Add ("asymp", '\u2248'); + _entities.Add ("ne", '\u2260'); + _entities.Add ("equiv", '\u2261'); + _entities.Add ("le", '\u2264'); + _entities.Add ("ge", '\u2265'); + _entities.Add ("sub", '\u2282'); + _entities.Add ("sup", '\u2283'); + _entities.Add ("nsub", '\u2284'); + _entities.Add ("sube", '\u2286'); + _entities.Add ("supe", '\u2287'); + _entities.Add ("oplus", '\u2295'); + _entities.Add ("otimes", '\u2297'); + _entities.Add ("perp", '\u22A5'); + _entities.Add ("sdot", '\u22C5'); + _entities.Add ("lceil", '\u2308'); + _entities.Add ("rceil", '\u2309'); + _entities.Add ("lfloor", '\u230A'); + _entities.Add ("rfloor", '\u230B'); + _entities.Add ("lang", '\u2329'); + _entities.Add ("rang", '\u232A'); + _entities.Add ("loz", '\u25CA'); + _entities.Add ("spades", '\u2660'); + _entities.Add ("clubs", '\u2663'); + _entities.Add ("hearts", '\u2665'); + _entities.Add ("diams", '\u2666'); + _entities.Add ("quot", '\u0022'); + _entities.Add ("amp", '\u0026'); + _entities.Add ("lt", '\u003C'); + _entities.Add ("gt", '\u003E'); + _entities.Add ("OElig", '\u0152'); + _entities.Add ("oelig", '\u0153'); + _entities.Add ("Scaron", '\u0160'); + _entities.Add ("scaron", '\u0161'); + _entities.Add ("Yuml", '\u0178'); + _entities.Add ("circ", '\u02C6'); + _entities.Add ("tilde", '\u02DC'); + _entities.Add ("ensp", '\u2002'); + _entities.Add ("emsp", '\u2003'); + _entities.Add ("thinsp", '\u2009'); + _entities.Add ("zwnj", '\u200C'); + _entities.Add ("zwj", '\u200D'); + _entities.Add ("lrm", '\u200E'); + _entities.Add ("rlm", '\u200F'); + _entities.Add ("ndash", '\u2013'); + _entities.Add ("mdash", '\u2014'); + _entities.Add ("lsquo", '\u2018'); + _entities.Add ("rsquo", '\u2019'); + _entities.Add ("sbquo", '\u201A'); + _entities.Add ("ldquo", '\u201C'); + _entities.Add ("rdquo", '\u201D'); + _entities.Add ("bdquo", '\u201E'); + _entities.Add ("dagger", '\u2020'); + _entities.Add ("Dagger", '\u2021'); + _entities.Add ("permil", '\u2030'); + _entities.Add ("lsaquo", '\u2039'); + _entities.Add ("rsaquo", '\u203A'); + _entities.Add ("euro", '\u20AC'); + } + + private static bool isAlphabet (char c) + { + return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z'); + } + + private static bool isNumeric (char c) + { + return c >= '0' && c <= '9'; + } + + private static bool isUnreserved (char c) + { + return c == '*' + || c == '-' + || c == '.' + || c == '_'; + } + + private static bool isUnreservedInRfc2396 (char c) + { + return c == '!' + || c == '\'' + || c == '(' + || c == ')' + || c == '*' + || c == '-' + || c == '.' + || c == '_' + || c == '~'; + } + + private static bool isUnreservedInRfc3986 (char c) + { + return c == '-' + || c == '.' + || c == '_' + || c == '~'; + } + + private static byte[] urlDecodeToBytes (byte[] bytes, int offset, int count) + { + using (var buff = new MemoryStream ()) { + var end = offset + count - 1; + + for (var i = offset; i <= end; i++) { + var b = bytes[i]; + var c = (char) b; + + if (c == '%') { + if (i > end - 2) + break; + + var num = getNumber (bytes, i + 1, 2); + + if (num == -1) + break; + + buff.WriteByte ((byte) num); + + i += 2; + + continue; + } + + if (c == '+') { + buff.WriteByte ((byte) ' '); + + continue; + } + + buff.WriteByte (b); + } + + buff.Close (); + + return buff.ToArray (); + } + } + + private static void urlEncode (byte b, Stream output) + { + if (b > 31 && b < 127) { + var c = (char) b; + + if (c == ' ') { + output.WriteByte ((byte) '+'); + + return; + } + + if (isNumeric (c)) { + output.WriteByte (b); + + return; + } + + if (isAlphabet (c)) { + output.WriteByte (b); + + return; + } + + if (isUnreserved (c)) { + output.WriteByte (b); + + return; + } + } + + var i = (int) b; + var bytes = new byte[] { + (byte) '%', + (byte) _hexChars[i >> 4], + (byte) _hexChars[i & 0x0F] + }; + + output.Write (bytes, 0, 3); + } + + private static byte[] urlEncodeToBytes (byte[] bytes, int offset, int count) + { + using (var buff = new MemoryStream ()) { + var end = offset + count - 1; + + for (var i = offset; i <= end; i++) + urlEncode (bytes[i], buff); + + buff.Close (); + + return buff.ToArray (); + } + } + + #endregion + + #region Internal Methods + + internal static Uri CreateRequestUrl ( + string requestUri, + string host, + bool websocketRequest, + bool secure + ) + { + if (requestUri == null || requestUri.Length == 0) + return null; + + if (host == null || host.Length == 0) + return null; + + string schm = null; + string path = null; + + if (requestUri.IndexOf ('/') == 0) { + path = requestUri; + } + else if (requestUri.MaybeUri ()) { + Uri uri; + + if (!Uri.TryCreate (requestUri, UriKind.Absolute, out uri)) + return null; + + schm = uri.Scheme; + var valid = websocketRequest + ? schm == "ws" || schm == "wss" + : schm == "http" || schm == "https"; + + if (!valid) + return null; + + host = uri.Authority; + path = uri.PathAndQuery; + } + else if (requestUri == "*") { + } + else { + // As the authority form. + + host = requestUri; + } + + if (schm == null) { + schm = websocketRequest + ? (secure ? "wss" : "ws") + : (secure ? "https" : "http"); + } + + if (host.IndexOf (':') == -1) + host = String.Format ("{0}:{1}", host, secure ? 443 : 80); + + var url = String.Format ("{0}://{1}{2}", schm, host, path); + Uri ret; + + return Uri.TryCreate (url, UriKind.Absolute, out ret) ? ret : null; + } + + internal static IPrincipal CreateUser ( + string response, + AuthenticationSchemes scheme, + string realm, + string method, + Func credentialsFinder + ) + { + if (response == null || response.Length == 0) + return null; + + if (scheme == AuthenticationSchemes.Digest) { + if (realm == null || realm.Length == 0) + return null; + + if (method == null || method.Length == 0) + return null; + } + else { + if (scheme != AuthenticationSchemes.Basic) + return null; + } + + if (credentialsFinder == null) + return null; + + var compType = StringComparison.OrdinalIgnoreCase; + + if (!response.StartsWith (scheme.ToString (), compType)) + return null; + + var res = AuthenticationResponse.Parse (response); + + if (res == null) + return null; + + var id = res.ToIdentity (); + + if (id == null) + return null; + + NetworkCredential cred = null; + + try { + cred = credentialsFinder (id); + } + catch { + } + + if (cred == null) + return null; + + if (scheme == AuthenticationSchemes.Basic) { + var basicId = (HttpBasicIdentity) id; + + return basicId.Password == cred.Password + ? new GenericPrincipal (id, cred.Roles) + : null; + } + + var digestId = (HttpDigestIdentity) id; + + return digestId.IsValid (cred.Password, realm, method, null) + ? new GenericPrincipal (id, cred.Roles) + : null; + } + + internal static Encoding GetEncoding (string contentType) + { + var name = "charset="; + var compType = StringComparison.OrdinalIgnoreCase; + + foreach (var elm in contentType.SplitHeaderValue (';')) { + var part = elm.Trim (); + + if (!part.StartsWith (name, compType)) + continue; + + var val = part.GetValue ('=', true); + + if (val == null || val.Length == 0) + return null; + + return Encoding.GetEncoding (val); + } + + return null; + } + + internal static bool TryGetEncoding ( + string contentType, + out Encoding result + ) + { + result = null; + + try { + result = GetEncoding (contentType); + } + catch { + return false; + } + + return result != null; + } + + #endregion + + #region Public Methods + + public static string HtmlAttributeEncode (string s) + { + if (s == null) + throw new ArgumentNullException ("s"); + + return s.Length > 0 ? htmlEncode (s, true) : s; + } + + public static void HtmlAttributeEncode (string s, TextWriter output) + { + if (s == null) + throw new ArgumentNullException ("s"); + + if (output == null) + throw new ArgumentNullException ("output"); + + if (s.Length == 0) + return; + + var encodedS = htmlEncode (s, true); + + output.Write (encodedS); + } + + public static string HtmlDecode (string s) + { + if (s == null) + throw new ArgumentNullException ("s"); + + return s.Length > 0 ? htmlDecode (s) : s; + } + + public static void HtmlDecode (string s, TextWriter output) + { + if (s == null) + throw new ArgumentNullException ("s"); + + if (output == null) + throw new ArgumentNullException ("output"); + + if (s.Length == 0) + return; + + var decodedS = htmlDecode (s); + + output.Write (decodedS); + } + + public static string HtmlEncode (string s) + { + if (s == null) + throw new ArgumentNullException ("s"); + + return s.Length > 0 ? htmlEncode (s, false) : s; + } + + public static void HtmlEncode (string s, TextWriter output) + { + if (s == null) + throw new ArgumentNullException ("s"); + + if (output == null) + throw new ArgumentNullException ("output"); + + if (s.Length == 0) + return; + + var encodedS = htmlEncode (s, false); + + output.Write (encodedS); + } + + public static string UrlDecode (string s) + { + return UrlDecode (s, Encoding.UTF8); + } + + public static string UrlDecode (byte[] bytes, Encoding encoding) + { + if (bytes == null) + throw new ArgumentNullException ("bytes"); + + var len = bytes.Length; + + if (len == 0) + return String.Empty; + + var decodedBytes = urlDecodeToBytes (bytes, 0, len); + + return (encoding ?? Encoding.UTF8).GetString (decodedBytes); + } + + public static string UrlDecode (string s, Encoding encoding) + { + if (s == null) + throw new ArgumentNullException ("s"); + + if (s.Length == 0) + return s; + + var bytes = Encoding.ASCII.GetBytes (s); + var decodedBytes = urlDecodeToBytes (bytes, 0, bytes.Length); + + return (encoding ?? Encoding.UTF8).GetString (decodedBytes); + } + + public static string UrlDecode ( + byte[] bytes, + int offset, + int count, + Encoding encoding + ) + { + if (bytes == null) + throw new ArgumentNullException ("bytes"); + + var len = bytes.Length; + + if (len == 0) { + if (offset != 0) + throw new ArgumentOutOfRangeException ("offset"); + + if (count != 0) + throw new ArgumentOutOfRangeException ("count"); + + return String.Empty; + } + + if (offset < 0 || offset >= len) + throw new ArgumentOutOfRangeException ("offset"); + + if (count < 0 || count > len - offset) + throw new ArgumentOutOfRangeException ("count"); + + if (count == 0) + return String.Empty; + + var decodedBytes = urlDecodeToBytes (bytes, offset, count); + + return (encoding ?? Encoding.UTF8).GetString (decodedBytes); + } + + public static byte[] UrlDecodeToBytes (byte[] bytes) + { + if (bytes == null) + throw new ArgumentNullException ("bytes"); + + var len = bytes.Length; + + return len > 0 ? urlDecodeToBytes (bytes, 0, len) : bytes; + } + + public static byte[] UrlDecodeToBytes (string s) + { + if (s == null) + throw new ArgumentNullException ("s"); + + if (s.Length == 0) + return new byte[0]; + + var bytes = Encoding.ASCII.GetBytes (s); + + return urlDecodeToBytes (bytes, 0, bytes.Length); + } + + public static byte[] UrlDecodeToBytes (byte[] bytes, int offset, int count) + { + if (bytes == null) + throw new ArgumentNullException ("bytes"); + + var len = bytes.Length; + + if (len == 0) { + if (offset != 0) + throw new ArgumentOutOfRangeException ("offset"); + + if (count != 0) + throw new ArgumentOutOfRangeException ("count"); + + return bytes; + } + + if (offset < 0 || offset >= len) + throw new ArgumentOutOfRangeException ("offset"); + + if (count < 0 || count > len - offset) + throw new ArgumentOutOfRangeException ("count"); + + return count > 0 ? urlDecodeToBytes (bytes, offset, count) : new byte[0]; + } + + public static string UrlEncode (byte[] bytes) + { + if (bytes == null) + throw new ArgumentNullException ("bytes"); + + var len = bytes.Length; + + if (len == 0) + return String.Empty; + + var encodedBytes = urlEncodeToBytes (bytes, 0, len); + + return Encoding.ASCII.GetString (encodedBytes); + } + + public static string UrlEncode (string s) + { + return UrlEncode (s, Encoding.UTF8); + } + + public static string UrlEncode (string s, Encoding encoding) + { + if (s == null) + throw new ArgumentNullException ("s"); + + var len = s.Length; + + if (len == 0) + return s; + + if (encoding == null) + encoding = Encoding.UTF8; + + var maxCnt = encoding.GetMaxByteCount (len); + var bytes = new byte[maxCnt]; + var cnt = encoding.GetBytes (s, 0, len, bytes, 0); + var encodedBytes = urlEncodeToBytes (bytes, 0, cnt); + + return Encoding.ASCII.GetString (encodedBytes); + } + + public static string UrlEncode (byte[] bytes, int offset, int count) + { + if (bytes == null) + throw new ArgumentNullException ("bytes"); + + var len = bytes.Length; + + if (len == 0) { + if (offset != 0) + throw new ArgumentOutOfRangeException ("offset"); + + if (count != 0) + throw new ArgumentOutOfRangeException ("count"); + + return String.Empty; + } + + if (offset < 0 || offset >= len) + throw new ArgumentOutOfRangeException ("offset"); + + if (count < 0 || count > len - offset) + throw new ArgumentOutOfRangeException ("count"); + + if (count == 0) + return String.Empty; + + var encodedBytes = urlEncodeToBytes (bytes, offset, count); + + return Encoding.ASCII.GetString (encodedBytes); + } + + public static byte[] UrlEncodeToBytes (byte[] bytes) + { + if (bytes == null) + throw new ArgumentNullException ("bytes"); + + var len = bytes.Length; + + return len > 0 ? urlEncodeToBytes (bytes, 0, len) : bytes; + } + + public static byte[] UrlEncodeToBytes (string s) + { + return UrlEncodeToBytes (s, Encoding.UTF8); + } + + public static byte[] UrlEncodeToBytes (string s, Encoding encoding) + { + if (s == null) + throw new ArgumentNullException ("s"); + + if (s.Length == 0) + return new byte[0]; + + var bytes = (encoding ?? Encoding.UTF8).GetBytes (s); + + return urlEncodeToBytes (bytes, 0, bytes.Length); + } + + public static byte[] UrlEncodeToBytes (byte[] bytes, int offset, int count) + { + if (bytes == null) + throw new ArgumentNullException ("bytes"); + + var len = bytes.Length; + + if (len == 0) { + if (offset != 0) + throw new ArgumentOutOfRangeException ("offset"); + + if (count != 0) + throw new ArgumentOutOfRangeException ("count"); + + return bytes; + } + + if (offset < 0 || offset >= len) + throw new ArgumentOutOfRangeException ("offset"); + + if (count < 0 || count > len - offset) + throw new ArgumentOutOfRangeException ("count"); + + return count > 0 ? urlEncodeToBytes (bytes, offset, count) : new byte[0]; + } + + #endregion + } +} diff --git a/Assets/External/websocket-sharp/Net/HttpUtility.cs.meta b/Assets/External/websocket-sharp/Net/HttpUtility.cs.meta new file mode 100644 index 00000000..c690fa0b --- /dev/null +++ b/Assets/External/websocket-sharp/Net/HttpUtility.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: f570a1781ffb6184ea93696443806d33 \ No newline at end of file diff --git a/Assets/External/websocket-sharp/Net/HttpVersion.cs b/Assets/External/websocket-sharp/Net/HttpVersion.cs new file mode 100644 index 00000000..95f8f0a3 --- /dev/null +++ b/Assets/External/websocket-sharp/Net/HttpVersion.cs @@ -0,0 +1,73 @@ +#region License +/* + * HttpVersion.cs + * + * This code is derived from HttpVersion.cs (System.Net) of Mono + * (http://www.mono-project.com). + * + * The MIT License + * + * Copyright (c) 2012-2024 sta.blockhead + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#endregion + +#region Authors +/* + * Authors: + * - Lawrence Pit + */ +#endregion + +using System; + +namespace WebSocketSharp.Net +{ + /// + /// Provides the HTTP version numbers. + /// + public class HttpVersion + { + #region Public Fields + + /// + /// Provides a instance for the HTTP/1.0. + /// + public static readonly Version Version10 = new Version (1, 0); + + /// + /// Provides a instance for the HTTP/1.1. + /// + public static readonly Version Version11 = new Version (1, 1); + + #endregion + + #region Public Constructors + + /// + /// Initializes a new instance of the class. + /// + public HttpVersion () + { + } + + #endregion + } +} diff --git a/Assets/External/websocket-sharp/Net/HttpVersion.cs.meta b/Assets/External/websocket-sharp/Net/HttpVersion.cs.meta new file mode 100644 index 00000000..b45dcb23 --- /dev/null +++ b/Assets/External/websocket-sharp/Net/HttpVersion.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 5b7b5bd46350c3642905967564402d63 \ No newline at end of file diff --git a/Assets/External/websocket-sharp/Net/InputChunkState.cs b/Assets/External/websocket-sharp/Net/InputChunkState.cs new file mode 100644 index 00000000..f50ad6b7 --- /dev/null +++ b/Assets/External/websocket-sharp/Net/InputChunkState.cs @@ -0,0 +1,52 @@ +#region License +/* + * InputChunkState.cs + * + * This code is derived from ChunkStream.cs (System.Net) of Mono + * (http://www.mono-project.com). + * + * The MIT License + * + * Copyright (c) 2003 Ximian, Inc (http://www.ximian.com) + * Copyright (c) 2014-2015 sta.blockhead + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#endregion + +#region Authors +/* + * Authors: + * - Gonzalo Paniagua Javier + */ +#endregion + +using System; + +namespace WebSocketSharp.Net +{ + internal enum InputChunkState + { + None, + Data, + DataEnded, + Trailer, + End + } +} diff --git a/Assets/External/websocket-sharp/Net/InputChunkState.cs.meta b/Assets/External/websocket-sharp/Net/InputChunkState.cs.meta new file mode 100644 index 00000000..0bda6fbe --- /dev/null +++ b/Assets/External/websocket-sharp/Net/InputChunkState.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 823ae53558ee9e746a94dbdb388f4c17 \ No newline at end of file diff --git a/Assets/External/websocket-sharp/Net/InputState.cs b/Assets/External/websocket-sharp/Net/InputState.cs new file mode 100644 index 00000000..9f566d24 --- /dev/null +++ b/Assets/External/websocket-sharp/Net/InputState.cs @@ -0,0 +1,49 @@ +#region License +/* + * InputState.cs + * + * This code is derived from HttpConnection.cs (System.Net) of Mono + * (http://www.mono-project.com). + * + * The MIT License + * + * Copyright (c) 2005 Novell, Inc. (http://www.novell.com) + * Copyright (c) 2014-2015 sta.blockhead + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#endregion + +#region Authors +/* + * Authors: + * - Gonzalo Paniagua Javier + */ +#endregion + +using System; + +namespace WebSocketSharp.Net +{ + internal enum InputState + { + RequestLine, + Headers + } +} diff --git a/Assets/External/websocket-sharp/Net/InputState.cs.meta b/Assets/External/websocket-sharp/Net/InputState.cs.meta new file mode 100644 index 00000000..0da85971 --- /dev/null +++ b/Assets/External/websocket-sharp/Net/InputState.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: c4bf22ee9c7cd2a44b73f930437c3a36 \ No newline at end of file diff --git a/Assets/External/websocket-sharp/Net/LineState.cs b/Assets/External/websocket-sharp/Net/LineState.cs new file mode 100644 index 00000000..84e271a7 --- /dev/null +++ b/Assets/External/websocket-sharp/Net/LineState.cs @@ -0,0 +1,50 @@ +#region License +/* + * LineState.cs + * + * This code is derived from HttpConnection.cs (System.Net) of Mono + * (http://www.mono-project.com). + * + * The MIT License + * + * Copyright (c) 2005 Novell, Inc. (http://www.novell.com) + * Copyright (c) 2014-2015 sta.blockhead + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#endregion + +#region Authors +/* + * Authors: + * - Gonzalo Paniagua Javier + */ +#endregion + +using System; + +namespace WebSocketSharp.Net +{ + internal enum LineState + { + None, + Cr, + Lf + } +} diff --git a/Assets/External/websocket-sharp/Net/LineState.cs.meta b/Assets/External/websocket-sharp/Net/LineState.cs.meta new file mode 100644 index 00000000..3d8c5098 --- /dev/null +++ b/Assets/External/websocket-sharp/Net/LineState.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 343792b09aa8edc4294cd342299555b9 \ No newline at end of file diff --git a/Assets/External/websocket-sharp/Net/NetworkCredential.cs b/Assets/External/websocket-sharp/Net/NetworkCredential.cs new file mode 100644 index 00000000..f97df971 --- /dev/null +++ b/Assets/External/websocket-sharp/Net/NetworkCredential.cs @@ -0,0 +1,217 @@ +#region License +/* + * NetworkCredential.cs + * + * The MIT License + * + * Copyright (c) 2014-2024 sta.blockhead + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#endregion + +using System; + +namespace WebSocketSharp.Net +{ + /// + /// Provides the credentials for the password-based authentication. + /// + public class NetworkCredential + { + #region Private Fields + + private string _domain; + private static readonly string[] _noRoles; + private string _password; + private string[] _roles; + private string _username; + + #endregion + + #region Static Constructor + + static NetworkCredential () + { + _noRoles = new string[0]; + } + + #endregion + + #region Public Constructors + + /// + /// Initializes a new instance of the class + /// with the specified username and password. + /// + /// + /// A that specifies the username associated with + /// the credentials. + /// + /// + /// A that specifies the password for the username + /// associated with the credentials. + /// + /// + /// is an empty string. + /// + /// + /// is . + /// + public NetworkCredential (string username, string password) + : this (username, password, null, null) + { + } + + /// + /// Initializes a new instance of the class + /// with the specified username, password, domain and roles. + /// + /// + /// A that specifies the username associated with + /// the credentials. + /// + /// + /// A that specifies the password for the username + /// associated with the credentials. + /// + /// + /// A that specifies the domain associated with + /// the credentials. + /// + /// + /// An array of that specifies the roles associated + /// with the credentials if any. + /// + /// + /// is an empty string. + /// + /// + /// is . + /// + public NetworkCredential ( + string username, + string password, + string domain, + params string[] roles + ) + { + if (username == null) + throw new ArgumentNullException ("username"); + + if (username.Length == 0) + throw new ArgumentException ("An empty string.", "username"); + + _username = username; + _password = password; + _domain = domain; + _roles = roles; + } + + #endregion + + #region Public Properties + + /// + /// Gets the domain associated with the credentials. + /// + /// + /// + /// A that represents the domain name + /// to which the username belongs. + /// + /// + /// An empty string if the value was initialized with + /// . + /// + /// + public string Domain { + get { + return _domain ?? String.Empty; + } + + internal set { + _domain = value; + } + } + + /// + /// Gets the password for the username associated with the credentials. + /// + /// + /// + /// A that represents the password. + /// + /// + /// An empty string if the value was initialized with + /// . + /// + /// + public string Password { + get { + return _password ?? String.Empty; + } + + internal set { + _password = value; + } + } + + /// + /// Gets the roles associated with the credentials. + /// + /// + /// + /// An array of that represents the role names + /// to which the username belongs. + /// + /// + /// An empty array if the value was initialized with + /// . + /// + /// + public string[] Roles { + get { + return _roles ?? _noRoles; + } + + internal set { + _roles = value; + } + } + + /// + /// Gets the username associated with the credentials. + /// + /// + /// A that represents the username. + /// + public string Username { + get { + return _username; + } + + internal set { + _username = value; + } + } + + #endregion + } +} diff --git a/Assets/External/websocket-sharp/Net/NetworkCredential.cs.meta b/Assets/External/websocket-sharp/Net/NetworkCredential.cs.meta new file mode 100644 index 00000000..a9fd59a5 --- /dev/null +++ b/Assets/External/websocket-sharp/Net/NetworkCredential.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: ae1432d0841e559499a32e91db01a3e6 \ No newline at end of file diff --git a/Assets/External/websocket-sharp/Net/QueryStringCollection.cs b/Assets/External/websocket-sharp/Net/QueryStringCollection.cs new file mode 100644 index 00000000..d5b7a8d9 --- /dev/null +++ b/Assets/External/websocket-sharp/Net/QueryStringCollection.cs @@ -0,0 +1,144 @@ +#region License +/* + * QueryStringCollection.cs + * + * This code is derived from HttpUtility.cs (System.Net) of Mono + * (http://www.mono-project.com). + * + * The MIT License + * + * Copyright (c) 2005-2009 Novell, Inc. (http://www.novell.com) + * Copyright (c) 2018-2023 sta.blockhead + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#endregion + +#region Authors +/* + * Authors: + * - Patrik Torstensson + * - Wictor Wilén (decode/encode functions) + * - Tim Coleman + * - Gonzalo Paniagua Javier + */ +#endregion + +using System; +using System.Collections.Specialized; +using System.Text; + +namespace WebSocketSharp.Net +{ + internal sealed class QueryStringCollection : NameValueCollection + { + #region Public Constructors + + public QueryStringCollection () + { + } + + public QueryStringCollection (int capacity) + : base (capacity) + { + } + + #endregion + + #region Public Methods + + public static QueryStringCollection Parse (string query) + { + return Parse (query, Encoding.UTF8); + } + + public static QueryStringCollection Parse (string query, Encoding encoding) + { + if (query == null) + return new QueryStringCollection (1); + + if (query.Length == 0) + return new QueryStringCollection (1); + + if (query == "?") + return new QueryStringCollection (1); + + if (query[0] == '?') + query = query.Substring (1); + + if (encoding == null) + encoding = Encoding.UTF8; + + var ret = new QueryStringCollection (); + + foreach (var component in query.Split ('&')) { + var len = component.Length; + + if (len == 0) + continue; + + if (component == "=") + continue; + + string name = null; + string val = null; + + var idx = component.IndexOf ('='); + + if (idx < 0) { + val = component.UrlDecode (encoding); + } + else if (idx == 0) { + val = component.Substring (1).UrlDecode (encoding); + } + else { + name = component.Substring (0, idx).UrlDecode (encoding); + + var start = idx + 1; + val = start < len + ? component.Substring (start).UrlDecode (encoding) + : String.Empty; + } + + ret.Add (name, val); + } + + return ret; + } + + public override string ToString () + { + if (Count == 0) + return String.Empty; + + var buff = new StringBuilder (); + + var fmt = "{0}={1}&"; + + foreach (var key in AllKeys) + buff.AppendFormat (fmt, key, this[key]); + + buff.Length--; + + return buff.ToString (); + } + + #endregion + } +} diff --git a/Assets/External/websocket-sharp/Net/QueryStringCollection.cs.meta b/Assets/External/websocket-sharp/Net/QueryStringCollection.cs.meta new file mode 100644 index 00000000..916eff21 --- /dev/null +++ b/Assets/External/websocket-sharp/Net/QueryStringCollection.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 9f634e4aee6608d4fbe4b35758b4a3ce \ No newline at end of file diff --git a/Assets/External/websocket-sharp/Net/ReadBufferState.cs b/Assets/External/websocket-sharp/Net/ReadBufferState.cs new file mode 100644 index 00000000..bf0de884 --- /dev/null +++ b/Assets/External/websocket-sharp/Net/ReadBufferState.cs @@ -0,0 +1,129 @@ +#region License +/* + * ReadBufferState.cs + * + * This code is derived from ChunkedInputStream.cs (System.Net) of Mono + * (http://www.mono-project.com). + * + * The MIT License + * + * Copyright (c) 2005 Novell, Inc. (http://www.novell.com) + * Copyright (c) 2014-2023 sta.blockhead + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#endregion + +#region Authors +/* + * Authors: + * - Gonzalo Paniagua Javier + */ +#endregion + +using System; + +namespace WebSocketSharp.Net +{ + internal class ReadBufferState + { + #region Private Fields + + private HttpStreamAsyncResult _asyncResult; + private byte[] _buffer; + private int _count; + private int _initialCount; + private int _offset; + + #endregion + + #region Public Constructors + + public ReadBufferState ( + byte[] buffer, + int offset, + int count, + HttpStreamAsyncResult asyncResult + ) + { + _buffer = buffer; + _offset = offset; + _count = count; + _asyncResult = asyncResult; + + _initialCount = count; + } + + #endregion + + #region Public Properties + + public HttpStreamAsyncResult AsyncResult { + get { + return _asyncResult; + } + + set { + _asyncResult = value; + } + } + + public byte[] Buffer { + get { + return _buffer; + } + + set { + _buffer = value; + } + } + + public int Count { + get { + return _count; + } + + set { + _count = value; + } + } + + public int InitialCount { + get { + return _initialCount; + } + + set { + _initialCount = value; + } + } + + public int Offset { + get { + return _offset; + } + + set { + _offset = value; + } + } + + #endregion + } +} diff --git a/Assets/External/websocket-sharp/Net/ReadBufferState.cs.meta b/Assets/External/websocket-sharp/Net/ReadBufferState.cs.meta new file mode 100644 index 00000000..37ad7bc2 --- /dev/null +++ b/Assets/External/websocket-sharp/Net/ReadBufferState.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 548596b3ae6566547ac9012e7121188b \ No newline at end of file diff --git a/Assets/External/websocket-sharp/Net/RequestStream.cs b/Assets/External/websocket-sharp/Net/RequestStream.cs new file mode 100644 index 00000000..dd40f920 --- /dev/null +++ b/Assets/External/websocket-sharp/Net/RequestStream.cs @@ -0,0 +1,354 @@ +#region License +/* + * RequestStream.cs + * + * This code is derived from RequestStream.cs (System.Net) of Mono + * (http://www.mono-project.com). + * + * The MIT License + * + * Copyright (c) 2005 Novell, Inc. (http://www.novell.com) + * Copyright (c) 2012-2023 sta.blockhead + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#endregion + +#region Authors +/* + * Authors: + * - Gonzalo Paniagua Javier + */ +#endregion + +using System; +using System.IO; + +namespace WebSocketSharp.Net +{ + internal class RequestStream : Stream + { + #region Private Fields + + private long _bodyLeft; + private int _count; + private bool _disposed; + private byte[] _initialBuffer; + private Stream _innerStream; + private int _offset; + + #endregion + + #region Internal Constructors + + internal RequestStream ( + Stream innerStream, + byte[] initialBuffer, + int offset, + int count, + long contentLength + ) + { + _innerStream = innerStream; + _initialBuffer = initialBuffer; + _offset = offset; + _count = count; + _bodyLeft = contentLength; + } + + #endregion + + #region Internal Properties + + internal int Count { + get { + return _count; + } + } + + internal byte[] InitialBuffer { + get { + return _initialBuffer; + } + } + + internal string ObjectName { + get { + return GetType ().ToString (); + } + } + + internal int Offset { + get { + return _offset; + } + } + + #endregion + + #region Public Properties + + public override bool CanRead { + get { + return true; + } + } + + public override bool CanSeek { + get { + return false; + } + } + + public override bool CanWrite { + get { + return false; + } + } + + public override long Length { + get { + throw new NotSupportedException (); + } + } + + public override long Position { + get { + throw new NotSupportedException (); + } + + set { + throw new NotSupportedException (); + } + } + + #endregion + + #region Private Methods + + private int fillFromInitialBuffer (byte[] buffer, int offset, int count) + { + // This method returns a int: + // - > 0 The number of bytes read from the initial buffer + // - 0 No more bytes read from the initial buffer + // - -1 No more content data + + if (_bodyLeft == 0) + return -1; + + if (_count == 0) + return 0; + + if (count > _count) + count = _count; + + if (_bodyLeft > 0 && _bodyLeft < count) + count = (int) _bodyLeft; + + Buffer.BlockCopy (_initialBuffer, _offset, buffer, offset, count); + + _offset += count; + _count -= count; + + if (_bodyLeft > 0) + _bodyLeft -= count; + + return count; + } + + #endregion + + #region Public Methods + + public override IAsyncResult BeginRead ( + byte[] buffer, + int offset, + int count, + AsyncCallback callback, + object state + ) + { + if (_disposed) + throw new ObjectDisposedException (ObjectName); + + if (buffer == null) + throw new ArgumentNullException ("buffer"); + + if (offset < 0) { + var msg = "A negative value."; + + throw new ArgumentOutOfRangeException ("offset", msg); + } + + if (count < 0) { + var msg = "A negative value."; + + throw new ArgumentOutOfRangeException ("count", msg); + } + + var len = buffer.Length; + + if (offset + count > len) { + var msg = "The sum of offset and count is greater than the length of buffer."; + + throw new ArgumentException (msg); + } + + if (count == 0) + return _innerStream.BeginRead (buffer, offset, 0, callback, state); + + var nread = fillFromInitialBuffer (buffer, offset, count); + + if (nread != 0) { + var ares = new HttpStreamAsyncResult (callback, state); + + ares.Buffer = buffer; + ares.Offset = offset; + ares.Count = count; + ares.SyncRead = nread > 0 ? nread : 0; + + ares.Complete (); + + return ares; + } + + if (_bodyLeft > 0 && _bodyLeft < count) + count = (int) _bodyLeft; + + return _innerStream.BeginRead (buffer, offset, count, callback, state); + } + + public override IAsyncResult BeginWrite ( + byte[] buffer, + int offset, + int count, + AsyncCallback callback, + object state + ) + { + throw new NotSupportedException (); + } + + public override void Close () + { + _disposed = true; + } + + public override int EndRead (IAsyncResult asyncResult) + { + if (_disposed) + throw new ObjectDisposedException (ObjectName); + + if (asyncResult == null) + throw new ArgumentNullException ("asyncResult"); + + if (asyncResult is HttpStreamAsyncResult) { + var ares = (HttpStreamAsyncResult) asyncResult; + + if (!ares.IsCompleted) + ares.AsyncWaitHandle.WaitOne (); + + return ares.SyncRead; + } + + var nread = _innerStream.EndRead (asyncResult); + + if (nread > 0 && _bodyLeft > 0) + _bodyLeft -= nread; + + return nread; + } + + public override void EndWrite (IAsyncResult asyncResult) + { + throw new NotSupportedException (); + } + + public override void Flush () + { + } + + public override int Read (byte[] buffer, int offset, int count) + { + if (_disposed) + throw new ObjectDisposedException (ObjectName); + + if (buffer == null) + throw new ArgumentNullException ("buffer"); + + if (offset < 0) { + var msg = "A negative value."; + + throw new ArgumentOutOfRangeException ("offset", msg); + } + + if (count < 0) { + var msg = "A negative value."; + + throw new ArgumentOutOfRangeException ("count", msg); + } + + var len = buffer.Length; + + if (offset + count > len) { + var msg = "The sum of offset and count is greater than the length of buffer."; + + throw new ArgumentException (msg); + } + + if (count == 0) + return 0; + + var nread = fillFromInitialBuffer (buffer, offset, count); + + if (nread == -1) + return 0; + + if (nread > 0) + return nread; + + if (_bodyLeft > 0 && _bodyLeft < count) + count = (int) _bodyLeft; + + nread = _innerStream.Read (buffer, offset, count); + + if (nread > 0 && _bodyLeft > 0) + _bodyLeft -= nread; + + return nread; + } + + public override long Seek (long offset, SeekOrigin origin) + { + throw new NotSupportedException (); + } + + public override void SetLength (long value) + { + throw new NotSupportedException (); + } + + public override void Write (byte[] buffer, int offset, int count) + { + throw new NotSupportedException (); + } + + #endregion + } +} diff --git a/Assets/External/websocket-sharp/Net/RequestStream.cs.meta b/Assets/External/websocket-sharp/Net/RequestStream.cs.meta new file mode 100644 index 00000000..e14214f4 --- /dev/null +++ b/Assets/External/websocket-sharp/Net/RequestStream.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 154f802ba52891c42a93181be89345c4 \ No newline at end of file diff --git a/Assets/External/websocket-sharp/Net/ResponseStream.cs b/Assets/External/websocket-sharp/Net/ResponseStream.cs new file mode 100644 index 00000000..456d1e47 --- /dev/null +++ b/Assets/External/websocket-sharp/Net/ResponseStream.cs @@ -0,0 +1,416 @@ +#region License +/* + * ResponseStream.cs + * + * This code is derived from ResponseStream.cs (System.Net) of Mono + * (http://www.mono-project.com). + * + * The MIT License + * + * Copyright (c) 2005 Novell, Inc. (http://www.novell.com) + * Copyright (c) 2012-2023 sta.blockhead + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#endregion + +#region Authors +/* + * Authors: + * - Gonzalo Paniagua Javier + */ +#endregion + +using System; +using System.IO; +using System.Text; + +namespace WebSocketSharp.Net +{ + internal class ResponseStream : Stream + { + #region Private Fields + + private MemoryStream _bodyBuffer; + private static readonly byte[] _crlf; + private bool _disposed; + private Stream _innerStream; + private static readonly byte[] _lastChunk; + private static readonly int _maxHeadersLength; + private HttpListenerResponse _response; + private bool _sendChunked; + private Action _write; + private Action _writeBody; + private Action _writeChunked; + + #endregion + + #region Static Constructor + + static ResponseStream () + { + _crlf = new byte[] { 13, 10 }; // "\r\n" + _lastChunk = new byte[] { 48, 13, 10, 13, 10 }; // "0\r\n\r\n" + _maxHeadersLength = 32768; + } + + #endregion + + #region Internal Constructors + + internal ResponseStream ( + Stream innerStream, + HttpListenerResponse response, + bool ignoreWriteExceptions + ) + { + _innerStream = innerStream; + _response = response; + + if (ignoreWriteExceptions) { + _write = writeWithoutThrowingException; + _writeChunked = writeChunkedWithoutThrowingException; + } + else { + _write = innerStream.Write; + _writeChunked = writeChunked; + } + + _bodyBuffer = new MemoryStream (); + } + + #endregion + + #region Internal Properties + + internal string ObjectName { + get { + return GetType ().ToString (); + } + } + + #endregion + + #region Public Properties + + public override bool CanRead { + get { + return false; + } + } + + public override bool CanSeek { + get { + return false; + } + } + + public override bool CanWrite { + get { + return !_disposed; + } + } + + public override long Length { + get { + throw new NotSupportedException (); + } + } + + public override long Position { + get { + throw new NotSupportedException (); + } + + set { + throw new NotSupportedException (); + } + } + + #endregion + + #region Private Methods + + private bool flush (bool closing) + { + if (!_response.HeadersSent) { + if (!flushHeaders ()) + return false; + + _response.HeadersSent = true; + + _sendChunked = _response.SendChunked; + _writeBody = _sendChunked ? _writeChunked : _write; + } + + flushBody (closing); + + return true; + } + + private void flushBody (bool closing) + { + using (_bodyBuffer) { + var len = _bodyBuffer.Length; + + if (len > Int32.MaxValue) { + _bodyBuffer.Position = 0; + + var buffLen = 1024; + var buff = new byte[buffLen]; + var nread = 0; + + while (true) { + nread = _bodyBuffer.Read (buff, 0, buffLen); + + if (nread <= 0) + break; + + _writeBody (buff, 0, nread); + } + } + else if (len > 0) { + var buff = _bodyBuffer.GetBuffer (); + + _writeBody (buff, 0, (int) len); + } + } + + if (!closing) { + _bodyBuffer = new MemoryStream (); + + return; + } + + if (_sendChunked) + _write (_lastChunk, 0, 5); + + _bodyBuffer = null; + } + + private bool flushHeaders () + { + if (!_response.SendChunked) { + if (_response.ContentLength64 != _bodyBuffer.Length) + return false; + } + + var headers = _response.FullHeaders; + + var stream = new MemoryStream (); + var enc = Encoding.UTF8; + + using (var writer = new StreamWriter (stream, enc, 256)) { + writer.Write (_response.StatusLine); + + var s = headers.ToStringMultiValue (true); + + writer.Write (s); + writer.Flush (); + + var start = enc.GetPreamble ().Length; + var len = stream.Length - start; + + if (len > _maxHeadersLength) + return false; + + var buff = stream.GetBuffer (); + + _write (buff, start, (int) len); + } + + _response.CloseConnection = headers["Connection"] == "close"; + + return true; + } + + private static byte[] getChunkSizeStringAsBytes (int size) + { + var fmt = "{0:x}\r\n"; + var s = String.Format (fmt, size); + + return Encoding.ASCII.GetBytes (s); + } + + private void writeChunked (byte[] buffer, int offset, int count) + { + var size = getChunkSizeStringAsBytes (count); + + _innerStream.Write (size, 0, size.Length); + _innerStream.Write (buffer, offset, count); + _innerStream.Write (_crlf, 0, 2); + } + + private void writeChunkedWithoutThrowingException ( + byte[] buffer, + int offset, + int count + ) + { + try { + writeChunked (buffer, offset, count); + } + catch { + } + } + + private void writeWithoutThrowingException ( + byte[] buffer, + int offset, + int count + ) + { + try { + _innerStream.Write (buffer, offset, count); + } + catch { + } + } + + #endregion + + #region Internal Methods + + internal void Close (bool force) + { + if (_disposed) + return; + + _disposed = true; + + if (!force) { + if (flush (true)) { + _response.Close (); + + _response = null; + _innerStream = null; + + return; + } + + _response.CloseConnection = true; + } + + if (_sendChunked) + _write (_lastChunk, 0, 5); + + _bodyBuffer.Dispose (); + _response.Abort (); + + _bodyBuffer = null; + _response = null; + _innerStream = null; + } + + internal void InternalWrite (byte[] buffer, int offset, int count) + { + _write (buffer, offset, count); + } + + #endregion + + #region Public Methods + + public override IAsyncResult BeginRead ( + byte[] buffer, + int offset, + int count, + AsyncCallback callback, + object state + ) + { + throw new NotSupportedException (); + } + + public override IAsyncResult BeginWrite ( + byte[] buffer, + int offset, + int count, + AsyncCallback callback, + object state + ) + { + if (_disposed) + throw new ObjectDisposedException (ObjectName); + + return _bodyBuffer.BeginWrite (buffer, offset, count, callback, state); + } + + public override void Close () + { + Close (false); + } + + protected override void Dispose (bool disposing) + { + Close (!disposing); + } + + public override int EndRead (IAsyncResult asyncResult) + { + throw new NotSupportedException (); + } + + public override void EndWrite (IAsyncResult asyncResult) + { + if (_disposed) + throw new ObjectDisposedException (ObjectName); + + _bodyBuffer.EndWrite (asyncResult); + } + + public override void Flush () + { + if (_disposed) + return; + + var sendChunked = _sendChunked || _response.SendChunked; + + if (!sendChunked) + return; + + flush (false); + } + + public override int Read (byte[] buffer, int offset, int count) + { + throw new NotSupportedException (); + } + + public override long Seek (long offset, SeekOrigin origin) + { + throw new NotSupportedException (); + } + + public override void SetLength (long value) + { + throw new NotSupportedException (); + } + + public override void Write (byte[] buffer, int offset, int count) + { + if (_disposed) + throw new ObjectDisposedException (ObjectName); + + _bodyBuffer.Write (buffer, offset, count); + } + + #endregion + } +} diff --git a/Assets/External/websocket-sharp/Net/ResponseStream.cs.meta b/Assets/External/websocket-sharp/Net/ResponseStream.cs.meta new file mode 100644 index 00000000..c1285b09 --- /dev/null +++ b/Assets/External/websocket-sharp/Net/ResponseStream.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: f80d37cc62e523e43a664bce71d36788 \ No newline at end of file diff --git a/Assets/External/websocket-sharp/Net/ServerSslConfiguration.cs b/Assets/External/websocket-sharp/Net/ServerSslConfiguration.cs new file mode 100644 index 00000000..b4de5d64 --- /dev/null +++ b/Assets/External/websocket-sharp/Net/ServerSslConfiguration.cs @@ -0,0 +1,239 @@ +#region License +/* + * ServerSslConfiguration.cs + * + * The MIT License + * + * Copyright (c) 2014 liryna + * Copyright (c) 2014-2024 sta.blockhead + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#endregion + +#region Authors +/* + * Authors: + * - Liryna + */ +#endregion + +using System; +using System.Net.Security; +using System.Security.Authentication; +using System.Security.Cryptography.X509Certificates; + +namespace WebSocketSharp.Net +{ + /// + /// Stores the parameters for instances used by + /// a server. + /// + public class ServerSslConfiguration + { + #region Private Fields + + private bool _checkCertRevocation; + private bool _clientCertRequired; + private RemoteCertificateValidationCallback _clientCertValidationCallback; + private SslProtocols _enabledSslProtocols; + private X509Certificate2 _serverCert; + + #endregion + + #region Public Constructors + + /// + /// Initializes a new instance of the + /// class. + /// + public ServerSslConfiguration () + { + _enabledSslProtocols = SslProtocols.None; + } + + /// + /// Initializes a new instance of the + /// class copying from the specified configuration. + /// + /// + /// A from which to copy. + /// + /// + /// is . + /// + public ServerSslConfiguration (ServerSslConfiguration configuration) + { + if (configuration == null) + throw new ArgumentNullException ("configuration"); + + _checkCertRevocation = configuration._checkCertRevocation; + _clientCertRequired = configuration._clientCertRequired; + _clientCertValidationCallback = configuration._clientCertValidationCallback; + _enabledSslProtocols = configuration._enabledSslProtocols; + _serverCert = configuration._serverCert; + } + + #endregion + + #region Public Properties + + /// + /// Gets or sets a value indicating whether the certificate revocation + /// list is checked during authentication. + /// + /// + /// + /// true if the certificate revocation list is checked during + /// authentication; otherwise, false. + /// + /// + /// The default value is false. + /// + /// + public bool CheckCertificateRevocation { + get { + return _checkCertRevocation; + } + + set { + _checkCertRevocation = value; + } + } + + /// + /// Gets or sets a value indicating whether each client is asked for + /// a certificate for authentication. + /// + /// + /// + /// true if each client is asked for a certificate for + /// authentication; otherwise, false. + /// + /// + /// The default value is false. + /// + /// + public bool ClientCertificateRequired { + get { + return _clientCertRequired; + } + + set { + _clientCertRequired = value; + } + } + + /// + /// Gets or sets the callback used to validate the certificate supplied by + /// each client. + /// + /// + /// The certificate is valid if the callback returns true. + /// + /// + /// + /// A delegate. + /// + /// + /// It represents the delegate called when the server validates + /// the certificate. + /// + /// + /// The default value invokes a method that only returns true. + /// + /// + public RemoteCertificateValidationCallback ClientCertificateValidationCallback { + get { + if (_clientCertValidationCallback == null) + _clientCertValidationCallback = defaultValidateClientCertificate; + + return _clientCertValidationCallback; + } + + set { + _clientCertValidationCallback = value; + } + } + + /// + /// Gets or sets the enabled versions of the SSL/TLS protocols. + /// + /// + /// + /// Any of the enum values. + /// + /// + /// It represents the enabled versions of the SSL/TLS protocols. + /// + /// + /// The default value is . + /// + /// + public SslProtocols EnabledSslProtocols { + get { + return _enabledSslProtocols; + } + + set { + _enabledSslProtocols = value; + } + } + + /// + /// Gets or sets the certificate used to authenticate the server. + /// + /// + /// + /// A that represents an X.509 certificate. + /// + /// + /// if not present. + /// + /// + /// The default value is . + /// + /// + public X509Certificate2 ServerCertificate { + get { + return _serverCert; + } + + set { + _serverCert = value; + } + } + + #endregion + + #region Private Methods + + private static bool defaultValidateClientCertificate ( + object sender, + X509Certificate certificate, + X509Chain chain, + SslPolicyErrors sslPolicyErrors + ) + { + return true; + } + + #endregion + } +} diff --git a/Assets/External/websocket-sharp/Net/ServerSslConfiguration.cs.meta b/Assets/External/websocket-sharp/Net/ServerSslConfiguration.cs.meta new file mode 100644 index 00000000..f4bdbbee --- /dev/null +++ b/Assets/External/websocket-sharp/Net/ServerSslConfiguration.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 00dcca2ef788e9149beb201524f880b2 \ No newline at end of file diff --git a/Assets/External/websocket-sharp/Net/WebHeaderCollection.cs b/Assets/External/websocket-sharp/Net/WebHeaderCollection.cs new file mode 100644 index 00000000..e4115081 --- /dev/null +++ b/Assets/External/websocket-sharp/Net/WebHeaderCollection.cs @@ -0,0 +1,1907 @@ +#region License +/* + * WebHeaderCollection.cs + * + * This code is derived from WebHeaderCollection.cs (System.Net) of Mono + * (http://www.mono-project.com). + * + * The MIT License + * + * Copyright (c) 2003 Ximian, Inc. (http://www.ximian.com) + * Copyright (c) 2007 Novell, Inc. (http://www.novell.com) + * Copyright (c) 2012-2024 sta.blockhead + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#endregion + +#region Authors +/* + * Authors: + * - Lawrence Pit + * - Gonzalo Paniagua Javier + * - Miguel de Icaza + */ +#endregion + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Runtime.InteropServices; +using System.Runtime.Serialization; +using System.Security.Permissions; +using System.Text; + +namespace WebSocketSharp.Net +{ + /// + /// Provides a collection of the HTTP headers associated with a request or + /// response. + /// + [Serializable] + [ComVisible (true)] + public class WebHeaderCollection : NameValueCollection, ISerializable + { + #region Private Fields + + private static readonly Dictionary _headers; + private bool _internallyUsed; + private HttpHeaderType _state; + + #endregion + + #region Static Constructor + + static WebHeaderCollection () + { + _headers = + new Dictionary ( + StringComparer.InvariantCultureIgnoreCase + ) + { + { + "Accept", + new HttpHeaderInfo ( + "Accept", + HttpHeaderType.Request + | HttpHeaderType.Restricted + | HttpHeaderType.MultiValue + ) + }, + { + "AcceptCharset", + new HttpHeaderInfo ( + "Accept-Charset", + HttpHeaderType.Request | HttpHeaderType.MultiValue + ) + }, + { + "AcceptEncoding", + new HttpHeaderInfo ( + "Accept-Encoding", + HttpHeaderType.Request | HttpHeaderType.MultiValue + ) + }, + { + "AcceptLanguage", + new HttpHeaderInfo ( + "Accept-Language", + HttpHeaderType.Request | HttpHeaderType.MultiValue + ) + }, + { + "AcceptRanges", + new HttpHeaderInfo ( + "Accept-Ranges", + HttpHeaderType.Response | HttpHeaderType.MultiValue + ) + }, + { + "Age", + new HttpHeaderInfo ( + "Age", + HttpHeaderType.Response + ) + }, + { + "Allow", + new HttpHeaderInfo ( + "Allow", + HttpHeaderType.Request + | HttpHeaderType.Response + | HttpHeaderType.MultiValue + ) + }, + { + "Authorization", + new HttpHeaderInfo ( + "Authorization", + HttpHeaderType.Request | HttpHeaderType.MultiValue + ) + }, + { + "CacheControl", + new HttpHeaderInfo ( + "Cache-Control", + HttpHeaderType.Request + | HttpHeaderType.Response + | HttpHeaderType.MultiValue + ) + }, + { + "Connection", + new HttpHeaderInfo ( + "Connection", + HttpHeaderType.Request + | HttpHeaderType.Response + | HttpHeaderType.Restricted + | HttpHeaderType.MultiValue + ) + }, + { + "ContentEncoding", + new HttpHeaderInfo ( + "Content-Encoding", + HttpHeaderType.Request + | HttpHeaderType.Response + | HttpHeaderType.MultiValue + ) + }, + { + "ContentLanguage", + new HttpHeaderInfo ( + "Content-Language", + HttpHeaderType.Request + | HttpHeaderType.Response + | HttpHeaderType.MultiValue + ) + }, + { + "ContentLength", + new HttpHeaderInfo ( + "Content-Length", + HttpHeaderType.Request + | HttpHeaderType.Response + | HttpHeaderType.Restricted + ) + }, + { + "ContentLocation", + new HttpHeaderInfo ( + "Content-Location", + HttpHeaderType.Request | HttpHeaderType.Response + ) + }, + { + "ContentMd5", + new HttpHeaderInfo ( + "Content-MD5", + HttpHeaderType.Request | HttpHeaderType.Response + ) + }, + { + "ContentRange", + new HttpHeaderInfo ( + "Content-Range", + HttpHeaderType.Request | HttpHeaderType.Response + ) + }, + { + "ContentType", + new HttpHeaderInfo ( + "Content-Type", + HttpHeaderType.Request + | HttpHeaderType.Response + | HttpHeaderType.Restricted + ) + }, + { + "Cookie", + new HttpHeaderInfo ( + "Cookie", + HttpHeaderType.Request + ) + }, + { + "Cookie2", + new HttpHeaderInfo ( + "Cookie2", + HttpHeaderType.Request + ) + }, + { + "Date", + new HttpHeaderInfo ( + "Date", + HttpHeaderType.Request + | HttpHeaderType.Response + | HttpHeaderType.Restricted + ) + }, + { + "Expect", + new HttpHeaderInfo ( + "Expect", + HttpHeaderType.Request + | HttpHeaderType.Restricted + | HttpHeaderType.MultiValue + ) + }, + { + "Expires", + new HttpHeaderInfo ( + "Expires", + HttpHeaderType.Request | HttpHeaderType.Response + ) + }, + { + "ETag", + new HttpHeaderInfo ( + "ETag", + HttpHeaderType.Response + ) + }, + { + "From", + new HttpHeaderInfo ( + "From", + HttpHeaderType.Request + ) + }, + { + "Host", + new HttpHeaderInfo ( + "Host", + HttpHeaderType.Request | HttpHeaderType.Restricted + ) + }, + { + "IfMatch", + new HttpHeaderInfo ( + "If-Match", + HttpHeaderType.Request | HttpHeaderType.MultiValue + ) + }, + { + "IfModifiedSince", + new HttpHeaderInfo ( + "If-Modified-Since", + HttpHeaderType.Request | HttpHeaderType.Restricted + ) + }, + { + "IfNoneMatch", + new HttpHeaderInfo ( + "If-None-Match", + HttpHeaderType.Request | HttpHeaderType.MultiValue + ) + }, + { + "IfRange", + new HttpHeaderInfo ( + "If-Range", + HttpHeaderType.Request + ) + }, + { + "IfUnmodifiedSince", + new HttpHeaderInfo ( + "If-Unmodified-Since", + HttpHeaderType.Request + ) + }, + { + "KeepAlive", + new HttpHeaderInfo ( + "Keep-Alive", + HttpHeaderType.Request + | HttpHeaderType.Response + | HttpHeaderType.MultiValue + ) + }, + { + "LastModified", + new HttpHeaderInfo ( + "Last-Modified", + HttpHeaderType.Request | HttpHeaderType.Response + ) + }, + { + "Location", + new HttpHeaderInfo ( + "Location", + HttpHeaderType.Response + ) + }, + { + "MaxForwards", + new HttpHeaderInfo ( + "Max-Forwards", + HttpHeaderType.Request + ) + }, + { + "Pragma", + new HttpHeaderInfo ( + "Pragma", + HttpHeaderType.Request | HttpHeaderType.Response + ) + }, + { + "ProxyAuthenticate", + new HttpHeaderInfo ( + "Proxy-Authenticate", + HttpHeaderType.Response | HttpHeaderType.MultiValue + ) + }, + { + "ProxyAuthorization", + new HttpHeaderInfo ( + "Proxy-Authorization", + HttpHeaderType.Request + ) + }, + { + "ProxyConnection", + new HttpHeaderInfo ( + "Proxy-Connection", + HttpHeaderType.Request + | HttpHeaderType.Response + | HttpHeaderType.Restricted + ) + }, + { + "Public", + new HttpHeaderInfo ( + "Public", + HttpHeaderType.Response | HttpHeaderType.MultiValue + ) + }, + { + "Range", + new HttpHeaderInfo ( + "Range", + HttpHeaderType.Request + | HttpHeaderType.Restricted + | HttpHeaderType.MultiValue + ) + }, + { + "Referer", + new HttpHeaderInfo ( + "Referer", + HttpHeaderType.Request | HttpHeaderType.Restricted + ) + }, + { + "RetryAfter", + new HttpHeaderInfo ( + "Retry-After", + HttpHeaderType.Response + ) + }, + { + "SecWebSocketAccept", + new HttpHeaderInfo ( + "Sec-WebSocket-Accept", + HttpHeaderType.Response | HttpHeaderType.Restricted + ) + }, + { + "SecWebSocketExtensions", + new HttpHeaderInfo ( + "Sec-WebSocket-Extensions", + HttpHeaderType.Request + | HttpHeaderType.Response + | HttpHeaderType.Restricted + | HttpHeaderType.MultiValueInRequest + ) + }, + { + "SecWebSocketKey", + new HttpHeaderInfo ( + "Sec-WebSocket-Key", + HttpHeaderType.Request | HttpHeaderType.Restricted + ) + }, + { + "SecWebSocketProtocol", + new HttpHeaderInfo ( + "Sec-WebSocket-Protocol", + HttpHeaderType.Request + | HttpHeaderType.Response + | HttpHeaderType.MultiValueInRequest + ) + }, + { + "SecWebSocketVersion", + new HttpHeaderInfo ( + "Sec-WebSocket-Version", + HttpHeaderType.Request + | HttpHeaderType.Response + | HttpHeaderType.Restricted + | HttpHeaderType.MultiValueInResponse + ) + }, + { + "Server", + new HttpHeaderInfo ( + "Server", + HttpHeaderType.Response + ) + }, + { + "SetCookie", + new HttpHeaderInfo ( + "Set-Cookie", + HttpHeaderType.Response | HttpHeaderType.MultiValue + ) + }, + { + "SetCookie2", + new HttpHeaderInfo ( + "Set-Cookie2", + HttpHeaderType.Response | HttpHeaderType.MultiValue + ) + }, + { + "Te", + new HttpHeaderInfo ( + "TE", + HttpHeaderType.Request + ) + }, + { + "Trailer", + new HttpHeaderInfo ( + "Trailer", + HttpHeaderType.Request | HttpHeaderType.Response + ) + }, + { + "TransferEncoding", + new HttpHeaderInfo ( + "Transfer-Encoding", + HttpHeaderType.Request + | HttpHeaderType.Response + | HttpHeaderType.Restricted + | HttpHeaderType.MultiValue + ) + }, + { + "Translate", + new HttpHeaderInfo ( + "Translate", + HttpHeaderType.Request + ) + }, + { + "Upgrade", + new HttpHeaderInfo ( + "Upgrade", + HttpHeaderType.Request + | HttpHeaderType.Response + | HttpHeaderType.MultiValue + ) + }, + { + "UserAgent", + new HttpHeaderInfo ( + "User-Agent", + HttpHeaderType.Request | HttpHeaderType.Restricted + ) + }, + { + "Vary", + new HttpHeaderInfo ( + "Vary", + HttpHeaderType.Response | HttpHeaderType.MultiValue + ) + }, + { + "Via", + new HttpHeaderInfo ( + "Via", + HttpHeaderType.Request + | HttpHeaderType.Response + | HttpHeaderType.MultiValue + ) + }, + { + "Warning", + new HttpHeaderInfo ( + "Warning", + HttpHeaderType.Request + | HttpHeaderType.Response + | HttpHeaderType.MultiValue + ) + }, + { + "WwwAuthenticate", + new HttpHeaderInfo ( + "WWW-Authenticate", + HttpHeaderType.Response + | HttpHeaderType.Restricted + | HttpHeaderType.MultiValue + ) + } + }; + } + + #endregion + + #region Internal Constructors + + internal WebHeaderCollection (HttpHeaderType state, bool internallyUsed) + { + _state = state; + _internallyUsed = internallyUsed; + } + + #endregion + + #region Protected Constructors + + /// + /// Initializes a new instance of the + /// class with the specified serialized data. + /// + /// + /// A that contains the serialized + /// object data. + /// + /// + /// A that specifies the source for + /// the deserialization. + /// + /// + /// An element with the specified name is not found in + /// . + /// + /// + /// is . + /// + protected WebHeaderCollection ( + SerializationInfo serializationInfo, + StreamingContext streamingContext + ) + { + if (serializationInfo == null) + throw new ArgumentNullException ("serializationInfo"); + + try { + _internallyUsed = serializationInfo.GetBoolean ("InternallyUsed"); + _state = (HttpHeaderType) serializationInfo.GetInt32 ("State"); + + var cnt = serializationInfo.GetInt32 ("Count"); + + for (var i = 0; i < cnt; i++) { + base.Add ( + serializationInfo.GetString (i.ToString ()), + serializationInfo.GetString ((cnt + i).ToString ()) + ); + } + } + catch (SerializationException ex) { + throw new ArgumentException (ex.Message, "serializationInfo", ex); + } + } + + #endregion + + #region Public Constructors + + /// + /// Initializes a new instance of the + /// class. + /// + public WebHeaderCollection () + { + } + + #endregion + + #region Internal Properties + + internal HttpHeaderType State { + get { + return _state; + } + } + + #endregion + + #region Public Properties + + /// + /// Gets all header names in the collection. + /// + /// + /// An array of that contains all header names in + /// the collection. + /// + public override string[] AllKeys { + get { + return base.AllKeys; + } + } + + /// + /// Gets the number of headers in the collection. + /// + /// + /// An that represents the number of headers in + /// the collection. + /// + public override int Count { + get { + return base.Count; + } + } + + /// + /// Gets or sets the specified request header. + /// + /// + /// A that represents the value of the request header. + /// + /// + /// + /// One of the enum values. + /// + /// + /// It specifies the request header to get or set. + /// + /// + /// + /// + /// is a restricted header. + /// + /// + /// -or- + /// + /// + /// contains an invalid character. + /// + /// + /// + /// The length of is greater than 65,535 + /// characters. + /// + /// + /// This instance does not allow the request header. + /// + public string this[HttpRequestHeader header] { + get { + var key = header.ToString (); + var name = getHeaderName (key); + + return Get (name); + } + + set { + Add (header, value); + } + } + + /// + /// Gets or sets the specified response header. + /// + /// + /// A that represents the value of the response header. + /// + /// + /// + /// One of the enum values. + /// + /// + /// It specifies the response header to get or set. + /// + /// + /// + /// + /// is a restricted header. + /// + /// + /// -or- + /// + /// + /// contains an invalid character. + /// + /// + /// + /// The length of is greater than 65,535 + /// characters. + /// + /// + /// This instance does not allow the response header. + /// + public string this[HttpResponseHeader header] { + get { + var key = header.ToString (); + var name = getHeaderName (key); + + return Get (name); + } + + set { + Add (header, value); + } + } + + /// + /// Gets a collection of header names in the collection. + /// + /// + /// A that contains + /// all header names in the collection. + /// + public override NameObjectCollectionBase.KeysCollection Keys { + get { + return base.Keys; + } + } + + #endregion + + #region Private Methods + + private void add (string name, string value, HttpHeaderType headerType) + { + base.Add (name, value); + + if (_state != HttpHeaderType.Unspecified) + return; + + if (headerType == HttpHeaderType.Unspecified) + return; + + _state = headerType; + } + + private void checkAllowed (HttpHeaderType headerType) + { + if (_state == HttpHeaderType.Unspecified) + return; + + if (headerType == HttpHeaderType.Unspecified) + return; + + if (headerType != _state) { + var msg = "This instance does not allow the header."; + + throw new InvalidOperationException (msg); + } + } + + private static string checkName (string name, string paramName) + { + if (name == null) { + var msg = "The name is null."; + + throw new ArgumentNullException (paramName, msg); + } + + if (name.Length == 0) { + var msg = "The name is an empty string."; + + throw new ArgumentException (msg, paramName); + } + + name = name.Trim (); + + if (name.Length == 0) { + var msg = "The name is a string of spaces."; + + throw new ArgumentException (msg, paramName); + } + + if (!name.IsToken ()) { + var msg = "The name contains an invalid character."; + + throw new ArgumentException (msg, paramName); + } + + return name; + } + + private void checkRestricted (string name, HttpHeaderType headerType) + { + if (_internallyUsed) + return; + + var res = headerType == HttpHeaderType.Response; + + if (isRestricted (name, res)) { + var msg = "The header is a restricted header."; + + throw new ArgumentException (msg); + } + } + + private static string checkValue (string value, string paramName) + { + if (value == null) + return String.Empty; + + value = value.Trim (); + + var len = value.Length; + + if (len == 0) + return value; + + if (len > 65535) { + var msg = "The length of the value is greater than 65,535 characters."; + + throw new ArgumentOutOfRangeException (paramName, msg); + } + + if (!value.IsText ()) { + var msg = "The value contains an invalid character."; + + throw new ArgumentException (msg, paramName); + } + + return value; + } + + private static HttpHeaderInfo getHeaderInfo (string name) + { + var compType = StringComparison.InvariantCultureIgnoreCase; + + foreach (var headerInfo in _headers.Values) { + if (headerInfo.HeaderName.Equals (name, compType)) + return headerInfo; + } + + return null; + } + + private static string getHeaderName (string key) + { + HttpHeaderInfo headerInfo; + + return _headers.TryGetValue (key, out headerInfo) + ? headerInfo.HeaderName + : null; + } + + private static HttpHeaderType getHeaderType (string name) + { + var headerInfo = getHeaderInfo (name); + + if (headerInfo == null) + return HttpHeaderType.Unspecified; + + if (headerInfo.IsRequest) { + return !headerInfo.IsResponse + ? HttpHeaderType.Request + : HttpHeaderType.Unspecified; + } + + return headerInfo.IsResponse + ? HttpHeaderType.Response + : HttpHeaderType.Unspecified; + } + + private static bool isMultiValue (string name, bool response) + { + var headerInfo = getHeaderInfo (name); + + return headerInfo != null && headerInfo.IsMultiValue (response); + } + + private static bool isRestricted (string name, bool response) + { + var headerInfo = getHeaderInfo (name); + + return headerInfo != null && headerInfo.IsRestricted (response); + } + + private void set (string name, string value, HttpHeaderType headerType) + { + base.Set (name, value); + + if (_state != HttpHeaderType.Unspecified) + return; + + if (headerType == HttpHeaderType.Unspecified) + return; + + _state = headerType; + } + + #endregion + + #region Internal Methods + + internal void InternalRemove (string name) + { + base.Remove (name); + } + + internal void InternalSet (string header, bool response) + { + var idx = header.IndexOf (':'); + + if (idx == -1) { + var msg = "It does not contain a colon character."; + + throw new ArgumentException (msg, "header"); + } + + var name = header.Substring (0, idx); + var val = idx < header.Length - 1 + ? header.Substring (idx + 1) + : String.Empty; + + name = checkName (name, "header"); + val = checkValue (val, "header"); + + if (isMultiValue (name, response)) { + base.Add (name, val); + + return; + } + + base.Set (name, val); + } + + internal void InternalSet (string name, string value, bool response) + { + value = checkValue (value, "value"); + + if (isMultiValue (name, response)) { + base.Add (name, value); + + return; + } + + base.Set (name, value); + } + + internal string ToStringMultiValue (bool response) + { + var cnt = Count; + + if (cnt == 0) + return "\r\n"; + + var buff = new StringBuilder (); + + var fmt = "{0}: {1}\r\n"; + + for (var i = 0; i < cnt; i++) { + var name = GetKey (i); + + if (isMultiValue (name, response)) { + foreach (var val in GetValues (i)) + buff.AppendFormat (fmt, name, val); + + continue; + } + + buff.AppendFormat (fmt, name, Get (i)); + } + + buff.Append ("\r\n"); + + return buff.ToString (); + } + + #endregion + + #region Protected Methods + + /// + /// Adds a header to the collection without checking if the header is on + /// the restricted header list. + /// + /// + /// A that specifies the name of the header to add. + /// + /// + /// A that specifies the value of the header to add. + /// + /// + /// + /// is an empty string. + /// + /// + /// -or- + /// + /// + /// is a string of spaces. + /// + /// + /// -or- + /// + /// + /// contains an invalid character. + /// + /// + /// -or- + /// + /// + /// contains an invalid character. + /// + /// + /// + /// is . + /// + /// + /// The length of is greater than 65,535 + /// characters. + /// + /// + /// This instance does not allow the header. + /// + protected void AddWithoutValidate (string headerName, string headerValue) + { + headerName = checkName (headerName, "headerName"); + headerValue = checkValue (headerValue, "headerValue"); + + var headerType = getHeaderType (headerName); + + checkAllowed (headerType); + + add (headerName, headerValue, headerType); + } + + #endregion + + #region Public Methods + + /// + /// Adds the specified header to the collection. + /// + /// + /// A that specifies the header to add, + /// with the name and value separated by a colon character (':'). + /// + /// + /// + /// is an empty string. + /// + /// + /// -or- + /// + /// + /// does not contain a colon character. + /// + /// + /// -or- + /// + /// + /// The name part of is an empty string. + /// + /// + /// -or- + /// + /// + /// The name part of is a string of spaces. + /// + /// + /// -or- + /// + /// + /// The name part of contains an invalid + /// character. + /// + /// + /// -or- + /// + /// + /// The value part of contains an invalid + /// character. + /// + /// + /// -or- + /// + /// + /// is a restricted header. + /// + /// + /// + /// is . + /// + /// + /// The length of the value part of is greater + /// than 65,535 characters. + /// + /// + /// This instance does not allow the header. + /// + public void Add (string header) + { + if (header == null) + throw new ArgumentNullException ("header"); + + var len = header.Length; + + if (len == 0) { + var msg = "An empty string."; + + throw new ArgumentException (msg, "header"); + } + + var idx = header.IndexOf (':'); + + if (idx == -1) { + var msg = "It does not contain a colon character."; + + throw new ArgumentException (msg, "header"); + } + + var name = header.Substring (0, idx); + var val = idx < len - 1 ? header.Substring (idx + 1) : String.Empty; + + name = checkName (name, "header"); + val = checkValue (val, "header"); + + var headerType = getHeaderType (name); + + checkRestricted (name, headerType); + checkAllowed (headerType); + + add (name, val, headerType); + } + + /// + /// Adds the specified request header with the specified value to + /// the collection. + /// + /// + /// + /// One of the enum values. + /// + /// + /// It specifies the request header to add. + /// + /// + /// + /// A that specifies the value of the header to add. + /// + /// + /// + /// contains an invalid character. + /// + /// + /// -or- + /// + /// + /// is a restricted header. + /// + /// + /// + /// The length of is greater than 65,535 + /// characters. + /// + /// + /// This instance does not allow the request header. + /// + public void Add (HttpRequestHeader header, string value) + { + value = checkValue (value, "value"); + + var key = header.ToString (); + var name = getHeaderName (key); + + checkRestricted (name, HttpHeaderType.Request); + checkAllowed (HttpHeaderType.Request); + + add (name, value, HttpHeaderType.Request); + } + + /// + /// Adds the specified response header with the specified value to + /// the collection. + /// + /// + /// + /// One of the enum values. + /// + /// + /// It specifies the response header to add. + /// + /// + /// + /// A that specifies the value of the header to add. + /// + /// + /// + /// contains an invalid character. + /// + /// + /// -or- + /// + /// + /// is a restricted header. + /// + /// + /// + /// The length of is greater than 65,535 + /// characters. + /// + /// + /// This instance does not allow the response header. + /// + public void Add (HttpResponseHeader header, string value) + { + value = checkValue (value, "value"); + + var key = header.ToString (); + var name = getHeaderName (key); + + checkRestricted (name, HttpHeaderType.Response); + checkAllowed (HttpHeaderType.Response); + + add (name, value, HttpHeaderType.Response); + } + + /// + /// Adds a header with the specified name and value to the collection. + /// + /// + /// A that specifies the name of the header to add. + /// + /// + /// A that specifies the value of the header to add. + /// + /// + /// + /// is an empty string. + /// + /// + /// -or- + /// + /// + /// is a string of spaces. + /// + /// + /// -or- + /// + /// + /// contains an invalid character. + /// + /// + /// -or- + /// + /// + /// contains an invalid character. + /// + /// + /// -or- + /// + /// + /// is a restricted header name. + /// + /// + /// + /// is . + /// + /// + /// The length of is greater than 65,535 + /// characters. + /// + /// + /// This instance does not allow the header. + /// + public override void Add (string name, string value) + { + name = checkName (name, "name"); + value = checkValue (value, "value"); + + var headerType = getHeaderType (name); + + checkRestricted (name, headerType); + checkAllowed (headerType); + + add (name, value, headerType); + } + + /// + /// Removes all headers from the collection. + /// + public override void Clear () + { + base.Clear (); + + _state = HttpHeaderType.Unspecified; + } + + /// + /// Get the value of the header at the specified index in the collection. + /// + /// + /// A that receives the value of the header. + /// + /// + /// An that specifies the zero-based index of the header + /// to get. + /// + /// + /// is out of allowable range of indexes for + /// the collection. + /// + public override string Get (int index) + { + return base.Get (index); + } + + /// + /// Get the value of the header with the specified name in the collection. + /// + /// + /// + /// A that receives the value of the header. + /// + /// + /// if not found. + /// + /// + /// + /// A that specifies the name of the header to get. + /// + public override string Get (string name) + { + return base.Get (name); + } + + /// + /// Gets the enumerator used to iterate through the collection. + /// + /// + /// An instance used to iterate through + /// the collection. + /// + public override IEnumerator GetEnumerator () + { + return base.GetEnumerator (); + } + + /// + /// Get the name of the header at the specified index in the collection. + /// + /// + /// A that receives the name of the header. + /// + /// + /// An that specifies the zero-based index of the header + /// to get. + /// + /// + /// is out of allowable range of indexes for + /// the collection. + /// + public override string GetKey (int index) + { + return base.GetKey (index); + } + + /// + /// Get the values of the header at the specified index in the collection. + /// + /// + /// + /// An array of that receives the values of + /// the header. + /// + /// + /// if not present. + /// + /// + /// + /// An that specifies the zero-based index of the header + /// to get. + /// + /// + /// is out of allowable range of indexes for + /// the collection. + /// + public override string[] GetValues (int index) + { + var vals = base.GetValues (index); + + return vals != null && vals.Length > 0 ? vals : null; + } + + /// + /// Get the values of the header with the specified name in the collection. + /// + /// + /// + /// An array of that receives the values of + /// the header. + /// + /// + /// if not present. + /// + /// + /// + /// A that specifies the name of the header to get. + /// + public override string[] GetValues (string name) + { + var vals = base.GetValues (name); + + return vals != null && vals.Length > 0 ? vals : null; + } + + /// + /// Populates the specified instance with + /// the data needed to serialize the current instance. + /// + /// + /// A that holds the serialized object data. + /// + /// + /// A that specifies the destination for + /// the serialization. + /// + /// + /// is . + /// + [ + SecurityPermission ( + SecurityAction.LinkDemand, + Flags = SecurityPermissionFlag.SerializationFormatter + ) + ] + public override void GetObjectData ( + SerializationInfo serializationInfo, + StreamingContext streamingContext + ) + { + if (serializationInfo == null) + throw new ArgumentNullException ("serializationInfo"); + + serializationInfo.AddValue ("InternallyUsed", _internallyUsed); + serializationInfo.AddValue ("State", (int) _state); + + var cnt = Count; + + serializationInfo.AddValue ("Count", cnt); + + for (var i = 0; i < cnt; i++) { + serializationInfo.AddValue (i.ToString (), GetKey (i)); + serializationInfo.AddValue ((cnt + i).ToString (), Get (i)); + } + } + + /// + /// Determines whether the specified header can be set for the request. + /// + /// + /// true if the header cannot be set; otherwise, false. + /// + /// + /// A that specifies the name of the header to test. + /// + /// + /// + /// is an empty string. + /// + /// + /// -or- + /// + /// + /// is a string of spaces. + /// + /// + /// -or- + /// + /// + /// contains an invalid character. + /// + /// + /// + /// is . + /// + public static bool IsRestricted (string headerName) + { + return IsRestricted (headerName, false); + } + + /// + /// Determines whether the specified header can be set for the request or + /// the response. + /// + /// + /// true if the header cannot be set; otherwise, false. + /// + /// + /// A that specifies the name of the header to test. + /// + /// + /// A : true if the test is for the response; + /// otherwise, false. + /// + /// + /// + /// is an empty string. + /// + /// + /// -or- + /// + /// + /// is a string of spaces. + /// + /// + /// -or- + /// + /// + /// contains an invalid character. + /// + /// + /// + /// is . + /// + public static bool IsRestricted (string headerName, bool response) + { + headerName = checkName (headerName, "headerName"); + + return isRestricted (headerName, response); + } + + /// + /// Implements the interface and raises + /// the deserialization event when the deserialization is complete. + /// + /// + /// An instance that represents the source of + /// the deserialization event. + /// + public override void OnDeserialization (object sender) + { + } + + /// + /// Removes the specified request header from the collection. + /// + /// + /// + /// One of the enum values. + /// + /// + /// It specifies the request header to remove. + /// + /// + /// + /// is a restricted header. + /// + /// + /// This instance does not allow the request header. + /// + public void Remove (HttpRequestHeader header) + { + var key = header.ToString (); + var name = getHeaderName (key); + + checkRestricted (name, HttpHeaderType.Request); + checkAllowed (HttpHeaderType.Request); + + base.Remove (name); + } + + /// + /// Removes the specified response header from the collection. + /// + /// + /// + /// One of the enum values. + /// + /// + /// It specifies the response header to remove. + /// + /// + /// + /// is a restricted header. + /// + /// + /// This instance does not allow the response header. + /// + public void Remove (HttpResponseHeader header) + { + var key = header.ToString (); + var name = getHeaderName (key); + + checkRestricted (name, HttpHeaderType.Response); + checkAllowed (HttpHeaderType.Response); + + base.Remove (name); + } + + /// + /// Removes the specified header from the collection. + /// + /// + /// A that specifies the name of the header to remove. + /// + /// + /// + /// is an empty string. + /// + /// + /// -or- + /// + /// + /// is a string of spaces. + /// + /// + /// -or- + /// + /// + /// contains an invalid character. + /// + /// + /// -or- + /// + /// + /// is a restricted header name. + /// + /// + /// + /// is . + /// + /// + /// This instance does not allow the header. + /// + public override void Remove (string name) + { + name = checkName (name, "name"); + + var headerType = getHeaderType (name); + + checkRestricted (name, headerType); + checkAllowed (headerType); + + base.Remove (name); + } + + /// + /// Sets the specified request header to the specified value. + /// + /// + /// + /// One of the enum values. + /// + /// + /// It specifies the request header to set. + /// + /// + /// + /// A that specifies the value of the request header + /// to set. + /// + /// + /// + /// contains an invalid character. + /// + /// + /// -or- + /// + /// + /// is a restricted header. + /// + /// + /// + /// The length of is greater than 65,535 + /// characters. + /// + /// + /// This instance does not allow the request header. + /// + public void Set (HttpRequestHeader header, string value) + { + value = checkValue (value, "value"); + + var key = header.ToString (); + var name = getHeaderName (key); + + checkRestricted (name, HttpHeaderType.Request); + checkAllowed (HttpHeaderType.Request); + + set (name, value, HttpHeaderType.Request); + } + + /// + /// Sets the specified response header to the specified value. + /// + /// + /// + /// One of the enum values. + /// + /// + /// It specifies the response header to set. + /// + /// + /// + /// A that specifies the value of the response header + /// to set. + /// + /// + /// + /// contains an invalid character. + /// + /// + /// -or- + /// + /// + /// is a restricted header. + /// + /// + /// + /// The length of is greater than 65,535 + /// characters. + /// + /// + /// This instance does not allow the response header. + /// + public void Set (HttpResponseHeader header, string value) + { + value = checkValue (value, "value"); + + var key = header.ToString (); + var name = getHeaderName (key); + + checkRestricted (name, HttpHeaderType.Response); + checkAllowed (HttpHeaderType.Response); + + set (name, value, HttpHeaderType.Response); + } + + /// + /// Sets the specified header to the specified value. + /// + /// + /// A that specifies the name of the header to set. + /// + /// + /// A that specifies the value of the header to set. + /// + /// + /// + /// is an empty string. + /// + /// + /// -or- + /// + /// + /// is a string of spaces. + /// + /// + /// -or- + /// + /// + /// contains an invalid character. + /// + /// + /// -or- + /// + /// + /// contains an invalid character. + /// + /// + /// -or- + /// + /// + /// is a restricted header name. + /// + /// + /// + /// is . + /// + /// + /// The length of is greater than 65,535 + /// characters. + /// + /// + /// This instance does not allow the header. + /// + public override void Set (string name, string value) + { + name = checkName (name, "name"); + value = checkValue (value, "value"); + + var headerType = getHeaderType (name); + + checkRestricted (name, headerType); + checkAllowed (headerType); + + set (name, value, headerType); + } + + /// + /// Converts the current instance to an array of byte. + /// + /// + /// An array of converted from a string that represents + /// the current instance. + /// + public byte[] ToByteArray () + { + var s = ToString (); + + return Encoding.UTF8.GetBytes (s); + } + + /// + /// Returns a string that represents the current instance. + /// + /// + /// A that represents all headers in the collection. + /// + public override string ToString () + { + var cnt = Count; + + if (cnt == 0) + return "\r\n"; + + var buff = new StringBuilder (); + + var fmt = "{0}: {1}\r\n"; + + for (var i = 0; i < cnt; i++) + buff.AppendFormat (fmt, GetKey (i), Get (i)); + + buff.Append ("\r\n"); + + return buff.ToString (); + } + + #endregion + + #region Explicit Interface Implementations + + /// + /// Populates the specified instance with + /// the data needed to serialize the current instance. + /// + /// + /// A that holds the serialized object data. + /// + /// + /// A that specifies the destination for + /// the serialization. + /// + /// + /// is . + /// + [ + SecurityPermission ( + SecurityAction.LinkDemand, + Flags = SecurityPermissionFlag.SerializationFormatter, + SerializationFormatter = true + ) + ] + void ISerializable.GetObjectData ( + SerializationInfo serializationInfo, + StreamingContext streamingContext + ) + { + GetObjectData (serializationInfo, streamingContext); + } + + #endregion + } +} diff --git a/Assets/External/websocket-sharp/Net/WebHeaderCollection.cs.meta b/Assets/External/websocket-sharp/Net/WebHeaderCollection.cs.meta new file mode 100644 index 00000000..4cb4b7ab --- /dev/null +++ b/Assets/External/websocket-sharp/Net/WebHeaderCollection.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: c5fbeb32f1e31cd4399025806668ea33 \ No newline at end of file diff --git a/Assets/External/websocket-sharp/Net/WebSockets.meta b/Assets/External/websocket-sharp/Net/WebSockets.meta new file mode 100644 index 00000000..a2c02d46 --- /dev/null +++ b/Assets/External/websocket-sharp/Net/WebSockets.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 1ee49f27a7dc7ec4fb4165d53162c565 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/External/websocket-sharp/Net/WebSockets/HttpListenerWebSocketContext.cs b/Assets/External/websocket-sharp/Net/WebSockets/HttpListenerWebSocketContext.cs new file mode 100644 index 00000000..7e0358d1 --- /dev/null +++ b/Assets/External/websocket-sharp/Net/WebSockets/HttpListenerWebSocketContext.cs @@ -0,0 +1,406 @@ +#region License +/* + * HttpListenerWebSocketContext.cs + * + * The MIT License + * + * Copyright (c) 2012-2025 sta.blockhead + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#endregion + +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.IO; +using System.Net.Sockets; +using System.Security.Principal; + +namespace WebSocketSharp.Net.WebSockets +{ + /// + /// Provides the access to the information in a WebSocket handshake request + /// to a instance. + /// + public class HttpListenerWebSocketContext : WebSocketContext + { + #region Private Fields + + private HttpListenerContext _context; + private WebSocket _websocket; + + #endregion + + #region Internal Constructors + + internal HttpListenerWebSocketContext ( + HttpListenerContext context, + string protocol + ) + { + _context = context; + _websocket = new WebSocket (this, protocol); + } + + #endregion + + #region Internal Properties + + internal Logger Log { + get { + return _context.Listener.Log; + } + } + + internal Socket Socket { + get { + return _context.Connection.Socket; + } + } + + internal Stream Stream { + get { + return _context.Connection.Stream; + } + } + + #endregion + + #region Public Properties + + /// + /// Gets the HTTP cookies included in the handshake request. + /// + /// + /// + /// A that contains + /// the cookies. + /// + /// + /// An empty collection if not included. + /// + /// + public override CookieCollection CookieCollection { + get { + return _context.Request.Cookies; + } + } + + /// + /// Gets the HTTP headers included in the handshake request. + /// + /// + /// A that contains the headers. + /// + public override NameValueCollection Headers { + get { + return _context.Request.Headers; + } + } + + /// + /// Gets the value of the Host header included in the handshake request. + /// + /// + /// + /// A that represents the server host name requested + /// by the client. + /// + /// + /// It includes the port number if provided. + /// + /// + public override string Host { + get { + return _context.Request.UserHostName; + } + } + + /// + /// Gets a value indicating whether the client is authenticated. + /// + /// + /// true if the client is authenticated; otherwise, false. + /// + public override bool IsAuthenticated { + get { + return _context.Request.IsAuthenticated; + } + } + + /// + /// Gets a value indicating whether the handshake request is sent from + /// the local computer. + /// + /// + /// true if the handshake request is sent from the same computer + /// as the server; otherwise, false. + /// + public override bool IsLocal { + get { + return _context.Request.IsLocal; + } + } + + /// + /// Gets a value indicating whether a secure connection is used to send + /// the handshake request. + /// + /// + /// true if the connection is secure; otherwise, false. + /// + public override bool IsSecureConnection { + get { + return _context.Request.IsSecureConnection; + } + } + + /// + /// Gets a value indicating whether the request is a WebSocket handshake + /// request. + /// + /// + /// true if the request is a WebSocket handshake request; otherwise, + /// false. + /// + public override bool IsWebSocketRequest { + get { + return _context.Request.IsWebSocketRequest; + } + } + + /// + /// Gets the value of the Origin header included in the handshake request. + /// + /// + /// + /// A that represents the value of the Origin header. + /// + /// + /// if not included. + /// + /// + public override string Origin { + get { + return _context.Request.Headers["Origin"]; + } + } + + /// + /// Gets the query string included in the handshake request. + /// + /// + /// + /// A that contains the query + /// parameters. + /// + /// + /// An empty collection if not included. + /// + /// + public override NameValueCollection QueryString { + get { + return _context.Request.QueryString; + } + } + + /// + /// Gets the URI requested by the client. + /// + /// + /// + /// A that represents the URI parsed from the request. + /// + /// + /// if the URI cannot be parsed. + /// + /// + public override Uri RequestUri { + get { + return _context.Request.Url; + } + } + + /// + /// Gets the value of the Sec-WebSocket-Key header included in + /// the handshake request. + /// + /// + /// + /// A that represents the value of + /// the Sec-WebSocket-Key header. + /// + /// + /// The value is used to prove that the server received + /// a valid WebSocket handshake request. + /// + /// + /// if not included. + /// + /// + public override string SecWebSocketKey { + get { + return _context.Request.Headers["Sec-WebSocket-Key"]; + } + } + + /// + /// Gets the names of the subprotocols from the Sec-WebSocket-Protocol + /// header included in the handshake request. + /// + /// + /// + /// An + /// instance. + /// + /// + /// It provides an enumerator which supports the iteration over + /// the collection of the names of the subprotocols. + /// + /// + public override IEnumerable SecWebSocketProtocols { + get { + var val = _context.Request.Headers["Sec-WebSocket-Protocol"]; + + if (val == null || val.Length == 0) + yield break; + + foreach (var elm in val.Split (',')) { + var protocol = elm.Trim (); + + if (protocol.Length == 0) + continue; + + yield return protocol; + } + } + } + + /// + /// Gets the value of the Sec-WebSocket-Version header included in + /// the handshake request. + /// + /// + /// + /// A that represents the WebSocket protocol + /// version specified by the client. + /// + /// + /// if not included. + /// + /// + public override string SecWebSocketVersion { + get { + return _context.Request.Headers["Sec-WebSocket-Version"]; + } + } + + /// + /// Gets the endpoint to which the handshake request is sent. + /// + /// + /// A that represents the server + /// IP address and port number. + /// + public override System.Net.IPEndPoint ServerEndPoint { + get { + return _context.Request.LocalEndPoint; + } + } + + /// + /// Gets the client information. + /// + /// + /// + /// A instance that represents identity, + /// authentication, and security roles for the client. + /// + /// + /// if the client is not authenticated. + /// + /// + public override IPrincipal User { + get { + return _context.User; + } + } + + /// + /// Gets the endpoint from which the handshake request is sent. + /// + /// + /// A that represents the client + /// IP address and port number. + /// + public override System.Net.IPEndPoint UserEndPoint { + get { + return _context.Request.RemoteEndPoint; + } + } + + /// + /// Gets the WebSocket interface used for two-way communication between + /// the client and server. + /// + /// + /// A that represents the interface. + /// + public override WebSocket WebSocket { + get { + return _websocket; + } + } + + #endregion + + #region Internal Methods + + internal void Close () + { + _context.Connection.Close (true); + } + + internal void Close (HttpStatusCode code) + { + _context.Response.StatusCode = (int) code; + + _context.Response.Close (); + } + + #endregion + + #region Public Methods + + /// + /// Returns a string that represents the current instance. + /// + /// + /// A that contains the request line and headers + /// included in the handshake request. + /// + public override string ToString () + { + return _context.Request.ToString (); + } + + #endregion + } +} diff --git a/Assets/External/websocket-sharp/Net/WebSockets/HttpListenerWebSocketContext.cs.meta b/Assets/External/websocket-sharp/Net/WebSockets/HttpListenerWebSocketContext.cs.meta new file mode 100644 index 00000000..94c08097 --- /dev/null +++ b/Assets/External/websocket-sharp/Net/WebSockets/HttpListenerWebSocketContext.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: d538316bf856b3443a372a4daa167ec3 \ No newline at end of file diff --git a/Assets/External/websocket-sharp/Net/WebSockets/TcpListenerWebSocketContext.cs b/Assets/External/websocket-sharp/Net/WebSockets/TcpListenerWebSocketContext.cs new file mode 100644 index 00000000..3e8d1277 --- /dev/null +++ b/Assets/External/websocket-sharp/Net/WebSockets/TcpListenerWebSocketContext.cs @@ -0,0 +1,501 @@ +#region License +/* + * TcpListenerWebSocketContext.cs + * + * The MIT License + * + * Copyright (c) 2012-2025 sta.blockhead + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#endregion + +#region Contributors +/* + * Contributors: + * - Liryna + */ +#endregion + +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.IO; +using System.Net.Security; +using System.Net.Sockets; +using System.Security.Principal; +using System.Text; + +namespace WebSocketSharp.Net.WebSockets +{ + /// + /// Provides the access to the information in a WebSocket handshake request + /// to a instance. + /// + internal class TcpListenerWebSocketContext : WebSocketContext + { + #region Private Fields + + private bool _isSecureConnection; + private Logger _log; + private NameValueCollection _queryString; + private HttpRequest _request; + private Uri _requestUri; + private Stream _stream; + private TcpClient _tcpClient; + private IPrincipal _user; + private WebSocket _websocket; + + #endregion + + #region Internal Constructors + + internal TcpListenerWebSocketContext ( + TcpClient tcpClient, + string protocol, + bool secure, + ServerSslConfiguration sslConfig, + Logger log + ) + { + _tcpClient = tcpClient; + _log = log; + + var netStream = tcpClient.GetStream (); + + if (secure) { + var sslStream = new SslStream ( + netStream, + false, + sslConfig.ClientCertificateValidationCallback + ); + + sslStream.AuthenticateAsServer ( + sslConfig.ServerCertificate, + sslConfig.ClientCertificateRequired, + sslConfig.EnabledSslProtocols, + sslConfig.CheckCertificateRevocation + ); + + _isSecureConnection = true; + _stream = sslStream; + } + else { + _stream = netStream; + } + + _request = HttpRequest.ReadRequest (_stream, 90000); + _websocket = new WebSocket (this, protocol); + } + + #endregion + + #region Internal Properties + + internal Logger Log { + get { + return _log; + } + } + + internal Socket Socket { + get { + return _tcpClient.Client; + } + } + + internal Stream Stream { + get { + return _stream; + } + } + + #endregion + + #region Public Properties + + /// + /// Gets the HTTP cookies included in the handshake request. + /// + /// + /// + /// A that contains + /// the cookies. + /// + /// + /// An empty collection if not included. + /// + /// + public override CookieCollection CookieCollection { + get { + return _request.Cookies; + } + } + + /// + /// Gets the HTTP headers included in the handshake request. + /// + /// + /// A that contains the headers. + /// + public override NameValueCollection Headers { + get { + return _request.Headers; + } + } + + /// + /// Gets the value of the Host header included in the handshake request. + /// + /// + /// + /// A that represents the server host name requested + /// by the client. + /// + /// + /// It includes the port number if provided. + /// + /// + public override string Host { + get { + return _request.Headers["Host"]; + } + } + + /// + /// Gets a value indicating whether the client is authenticated. + /// + /// + /// true if the client is authenticated; otherwise, false. + /// + public override bool IsAuthenticated { + get { + return _user != null; + } + } + + /// + /// Gets a value indicating whether the handshake request is sent from + /// the local computer. + /// + /// + /// true if the handshake request is sent from the same computer + /// as the server; otherwise, false. + /// + public override bool IsLocal { + get { + return UserEndPoint.Address.IsLocal (); + } + } + + /// + /// Gets a value indicating whether a secure connection is used to send + /// the handshake request. + /// + /// + /// true if the connection is secure; otherwise, false. + /// + public override bool IsSecureConnection { + get { + return _isSecureConnection; + } + } + + /// + /// Gets a value indicating whether the request is a WebSocket handshake + /// request. + /// + /// + /// true if the request is a WebSocket handshake request; otherwise, + /// false. + /// + public override bool IsWebSocketRequest { + get { + return _request.IsWebSocketRequest; + } + } + + /// + /// Gets the value of the Origin header included in the handshake request. + /// + /// + /// + /// A that represents the value of the Origin header. + /// + /// + /// if not included. + /// + /// + public override string Origin { + get { + return _request.Headers["Origin"]; + } + } + + /// + /// Gets the query string included in the handshake request. + /// + /// + /// + /// A that contains the query + /// parameters. + /// + /// + /// An empty collection if not included. + /// + /// + public override NameValueCollection QueryString { + get { + if (_queryString == null) { + var uri = RequestUri; + var query = uri != null ? uri.Query : null; + + _queryString = QueryStringCollection.Parse (query, Encoding.UTF8); + } + + return _queryString; + } + } + + /// + /// Gets the URI requested by the client. + /// + /// + /// + /// A that represents the URI parsed from the request. + /// + /// + /// if the URI cannot be parsed. + /// + /// + public override Uri RequestUri { + get { + if (_requestUri == null) { + _requestUri = HttpUtility.CreateRequestUrl ( + _request.RequestTarget, + _request.Headers["Host"], + _request.IsWebSocketRequest, + _isSecureConnection + ); + } + + return _requestUri; + } + } + + /// + /// Gets the value of the Sec-WebSocket-Key header included in + /// the handshake request. + /// + /// + /// + /// A that represents the value of + /// the Sec-WebSocket-Key header. + /// + /// + /// The value is used to prove that the server received + /// a valid WebSocket handshake request. + /// + /// + /// if not included. + /// + /// + public override string SecWebSocketKey { + get { + return _request.Headers["Sec-WebSocket-Key"]; + } + } + + /// + /// Gets the names of the subprotocols from the Sec-WebSocket-Protocol + /// header included in the handshake request. + /// + /// + /// + /// An + /// instance. + /// + /// + /// It provides an enumerator which supports the iteration over + /// the collection of the names of the subprotocols. + /// + /// + public override IEnumerable SecWebSocketProtocols { + get { + var val = _request.Headers["Sec-WebSocket-Protocol"]; + + if (val == null || val.Length == 0) + yield break; + + foreach (var elm in val.Split (',')) { + var protocol = elm.Trim (); + + if (protocol.Length == 0) + continue; + + yield return protocol; + } + } + } + + /// + /// Gets the value of the Sec-WebSocket-Version header included in + /// the handshake request. + /// + /// + /// + /// A that represents the WebSocket protocol + /// version specified by the client. + /// + /// + /// if not included. + /// + /// + public override string SecWebSocketVersion { + get { + return _request.Headers["Sec-WebSocket-Version"]; + } + } + + /// + /// Gets the endpoint to which the handshake request is sent. + /// + /// + /// A that represents the server + /// IP address and port number. + /// + public override System.Net.IPEndPoint ServerEndPoint { + get { + return (System.Net.IPEndPoint) _tcpClient.Client.LocalEndPoint; + } + } + + /// + /// Gets the client information. + /// + /// + /// + /// A instance that represents identity, + /// authentication, and security roles for the client. + /// + /// + /// if the client is not authenticated. + /// + /// + public override IPrincipal User { + get { + return _user; + } + } + + /// + /// Gets the endpoint from which the handshake request is sent. + /// + /// + /// A that represents the client + /// IP address and port number. + /// + public override System.Net.IPEndPoint UserEndPoint { + get { + return (System.Net.IPEndPoint) _tcpClient.Client.RemoteEndPoint; + } + } + + /// + /// Gets the WebSocket interface used for two-way communication between + /// the client and server. + /// + /// + /// A that represents the interface. + /// + public override WebSocket WebSocket { + get { + return _websocket; + } + } + + #endregion + + #region Internal Methods + + internal void Close () + { + _stream.Close (); + _tcpClient.Close (); + } + + internal void Close (HttpStatusCode code) + { + HttpResponse.CreateCloseResponse (code).WriteTo (_stream); + + _stream.Close (); + _tcpClient.Close (); + } + + internal void SendAuthenticationChallenge (string challenge) + { + HttpResponse.CreateUnauthorizedResponse (challenge).WriteTo (_stream); + + _request = HttpRequest.ReadRequest (_stream, 15000); + } + + internal bool SetUser ( + AuthenticationSchemes scheme, + string realm, + Func credentialsFinder + ) + { + var user = HttpUtility.CreateUser ( + _request.Headers["Authorization"], + scheme, + realm, + _request.HttpMethod, + credentialsFinder + ); + + if (user == null) + return false; + + if (!user.Identity.IsAuthenticated) + return false; + + _user = user; + + return true; + } + + #endregion + + #region Public Methods + + /// + /// Returns a string that represents the current instance. + /// + /// + /// A that contains the request line and headers + /// included in the handshake request. + /// + public override string ToString () + { + return _request.ToString (); + } + + #endregion + } +} diff --git a/Assets/External/websocket-sharp/Net/WebSockets/TcpListenerWebSocketContext.cs.meta b/Assets/External/websocket-sharp/Net/WebSockets/TcpListenerWebSocketContext.cs.meta new file mode 100644 index 00000000..28ea5e98 --- /dev/null +++ b/Assets/External/websocket-sharp/Net/WebSockets/TcpListenerWebSocketContext.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 7786101eb71dbfb43866febd1c9c8179 \ No newline at end of file diff --git a/Assets/External/websocket-sharp/Net/WebSockets/WebSocketContext.cs b/Assets/External/websocket-sharp/Net/WebSockets/WebSocketContext.cs new file mode 100644 index 00000000..84841f5b --- /dev/null +++ b/Assets/External/websocket-sharp/Net/WebSockets/WebSocketContext.cs @@ -0,0 +1,224 @@ +#region License +/* + * WebSocketContext.cs + * + * The MIT License + * + * Copyright (c) 2012-2022 sta.blockhead + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#endregion + +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Security.Principal; + +namespace WebSocketSharp.Net.WebSockets +{ + /// + /// Exposes the access to the information in a WebSocket handshake request. + /// + /// + /// This class is an abstract class. + /// + public abstract class WebSocketContext + { + #region Protected Constructors + + /// + /// Initializes a new instance of the class. + /// + protected WebSocketContext () + { + } + + #endregion + + #region Public Properties + + /// + /// Gets the HTTP cookies included in the handshake request. + /// + /// + /// A that contains + /// the cookies. + /// + public abstract CookieCollection CookieCollection { get; } + + /// + /// Gets the HTTP headers included in the handshake request. + /// + /// + /// A that contains the headers. + /// + public abstract NameValueCollection Headers { get; } + + /// + /// Gets the value of the Host header included in the handshake request. + /// + /// + /// A that represents the server host name requested + /// by the client. + /// + public abstract string Host { get; } + + /// + /// Gets a value indicating whether the client is authenticated. + /// + /// + /// true if the client is authenticated; otherwise, false. + /// + public abstract bool IsAuthenticated { get; } + + /// + /// Gets a value indicating whether the handshake request is sent from + /// the local computer. + /// + /// + /// true if the handshake request is sent from the same computer + /// as the server; otherwise, false. + /// + public abstract bool IsLocal { get; } + + /// + /// Gets a value indicating whether a secure connection is used to send + /// the handshake request. + /// + /// + /// true if the connection is secure; otherwise, false. + /// + public abstract bool IsSecureConnection { get; } + + /// + /// Gets a value indicating whether the request is a WebSocket handshake + /// request. + /// + /// + /// true if the request is a WebSocket handshake request; otherwise, + /// false. + /// + public abstract bool IsWebSocketRequest { get; } + + /// + /// Gets the value of the Origin header included in the handshake request. + /// + /// + /// A that represents the value of the Origin header. + /// + public abstract string Origin { get; } + + /// + /// Gets the query string included in the handshake request. + /// + /// + /// A that contains the query parameters. + /// + public abstract NameValueCollection QueryString { get; } + + /// + /// Gets the URI requested by the client. + /// + /// + /// A that represents the URI parsed from the request. + /// + public abstract Uri RequestUri { get; } + + /// + /// Gets the value of the Sec-WebSocket-Key header included in + /// the handshake request. + /// + /// + /// + /// A that represents the value of + /// the Sec-WebSocket-Key header. + /// + /// + /// The value is used to prove that the server received + /// a valid WebSocket handshake request. + /// + /// + public abstract string SecWebSocketKey { get; } + + /// + /// Gets the names of the subprotocols from the Sec-WebSocket-Protocol + /// header included in the handshake request. + /// + /// + /// + /// An + /// instance. + /// + /// + /// It provides an enumerator which supports the iteration over + /// the collection of the names of the subprotocols. + /// + /// + public abstract IEnumerable SecWebSocketProtocols { get; } + + /// + /// Gets the value of the Sec-WebSocket-Version header included in + /// the handshake request. + /// + /// + /// A that represents the WebSocket protocol + /// version specified by the client. + /// + public abstract string SecWebSocketVersion { get; } + + /// + /// Gets the endpoint to which the handshake request is sent. + /// + /// + /// A that represents the server + /// IP address and port number. + /// + public abstract System.Net.IPEndPoint ServerEndPoint { get; } + + /// + /// Gets the client information. + /// + /// + /// A instance that represents identity, + /// authentication, and security roles for the client. + /// + public abstract IPrincipal User { get; } + + /// + /// Gets the endpoint from which the handshake request is sent. + /// + /// + /// A that represents the client + /// IP address and port number. + /// + public abstract System.Net.IPEndPoint UserEndPoint { get; } + + /// + /// Gets the WebSocket interface used for two-way communication between + /// the client and server. + /// + /// + /// A that represents the interface. + /// + public abstract WebSocket WebSocket { get; } + + #endregion + } +} diff --git a/Assets/External/websocket-sharp/Net/WebSockets/WebSocketContext.cs.meta b/Assets/External/websocket-sharp/Net/WebSockets/WebSocketContext.cs.meta new file mode 100644 index 00000000..7f0c5bca --- /dev/null +++ b/Assets/External/websocket-sharp/Net/WebSockets/WebSocketContext.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 73dc73292914bc443a4a0157217617bc \ No newline at end of file diff --git a/Assets/External/websocket-sharp/Opcode.cs b/Assets/External/websocket-sharp/Opcode.cs new file mode 100644 index 00000000..06da1b5e --- /dev/null +++ b/Assets/External/websocket-sharp/Opcode.cs @@ -0,0 +1,68 @@ +#region License +/* + * Opcode.cs + * + * The MIT License + * + * Copyright (c) 2012-2025 sta.blockhead + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#endregion + +using System; + +namespace WebSocketSharp +{ + /// + /// Indicates the WebSocket frame type. + /// + /// + /// The values of this enumeration are defined in + /// + /// Section 5.2 of RFC 6455. + /// + internal enum Opcode + { + /// + /// Equivalent to numeric value 0. Indicates continuation frame. + /// + Cont = 0x0, + /// + /// Equivalent to numeric value 1. Indicates text frame. + /// + Text = 0x1, + /// + /// Equivalent to numeric value 2. Indicates binary frame. + /// + Binary = 0x2, + /// + /// Equivalent to numeric value 8. Indicates connection close frame. + /// + Close = 0x8, + /// + /// Equivalent to numeric value 9. Indicates ping frame. + /// + Ping = 0x9, + /// + /// Equivalent to numeric value 10. Indicates pong frame. + /// + Pong = 0xa + } +} diff --git a/Assets/External/websocket-sharp/Opcode.cs.meta b/Assets/External/websocket-sharp/Opcode.cs.meta new file mode 100644 index 00000000..b8e1abb8 --- /dev/null +++ b/Assets/External/websocket-sharp/Opcode.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: befd25291fd922943ab1a42d89fc133d \ No newline at end of file diff --git a/Assets/External/websocket-sharp/PayloadData.cs b/Assets/External/websocket-sharp/PayloadData.cs new file mode 100644 index 00000000..e01fcd27 --- /dev/null +++ b/Assets/External/websocket-sharp/PayloadData.cs @@ -0,0 +1,212 @@ +#region License +/* + * PayloadData.cs + * + * The MIT License + * + * Copyright (c) 2012-2025 sta.blockhead + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#endregion + +using System; +using System.Collections; +using System.Collections.Generic; + +namespace WebSocketSharp +{ + internal class PayloadData : IEnumerable + { + #region Private Fields + + private byte[] _data; + private static readonly byte[] _emptyBytes; + private long _extDataLength; + private long _length; + + #endregion + + #region Public Fields + + /// + /// Represents the empty payload data. + /// + public static readonly PayloadData Empty; + + /// + /// Represents the allowable max length of payload data. + /// + /// + /// + /// A is thrown when the length of + /// incoming payload data is greater than the value of this field. + /// + /// + /// If you would like to change the value of this field, it must be + /// a number between and + /// inclusive. + /// + /// + public static readonly ulong MaxLength; + + #endregion + + #region Static Constructor + + static PayloadData () + { + _emptyBytes = new byte[0]; + + Empty = new PayloadData (_emptyBytes, 0); + MaxLength = Int64.MaxValue; + } + + #endregion + + #region Internal Constructors + + internal PayloadData (byte[] data) + : this (data, data.LongLength) + { + } + + internal PayloadData (byte[] data, long length) + { + _data = data; + _length = length; + } + + internal PayloadData (ushort code, string reason) + { + _data = code.Append (reason); + _length = _data.LongLength; + } + + #endregion + + #region Internal Properties + + internal ushort Code { + get { + return _length >= 2 + ? _data.SubArray (0, 2).ToUInt16 (ByteOrder.Big) + : (ushort) 1005; + } + } + + internal long ExtensionDataLength { + get { + return _extDataLength; + } + + set { + _extDataLength = value; + } + } + + internal bool HasReservedCode { + get { + return _length >= 2 && Code.IsReservedStatusCode (); + } + } + + internal string Reason { + get { + if (_length <= 2) + return String.Empty; + + var bytes = _data.SubArray (2, _length - 2); + + string reason; + + return bytes.TryGetUTF8DecodedString (out reason) + ? reason + : String.Empty; + } + } + + #endregion + + #region Public Properties + + public byte[] ApplicationData { + get { + return _extDataLength > 0 + ? _data.SubArray (_extDataLength, _length - _extDataLength) + : _data; + } + } + + public byte[] ExtensionData { + get { + return _extDataLength > 0 + ? _data.SubArray (0, _extDataLength) + : _emptyBytes; + } + } + + public ulong Length { + get { + return (ulong) _length; + } + } + + #endregion + + #region Internal Methods + + internal void Mask (byte[] key) + { + for (long i = 0; i < _length; i++) + _data[i] = (byte) (_data[i] ^ key[i % 4]); + } + + #endregion + + #region Public Methods + + public IEnumerator GetEnumerator () + { + foreach (var b in _data) + yield return b; + } + + public byte[] ToArray () + { + return _data; + } + + public override string ToString () + { + return BitConverter.ToString (_data); + } + + #endregion + + #region Explicit Interface Implementations + + IEnumerator IEnumerable.GetEnumerator () + { + return GetEnumerator (); + } + + #endregion + } +} diff --git a/Assets/External/websocket-sharp/PayloadData.cs.meta b/Assets/External/websocket-sharp/PayloadData.cs.meta new file mode 100644 index 00000000..db3304a9 --- /dev/null +++ b/Assets/External/websocket-sharp/PayloadData.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 3a3bad22a27c2534fa57956a869b0eec \ No newline at end of file diff --git a/Assets/External/websocket-sharp/Rsv.cs b/Assets/External/websocket-sharp/Rsv.cs new file mode 100644 index 00000000..c2a4dea7 --- /dev/null +++ b/Assets/External/websocket-sharp/Rsv.cs @@ -0,0 +1,53 @@ +#region License +/* + * Rsv.cs + * + * The MIT License + * + * Copyright (c) 2012-2025 sta.blockhead + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#endregion + +using System; + +namespace WebSocketSharp +{ + /// + /// Indicates whether each RSV (RSV1, RSV2, and RSV3) of a WebSocket + /// frame is non-zero. + /// + /// + /// The values of this enumeration are defined in + /// + /// Section 5.2 of RFC 6455. + /// + internal enum Rsv + { + /// + /// Equivalent to numeric value 0. Indicates zero. + /// + Off = 0x0, + /// + /// Equivalent to numeric value 1. Indicates non-zero. + /// + On = 0x1 + } +} diff --git a/Assets/External/websocket-sharp/Rsv.cs.meta b/Assets/External/websocket-sharp/Rsv.cs.meta new file mode 100644 index 00000000..a364479a --- /dev/null +++ b/Assets/External/websocket-sharp/Rsv.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 1d6533743cf8690459edcbaea690d25c \ No newline at end of file diff --git a/Assets/External/websocket-sharp/Server.meta b/Assets/External/websocket-sharp/Server.meta new file mode 100644 index 00000000..56fc04d2 --- /dev/null +++ b/Assets/External/websocket-sharp/Server.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 124757fa40da5044786652c061a97954 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/External/websocket-sharp/Server/HttpRequestEventArgs.cs b/Assets/External/websocket-sharp/Server/HttpRequestEventArgs.cs new file mode 100644 index 00000000..45af3c1e --- /dev/null +++ b/Assets/External/websocket-sharp/Server/HttpRequestEventArgs.cs @@ -0,0 +1,266 @@ +#region License +/* + * HttpRequestEventArgs.cs + * + * The MIT License + * + * Copyright (c) 2012-2024 sta.blockhead + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#endregion + +using System; +using System.IO; +using System.Security.Principal; +using System.Text; +using WebSocketSharp.Net; + +namespace WebSocketSharp.Server +{ + /// + /// Represents the event data for the HTTP request events of the + /// class. + /// + /// + /// + /// An HTTP request event occurs when the + /// instance receives an HTTP request. + /// + /// + /// You should access the property if you would + /// like to get the request data sent from a client. + /// + /// + /// And you should access the property if you + /// would like to get the response data to return to the client. + /// + /// + public class HttpRequestEventArgs : EventArgs + { + #region Private Fields + + private HttpListenerContext _context; + private string _docRootPath; + + #endregion + + #region Internal Constructors + + internal HttpRequestEventArgs ( + HttpListenerContext context, + string documentRootPath + ) + { + _context = context; + _docRootPath = documentRootPath; + } + + #endregion + + #region Public Properties + + /// + /// Gets the request data sent from a client. + /// + /// + /// A that provides the methods and + /// properties for the request data. + /// + public HttpListenerRequest Request { + get { + return _context.Request; + } + } + + /// + /// Gets the response data to return to the client. + /// + /// + /// A that provides the methods and + /// properties for the response data. + /// + public HttpListenerResponse Response { + get { + return _context.Response; + } + } + + /// + /// Gets the information for the client. + /// + /// + /// + /// A instance that represents identity, + /// authentication scheme, and security roles for the client. + /// + /// + /// if the client is not authenticated. + /// + /// + public IPrincipal User { + get { + return _context.User; + } + } + + #endregion + + #region Private Methods + + private string createFilePath (string childPath) + { + childPath = childPath.TrimStart ('/', '\\'); + + return new StringBuilder (_docRootPath, 32) + .AppendFormat ("/{0}", childPath) + .ToString () + .Replace ('\\', '/'); + } + + private static bool tryReadFile (string path, out byte[] contents) + { + contents = null; + + if (!File.Exists (path)) + return false; + + try { + contents = File.ReadAllBytes (path); + } + catch { + return false; + } + + return true; + } + + #endregion + + #region Public Methods + + /// + /// Reads the specified file from the document folder of the + /// class. + /// + /// + /// + /// An array of that receives the contents of + /// the file. + /// + /// + /// if the read has failed. + /// + /// + /// + /// A that specifies a virtual path to find + /// the file from the document folder. + /// + /// + /// + /// is an empty string. + /// + /// + /// -or- + /// + /// + /// contains "..". + /// + /// + /// + /// is . + /// + public byte[] ReadFile (string path) + { + if (path == null) + throw new ArgumentNullException ("path"); + + if (path.Length == 0) + throw new ArgumentException ("An empty string.", "path"); + + if (path.Contains ("..")) { + var msg = "It contains \"..\"."; + + throw new ArgumentException (msg, "path"); + } + + path = createFilePath (path); + byte[] contents; + + tryReadFile (path, out contents); + + return contents; + } + + /// + /// Tries to read the specified file from the document folder of + /// the class. + /// + /// + /// true if the try has succeeded; otherwise, false. + /// + /// + /// A that specifies a virtual path to find + /// the file from the document folder. + /// + /// + /// + /// When this method returns, an array of that + /// receives the contents of the file. + /// + /// + /// if the read has failed. + /// + /// + /// + /// + /// is an empty string. + /// + /// + /// -or- + /// + /// + /// contains "..". + /// + /// + /// + /// is . + /// + public bool TryReadFile (string path, out byte[] contents) + { + if (path == null) + throw new ArgumentNullException ("path"); + + if (path.Length == 0) + throw new ArgumentException ("An empty string.", "path"); + + if (path.Contains ("..")) { + var msg = "It contains \"..\"."; + + throw new ArgumentException (msg, "path"); + } + + path = createFilePath (path); + + return tryReadFile (path, out contents); + } + + #endregion + } +} diff --git a/Assets/External/websocket-sharp/Server/HttpRequestEventArgs.cs.meta b/Assets/External/websocket-sharp/Server/HttpRequestEventArgs.cs.meta new file mode 100644 index 00000000..d0b37536 --- /dev/null +++ b/Assets/External/websocket-sharp/Server/HttpRequestEventArgs.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 15241bd4f65d38f438d262a051f78d44 \ No newline at end of file diff --git a/Assets/External/websocket-sharp/Server/HttpServer.cs b/Assets/External/websocket-sharp/Server/HttpServer.cs new file mode 100644 index 00000000..20910998 --- /dev/null +++ b/Assets/External/websocket-sharp/Server/HttpServer.cs @@ -0,0 +1,1334 @@ +#region License +/* + * HttpServer.cs + * + * The MIT License + * + * Copyright (c) 2012-2024 sta.blockhead + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#endregion + +#region Contributors +/* + * Contributors: + * - Juan Manuel Lallana + * - Liryna + * - Rohan Singh + */ +#endregion + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Net.Sockets; +using System.Security.Cryptography.X509Certificates; +using System.Security.Principal; +using System.Text; +using System.Threading; +using WebSocketSharp.Net; +using WebSocketSharp.Net.WebSockets; + +namespace WebSocketSharp.Server +{ + /// + /// Provides a simple HTTP server. + /// + /// + /// + /// The server supports HTTP/1.1 version request and response. + /// + /// + /// Also the server allows to accept WebSocket handshake requests. + /// + /// + /// This class can provide multiple WebSocket services. + /// + /// + public class HttpServer + { + #region Private Fields + + private System.Net.IPAddress _address; + private string _docRootPath; + private bool _isSecure; + private HttpListener _listener; + private Logger _log; + private int _port; + private Thread _receiveThread; + private WebSocketServiceManager _services; + private volatile ServerState _state; + private object _sync; + + #endregion + + #region Public Constructors + + /// + /// Initializes a new instance of the class. + /// + /// + /// The new instance listens for incoming requests on + /// and port 80. + /// + public HttpServer () + { + init ("*", System.Net.IPAddress.Any, 80, false); + } + + /// + /// Initializes a new instance of the class with + /// the specified port. + /// + /// + /// + /// The new instance listens for incoming requests on + /// and . + /// + /// + /// It provides secure connections if is 443. + /// + /// + /// + /// An that specifies the number of the port on which + /// to listen. + /// + /// + /// is less than 1 or greater than 65535. + /// + public HttpServer (int port) + : this (port, port == 443) + { + } + + /// + /// Initializes a new instance of the class with + /// the specified URL. + /// + /// + /// + /// The new instance listens for incoming requests on the IP address and + /// port of . + /// + /// + /// Either port 80 or 443 is used if includes + /// no port. Port 443 is used if the scheme of + /// is https; otherwise, port 80 is used. + /// + /// + /// The new instance provides secure connections if the scheme of + /// is https. + /// + /// + /// + /// A that specifies the HTTP URL of the server. + /// + /// + /// + /// is an empty string. + /// + /// + /// -or- + /// + /// + /// is invalid. + /// + /// + /// + /// is . + /// + public HttpServer (string url) + { + if (url == null) + throw new ArgumentNullException ("url"); + + if (url.Length == 0) + throw new ArgumentException ("An empty string.", "url"); + + Uri uri; + string msg; + + if (!tryCreateUri (url, out uri, out msg)) + throw new ArgumentException (msg, "url"); + + var host = uri.GetDnsSafeHost (true); + var addr = host.ToIPAddress (); + + if (addr == null) { + msg = "The host part could not be converted to an IP address."; + + throw new ArgumentException (msg, "url"); + } + + if (!addr.IsLocal ()) { + msg = "The IP address of the host is not a local IP address."; + + throw new ArgumentException (msg, "url"); + } + + init (host, addr, uri.Port, uri.Scheme == "https"); + } + + /// + /// Initializes a new instance of the class with + /// the specified port and boolean if secure or not. + /// + /// + /// The new instance listens for incoming requests on + /// and . + /// + /// + /// An that specifies the number of the port on which + /// to listen. + /// + /// + /// A : true if the new instance provides + /// secure connections; otherwise, false. + /// + /// + /// is less than 1 or greater than 65535. + /// + public HttpServer (int port, bool secure) + { + if (!port.IsPortNumber ()) { + var msg = "Less than 1 or greater than 65535."; + + throw new ArgumentOutOfRangeException ("port", msg); + } + + init ("*", System.Net.IPAddress.Any, port, secure); + } + + /// + /// Initializes a new instance of the class with + /// the specified IP address and port. + /// + /// + /// + /// The new instance listens for incoming requests on + /// and . + /// + /// + /// It provides secure connections if is 443. + /// + /// + /// + /// A that specifies the local IP + /// address on which to listen. + /// + /// + /// An that specifies the number of the port on which + /// to listen. + /// + /// + /// is not a local IP address. + /// + /// + /// is . + /// + /// + /// is less than 1 or greater than 65535. + /// + public HttpServer (System.Net.IPAddress address, int port) + : this (address, port, port == 443) + { + } + + /// + /// Initializes a new instance of the class with + /// the specified IP address, port, and boolean if secure or not. + /// + /// + /// The new instance listens for incoming requests on + /// and . + /// + /// + /// A that specifies the local IP + /// address on which to listen. + /// + /// + /// An that specifies the number of the port on which + /// to listen. + /// + /// + /// A : true if the new instance provides + /// secure connections; otherwise, false. + /// + /// + /// is not a local IP address. + /// + /// + /// is . + /// + /// + /// is less than 1 or greater than 65535. + /// + public HttpServer (System.Net.IPAddress address, int port, bool secure) + { + if (address == null) + throw new ArgumentNullException ("address"); + + if (!address.IsLocal ()) { + var msg = "Not a local IP address."; + + throw new ArgumentException (msg, "address"); + } + + if (!port.IsPortNumber ()) { + var msg = "Less than 1 or greater than 65535."; + + throw new ArgumentOutOfRangeException ("port", msg); + } + + init (address.ToString (true), address, port, secure); + } + + #endregion + + #region Public Properties + + /// + /// Gets the IP address of the server. + /// + /// + /// A that represents the local IP + /// address on which to listen for incoming requests. + /// + public System.Net.IPAddress Address { + get { + return _address; + } + } + + /// + /// Gets or sets the scheme used to authenticate the clients. + /// + /// + /// The set operation works if the current state of the server is + /// Ready or Stop. + /// + /// + /// + /// One of the + /// enum values. + /// + /// + /// It represents the scheme used to authenticate the clients. + /// + /// + /// The default value is + /// . + /// + /// + public AuthenticationSchemes AuthenticationSchemes { + get { + return _listener.AuthenticationSchemes; + } + + set { + lock (_sync) { + if (!canSet ()) + return; + + _listener.AuthenticationSchemes = value; + } + } + } + + /// + /// Gets or sets the path to the document folder of the server. + /// + /// + /// The set operation works if the current state of the server is + /// Ready or Stop. + /// + /// + /// + /// A that represents a path to the folder + /// from which to find the requested file. + /// + /// + /// / or \ is trimmed from the end of the value if present. + /// + /// + /// The default value is "./Public". + /// + /// + /// + /// + /// The value specified for a set operation is an empty string. + /// + /// + /// -or- + /// + /// + /// The value specified for a set operation is an absolute root. + /// + /// + /// -or- + /// + /// + /// The value specified for a set operation is an invalid path string. + /// + /// + /// + /// The value specified for a set operation is . + /// + public string DocumentRootPath { + get { + return _docRootPath; + } + + set { + if (value == null) + throw new ArgumentNullException ("value"); + + if (value.Length == 0) + throw new ArgumentException ("An empty string.", "value"); + + value = value.TrimSlashOrBackslashFromEnd (); + + if (value == "/") + throw new ArgumentException ("An absolute root.", "value"); + + if (value == "\\") + throw new ArgumentException ("An absolute root.", "value"); + + if (value.Length == 2 && value[1] == ':') + throw new ArgumentException ("An absolute root.", "value"); + + string full = null; + + try { + full = Path.GetFullPath (value); + } + catch (Exception ex) { + throw new ArgumentException ("An invalid path string.", "value", ex); + } + + if (full == "/") + throw new ArgumentException ("An absolute root.", "value"); + + full = full.TrimSlashOrBackslashFromEnd (); + + if (full.Length == 2 && full[1] == ':') + throw new ArgumentException ("An absolute root.", "value"); + + lock (_sync) { + if (!canSet ()) + return; + + _docRootPath = value; + } + } + } + + /// + /// Gets a value indicating whether the server has started. + /// + /// + /// true if the server has started; otherwise, false. + /// + public bool IsListening { + get { + return _state == ServerState.Start; + } + } + + /// + /// Gets a value indicating whether the server provides secure connections. + /// + /// + /// true if the server provides secure connections; otherwise, + /// false. + /// + public bool IsSecure { + get { + return _isSecure; + } + } + + /// + /// Gets or sets a value indicating whether the server cleans up + /// the inactive sessions periodically. + /// + /// + /// The set operation works if the current state of the server is + /// Ready or Stop. + /// + /// + /// + /// true if the server cleans up the inactive sessions + /// every 60 seconds; otherwise, false. + /// + /// + /// The default value is false. + /// + /// + public bool KeepClean { + get { + return _services.KeepClean; + } + + set { + _services.KeepClean = value; + } + } + + /// + /// Gets the logging function for the server. + /// + /// + /// The default logging level is . + /// + /// + /// A that provides the logging function. + /// + public Logger Log { + get { + return _log; + } + } + + /// + /// Gets the port of the server. + /// + /// + /// An that represents the number of the port on which + /// to listen for incoming requests. + /// + public int Port { + get { + return _port; + } + } + + /// + /// Gets or sets the name of the realm associated with the server. + /// + /// + /// The set operation works if the current state of the server is + /// Ready or Stop. + /// + /// + /// + /// A that represents the name of the realm. + /// + /// + /// "SECRET AREA" is used as the name of the realm if the value is + /// or an empty string. + /// + /// + /// The default value is . + /// + /// + public string Realm { + get { + return _listener.Realm; + } + + set { + lock (_sync) { + if (!canSet ()) + return; + + _listener.Realm = value; + } + } + } + + /// + /// Gets or sets a value indicating whether the server is allowed to + /// be bound to an address that is already in use. + /// + /// + /// + /// You should set this property to true if you would like to + /// resolve to wait for socket in TIME_WAIT state. + /// + /// + /// The set operation works if the current state of the server is + /// Ready or Stop. + /// + /// + /// + /// + /// true if the server is allowed to be bound to an address + /// that is already in use; otherwise, false. + /// + /// + /// The default value is false. + /// + /// + public bool ReuseAddress { + get { + return _listener.ReuseAddress; + } + + set { + lock (_sync) { + if (!canSet ()) + return; + + _listener.ReuseAddress = value; + } + } + } + + /// + /// Gets the configuration for secure connection. + /// + /// + /// The configuration is used when the server attempts to start, + /// so it must be configured before the start method is called. + /// + /// + /// A that represents the + /// configuration used to provide secure connections. + /// + /// + /// The server does not provide secure connections. + /// + public ServerSslConfiguration SslConfiguration { + get { + if (!_isSecure) { + var msg = "The server does not provide secure connections."; + + throw new InvalidOperationException (msg); + } + + return _listener.SslConfiguration; + } + } + + /// + /// Gets or sets the delegate called to find the credentials for + /// an identity used to authenticate a client. + /// + /// + /// The set operation works if the current state of the server is + /// Ready or Stop. + /// + /// + /// + /// A + /// delegate. + /// + /// + /// It represents the delegate called when the server finds + /// the credentials used to authenticate a client. + /// + /// + /// It must return if the credentials + /// are not found. + /// + /// + /// if not necessary. + /// + /// + /// The default value is . + /// + /// + public Func UserCredentialsFinder { + get { + return _listener.UserCredentialsFinder; + } + + set { + lock (_sync) { + if (!canSet ()) + return; + + _listener.UserCredentialsFinder = value; + } + } + } + + /// + /// Gets or sets the time to wait for the response to the WebSocket + /// Ping or Close. + /// + /// + /// The set operation works if the current state of the server is + /// Ready or Stop. + /// + /// + /// + /// A that represents the time to wait for + /// the response. + /// + /// + /// The default value is the same as 1 second. + /// + /// + /// + /// The value specified for a set operation is zero or less. + /// + public TimeSpan WaitTime { + get { + return _services.WaitTime; + } + + set { + _services.WaitTime = value; + } + } + + /// + /// Gets the management function for the WebSocket services provided by + /// the server. + /// + /// + /// A that manages the WebSocket + /// services provided by the server. + /// + public WebSocketServiceManager WebSocketServices { + get { + return _services; + } + } + + #endregion + + #region Public Events + + /// + /// Occurs when the server receives an HTTP CONNECT request. + /// + public event EventHandler OnConnect; + + /// + /// Occurs when the server receives an HTTP DELETE request. + /// + public event EventHandler OnDelete; + + /// + /// Occurs when the server receives an HTTP GET request. + /// + public event EventHandler OnGet; + + /// + /// Occurs when the server receives an HTTP HEAD request. + /// + public event EventHandler OnHead; + + /// + /// Occurs when the server receives an HTTP OPTIONS request. + /// + public event EventHandler OnOptions; + + /// + /// Occurs when the server receives an HTTP POST request. + /// + public event EventHandler OnPost; + + /// + /// Occurs when the server receives an HTTP PUT request. + /// + public event EventHandler OnPut; + + /// + /// Occurs when the server receives an HTTP TRACE request. + /// + public event EventHandler OnTrace; + + #endregion + + #region Private Methods + + private void abort () + { + lock (_sync) { + if (_state != ServerState.Start) + return; + + _state = ServerState.ShuttingDown; + } + + try { + _services.Stop (1006, String.Empty); + } + catch (Exception ex) { + _log.Fatal (ex.Message); + _log.Debug (ex.ToString ()); + } + + try { + _listener.Abort (); + } + catch (Exception ex) { + _log.Fatal (ex.Message); + _log.Debug (ex.ToString ()); + } + + _state = ServerState.Stop; + } + + private bool canSet () + { + return _state == ServerState.Ready || _state == ServerState.Stop; + } + + private bool checkCertificate (out string message) + { + message = null; + + var byUser = _listener.SslConfiguration.ServerCertificate != null; + + var path = _listener.CertificateFolderPath; + var withPort = EndPointListener.CertificateExists (_port, path); + + var either = byUser || withPort; + + if (!either) { + message = "There is no server certificate for secure connection."; + + return false; + } + + var both = byUser && withPort; + + if (both) { + var msg = "The server certificate associated with the port is used."; + + _log.Warn (msg); + } + + return true; + } + + private static HttpListener createListener ( + string hostname, + int port, + bool secure + ) + { + var ret = new HttpListener (); + + var fmt = "{0}://{1}:{2}/"; + var schm = secure ? "https" : "http"; + var pref = String.Format (fmt, schm, hostname, port); + + ret.Prefixes.Add (pref); + + return ret; + } + + private void init ( + string hostname, + System.Net.IPAddress address, + int port, + bool secure + ) + { + _address = address; + _port = port; + _isSecure = secure; + + _docRootPath = "./Public"; + _listener = createListener (hostname, port, secure); + _log = _listener.Log; + _services = new WebSocketServiceManager (_log); + _sync = new object (); + } + + private void processRequest (HttpListenerContext context) + { + var method = context.Request.HttpMethod; + var evt = method == "GET" + ? OnGet + : method == "HEAD" + ? OnHead + : method == "POST" + ? OnPost + : method == "PUT" + ? OnPut + : method == "DELETE" + ? OnDelete + : method == "CONNECT" + ? OnConnect + : method == "OPTIONS" + ? OnOptions + : method == "TRACE" + ? OnTrace + : null; + + if (evt == null) { + context.ErrorStatusCode = 501; + + context.SendError (); + + return; + } + + var e = new HttpRequestEventArgs (context, _docRootPath); + + evt (this, e); + + context.Response.Close (); + } + + private void processRequest (HttpListenerWebSocketContext context) + { + var uri = context.RequestUri; + + if (uri == null) { + context.Close (HttpStatusCode.BadRequest); + + return; + } + + var path = uri.AbsolutePath; + + if (path.IndexOfAny (new[] { '%', '+' }) > -1) + path = HttpUtility.UrlDecode (path, Encoding.UTF8); + + WebSocketServiceHost host; + + if (!_services.InternalTryGetServiceHost (path, out host)) { + context.Close (HttpStatusCode.NotImplemented); + + return; + } + + host.StartSession (context); + } + + private void receiveRequest () + { + while (true) { + HttpListenerContext ctx = null; + + try { + ctx = _listener.GetContext (); + + ThreadPool.QueueUserWorkItem ( + state => { + try { + if (ctx.Request.IsUpgradeRequest ("websocket")) { + processRequest (ctx.GetWebSocketContext (null)); + + return; + } + + processRequest (ctx); + } + catch (Exception ex) { + _log.Error (ex.Message); + _log.Debug (ex.ToString ()); + + ctx.Connection.Close (true); + } + } + ); + } + catch (HttpListenerException ex) { + if (_state == ServerState.ShuttingDown) + return; + + _log.Fatal (ex.Message); + _log.Debug (ex.ToString ()); + + break; + } + catch (InvalidOperationException ex) { + if (_state == ServerState.ShuttingDown) + return; + + _log.Fatal (ex.Message); + _log.Debug (ex.ToString ()); + + break; + } + catch (Exception ex) { + _log.Fatal (ex.Message); + _log.Debug (ex.ToString ()); + + if (ctx != null) + ctx.Connection.Close (true); + + if (_state == ServerState.ShuttingDown) + return; + + break; + } + } + + abort (); + } + + private void start () + { + lock (_sync) { + if (_state == ServerState.Start || _state == ServerState.ShuttingDown) + return; + + if (_isSecure) { + string msg; + + if (!checkCertificate (out msg)) + throw new InvalidOperationException (msg); + } + + _services.Start (); + + try { + startReceiving (); + } + catch { + _services.Stop (1011, String.Empty); + + throw; + } + + _state = ServerState.Start; + } + } + + private void startReceiving () + { + try { + _listener.Start (); + } + catch (Exception ex) { + var msg = "The underlying listener has failed to start."; + + throw new InvalidOperationException (msg, ex); + } + + var receiver = new ThreadStart (receiveRequest); + _receiveThread = new Thread (receiver); + _receiveThread.IsBackground = true; + + _receiveThread.Start (); + } + + private void stop (ushort code, string reason) + { + lock (_sync) { + if (_state != ServerState.Start) + return; + + _state = ServerState.ShuttingDown; + } + + try { + _services.Stop (code, reason); + } + catch (Exception ex) { + _log.Fatal (ex.Message); + _log.Debug (ex.ToString ()); + } + + try { + var timeout = 5000; + + stopReceiving (timeout); + } + catch (Exception ex) { + _log.Fatal (ex.Message); + _log.Debug (ex.ToString ()); + } + + _state = ServerState.Stop; + } + + private void stopReceiving (int millisecondsTimeout) + { + _listener.Stop (); + _receiveThread.Join (millisecondsTimeout); + } + + private static bool tryCreateUri ( + string uriString, + out Uri result, + out string message + ) + { + result = null; + message = null; + + var uri = uriString.ToUri (); + + if (uri == null) { + message = "An invalid URI string."; + + return false; + } + + if (!uri.IsAbsoluteUri) { + message = "A relative URI."; + + return false; + } + + var schm = uri.Scheme; + var isHttpSchm = schm == "http" || schm == "https"; + + if (!isHttpSchm) { + message = "The scheme part is not 'http' or 'https'."; + + return false; + } + + if (uri.PathAndQuery != "/") { + message = "It includes either or both path and query components."; + + return false; + } + + if (uri.Fragment.Length > 0) { + message = "It includes the fragment component."; + + return false; + } + + if (uri.Port == 0) { + message = "The port part is zero."; + + return false; + } + + result = uri; + + return true; + } + + #endregion + + #region Public Methods + + /// + /// Adds a WebSocket service with the specified behavior and path. + /// + /// + /// + /// A that specifies an absolute path to + /// the service to add. + /// + /// + /// / is trimmed from the end of the string if present. + /// + /// + /// + /// + /// The type of the behavior for the service. + /// + /// + /// It must inherit the class. + /// + /// + /// Also it must have a public parameterless constructor. + /// + /// + /// + /// + /// is an empty string. + /// + /// + /// -or- + /// + /// + /// is not an absolute path. + /// + /// + /// -or- + /// + /// + /// includes either or both + /// query and fragment components. + /// + /// + /// -or- + /// + /// + /// is already in use. + /// + /// + /// + /// is . + /// + public void AddWebSocketService (string path) + where TBehavior : WebSocketBehavior, new () + { + _services.AddService (path, null); + } + + /// + /// Adds a WebSocket service with the specified behavior, path, + /// and initializer. + /// + /// + /// + /// A that specifies an absolute path to + /// the service to add. + /// + /// + /// / is trimmed from the end of the string if present. + /// + /// + /// + /// + /// An delegate. + /// + /// + /// It specifies the delegate called when the service initializes + /// a new session instance. + /// + /// + /// if not necessary. + /// + /// + /// + /// + /// The type of the behavior for the service. + /// + /// + /// It must inherit the class. + /// + /// + /// Also it must have a public parameterless constructor. + /// + /// + /// + /// + /// is an empty string. + /// + /// + /// -or- + /// + /// + /// is not an absolute path. + /// + /// + /// -or- + /// + /// + /// includes either or both + /// query and fragment components. + /// + /// + /// -or- + /// + /// + /// is already in use. + /// + /// + /// + /// is . + /// + public void AddWebSocketService ( + string path, + Action initializer + ) + where TBehavior : WebSocketBehavior, new () + { + _services.AddService (path, initializer); + } + + /// + /// Removes a WebSocket service with the specified path. + /// + /// + /// The service is stopped with close status 1001 (going away) + /// if the current state of the service is Start. + /// + /// + /// true if the service is successfully found and removed; + /// otherwise, false. + /// + /// + /// + /// A that specifies an absolute path to + /// the service to remove. + /// + /// + /// / is trimmed from the end of the string if present. + /// + /// + /// + /// + /// is an empty string. + /// + /// + /// -or- + /// + /// + /// is not an absolute path. + /// + /// + /// -or- + /// + /// + /// includes either or both + /// query and fragment components. + /// + /// + /// + /// is . + /// + public bool RemoveWebSocketService (string path) + { + return _services.RemoveService (path); + } + + /// + /// Starts receiving incoming requests. + /// + /// + /// This method works if the current state of the server is Ready or Stop. + /// + /// + /// + /// There is no server certificate for secure connection. + /// + /// + /// -or- + /// + /// + /// The underlying has failed to start. + /// + /// + public void Start () + { + if (_state == ServerState.Start || _state == ServerState.ShuttingDown) + return; + + start (); + } + + /// + /// Stops receiving incoming requests. + /// + /// + /// This method works if the current state of the server is Start. + /// + public void Stop () + { + if (_state != ServerState.Start) + return; + + stop (1001, String.Empty); + } + + #endregion + } +} diff --git a/Assets/External/websocket-sharp/Server/HttpServer.cs.meta b/Assets/External/websocket-sharp/Server/HttpServer.cs.meta new file mode 100644 index 00000000..4689ca4e --- /dev/null +++ b/Assets/External/websocket-sharp/Server/HttpServer.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 8d339d9aa7e38394f843e684efdd9c21 \ No newline at end of file diff --git a/Assets/External/websocket-sharp/Server/IWebSocketSession.cs b/Assets/External/websocket-sharp/Server/IWebSocketSession.cs new file mode 100644 index 00000000..4ddc84ac --- /dev/null +++ b/Assets/External/websocket-sharp/Server/IWebSocketSession.cs @@ -0,0 +1,67 @@ +#region License +/* + * IWebSocketSession.cs + * + * The MIT License + * + * Copyright (c) 2013-2022 sta.blockhead + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#endregion + +using System; + +namespace WebSocketSharp.Server +{ + /// + /// Exposes the access to the information in a WebSocket session. + /// + public interface IWebSocketSession + { + #region Properties + + /// + /// Gets the unique ID of the session. + /// + /// + /// A that represents the unique ID of the session. + /// + string ID { get; } + + /// + /// Gets the time that the session has started. + /// + /// + /// A that represents the time that the session + /// has started. + /// + DateTime StartTime { get; } + + /// + /// Gets the WebSocket interface for the session. + /// + /// + /// A that represents the interface. + /// + WebSocket WebSocket { get; } + + #endregion + } +} diff --git a/Assets/External/websocket-sharp/Server/IWebSocketSession.cs.meta b/Assets/External/websocket-sharp/Server/IWebSocketSession.cs.meta new file mode 100644 index 00000000..9d20d581 --- /dev/null +++ b/Assets/External/websocket-sharp/Server/IWebSocketSession.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 5b07fb165c9332243ab441eb20cbc943 \ No newline at end of file diff --git a/Assets/External/websocket-sharp/Server/ServerState.cs b/Assets/External/websocket-sharp/Server/ServerState.cs new file mode 100644 index 00000000..2d758292 --- /dev/null +++ b/Assets/External/websocket-sharp/Server/ServerState.cs @@ -0,0 +1,40 @@ +#region License +/* + * ServerState.cs + * + * The MIT License + * + * Copyright (c) 2013-2014 sta.blockhead + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#endregion + +using System; + +namespace WebSocketSharp.Server +{ + internal enum ServerState + { + Ready, + Start, + ShuttingDown, + Stop + } +} diff --git a/Assets/External/websocket-sharp/Server/ServerState.cs.meta b/Assets/External/websocket-sharp/Server/ServerState.cs.meta new file mode 100644 index 00000000..6c1adef4 --- /dev/null +++ b/Assets/External/websocket-sharp/Server/ServerState.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: f3824b70e1911674ab2224cdf519e5e2 \ No newline at end of file diff --git a/Assets/External/websocket-sharp/Server/WebSocketBehavior.cs b/Assets/External/websocket-sharp/Server/WebSocketBehavior.cs new file mode 100644 index 00000000..b8375c8b --- /dev/null +++ b/Assets/External/websocket-sharp/Server/WebSocketBehavior.cs @@ -0,0 +1,1570 @@ +#region License +/* + * WebSocketBehavior.cs + * + * The MIT License + * + * Copyright (c) 2012-2025 sta.blockhead + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#endregion + +using System; +using System.Collections.Specialized; +using System.IO; +using System.Security.Principal; +using WebSocketSharp.Net; +using WebSocketSharp.Net.WebSockets; + +namespace WebSocketSharp.Server +{ + /// + /// Exposes a set of methods and properties used to define the behavior of + /// a WebSocket service provided by the or + /// class. + /// + /// + /// This class is an abstract class. + /// + public abstract class WebSocketBehavior : IWebSocketSession + { + #region Private Fields + + private WebSocketContext _context; + private Func _cookiesValidator; + private bool _emitOnPing; + private Func _hostValidator; + private string _id; + private bool _ignoreExtensions; + private bool _noDelay; + private Func _originValidator; + private string _protocol; + private WebSocketSessionManager _sessions; + private DateTime _startTime; + private WebSocket _websocket; + + #endregion + + #region Protected Constructors + + /// + /// Initializes a new instance of the class. + /// + protected WebSocketBehavior () + { + _startTime = DateTime.MaxValue; + } + + #endregion + + #region Protected Properties + + /// + /// Gets the HTTP headers for a session. + /// + /// + /// A that contains the headers + /// included in the WebSocket handshake request. + /// + /// + /// The get operation is not available when the session has not started yet. + /// + protected NameValueCollection Headers { + get { + if (_context == null) { + var msg = "The get operation is not available."; + + throw new InvalidOperationException (msg); + } + + return _context.Headers; + } + } + + /// + /// Gets a value indicating whether the communication is possible for + /// a session. + /// + /// + /// true if the communication is possible; otherwise, false. + /// + /// + /// The get operation is not available when the session has not started yet. + /// + protected bool IsAlive { + get { + if (_websocket == null) { + var msg = "The get operation is not available."; + + throw new InvalidOperationException (msg); + } + + return _websocket.IsAlive; + } + } + + /// + /// Gets the query string for a session. + /// + /// + /// + /// A that contains the query + /// parameters included in the WebSocket handshake request. + /// + /// + /// An empty collection if not included. + /// + /// + /// + /// The get operation is not available when the session has not started yet. + /// + protected NameValueCollection QueryString { + get { + if (_context == null) { + var msg = "The get operation is not available."; + + throw new InvalidOperationException (msg); + } + + return _context.QueryString; + } + } + + /// + /// Gets the current state of the WebSocket interface for a session. + /// + /// + /// + /// One of the enum values. + /// + /// + /// It indicates the current state of the interface. + /// + /// + /// + /// The get operation is not available when the session has not started yet. + /// + protected WebSocketState ReadyState { + get { + if (_websocket == null) { + var msg = "The get operation is not available."; + + throw new InvalidOperationException (msg); + } + + return _websocket.ReadyState; + } + } + + /// + /// Gets the management function for the sessions in the service. + /// + /// + /// A that manages the sessions in + /// the service. + /// + /// + /// The get operation is not available when the session has not started yet. + /// + protected WebSocketSessionManager Sessions { + get { + if (_sessions == null) { + var msg = "The get operation is not available."; + + throw new InvalidOperationException (msg); + } + + return _sessions; + } + } + + /// + /// Gets the client information for a session. + /// + /// + /// + /// A instance that represents identity, + /// authentication, and security roles for the client. + /// + /// + /// if the client is not authenticated. + /// + /// + /// + /// The get operation is not available when the session has not started yet. + /// + protected IPrincipal User { + get { + if (_context == null) { + var msg = "The get operation is not available."; + + throw new InvalidOperationException (msg); + } + + return _context.User; + } + } + + /// + /// Gets the client endpoint for a session. + /// + /// + /// A that represents the client + /// IP address and port number. + /// + /// + /// The get operation is not available when the session has not started yet. + /// + protected System.Net.IPEndPoint UserEndPoint { + get { + if (_context == null) { + var msg = "The get operation is not available."; + + throw new InvalidOperationException (msg); + } + + return _context.UserEndPoint; + } + } + + #endregion + + #region Public Properties + + /// + /// Gets or sets the delegate used to validate the HTTP cookies. + /// + /// + /// + /// A + /// delegate. + /// + /// + /// It represents the delegate called when the WebSocket interface + /// for a session validates the handshake request. + /// + /// + /// 1st parameter passed to the delegate + /// contains the cookies to validate. + /// + /// + /// 2nd parameter passed to the delegate + /// holds the cookies to send to the client. + /// + /// + /// The method invoked by the delegate must return true + /// if the cookies are valid. + /// + /// + /// if not necessary. + /// + /// + /// The default value is . + /// + /// + /// + /// The set operation is not available when the session has already started. + /// + public Func CookiesValidator { + get { + return _cookiesValidator; + } + + set { + if (_websocket != null) { + var msg = "The set operation is not available."; + + throw new InvalidOperationException (msg); + } + + _cookiesValidator = value; + } + } + + /// + /// Gets or sets a value indicating whether the WebSocket interface for + /// a session emits the message event when it receives a ping. + /// + /// + /// + /// true if the interface emits the message event when it receives + /// a ping; otherwise, false. + /// + /// + /// The default value is false. + /// + /// + /// + /// The set operation is not available when the session has already started. + /// + public bool EmitOnPing { + get { + return _emitOnPing; + } + + set { + if (_websocket != null) { + var msg = "The set operation is not available."; + + throw new InvalidOperationException (msg); + } + + _emitOnPing = value; + } + } + + /// + /// Gets or sets the delegate used to validate the Host header. + /// + /// + /// + /// A delegate. + /// + /// + /// It represents the delegate called when the WebSocket interface + /// for a session validates the handshake request. + /// + /// + /// The parameter passed to the delegate is + /// the value of the Host header. + /// + /// + /// The method invoked by the delegate must return true + /// if the header value is valid. + /// + /// + /// if not necessary. + /// + /// + /// The default value is . + /// + /// + /// + /// The set operation is not available when the session has already started. + /// + public Func HostValidator { + get { + return _hostValidator; + } + + set { + if (_websocket != null) { + var msg = "The set operation is not available."; + + throw new InvalidOperationException (msg); + } + + _hostValidator = value; + } + } + + /// + /// Gets the unique ID of a session. + /// + /// + /// + /// A that represents the unique ID of the session. + /// + /// + /// when the session has not started yet. + /// + /// + public string ID { + get { + return _id; + } + } + + /// + /// Gets or sets a value indicating whether the WebSocket interface for + /// a session ignores the Sec-WebSocket-Extensions header. + /// + /// + /// + /// true if the interface ignores the extensions requested + /// from the client; otherwise, false. + /// + /// + /// The default value is false. + /// + /// + /// + /// The set operation is not available when the session has already started. + /// + public bool IgnoreExtensions { + get { + return _ignoreExtensions; + } + + set { + if (_websocket != null) { + var msg = "The set operation is not available."; + + throw new InvalidOperationException (msg); + } + + _ignoreExtensions = value; + } + } + + /// + /// Gets or sets a value indicating whether the underlying TCP socket of + /// the WebSocket interface for a session disables a delay when send or + /// receive buffer is not full. + /// + /// + /// + /// true if the delay is disabled; otherwise, false. + /// + /// + /// The default value is false. + /// + /// + /// + /// + /// The set operation is not available when the session has already started. + /// + public bool NoDelay { + get { + return _noDelay; + } + + set { + if (_websocket != null) { + var msg = "The set operation is not available."; + + throw new InvalidOperationException (msg); + } + + _noDelay = value; + } + } + + /// + /// Gets or sets the delegate used to validate the Origin header. + /// + /// + /// + /// A delegate. + /// + /// + /// It represents the delegate called when the WebSocket interface + /// for a session validates the handshake request. + /// + /// + /// The parameter passed to the delegate is + /// the value of the Origin header or if + /// the header is not present. + /// + /// + /// The method invoked by the delegate must return true + /// if the header value is valid. + /// + /// + /// if not necessary. + /// + /// + /// The default value is . + /// + /// + /// + /// The set operation is not available when the session has already started. + /// + public Func OriginValidator { + get { + return _originValidator; + } + + set { + if (_websocket != null) { + var msg = "The set operation is not available."; + + throw new InvalidOperationException (msg); + } + + _originValidator = value; + } + } + + /// + /// Gets or sets the name of the WebSocket subprotocol for a session. + /// + /// + /// + /// A that represents the name of the subprotocol. + /// + /// + /// The value specified for a set operation must be a token defined in + /// + /// RFC 2616. + /// + /// + /// The value is initialized if not requested. + /// + /// + /// The default value is an empty string. + /// + /// + /// + /// The value specified for a set operation is not a token. + /// + /// + /// The set operation is not available when the session has already started. + /// + public string Protocol { + get { + return _websocket != null + ? _websocket.Protocol + : (_protocol ?? String.Empty); + } + + set { + if (_websocket != null) { + var msg = "The set operation is not available."; + + throw new InvalidOperationException (msg); + } + + if (value == null || value.Length == 0) { + _protocol = null; + + return; + } + + if (!value.IsToken ()) { + var msg = "Not a token."; + + throw new ArgumentException (msg, "value"); + } + + _protocol = value; + } + } + + /// + /// Gets the time that a session has started. + /// + /// + /// + /// A that represents the time that the session + /// has started. + /// + /// + /// when the session has not started yet. + /// + /// + public DateTime StartTime { + get { + return _startTime; + } + } + + #endregion + + #region Private Methods + + private string checkHandshakeRequest (WebSocketContext context) + { + if (_hostValidator != null) { + if (!_hostValidator (context.Host)) { + var msg = "The Host header is invalid."; + + return msg; + } + } + + if (_originValidator != null) { + if (!_originValidator (context.Origin)) { + var msg = "The Origin header is non-existent or invalid."; + + return msg; + } + } + + if (_cookiesValidator != null) { + var req = context.CookieCollection; + var res = context.WebSocket.CookieCollection; + + if (!_cookiesValidator (req, res)) { + var msg = "The Cookie header is non-existent or invalid."; + + return msg; + } + } + + return null; + } + + private void onClose (object sender, CloseEventArgs e) + { + if (_id == null) + return; + + _sessions.Remove (_id); + + OnClose (e); + } + + private void onError (object sender, ErrorEventArgs e) + { + OnError (e); + } + + private void onMessage (object sender, MessageEventArgs e) + { + OnMessage (e); + } + + private void onOpen (object sender, EventArgs e) + { + _id = _sessions.Add (this); + + if (_id == null) { + _websocket.Close (CloseStatusCode.Away); + + return; + } + + _startTime = DateTime.Now; + + OnOpen (); + } + + #endregion + + #region Internal Methods + + internal void Start ( + WebSocketContext context, + WebSocketSessionManager sessions + ) + { + _context = context; + _sessions = sessions; + + _websocket = context.WebSocket; + _websocket.CustomHandshakeRequestChecker = checkHandshakeRequest; + + if (_emitOnPing) + _websocket.EmitOnPing = true; + + if (_ignoreExtensions) + _websocket.IgnoreExtensions = true; + + if (_noDelay) + _websocket.NoDelay = true; + + if (_protocol != null) + _websocket.Protocol = _protocol; + + var waitTime = sessions.WaitTime; + + if (waitTime != _websocket.WaitTime) + _websocket.WaitTime = waitTime; + + _websocket.OnClose += onClose; + _websocket.OnError += onError; + _websocket.OnMessage += onMessage; + _websocket.OnOpen += onOpen; + + _websocket.Accept (); + } + + #endregion + + #region Protected Methods + + /// + /// Closes the WebSocket connection for a session. + /// + /// + /// This method does nothing when the current state of the WebSocket + /// interface is Closing or Closed. + /// + /// + /// The Close method is not available when the session has not started yet. + /// + protected void Close () + { + if (_websocket == null) { + var msg = "The Close method is not available."; + + throw new InvalidOperationException (msg); + } + + _websocket.Close (); + } + + /// + /// Closes the WebSocket connection for a session with the specified + /// status code and reason. + /// + /// + /// This method does nothing when the current state of the WebSocket + /// interface is Closing or Closed. + /// + /// + /// + /// A that specifies the status code indicating + /// the reason for the close. + /// + /// + /// The status codes are defined in + /// + /// Section 7.4 of RFC 6455. + /// + /// + /// + /// + /// A that specifies the reason for the close. + /// + /// + /// Its size must be 123 bytes or less in UTF-8. + /// + /// + /// + /// + /// is 1010 (mandatory extension). + /// + /// + /// -or- + /// + /// + /// is 1005 (no status) and + /// is specified. + /// + /// + /// -or- + /// + /// + /// could not be UTF-8-encoded. + /// + /// + /// + /// + /// is less than 1000 or greater than 4999. + /// + /// + /// -or- + /// + /// + /// The size of is greater than 123 bytes. + /// + /// + /// + /// The Close method is not available when the session has not started yet. + /// + protected void Close (ushort code, string reason) + { + if (_websocket == null) { + var msg = "The Close method is not available."; + + throw new InvalidOperationException (msg); + } + + _websocket.Close (code, reason); + } + + /// + /// Closes the WebSocket connection for a session with the specified + /// status code and reason. + /// + /// + /// This method does nothing when the current state of the WebSocket + /// interface is Closing or Closed. + /// + /// + /// + /// One of the enum values. + /// + /// + /// It specifies the status code indicating the reason for the close. + /// + /// + /// + /// + /// A that specifies the reason for the close. + /// + /// + /// Its size must be 123 bytes or less in UTF-8. + /// + /// + /// + /// + /// is an undefined enum value. + /// + /// + /// -or- + /// + /// + /// is . + /// + /// + /// -or- + /// + /// + /// is and + /// is specified. + /// + /// + /// -or- + /// + /// + /// could not be UTF-8-encoded. + /// + /// + /// + /// The size of is greater than 123 bytes. + /// + /// + /// The Close method is not available when the session has not started yet. + /// + protected void Close (CloseStatusCode code, string reason) + { + if (_websocket == null) { + var msg = "The Close method is not available."; + + throw new InvalidOperationException (msg); + } + + _websocket.Close (code, reason); + } + + /// + /// Closes the WebSocket connection for a session asynchronously. + /// + /// + /// + /// This method does not wait for the close to be complete. + /// + /// + /// This method does nothing when the current state of the WebSocket + /// interface is Closing or Closed. + /// + /// + /// + /// The CloseAsync method is not available when the session has not + /// started yet. + /// + protected void CloseAsync () + { + if (_websocket == null) { + var msg = "The CloseAsync method is not available."; + + throw new InvalidOperationException (msg); + } + + _websocket.CloseAsync (); + } + + /// + /// Closes the WebSocket connection for a session asynchronously with + /// the specified status code and reason. + /// + /// + /// + /// This method does not wait for the close to be complete. + /// + /// + /// This method does nothing when the current state of the WebSocket + /// interface is Closing or Closed. + /// + /// + /// + /// + /// A that specifies the status code indicating + /// the reason for the close. + /// + /// + /// The status codes are defined in + /// + /// Section 7.4 of RFC 6455. + /// + /// + /// + /// + /// A that specifies the reason for the close. + /// + /// + /// Its size must be 123 bytes or less in UTF-8. + /// + /// + /// + /// + /// is 1010 (mandatory extension). + /// + /// + /// -or- + /// + /// + /// is 1005 (no status) and + /// is specified. + /// + /// + /// -or- + /// + /// + /// could not be UTF-8-encoded. + /// + /// + /// + /// + /// is less than 1000 or greater than 4999. + /// + /// + /// -or- + /// + /// + /// The size of is greater than 123 bytes. + /// + /// + /// + /// The CloseAsync method is not available when the session has not + /// started yet. + /// + protected void CloseAsync (ushort code, string reason) + { + if (_websocket == null) { + var msg = "The CloseAsync method is not available."; + + throw new InvalidOperationException (msg); + } + + _websocket.CloseAsync (code, reason); + } + + /// + /// Closes the WebSocket connection for a session asynchronously with + /// the specified status code and reason. + /// + /// + /// + /// This method does not wait for the close to be complete. + /// + /// + /// This method does nothing when the current state of the WebSocket + /// interface is Closing or Closed. + /// + /// + /// + /// + /// One of the enum values. + /// + /// + /// It specifies the status code indicating the reason for the close. + /// + /// + /// + /// + /// A that specifies the reason for the close. + /// + /// + /// Its size must be 123 bytes or less in UTF-8. + /// + /// + /// + /// + /// is an undefined enum value. + /// + /// + /// -or- + /// + /// + /// is . + /// + /// + /// -or- + /// + /// + /// is and + /// is specified. + /// + /// + /// -or- + /// + /// + /// could not be UTF-8-encoded. + /// + /// + /// + /// The size of is greater than 123 bytes. + /// + /// + /// The CloseAsync method is not available when the session has not + /// started yet. + /// + protected void CloseAsync (CloseStatusCode code, string reason) + { + if (_websocket == null) { + var msg = "The CloseAsync method is not available."; + + throw new InvalidOperationException (msg); + } + + _websocket.CloseAsync (code, reason); + } + + /// + /// Called when the WebSocket connection for a session has been closed. + /// + /// + /// A that represents the event data passed + /// from a event. + /// + protected virtual void OnClose (CloseEventArgs e) + { + } + + /// + /// Called when the WebSocket interface for a session gets an error. + /// + /// + /// A that represents the event data passed + /// from a event. + /// + protected virtual void OnError (ErrorEventArgs e) + { + } + + /// + /// Called when the WebSocket interface for a session receives a message. + /// + /// + /// A that represents the event data passed + /// from a event. + /// + protected virtual void OnMessage (MessageEventArgs e) + { + } + + /// + /// Called when the WebSocket connection for a session has been established. + /// + protected virtual void OnOpen () + { + } + + /// + /// Sends a ping to the client for a session. + /// + /// + /// true if the send has successfully done and a pong has been + /// received within a time; otherwise, false. + /// + /// + /// The Ping method is not available when the session has not started yet. + /// + protected bool Ping () + { + if (_websocket == null) { + var msg = "The Ping method is not available."; + + throw new InvalidOperationException (msg); + } + + return _websocket.Ping (); + } + + /// + /// Sends a ping with the specified message to the client for a session. + /// + /// + /// true if the send has successfully done and a pong has been + /// received within a time; otherwise, false. + /// + /// + /// + /// A that specifies the message to send. + /// + /// + /// Its size must be 125 bytes or less in UTF-8. + /// + /// + /// + /// could not be UTF-8-encoded. + /// + /// + /// The size of is greater than 125 bytes. + /// + /// + /// The Ping method is not available when the session has not started yet. + /// + protected bool Ping (string message) + { + if (_websocket == null) { + var msg = "The Ping method is not available."; + + throw new InvalidOperationException (msg); + } + + return _websocket.Ping (message); + } + + /// + /// Sends the specified data to the client for a session. + /// + /// + /// An array of that specifies the binary data to send. + /// + /// + /// is . + /// + /// + /// + /// The Send method is not available when the session has not + /// started yet. + /// + /// + /// -or- + /// + /// + /// The Send method is not available when the current state of + /// the WebSocket interface is not Open. + /// + /// + protected void Send (byte[] data) + { + if (_websocket == null) { + var msg = "The Send method is not available."; + + throw new InvalidOperationException (msg); + } + + _websocket.Send (data); + } + + /// + /// Sends the specified file to the client for a session. + /// + /// + /// + /// A that specifies the file to send. + /// + /// + /// The file is sent as the binary data. + /// + /// + /// + /// + /// The file does not exist. + /// + /// + /// -or- + /// + /// + /// The file could not be opened. + /// + /// + /// + /// is . + /// + /// + /// + /// The Send method is not available when the session has not + /// started yet. + /// + /// + /// -or- + /// + /// + /// The Send method is not available when the current state of + /// the WebSocket interface is not Open. + /// + /// + protected void Send (FileInfo fileInfo) + { + if (_websocket == null) { + var msg = "The Send method is not available."; + + throw new InvalidOperationException (msg); + } + + _websocket.Send (fileInfo); + } + + /// + /// Sends the specified data to the client for a session. + /// + /// + /// A that specifies the text data to send. + /// + /// + /// could not be UTF-8-encoded. + /// + /// + /// is . + /// + /// + /// + /// The Send method is not available when the session has not + /// started yet. + /// + /// + /// -or- + /// + /// + /// The Send method is not available when the current state of + /// the WebSocket interface is not Open. + /// + /// + protected void Send (string data) + { + if (_websocket == null) { + var msg = "The Send method is not available."; + + throw new InvalidOperationException (msg); + } + + _websocket.Send (data); + } + + /// + /// Sends the data from the specified stream instance to the client for + /// a session. + /// + /// + /// + /// A instance from which to read the data to send. + /// + /// + /// The data is sent as the binary data. + /// + /// + /// + /// An that specifies the number of bytes to send. + /// + /// + /// + /// cannot be read. + /// + /// + /// -or- + /// + /// + /// is less than 1. + /// + /// + /// -or- + /// + /// + /// No data could be read from . + /// + /// + /// + /// is . + /// + /// + /// + /// The Send method is not available when the session has not + /// started yet. + /// + /// + /// -or- + /// + /// + /// The Send method is not available when the current state of + /// the WebSocket interface is not Open. + /// + /// + protected void Send (Stream stream, int length) + { + if (_websocket == null) { + var msg = "The Send method is not available."; + + throw new InvalidOperationException (msg); + } + + _websocket.Send (stream, length); + } + + /// + /// Sends the specified data to the client for a session asynchronously. + /// + /// + /// This method does not wait for the send to be complete. + /// + /// + /// An array of that specifies the binary data to send. + /// + /// + /// + /// An delegate. + /// + /// + /// It specifies the delegate called when the send is complete. + /// + /// + /// The parameter passed to the delegate is true + /// if the send has successfully done; otherwise, false. + /// + /// + /// if not necessary. + /// + /// + /// + /// is . + /// + /// + /// + /// The SendAsync method is not available when the session has not + /// started yet. + /// + /// + /// -or- + /// + /// + /// The SendAsync method is not available when the current state of + /// the WebSocket interface is not Open. + /// + /// + protected void SendAsync (byte[] data, Action completed) + { + if (_websocket == null) { + var msg = "The SendAsync method is not available."; + + throw new InvalidOperationException (msg); + } + + _websocket.SendAsync (data, completed); + } + + /// + /// Sends the specified file to the client for a session asynchronously. + /// + /// + /// This method does not wait for the send to be complete. + /// + /// + /// + /// A that specifies the file to send. + /// + /// + /// The file is sent as the binary data. + /// + /// + /// + /// + /// An delegate. + /// + /// + /// It specifies the delegate called when the send is complete. + /// + /// + /// The parameter passed to the delegate is true + /// if the send has successfully done; otherwise, false. + /// + /// + /// if not necessary. + /// + /// + /// + /// + /// The file does not exist. + /// + /// + /// -or- + /// + /// + /// The file could not be opened. + /// + /// + /// + /// is . + /// + /// + /// + /// The SendAsync method is not available when the session has not + /// started yet. + /// + /// + /// -or- + /// + /// + /// The SendAsync method is not available when the current state of + /// the WebSocket interface is not Open. + /// + /// + protected void SendAsync (FileInfo fileInfo, Action completed) + { + if (_websocket == null) { + var msg = "The SendAsync method is not available."; + + throw new InvalidOperationException (msg); + } + + _websocket.SendAsync (fileInfo, completed); + } + + /// + /// Sends the specified data to the client for a session asynchronously. + /// + /// + /// This method does not wait for the send to be complete. + /// + /// + /// A that specifies the text data to send. + /// + /// + /// + /// An delegate. + /// + /// + /// It specifies the delegate called when the send is complete. + /// + /// + /// The parameter passed to the delegate is true + /// if the send has successfully done; otherwise, false. + /// + /// + /// if not necessary. + /// + /// + /// + /// could not be UTF-8-encoded. + /// + /// + /// is . + /// + /// + /// + /// The SendAsync method is not available when the session has not + /// started yet. + /// + /// + /// -or- + /// + /// + /// The SendAsync method is not available when the current state of + /// the WebSocket interface is not Open. + /// + /// + protected void SendAsync (string data, Action completed) + { + if (_websocket == null) { + var msg = "The SendAsync method is not available."; + + throw new InvalidOperationException (msg); + } + + _websocket.SendAsync (data, completed); + } + + /// + /// Sends the data from the specified stream instance to the client for + /// a session asynchronously. + /// + /// + /// This method does not wait for the send to be complete. + /// + /// + /// + /// A instance from which to read the data to send. + /// + /// + /// The data is sent as the binary data. + /// + /// + /// + /// An that specifies the number of bytes to send. + /// + /// + /// + /// An delegate. + /// + /// + /// It specifies the delegate called when the send is complete. + /// + /// + /// The parameter passed to the delegate is true + /// if the send has successfully done; otherwise, false. + /// + /// + /// if not necessary. + /// + /// + /// + /// + /// cannot be read. + /// + /// + /// -or- + /// + /// + /// is less than 1. + /// + /// + /// -or- + /// + /// + /// No data could be read from . + /// + /// + /// + /// is . + /// + /// + /// + /// The SendAsync method is not available when the session has not + /// started yet. + /// + /// + /// -or- + /// + /// + /// The SendAsync method is not available when the current state of + /// the WebSocket interface is not Open. + /// + /// + protected void SendAsync (Stream stream, int length, Action completed) + { + if (_websocket == null) { + var msg = "The SendAsync method is not available."; + + throw new InvalidOperationException (msg); + } + + _websocket.SendAsync (stream, length, completed); + } + + #endregion + + #region Explicit Interface Implementations + + /// + /// Gets the WebSocket interface for a session. + /// + /// + /// + /// A that represents + /// the WebSocket interface. + /// + /// + /// when the session has not started yet. + /// + /// + WebSocket IWebSocketSession.WebSocket { + get { + return _websocket; + } + } + + #endregion + } +} diff --git a/Assets/External/websocket-sharp/Server/WebSocketBehavior.cs.meta b/Assets/External/websocket-sharp/Server/WebSocketBehavior.cs.meta new file mode 100644 index 00000000..96ae0887 --- /dev/null +++ b/Assets/External/websocket-sharp/Server/WebSocketBehavior.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: c7cce5577fefb5649a0ceddc552c5b1e \ No newline at end of file diff --git a/Assets/External/websocket-sharp/Server/WebSocketServer.cs b/Assets/External/websocket-sharp/Server/WebSocketServer.cs new file mode 100644 index 00000000..50f03f02 --- /dev/null +++ b/Assets/External/websocket-sharp/Server/WebSocketServer.cs @@ -0,0 +1,1185 @@ +#region License +/* + * WebSocketServer.cs + * + * The MIT License + * + * Copyright (c) 2012-2024 sta.blockhead + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#endregion + +#region Contributors +/* + * Contributors: + * - Juan Manuel Lallana + * - Jonas Hovgaard + * - Liryna + * - Rohan Singh + */ +#endregion + +using System; +using System.Collections.Generic; +using System.Net.Sockets; +using System.Security.Cryptography.X509Certificates; +using System.Security.Principal; +using System.Text; +using System.Threading; +using WebSocketSharp.Net; +using WebSocketSharp.Net.WebSockets; + +namespace WebSocketSharp.Server +{ + /// + /// Provides a WebSocket protocol server. + /// + /// + /// This class can provide multiple WebSocket services. + /// + public class WebSocketServer + { + #region Private Fields + + private System.Net.IPAddress _address; + private AuthenticationSchemes _authSchemes; + private static readonly string _defaultRealm; + private string _hostname; + private bool _isDnsStyle; + private bool _isSecure; + private TcpListener _listener; + private Logger _log; + private int _port; + private string _realm; + private string _realmInUse; + private Thread _receiveThread; + private bool _reuseAddress; + private WebSocketServiceManager _services; + private ServerSslConfiguration _sslConfig; + private ServerSslConfiguration _sslConfigInUse; + private volatile ServerState _state; + private object _sync; + private Func _userCredFinder; + + #endregion + + #region Static Constructor + + static WebSocketServer () + { + _defaultRealm = "SECRET AREA"; + } + + #endregion + + #region Public Constructors + + /// + /// Initializes a new instance of the class. + /// + /// + /// The new instance listens for incoming handshake requests on + /// and port 80. + /// + public WebSocketServer () + { + var addr = System.Net.IPAddress.Any; + + init (addr.ToString (), addr, 80, false); + } + + /// + /// Initializes a new instance of the class + /// with the specified port. + /// + /// + /// + /// The new instance listens for incoming handshake requests on + /// and . + /// + /// + /// It provides secure connections if is 443. + /// + /// + /// + /// An that specifies the number of the port on which + /// to listen. + /// + /// + /// is less than 1 or greater than 65535. + /// + public WebSocketServer (int port) + : this (port, port == 443) + { + } + + /// + /// Initializes a new instance of the class + /// with the specified URL. + /// + /// + /// + /// The new instance listens for incoming handshake requests on + /// the IP address and port of . + /// + /// + /// Either port 80 or 443 is used if includes + /// no port. Port 443 is used if the scheme of + /// is wss; otherwise, port 80 is used. + /// + /// + /// The new instance provides secure connections if the scheme of + /// is wss. + /// + /// + /// + /// A that specifies the WebSocket URL of the server. + /// + /// + /// + /// is an empty string. + /// + /// + /// -or- + /// + /// + /// is invalid. + /// + /// + /// + /// is . + /// + public WebSocketServer (string url) + { + if (url == null) + throw new ArgumentNullException ("url"); + + if (url.Length == 0) + throw new ArgumentException ("An empty string.", "url"); + + Uri uri; + string msg; + + if (!tryCreateUri (url, out uri, out msg)) + throw new ArgumentException (msg, "url"); + + var host = uri.DnsSafeHost; + var addr = host.ToIPAddress (); + + if (addr == null) { + msg = "The host part could not be converted to an IP address."; + + throw new ArgumentException (msg, "url"); + } + + if (!addr.IsLocal ()) { + msg = "The IP address of the host is not a local IP address."; + + throw new ArgumentException (msg, "url"); + } + + init (host, addr, uri.Port, uri.Scheme == "wss"); + } + + /// + /// Initializes a new instance of the class + /// with the specified port and boolean if secure or not. + /// + /// + /// The new instance listens for incoming handshake requests on + /// and . + /// + /// + /// An that specifies the number of the port on which + /// to listen. + /// + /// + /// A : true if the new instance provides + /// secure connections; otherwise, false. + /// + /// + /// is less than 1 or greater than 65535. + /// + public WebSocketServer (int port, bool secure) + { + if (!port.IsPortNumber ()) { + var msg = "Less than 1 or greater than 65535."; + + throw new ArgumentOutOfRangeException ("port", msg); + } + + var addr = System.Net.IPAddress.Any; + + init (addr.ToString (), addr, port, secure); + } + + /// + /// Initializes a new instance of the class + /// with the specified IP address and port. + /// + /// + /// + /// The new instance listens for incoming handshake requests on + /// and . + /// + /// + /// It provides secure connections if is 443. + /// + /// + /// + /// A that specifies the local IP + /// address on which to listen. + /// + /// + /// An that specifies the number of the port on which + /// to listen. + /// + /// + /// is not a local IP address. + /// + /// + /// is . + /// + /// + /// is less than 1 or greater than 65535. + /// + public WebSocketServer (System.Net.IPAddress address, int port) + : this (address, port, port == 443) + { + } + + /// + /// Initializes a new instance of the class + /// with the specified IP address, port, and boolean if secure or not. + /// + /// + /// The new instance listens for incoming handshake requests on + /// and . + /// + /// + /// A that specifies the local IP + /// address on which to listen. + /// + /// + /// An that specifies the number of the port on which + /// to listen. + /// + /// + /// A : true if the new instance provides + /// secure connections; otherwise, false. + /// + /// + /// is not a local IP address. + /// + /// + /// is . + /// + /// + /// is less than 1 or greater than 65535. + /// + public WebSocketServer (System.Net.IPAddress address, int port, bool secure) + { + if (address == null) + throw new ArgumentNullException ("address"); + + if (!address.IsLocal ()) { + var msg = "Not a local IP address."; + + throw new ArgumentException (msg, "address"); + } + + if (!port.IsPortNumber ()) { + var msg = "Less than 1 or greater than 65535."; + + throw new ArgumentOutOfRangeException ("port", msg); + } + + init (address.ToString (), address, port, secure); + } + + #endregion + + #region Public Properties + + /// + /// Gets the IP address of the server. + /// + /// + /// A that represents the local IP + /// address on which to listen for incoming handshake requests. + /// + public System.Net.IPAddress Address { + get { + return _address; + } + } + + /// + /// Gets or sets the scheme used to authenticate the clients. + /// + /// + /// The set operation works if the current state of the server is + /// Ready or Stop. + /// + /// + /// + /// One of the + /// enum values. + /// + /// + /// It represents the scheme used to authenticate the clients. + /// + /// + /// The default value is + /// . + /// + /// + public AuthenticationSchemes AuthenticationSchemes { + get { + return _authSchemes; + } + + set { + lock (_sync) { + if (!canSet ()) + return; + + _authSchemes = value; + } + } + } + + /// + /// Gets a value indicating whether the server has started. + /// + /// + /// true if the server has started; otherwise, false. + /// + public bool IsListening { + get { + return _state == ServerState.Start; + } + } + + /// + /// Gets a value indicating whether the server provides secure connections. + /// + /// + /// true if the server provides secure connections; otherwise, + /// false. + /// + public bool IsSecure { + get { + return _isSecure; + } + } + + /// + /// Gets or sets a value indicating whether the server cleans up + /// the inactive sessions periodically. + /// + /// + /// The set operation works if the current state of the server is + /// Ready or Stop. + /// + /// + /// + /// true if the server cleans up the inactive sessions + /// every 60 seconds; otherwise, false. + /// + /// + /// The default value is false. + /// + /// + public bool KeepClean { + get { + return _services.KeepClean; + } + + set { + _services.KeepClean = value; + } + } + + /// + /// Gets the logging function for the server. + /// + /// + /// The default logging level is . + /// + /// + /// A that provides the logging function. + /// + public Logger Log { + get { + return _log; + } + } + + /// + /// Gets the port of the server. + /// + /// + /// An that represents the number of the port on which + /// to listen for incoming handshake requests. + /// + public int Port { + get { + return _port; + } + } + + /// + /// Gets or sets the name of the realm associated with the server. + /// + /// + /// The set operation works if the current state of the server is + /// Ready or Stop. + /// + /// + /// + /// A that represents the name of the realm. + /// + /// + /// "SECRET AREA" is used as the name of the realm if the value is + /// or an empty string. + /// + /// + /// The default value is . + /// + /// + public string Realm { + get { + return _realm; + } + + set { + lock (_sync) { + if (!canSet ()) + return; + + _realm = value; + } + } + } + + /// + /// Gets or sets a value indicating whether the server is allowed to + /// be bound to an address that is already in use. + /// + /// + /// + /// You should set this property to true if you would like to + /// resolve to wait for socket in TIME_WAIT state. + /// + /// + /// The set operation works if the current state of the server is + /// Ready or Stop. + /// + /// + /// + /// + /// true if the server is allowed to be bound to an address + /// that is already in use; otherwise, false. + /// + /// + /// The default value is false. + /// + /// + public bool ReuseAddress { + get { + return _reuseAddress; + } + + set { + lock (_sync) { + if (!canSet ()) + return; + + _reuseAddress = value; + } + } + } + + /// + /// Gets the configuration for secure connection. + /// + /// + /// The configuration is used when the server attempts to start, + /// so it must be configured before the start method is called. + /// + /// + /// A that represents the + /// configuration used to provide secure connections. + /// + /// + /// The server does not provide secure connections. + /// + public ServerSslConfiguration SslConfiguration { + get { + if (!_isSecure) { + var msg = "The server does not provide secure connections."; + + throw new InvalidOperationException (msg); + } + + return getSslConfiguration (); + } + } + + /// + /// Gets or sets the delegate called to find the credentials for + /// an identity used to authenticate a client. + /// + /// + /// The set operation works if the current state of the server is + /// Ready or Stop. + /// + /// + /// + /// A + /// delegate. + /// + /// + /// It represents the delegate called when the server finds + /// the credentials used to authenticate a client. + /// + /// + /// It must return if the credentials + /// are not found. + /// + /// + /// if not necessary. + /// + /// + /// The default value is . + /// + /// + public Func UserCredentialsFinder { + get { + return _userCredFinder; + } + + set { + lock (_sync) { + if (!canSet ()) + return; + + _userCredFinder = value; + } + } + } + + /// + /// Gets or sets the time to wait for the response to the WebSocket + /// Ping or Close. + /// + /// + /// The set operation works if the current state of the server is + /// Ready or Stop. + /// + /// + /// + /// A that represents the time to wait for + /// the response. + /// + /// + /// The default value is the same as 1 second. + /// + /// + /// + /// The value specified for a set operation is zero or less. + /// + public TimeSpan WaitTime { + get { + return _services.WaitTime; + } + + set { + _services.WaitTime = value; + } + } + + /// + /// Gets the management function for the WebSocket services provided by + /// the server. + /// + /// + /// A that manages the WebSocket + /// services provided by the server. + /// + public WebSocketServiceManager WebSocketServices { + get { + return _services; + } + } + + #endregion + + #region Private Methods + + private void abort () + { + lock (_sync) { + if (_state != ServerState.Start) + return; + + _state = ServerState.ShuttingDown; + } + + try { + _listener.Stop (); + } + catch (Exception ex) { + _log.Fatal (ex.Message); + _log.Debug (ex.ToString ()); + } + + try { + _services.Stop (1006, String.Empty); + } + catch (Exception ex) { + _log.Fatal (ex.Message); + _log.Debug (ex.ToString ()); + } + + _state = ServerState.Stop; + } + + private bool authenticateClient (TcpListenerWebSocketContext context) + { + if (_authSchemes == AuthenticationSchemes.Anonymous) + return true; + + if (_authSchemes == AuthenticationSchemes.None) + return false; + + var chal = new AuthenticationChallenge (_authSchemes, _realmInUse) + .ToString (); + + var retry = -1; + Func auth = null; + auth = + () => { + retry++; + + if (retry > 99) + return false; + + if (context.SetUser (_authSchemes, _realmInUse, _userCredFinder)) + return true; + + context.SendAuthenticationChallenge (chal); + + return auth (); + }; + + return auth (); + } + + private bool canSet () + { + return _state == ServerState.Ready || _state == ServerState.Stop; + } + + private bool checkHostNameForRequest (string name) + { + return !_isDnsStyle + || Uri.CheckHostName (name) != UriHostNameType.Dns + || name == _hostname; + } + + private string getRealm () + { + var realm = _realm; + + return realm != null && realm.Length > 0 ? realm : _defaultRealm; + } + + private ServerSslConfiguration getSslConfiguration () + { + if (_sslConfig == null) + _sslConfig = new ServerSslConfiguration (); + + return _sslConfig; + } + + private void init ( + string hostname, + System.Net.IPAddress address, + int port, + bool secure + ) + { + _hostname = hostname; + _address = address; + _port = port; + _isSecure = secure; + + _authSchemes = AuthenticationSchemes.Anonymous; + _isDnsStyle = Uri.CheckHostName (hostname) == UriHostNameType.Dns; + _listener = new TcpListener (address, port); + _log = new Logger (); + _services = new WebSocketServiceManager (_log); + _sync = new object (); + } + + private void processRequest (TcpListenerWebSocketContext context) + { + if (!authenticateClient (context)) { + context.Close (HttpStatusCode.Forbidden); + + return; + } + + var uri = context.RequestUri; + + if (uri == null) { + context.Close (HttpStatusCode.BadRequest); + + return; + } + + var name = uri.DnsSafeHost; + + if (!checkHostNameForRequest (name)) { + context.Close (HttpStatusCode.NotFound); + + return; + } + + var path = uri.AbsolutePath; + + if (path.IndexOfAny (new[] { '%', '+' }) > -1) + path = HttpUtility.UrlDecode (path, Encoding.UTF8); + + WebSocketServiceHost host; + + if (!_services.InternalTryGetServiceHost (path, out host)) { + context.Close (HttpStatusCode.NotImplemented); + + return; + } + + host.StartSession (context); + } + + private void receiveRequest () + { + while (true) { + TcpClient cl = null; + + try { + cl = _listener.AcceptTcpClient (); + + ThreadPool.QueueUserWorkItem ( + state => { + try { + var ctx = new TcpListenerWebSocketContext ( + cl, + null, + _isSecure, + _sslConfigInUse, + _log + ); + + processRequest (ctx); + } + catch (Exception ex) { + _log.Error (ex.Message); + _log.Debug (ex.ToString ()); + + cl.Close (); + } + } + ); + } + catch (SocketException ex) { + if (_state == ServerState.ShuttingDown) + return; + + _log.Fatal (ex.Message); + _log.Debug (ex.ToString ()); + + break; + } + catch (InvalidOperationException ex) { + if (_state == ServerState.ShuttingDown) + return; + + _log.Fatal (ex.Message); + _log.Debug (ex.ToString ()); + + break; + } + catch (Exception ex) { + _log.Fatal (ex.Message); + _log.Debug (ex.ToString ()); + + if (cl != null) + cl.Close (); + + if (_state == ServerState.ShuttingDown) + return; + + break; + } + } + + abort (); + } + + private void start () + { + lock (_sync) { + if (_state == ServerState.Start || _state == ServerState.ShuttingDown) + return; + + if (_isSecure) { + var src = getSslConfiguration (); + var conf = new ServerSslConfiguration (src); + + if (conf.ServerCertificate == null) { + var msg = "There is no server certificate for secure connection."; + + throw new InvalidOperationException (msg); + } + + _sslConfigInUse = conf; + } + + _realmInUse = getRealm (); + + _services.Start (); + + try { + startReceiving (); + } + catch { + _services.Stop (1011, String.Empty); + + throw; + } + + _state = ServerState.Start; + } + } + + private void startReceiving () + { + if (_reuseAddress) { + _listener.Server.SetSocketOption ( + SocketOptionLevel.Socket, + SocketOptionName.ReuseAddress, + true + ); + } + + try { + _listener.Start (); + } + catch (Exception ex) { + var msg = "The underlying listener has failed to start."; + + throw new InvalidOperationException (msg, ex); + } + + var receiver = new ThreadStart (receiveRequest); + _receiveThread = new Thread (receiver); + _receiveThread.IsBackground = true; + + _receiveThread.Start (); + } + + private void stop (ushort code, string reason) + { + lock (_sync) { + if (_state != ServerState.Start) + return; + + _state = ServerState.ShuttingDown; + } + + try { + var timeout = 5000; + + stopReceiving (timeout); + } + catch (Exception ex) { + _log.Fatal (ex.Message); + _log.Debug (ex.ToString ()); + } + + try { + _services.Stop (code, reason); + } + catch (Exception ex) { + _log.Fatal (ex.Message); + _log.Debug (ex.ToString ()); + } + + _state = ServerState.Stop; + } + + private void stopReceiving (int millisecondsTimeout) + { + _listener.Stop (); + _receiveThread.Join (millisecondsTimeout); + } + + private static bool tryCreateUri ( + string uriString, + out Uri result, + out string message + ) + { + if (!uriString.TryCreateWebSocketUri (out result, out message)) + return false; + + if (result.PathAndQuery != "/") { + result = null; + message = "It includes either or both path and query components."; + + return false; + } + + return true; + } + + #endregion + + #region Public Methods + + /// + /// Adds a WebSocket service with the specified behavior and path. + /// + /// + /// + /// A that specifies an absolute path to + /// the service to add. + /// + /// + /// / is trimmed from the end of the string if present. + /// + /// + /// + /// + /// The type of the behavior for the service. + /// + /// + /// It must inherit the class. + /// + /// + /// Also it must have a public parameterless constructor. + /// + /// + /// + /// + /// is an empty string. + /// + /// + /// -or- + /// + /// + /// is not an absolute path. + /// + /// + /// -or- + /// + /// + /// includes either or both + /// query and fragment components. + /// + /// + /// -or- + /// + /// + /// is already in use. + /// + /// + /// + /// is . + /// + public void AddWebSocketService (string path) + where TBehavior : WebSocketBehavior, new () + { + _services.AddService (path, null); + } + + /// + /// Adds a WebSocket service with the specified behavior, path, + /// and initializer. + /// + /// + /// + /// A that specifies an absolute path to + /// the service to add. + /// + /// + /// / is trimmed from the end of the string if present. + /// + /// + /// + /// + /// An delegate. + /// + /// + /// It specifies the delegate called when the service initializes + /// a new session instance. + /// + /// + /// if not necessary. + /// + /// + /// + /// + /// The type of the behavior for the service. + /// + /// + /// It must inherit the class. + /// + /// + /// Also it must have a public parameterless constructor. + /// + /// + /// + /// + /// is an empty string. + /// + /// + /// -or- + /// + /// + /// is not an absolute path. + /// + /// + /// -or- + /// + /// + /// includes either or both + /// query and fragment components. + /// + /// + /// -or- + /// + /// + /// is already in use. + /// + /// + /// + /// is . + /// + public void AddWebSocketService ( + string path, + Action initializer + ) + where TBehavior : WebSocketBehavior, new () + { + _services.AddService (path, initializer); + } + + /// + /// Removes a WebSocket service with the specified path. + /// + /// + /// The service is stopped with close status 1001 (going away) + /// if the current state of the service is Start. + /// + /// + /// true if the service is successfully found and removed; + /// otherwise, false. + /// + /// + /// + /// A that specifies an absolute path to + /// the service to remove. + /// + /// + /// / is trimmed from the end of the string if present. + /// + /// + /// + /// + /// is an empty string. + /// + /// + /// -or- + /// + /// + /// is not an absolute path. + /// + /// + /// -or- + /// + /// + /// includes either or both + /// query and fragment components. + /// + /// + /// + /// is . + /// + public bool RemoveWebSocketService (string path) + { + return _services.RemoveService (path); + } + + /// + /// Starts receiving incoming handshake requests. + /// + /// + /// This method works if the current state of the server is Ready or Stop. + /// + /// + /// + /// There is no server certificate for secure connection. + /// + /// + /// -or- + /// + /// + /// The underlying has failed to start. + /// + /// + public void Start () + { + if (_state == ServerState.Start || _state == ServerState.ShuttingDown) + return; + + start (); + } + + /// + /// Stops receiving incoming handshake requests. + /// + /// + /// This method works if the current state of the server is Start. + /// + public void Stop () + { + if (_state != ServerState.Start) + return; + + stop (1001, String.Empty); + } + + #endregion + } +} diff --git a/Assets/External/websocket-sharp/Server/WebSocketServer.cs.meta b/Assets/External/websocket-sharp/Server/WebSocketServer.cs.meta new file mode 100644 index 00000000..b3ad2911 --- /dev/null +++ b/Assets/External/websocket-sharp/Server/WebSocketServer.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 49525996bcadedf4fa4e6f2d9e20a97e \ No newline at end of file diff --git a/Assets/External/websocket-sharp/Server/WebSocketServiceHost.cs b/Assets/External/websocket-sharp/Server/WebSocketServiceHost.cs new file mode 100644 index 00000000..ee20d92a --- /dev/null +++ b/Assets/External/websocket-sharp/Server/WebSocketServiceHost.cs @@ -0,0 +1,227 @@ +#region License +/* + * WebSocketServiceHost.cs + * + * The MIT License + * + * Copyright (c) 2012-2023 sta.blockhead + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#endregion + +#region Contributors +/* + * Contributors: + * - Juan Manuel Lallana + */ +#endregion + +using System; +using WebSocketSharp.Net.WebSockets; + +namespace WebSocketSharp.Server +{ + /// + /// Exposes the methods and properties used to access the information in + /// a WebSocket service provided by the or + /// class. + /// + /// + /// This class is an abstract class. + /// + public abstract class WebSocketServiceHost + { + #region Private Fields + + private Logger _log; + private string _path; + private WebSocketSessionManager _sessions; + + #endregion + + #region Protected Constructors + + /// + /// Initializes a new instance of the + /// class with the specified path and logging function. + /// + /// + /// A that specifies the absolute path to + /// the service. + /// + /// + /// A that specifies the logging function for + /// the service. + /// + protected WebSocketServiceHost (string path, Logger log) + { + _path = path; + _log = log; + + _sessions = new WebSocketSessionManager (log); + } + + #endregion + + #region Internal Properties + + internal ServerState State { + get { + return _sessions.State; + } + } + + #endregion + + #region Protected Properties + + /// + /// Gets the logging function for the service. + /// + /// + /// A that provides the logging function. + /// + protected Logger Log { + get { + return _log; + } + } + + #endregion + + #region Public Properties + + /// + /// Gets or sets a value indicating whether the service cleans up + /// the inactive sessions periodically. + /// + /// + /// The set operation works if the current state of the service is + /// Ready or Stop. + /// + /// + /// true if the service cleans up the inactive sessions every + /// 60 seconds; otherwise, false. + /// + public bool KeepClean { + get { + return _sessions.KeepClean; + } + + set { + _sessions.KeepClean = value; + } + } + + /// + /// Gets the path to the service. + /// + /// + /// A that represents the absolute path to + /// the service. + /// + public string Path { + get { + return _path; + } + } + + /// + /// Gets the management function for the sessions in the service. + /// + /// + /// A that manages the sessions in + /// the service. + /// + public WebSocketSessionManager Sessions { + get { + return _sessions; + } + } + + /// + /// Gets the type of the behavior of the service. + /// + /// + /// A that represents the type of the behavior of + /// the service. + /// + public abstract Type BehaviorType { get; } + + /// + /// Gets or sets the time to wait for the response to the WebSocket + /// Ping or Close. + /// + /// + /// The set operation works if the current state of the service is + /// Ready or Stop. + /// + /// + /// A that represents the time to wait for + /// the response. + /// + /// + /// The value specified for a set operation is zero or less. + /// + public TimeSpan WaitTime { + get { + return _sessions.WaitTime; + } + + set { + _sessions.WaitTime = value; + } + } + + #endregion + + #region Internal Methods + + internal void Start () + { + _sessions.Start (); + } + + internal void StartSession (WebSocketContext context) + { + CreateSession ().Start (context, _sessions); + } + + internal void Stop (ushort code, string reason) + { + _sessions.Stop (code, reason); + } + + #endregion + + #region Protected Methods + + /// + /// Creates a new session for the service. + /// + /// + /// A instance that represents + /// the new session. + /// + protected abstract WebSocketBehavior CreateSession (); + + #endregion + } +} diff --git a/Assets/External/websocket-sharp/Server/WebSocketServiceHost.cs.meta b/Assets/External/websocket-sharp/Server/WebSocketServiceHost.cs.meta new file mode 100644 index 00000000..c6a57d68 --- /dev/null +++ b/Assets/External/websocket-sharp/Server/WebSocketServiceHost.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: c23b3e16ef5430845954233291ba2fca \ No newline at end of file diff --git a/Assets/External/websocket-sharp/Server/WebSocketServiceHost`1.cs b/Assets/External/websocket-sharp/Server/WebSocketServiceHost`1.cs new file mode 100644 index 00000000..8aac424e --- /dev/null +++ b/Assets/External/websocket-sharp/Server/WebSocketServiceHost`1.cs @@ -0,0 +1,95 @@ +#region License +/* + * WebSocketServiceHost`1.cs + * + * The MIT License + * + * Copyright (c) 2015-2024 sta.blockhead + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#endregion + +using System; + +namespace WebSocketSharp.Server +{ + internal class WebSocketServiceHost : WebSocketServiceHost + where TBehavior : WebSocketBehavior, new () + { + #region Private Fields + + private Func _creator; + + #endregion + + #region Internal Constructors + + internal WebSocketServiceHost ( + string path, + Action initializer, + Logger log + ) + : base (path, log) + { + _creator = createSessionCreator (initializer); + } + + #endregion + + #region Public Properties + + public override Type BehaviorType { + get { + return typeof (TBehavior); + } + } + + #endregion + + #region Private Methods + + private static Func createSessionCreator ( + Action initializer + ) + { + if (initializer == null) + return () => new TBehavior (); + + return () => { + var ret = new TBehavior (); + + initializer (ret); + + return ret; + }; + } + + #endregion + + #region Protected Methods + + protected override WebSocketBehavior CreateSession () + { + return _creator (); + } + + #endregion + } +} diff --git a/Assets/External/websocket-sharp/Server/WebSocketServiceHost`1.cs.meta b/Assets/External/websocket-sharp/Server/WebSocketServiceHost`1.cs.meta new file mode 100644 index 00000000..777c0961 --- /dev/null +++ b/Assets/External/websocket-sharp/Server/WebSocketServiceHost`1.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: e0aafad565049ac4d96a16ce24b080e9 \ No newline at end of file diff --git a/Assets/External/websocket-sharp/Server/WebSocketServiceManager.cs b/Assets/External/websocket-sharp/Server/WebSocketServiceManager.cs new file mode 100644 index 00000000..29021062 --- /dev/null +++ b/Assets/External/websocket-sharp/Server/WebSocketServiceManager.cs @@ -0,0 +1,614 @@ +#region License +/* + * WebSocketServiceManager.cs + * + * The MIT License + * + * Copyright (c) 2012-2024 sta.blockhead + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#endregion + +using System; +using System.Collections; +using System.Collections.Generic; + +namespace WebSocketSharp.Server +{ + /// + /// Provides the management function for the WebSocket services. + /// + /// + /// This class manages the WebSocket services provided by the + /// or class. + /// + public class WebSocketServiceManager + { + #region Private Fields + + private Dictionary _hosts; + private volatile bool _keepClean; + private Logger _log; + private volatile ServerState _state; + private object _sync; + private TimeSpan _waitTime; + + #endregion + + #region Internal Constructors + + internal WebSocketServiceManager (Logger log) + { + _log = log; + + _hosts = new Dictionary (); + _state = ServerState.Ready; + _sync = ((ICollection) _hosts).SyncRoot; + _waitTime = TimeSpan.FromSeconds (1); + } + + #endregion + + #region Public Properties + + /// + /// Gets the number of the WebSocket services. + /// + /// + /// An that represents the number of the services. + /// + public int Count { + get { + lock (_sync) + return _hosts.Count; + } + } + + /// + /// Gets the service host instances for the WebSocket services. + /// + /// + /// + /// An + /// instance. + /// + /// + /// It provides an enumerator which supports the iteration over + /// the collection of the service host instances. + /// + /// + public IEnumerable Hosts { + get { + lock (_sync) + return _hosts.Values.ToList (); + } + } + + /// + /// Gets the service host instance for a WebSocket service with + /// the specified path. + /// + /// + /// + /// A instance that represents + /// the service host instance. + /// + /// + /// It provides the function to access the information in the service. + /// + /// + /// if not found. + /// + /// + /// + /// + /// A that specifies an absolute path to + /// the service to get. + /// + /// + /// / is trimmed from the end of the string if present. + /// + /// + /// + /// + /// is an empty string. + /// + /// + /// -or- + /// + /// + /// is not an absolute path. + /// + /// + /// -or- + /// + /// + /// includes either or both + /// query and fragment components. + /// + /// + /// + /// is . + /// + public WebSocketServiceHost this[string path] { + get { + if (path == null) + throw new ArgumentNullException ("path"); + + if (path.Length == 0) + throw new ArgumentException ("An empty string.", "path"); + + if (path[0] != '/') { + var msg = "Not an absolute path."; + + throw new ArgumentException (msg, "path"); + } + + if (path.IndexOfAny (new[] { '?', '#' }) > -1) { + var msg = "It includes either or both query and fragment components."; + + throw new ArgumentException (msg, "path"); + } + + WebSocketServiceHost host; + + InternalTryGetServiceHost (path, out host); + + return host; + } + } + + /// + /// Gets or sets a value indicating whether the inactive sessions in + /// the WebSocket services are cleaned up periodically. + /// + /// + /// The set operation works if the current state of the server is + /// Ready or Stop. + /// + /// + /// + /// true if the inactive sessions are cleaned up every 60 + /// seconds; otherwise, false. + /// + /// + /// The default value is false. + /// + /// + public bool KeepClean { + get { + return _keepClean; + } + + set { + lock (_sync) { + if (!canSet ()) + return; + + foreach (var host in _hosts.Values) + host.KeepClean = value; + + _keepClean = value; + } + } + } + + /// + /// Gets the paths for the WebSocket services. + /// + /// + /// + /// An + /// instance. + /// + /// + /// It provides an enumerator which supports the iteration over + /// the collection of the paths. + /// + /// + public IEnumerable Paths { + get { + lock (_sync) + return _hosts.Keys.ToList (); + } + } + + /// + /// Gets or sets the time to wait for the response to the WebSocket + /// Ping or Close. + /// + /// + /// The set operation works if the current state of the server is + /// Ready or Stop. + /// + /// + /// + /// A that represents the time to wait for + /// the response. + /// + /// + /// The default value is the same as 1 second. + /// + /// + /// + /// The value specified for a set operation is zero or less. + /// + public TimeSpan WaitTime { + get { + return _waitTime; + } + + set { + if (value <= TimeSpan.Zero) { + var msg = "Zero or less."; + + throw new ArgumentOutOfRangeException ("value", msg); + } + + lock (_sync) { + if (!canSet ()) + return; + + foreach (var host in _hosts.Values) + host.WaitTime = value; + + _waitTime = value; + } + } + } + + #endregion + + #region Private Methods + + private bool canSet () + { + return _state == ServerState.Ready || _state == ServerState.Stop; + } + + #endregion + + #region Internal Methods + + internal bool InternalTryGetServiceHost ( + string path, + out WebSocketServiceHost host + ) + { + path = path.TrimSlashFromEnd (); + + lock (_sync) + return _hosts.TryGetValue (path, out host); + } + + internal void Start () + { + lock (_sync) { + foreach (var host in _hosts.Values) + host.Start (); + + _state = ServerState.Start; + } + } + + internal void Stop (ushort code, string reason) + { + lock (_sync) { + _state = ServerState.ShuttingDown; + + foreach (var host in _hosts.Values) + host.Stop (code, reason); + + _state = ServerState.Stop; + } + } + + #endregion + + #region Public Methods + + /// + /// Adds a WebSocket service with the specified behavior, path, + /// and initializer. + /// + /// + /// + /// A that specifies an absolute path to + /// the service to add. + /// + /// + /// / is trimmed from the end of the string if present. + /// + /// + /// + /// + /// An delegate. + /// + /// + /// It specifies the delegate called when the service initializes + /// a new session instance. + /// + /// + /// if not necessary. + /// + /// + /// + /// + /// The type of the behavior for the service. + /// + /// + /// It must inherit the class. + /// + /// + /// Also it must have a public parameterless constructor. + /// + /// + /// + /// + /// is an empty string. + /// + /// + /// -or- + /// + /// + /// is not an absolute path. + /// + /// + /// -or- + /// + /// + /// includes either or both + /// query and fragment components. + /// + /// + /// -or- + /// + /// + /// is already in use. + /// + /// + /// + /// is . + /// + public void AddService ( + string path, + Action initializer + ) + where TBehavior : WebSocketBehavior, new () + { + if (path == null) + throw new ArgumentNullException ("path"); + + if (path.Length == 0) + throw new ArgumentException ("An empty string.", "path"); + + if (path[0] != '/') { + var msg = "Not an absolute path."; + + throw new ArgumentException (msg, "path"); + } + + if (path.IndexOfAny (new[] { '?', '#' }) > -1) { + var msg = "It includes either or both query and fragment components."; + + throw new ArgumentException (msg, "path"); + } + + path = path.TrimSlashFromEnd (); + + lock (_sync) { + WebSocketServiceHost host; + + if (_hosts.TryGetValue (path, out host)) { + var msg = "It is already in use."; + + throw new ArgumentException (msg, "path"); + } + + host = new WebSocketServiceHost (path, initializer, _log); + + if (_keepClean) + host.KeepClean = true; + + if (_waitTime != host.WaitTime) + host.WaitTime = _waitTime; + + if (_state == ServerState.Start) + host.Start (); + + _hosts.Add (path, host); + } + } + + /// + /// Removes all WebSocket services managed by the manager. + /// + /// + /// Each service is stopped with close status 1001 (going away) + /// if the current state of the service is Start. + /// + public void Clear () + { + List hosts = null; + + lock (_sync) { + hosts = _hosts.Values.ToList (); + + _hosts.Clear (); + } + + foreach (var host in hosts) { + if (host.State == ServerState.Start) + host.Stop (1001, String.Empty); + } + } + + /// + /// Removes a WebSocket service with the specified path. + /// + /// + /// The service is stopped with close status 1001 (going away) + /// if the current state of the service is Start. + /// + /// + /// true if the service is successfully found and removed; + /// otherwise, false. + /// + /// + /// + /// A that specifies an absolute path to + /// the service to remove. + /// + /// + /// / is trimmed from the end of the string if present. + /// + /// + /// + /// + /// is an empty string. + /// + /// + /// -or- + /// + /// + /// is not an absolute path. + /// + /// + /// -or- + /// + /// + /// includes either or both + /// query and fragment components. + /// + /// + /// + /// is . + /// + public bool RemoveService (string path) + { + if (path == null) + throw new ArgumentNullException ("path"); + + if (path.Length == 0) + throw new ArgumentException ("An empty string.", "path"); + + if (path[0] != '/') { + var msg = "Not an absolute path."; + + throw new ArgumentException (msg, "path"); + } + + if (path.IndexOfAny (new[] { '?', '#' }) > -1) { + var msg = "It includes either or both query and fragment components."; + + throw new ArgumentException (msg, "path"); + } + + path = path.TrimSlashFromEnd (); + WebSocketServiceHost host; + + lock (_sync) { + if (!_hosts.TryGetValue (path, out host)) + return false; + + _hosts.Remove (path); + } + + if (host.State == ServerState.Start) + host.Stop (1001, String.Empty); + + return true; + } + + /// + /// Tries to get the service host instance for a WebSocket service with + /// the specified path. + /// + /// + /// true if the try has succeeded; otherwise, false. + /// + /// + /// + /// A that specifies an absolute path to + /// the service to get. + /// + /// + /// / is trimmed from the end of the string if present. + /// + /// + /// + /// + /// When this method returns, a + /// instance that receives the service host instance. + /// + /// + /// It provides the function to access the information in the service. + /// + /// + /// if not found. + /// + /// + /// + /// + /// is an empty string. + /// + /// + /// -or- + /// + /// + /// is not an absolute path. + /// + /// + /// -or- + /// + /// + /// includes either or both + /// query and fragment components. + /// + /// + /// + /// is . + /// + public bool TryGetServiceHost (string path, out WebSocketServiceHost host) + { + if (path == null) + throw new ArgumentNullException ("path"); + + if (path.Length == 0) + throw new ArgumentException ("An empty string.", "path"); + + if (path[0] != '/') { + var msg = "Not an absolute path."; + + throw new ArgumentException (msg, "path"); + } + + if (path.IndexOfAny (new[] { '?', '#' }) > -1) { + var msg = "It includes either or both query and fragment components."; + + throw new ArgumentException (msg, "path"); + } + + return InternalTryGetServiceHost (path, out host); + } + + #endregion + } +} diff --git a/Assets/External/websocket-sharp/Server/WebSocketServiceManager.cs.meta b/Assets/External/websocket-sharp/Server/WebSocketServiceManager.cs.meta new file mode 100644 index 00000000..81f73156 --- /dev/null +++ b/Assets/External/websocket-sharp/Server/WebSocketServiceManager.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 40c8453c75273b241844a2b11ebfb1e0 \ No newline at end of file diff --git a/Assets/External/websocket-sharp/Server/WebSocketSessionManager.cs b/Assets/External/websocket-sharp/Server/WebSocketSessionManager.cs new file mode 100644 index 00000000..43072fcf --- /dev/null +++ b/Assets/External/websocket-sharp/Server/WebSocketSessionManager.cs @@ -0,0 +1,1641 @@ +#region License +/* + * WebSocketSessionManager.cs + * + * The MIT License + * + * Copyright (c) 2012-2025 sta.blockhead + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#endregion + +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; +using System.Timers; + +namespace WebSocketSharp.Server +{ + /// + /// Provides the management function for the sessions in a WebSocket service. + /// + /// + /// This class manages the sessions in a WebSocket service provided by the + /// or class. + /// + public class WebSocketSessionManager + { + #region Private Fields + + private object _forSweep; + private volatile bool _keepClean; + private Logger _log; + private static readonly byte[] _rawEmptyPingFrame; + private Dictionary _sessions; + private volatile ServerState _state; + private volatile bool _sweeping; + private System.Timers.Timer _sweepTimer; + private object _sync; + private TimeSpan _waitTime; + + #endregion + + #region Static Constructor + + static WebSocketSessionManager () + { + _rawEmptyPingFrame = WebSocketFrame.CreatePingFrame (false).ToArray (); + } + + #endregion + + #region Internal Constructors + + internal WebSocketSessionManager (Logger log) + { + _log = log; + + _forSweep = new object (); + _sessions = new Dictionary (); + _state = ServerState.Ready; + _sync = ((ICollection) _sessions).SyncRoot; + _waitTime = TimeSpan.FromSeconds (1); + + setSweepTimer (60000); + } + + #endregion + + #region Internal Properties + + internal ServerState State { + get { + return _state; + } + } + + #endregion + + #region Public Properties + + /// + /// Gets the IDs for the active sessions in the WebSocket service. + /// + /// + /// + /// An + /// instance. + /// + /// + /// It provides an enumerator which supports the iteration over + /// the collection of the IDs for the active sessions. + /// + /// + public IEnumerable ActiveIDs { + get { + foreach (var res in broadping (_rawEmptyPingFrame)) { + if (res.Value) + yield return res.Key; + } + } + } + + /// + /// Gets the number of the sessions in the WebSocket service. + /// + /// + /// An that represents the number of the sessions. + /// + public int Count { + get { + lock (_sync) + return _sessions.Count; + } + } + + /// + /// Gets the IDs for the sessions in the WebSocket service. + /// + /// + /// + /// An + /// instance. + /// + /// + /// It provides an enumerator which supports the iteration over + /// the collection of the IDs for the sessions. + /// + /// + public IEnumerable IDs { + get { + if (_state != ServerState.Start) + return Enumerable.Empty (); + + lock (_sync) { + if (_state != ServerState.Start) + return Enumerable.Empty (); + + return _sessions.Keys.ToList (); + } + } + } + + /// + /// Gets the IDs for the inactive sessions in the WebSocket service. + /// + /// + /// + /// An + /// instance. + /// + /// + /// It provides an enumerator which supports the iteration over + /// the collection of the IDs for the inactive sessions. + /// + /// + public IEnumerable InactiveIDs { + get { + foreach (var res in broadping (_rawEmptyPingFrame)) { + if (!res.Value) + yield return res.Key; + } + } + } + + /// + /// Gets the session instance with the specified ID. + /// + /// + /// + /// A instance that provides + /// the function to access the information in the session. + /// + /// + /// if not found. + /// + /// + /// + /// A that specifies the ID of the session to get. + /// + /// + /// is an empty string. + /// + /// + /// is . + /// + public IWebSocketSession this[string id] { + get { + if (id == null) + throw new ArgumentNullException ("id"); + + if (id.Length == 0) + throw new ArgumentException ("An empty string.", "id"); + + IWebSocketSession session; + + tryGetSession (id, out session); + + return session; + } + } + + /// + /// Gets or sets a value indicating whether the inactive sessions in + /// the WebSocket service are cleaned up periodically. + /// + /// + /// The set operation works if the current state of the service is + /// Ready or Stop. + /// + /// + /// true if the inactive sessions are cleaned up every 60 seconds; + /// otherwise, false. + /// + public bool KeepClean { + get { + return _keepClean; + } + + set { + lock (_sync) { + if (!canSet ()) + return; + + _keepClean = value; + } + } + } + + /// + /// Gets the session instances in the WebSocket service. + /// + /// + /// + /// An + /// instance. + /// + /// + /// It provides an enumerator which supports the iteration over + /// the collection of the session instances. + /// + /// + public IEnumerable Sessions { + get { + if (_state != ServerState.Start) + return Enumerable.Empty (); + + lock (_sync) { + if (_state != ServerState.Start) + return Enumerable.Empty (); + + return _sessions.Values.ToList (); + } + } + } + + /// + /// Gets or sets the time to wait for the response to the WebSocket + /// Ping or Close. + /// + /// + /// The set operation works if the current state of the service is + /// Ready or Stop. + /// + /// + /// A that represents the time to wait for + /// the response. + /// + /// + /// The value specified for a set operation is zero or less. + /// + public TimeSpan WaitTime { + get { + return _waitTime; + } + + set { + if (value <= TimeSpan.Zero) { + var msg = "Zero or less."; + + throw new ArgumentOutOfRangeException ("value", msg); + } + + lock (_sync) { + if (!canSet ()) + return; + + _waitTime = value; + } + } + } + + #endregion + + #region Private Methods + + private void broadcast (Opcode opcode, byte[] data, Action completed) + { + var cache = new Dictionary (); + + try { + foreach (var session in Sessions) { + if (_state != ServerState.Start) { + _log.Error ("The send is cancelled."); + + break; + } + + session.WebSocket.Send (opcode, data, cache); + } + + if (completed != null) + completed (); + } + catch (Exception ex) { + _log.Error (ex.Message); + _log.Debug (ex.ToString ()); + } + finally { + cache.Clear (); + } + } + + private void broadcast ( + Opcode opcode, + Stream sourceStream, + Action completed + ) + { + var cache = new Dictionary (); + + try { + foreach (var session in Sessions) { + if (_state != ServerState.Start) { + _log.Error ("The send is cancelled."); + + break; + } + + session.WebSocket.Send (opcode, sourceStream, cache); + } + + if (completed != null) + completed (); + } + catch (Exception ex) { + _log.Error (ex.Message); + _log.Debug (ex.ToString ()); + } + finally { + foreach (var cached in cache.Values) + cached.Dispose (); + + cache.Clear (); + } + } + + private void broadcastAsync (Opcode opcode, byte[] data, Action completed) + { + ThreadPool.QueueUserWorkItem ( + state => broadcast (opcode, data, completed) + ); + } + + private void broadcastAsync ( + Opcode opcode, + Stream sourceStream, + Action completed + ) + { + ThreadPool.QueueUserWorkItem ( + state => broadcast (opcode, sourceStream, completed) + ); + } + + private Dictionary broadping (byte[] rawFrame) + { + var ret = new Dictionary (); + + foreach (var session in Sessions) { + if (_state != ServerState.Start) { + ret.Clear (); + + break; + } + + var res = session.WebSocket.Ping (rawFrame); + + ret.Add (session.ID, res); + } + + return ret; + } + + private bool canSet () + { + return _state == ServerState.Ready || _state == ServerState.Stop; + } + + private static string createID () + { + return Guid.NewGuid ().ToString ("N"); + } + + private void setSweepTimer (double interval) + { + _sweepTimer = new System.Timers.Timer (interval); + _sweepTimer.Elapsed += (sender, e) => Sweep (); + } + + private void stop (PayloadData payloadData, bool send) + { + var rawFrame = send + ? WebSocketFrame + .CreateCloseFrame (payloadData, false) + .ToArray () + : null; + + lock (_sync) { + _state = ServerState.ShuttingDown; + _sweepTimer.Enabled = false; + + foreach (var session in _sessions.Values.ToList ()) + session.WebSocket.Close (payloadData, rawFrame); + + _state = ServerState.Stop; + } + } + + private bool tryGetSession (string id, out IWebSocketSession session) + { + session = null; + + if (_state != ServerState.Start) + return false; + + lock (_sync) { + if (_state != ServerState.Start) + return false; + + return _sessions.TryGetValue (id, out session); + } + } + + #endregion + + #region Internal Methods + + internal string Add (IWebSocketSession session) + { + lock (_sync) { + if (_state != ServerState.Start) + return null; + + var id = createID (); + + _sessions.Add (id, session); + + return id; + } + } + + internal bool Remove (string id) + { + lock (_sync) + return _sessions.Remove (id); + } + + internal void Start () + { + lock (_sync) { + _sweepTimer.Enabled = _keepClean; + _state = ServerState.Start; + } + } + + internal void Stop (ushort code, string reason) + { + if (code == 1005) { + stop (PayloadData.Empty, true); + + return; + } + + var payloadData = new PayloadData (code, reason); + var send = !code.IsReservedStatusCode (); + + stop (payloadData, send); + } + + #endregion + + #region Public Methods + + /// + /// Sends the specified data to every client in the WebSocket service. + /// + /// + /// An array of that specifies the binary data to send. + /// + /// + /// is . + /// + /// + /// The current state of the service is not Start. + /// + public void Broadcast (byte[] data) + { + if (_state != ServerState.Start) { + var msg = "The current state of the service is not Start."; + + throw new InvalidOperationException (msg); + } + + if (data == null) + throw new ArgumentNullException ("data"); + + if (data.LongLength <= WebSocket.FragmentLength) + broadcast (Opcode.Binary, data, null); + else + broadcast (Opcode.Binary, new MemoryStream (data), null); + } + + /// + /// Sends the specified data to every client in the WebSocket service. + /// + /// + /// A that specifies the text data to send. + /// + /// + /// could not be UTF-8-encoded. + /// + /// + /// is . + /// + /// + /// The current state of the service is not Start. + /// + public void Broadcast (string data) + { + if (_state != ServerState.Start) { + var msg = "The current state of the service is not Start."; + + throw new InvalidOperationException (msg); + } + + if (data == null) + throw new ArgumentNullException ("data"); + + byte[] bytes; + + if (!data.TryGetUTF8EncodedBytes (out bytes)) { + var msg = "It could not be UTF-8-encoded."; + + throw new ArgumentException (msg, "data"); + } + + if (bytes.LongLength <= WebSocket.FragmentLength) + broadcast (Opcode.Text, bytes, null); + else + broadcast (Opcode.Text, new MemoryStream (bytes), null); + } + + /// + /// Sends the data from the specified stream instance to every client in + /// the WebSocket service. + /// + /// + /// + /// A instance from which to read the data to send. + /// + /// + /// The data is sent as the binary data. + /// + /// + /// + /// An that specifies the number of bytes to send. + /// + /// + /// + /// cannot be read. + /// + /// + /// -or- + /// + /// + /// is less than 1. + /// + /// + /// -or- + /// + /// + /// No data could be read from . + /// + /// + /// + /// is . + /// + /// + /// The current state of the service is not Start. + /// + public void Broadcast (Stream stream, int length) + { + if (_state != ServerState.Start) { + var msg = "The current state of the service is not Start."; + + throw new InvalidOperationException (msg); + } + + if (stream == null) + throw new ArgumentNullException ("stream"); + + if (!stream.CanRead) { + var msg = "It cannot be read."; + + throw new ArgumentException (msg, "stream"); + } + + if (length < 1) { + var msg = "Less than 1."; + + throw new ArgumentException (msg, "length"); + } + + var bytes = stream.ReadBytes (length); + var len = bytes.Length; + + if (len == 0) { + var msg = "No data could be read from it."; + + throw new ArgumentException (msg, "stream"); + } + + if (len < length) { + var fmt = "Only {0} byte(s) of data could be read from the stream."; + var msg = String.Format (fmt, len); + + _log.Warn (msg); + } + + if (len <= WebSocket.FragmentLength) + broadcast (Opcode.Binary, bytes, null); + else + broadcast (Opcode.Binary, new MemoryStream (bytes), null); + } + + /// + /// Sends the specified data to every client in the WebSocket service + /// asynchronously. + /// + /// + /// This method does not wait for the send to be complete. + /// + /// + /// An array of that specifies the binary data to send. + /// + /// + /// + /// An delegate. + /// + /// + /// It specifies the delegate called when the send is complete. + /// + /// + /// if not necessary. + /// + /// + /// + /// is . + /// + /// + /// The current state of the service is not Start. + /// + public void BroadcastAsync (byte[] data, Action completed) + { + if (_state != ServerState.Start) { + var msg = "The current state of the service is not Start."; + + throw new InvalidOperationException (msg); + } + + if (data == null) + throw new ArgumentNullException ("data"); + + if (data.LongLength <= WebSocket.FragmentLength) + broadcastAsync (Opcode.Binary, data, completed); + else + broadcastAsync (Opcode.Binary, new MemoryStream (data), completed); + } + + /// + /// Sends the specified data to every client in the WebSocket service + /// asynchronously. + /// + /// + /// This method does not wait for the send to be complete. + /// + /// + /// A that specifies the text data to send. + /// + /// + /// + /// An delegate. + /// + /// + /// It specifies the delegate called when the send is complete. + /// + /// + /// if not necessary. + /// + /// + /// + /// could not be UTF-8-encoded. + /// + /// + /// is . + /// + /// + /// The current state of the service is not Start. + /// + public void BroadcastAsync (string data, Action completed) + { + if (_state != ServerState.Start) { + var msg = "The current state of the service is not Start."; + + throw new InvalidOperationException (msg); + } + + if (data == null) + throw new ArgumentNullException ("data"); + + byte[] bytes; + + if (!data.TryGetUTF8EncodedBytes (out bytes)) { + var msg = "It could not be UTF-8-encoded."; + + throw new ArgumentException (msg, "data"); + } + + if (bytes.LongLength <= WebSocket.FragmentLength) + broadcastAsync (Opcode.Text, bytes, completed); + else + broadcastAsync (Opcode.Text, new MemoryStream (bytes), completed); + } + + /// + /// Sends the data from the specified stream instance to every client in + /// the WebSocket service asynchronously. + /// + /// + /// This method does not wait for the send to be complete. + /// + /// + /// + /// A instance from which to read the data to send. + /// + /// + /// The data is sent as the binary data. + /// + /// + /// + /// An that specifies the number of bytes to send. + /// + /// + /// + /// An delegate. + /// + /// + /// It specifies the delegate called when the send is complete. + /// + /// + /// if not necessary. + /// + /// + /// + /// + /// cannot be read. + /// + /// + /// -or- + /// + /// + /// is less than 1. + /// + /// + /// -or- + /// + /// + /// No data could be read from . + /// + /// + /// + /// is . + /// + /// + /// The current state of the service is not Start. + /// + public void BroadcastAsync (Stream stream, int length, Action completed) + { + if (_state != ServerState.Start) { + var msg = "The current state of the service is not Start."; + + throw new InvalidOperationException (msg); + } + + if (stream == null) + throw new ArgumentNullException ("stream"); + + if (!stream.CanRead) { + var msg = "It cannot be read."; + + throw new ArgumentException (msg, "stream"); + } + + if (length < 1) { + var msg = "Less than 1."; + + throw new ArgumentException (msg, "length"); + } + + var bytes = stream.ReadBytes (length); + var len = bytes.Length; + + if (len == 0) { + var msg = "No data could be read from it."; + + throw new ArgumentException (msg, "stream"); + } + + if (len < length) { + var fmt = "Only {0} byte(s) of data could be read from the stream."; + var msg = String.Format (fmt, len); + + _log.Warn (msg); + } + + if (len <= WebSocket.FragmentLength) + broadcastAsync (Opcode.Binary, bytes, completed); + else + broadcastAsync (Opcode.Binary, new MemoryStream (bytes), completed); + } + + /// + /// Closes the session with the specified ID. + /// + /// + /// A that specifies the ID of the session to close. + /// + /// + /// is an empty string. + /// + /// + /// is . + /// + /// + /// The session could not be found. + /// + public void CloseSession (string id) + { + IWebSocketSession session; + + if (!TryGetSession (id, out session)) { + var msg = "The session could not be found."; + + throw new InvalidOperationException (msg); + } + + session.WebSocket.Close (); + } + + /// + /// Closes the session with the specified ID, status code, and reason. + /// + /// + /// A that specifies the ID of the session to close. + /// + /// + /// + /// A that specifies the status code indicating + /// the reason for the close. + /// + /// + /// The status codes are defined in + /// + /// Section 7.4 of RFC 6455. + /// + /// + /// + /// + /// A that specifies the reason for the close. + /// + /// + /// Its size must be 123 bytes or less in UTF-8. + /// + /// + /// + /// + /// is an empty string. + /// + /// + /// -or- + /// + /// + /// is 1010 (mandatory extension). + /// + /// + /// -or- + /// + /// + /// is 1005 (no status) and + /// is specified. + /// + /// + /// -or- + /// + /// + /// could not be UTF-8-encoded. + /// + /// + /// + /// is . + /// + /// + /// + /// is less than 1000 or greater than 4999. + /// + /// + /// -or- + /// + /// + /// The size of is greater than 123 bytes. + /// + /// + /// + /// The session could not be found. + /// + public void CloseSession (string id, ushort code, string reason) + { + IWebSocketSession session; + + if (!TryGetSession (id, out session)) { + var msg = "The session could not be found."; + + throw new InvalidOperationException (msg); + } + + session.WebSocket.Close (code, reason); + } + + /// + /// Closes the session with the specified ID, status code, and reason. + /// + /// + /// A that specifies the ID of the session to close. + /// + /// + /// + /// One of the enum values. + /// + /// + /// It specifies the status code indicating the reason for the close. + /// + /// + /// + /// + /// A that specifies the reason for the close. + /// + /// + /// Its size must be 123 bytes or less in UTF-8. + /// + /// + /// + /// + /// is an empty string. + /// + /// + /// -or- + /// + /// + /// is an undefined enum value. + /// + /// + /// -or- + /// + /// + /// is . + /// + /// + /// -or- + /// + /// + /// is and + /// is specified. + /// + /// + /// -or- + /// + /// + /// could not be UTF-8-encoded. + /// + /// + /// + /// is . + /// + /// + /// The size of is greater than 123 bytes. + /// + /// + /// The session could not be found. + /// + public void CloseSession (string id, CloseStatusCode code, string reason) + { + IWebSocketSession session; + + if (!TryGetSession (id, out session)) { + var msg = "The session could not be found."; + + throw new InvalidOperationException (msg); + } + + session.WebSocket.Close (code, reason); + } + + /// + /// Sends a ping to the client using the specified session. + /// + /// + /// true if the send has successfully done and a pong has been + /// received within a time; otherwise, false. + /// + /// + /// A that specifies the ID of the session. + /// + /// + /// is an empty string. + /// + /// + /// is . + /// + /// + /// The session could not be found. + /// + public bool PingTo (string id) + { + IWebSocketSession session; + + if (!TryGetSession (id, out session)) { + var msg = "The session could not be found."; + + throw new InvalidOperationException (msg); + } + + return session.WebSocket.Ping (); + } + + /// + /// Sends a ping with the specified message to the client using + /// the specified session. + /// + /// + /// true if the send has successfully done and a pong has been + /// received within a time; otherwise, false. + /// + /// + /// + /// A that specifies the message to send. + /// + /// + /// Its size must be 125 bytes or less in UTF-8. + /// + /// + /// + /// A that specifies the ID of the session. + /// + /// + /// + /// is an empty string. + /// + /// + /// -or- + /// + /// + /// could not be UTF-8-encoded. + /// + /// + /// + /// is . + /// + /// + /// The size of is greater than 125 bytes. + /// + /// + /// The session could not be found. + /// + public bool PingTo (string message, string id) + { + IWebSocketSession session; + + if (!TryGetSession (id, out session)) { + var msg = "The session could not be found."; + + throw new InvalidOperationException (msg); + } + + return session.WebSocket.Ping (message); + } + + /// + /// Sends the specified data to the client using the specified session. + /// + /// + /// An array of that specifies the binary data to send. + /// + /// + /// A that specifies the ID of the session. + /// + /// + /// is an empty string. + /// + /// + /// + /// is . + /// + /// + /// -or- + /// + /// + /// is . + /// + /// + /// + /// + /// The session could not be found. + /// + /// + /// -or- + /// + /// + /// The current state of the WebSocket interface is not Open. + /// + /// + public void SendTo (byte[] data, string id) + { + IWebSocketSession session; + + if (!TryGetSession (id, out session)) { + var msg = "The session could not be found."; + + throw new InvalidOperationException (msg); + } + + session.WebSocket.Send (data); + } + + /// + /// Sends the specified data to the client using the specified session. + /// + /// + /// A that specifies the text data to send. + /// + /// + /// A that specifies the ID of the session. + /// + /// + /// + /// is an empty string. + /// + /// + /// -or- + /// + /// + /// could not be UTF-8-encoded. + /// + /// + /// + /// + /// is . + /// + /// + /// -or- + /// + /// + /// is . + /// + /// + /// + /// + /// The session could not be found. + /// + /// + /// -or- + /// + /// + /// The current state of the WebSocket interface is not Open. + /// + /// + public void SendTo (string data, string id) + { + IWebSocketSession session; + + if (!TryGetSession (id, out session)) { + var msg = "The session could not be found."; + + throw new InvalidOperationException (msg); + } + + session.WebSocket.Send (data); + } + + /// + /// Sends the data from the specified stream instance to the client using + /// the specified session. + /// + /// + /// + /// A instance from which to read the data to send. + /// + /// + /// The data is sent as the binary data. + /// + /// + /// + /// An that specifies the number of bytes to send. + /// + /// + /// A that specifies the ID of the session. + /// + /// + /// + /// is an empty string. + /// + /// + /// -or- + /// + /// + /// cannot be read. + /// + /// + /// -or- + /// + /// + /// is less than 1. + /// + /// + /// -or- + /// + /// + /// No data could be read from . + /// + /// + /// + /// + /// is . + /// + /// + /// -or- + /// + /// + /// is . + /// + /// + /// + /// + /// The session could not be found. + /// + /// + /// -or- + /// + /// + /// The current state of the WebSocket interface is not Open. + /// + /// + public void SendTo (Stream stream, int length, string id) + { + IWebSocketSession session; + + if (!TryGetSession (id, out session)) { + var msg = "The session could not be found."; + + throw new InvalidOperationException (msg); + } + + session.WebSocket.Send (stream, length); + } + + /// + /// Sends the specified data to the client using the specified session + /// asynchronously. + /// + /// + /// This method does not wait for the send to be complete. + /// + /// + /// An array of that specifies the binary data to send. + /// + /// + /// A that specifies the ID of the session. + /// + /// + /// + /// An delegate. + /// + /// + /// It specifies the delegate called when the send is complete. + /// + /// + /// The parameter passed to the delegate is + /// true if the send has successfully done; otherwise, + /// false. + /// + /// + /// if not necessary. + /// + /// + /// + /// is an empty string. + /// + /// + /// + /// is . + /// + /// + /// -or- + /// + /// + /// is . + /// + /// + /// + /// + /// The session could not be found. + /// + /// + /// -or- + /// + /// + /// The current state of the WebSocket interface is not Open. + /// + /// + public void SendToAsync (byte[] data, string id, Action completed) + { + IWebSocketSession session; + + if (!TryGetSession (id, out session)) { + var msg = "The session could not be found."; + + throw new InvalidOperationException (msg); + } + + session.WebSocket.SendAsync (data, completed); + } + + /// + /// Sends the specified data to the client using the specified session + /// asynchronously. + /// + /// + /// This method does not wait for the send to be complete. + /// + /// + /// A that specifies the text data to send. + /// + /// + /// A that specifies the ID of the session. + /// + /// + /// + /// An delegate. + /// + /// + /// It specifies the delegate called when the send is complete. + /// + /// + /// The parameter passed to the delegate is + /// true if the send has successfully done; otherwise, + /// false. + /// + /// + /// if not necessary. + /// + /// + /// + /// + /// is an empty string. + /// + /// + /// -or- + /// + /// + /// could not be UTF-8-encoded. + /// + /// + /// + /// + /// is . + /// + /// + /// -or- + /// + /// + /// is . + /// + /// + /// + /// + /// The session could not be found. + /// + /// + /// -or- + /// + /// + /// The current state of the WebSocket interface is not Open. + /// + /// + public void SendToAsync (string data, string id, Action completed) + { + IWebSocketSession session; + + if (!TryGetSession (id, out session)) { + var msg = "The session could not be found."; + + throw new InvalidOperationException (msg); + } + + session.WebSocket.SendAsync (data, completed); + } + + /// + /// Sends the data from the specified stream instance to the client using + /// the specified session asynchronously. + /// + /// + /// This method does not wait for the send to be complete. + /// + /// + /// + /// A instance from which to read the data to send. + /// + /// + /// The data is sent as the binary data. + /// + /// + /// + /// An that specifies the number of bytes to send. + /// + /// + /// A that specifies the ID of the session. + /// + /// + /// + /// An delegate. + /// + /// + /// It specifies the delegate called when the send is complete. + /// + /// + /// The parameter passed to the delegate is + /// true if the send has successfully done; otherwise, + /// false. + /// + /// + /// if not necessary. + /// + /// + /// + /// + /// is an empty string. + /// + /// + /// -or- + /// + /// + /// cannot be read. + /// + /// + /// -or- + /// + /// + /// is less than 1. + /// + /// + /// -or- + /// + /// + /// No data could be read from . + /// + /// + /// + /// + /// is . + /// + /// + /// -or- + /// + /// + /// is . + /// + /// + /// + /// + /// The session could not be found. + /// + /// + /// -or- + /// + /// + /// The current state of the WebSocket interface is not Open. + /// + /// + public void SendToAsync ( + Stream stream, + int length, + string id, + Action completed + ) + { + IWebSocketSession session; + + if (!TryGetSession (id, out session)) { + var msg = "The session could not be found."; + + throw new InvalidOperationException (msg); + } + + session.WebSocket.SendAsync (stream, length, completed); + } + + /// + /// Cleans up the inactive sessions in the WebSocket service. + /// + public void Sweep () + { + if (_sweeping) { + _log.Trace ("The sweep process is already in progress."); + + return; + } + + lock (_forSweep) { + if (_sweeping) { + _log.Trace ("The sweep process is already in progress."); + + return; + } + + _sweeping = true; + } + + foreach (var id in InactiveIDs) { + if (_state != ServerState.Start) + break; + + lock (_sync) { + if (_state != ServerState.Start) + break; + + IWebSocketSession session; + + if (!_sessions.TryGetValue (id, out session)) + continue; + + var state = session.WebSocket.ReadyState; + + if (state == WebSocketState.Open) { + session.WebSocket.Close (CloseStatusCode.Abnormal); + + continue; + } + + if (state == WebSocketState.Closing) + continue; + + _sessions.Remove (id); + } + } + + lock (_forSweep) + _sweeping = false; + } + + /// + /// Tries to get the session instance with the specified ID. + /// + /// + /// true if the try has succeeded; otherwise, false. + /// + /// + /// A that specifies the ID of the session to get. + /// + /// + /// + /// When this method returns, a instance + /// that receives the session instance. + /// + /// + /// It provides the function to access the information in the session. + /// + /// + /// if not found. + /// + /// + /// + /// is an empty string. + /// + /// + /// is . + /// + public bool TryGetSession (string id, out IWebSocketSession session) + { + if (id == null) + throw new ArgumentNullException ("id"); + + if (id.Length == 0) + throw new ArgumentException ("An empty string.", "id"); + + return tryGetSession (id, out session); + } + + #endregion + } +} diff --git a/Assets/External/websocket-sharp/Server/WebSocketSessionManager.cs.meta b/Assets/External/websocket-sharp/Server/WebSocketSessionManager.cs.meta new file mode 100644 index 00000000..cb3ff581 --- /dev/null +++ b/Assets/External/websocket-sharp/Server/WebSocketSessionManager.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 333da605558912f45a17f746792fd2dc \ No newline at end of file diff --git a/Assets/External/websocket-sharp/WebSocket.cs b/Assets/External/websocket-sharp/WebSocket.cs new file mode 100644 index 00000000..002eb736 --- /dev/null +++ b/Assets/External/websocket-sharp/WebSocket.cs @@ -0,0 +1,4320 @@ +#region License +/* + * WebSocket.cs + * + * This code is derived from WebSocket.java + * (http://github.com/adamac/Java-WebSocket-client). + * + * The MIT License + * + * Copyright (c) 2009 Adam MacBeth + * Copyright (c) 2010-2025 sta.blockhead + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#endregion + +#region Contributors +/* + * Contributors: + * - Frank Razenberg + * - David Wood + * - Liryna + */ +#endregion + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Diagnostics; +using System.IO; +using System.Net.Security; +using System.Net.Sockets; +using System.Security.Cryptography; +using System.Text; +using System.Threading; +using WebSocketSharp.Net; +using WebSocketSharp.Net.WebSockets; + +namespace WebSocketSharp +{ + /// + /// Implements the WebSocket interface. + /// + /// + /// + /// This class provides a set of methods and properties for two-way + /// communication using the WebSocket protocol. + /// + /// + /// The WebSocket protocol is defined in + /// RFC 6455. + /// + /// + public class WebSocket : IDisposable + { + #region Private Fields + + private AuthenticationChallenge _authChallenge; + private string _base64Key; + private Action _closeContext; + private CompressionMethod _compression; + private WebSocketContext _context; + private CookieCollection _cookies; + private NetworkCredential _credentials; + private bool _emitOnPing; + private static readonly byte[] _emptyBytes; + private bool _enableRedirection; + private string _extensions; + private bool _extensionsRequested; + private object _forMessageEventQueue; + private object _forPing; + private object _forSend; + private object _forState; + private MemoryStream _fragmentsBuffer; + private bool _fragmentsCompressed; + private Opcode _fragmentsOpcode; + private const string _guid = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; + private Func _handshakeRequestChecker; + private bool _ignoreExtensions; + private bool _inContinuation; + private volatile bool _inMessage; + private bool _isClient; + private bool _isSecure; + private volatile Logger _log; + private static readonly int _maxRetryCountForConnect; + private Action _message; + private Queue _messageEventQueue; + private bool _noDelay; + private uint _nonceCount; + private string _origin; + private ManualResetEvent _pongReceived; + private bool _preAuth; + private string _protocol; + private string[] _protocols; + private bool _protocolsRequested; + private NetworkCredential _proxyCredentials; + private Uri _proxyUri; + private volatile WebSocketState _readyState; + private ManualResetEvent _receivingExited; + private int _retryCountForConnect; + private Socket _socket; + private ClientSslConfiguration _sslConfig; + private Stream _stream; + private TcpClient _tcpClient; + private Uri _uri; + private const string _version = "13"; + private TimeSpan _waitTime; + + #endregion + + #region Internal Fields + + /// + /// Represents the length used to determine whether the data should + /// be fragmented in sending. + /// + /// + /// + /// The data will be fragmented if its length is greater than + /// the value of this field. + /// + /// + /// If you would like to change the value, you must set it to + /// a value between 125 and Int32.MaxValue - 14 inclusive. + /// + /// + internal static readonly int FragmentLength; + + /// + /// Represents the random number generator used internally. + /// + internal static readonly RandomNumberGenerator RandomNumber; + + #endregion + + #region Static Constructor + + static WebSocket () + { + _emptyBytes = new byte[0]; + _maxRetryCountForConnect = 10; + + FragmentLength = 1016; + RandomNumber = new RNGCryptoServiceProvider (); + } + + #endregion + + #region Internal Constructors + + // As server + internal WebSocket (HttpListenerWebSocketContext context, string protocol) + { + _context = context; + _protocol = protocol; + + _closeContext = context.Close; + _isSecure = context.IsSecureConnection; + _log = context.Log; + _message = messages; + _socket = context.Socket; + _stream = context.Stream; + _waitTime = TimeSpan.FromSeconds (1); + + init (); + } + + // As server + internal WebSocket (TcpListenerWebSocketContext context, string protocol) + { + _context = context; + _protocol = protocol; + + _closeContext = context.Close; + _isSecure = context.IsSecureConnection; + _log = context.Log; + _message = messages; + _socket = context.Socket; + _stream = context.Stream; + _waitTime = TimeSpan.FromSeconds (1); + + init (); + } + + #endregion + + #region Public Constructors + + /// + /// Initializes a new instance of the class with + /// the specified URL and optionally subprotocols. + /// + /// + /// + /// A that specifies the URL to which to connect. + /// + /// + /// The scheme of the URL must be ws or wss. + /// + /// + /// The new instance uses a secure connection if the scheme is wss. + /// + /// + /// + /// + /// An array of that specifies the names of + /// the subprotocols if necessary. + /// + /// + /// Each value of the array must be a token defined in + /// + /// RFC 2616. + /// + /// + /// + /// + /// is an empty string. + /// + /// + /// -or- + /// + /// + /// is an invalid WebSocket URL string. + /// + /// + /// -or- + /// + /// + /// contains a value that is not a token. + /// + /// + /// -or- + /// + /// + /// contains a value twice. + /// + /// + /// + /// is . + /// + public WebSocket (string url, params string[] protocols) + { + if (url == null) + throw new ArgumentNullException ("url"); + + if (url.Length == 0) + throw new ArgumentException ("An empty string.", "url"); + + string msg; + + if (!url.TryCreateWebSocketUri (out _uri, out msg)) + throw new ArgumentException (msg, "url"); + + if (protocols != null && protocols.Length > 0) { + if (!checkProtocols (protocols, out msg)) + throw new ArgumentException (msg, "protocols"); + + _protocols = protocols; + } + + _base64Key = CreateBase64Key (); + _isClient = true; + _isSecure = _uri.Scheme == "wss"; + _log = new Logger (); + _message = messagec; + _retryCountForConnect = -1; + _waitTime = TimeSpan.FromSeconds (5); + + init (); + } + + #endregion + + #region Internal Properties + + internal CookieCollection CookieCollection { + get { + return _cookies; + } + } + + // As server + internal Func CustomHandshakeRequestChecker { + get { + return _handshakeRequestChecker; + } + + set { + _handshakeRequestChecker = value; + } + } + + // As server + internal bool IgnoreExtensions { + get { + return _ignoreExtensions; + } + + set { + _ignoreExtensions = value; + } + } + + #endregion + + #region Public Properties + + /// + /// Gets or sets the compression method used to compress a message. + /// + /// + /// + /// One of the enum values. + /// + /// + /// It indicates the compression method used to compress a message. + /// + /// + /// The default value is . + /// + /// + /// + /// + /// The set operation is not available if the interface is not for + /// the client. + /// + /// + /// -or- + /// + /// + /// The set operation is not available when the current state of + /// the interface is neither New nor Closed. + /// + /// + public CompressionMethod Compression { + get { + return _compression; + } + + set { + if (!_isClient) { + var msg = "The set operation is not available."; + + throw new InvalidOperationException (msg); + } + + lock (_forState) { + if (!canSet ()) { + var msg = "The set operation is not available."; + + throw new InvalidOperationException (msg); + } + + _compression = value; + } + } + } + + /// + /// Gets the HTTP cookies included in the handshake request/response. + /// + /// + /// + /// An + /// instance. + /// + /// + /// It provides an enumerator which supports the iteration over + /// the collection of the cookies. + /// + /// + public IEnumerable Cookies { + get { + lock (_cookies.SyncRoot) { + foreach (var cookie in _cookies) + yield return cookie; + } + } + } + + /// + /// Gets the credentials for the HTTP authentication (Basic/Digest). + /// + /// + /// + /// A that represents the credentials + /// used to authenticate the client. + /// + /// + /// The default value is . + /// + /// + public NetworkCredential Credentials { + get { + return _credentials; + } + } + + /// + /// Gets or sets a value indicating whether the interface emits + /// the message event when it receives a ping. + /// + /// + /// + /// true if the interface emits the message event when + /// it receives a ping; otherwise, false. + /// + /// + /// The default value is false. + /// + /// + /// + /// The set operation is not available when the current state of + /// the interface is neither New nor Closed. + /// + public bool EmitOnPing { + get { + return _emitOnPing; + } + + set { + lock (_forState) { + if (!canSet ()) { + var msg = "The set operation is not available."; + + throw new InvalidOperationException (msg); + } + + _emitOnPing = value; + } + } + } + + /// + /// Gets or sets a value indicating whether the URL redirection for + /// the handshake request is allowed. + /// + /// + /// + /// true if the interface allows the URL redirection for + /// the handshake request; otherwise, false. + /// + /// + /// The default value is false. + /// + /// + /// + /// + /// The set operation is not available if the interface is not for + /// the client. + /// + /// + /// -or- + /// + /// + /// The set operation is not available when the current state of + /// the interface is neither New nor Closed. + /// + /// + public bool EnableRedirection { + get { + return _enableRedirection; + } + + set { + if (!_isClient) { + var msg = "The set operation is not available."; + + throw new InvalidOperationException (msg); + } + + lock (_forState) { + if (!canSet ()) { + var msg = "The set operation is not available."; + + throw new InvalidOperationException (msg); + } + + _enableRedirection = value; + } + } + } + + /// + /// Gets the extensions selected by the server. + /// + /// + /// + /// A that represents a list of the extensions + /// negotiated between the client and server. + /// + /// + /// An empty string if not specified or selected. + /// + /// + public string Extensions { + get { + return _extensions ?? String.Empty; + } + } + + /// + /// Gets a value indicating whether the communication is possible. + /// + /// + /// true if the communication is possible; otherwise, false. + /// + public bool IsAlive { + get { + return ping (_emptyBytes); + } + } + + /// + /// Gets a value indicating whether the connection is secure. + /// + /// + /// true if the connection is secure; otherwise, false. + /// + public bool IsSecure { + get { + return _isSecure; + } + } + + /// + /// Gets the logging function. + /// + /// + /// The default logging level is . + /// + /// + /// A that provides the logging function. + /// + /// + /// The get operation is not available if the interface is not for + /// the client. + /// + public Logger Log { + get { + if (!_isClient) { + var msg = "The get operation is not available."; + + throw new InvalidOperationException (msg); + } + + return _log; + } + + internal set { + _log = value; + } + } + + /// + /// Gets or sets a value indicating whether the underlying TCP socket + /// disables a delay when send or receive buffer is not full. + /// + /// + /// + /// true if the delay is disabled; otherwise, false. + /// + /// + /// The default value is false. + /// + /// + /// + /// + /// The set operation is not available when the current state of + /// the interface is neither New nor Closed. + /// + public bool NoDelay { + get { + return _noDelay; + } + + set { + lock (_forState) { + if (!canSet ()) { + var msg = "The set operation is not available."; + + throw new InvalidOperationException (msg); + } + + _noDelay = value; + } + } + } + + /// + /// Gets or sets the value of the HTTP Origin header to send with + /// the handshake request. + /// + /// + /// + /// The HTTP Origin header is defined in + /// + /// Section 7 of RFC 6454. + /// + /// + /// The interface sends the Origin header if this property has any. + /// + /// + /// + /// + /// A that represents the value of the Origin + /// header to send. + /// + /// + /// The syntax is <scheme>://<host>[:<port>]. + /// + /// + /// The default value is . + /// + /// + /// + /// + /// The value specified for a set operation is not an absolute URI string. + /// + /// + /// -or- + /// + /// + /// The value specified for a set operation includes the path segments. + /// + /// + /// + /// + /// The set operation is not available if the interface is not for + /// the client. + /// + /// + /// -or- + /// + /// + /// The set operation is not available when the current state of + /// the interface is neither New nor Closed. + /// + /// + public string Origin { + get { + return _origin; + } + + set { + if (!_isClient) { + var msg = "The set operation is not available."; + + throw new InvalidOperationException (msg); + } + + if (!value.IsNullOrEmpty ()) { + Uri uri; + + if (!Uri.TryCreate (value, UriKind.Absolute, out uri)) { + var msg = "Not an absolute URI string."; + + throw new ArgumentException (msg, "value"); + } + + if (uri.Segments.Length > 1) { + var msg = "It includes the path segments."; + + throw new ArgumentException (msg, "value"); + } + } + + lock (_forState) { + if (!canSet ()) { + var msg = "The set operation is not available."; + + throw new InvalidOperationException (msg); + } + + _origin = !value.IsNullOrEmpty () ? value.TrimEnd ('/') : value; + } + } + } + + /// + /// Gets the name of subprotocol selected by the server. + /// + /// + /// + /// A that will be one of the names of + /// subprotocols specified by client. + /// + /// + /// An empty string if not specified or selected. + /// + /// + public string Protocol { + get { + return _protocol ?? String.Empty; + } + + internal set { + _protocol = value; + } + } + + /// + /// Gets the current state of the interface. + /// + /// + /// + /// One of the enum values. + /// + /// + /// It indicates the current state of the interface. + /// + /// + /// The default value is . + /// + /// + public WebSocketState ReadyState { + get { + return _readyState; + } + } + + /// + /// Gets the configuration for secure connection. + /// + /// + /// The configuration is used when the interface attempts to connect, + /// so it must be configured before any connect method is called. + /// + /// + /// A that represents the + /// configuration used to establish a secure connection. + /// + /// + /// + /// The get operation is not available if the interface is not for + /// the client. + /// + /// + /// -or- + /// + /// + /// The get operation is not available if the interface does not use + /// a secure connection. + /// + /// + public ClientSslConfiguration SslConfiguration { + get { + if (!_isClient) { + var msg = "The get operation is not available."; + + throw new InvalidOperationException (msg); + } + + if (!_isSecure) { + var msg = "The get operation is not available."; + + throw new InvalidOperationException (msg); + } + + return getSslConfiguration (); + } + } + + /// + /// Gets the URL to which to connect. + /// + /// + /// + /// A that represents the URL to which to connect. + /// + /// + /// Also it represents the URL requested by the client if the interface + /// is for the server. + /// + /// + public Uri Url { + get { + return _isClient ? _uri : _context.RequestUri; + } + } + + /// + /// Gets or sets the time to wait for the response to the ping or close. + /// + /// + /// + /// A that represents the time to wait for + /// the response. + /// + /// + /// The default value is the same as 5 seconds if the interface is + /// for the client. + /// + /// + /// + /// The value specified for a set operation is zero or less. + /// + /// + /// The set operation is not available when the current state of + /// the interface is neither New nor Closed. + /// + public TimeSpan WaitTime { + get { + return _waitTime; + } + + set { + if (value <= TimeSpan.Zero) { + var msg = "Zero or less."; + + throw new ArgumentOutOfRangeException ("value", msg); + } + + lock (_forState) { + if (!canSet ()) { + var msg = "The set operation is not available."; + + throw new InvalidOperationException (msg); + } + + _waitTime = value; + } + } + } + + #endregion + + #region Public Events + + /// + /// Occurs when the connection has been closed. + /// + public event EventHandler OnClose; + + /// + /// Occurs when the interface gets an error. + /// + public event EventHandler OnError; + + /// + /// Occurs when the interface receives a message. + /// + public event EventHandler OnMessage; + + /// + /// Occurs when the connection has been established. + /// + public event EventHandler OnOpen; + + #endregion + + #region Private Methods + + private void abort (string reason, Exception exception) + { + var code = exception is WebSocketException + ? ((WebSocketException) exception).Code + : (ushort) 1006; + + abort (code, reason); + } + + private void abort (ushort code, string reason) + { + var data = new PayloadData (code, reason); + + close (data, false, false); + } + + // As server + private bool accept () + { + lock (_forState) { + if (_readyState == WebSocketState.Open) { + _log.Trace ("The connection has already been established."); + + return false; + } + + if (_readyState == WebSocketState.Closing) { + _log.Error ("The close process is in progress."); + + error ("An error has occurred before accepting.", null); + + return false; + } + + if (_readyState == WebSocketState.Closed) { + _log.Error ("The connection has been closed."); + + error ("An error has occurred before accepting.", null); + + return false; + } + + _readyState = WebSocketState.Connecting; + + var accepted = false; + + try { + accepted = acceptHandshake (); + } + catch (Exception ex) { + _log.Fatal (ex.Message); + _log.Debug (ex.ToString ()); + + abort (1011, "An exception has occurred while accepting."); + } + + if (!accepted) + return false; + + _readyState = WebSocketState.Open; + + return true; + } + } + + // As server + private bool acceptHandshake () + { + string msg; + + if (!checkHandshakeRequest (_context, out msg)) { + _log.Error (msg); + _log.Debug (_context.ToString ()); + + refuseHandshake (1002, "A handshake error has occurred."); + + return false; + } + + if (!customCheckHandshakeRequest (_context, out msg)) { + _log.Error (msg); + _log.Debug (_context.ToString ()); + + refuseHandshake (1002, "A handshake error has occurred."); + + return false; + } + + _base64Key = _context.Headers["Sec-WebSocket-Key"]; + + if (_protocol != null) { + var matched = _context + .SecWebSocketProtocols + .Contains (p => p == _protocol); + + if (!matched) + _protocol = null; + } + + if (!_ignoreExtensions) { + var val = _context.Headers["Sec-WebSocket-Extensions"]; + + processSecWebSocketExtensionsClientHeader (val); + } + + if (_noDelay) + _socket.NoDelay = true; + + createHandshakeResponse ().WriteTo (_stream); + + return true; + } + + private bool canSet () + { + return _readyState == WebSocketState.New + || _readyState == WebSocketState.Closed; + } + + // As server + private bool checkHandshakeRequest ( + WebSocketContext context, + out string message + ) + { + message = null; + + if (!context.IsWebSocketRequest) { + message = "Not a WebSocket handshake request."; + + return false; + } + + var headers = context.Headers; + + var key = headers["Sec-WebSocket-Key"]; + + if (key == null) { + message = "The Sec-WebSocket-Key header is non-existent."; + + return false; + } + + if (key.Length == 0) { + message = "The Sec-WebSocket-Key header is invalid."; + + return false; + } + + var ver = headers["Sec-WebSocket-Version"]; + + if (ver == null) { + message = "The Sec-WebSocket-Version header is non-existent."; + + return false; + } + + if (ver != _version) { + message = "The Sec-WebSocket-Version header is invalid."; + + return false; + } + + var subps = headers["Sec-WebSocket-Protocol"]; + + if (subps != null) { + if (subps.Length == 0) { + message = "The Sec-WebSocket-Protocol header is invalid."; + + return false; + } + } + + if (!_ignoreExtensions) { + var exts = headers["Sec-WebSocket-Extensions"]; + + if (exts != null) { + if (exts.Length == 0) { + message = "The Sec-WebSocket-Extensions header is invalid."; + + return false; + } + } + } + + return true; + } + + // As client + private bool checkHandshakeResponse ( + HttpResponse response, + out string message + ) + { + message = null; + + if (response.IsRedirect) { + message = "The redirection is indicated."; + + return false; + } + + if (response.IsUnauthorized) { + message = "The authentication is required."; + + return false; + } + + if (!response.IsWebSocketResponse) { + message = "Not a WebSocket handshake response."; + + return false; + } + + var headers = response.Headers; + + var key = headers["Sec-WebSocket-Accept"]; + + if (key == null) { + message = "The Sec-WebSocket-Accept header is non-existent."; + + return false; + } + + if (key != CreateResponseKey (_base64Key)) { + message = "The Sec-WebSocket-Accept header is invalid."; + + return false; + } + + var ver = headers["Sec-WebSocket-Version"]; + + if (ver != null) { + if (ver != _version) { + message = "The Sec-WebSocket-Version header is invalid."; + + return false; + } + } + + var subp = headers["Sec-WebSocket-Protocol"]; + + if (subp == null) { + if (_protocolsRequested) { + message = "The Sec-WebSocket-Protocol header is non-existent."; + + return false; + } + } + else { + var isValid = _protocolsRequested + && subp.Length > 0 + && _protocols.Contains (p => p == subp); + + if (!isValid) { + message = "The Sec-WebSocket-Protocol header is invalid."; + + return false; + } + } + + var exts = headers["Sec-WebSocket-Extensions"]; + + if (exts != null) { + if (!validateSecWebSocketExtensionsServerHeader (exts)) { + message = "The Sec-WebSocket-Extensions header is invalid."; + + return false; + } + } + + return true; + } + + private static bool checkProtocols (string[] protocols, out string message) + { + message = null; + + Func cond = p => p.IsNullOrEmpty () || !p.IsToken (); + + if (protocols.Contains (cond)) { + message = "It contains a value that is not a token."; + + return false; + } + + if (protocols.ContainsTwice ()) { + message = "It contains a value twice."; + + return false; + } + + return true; + } + + // As client + private bool checkProxyConnectResponse ( + HttpResponse response, + out string message + ) + { + message = null; + + if (response.IsProxyAuthenticationRequired) { + message = "The proxy authentication is required."; + + return false; + } + + if (!response.IsSuccess) { + message = "The proxy has failed a connection to the requested URL."; + + return false; + } + + return true; + } + + private bool checkReceivedFrame (WebSocketFrame frame, out string message) + { + message = null; + + if (frame.IsMasked) { + if (_isClient) { + message = "A frame from the server is masked."; + + return false; + } + } + else { + if (!_isClient) { + message = "A frame from a client is not masked."; + + return false; + } + } + + if (frame.IsCompressed) { + if (_compression == CompressionMethod.None) { + message = "A frame is compressed without any agreement for it."; + + return false; + } + + if (!frame.IsData) { + message = "A non data frame is compressed."; + + return false; + } + } + + if (frame.IsData) { + if (_inContinuation) { + message = "A data frame was received while receiving continuation frames."; + + return false; + } + } + + if (frame.IsControl) { + if (frame.Fin == Fin.More) { + message = "A control frame is fragmented."; + + return false; + } + + if (frame.PayloadLength > 125) { + message = "The payload length of a control frame is greater than 125."; + + return false; + } + } + + if (frame.Rsv2 == Rsv.On) { + message = "The RSV2 of a frame is non-zero without any negotiation for it."; + + return false; + } + + if (frame.Rsv3 == Rsv.On) { + message = "The RSV3 of a frame is non-zero without any negotiation for it."; + + return false; + } + + return true; + } + + private void close (ushort code, string reason) + { + if (_readyState == WebSocketState.Closing) { + _log.Trace ("The close process is already in progress."); + + return; + } + + if (_readyState == WebSocketState.Closed) { + _log.Trace ("The connection has already been closed."); + + return; + } + + if (code == 1005) { + close (PayloadData.Empty, true, false); + + return; + } + + var data = new PayloadData (code, reason); + var send = !code.IsReservedStatusCode (); + + close (data, send, false); + } + + private void close (PayloadData payloadData, bool send, bool received) + { + lock (_forState) { + if (_readyState == WebSocketState.Closing) { + _log.Trace ("The close process is already in progress."); + + return; + } + + if (_readyState == WebSocketState.Closed) { + _log.Trace ("The connection has already been closed."); + + return; + } + + send = send && _readyState == WebSocketState.Open; + + _readyState = WebSocketState.Closing; + } + + _log.Trace ("Begin closing the connection."); + + var res = closeHandshake (payloadData, send, received); + + releaseResources (); + + _log.Trace ("End closing the connection."); + + _readyState = WebSocketState.Closed; + + var e = new CloseEventArgs (payloadData, res); + + try { + OnClose.Emit (this, e); + } + catch (Exception ex) { + _log.Error (ex.Message); + _log.Debug (ex.ToString ()); + } + } + + private void closeAsync (ushort code, string reason) + { + if (_readyState == WebSocketState.Closing) { + _log.Trace ("The close process is already in progress."); + + return; + } + + if (_readyState == WebSocketState.Closed) { + _log.Trace ("The connection has already been closed."); + + return; + } + + if (code == 1005) { + closeAsync (PayloadData.Empty, true, false); + + return; + } + + var data = new PayloadData (code, reason); + var send = !code.IsReservedStatusCode (); + + closeAsync (data, send, false); + } + + private void closeAsync (PayloadData payloadData, bool send, bool received) + { + Action closer = close; + + closer.BeginInvoke ( + payloadData, + send, + received, + ar => closer.EndInvoke (ar), + null + ); + } + + private bool closeHandshake ( + PayloadData payloadData, + bool send, + bool received + ) + { + var sent = false; + + if (send) { + var frame = WebSocketFrame.CreateCloseFrame (payloadData, _isClient); + var bytes = frame.ToArray (); + + sent = sendBytes (bytes); + + if (_isClient) + frame.Unmask (); + } + + var wait = !received && sent && _receivingExited != null; + + if (wait) + received = _receivingExited.WaitOne (_waitTime); + + var ret = sent && received; + + var msg = String.Format ( + "The closing was clean? {0} (sent: {1} received: {2})", + ret, + sent, + received + ); + + _log.Debug (msg); + + return ret; + } + + // As client + private bool connect () + { + if (_readyState == WebSocketState.Connecting) { + _log.Trace ("The connect process is in progress."); + + return false; + } + + lock (_forState) { + if (_readyState == WebSocketState.Open) { + _log.Trace ("The connection has already been established."); + + return false; + } + + if (_readyState == WebSocketState.Closing) { + _log.Error ("The close process is in progress."); + + error ("An error has occurred before connecting.", null); + + return false; + } + + if (_retryCountForConnect >= _maxRetryCountForConnect) { + _log.Error ("An opportunity for reconnecting has been lost."); + + error ("An error has occurred before connecting.", null); + + return false; + } + + _retryCountForConnect++; + + _readyState = WebSocketState.Connecting; + + var done = false; + + try { + done = doHandshake (); + } + catch (Exception ex) { + _log.Fatal (ex.Message); + _log.Debug (ex.ToString ()); + + abort ("An exception has occurred while connecting.", ex); + } + + if (!done) + return false; + + _retryCountForConnect = -1; + + _readyState = WebSocketState.Open; + + return true; + } + } + + // As client + private AuthenticationResponse createAuthenticationResponse () + { + if (_credentials == null) + return null; + + if (_authChallenge == null) + return _preAuth ? new AuthenticationResponse (_credentials) : null; + + var ret = new AuthenticationResponse ( + _authChallenge, + _credentials, + _nonceCount + ); + + _nonceCount = ret.NonceCount; + + return ret; + } + + // As client + private string createExtensions () + { + var buff = new StringBuilder (80); + + if (_compression != CompressionMethod.None) { + var str = _compression.ToExtensionString ( + "server_no_context_takeover", + "client_no_context_takeover" + ); + + buff.AppendFormat ("{0}, ", str); + } + + var len = buff.Length; + + if (len <= 2) + return null; + + buff.Length = len - 2; + + return buff.ToString (); + } + + // As server + private HttpResponse createHandshakeFailureResponse () + { + var ret = HttpResponse.CreateCloseResponse (HttpStatusCode.BadRequest); + + ret.Headers["Sec-WebSocket-Version"] = _version; + + return ret; + } + + // As client + private HttpRequest createHandshakeRequest () + { + var ret = HttpRequest.CreateWebSocketHandshakeRequest (_uri); + + var headers = ret.Headers; + + headers["Sec-WebSocket-Key"] = _base64Key; + headers["Sec-WebSocket-Version"] = _version; + + if (!_origin.IsNullOrEmpty ()) + headers["Origin"] = _origin; + + if (_protocols != null) { + headers["Sec-WebSocket-Protocol"] = _protocols.ToString (", "); + + _protocolsRequested = true; + } + + var exts = createExtensions (); + + if (exts != null) { + headers["Sec-WebSocket-Extensions"] = exts; + + _extensionsRequested = true; + } + + var ares = createAuthenticationResponse (); + + if (ares != null) + headers["Authorization"] = ares.ToString (); + + if (_cookies.Count > 0) + ret.SetCookies (_cookies); + + return ret; + } + + // As server + private HttpResponse createHandshakeResponse () + { + var ret = HttpResponse.CreateWebSocketHandshakeResponse (); + + var headers = ret.Headers; + + headers["Sec-WebSocket-Accept"] = CreateResponseKey (_base64Key); + + if (_protocol != null) + headers["Sec-WebSocket-Protocol"] = _protocol; + + if (_extensions != null) + headers["Sec-WebSocket-Extensions"] = _extensions; + + if (_cookies.Count > 0) + ret.SetCookies (_cookies); + + return ret; + } + + // As client + private TcpClient createTcpClient (string hostname, int port) + { + var ret = new TcpClient (hostname, port); + + if (_noDelay) + ret.NoDelay = true; + + return ret; + } + + // As server + private bool customCheckHandshakeRequest ( + WebSocketContext context, + out string message + ) + { + message = null; + + if (_handshakeRequestChecker == null) + return true; + + message = _handshakeRequestChecker (context); + + return message == null; + } + + private MessageEventArgs dequeueFromMessageEventQueue () + { + lock (_forMessageEventQueue) { + return _messageEventQueue.Count > 0 + ? _messageEventQueue.Dequeue () + : null; + } + } + + // As client + private bool doHandshake () + { + setClientStream (); + + var res = sendHandshakeRequest (); + + string msg; + + if (!checkHandshakeResponse (res, out msg)) { + _log.Error (msg); + _log.Debug (res.ToString ()); + + abort (1002, "A handshake error has occurred."); + + return false; + } + + if (_protocolsRequested) + _protocol = res.Headers["Sec-WebSocket-Protocol"]; + + if (_extensionsRequested) { + var exts = res.Headers["Sec-WebSocket-Extensions"]; + + if (exts == null) + _compression = CompressionMethod.None; + else + _extensions = exts; + } + + var cookies = res.Cookies; + + if (cookies.Count > 0) + _cookies.SetOrRemove (cookies); + + return true; + } + + private void enqueueToMessageEventQueue (MessageEventArgs e) + { + lock (_forMessageEventQueue) + _messageEventQueue.Enqueue (e); + } + + private void error (string message, Exception exception) + { + var e = new ErrorEventArgs (message, exception); + + try { + OnError.Emit (this, e); + } + catch (Exception ex) { + _log.Error (ex.Message); + _log.Debug (ex.ToString ()); + } + } + + private ClientSslConfiguration getSslConfiguration () + { + if (_sslConfig == null) + _sslConfig = new ClientSslConfiguration (_uri.DnsSafeHost); + + return _sslConfig; + } + + private void init () + { + _compression = CompressionMethod.None; + _cookies = new CookieCollection (); + _forPing = new object (); + _forSend = new object (); + _forState = new object (); + _messageEventQueue = new Queue (); + _forMessageEventQueue = ((ICollection) _messageEventQueue).SyncRoot; + _readyState = WebSocketState.New; + } + + private void message () + { + MessageEventArgs e = null; + + lock (_forMessageEventQueue) { + if (_inMessage) + return; + + if (_messageEventQueue.Count == 0) + return; + + if (_readyState != WebSocketState.Open) + return; + + e = _messageEventQueue.Dequeue (); + + _inMessage = true; + } + + _message (e); + } + + private void messagec (MessageEventArgs e) + { + do { + try { + OnMessage.Emit (this, e); + } + catch (Exception ex) { + _log.Error (ex.Message); + _log.Debug (ex.ToString ()); + + error ("An exception has occurred during an OnMessage event.", ex); + } + + lock (_forMessageEventQueue) { + if (_messageEventQueue.Count == 0) { + _inMessage = false; + + break; + } + + if (_readyState != WebSocketState.Open) { + _inMessage = false; + + break; + } + + e = _messageEventQueue.Dequeue (); + } + } + while (true); + } + + private void messages (MessageEventArgs e) + { + try { + OnMessage.Emit (this, e); + } + catch (Exception ex) { + _log.Error (ex.Message); + _log.Debug (ex.ToString ()); + + error ("An exception has occurred during an OnMessage event.", ex); + } + + lock (_forMessageEventQueue) { + if (_messageEventQueue.Count == 0) { + _inMessage = false; + + return; + } + + if (_readyState != WebSocketState.Open) { + _inMessage = false; + + return; + } + + e = _messageEventQueue.Dequeue (); + } + + ThreadPool.QueueUserWorkItem (state => messages (e)); + } + + private void open () + { + _inMessage = true; + + startReceiving (); + + try { + OnOpen.Emit (this, EventArgs.Empty); + } + catch (Exception ex) { + _log.Error (ex.Message); + _log.Debug (ex.ToString ()); + + error ("An exception has occurred during the OnOpen event.", ex); + } + + MessageEventArgs e = null; + + lock (_forMessageEventQueue) { + if (_messageEventQueue.Count == 0) { + _inMessage = false; + + return; + } + + if (_readyState != WebSocketState.Open) { + _inMessage = false; + + return; + } + + e = _messageEventQueue.Dequeue (); + } + + _message.BeginInvoke (e, ar => _message.EndInvoke (ar), null); + } + + private bool ping (byte[] data) + { + if (_readyState != WebSocketState.Open) + return false; + + var received = _pongReceived; + + if (received == null) + return false; + + lock (_forPing) { + try { + received.Reset (); + + var sent = send (Fin.Final, Opcode.Ping, data, false); + + if (!sent) + return false; + + return received.WaitOne (_waitTime); + } + catch (ObjectDisposedException) { + return false; + } + } + } + + private bool processCloseFrame (WebSocketFrame frame) + { + var data = frame.PayloadData; + var send = !data.HasReservedCode; + + close (data, send, true); + + return false; + } + + private bool processDataFrame (WebSocketFrame frame) + { + var e = frame.IsCompressed + ? new MessageEventArgs ( + frame.Opcode, + frame.PayloadData.ApplicationData.Decompress (_compression) + ) + : new MessageEventArgs (frame); + + enqueueToMessageEventQueue (e); + + return true; + } + + private bool processFragmentFrame (WebSocketFrame frame) + { + if (!_inContinuation) { + if (frame.IsContinuation) + return true; + + _fragmentsOpcode = frame.Opcode; + _fragmentsCompressed = frame.IsCompressed; + _fragmentsBuffer = new MemoryStream (); + _inContinuation = true; + } + + _fragmentsBuffer.WriteBytes (frame.PayloadData.ApplicationData, 1024); + + if (frame.IsFinal) { + using (_fragmentsBuffer) { + var data = _fragmentsCompressed + ? _fragmentsBuffer.DecompressToArray (_compression) + : _fragmentsBuffer.ToArray (); + + var e = new MessageEventArgs (_fragmentsOpcode, data); + + enqueueToMessageEventQueue (e); + } + + _fragmentsBuffer = null; + _inContinuation = false; + } + + return true; + } + + private bool processPingFrame (WebSocketFrame frame) + { + _log.Trace ("A ping was received."); + + var pong = WebSocketFrame.CreatePongFrame (frame.PayloadData, _isClient); + + lock (_forState) { + if (_readyState != WebSocketState.Open) { + _log.Trace ("A pong to this ping cannot be sent."); + + return true; + } + + var bytes = pong.ToArray (); + var sent = sendBytes (bytes); + + if (!sent) + return false; + } + + _log.Trace ("A pong to this ping has been sent."); + + if (_emitOnPing) { + if (_isClient) + pong.Unmask (); + + var e = new MessageEventArgs (frame); + + enqueueToMessageEventQueue (e); + } + + return true; + } + + private bool processPongFrame (WebSocketFrame frame) + { + _log.Trace ("A pong was received."); + + try { + _pongReceived.Set (); + } + catch (NullReferenceException) { + return false; + } + catch (ObjectDisposedException) { + return false; + } + + _log.Trace ("It has been signaled."); + + return true; + } + + private bool processReceivedFrame (WebSocketFrame frame) + { + string msg; + + if (!checkReceivedFrame (frame, out msg)) { + _log.Error (msg); + _log.Debug (frame.ToString (false)); + + abort (1002, "An error has occurred while receiving."); + + return false; + } + + frame.Unmask (); + + return frame.IsFragment + ? processFragmentFrame (frame) + : frame.IsData + ? processDataFrame (frame) + : frame.IsPing + ? processPingFrame (frame) + : frame.IsPong + ? processPongFrame (frame) + : frame.IsClose + ? processCloseFrame (frame) + : processUnsupportedFrame (frame); + } + + // As server + private void processSecWebSocketExtensionsClientHeader (string value) + { + if (value == null) + return; + + var buff = new StringBuilder (80); + + var compRequested = false; + + foreach (var elm in value.SplitHeaderValue (',')) { + var ext = elm.Trim (); + + if (ext.Length == 0) + continue; + + if (!compRequested) { + if (ext.IsCompressionExtension (CompressionMethod.Deflate)) { + _compression = CompressionMethod.Deflate; + + var str = _compression.ToExtensionString ( + "client_no_context_takeover", + "server_no_context_takeover" + ); + + buff.AppendFormat ("{0}, ", str); + + compRequested = true; + } + } + } + + var len = buff.Length; + + if (len <= 2) + return; + + buff.Length = len - 2; + + _extensions = buff.ToString (); + } + + private bool processUnsupportedFrame (WebSocketFrame frame) + { + _log.Fatal ("An unsupported frame was received."); + _log.Debug (frame.ToString (false)); + + abort (1003, "There is no way to handle it."); + + return false; + } + + // As server + private void refuseHandshake (ushort code, string reason) + { + createHandshakeFailureResponse ().WriteTo (_stream); + + abort (code, reason); + } + + // As client + private void releaseClientResources () + { + if (_stream != null) { + _stream.Dispose (); + + _stream = null; + } + + if (_tcpClient != null) { + _tcpClient.Close (); + + _tcpClient = null; + } + } + + private void releaseCommonResources () + { + if (_fragmentsBuffer != null) { + _fragmentsBuffer.Dispose (); + + _fragmentsBuffer = null; + _inContinuation = false; + } + + if (_pongReceived != null) { + _pongReceived.Close (); + + _pongReceived = null; + } + + if (_receivingExited != null) { + _receivingExited.Close (); + + _receivingExited = null; + } + } + + private void releaseResources () + { + if (_isClient) + releaseClientResources (); + else + releaseServerResources (); + + releaseCommonResources (); + } + + // As server + private void releaseServerResources () + { + if (_closeContext != null) { + _closeContext (); + + _closeContext = null; + } + + _stream = null; + _context = null; + } + + private bool send (byte[] rawFrame) + { + lock (_forState) { + if (_readyState != WebSocketState.Open) { + _log.Error ("The current state of the interface is not Open."); + + return false; + } + + return sendBytes (rawFrame); + } + } + + private bool send (Opcode opcode, Stream sourceStream) + { + lock (_forSend) { + var dataStream = sourceStream; + var compressed = false; + var sent = false; + + try { + if (_compression != CompressionMethod.None) { + dataStream = sourceStream.Compress (_compression); + compressed = true; + } + + sent = send (opcode, dataStream, compressed); + + if (!sent) + error ("A send has failed.", null); + } + catch (Exception ex) { + _log.Error (ex.Message); + _log.Debug (ex.ToString ()); + + error ("An exception has occurred during a send.", ex); + } + finally { + if (compressed) + dataStream.Dispose (); + + sourceStream.Dispose (); + } + + return sent; + } + } + + private bool send (Opcode opcode, Stream dataStream, bool compressed) + { + var len = dataStream.Length; + + if (len == 0) + return send (Fin.Final, opcode, _emptyBytes, false); + + var quo = len / FragmentLength; + var rem = (int) (len % FragmentLength); + + byte[] buff = null; + + if (quo == 0) { + buff = new byte[rem]; + + return dataStream.Read (buff, 0, rem) == rem + && send (Fin.Final, opcode, buff, compressed); + } + + if (quo == 1 && rem == 0) { + buff = new byte[FragmentLength]; + + return dataStream.Read (buff, 0, FragmentLength) == FragmentLength + && send (Fin.Final, opcode, buff, compressed); + } + + /* Send fragments */ + + // Begin + + buff = new byte[FragmentLength]; + + var sent = dataStream.Read (buff, 0, FragmentLength) == FragmentLength + && send (Fin.More, opcode, buff, compressed); + + if (!sent) + return false; + + // Continue + + var n = rem == 0 ? quo - 2 : quo - 1; + + for (long i = 0; i < n; i++) { + sent = dataStream.Read (buff, 0, FragmentLength) == FragmentLength + && send (Fin.More, Opcode.Cont, buff, false); + + if (!sent) + return false; + } + + // End + + if (rem == 0) + rem = FragmentLength; + else + buff = new byte[rem]; + + return dataStream.Read (buff, 0, rem) == rem + && send (Fin.Final, Opcode.Cont, buff, false); + } + + private bool send (Fin fin, Opcode opcode, byte[] data, bool compressed) + { + var frame = new WebSocketFrame (fin, opcode, data, compressed, _isClient); + var rawFrame = frame.ToArray (); + + return send (rawFrame); + } + + private void sendAsync ( + Opcode opcode, + Stream sourceStream, + Action completed + ) + { + Func sender = send; + + sender.BeginInvoke ( + opcode, + sourceStream, + ar => { + try { + var sent = sender.EndInvoke (ar); + + if (completed != null) + completed (sent); + } + catch (Exception ex) { + _log.Error (ex.Message); + _log.Debug (ex.ToString ()); + + error ( + "An exception has occurred during the callback for an async send.", + ex + ); + } + }, + null + ); + } + + private bool sendBytes (byte[] bytes) + { + try { + _stream.Write (bytes, 0, bytes.Length); + } + catch (Exception ex) { + _log.Error (ex.Message); + _log.Debug (ex.ToString ()); + + return false; + } + + return true; + } + + // As client + private HttpResponse sendHandshakeRequest () + { + var req = createHandshakeRequest (); + + var timeout = 90000; + var res = req.GetResponse (_stream, timeout); + + if (res.IsUnauthorized) { + var val = res.Headers["WWW-Authenticate"]; + + if (val.IsNullOrEmpty ()) { + _log.Debug ("No authentication challenge is specified."); + + return res; + } + + var achal = AuthenticationChallenge.Parse (val); + + if (achal == null) { + _log.Debug ("An invalid authentication challenge is specified."); + + return res; + } + + _authChallenge = achal; + + if (_credentials == null) + return res; + + var ares = new AuthenticationResponse ( + _authChallenge, + _credentials, + _nonceCount + ); + + _nonceCount = ares.NonceCount; + + req.Headers["Authorization"] = ares.ToString (); + + if (res.CloseConnection) { + releaseClientResources (); + setClientStream (); + } + + timeout = 15000; + res = req.GetResponse (_stream, timeout); + } + + if (res.IsRedirect) { + if (!_enableRedirection) + return res; + + var val = res.Headers["Location"]; + + if (val.IsNullOrEmpty ()) { + _log.Debug ("No URL to redirect is located."); + + return res; + } + + Uri uri; + string msg; + + if (!val.TryCreateWebSocketUri (out uri, out msg)) { + _log.Debug ("An invalid URL to redirect is located."); + + return res; + } + + releaseClientResources (); + + _uri = uri; + _isSecure = uri.Scheme == "wss"; + + setClientStream (); + + return sendHandshakeRequest (); + } + + return res; + } + + // As client + private HttpResponse sendProxyConnectRequest () + { + var req = HttpRequest.CreateConnectRequest (_uri); + + var timeout = 90000; + var res = req.GetResponse (_stream, timeout); + + if (res.IsProxyAuthenticationRequired) { + if (_proxyCredentials == null) + return res; + + var val = res.Headers["Proxy-Authenticate"]; + + if (val.IsNullOrEmpty ()) { + _log.Debug ("No proxy authentication challenge is specified."); + + return res; + } + + var achal = AuthenticationChallenge.Parse (val); + + if (achal == null) { + _log.Debug ("An invalid proxy authentication challenge is specified."); + + return res; + } + + var ares = new AuthenticationResponse (achal, _proxyCredentials, 0); + + req.Headers["Proxy-Authorization"] = ares.ToString (); + + if (res.CloseConnection) { + releaseClientResources (); + + _tcpClient = createTcpClient (_proxyUri.DnsSafeHost, _proxyUri.Port); + _stream = _tcpClient.GetStream (); + } + + timeout = 15000; + res = req.GetResponse (_stream, timeout); + } + + return res; + } + + // As client + private void setClientStream () + { + if (_proxyUri != null) { + _tcpClient = createTcpClient (_proxyUri.DnsSafeHost, _proxyUri.Port); + _stream = _tcpClient.GetStream (); + + var res = sendProxyConnectRequest (); + + string msg; + + if (!checkProxyConnectResponse (res, out msg)) + throw new WebSocketException (msg); + } + else { + _tcpClient = createTcpClient (_uri.DnsSafeHost, _uri.Port); + _stream = _tcpClient.GetStream (); + } + + if (_isSecure) { + var conf = getSslConfiguration (); + var host = conf.TargetHost; + + if (host != _uri.DnsSafeHost) { + var msg = "An invalid host name is specified."; + + throw new WebSocketException ( + CloseStatusCode.TlsHandshakeFailure, + msg + ); + } + + try { + var sslStream = new SslStream ( + _stream, + false, + conf.ServerCertificateValidationCallback, + conf.ClientCertificateSelectionCallback + ); + + sslStream.AuthenticateAsClient ( + host, + conf.ClientCertificates, + conf.EnabledSslProtocols, + conf.CheckCertificateRevocation + ); + + _stream = sslStream; + } + catch (Exception ex) { + throw new WebSocketException ( + CloseStatusCode.TlsHandshakeFailure, + ex + ); + } + } + } + + private void startReceiving () + { + if (_messageEventQueue.Count > 0) + _messageEventQueue.Clear (); + + _pongReceived = new ManualResetEvent (false); + _receivingExited = new ManualResetEvent (false); + + Action receive = null; + receive = + () => WebSocketFrame.ReadFrameAsync ( + _stream, + false, + frame => { + var doNext = processReceivedFrame (frame) + && _readyState != WebSocketState.Closed; + + if (!doNext) { + var exited = _receivingExited; + + if (exited != null) + exited.Set (); + + return; + } + + receive (); + + if (_inMessage) + return; + + message (); + }, + ex => { + _log.Fatal (ex.Message); + _log.Debug (ex.ToString ()); + + abort ("An exception has occurred while receiving.", ex); + } + ); + + receive (); + } + + // As client + private bool validateSecWebSocketExtensionsServerHeader (string value) + { + if (!_extensionsRequested) + return false; + + if (value.Length == 0) + return false; + + var compRequested = _compression != CompressionMethod.None; + + foreach (var elm in value.SplitHeaderValue (',')) { + var ext = elm.Trim (); + + if (compRequested && ext.IsCompressionExtension (_compression)) { + var param1 = "server_no_context_takeover"; + var param2 = "client_no_context_takeover"; + + if (!ext.Contains (param1)) { + // The server did not send back "server_no_context_takeover". + + return false; + } + + var name = _compression.ToExtensionString (); + + var isInvalid = ext.SplitHeaderValue (';').Contains ( + t => { + t = t.Trim (); + + var isValid = t == name + || t == param1 + || t == param2; + + return !isValid; + } + ); + + if (isInvalid) + return false; + + compRequested = false; + } + else { + return false; + } + } + + return true; + } + + #endregion + + #region Internal Methods + + // As server + internal void Accept () + { + var accepted = accept (); + + if (!accepted) + return; + + open (); + } + + // As server + internal void AcceptAsync () + { + Func acceptor = accept; + + acceptor.BeginInvoke ( + ar => { + var accepted = acceptor.EndInvoke (ar); + + if (!accepted) + return; + + open (); + }, + null + ); + } + + // As server + internal void Close (PayloadData payloadData, byte[] rawFrame) + { + lock (_forState) { + if (_readyState == WebSocketState.Closing) { + _log.Trace ("The close process is already in progress."); + + return; + } + + if (_readyState == WebSocketState.Closed) { + _log.Trace ("The connection has already been closed."); + + return; + } + + _readyState = WebSocketState.Closing; + } + + _log.Trace ("Begin closing the connection."); + + var sent = rawFrame != null && sendBytes (rawFrame); + var received = sent && _receivingExited != null + ? _receivingExited.WaitOne (_waitTime) + : false; + + var res = sent && received; + + var msg = String.Format ( + "The closing was clean? {0} (sent: {1} received: {2})", + res, + sent, + received + ); + + _log.Debug (msg); + + releaseServerResources (); + releaseCommonResources (); + + _log.Trace ("End closing the connection."); + + _readyState = WebSocketState.Closed; + + var e = new CloseEventArgs (payloadData, res); + + try { + OnClose.Emit (this, e); + } + catch (Exception ex) { + _log.Error (ex.Message); + _log.Debug (ex.ToString ()); + } + } + + // As client + internal static string CreateBase64Key () + { + var key = new byte[16]; + + RandomNumber.GetBytes (key); + + return Convert.ToBase64String (key); + } + + internal static string CreateResponseKey (string base64Key) + { + SHA1 sha1 = new SHA1CryptoServiceProvider (); + + var src = base64Key + _guid; + var bytes = src.GetUTF8EncodedBytes (); + var key = sha1.ComputeHash (bytes); + + return Convert.ToBase64String (key); + } + + // As server + internal bool Ping (byte[] rawFrame) + { + if (_readyState != WebSocketState.Open) + return false; + + var received = _pongReceived; + + if (received == null) + return false; + + lock (_forPing) { + try { + received.Reset (); + + var sent = send (rawFrame); + + if (!sent) + return false; + + return received.WaitOne (_waitTime); + } + catch (ObjectDisposedException) { + return false; + } + } + } + + // As server + internal void Send ( + Opcode opcode, + byte[] data, + Dictionary cache + ) + { + lock (_forSend) { + byte[] found; + + if (!cache.TryGetValue (_compression, out found)) { + found = new WebSocketFrame ( + Fin.Final, + opcode, + data.Compress (_compression), + _compression != CompressionMethod.None, + false + ) + .ToArray (); + + cache.Add (_compression, found); + } + + send (found); + } + } + + // As server + internal void Send ( + Opcode opcode, + Stream sourceStream, + Dictionary cache + ) + { + lock (_forSend) { + Stream found; + + if (!cache.TryGetValue (_compression, out found)) { + found = sourceStream.Compress (_compression); + + cache.Add (_compression, found); + } + else { + found.Position = 0; + } + + send (opcode, found, _compression != CompressionMethod.None); + } + } + + #endregion + + #region Public Methods + + /// + /// Closes the connection. + /// + /// + /// This method does nothing if the current state of the interface is + /// Closing or Closed. + /// + public void Close () + { + close (1005, String.Empty); + } + + /// + /// Closes the connection with the specified status code. + /// + /// + /// This method does nothing if the current state of the interface is + /// Closing or Closed. + /// + /// + /// + /// A that specifies the status code indicating + /// the reason for the close. + /// + /// + /// The status codes are defined in + /// + /// Section 7.4 of RFC 6455. + /// + /// + /// + /// + /// is 1011 (server error). + /// It cannot be used by a client. + /// + /// + /// -or- + /// + /// + /// is 1010 (mandatory extension). + /// It cannot be used by a server. + /// + /// + /// + /// is less than 1000 or greater than 4999. + /// + public void Close (ushort code) + { + Close (code, String.Empty); + } + + /// + /// Closes the connection with the specified status code. + /// + /// + /// This method does nothing if the current state of the interface is + /// Closing or Closed. + /// + /// + /// + /// One of the enum values. + /// + /// + /// It specifies the status code indicating the reason for the close. + /// + /// + /// + /// + /// is an undefined enum value. + /// + /// + /// -or- + /// + /// + /// is . + /// It cannot be used by a client. + /// + /// + /// -or- + /// + /// + /// is . + /// It cannot be used by a server. + /// + /// + public void Close (CloseStatusCode code) + { + Close (code, String.Empty); + } + + /// + /// Closes the connection with the specified status code and reason. + /// + /// + /// This method does nothing if the current state of the interface is + /// Closing or Closed. + /// + /// + /// + /// A that specifies the status code indicating + /// the reason for the close. + /// + /// + /// The status codes are defined in + /// + /// Section 7.4 of RFC 6455. + /// + /// + /// + /// + /// A that specifies the reason for the close. + /// + /// + /// Its size must be 123 bytes or less in UTF-8. + /// + /// + /// + /// + /// is 1011 (server error). + /// It cannot be used by a client. + /// + /// + /// -or- + /// + /// + /// is 1010 (mandatory extension). + /// It cannot be used by a server. + /// + /// + /// -or- + /// + /// + /// is 1005 (no status) and + /// is specified. + /// + /// + /// -or- + /// + /// + /// could not be UTF-8-encoded. + /// + /// + /// + /// + /// is less than 1000 or greater than 4999. + /// + /// + /// -or- + /// + /// + /// The size of is greater than 123 bytes. + /// + /// + public void Close (ushort code, string reason) + { + if (!code.IsCloseStatusCode ()) { + var msg = "Less than 1000 or greater than 4999."; + + throw new ArgumentOutOfRangeException ("code", msg); + } + + if (_isClient) { + if (code == 1011) { + var msg = "1011 cannot be used."; + + throw new ArgumentException (msg, "code"); + } + } + else { + if (code == 1010) { + var msg = "1010 cannot be used."; + + throw new ArgumentException (msg, "code"); + } + } + + if (reason.IsNullOrEmpty ()) { + close (code, String.Empty); + + return; + } + + if (code == 1005) { + var msg = "1005 cannot be used."; + + throw new ArgumentException (msg, "code"); + } + + byte[] bytes; + + if (!reason.TryGetUTF8EncodedBytes (out bytes)) { + var msg = "It could not be UTF-8-encoded."; + + throw new ArgumentException (msg, "reason"); + } + + if (bytes.Length > 123) { + var msg = "Its size is greater than 123 bytes."; + + throw new ArgumentOutOfRangeException ("reason", msg); + } + + close (code, reason); + } + + /// + /// Closes the connection with the specified status code and reason. + /// + /// + /// This method does nothing if the current state of the interface is + /// Closing or Closed. + /// + /// + /// + /// One of the enum values. + /// + /// + /// It specifies the status code indicating the reason for the close. + /// + /// + /// + /// + /// A that specifies the reason for the close. + /// + /// + /// Its size must be 123 bytes or less in UTF-8. + /// + /// + /// + /// + /// is an undefined enum value. + /// + /// + /// -or- + /// + /// + /// is . + /// It cannot be used by a client. + /// + /// + /// -or- + /// + /// + /// is . + /// It cannot be used by a server. + /// + /// + /// -or- + /// + /// + /// is and + /// is specified. + /// + /// + /// -or- + /// + /// + /// could not be UTF-8-encoded. + /// + /// + /// + /// The size of is greater than 123 bytes. + /// + public void Close (CloseStatusCode code, string reason) + { + if (!code.IsDefined ()) { + var msg = "An undefined enum value."; + + throw new ArgumentException (msg, "code"); + } + + if (_isClient) { + if (code == CloseStatusCode.ServerError) { + var msg = "ServerError cannot be used."; + + throw new ArgumentException (msg, "code"); + } + } + else { + if (code == CloseStatusCode.MandatoryExtension) { + var msg = "MandatoryExtension cannot be used."; + + throw new ArgumentException (msg, "code"); + } + } + + if (reason.IsNullOrEmpty ()) { + close ((ushort) code, String.Empty); + + return; + } + + if (code == CloseStatusCode.NoStatus) { + var msg = "NoStatus cannot be used."; + + throw new ArgumentException (msg, "code"); + } + + byte[] bytes; + + if (!reason.TryGetUTF8EncodedBytes (out bytes)) { + var msg = "It could not be UTF-8-encoded."; + + throw new ArgumentException (msg, "reason"); + } + + if (bytes.Length > 123) { + var msg = "Its size is greater than 123 bytes."; + + throw new ArgumentOutOfRangeException ("reason", msg); + } + + close ((ushort) code, reason); + } + + /// + /// Closes the connection asynchronously. + /// + /// + /// + /// This method does not wait for the close to be complete. + /// + /// + /// This method does nothing if the current state of the interface is + /// Closing or Closed. + /// + /// + public void CloseAsync () + { + closeAsync (1005, String.Empty); + } + + /// + /// Closes the connection asynchronously with the specified status code. + /// + /// + /// + /// This method does not wait for the close to be complete. + /// + /// + /// This method does nothing if the current state of the interface is + /// Closing or Closed. + /// + /// + /// + /// + /// A that specifies the status code indicating + /// the reason for the close. + /// + /// + /// The status codes are defined in + /// + /// Section 7.4 of RFC 6455. + /// + /// + /// + /// + /// is 1011 (server error). + /// It cannot be used by a client. + /// + /// + /// -or- + /// + /// + /// is 1010 (mandatory extension). + /// It cannot be used by a server. + /// + /// + /// + /// is less than 1000 or greater than 4999. + /// + public void CloseAsync (ushort code) + { + CloseAsync (code, String.Empty); + } + + /// + /// Closes the connection asynchronously with the specified status code. + /// + /// + /// + /// This method does not wait for the close to be complete. + /// + /// + /// This method does nothing if the current state of the interface is + /// Closing or Closed. + /// + /// + /// + /// + /// One of the enum values. + /// + /// + /// It specifies the status code indicating the reason for the close. + /// + /// + /// + /// + /// is an undefined enum value. + /// + /// + /// -or- + /// + /// + /// is . + /// It cannot be used by a client. + /// + /// + /// -or- + /// + /// + /// is . + /// It cannot be used by a server. + /// + /// + public void CloseAsync (CloseStatusCode code) + { + CloseAsync (code, String.Empty); + } + + /// + /// Closes the connection asynchronously with the specified status code and + /// reason. + /// + /// + /// + /// This method does not wait for the close to be complete. + /// + /// + /// This method does nothing if the current state of the interface is + /// Closing or Closed. + /// + /// + /// + /// + /// A that specifies the status code indicating + /// the reason for the close. + /// + /// + /// The status codes are defined in + /// + /// Section 7.4 of RFC 6455. + /// + /// + /// + /// + /// A that specifies the reason for the close. + /// + /// + /// Its size must be 123 bytes or less in UTF-8. + /// + /// + /// + /// + /// is 1011 (server error). + /// It cannot be used by a client. + /// + /// + /// -or- + /// + /// + /// is 1010 (mandatory extension). + /// It cannot be used by a server. + /// + /// + /// -or- + /// + /// + /// is 1005 (no status) and + /// is specified. + /// + /// + /// -or- + /// + /// + /// could not be UTF-8-encoded. + /// + /// + /// + /// + /// is less than 1000 or greater than 4999. + /// + /// + /// -or- + /// + /// + /// The size of is greater than 123 bytes. + /// + /// + public void CloseAsync (ushort code, string reason) + { + if (!code.IsCloseStatusCode ()) { + var msg = "Less than 1000 or greater than 4999."; + + throw new ArgumentOutOfRangeException ("code", msg); + } + + if (_isClient) { + if (code == 1011) { + var msg = "1011 cannot be used."; + + throw new ArgumentException (msg, "code"); + } + } + else { + if (code == 1010) { + var msg = "1010 cannot be used."; + + throw new ArgumentException (msg, "code"); + } + } + + if (reason.IsNullOrEmpty ()) { + closeAsync (code, String.Empty); + + return; + } + + if (code == 1005) { + var msg = "1005 cannot be used."; + + throw new ArgumentException (msg, "code"); + } + + byte[] bytes; + + if (!reason.TryGetUTF8EncodedBytes (out bytes)) { + var msg = "It could not be UTF-8-encoded."; + + throw new ArgumentException (msg, "reason"); + } + + if (bytes.Length > 123) { + var msg = "Its size is greater than 123 bytes."; + + throw new ArgumentOutOfRangeException ("reason", msg); + } + + closeAsync (code, reason); + } + + /// + /// Closes the connection asynchronously with the specified status code and + /// reason. + /// + /// + /// + /// This method does not wait for the close to be complete. + /// + /// + /// This method does nothing if the current state of the interface is + /// Closing or Closed. + /// + /// + /// + /// + /// One of the enum values. + /// + /// + /// It specifies the status code indicating the reason for the close. + /// + /// + /// + /// + /// A that specifies the reason for the close. + /// + /// + /// Its size must be 123 bytes or less in UTF-8. + /// + /// + /// + /// + /// is an undefined enum value. + /// + /// + /// -or- + /// + /// + /// is . + /// It cannot be used by a client. + /// + /// + /// -or- + /// + /// + /// is . + /// It cannot be used by a server. + /// + /// + /// -or- + /// + /// + /// is and + /// is specified. + /// + /// + /// -or- + /// + /// + /// could not be UTF-8-encoded. + /// + /// + /// + /// The size of is greater than 123 bytes. + /// + public void CloseAsync (CloseStatusCode code, string reason) + { + if (!code.IsDefined ()) { + var msg = "An undefined enum value."; + + throw new ArgumentException (msg, "code"); + } + + if (_isClient) { + if (code == CloseStatusCode.ServerError) { + var msg = "ServerError cannot be used."; + + throw new ArgumentException (msg, "code"); + } + } + else { + if (code == CloseStatusCode.MandatoryExtension) { + var msg = "MandatoryExtension cannot be used."; + + throw new ArgumentException (msg, "code"); + } + } + + if (reason.IsNullOrEmpty ()) { + closeAsync ((ushort) code, String.Empty); + + return; + } + + if (code == CloseStatusCode.NoStatus) { + var msg = "NoStatus cannot be used."; + + throw new ArgumentException (msg, "code"); + } + + byte[] bytes; + + if (!reason.TryGetUTF8EncodedBytes (out bytes)) { + var msg = "It could not be UTF-8-encoded."; + + throw new ArgumentException (msg, "reason"); + } + + if (bytes.Length > 123) { + var msg = "Its size is greater than 123 bytes."; + + throw new ArgumentOutOfRangeException ("reason", msg); + } + + closeAsync ((ushort) code, reason); + } + + /// + /// Establishes a connection. + /// + /// + /// This method does nothing when the current state of the interface is + /// Connecting or Open. + /// + /// + /// + /// The Connect method is not available if the interface is not for + /// the client. + /// + /// + /// -or- + /// + /// + /// The Connect method is not available if a series of reconnecting + /// has failed. + /// + /// + public void Connect () + { + if (!_isClient) { + var msg = "The Connect method is not available."; + + throw new InvalidOperationException (msg); + } + + if (_retryCountForConnect >= _maxRetryCountForConnect) { + var msg = "The Connect method is not available."; + + throw new InvalidOperationException (msg); + } + + var connected = connect (); + + if (!connected) + return; + + open (); + } + + /// + /// Establishes a connection asynchronously. + /// + /// + /// + /// This method does not wait for the connect process to be complete. + /// + /// + /// This method does nothing when the current state of the interface is + /// Connecting or Open. + /// + /// + /// + /// + /// The ConnectAsync method is not available if the interface is not + /// for the client. + /// + /// + /// -or- + /// + /// + /// The ConnectAsync method is not available if a series of reconnecting + /// has failed. + /// + /// + public void ConnectAsync () + { + if (!_isClient) { + var msg = "The ConnectAsync method is not available."; + + throw new InvalidOperationException (msg); + } + + if (_retryCountForConnect >= _maxRetryCountForConnect) { + var msg = "The ConnectAsync method is not available."; + + throw new InvalidOperationException (msg); + } + + Func connector = connect; + + connector.BeginInvoke ( + ar => { + var connected = connector.EndInvoke (ar); + + if (!connected) + return; + + open (); + }, + null + ); + } + + /// + /// Sends a ping to the remote endpoint. + /// + /// + /// true if the send has successfully done and a pong has been + /// received within a time; otherwise, false. + /// + public bool Ping () + { + return ping (_emptyBytes); + } + + /// + /// Sends a ping with the specified message to the remote endpoint. + /// + /// + /// true if the send has successfully done and a pong has been + /// received within a time; otherwise, false. + /// + /// + /// + /// A that specifies the message to send. + /// + /// + /// Its size must be 125 bytes or less in UTF-8. + /// + /// + /// + /// could not be UTF-8-encoded. + /// + /// + /// The size of is greater than 125 bytes. + /// + public bool Ping (string message) + { + if (message.IsNullOrEmpty ()) + return ping (_emptyBytes); + + byte[] bytes; + + if (!message.TryGetUTF8EncodedBytes (out bytes)) { + var msg = "It could not be UTF-8-encoded."; + + throw new ArgumentException (msg, "message"); + } + + if (bytes.Length > 125) { + var msg = "Its size is greater than 125 bytes."; + + throw new ArgumentOutOfRangeException ("message", msg); + } + + return ping (bytes); + } + + /// + /// Sends the specified data to the remote endpoint. + /// + /// + /// An array of that specifies the binary data to send. + /// + /// + /// is . + /// + /// + /// The Send method is not available when the current state of + /// the interface is not Open. + /// + public void Send (byte[] data) + { + if (_readyState != WebSocketState.Open) { + var msg = "The Send method is not available."; + + throw new InvalidOperationException (msg); + } + + if (data == null) + throw new ArgumentNullException ("data"); + + send (Opcode.Binary, new MemoryStream (data)); + } + + /// + /// Sends the specified file to the remote endpoint. + /// + /// + /// + /// A that specifies the file to send. + /// + /// + /// The file is sent as the binary data. + /// + /// + /// + /// + /// The file does not exist. + /// + /// + /// -or- + /// + /// + /// The file could not be opened. + /// + /// + /// + /// is . + /// + /// + /// The Send method is not available when the current state of + /// the interface is not Open. + /// + public void Send (FileInfo fileInfo) + { + if (_readyState != WebSocketState.Open) { + var msg = "The Send method is not available."; + + throw new InvalidOperationException (msg); + } + + if (fileInfo == null) + throw new ArgumentNullException ("fileInfo"); + + if (!fileInfo.Exists) { + var msg = "The file does not exist."; + + throw new ArgumentException (msg, "fileInfo"); + } + + FileStream stream; + + if (!fileInfo.TryOpenRead (out stream)) { + var msg = "The file could not be opened."; + + throw new ArgumentException (msg, "fileInfo"); + } + + send (Opcode.Binary, stream); + } + + /// + /// Sends the specified data to the remote endpoint. + /// + /// + /// A that specifies the text data to send. + /// + /// + /// could not be UTF-8-encoded. + /// + /// + /// is . + /// + /// + /// The Send method is not available when the current state of + /// the interface is not Open. + /// + public void Send (string data) + { + if (_readyState != WebSocketState.Open) { + var msg = "The Send method is not available."; + + throw new InvalidOperationException (msg); + } + + if (data == null) + throw new ArgumentNullException ("data"); + + byte[] bytes; + + if (!data.TryGetUTF8EncodedBytes (out bytes)) { + var msg = "It could not be UTF-8-encoded."; + + throw new ArgumentException (msg, "data"); + } + + send (Opcode.Text, new MemoryStream (bytes)); + } + + /// + /// Sends the data from the specified stream instance to the remote endpoint. + /// + /// + /// + /// A instance from which to read the data to send. + /// + /// + /// The data is sent as the binary data. + /// + /// + /// + /// An that specifies the number of bytes to send. + /// + /// + /// + /// cannot be read. + /// + /// + /// -or- + /// + /// + /// is less than 1. + /// + /// + /// -or- + /// + /// + /// No data could be read from . + /// + /// + /// + /// is . + /// + /// + /// The Send method is not available when the current state of + /// the interface is not Open. + /// + public void Send (Stream stream, int length) + { + if (_readyState != WebSocketState.Open) { + var msg = "The Send method is not available."; + + throw new InvalidOperationException (msg); + } + + if (stream == null) + throw new ArgumentNullException ("stream"); + + if (!stream.CanRead) { + var msg = "It cannot be read."; + + throw new ArgumentException (msg, "stream"); + } + + if (length < 1) { + var msg = "Less than 1."; + + throw new ArgumentException (msg, "length"); + } + + var bytes = stream.ReadBytes (length); + var len = bytes.Length; + + if (len == 0) { + var msg = "No data could be read from it."; + + throw new ArgumentException (msg, "stream"); + } + + if (len < length) { + var fmt = "Only {0} byte(s) of data could be read from the stream."; + var msg = String.Format (fmt, len); + + _log.Warn (msg); + } + + send (Opcode.Binary, new MemoryStream (bytes)); + } + + /// + /// Sends the specified data to the remote endpoint asynchronously. + /// + /// + /// This method does not wait for the send to be complete. + /// + /// + /// An array of that specifies the binary data to send. + /// + /// + /// + /// An delegate. + /// + /// + /// It specifies the delegate called when the send is complete. + /// + /// + /// The parameter passed to the delegate is true + /// if the send has successfully done; otherwise, false. + /// + /// + /// if not necessary. + /// + /// + /// + /// is . + /// + /// + /// The SendAsync method is not available when the current state of + /// the interface is not Open. + /// + public void SendAsync (byte[] data, Action completed) + { + if (_readyState != WebSocketState.Open) { + var msg = "The SendAsync method is not available."; + + throw new InvalidOperationException (msg); + } + + if (data == null) + throw new ArgumentNullException ("data"); + + sendAsync (Opcode.Binary, new MemoryStream (data), completed); + } + + /// + /// Sends the specified file to the remote endpoint asynchronously. + /// + /// + /// This method does not wait for the send to be complete. + /// + /// + /// + /// A that specifies the file to send. + /// + /// + /// The file is sent as the binary data. + /// + /// + /// + /// + /// An delegate. + /// + /// + /// It specifies the delegate called when the send is complete. + /// + /// + /// The parameter passed to the delegate is true + /// if the send has successfully done; otherwise, false. + /// + /// + /// if not necessary. + /// + /// + /// + /// + /// The file does not exist. + /// + /// + /// -or- + /// + /// + /// The file could not be opened. + /// + /// + /// + /// is . + /// + /// + /// The SendAsync method is not available when the current state of + /// the interface is not Open. + /// + public void SendAsync (FileInfo fileInfo, Action completed) + { + if (_readyState != WebSocketState.Open) { + var msg = "The SendAsync method is not available."; + + throw new InvalidOperationException (msg); + } + + if (fileInfo == null) + throw new ArgumentNullException ("fileInfo"); + + if (!fileInfo.Exists) { + var msg = "The file does not exist."; + + throw new ArgumentException (msg, "fileInfo"); + } + + FileStream stream; + + if (!fileInfo.TryOpenRead (out stream)) { + var msg = "The file could not be opened."; + + throw new ArgumentException (msg, "fileInfo"); + } + + sendAsync (Opcode.Binary, stream, completed); + } + + /// + /// Sends the specified data to the remote endpoint asynchronously. + /// + /// + /// This method does not wait for the send to be complete. + /// + /// + /// A that specifies the text data to send. + /// + /// + /// + /// An delegate. + /// + /// + /// It specifies the delegate called when the send is complete. + /// + /// + /// The parameter passed to the delegate is true + /// if the send has successfully done; otherwise, false. + /// + /// + /// if not necessary. + /// + /// + /// + /// could not be UTF-8-encoded. + /// + /// + /// is . + /// + /// + /// The SendAsync method is not available when the current state of + /// the interface is not Open. + /// + public void SendAsync (string data, Action completed) + { + if (_readyState != WebSocketState.Open) { + var msg = "The SendAsync method is not available."; + + throw new InvalidOperationException (msg); + } + + if (data == null) + throw new ArgumentNullException ("data"); + + byte[] bytes; + + if (!data.TryGetUTF8EncodedBytes (out bytes)) { + var msg = "It could not be UTF-8-encoded."; + + throw new ArgumentException (msg, "data"); + } + + sendAsync (Opcode.Text, new MemoryStream (bytes), completed); + } + + /// + /// Sends the data from the specified stream instance to the remote + /// endpoint asynchronously. + /// + /// + /// This method does not wait for the send to be complete. + /// + /// + /// + /// A instance from which to read the data to send. + /// + /// + /// The data is sent as the binary data. + /// + /// + /// + /// An that specifies the number of bytes to send. + /// + /// + /// + /// An delegate. + /// + /// + /// It specifies the delegate called when the send is complete. + /// + /// + /// The parameter passed to the delegate is true + /// if the send has successfully done; otherwise, false. + /// + /// + /// if not necessary. + /// + /// + /// + /// + /// cannot be read. + /// + /// + /// -or- + /// + /// + /// is less than 1. + /// + /// + /// -or- + /// + /// + /// No data could be read from . + /// + /// + /// + /// is . + /// + /// + /// The SendAsync method is not available when the current state of + /// the interface is not Open. + /// + public void SendAsync (Stream stream, int length, Action completed) + { + if (_readyState != WebSocketState.Open) { + var msg = "The SendAsync method is not available."; + + throw new InvalidOperationException (msg); + } + + if (stream == null) + throw new ArgumentNullException ("stream"); + + if (!stream.CanRead) { + var msg = "It cannot be read."; + + throw new ArgumentException (msg, "stream"); + } + + if (length < 1) { + var msg = "Less than 1."; + + throw new ArgumentException (msg, "length"); + } + + var bytes = stream.ReadBytes (length); + var len = bytes.Length; + + if (len == 0) { + var msg = "No data could be read from it."; + + throw new ArgumentException (msg, "stream"); + } + + if (len < length) { + var fmt = "Only {0} byte(s) of data could be read from the stream."; + var msg = String.Format (fmt, len); + + _log.Warn (msg); + } + + sendAsync (Opcode.Binary, new MemoryStream (bytes), completed); + } + + /// + /// Sets an HTTP cookie to send with the handshake request or response. + /// + /// + /// A that specifies the cookie to send. + /// + /// + /// is . + /// + /// + /// The SetCookie method is not available when the current state of + /// the interface is neither New nor Closed. + /// + public void SetCookie (Cookie cookie) + { + if (cookie == null) + throw new ArgumentNullException ("cookie"); + + lock (_forState) { + if (!canSet ()) { + var msg = "The SetCookie method is not available."; + + throw new InvalidOperationException (msg); + } + + lock (_cookies.SyncRoot) + _cookies.SetOrRemove (cookie); + } + } + + /// + /// Sets the credentials for the HTTP authentication (Basic/Digest). + /// + /// + /// + /// A that specifies the username associated + /// with the credentials. + /// + /// + /// or an empty string if initializes + /// the credentials. + /// + /// + /// + /// + /// A that specifies the password for the + /// username associated with the credentials. + /// + /// + /// or an empty string if not necessary. + /// + /// + /// + /// A : true if sends the credentials for + /// the Basic authentication in advance with the first handshake + /// request; otherwise, false. + /// + /// + /// + /// contains an invalid character. + /// + /// + /// -or- + /// + /// + /// contains an invalid character. + /// + /// + /// + /// + /// The SetCredentials method is not available if the interface is not for + /// the client. + /// + /// + /// -or- + /// + /// + /// The SetCredentials method is not available when the current state of + /// the interface is neither New nor Closed. + /// + /// + public void SetCredentials (string username, string password, bool preAuth) + { + if (!_isClient) { + var msg = "The SetCredentials method is not available."; + + throw new InvalidOperationException (msg); + } + + if (!username.IsNullOrEmpty ()) { + if (username.Contains (':') || !username.IsText ()) { + var msg = "It contains an invalid character."; + + throw new ArgumentException (msg, "username"); + } + } + + if (!password.IsNullOrEmpty ()) { + if (!password.IsText ()) { + var msg = "It contains an invalid character."; + + throw new ArgumentException (msg, "password"); + } + } + + lock (_forState) { + if (!canSet ()) { + var msg = "The SetCredentials method is not available."; + + throw new InvalidOperationException (msg); + } + + if (username.IsNullOrEmpty ()) { + _credentials = null; + _preAuth = false; + + return; + } + + _credentials = new NetworkCredential ( + username, + password, + _uri.PathAndQuery + ); + + _preAuth = preAuth; + } + } + + /// + /// Sets the URL of the HTTP proxy server through which to connect and + /// the credentials for the HTTP proxy authentication (Basic/Digest). + /// + /// + /// + /// A that specifies the URL of the proxy + /// server through which to connect. + /// + /// + /// The syntax is http://<host>[:<port>]. + /// + /// + /// or an empty string if initializes + /// the URL and the credentials. + /// + /// + /// + /// + /// A that specifies the username associated + /// with the credentials. + /// + /// + /// or an empty string if the credentials + /// are not necessary. + /// + /// + /// + /// + /// A that specifies the password for the + /// username associated with the credentials. + /// + /// + /// or an empty string if not necessary. + /// + /// + /// + /// + /// is not an absolute URI string. + /// + /// + /// -or- + /// + /// + /// The scheme of is not http. + /// + /// + /// -or- + /// + /// + /// includes the path segments. + /// + /// + /// -or- + /// + /// + /// contains an invalid character. + /// + /// + /// -or- + /// + /// + /// contains an invalid character. + /// + /// + /// + /// + /// The SetProxy method is not available if the interface is not for + /// the client. + /// + /// + /// -or- + /// + /// + /// The SetProxy method is not available when the current state of + /// the interface is neither New nor Closed. + /// + /// + public void SetProxy (string url, string username, string password) + { + if (!_isClient) { + var msg = "The SetProxy method is not available."; + + throw new InvalidOperationException (msg); + } + + Uri uri = null; + + if (!url.IsNullOrEmpty ()) { + if (!Uri.TryCreate (url, UriKind.Absolute, out uri)) { + var msg = "Not an absolute URI string."; + + throw new ArgumentException (msg, "url"); + } + + if (uri.Scheme != "http") { + var msg = "The scheme part is not http."; + + throw new ArgumentException (msg, "url"); + } + + if (uri.Segments.Length > 1) { + var msg = "It includes the path segments."; + + throw new ArgumentException (msg, "url"); + } + } + + if (!username.IsNullOrEmpty ()) { + if (username.Contains (':') || !username.IsText ()) { + var msg = "It contains an invalid character."; + + throw new ArgumentException (msg, "username"); + } + } + + if (!password.IsNullOrEmpty ()) { + if (!password.IsText ()) { + var msg = "It contains an invalid character."; + + throw new ArgumentException (msg, "password"); + } + } + + lock (_forState) { + if (!canSet ()) { + var msg = "The SetProxy method is not available."; + + throw new InvalidOperationException (msg); + } + + if (url.IsNullOrEmpty ()) { + _proxyUri = null; + _proxyCredentials = null; + + return; + } + + _proxyUri = uri; + + if (username.IsNullOrEmpty ()) { + _proxyCredentials = null; + + return; + } + + var domain = String.Format ("{0}:{1}", _uri.DnsSafeHost, _uri.Port); + + _proxyCredentials = new NetworkCredential (username, password, domain); + } + } + + #endregion + + #region Explicit Interface Implementations + + /// + /// Closes the connection and releases all associated resources. + /// + /// + /// + /// This method closes the connection with close status 1001 (going away). + /// + /// + /// This method does nothing if the current state of the interface is + /// Closing or Closed. + /// + /// + void IDisposable.Dispose () + { + close (1001, String.Empty); + } + + #endregion + } +} diff --git a/Assets/External/websocket-sharp/WebSocket.cs.meta b/Assets/External/websocket-sharp/WebSocket.cs.meta new file mode 100644 index 00000000..45174e63 --- /dev/null +++ b/Assets/External/websocket-sharp/WebSocket.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 3a480253206dc3c4f912732f51a49115 \ No newline at end of file diff --git a/Assets/External/websocket-sharp/WebSocketException.cs b/Assets/External/websocket-sharp/WebSocketException.cs new file mode 100644 index 00000000..12bfc48f --- /dev/null +++ b/Assets/External/websocket-sharp/WebSocketException.cs @@ -0,0 +1,129 @@ +#region License +/* + * WebSocketException.cs + * + * The MIT License + * + * Copyright (c) 2012-2024 sta.blockhead + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#endregion + +using System; + +namespace WebSocketSharp +{ + /// + /// The exception that is thrown when a fatal error occurs in + /// the WebSocket communication. + /// + public class WebSocketException : Exception + { + #region Private Fields + + private ushort _code; + + #endregion + + #region Private Constructors + + private WebSocketException ( + ushort code, + string message, + Exception innerException + ) + : base (message ?? code.GetErrorMessage (), innerException) + { + _code = code; + } + + #endregion + + #region Internal Constructors + + internal WebSocketException () + : this (CloseStatusCode.Abnormal, null, null) + { + } + + internal WebSocketException (Exception innerException) + : this (CloseStatusCode.Abnormal, null, innerException) + { + } + + internal WebSocketException (string message) + : this (CloseStatusCode.Abnormal, message, null) + { + } + + internal WebSocketException (CloseStatusCode code) + : this (code, null, null) + { + } + + internal WebSocketException (string message, Exception innerException) + : this (CloseStatusCode.Abnormal, message, innerException) + { + } + + internal WebSocketException (CloseStatusCode code, Exception innerException) + : this (code, null, innerException) + { + } + + internal WebSocketException (CloseStatusCode code, string message) + : this (code, message, null) + { + } + + internal WebSocketException ( + CloseStatusCode code, + string message, + Exception innerException + ) + : this ((ushort) code, message, innerException) + { + } + + #endregion + + #region Public Properties + + /// + /// Gets the status code indicating the cause of the exception. + /// + /// + /// + /// A that represents the status code indicating + /// the cause of the exception. + /// + /// + /// It is one of the status codes for the WebSocket connection close. + /// + /// + public ushort Code { + get { + return _code; + } + } + + #endregion + } +} diff --git a/Assets/External/websocket-sharp/WebSocketException.cs.meta b/Assets/External/websocket-sharp/WebSocketException.cs.meta new file mode 100644 index 00000000..81f3a654 --- /dev/null +++ b/Assets/External/websocket-sharp/WebSocketException.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 9d25841977aab4448890e1106f19d70a \ No newline at end of file diff --git a/Assets/External/websocket-sharp/WebSocketFrame.cs b/Assets/External/websocket-sharp/WebSocketFrame.cs new file mode 100644 index 00000000..9ce51b94 --- /dev/null +++ b/Assets/External/websocket-sharp/WebSocketFrame.cs @@ -0,0 +1,916 @@ +#region License +/* + * WebSocketFrame.cs + * + * The MIT License + * + * Copyright (c) 2012-2025 sta.blockhead + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#endregion + +#region Contributors +/* + * Contributors: + * - Chris Swiedler + */ +#endregion + +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Text; + +namespace WebSocketSharp +{ + internal class WebSocketFrame : IEnumerable + { + #region Private Fields + + private static readonly int _defaultHeaderLength; + private static readonly int _defaultMaskingKeyLength; + private static readonly byte[] _emptyBytes; + private byte[] _extPayloadLength; + private Fin _fin; + private Mask _mask; + private byte[] _maskingKey; + private Opcode _opcode; + private PayloadData _payloadData; + private int _payloadLength; + private Rsv _rsv1; + private Rsv _rsv2; + private Rsv _rsv3; + + #endregion + + #region Static Constructor + + static WebSocketFrame () + { + _defaultHeaderLength = 2; + _defaultMaskingKeyLength = 4; + _emptyBytes = new byte[0]; + } + + #endregion + + #region Private Constructors + + private WebSocketFrame () + { + } + + #endregion + + #region Internal Constructors + + internal WebSocketFrame ( + Fin fin, + Opcode opcode, + byte[] data, + bool compressed, + bool mask + ) + : this (fin, opcode, new PayloadData (data), compressed, mask) + { + } + + internal WebSocketFrame ( + Fin fin, + Opcode opcode, + PayloadData payloadData, + bool compressed, + bool mask + ) + { + _fin = fin; + _opcode = opcode; + + _rsv1 = compressed ? Rsv.On : Rsv.Off; + _rsv2 = Rsv.Off; + _rsv3 = Rsv.Off; + + var len = payloadData.Length; + + if (len < 126) { + _payloadLength = (int) len; + _extPayloadLength = _emptyBytes; + } + else if (len < 0x010000) { + _payloadLength = 126; + _extPayloadLength = ((ushort) len).ToByteArray (ByteOrder.Big); + } + else { + _payloadLength = 127; + _extPayloadLength = len.ToByteArray (ByteOrder.Big); + } + + if (mask) { + _mask = Mask.On; + _maskingKey = createMaskingKey (); + + payloadData.Mask (_maskingKey); + } + else { + _mask = Mask.Off; + _maskingKey = _emptyBytes; + } + + _payloadData = payloadData; + } + + #endregion + + #region Internal Properties + + internal ulong ExactPayloadLength { + get { + return _payloadLength < 126 + ? (ulong) _payloadLength + : _payloadLength == 126 + ? _extPayloadLength.ToUInt16 (ByteOrder.Big) + : _extPayloadLength.ToUInt64 (ByteOrder.Big); + } + } + + internal int ExtendedPayloadLengthWidth { + get { + return _payloadLength < 126 + ? 0 + : _payloadLength == 126 + ? 2 + : 8; + } + } + + #endregion + + #region Public Properties + + public byte[] ExtendedPayloadLength { + get { + return _extPayloadLength; + } + } + + public Fin Fin { + get { + return _fin; + } + } + + public bool IsBinary { + get { + return _opcode == Opcode.Binary; + } + } + + public bool IsClose { + get { + return _opcode == Opcode.Close; + } + } + + public bool IsCompressed { + get { + return _rsv1 == Rsv.On; + } + } + + public bool IsContinuation { + get { + return _opcode == Opcode.Cont; + } + } + + public bool IsControl { + get { + return _opcode >= Opcode.Close; + } + } + + public bool IsData { + get { + return _opcode == Opcode.Text || _opcode == Opcode.Binary; + } + } + + public bool IsFinal { + get { + return _fin == Fin.Final; + } + } + + public bool IsFragment { + get { + return _fin == Fin.More || _opcode == Opcode.Cont; + } + } + + public bool IsMasked { + get { + return _mask == Mask.On; + } + } + + public bool IsPing { + get { + return _opcode == Opcode.Ping; + } + } + + public bool IsPong { + get { + return _opcode == Opcode.Pong; + } + } + + public bool IsText { + get { + return _opcode == Opcode.Text; + } + } + + public ulong Length { + get { + return (ulong) ( + _defaultHeaderLength + + _extPayloadLength.Length + + _maskingKey.Length + ) + + _payloadData.Length; + } + } + + public Mask Mask { + get { + return _mask; + } + } + + public byte[] MaskingKey { + get { + return _maskingKey; + } + } + + public Opcode Opcode { + get { + return _opcode; + } + } + + public PayloadData PayloadData { + get { + return _payloadData; + } + } + + public int PayloadLength { + get { + return _payloadLength; + } + } + + public Rsv Rsv1 { + get { + return _rsv1; + } + } + + public Rsv Rsv2 { + get { + return _rsv2; + } + } + + public Rsv Rsv3 { + get { + return _rsv3; + } + } + + #endregion + + #region Private Methods + + private static byte[] createMaskingKey () + { + var key = new byte[_defaultMaskingKeyLength]; + + WebSocket.RandomNumber.GetBytes (key); + + return key; + } + + private static WebSocketFrame processHeader (byte[] header) + { + if (header.Length != _defaultHeaderLength) { + var msg = "The header part of a frame could not be read."; + + throw new WebSocketException (msg); + } + + // FIN + var fin = (header[0] & 0x80) == 0x80 ? Fin.Final : Fin.More; + + // RSV1 + var rsv1 = (header[0] & 0x40) == 0x40 ? Rsv.On : Rsv.Off; + + // RSV2 + var rsv2 = (header[0] & 0x20) == 0x20 ? Rsv.On : Rsv.Off; + + // RSV3 + var rsv3 = (header[0] & 0x10) == 0x10 ? Rsv.On : Rsv.Off; + + // Opcode + var opcode = header[0] & 0x0f; + + // MASK + var mask = (header[1] & 0x80) == 0x80 ? Mask.On : Mask.Off; + + // Payload Length + var payloadLen = header[1] & 0x7f; + + if (!opcode.IsSupportedOpcode ()) { + var msg = "The opcode of a frame is not supported."; + + throw new WebSocketException (CloseStatusCode.UnsupportedData, msg); + } + + var frame = new WebSocketFrame (); + + frame._fin = fin; + frame._rsv1 = rsv1; + frame._rsv2 = rsv2; + frame._rsv3 = rsv3; + frame._opcode = (Opcode) opcode; + frame._mask = mask; + frame._payloadLength = payloadLen; + + return frame; + } + + private static WebSocketFrame readExtendedPayloadLength ( + Stream stream, + WebSocketFrame frame + ) + { + var len = frame.ExtendedPayloadLengthWidth; + + if (len == 0) { + frame._extPayloadLength = _emptyBytes; + + return frame; + } + + var bytes = stream.ReadBytes (len); + + if (bytes.Length != len) { + var msg = "The extended payload length of a frame could not be read."; + + throw new WebSocketException (msg); + } + + frame._extPayloadLength = bytes; + + return frame; + } + + private static void readExtendedPayloadLengthAsync ( + Stream stream, + WebSocketFrame frame, + Action completed, + Action error + ) + { + var len = frame.ExtendedPayloadLengthWidth; + + if (len == 0) { + frame._extPayloadLength = _emptyBytes; + + completed (frame); + + return; + } + + stream.ReadBytesAsync ( + len, + bytes => { + if (bytes.Length != len) { + var msg = "The extended payload length of a frame could not be read."; + + throw new WebSocketException (msg); + } + + frame._extPayloadLength = bytes; + + completed (frame); + }, + error + ); + } + + private static WebSocketFrame readHeader (Stream stream) + { + var bytes = stream.ReadBytes (_defaultHeaderLength); + + return processHeader (bytes); + } + + private static void readHeaderAsync ( + Stream stream, + Action completed, + Action error + ) + { + stream.ReadBytesAsync ( + _defaultHeaderLength, + bytes => { + var frame = processHeader (bytes); + + completed (frame); + }, + error + ); + } + + private static WebSocketFrame readMaskingKey ( + Stream stream, + WebSocketFrame frame + ) + { + if (!frame.IsMasked) { + frame._maskingKey = _emptyBytes; + + return frame; + } + + var bytes = stream.ReadBytes (_defaultMaskingKeyLength); + + if (bytes.Length != _defaultMaskingKeyLength) { + var msg = "The masking key of a frame could not be read."; + + throw new WebSocketException (msg); + } + + frame._maskingKey = bytes; + + return frame; + } + + private static void readMaskingKeyAsync ( + Stream stream, + WebSocketFrame frame, + Action completed, + Action error + ) + { + if (!frame.IsMasked) { + frame._maskingKey = _emptyBytes; + + completed (frame); + + return; + } + + stream.ReadBytesAsync ( + _defaultMaskingKeyLength, + bytes => { + if (bytes.Length != _defaultMaskingKeyLength) { + var msg = "The masking key of a frame could not be read."; + + throw new WebSocketException (msg); + } + + frame._maskingKey = bytes; + + completed (frame); + }, + error + ); + } + + private static WebSocketFrame readPayloadData ( + Stream stream, + WebSocketFrame frame + ) + { + var exactPayloadLen = frame.ExactPayloadLength; + + if (exactPayloadLen > PayloadData.MaxLength) { + var msg = "The payload data of a frame is too big."; + + throw new WebSocketException (CloseStatusCode.TooBig, msg); + } + + if (exactPayloadLen == 0) { + frame._payloadData = PayloadData.Empty; + + return frame; + } + + var len = (long) exactPayloadLen; + var bytes = frame._payloadLength > 126 + ? stream.ReadBytes (len, 1024) + : stream.ReadBytes ((int) len); + + if (bytes.LongLength != len) { + var msg = "The payload data of a frame could not be read."; + + throw new WebSocketException (msg); + } + + frame._payloadData = new PayloadData (bytes, len); + + return frame; + } + + private static void readPayloadDataAsync ( + Stream stream, + WebSocketFrame frame, + Action completed, + Action error + ) + { + var exactPayloadLen = frame.ExactPayloadLength; + + if (exactPayloadLen > PayloadData.MaxLength) { + var msg = "The payload data of a frame is too big."; + + throw new WebSocketException (CloseStatusCode.TooBig, msg); + } + + if (exactPayloadLen == 0) { + frame._payloadData = PayloadData.Empty; + + completed (frame); + + return; + } + + var len = (long) exactPayloadLen; + + Action comp = + bytes => { + if (bytes.LongLength != len) { + var msg = "The payload data of a frame could not be read."; + + throw new WebSocketException (msg); + } + + frame._payloadData = new PayloadData (bytes, len); + + completed (frame); + }; + + if (frame._payloadLength > 126) { + stream.ReadBytesAsync (len, 1024, comp, error); + + return; + } + + stream.ReadBytesAsync ((int) len, comp, error); + } + + private string toDumpString () + { + var len = Length; + var cnt = (long) (len / 4); + var rem = (int) (len % 4); + + string spFmt; + string cntFmt; + + if (cnt < 10000) { + spFmt = "{0,4}"; + cntFmt = "{0,4}"; + } + else if (cnt < 0x010000) { + spFmt = "{0,4}"; + cntFmt = "{0,4:X}"; + } + else if (cnt < 0x0100000000) { + spFmt = "{0,8}"; + cntFmt = "{0,8:X}"; + } + else { + spFmt = "{0,16}"; + cntFmt = "{0,16:X}"; + } + + var baseFmt = @"{0} 01234567 89ABCDEF 01234567 89ABCDEF +{0}+--------+--------+--------+--------+ +"; + var headerFmt = String.Format (baseFmt, spFmt); + + baseFmt = "{0}|{{1,8}} {{2,8}} {{3,8}} {{4,8}}|\n"; + var lineFmt = String.Format (baseFmt, cntFmt); + + baseFmt = "{0}+--------+--------+--------+--------+"; + var footerFmt = String.Format (baseFmt, spFmt); + + var buff = new StringBuilder (64); + + Func> lineWriter = + () => { + long lineCnt = 0; + + return (arg1, arg2, arg3, arg4) => { + buff.AppendFormat ( + lineFmt, + ++lineCnt, + arg1, + arg2, + arg3, + arg4 + ); + }; + }; + + var writeLine = lineWriter (); + var bytes = ToArray (); + + buff.AppendFormat (headerFmt, String.Empty); + + for (long i = 0; i <= cnt; i++) { + var j = i * 4; + + if (i < cnt) { + var arg1 = Convert.ToString (bytes[j], 2).PadLeft (8, '0'); + var arg2 = Convert.ToString (bytes[j + 1], 2).PadLeft (8, '0'); + var arg3 = Convert.ToString (bytes[j + 2], 2).PadLeft (8, '0'); + var arg4 = Convert.ToString (bytes[j + 3], 2).PadLeft (8, '0'); + + writeLine (arg1, arg2, arg3, arg4); + + continue; + } + + if (rem > 0) { + var arg1 = Convert.ToString (bytes[j], 2).PadLeft (8, '0'); + var arg2 = rem >= 2 + ? Convert.ToString (bytes[j + 1], 2).PadLeft (8, '0') + : String.Empty; + + var arg3 = rem == 3 + ? Convert.ToString (bytes[j + 2], 2).PadLeft (8, '0') + : String.Empty; + + writeLine (arg1, arg2, arg3, String.Empty); + } + } + + buff.AppendFormat (footerFmt, String.Empty); + + return buff.ToString (); + } + + private string toString () + { + var extPayloadLen = _payloadLength >= 126 + ? ExactPayloadLength.ToString () + : String.Empty; + + var maskingKey = _mask == Mask.On + ? BitConverter.ToString (_maskingKey) + : String.Empty; + + var payloadData = _payloadLength >= 126 + ? "***" + : _payloadLength > 0 + ? _payloadData.ToString () + : String.Empty; + + var fmt = @" FIN: {0} + RSV1: {1} + RSV2: {2} + RSV3: {3} + Opcode: {4} + MASK: {5} + Payload Length: {6} +Extended Payload Length: {7} + Masking Key: {8} + Payload Data: {9}"; + + return String.Format ( + fmt, + _fin, + _rsv1, + _rsv2, + _rsv3, + _opcode, + _mask, + _payloadLength, + extPayloadLen, + maskingKey, + payloadData + ); + } + + #endregion + + #region Internal Methods + + internal static WebSocketFrame CreateCloseFrame ( + PayloadData payloadData, + bool mask + ) + { + return new WebSocketFrame ( + Fin.Final, + Opcode.Close, + payloadData, + false, + mask + ); + } + + internal static WebSocketFrame CreatePingFrame (bool mask) + { + return new WebSocketFrame ( + Fin.Final, + Opcode.Ping, + PayloadData.Empty, + false, + mask + ); + } + + internal static WebSocketFrame CreatePingFrame (byte[] data, bool mask) + { + return new WebSocketFrame ( + Fin.Final, + Opcode.Ping, + new PayloadData (data), + false, + mask + ); + } + + internal static WebSocketFrame CreatePongFrame ( + PayloadData payloadData, + bool mask + ) + { + return new WebSocketFrame ( + Fin.Final, + Opcode.Pong, + payloadData, + false, + mask + ); + } + + internal static WebSocketFrame ReadFrame (Stream stream, bool unmask) + { + var frame = readHeader (stream); + + readExtendedPayloadLength (stream, frame); + readMaskingKey (stream, frame); + readPayloadData (stream, frame); + + if (unmask) + frame.Unmask (); + + return frame; + } + + internal static void ReadFrameAsync ( + Stream stream, + bool unmask, + Action completed, + Action error + ) + { + readHeaderAsync ( + stream, + frame => + readExtendedPayloadLengthAsync ( + stream, + frame, + frame1 => + readMaskingKeyAsync ( + stream, + frame1, + frame2 => + readPayloadDataAsync ( + stream, + frame2, + frame3 => { + if (unmask) + frame3.Unmask (); + + completed (frame3); + }, + error + ), + error + ), + error + ), + error + ); + } + + internal string ToString (bool dump) + { + return dump ? toDumpString () : toString (); + } + + internal void Unmask () + { + if (_mask == Mask.Off) + return; + + _payloadData.Mask (_maskingKey); + + _maskingKey = _emptyBytes; + _mask = Mask.Off; + } + + #endregion + + #region Public Methods + + public IEnumerator GetEnumerator () + { + foreach (var b in ToArray ()) + yield return b; + } + + public byte[] ToArray () + { + using (var buff = new MemoryStream ()) { + var header = (int) _fin; + + header = (header << 1) + (int) _rsv1; + header = (header << 1) + (int) _rsv2; + header = (header << 1) + (int) _rsv3; + header = (header << 4) + (int) _opcode; + header = (header << 1) + (int) _mask; + header = (header << 7) + _payloadLength; + + var headerAsUInt16 = (ushort) header; + var headerAsBytes = headerAsUInt16.ToByteArray (ByteOrder.Big); + + buff.Write (headerAsBytes, 0, _defaultHeaderLength); + + if (_payloadLength >= 126) + buff.Write (_extPayloadLength, 0, _extPayloadLength.Length); + + if (_mask == Mask.On) + buff.Write (_maskingKey, 0, _defaultMaskingKeyLength); + + if (_payloadLength > 0) { + var bytes = _payloadData.ToArray (); + + if (_payloadLength > 126) + buff.WriteBytes (bytes, 1024); + else + buff.Write (bytes, 0, bytes.Length); + } + + buff.Close (); + + return buff.ToArray (); + } + } + + public override string ToString () + { + var val = ToArray (); + + return BitConverter.ToString (val); + } + + #endregion + + #region Explicit Interface Implementations + + IEnumerator IEnumerable.GetEnumerator () + { + return GetEnumerator (); + } + + #endregion + } +} diff --git a/Assets/External/websocket-sharp/WebSocketFrame.cs.meta b/Assets/External/websocket-sharp/WebSocketFrame.cs.meta new file mode 100644 index 00000000..6c451de4 --- /dev/null +++ b/Assets/External/websocket-sharp/WebSocketFrame.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 5859a82080f86b94eb109ce6fbe5cccc \ No newline at end of file diff --git a/Assets/External/websocket-sharp/WebSocketState.cs b/Assets/External/websocket-sharp/WebSocketState.cs new file mode 100644 index 00000000..fa170497 --- /dev/null +++ b/Assets/External/websocket-sharp/WebSocketState.cs @@ -0,0 +1,64 @@ +#region License +/* + * WebSocketState.cs + * + * The MIT License + * + * Copyright (c) 2010-2022 sta.blockhead + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#endregion + +using System; + +namespace WebSocketSharp +{ + /// + /// Indicates the state of the WebSocket interface. + /// + public enum WebSocketState : ushort + { + /// + /// Equivalent to numeric value 0. Indicates that a new interface has + /// been created. + /// + New = 0, + /// + /// Equivalent to numeric value 1. Indicates that the connect process is + /// in progress. + /// + Connecting = 1, + /// + /// Equivalent to numeric value 2. Indicates that the connection has + /// been established and the communication is possible. + /// + Open = 2, + /// + /// Equivalent to numeric value 3. Indicates that the close process is + /// in progress. + /// + Closing = 3, + /// + /// Equivalent to numeric value 4. Indicates that the connection has + /// been closed or could not be established. + /// + Closed = 4 + } +} diff --git a/Assets/External/websocket-sharp/WebSocketState.cs.meta b/Assets/External/websocket-sharp/WebSocketState.cs.meta new file mode 100644 index 00000000..1b930b4c --- /dev/null +++ b/Assets/External/websocket-sharp/WebSocketState.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: af536ce2e98f80c45b6d12fcc09c7091 \ No newline at end of file diff --git a/Assets/External/websocket-sharp/doc.meta b/Assets/External/websocket-sharp/doc.meta new file mode 100644 index 00000000..61833149 --- /dev/null +++ b/Assets/External/websocket-sharp/doc.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 46c4ea1e7307741499dc638fa5212eff +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/External/websocket-sharp/doc/.gitignore b/Assets/External/websocket-sharp/doc/.gitignore new file mode 100644 index 00000000..7b744c39 --- /dev/null +++ b/Assets/External/websocket-sharp/doc/.gitignore @@ -0,0 +1,4 @@ +## Ignore MonoDevelop build results. + +html +mdoc diff --git a/Assets/External/websocket-sharp/doc/doc.sh b/Assets/External/websocket-sharp/doc/doc.sh new file mode 100644 index 00000000..e4f3fa6b --- /dev/null +++ b/Assets/External/websocket-sharp/doc/doc.sh @@ -0,0 +1,31 @@ +#!/bin/sh +# +# @(#) doc.sh ver.0.0.2 2013.01.24 +# +# Usage: +# doc.sh +# +# Description: +# Creating documentation for websocket-sharp. +# +########################################################################### + +SRC_DIR="../bin/Release_Ubuntu" +XML="${SRC_DIR}/websocket-sharp.xml" +DLL="${SRC_DIR}/websocket-sharp.dll" + +DOC_DIR="." +MDOC_DIR="${DOC_DIR}/mdoc" +HTML_DIR="${DOC_DIR}/html" + +createDir() { + if [ ! -d $1 ]; then + mkdir -p $1 + fi +} + +set -e +createDir ${MDOC_DIR} +createDir ${HTML_DIR} +mdoc update --delete -fno-assembly-versions -i ${XML} -o ${MDOC_DIR}/ ${DLL} +mdoc export-html -o ${HTML_DIR}/ ${MDOC_DIR}/ diff --git a/Assets/External/websocket-sharp/doc/doc.sh.meta b/Assets/External/websocket-sharp/doc/doc.sh.meta new file mode 100644 index 00000000..0b0a2bbf --- /dev/null +++ b/Assets/External/websocket-sharp/doc/doc.sh.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 24ca82d1a0c4a0747ab06a04aa62e734 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/External/websocket-sharp/websocket-sharp.csproj.meta b/Assets/External/websocket-sharp/websocket-sharp.csproj.meta new file mode 100644 index 00000000..66af5f92 --- /dev/null +++ b/Assets/External/websocket-sharp/websocket-sharp.csproj.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: ae0a68acee725e141b02318f249f7990 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/External/websocket-sharp/websocket-sharp.snk b/Assets/External/websocket-sharp/websocket-sharp.snk new file mode 100644 index 00000000..a2546f38 Binary files /dev/null and b/Assets/External/websocket-sharp/websocket-sharp.snk differ diff --git a/Assets/External/websocket-sharp/websocket-sharp.snk.meta b/Assets/External/websocket-sharp/websocket-sharp.snk.meta new file mode 100644 index 00000000..58b1e313 --- /dev/null +++ b/Assets/External/websocket-sharp/websocket-sharp.snk.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 902210c13b972ad4d83481b1eb803044 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/ResourcesData/Project/250506_페닷/250506_페닷_아바타.unity b/Assets/ResourcesData/Project/250506_페닷/250506_페닷_아바타.unity index 2bdbe9d6..a03b3afc 100644 --- a/Assets/ResourcesData/Project/250506_페닷/250506_페닷_아바타.unity +++ b/Assets/ResourcesData/Project/250506_페닷/250506_페닷_아바타.unity @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e33c3bdcdfdf5e5526acd5ceece6bcfe8ee009f2ec690dfa284655265e07d773 -size 1183091 +oid sha256:9a9a49b9b3a2e2378e9fd5b7175eadb8a655f0e636fd46323ccdfaa2bb3e2f24 +size 1195131 diff --git a/Assets/Scripts/Streamdeck/DebugTest.cs b/Assets/Scripts/Streamdeck/DebugTest.cs deleted file mode 100644 index 7b6782fd..00000000 --- a/Assets/Scripts/Streamdeck/DebugTest.cs +++ /dev/null @@ -1,9 +0,0 @@ -using UnityEngine; - -public class DebugTest : MonoBehaviour -{ - public void DebugLog() - { - Debug.Log("message"); - } -} diff --git a/Assets/Scripts/Streamdeck/DebugTest.cs.meta b/Assets/Scripts/Streamdeck/DebugTest.cs.meta deleted file mode 100644 index 2a94d5f4..00000000 --- a/Assets/Scripts/Streamdeck/DebugTest.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 88aefb43401e8a94f820bc478c2202d5 \ No newline at end of file diff --git a/Assets/Scripts/Streamdeck/SimpleEventExample.cs b/Assets/Scripts/Streamdeck/SimpleEventExample.cs deleted file mode 100644 index 11120096..00000000 --- a/Assets/Scripts/Streamdeck/SimpleEventExample.cs +++ /dev/null @@ -1,101 +0,0 @@ -using UnityEngine; - -public class SimpleEventExample : MonoBehaviour -{ - [Header("StreamDock 통신")] - [SerializeField] private SimpleStreamDockCommunicator streamDock; - - void Start() - { - // StreamDock 이벤트 구독 - if (streamDock != null) - { - streamDock.OnStreamDockMessageReceived.AddListener(OnStreamDockMessage); - streamDock.OnConnected.AddListener(OnStreamDockConnected); - streamDock.OnDisconnected.AddListener(OnStreamDockDisconnected); - } - } - - /// - /// StreamDock에서 메시지 수신 시 처리 - /// - private void OnStreamDockMessage(string eventType, object data) - { - //Debug.Log($"StreamDock 이벤트 수신: {eventType}"); - - switch (eventType) - { - case "button_clicked": - // 버튼 클릭 시 실행할 코드 - Debug.Log("버튼 클릭 이벤트 실행!"); - DoSomething(); - break; - - case "dial_rotate": - // 다이얼 회전 시 실행할 코드 - Debug.Log("다이얼 회전 이벤트 실행!"); - DoSomethingElse(); - break; - - case "dial_press": - // 다이얼 누름 시 실행할 코드 - Debug.Log("다이얼 누름 이벤트 실행!"); - DoAnotherThing(); - break; - } - } - - /// - /// StreamDock 연결 시 처리 - /// - private void OnStreamDockConnected() - { - Debug.Log("StreamDock에 연결되었습니다!"); - } - - /// - /// StreamDock 연결 해제 시 처리 - /// - private void OnStreamDockDisconnected() - { - Debug.Log("StreamDock 연결이 해제되었습니다."); - } - - // 여기에 원하는 동작들을 구현하세요 - private void DoSomething() - { - Debug.Log("버튼 클릭으로 실행된 동작!"); - // 예: 오브젝트 활성화/비활성화, 애니메이션 재생, 사운드 재생 등 - } - - private void DoSomethingElse() - { - Debug.Log("다이얼 회전으로 실행된 동작!"); - // 예: 볼륨 조절, 카메라 회전, 값 변경 등 - } - - private void DoAnotherThing() - { - Debug.Log("다이얼 누름으로 실행된 동작!"); - // 예: 특수 기능 실행, 모드 변경 등 - } - - // 공개 메서드들 (Inspector에서 호출 가능) - [ContextMenu("테스트 동작 1")] - public void TestAction1() - { - DoSomething(); - } - - [ContextMenu("테스트 동작 2")] - public void TestAction2() - { - DoSomethingElse(); - } - - [ContextMenu("테스트 동작 3")] - public void TestAction3() - { - DoAnotherThing(); - } -} \ No newline at end of file diff --git a/Assets/Scripts/Streamdeck/SimpleEventExample.cs.meta b/Assets/Scripts/Streamdeck/SimpleEventExample.cs.meta deleted file mode 100644 index ab7c1e73..00000000 --- a/Assets/Scripts/Streamdeck/SimpleEventExample.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: e65616cccbfc27348b6c11acf939a5f4 \ No newline at end of file diff --git a/Assets/Scripts/Streamdeck/SimpleStreamDockCommunicator.cs b/Assets/Scripts/Streamdeck/SimpleStreamDockCommunicator.cs deleted file mode 100644 index 73541a70..00000000 --- a/Assets/Scripts/Streamdeck/SimpleStreamDockCommunicator.cs +++ /dev/null @@ -1,274 +0,0 @@ -using UnityEngine; -using UnityEngine.Events; -using System; -using System.Collections; -using System.Text; -using System.Net.WebSockets; -using System.Threading; -using System.Threading.Tasks; - -[System.Serializable] -public class StreamDockEvent : UnityEvent { } - -public class SimpleStreamDockCommunicator : MonoBehaviour -{ - [Header("연결 설정")] - [SerializeField] private string serverUrl = "ws://localhost:15732"; - [SerializeField] private bool autoConnect = true; - [SerializeField] private float reconnectInterval = 5f; - - [Header("이벤트")] - public StreamDockEvent OnStreamDockMessageReceived; - public UnityEvent OnConnected; - public UnityEvent OnDisconnected; - - // 내부 변수 - private ClientWebSocket webSocket; - private CancellationTokenSource cancellationTokenSource; - private bool isConnecting = false; - private bool isConnected = false; - - // 프로퍼티 - public bool IsConnected => isConnected; - - void Start() - { - if (autoConnect) - { - ConnectToStreamDock(); - } - } - - void OnDestroy() - { - DisconnectFromStreamDock(); - } - - /// - /// StreamDock에 연결 - /// - public async void ConnectToStreamDock() - { - if (isConnecting || isConnected) return; - - isConnecting = true; - - try - { - webSocket = new ClientWebSocket(); - cancellationTokenSource = new CancellationTokenSource(); - - Debug.Log($"StreamDock에 연결 중... {serverUrl}"); - - await webSocket.ConnectAsync(new Uri(serverUrl), cancellationTokenSource.Token); - - isConnected = true; - isConnecting = false; - - Debug.Log("StreamDock에 연결되었습니다!"); - OnConnected?.Invoke(); - - // 메시지 수신 시작 - _ = ReceiveMessages(); - - } - catch (Exception e) - { - Debug.LogError($"StreamDock 연결 실패: {e.Message}"); - isConnecting = false; - OnDisconnected?.Invoke(); - - // 재연결 시도 - StartCoroutine(TryReconnect()); - } - } - - /// - /// StreamDock 연결 해제 - /// - public async void DisconnectFromStreamDock() - { - if (!isConnected) return; - - try - { - cancellationTokenSource?.Cancel(); - - if (webSocket != null && webSocket.State == WebSocketState.Open) - { - await webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Unity 종료", CancellationToken.None); - } - } - catch (Exception e) - { - Debug.LogError($"연결 해제 중 오류: {e.Message}"); - } - finally - { - isConnected = false; - webSocket?.Dispose(); - webSocket = null; - OnDisconnected?.Invoke(); - } - } - - /// - /// StreamDock으로 메시지 전송 - /// - public async void SendMessageToStreamDock(string eventType, object data = null) - { - if (!isConnected) - { - Debug.LogWarning("StreamDock에 연결되지 않았습니다."); - return; - } - - try - { - var message = new - { - type = eventType, - data = data, - timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() - }; - - string jsonMessage = JsonUtility.ToJson(message); - byte[] buffer = Encoding.UTF8.GetBytes(jsonMessage); - - await webSocket.SendAsync(new ArraySegment(buffer), WebSocketMessageType.Text, true, cancellationTokenSource.Token); - - Debug.Log($"StreamDock으로 메시지 전송: {eventType}"); - } - catch (Exception e) - { - Debug.LogError($"메시지 전송 실패: {e.Message}"); - } - } - - /// - /// 메시지 수신 처리 - /// - private async Task ReceiveMessages() - { - var buffer = new byte[4096]; - - try - { - while (webSocket.State == WebSocketState.Open) - { - var result = await webSocket.ReceiveAsync(new ArraySegment(buffer), cancellationTokenSource.Token); - - if (result.MessageType == WebSocketMessageType.Text) - { - string message = Encoding.UTF8.GetString(buffer, 0, result.Count); - ProcessReceivedMessage(message); - } - else if (result.MessageType == WebSocketMessageType.Close) - { - Debug.Log("StreamDock에서 연결을 종료했습니다."); - break; - } - } - } - catch (Exception e) - { - Debug.LogError($"메시지 수신 중 오류: {e.Message}"); - } - finally - { - isConnected = false; - OnDisconnected?.Invoke(); - StartCoroutine(TryReconnect()); - } - } - - /// - /// 수신된 메시지 처리 - /// - private void ProcessReceivedMessage(string message) - { - try - { - //Debug.Log($"StreamDock에서 메시지 수신: {message}"); - - // JSON 파싱 (간단한 구조) - if (message.Contains("type")) - { - // UnityEvent 호출 - OnStreamDockMessageReceived?.Invoke("streamdock_message", message); - - // 특정 이벤트 타입 처리 - if (message.Contains("streamdock_button_clicked")) - { - HandleButtonClick(message); - } - else if (message.Contains("dial_rotate")) - { - HandleDialRotate(message); - } - else if (message.Contains("dial_press")) - { - HandleDialPress(message); - } - } - } - catch (Exception e) - { - Debug.LogError($"메시지 처리 중 오류: {e.Message}"); - } - } - - /// - /// 버튼 클릭 이벤트 처리 - /// - private void HandleButtonClick(string message) - { - Debug.Log("StreamDock 버튼이 클릭되었습니다!"); - OnStreamDockMessageReceived?.Invoke("button_clicked", message); - } - - /// - /// 다이얼 회전 이벤트 처리 - /// - private void HandleDialRotate(string message) - { - Debug.Log("StreamDock 다이얼이 회전했습니다!"); - OnStreamDockMessageReceived?.Invoke("dial_rotate", message); - } - - /// - /// 다이얼 누름 이벤트 처리 - /// - private void HandleDialPress(string message) - { - Debug.Log("StreamDock 다이얼이 눌렸습니다!"); - OnStreamDockMessageReceived?.Invoke("dial_press", message); - } - - /// - /// 재연결 시도 - /// - private IEnumerator TryReconnect() - { - yield return new WaitForSeconds(reconnectInterval); - - if (!isConnected && !isConnecting) - { - Debug.Log("StreamDock 재연결 시도..."); - ConnectToStreamDock(); - } - } - - // 테스트용 메서드들 - [ContextMenu("테스트 메시지 전송")] - public void SendTestMessage() - { - SendMessageToStreamDock("test_message", new { message = "Unity에서 테스트 메시지" }); - } - - [ContextMenu("커스텀 이벤트 전송")] - public void SendCustomEvent() - { - SendMessageToStreamDock("custom_event", new { action = "test_action", value = 123 }); - } -} \ No newline at end of file diff --git a/Assets/Scripts/Streamdeck/SimpleStreamDockCommunicator.cs.meta b/Assets/Scripts/Streamdeck/SimpleStreamDockCommunicator.cs.meta deleted file mode 100644 index f197e15e..00000000 --- a/Assets/Scripts/Streamdeck/SimpleStreamDockCommunicator.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: dd2d6415e76fac64ea5a022f3cfefa97 \ No newline at end of file diff --git a/Assets/Scripts/Streamdeck/StreamDeckServerManager.cs b/Assets/Scripts/Streamdeck/StreamDeckServerManager.cs new file mode 100644 index 00000000..5fa6be15 --- /dev/null +++ b/Assets/Scripts/Streamdeck/StreamDeckServerManager.cs @@ -0,0 +1,647 @@ +using UnityEngine; +using WebSocketSharp.Server; +using System.Collections.Generic; +using Newtonsoft.Json; +using System; +using System.Collections; + +public class StreamDeckServerManager : MonoBehaviour +{ + [Header("서버 설정")] + public int port = 10701; + + private WebSocketServer server; + private List connectedClients = new List(); + public CameraManager cameraManager { get; private set; } + public ItemController itemController { get; private set; } + + // 싱글톤 패턴으로 StreamDeckService에서 접근 가능하도록 + public static StreamDeckServerManager Instance { get; private set; } + + // 메인 스레드에서 처리할 작업 큐 + private readonly Queue mainThreadActions = new Queue(); + private readonly object lockObject = new object(); + + void Awake() + { + Instance = this; + } + + void Start() + { + // CameraManager 찾기 + cameraManager = FindObjectOfType(); + if (cameraManager == null) + { + Debug.LogError("[StreamDeckServerManager] CameraManager를 찾을 수 없습니다!"); + return; + } + + // ItemController 찾기 + itemController = FindObjectOfType(); + if (itemController == null) + { + Debug.LogWarning("[StreamDeckServerManager] ItemController를 찾을 수 없습니다. 아이템 컨트롤 기능이 비활성화됩니다."); + } + + StartServer(); + } + + void Update() + { + // 메인 스레드에서 대기 중인 작업들을 처리 + lock (lockObject) + { + while (mainThreadActions.Count > 0) + { + var action = mainThreadActions.Dequeue(); + try + { + action?.Invoke(); + } + catch (Exception ex) + { + Debug.LogError($"[StreamDeckServerManager] 메인 스레드 작업 실행 오류: {ex.Message}"); + } + } + } + } + + void OnApplicationQuit() + { + StopServer(); + } + + private void StartServer() + { + try + { + server = new WebSocketServer($"ws://127.0.0.1:{port}"); + server.AddWebSocketService("/"); + server.Start(); + Debug.Log($"[StreamDeckServerManager] WebSocket 서버 시작됨, 포트: {port}"); + } + catch (Exception e) + { + Debug.LogError($"[StreamDeckServerManager] 서버 시작 실패: {e.Message}"); + } + } + + private void StopServer() + { + if (server != null) + { + server.Stop(); + server = null; + Debug.Log("[StreamDeckServerManager] 서버 중지됨"); + } + } + + public void OnClientConnected(StreamDeckService service) + { + // 메인 스레드에서 처리하도록 큐에 추가 + lock (lockObject) + { + mainThreadActions.Enqueue(() => { + connectedClients.Add(service); + Debug.Log($"[StreamDeckServerManager] 클라이언트 연결됨. 총 연결: {connectedClients.Count}"); + SendInitialCameraData(service); + }); + } + } + + // 메시지 처리도 메인 스레드로 전달 + public void ProcessMessageOnMainThread(string messageData, StreamDeckService service) + { + lock (lockObject) + { + mainThreadActions.Enqueue(() => { + try + { + ProcessMessage(messageData, service); + } + catch (Exception ex) + { + Debug.LogError($"[StreamDeckServerManager] 메시지 처리 오류: {ex.Message}"); + } + }); + } + } + + public void OnClientDisconnected(StreamDeckService service) + { + connectedClients.Remove(service); + Debug.Log($"[StreamDeckServerManager] 클라이언트 연결 해제됨. 총 연결: {connectedClients.Count}"); + } + + private void SendInitialCameraData(StreamDeckService service) + { + if (cameraManager == null) return; + + var initialData = new + { + type = "connection_established", + timestamp = DateTime.UtcNow.ToString("o"), + version = "1.0", + data = new + { + session_id = Guid.NewGuid().ToString(), + message = "유니티 서버에 연결되었습니다!", + camera_data = cameraManager.GetCameraListData(), + current_camera = cameraManager.GetCurrentCameraState(), + item_data = itemController?.GetItemListData(), + current_item = itemController?.GetCurrentItemState() + } + }; + + string json = JsonConvert.SerializeObject(initialData); + service.SendMessage(json); + Debug.Log("[StreamDeckServerManager] 초기 데이터 전송됨 (카메라 + 아이템)"); + } + + public void BroadcastMessage(string message) + { + foreach (var client in connectedClients.ToArray()) + { + try + { + client.SendMessage(message); + } + catch (Exception e) + { + Debug.LogError($"[StreamDeckServerManager] 메시지 전송 실패: {e.Message}"); + connectedClients.Remove(client); + } + } + } + + // 카메라 변경 시 모든 클라이언트에게 알림 + public void NotifyCameraChanged() + { + if (cameraManager == null) return; + + var updateMessage = new + { + type = "camera_changed", + timestamp = DateTime.UtcNow.ToString("o"), + version = "1.0", + data = new + { + camera_data = cameraManager.GetCameraListData(), + current_camera = cameraManager.GetCurrentCameraState() + } + }; + + string json = JsonConvert.SerializeObject(updateMessage); + BroadcastMessage(json); + Debug.Log("[StreamDeckServerManager] 카메라 변경 알림 전송됨"); + } + + // 아이템 변경 시 모든 클라이언트에게 알림 + public void NotifyItemChanged() + { + if (itemController == null) return; + + var updateMessage = new + { + type = "item_changed", + timestamp = DateTime.UtcNow.ToString("o"), + version = "1.0", + data = new + { + item_data = itemController.GetItemListData(), + current_item = itemController.GetCurrentItemState() + } + }; + + string json = JsonConvert.SerializeObject(updateMessage); + BroadcastMessage(json); + Debug.Log("[StreamDeckServerManager] 아이템 변경 알림 전송됨"); + } + + // 메시지 처리 로직을 여기로 이동 + private void ProcessMessage(string messageData, StreamDeckService service) + { + // JSON 파싱 시도 + var message = JsonConvert.DeserializeObject>(messageData); + string messageType = message.ContainsKey("type") ? message["type"].ToString() : null; + + switch (messageType) + { + case "switch_camera": + HandleSwitchCamera(message); + break; + + case "get_camera_list": + HandleGetCameraList(service); + break; + + case "toggle_item": + HandleToggleItem(message); + break; + + case "set_item": + HandleSetItem(message); + break; + + case "get_item_list": + HandleGetItemList(service); + break; + + case "test": + // 테스트 메시지 에코 응답 + var response = new + { + type = "echo", + timestamp = DateTime.UtcNow.ToString("o"), + data = new + { + received_message = messageData + } + }; + + string json = JsonConvert.SerializeObject(response); + service.SendMessage(json); + break; + + default: + Debug.Log($"[StreamDeckServerManager] 알 수 없는 메시지 타입: {messageType}"); + break; + } + } + + private void HandleSwitchCamera(Dictionary message) + { + Debug.Log($"[StreamDeckServerManager] 카메라 전환 요청 수신"); + + if (cameraManager == null) + { + Debug.LogError("[StreamDeckServerManager] cameraManager가 null입니다!"); + return; + } + + try + { + if (message.ContainsKey("data")) + { + var dataObject = message["data"]; + + if (dataObject is Newtonsoft.Json.Linq.JObject jObject) + { + if (jObject.ContainsKey("camera_index")) + { + var cameraIndexToken = jObject["camera_index"]; + + if (int.TryParse(cameraIndexToken?.ToString(), out int cameraIndex)) + { + if (cameraIndex >= 0 && cameraIndex < (cameraManager.cameraPresets?.Count ?? 0)) + { + Debug.Log($"[StreamDeckServerManager] 카메라 {cameraIndex}번으로 전환"); + cameraManager.Set(cameraIndex); + + // 카메라 변경 알림 전송 + NotifyCameraChanged(); + } + else + { + Debug.LogError($"[StreamDeckServerManager] 잘못된 카메라 인덱스: {cameraIndex}, 유효 범위: 0-{(cameraManager.cameraPresets?.Count ?? 0) - 1}"); + } + } + else + { + Debug.LogError($"[StreamDeckServerManager] camera_index 파싱 실패: {cameraIndexToken}"); + } + } + else + { + Debug.LogError("[StreamDeckServerManager] data에 'camera_index' 키가 없습니다"); + } + } + else if (dataObject is Dictionary data) + { + if (data.ContainsKey("camera_index")) + { + var cameraIndexObj = data["camera_index"]; + + if (int.TryParse(cameraIndexObj?.ToString(), out int cameraIndex)) + { + if (cameraIndex >= 0 && cameraIndex < (cameraManager.cameraPresets?.Count ?? 0)) + { + Debug.Log($"[StreamDeckServerManager] 카메라 {cameraIndex}번으로 전환"); + cameraManager.Set(cameraIndex); + + // 카메라 변경 알림 전송 + NotifyCameraChanged(); + } + else + { + Debug.LogError($"[StreamDeckServerManager] 잘못된 카메라 인덱스: {cameraIndex}, 유효 범위: 0-{(cameraManager.cameraPresets?.Count ?? 0) - 1}"); + } + } + else + { + Debug.LogError($"[StreamDeckServerManager] camera_index 파싱 실패: {cameraIndexObj}"); + } + } + else + { + Debug.LogError("[StreamDeckServerManager] data에 'camera_index' 키가 없습니다"); + } + } + else + { + Debug.LogError($"[StreamDeckServerManager] data가 예상된 타입이 아닙니다: {dataObject?.GetType().Name}"); + } + } + else + { + Debug.LogError("[StreamDeckServerManager] 메시지에 'data' 키가 없습니다"); + } + } + catch (Exception ex) + { + Debug.LogError($"[StreamDeckServerManager] 카메라 전환 실패: {ex.Message}"); + } + } + + private void HandleGetCameraList(StreamDeckService service) + { + if (cameraManager == null) return; + + var response = new + { + type = "camera_list_response", + timestamp = DateTime.UtcNow.ToString("o"), + version = "1.0", + data = new + { + camera_data = cameraManager.GetCameraListData(), + current_camera = cameraManager.GetCurrentCameraState() + } + }; + + string json = JsonConvert.SerializeObject(response); + service.SendMessage(json); + } + + private void HandleToggleItem(Dictionary message) + { + Debug.Log($"[StreamDeckServerManager] 아이템 토글 요청 수신"); + + if (itemController == null) + { + Debug.LogError("[StreamDeckServerManager] itemController가 null입니다!"); + return; + } + + try + { + if (message.ContainsKey("data")) + { + var dataObject = message["data"]; + + if (dataObject is Newtonsoft.Json.Linq.JObject jObject) + { + if (jObject.ContainsKey("item_index")) + { + var itemIndexToken = jObject["item_index"]; + + if (int.TryParse(itemIndexToken?.ToString(), out int itemIndex)) + { + if (itemIndex >= 0 && itemIndex < (itemController.itemGroups?.Count ?? 0)) + { + Debug.Log($"[StreamDeckServerManager] 아이템 그룹 {itemIndex}번 토글"); + itemController.ToggleGroup(itemIndex); + + // 아이템 변경 알림 전송 + NotifyItemChanged(); + } + else + { + Debug.LogError($"[StreamDeckServerManager] 잘못된 아이템 인덱스: {itemIndex}, 유효 범위: 0-{(itemController.itemGroups?.Count ?? 0) - 1}"); + } + } + else + { + Debug.LogError($"[StreamDeckServerManager] item_index 파싱 실패: {itemIndexToken}"); + } + } + else + { + Debug.LogError("[StreamDeckServerManager] data에 'item_index' 키가 없습니다"); + } + } + else if (dataObject is Dictionary data) + { + if (data.ContainsKey("item_index")) + { + var itemIndexObj = data["item_index"]; + + if (int.TryParse(itemIndexObj?.ToString(), out int itemIndex)) + { + if (itemIndex >= 0 && itemIndex < (itemController.itemGroups?.Count ?? 0)) + { + Debug.Log($"[StreamDeckServerManager] 아이템 그룹 {itemIndex}번 토글"); + itemController.ToggleGroup(itemIndex); + + // 아이템 변경 알림 전송 + NotifyItemChanged(); + } + else + { + Debug.LogError($"[StreamDeckServerManager] 잘못된 아이템 인덱스: {itemIndex}, 유효 범위: 0-{(itemController.itemGroups?.Count ?? 0) - 1}"); + } + } + else + { + Debug.LogError($"[StreamDeckServerManager] item_index 파싱 실패: {itemIndexObj}"); + } + } + else + { + Debug.LogError("[StreamDeckServerManager] data에 'item_index' 키가 없습니다"); + } + } + else + { + Debug.LogError($"[StreamDeckServerManager] data가 예상된 타입이 아닙니다: {dataObject?.GetType().Name}"); + } + } + else + { + Debug.LogError("[StreamDeckServerManager] 메시지에 'data' 키가 없습니다"); + } + } + catch (Exception ex) + { + Debug.LogError($"[StreamDeckServerManager] 아이템 토글 실패: {ex.Message}"); + } + } + + private void HandleSetItem(Dictionary message) + { + Debug.Log($"[StreamDeckServerManager] 아이템 설정 요청 수신"); + + if (itemController == null) + { + Debug.LogError("[StreamDeckServerManager] itemController가 null입니다!"); + return; + } + + try + { + if (message.ContainsKey("data")) + { + var dataObject = message["data"]; + + if (dataObject is Newtonsoft.Json.Linq.JObject jObject) + { + if (jObject.ContainsKey("item_index")) + { + var itemIndexToken = jObject["item_index"]; + + if (int.TryParse(itemIndexToken?.ToString(), out int itemIndex)) + { + if (itemIndex >= 0 && itemIndex < (itemController.itemGroups?.Count ?? 0)) + { + Debug.Log($"[StreamDeckServerManager] 아이템 그룹 {itemIndex}번으로 설정"); + itemController.Set(itemIndex); + + // 아이템 변경 알림 전송 + NotifyItemChanged(); + } + else + { + Debug.LogError($"[StreamDeckServerManager] 잘못된 아이템 인덱스: {itemIndex}, 유효 범위: 0-{(itemController.itemGroups?.Count ?? 0) - 1}"); + } + } + else + { + Debug.LogError($"[StreamDeckServerManager] item_index 파싱 실패: {itemIndexToken}"); + } + } + else + { + Debug.LogError("[StreamDeckServerManager] data에 'item_index' 키가 없습니다"); + } + } + else if (dataObject is Dictionary data) + { + if (data.ContainsKey("item_index")) + { + var itemIndexObj = data["item_index"]; + + if (int.TryParse(itemIndexObj?.ToString(), out int itemIndex)) + { + if (itemIndex >= 0 && itemIndex < (itemController.itemGroups?.Count ?? 0)) + { + Debug.Log($"[StreamDeckServerManager] 아이템 {itemIndex}번으로 설정"); + itemController.Set(itemIndex); + + // 아이템 변경 알림 전송 + NotifyItemChanged(); + } + else + { + Debug.LogError($"[StreamDeckServerManager] 잘못된 아이템 인덱스: {itemIndex}, 유효 범위: 0-{(itemController.itemGroups?.Count ?? 0) - 1}"); + } + } + else + { + Debug.LogError($"[StreamDeckServerManager] item_index 파싱 실패: {itemIndexObj}"); + } + } + else + { + Debug.LogError("[StreamDeckServerManager] data에 'item_index' 키가 없습니다"); + } + } + else + { + Debug.LogError($"[StreamDeckServerManager] data가 예상된 타입이 아닙니다: {dataObject?.GetType().Name}"); + } + } + else + { + Debug.LogError("[StreamDeckServerManager] 메시지에 'data' 키가 없습니다"); + } + } + catch (Exception ex) + { + Debug.LogError($"[StreamDeckServerManager] 아이템 설정 실패: {ex.Message}"); + } + } + + private void HandleGetItemList(StreamDeckService service) + { + if (itemController == null) return; + + var response = new + { + type = "item_list_response", + timestamp = DateTime.UtcNow.ToString("o"), + version = "1.0", + data = new + { + item_data = itemController.GetItemListData(), + current_item = itemController.GetCurrentItemState() + } + }; + + string json = JsonConvert.SerializeObject(response); + service.SendMessage(json); + } +} + +public class StreamDeckService : WebSocketBehavior +{ + private StreamDeckServerManager serverManager; + + protected override void OnOpen() + { + serverManager = StreamDeckServerManager.Instance; + if (serverManager != null) + { + serverManager.OnClientConnected(this); + } + Debug.Log("[StreamDeckService] WebSocket 연결 열림"); + } + + protected override void OnMessage(WebSocketSharp.MessageEventArgs e) + { + Debug.Log($"[StreamDeckService] 원본 메시지 수신: {e.Data}"); + + // 메인 스레드에서 처리하도록 매니저에게 전달 + if (serverManager != null) + { + serverManager.ProcessMessageOnMainThread(e.Data, this); + } + } + + // WebSocketSharp의 Send 메서드 래퍼 + public void SendMessage(string message) + { + try + { + Send(message); + } + catch (Exception ex) + { + Debug.LogError($"[StreamDeckService] 메시지 전송 실패: {ex.Message}"); + } + } + + protected override void OnClose(WebSocketSharp.CloseEventArgs e) + { + if (serverManager != null) + { + serverManager.OnClientDisconnected(this); + } + Debug.Log($"[StreamDeckService] WebSocket 연결 닫힘: {e.Reason}"); + } + + protected override void OnError(WebSocketSharp.ErrorEventArgs e) + { + Debug.LogError($"[StreamDeckService] WebSocket 오류: {e.Message}"); + } +} \ No newline at end of file diff --git a/Assets/Scripts/Streamdeck/StreamDeckServerManager.cs.meta b/Assets/Scripts/Streamdeck/StreamDeckServerManager.cs.meta new file mode 100644 index 00000000..78eda80c --- /dev/null +++ b/Assets/Scripts/Streamdeck/StreamDeckServerManager.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: c76384faff9945849bb1c7db40a7fac8 \ No newline at end of file diff --git a/Assets/Scripts/Streamingle/IController.cs b/Assets/Scripts/Streamingle/IController.cs new file mode 100644 index 00000000..c397823d --- /dev/null +++ b/Assets/Scripts/Streamingle/IController.cs @@ -0,0 +1,12 @@ +using UnityEngine; + +namespace Streamingle +{ + public interface IController + { + string GetControllerId(); + string GetControllerName(); + object GetControllerData(); + void ExecuteAction(string actionId, object parameters); + } +} \ No newline at end of file diff --git a/Assets/Scripts/Streamingle/IController.cs.meta b/Assets/Scripts/Streamingle/IController.cs.meta new file mode 100644 index 00000000..0d62bda1 --- /dev/null +++ b/Assets/Scripts/Streamingle/IController.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: b0242bdf1a5d64746b8ff548f1619b81 \ No newline at end of file diff --git a/Assets/Scripts/Streamingle/StreamingleControl/Controllers/CameraController.cs b/Assets/Scripts/Streamingle/StreamingleControl/Controllers/CameraController.cs index 330a7f90..7fb2c3c5 100644 --- a/Assets/Scripts/Streamingle/StreamingleControl/Controllers/CameraController.cs +++ b/Assets/Scripts/Streamingle/StreamingleControl/Controllers/CameraController.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using UnityRawInput; using System.Linq; using Unity.Cinemachine; +using Streamingle; public class CameraManager : MonoBehaviour, IController { @@ -167,6 +168,11 @@ public class CameraManager : MonoBehaviour, IController private Vector3 initialPosition; private Quaternion initialRotation; private bool isInitialStateSet = false; + + // 스트림덱 연동 + private StreamDeckServerManager streamDeckManager; + + #endregion #region Properties @@ -180,6 +186,13 @@ public class CameraManager : MonoBehaviour, IController InitializeInputHandler(); InitializeRawInput(); InitializeCameraPresets(); + + // StreamDeckServerManager 찾기 + streamDeckManager = FindObjectOfType(); + if (streamDeckManager == null) + { + Debug.LogWarning("[CameraManager] StreamDeckServerManager를 찾을 수 없습니다. 스트림덱 연동이 비활성화됩니다."); + } } private void OnDestroy() @@ -259,6 +272,8 @@ public class CameraManager : MonoBehaviour, IController isInitialStateSet = true; } } + + #endregion #region Input Handling @@ -450,21 +465,52 @@ public class CameraManager : MonoBehaviour, IController #region Camera Management public void Set(int index) { - if (cameraPresets == null || index < 0 || index >= cameraPresets.Count) return; + Debug.Log($"[CameraManager] 카메라 {index}번으로 전환 시작 (총 {cameraPresets?.Count ?? 0}개)"); + + if (cameraPresets == null) + { + Debug.LogError("[CameraManager] cameraPresets가 null입니다!"); + return; + } + + if (index < 0 || index >= cameraPresets.Count) + { + Debug.LogError($"[CameraManager] 잘못된 인덱스: {index}, 유효 범위: 0-{cameraPresets.Count - 1}"); + return; + } var newPreset = cameraPresets[index]; - if (!newPreset.IsValid()) return; + + if (!newPreset.IsValid()) + { + Debug.LogError($"[CameraManager] 프리셋이 유효하지 않습니다 - 인덱스: {index}"); + return; + } var oldPreset = currentPreset; + var newCameraName = newPreset.virtualCamera?.gameObject.name ?? "Unknown"; + currentPreset = newPreset; UpdateCameraPriorities(newPreset.virtualCamera); OnCameraChanged?.Invoke(oldPreset, newPreset); + + // 스트림덱에 카메라 변경 알림 전송 + if (streamDeckManager != null) + { + streamDeckManager.NotifyCameraChanged(); + } + + Debug.Log($"[CameraManager] 카메라 전환 완료: {newCameraName}"); } private void UpdateCameraPriorities(CinemachineCamera newCamera) { - if (newCamera == null) return; + if (newCamera == null) + { + Debug.LogError("[CameraManager] 새 카메라가 null입니다!"); + return; + } if (currentCamera != null) { @@ -506,4 +552,112 @@ public class CameraManager : MonoBehaviour, IController } } #endregion + + #region Camera Data + // 카메라 목록 데이터 반환 (HTTP 요청 시 직접 호출됨) + public CameraListData GetCameraListData() + { + var presetList = cameraPresets.Select((preset, index) => new CameraPresetData + { + index = index, + name = preset?.virtualCamera?.gameObject.name ?? $"Camera {index}", + isActive = currentPreset == preset, + hotkey = preset?.hotkey?.ToString() ?? "설정되지 않음" + }).ToArray(); + + return new CameraListData + { + camera_count = cameraPresets.Count, + presets = presetList, + current_index = currentPreset != null ? cameraPresets.IndexOf(currentPreset) : -1 + }; + } + + // 현재 카메라 상태 데이터 반환 + public CameraStateData GetCurrentCameraState() + { + if (currentPreset == null) return null; + + var currentIndex = cameraPresets.IndexOf(currentPreset); + return new CameraStateData + { + current_index = currentIndex, + camera_name = currentPreset.virtualCamera?.gameObject.name ?? "Unknown", + preset_name = currentPreset.presetName, + total_cameras = cameraPresets.Count + }; + } + + [System.Serializable] + public class CameraPresetData + { + public int index; + public string name; + public bool isActive; + public string hotkey; + } + + [System.Serializable] + public class CameraListData + { + public int camera_count; + public CameraPresetData[] presets; + public int current_index; + } + + [System.Serializable] + public class CameraStateData + { + public int current_index; + public string camera_name; + public string preset_name; + public int total_cameras; + } + #endregion + + #region IController Implementation + public string GetControllerId() + { + return "camera_controller"; + } + + public string GetControllerName() + { + return "카메라 컨트롤러"; + } + + public object GetControllerData() + { + return GetCameraListData(); + } + + public void ExecuteAction(string actionId, object parameters) + { + switch (actionId) + { + case "switch_camera": + if (parameters is int cameraIndex) + { + Set(cameraIndex); + } + else if (parameters is System.Dynamic.ExpandoObject expando) + { + var dict = (IDictionary)expando; + if (dict.ContainsKey("camera_index") && dict["camera_index"] is int index) + { + Set(index); + } + } + break; + + case "get_camera_list": + // 카메라 목록은 GetControllerData()에서 반환됨 + break; + + default: + Debug.LogWarning($"[CameraManager] 알 수 없는 액션: {actionId}"); + break; + } + } + #endregion } \ No newline at end of file diff --git a/Assets/Scripts/Streamingle/StreamingleControl/Controllers/Editor.meta b/Assets/Scripts/Streamingle/StreamingleControl/Controllers/Editor.meta new file mode 100644 index 00000000..c9f32d29 --- /dev/null +++ b/Assets/Scripts/Streamingle/StreamingleControl/Controllers/Editor.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 1391a0125a12fdb46bb61bc345044032 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Streamingle/StreamingleControl/Controllers/Editor/ItemControllerEditor.cs b/Assets/Scripts/Streamingle/StreamingleControl/Controllers/Editor/ItemControllerEditor.cs new file mode 100644 index 00000000..2958f92e --- /dev/null +++ b/Assets/Scripts/Streamingle/StreamingleControl/Controllers/Editor/ItemControllerEditor.cs @@ -0,0 +1,259 @@ +using UnityEngine; +using UnityEditor; + +[CustomEditor(typeof(ItemController))] +public class ItemControllerEditor : Editor +{ + private ItemController itemController; + private bool showGroupSettings = true; + private bool showGroupList = true; + private bool showTestActions = true; + + private void OnEnable() + { + itemController = (ItemController)target; + } + + public override void OnInspectorGUI() + { + DrawDefaultInspector(); + + EditorGUILayout.Space(); + EditorGUILayout.LabelField("Item Controller Tools", EditorStyles.boldLabel); + + // 현재 상태 표시 + DrawCurrentState(); + + // 그룹 설정 섹션 + showGroupSettings = EditorGUILayout.Foldout(showGroupSettings, "Group Management"); + if (showGroupSettings) + { + EditorGUILayout.BeginVertical("box"); + + EditorGUILayout.LabelField("Quick Group Actions", EditorStyles.boldLabel); + + EditorGUILayout.BeginHorizontal(); + if (GUILayout.Button("Add Selected Objects as New Group")) + { + AddSelectedObjectsAsGroup(); + } + + if (GUILayout.Button("Refresh Item Groups")) + { + itemController.InitializeItemGroups(); + } + EditorGUILayout.EndHorizontal(); + + if (GUILayout.Button("Clear All Groups")) + { + if (EditorUtility.DisplayDialog("Clear Groups", "모든 그룹을 삭제하시겠습니까?", "Yes", "No")) + { + itemController.itemGroups.Clear(); + Debug.Log("[ItemControllerEditor] 모든 그룹이 삭제되었습니다."); + } + } + + EditorGUILayout.EndVertical(); + } + + // 그룹 리스트 섹션 + showGroupList = EditorGUILayout.Foldout(showGroupList, "Current Group List"); + if (showGroupList) + { + EditorGUILayout.BeginVertical("box"); + + if (itemController.itemGroups != null && itemController.itemGroups.Count > 0) + { + for (int i = 0; i < itemController.itemGroups.Count; i++) + { + var group = itemController.itemGroups[i]; + if (group != null) + { + DrawGroupItem(i, group); + } + } + } + else + { + EditorGUILayout.LabelField("No groups registered", EditorStyles.centeredGreyMiniLabel); + } + + EditorGUILayout.EndVertical(); + } + + // 테스트 액션 섹션 + showTestActions = EditorGUILayout.Foldout(showTestActions, "Test Actions"); + if (showTestActions) + { + EditorGUILayout.BeginVertical("box"); + + EditorGUILayout.BeginHorizontal(); + if (GUILayout.Button("Activate All Groups")) + { + itemController.ActivateAllGroups(); + } + + if (GUILayout.Button("Deactivate All Groups")) + { + itemController.DeactivateAllGroups(); + } + EditorGUILayout.EndHorizontal(); + + if (itemController.itemGroups.Count > 0) + { + EditorGUILayout.LabelField("Set Active Group:", EditorStyles.miniLabel); + EditorGUILayout.BeginHorizontal(); + for (int i = 0; i < Mathf.Min(3, itemController.itemGroups.Count); i++) + { + if (GUILayout.Button($"Group {i}")) + { + itemController.Set(i); + } + } + EditorGUILayout.EndHorizontal(); + } + + EditorGUILayout.EndVertical(); + } + } + + private void DrawCurrentState() + { + EditorGUILayout.BeginVertical("box"); + EditorGUILayout.LabelField("Current State", EditorStyles.boldLabel); + + EditorGUILayout.LabelField($"Total Groups: {itemController.itemGroups?.Count ?? 0}"); + EditorGUILayout.LabelField($"Current Group: {(itemController.CurrentGroup?.groupName ?? "None")}"); + EditorGUILayout.LabelField($"Current Index: {itemController.CurrentIndex}"); + + if (itemController.CurrentGroup != null) + { + EditorGUILayout.LabelField($"Active: {itemController.CurrentGroup.IsActive()}", + itemController.CurrentGroup.IsActive() ? EditorStyles.boldLabel : EditorStyles.miniLabel); + } + + EditorGUILayout.EndVertical(); + EditorGUILayout.Space(); + } + + private void DrawGroupItem(int index, ItemController.ItemGroup group) + { + EditorGUILayout.BeginVertical("box"); + + // 그룹 헤더 + EditorGUILayout.BeginHorizontal(); + EditorGUILayout.LabelField($"{index + 1}. {group.groupName}", EditorStyles.boldLabel); + + // 활성화 상태 토글 + bool isActive = group.IsActive(); + bool newActive = EditorGUILayout.Toggle(isActive, GUILayout.Width(20)); + if (newActive != isActive) + { + group.SetActive(newActive); + } + + // 토글 버튼 + if (GUILayout.Button("Toggle", GUILayout.Width(50))) + { + itemController.ToggleGroup(index); + } + + // 핫키 레코딩 버튼 + if (GUILayout.Button(group.isRecording ? "Stop" : "Record", GUILayout.Width(60))) + { + if (group.isRecording) + { + itemController.StopRecordingHotkey(index); + } + else + { + itemController.StartRecordingHotkey(index); + } + } + + // 삭제 버튼 + if (GUILayout.Button("X", GUILayout.Width(20))) + { + if (EditorUtility.DisplayDialog("Delete Group", $"그룹 '{group.groupName}'을 삭제하시겠습니까?", "Yes", "No")) + { + itemController.RemoveGroup(index); + return; + } + } + + EditorGUILayout.EndHorizontal(); + + // 핫키 표시 + if (group.hotkeys != null && group.hotkeys.Count > 0) + { + EditorGUILayout.LabelField($"Hotkey: {string.Join(" + ", group.hotkeys)}", EditorStyles.miniLabel); + } + else + { + EditorGUILayout.LabelField("Hotkey: 설정되지 않음", EditorStyles.miniLabel); + } + + // 아이템 오브젝트들 + EditorGUILayout.LabelField("Items:", EditorStyles.miniLabel); + if (group.itemObjects != null) + { + for (int j = 0; j < group.itemObjects.Length; j++) + { + EditorGUILayout.BeginHorizontal(); + EditorGUILayout.LabelField($" {j + 1}.", GUILayout.Width(30)); + + GameObject obj = group.itemObjects[j]; + GameObject newObj = (GameObject)EditorGUILayout.ObjectField(obj, typeof(GameObject), true); + if (newObj != obj) + { + group.itemObjects[j] = newObj; + } + + // 아이템 삭제 버튼 + if (GUILayout.Button("X", GUILayout.Width(20))) + { + var newArray = new GameObject[group.itemObjects.Length - 1]; + int newIndex = 0; + for (int k = 0; k < group.itemObjects.Length; k++) + { + if (k != j) + { + newArray[newIndex] = group.itemObjects[k]; + newIndex++; + } + } + group.itemObjects = newArray; + break; + } + + EditorGUILayout.EndHorizontal(); + } + + // 아이템 추가 버튼 + if (GUILayout.Button("Add Item")) + { + var newArray = new GameObject[group.itemObjects.Length + 1]; + group.itemObjects.CopyTo(newArray, 0); + group.itemObjects = newArray; + } + } + + EditorGUILayout.EndVertical(); + EditorGUILayout.Space(); + } + + private void AddSelectedObjectsAsGroup() + { + GameObject[] selectedObjects = Selection.gameObjects; + if (selectedObjects.Length > 0) + { + string groupName = $"Group {itemController.itemGroups.Count + 1}"; + itemController.AddGroup(groupName, selectedObjects); + Debug.Log($"[ItemControllerEditor] {selectedObjects.Length}개의 선택된 오브젝트를 새 그룹 '{groupName}'으로 추가했습니다."); + } + else + { + Debug.LogWarning("[ItemControllerEditor] 선택된 오브젝트가 없습니다."); + } + } +} \ No newline at end of file diff --git a/Assets/Scripts/Streamingle/StreamingleControl/Controllers/Editor/ItemControllerEditor.cs.meta b/Assets/Scripts/Streamingle/StreamingleControl/Controllers/Editor/ItemControllerEditor.cs.meta new file mode 100644 index 00000000..0939703b --- /dev/null +++ b/Assets/Scripts/Streamingle/StreamingleControl/Controllers/Editor/ItemControllerEditor.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 31447d4896db6c9458ce66dae88feec8 \ No newline at end of file diff --git a/Assets/Scripts/Streamingle/StreamingleControl/Controllers/ItemController.cs b/Assets/Scripts/Streamingle/StreamingleControl/Controllers/ItemController.cs new file mode 100644 index 00000000..1d54d88f --- /dev/null +++ b/Assets/Scripts/Streamingle/StreamingleControl/Controllers/ItemController.cs @@ -0,0 +1,562 @@ +using UnityEngine; +using System.Collections.Generic; +using System.Linq; +using Streamingle; +using UnityRawInput; + +public class ItemController : MonoBehaviour, IController +{ + #region Classes + [System.Serializable] + public class ItemGroup + { + [Header("Group Settings")] + public string groupName = "New Item Group"; + public GameObject[] itemObjects = new GameObject[0]; + + [Header("Hotkey Settings")] + public List hotkeys = new List(); + [System.NonSerialized] public bool isRecording = false; + [System.NonSerialized] private List unityKeys = new List(); + [System.NonSerialized] private float recordStartTime; + [System.NonSerialized] private const float MAX_RECORD_TIME = 2f; + + public ItemGroup(string name) + { + groupName = name; + hotkeys = new List(); + } + + public void StartRecording() + { + isRecording = true; + recordStartTime = Time.time; + hotkeys.Clear(); + } + + public void StopRecording() + { + isRecording = false; + InitializeUnityKeys(); + } + + public void UpdateRecording() + { + if (!isRecording) return; + + if (Time.time - recordStartTime > MAX_RECORD_TIME) + { + StopRecording(); + return; + } + + foreach (KeyCode keyCode in System.Enum.GetValues(typeof(KeyCode))) + { + if (Input.GetKeyDown(keyCode) && KeyMapping.TryGetRawKey(keyCode, out RawKey rawKey)) + { + if (!hotkeys.Contains(rawKey)) + { + hotkeys.Add(rawKey); + } + } + } + + bool allKeysReleased = hotkeys.Any() && hotkeys.All(key => !Input.GetKey(KeyMapping.TryGetKeyCode(key, out KeyCode keyCode) ? keyCode : KeyCode.None)); + + if (allKeysReleased) + { + StopRecording(); + } + } + + public void InitializeUnityKeys() + { + unityKeys.Clear(); + + if (hotkeys == null || !hotkeys.Any()) return; + + foreach (var hotkey in hotkeys) + { + if (KeyMapping.TryGetKeyCode(hotkey, out KeyCode keyCode) && keyCode != KeyCode.None) + { + unityKeys.Add(keyCode); + } + } + } + + public bool IsTriggered() + { + if (isRecording) return false; + + if (hotkeys == null || !hotkeys.Any()) return false; + + bool allHotkeysPressed = hotkeys.All(key => RawInput.IsKeyDown(key)); + if (allHotkeysPressed) return true; + + if (unityKeys.Any()) + { + return unityKeys.All(key => Input.GetKey(key)); + } + + return false; + } + + public void SetActive(bool active) + { + if (itemObjects == null) return; + + foreach (var obj in itemObjects) + { + if (obj != null) + { + obj.SetActive(active); + } + } + } + + public bool IsActive() + { + if (itemObjects == null || itemObjects.Length == 0) return false; + + // 모든 오브젝트가 활성화되어 있으면 true + return itemObjects.All(obj => obj != null && obj.activeSelf); + } + + public override string ToString() => + hotkeys?.Any() == true ? $"{groupName} ({string.Join(" + ", hotkeys)})" : $"{groupName} (설정되지 않음)"; + } + + private static class KeyMapping + { + private static readonly Dictionary _mapping; + + static KeyMapping() + { + _mapping = new Dictionary(RawKeySetup.KeyMapping); + } + + public static bool TryGetRawKey(KeyCode keyCode, out RawKey rawKey) + { + return _mapping.TryGetValue(keyCode, out rawKey); + } + + public static bool TryGetKeyCode(RawKey rawKey, out KeyCode keyCode) + { + var pair = _mapping.FirstOrDefault(x => x.Value == rawKey); + keyCode = pair.Key; + return keyCode != KeyCode.None; + } + + public static bool IsValidRawKey(RawKey key) + { + return _mapping.ContainsValue(key); + } + } + #endregion + + #region Events + public delegate void ItemGroupChangedEventHandler(ItemGroup oldGroup, ItemGroup newGroup); + public event ItemGroupChangedEventHandler OnItemGroupChanged; + #endregion + + #region Fields + [SerializeField] public List itemGroups = new List(); + + [Header("Item Control Settings")] + [SerializeField] private bool autoFindGroups = true; + [SerializeField] private string groupTag = "ItemGroup"; + + private ItemGroup currentGroup; + private StreamDeckServerManager streamDeckManager; + #endregion + + #region Properties + public ItemGroup CurrentGroup => currentGroup; + public int CurrentIndex => itemGroups.IndexOf(currentGroup); + #endregion + + #region Unity Messages + private void Awake() + { + InitializeItemGroups(); + InitializeRawInput(); + + // StreamDeckServerManager 찾기 + streamDeckManager = FindObjectOfType(); + if (streamDeckManager == null) + { + Debug.LogWarning("[ItemController] StreamDeckServerManager를 찾을 수 없습니다. 스트림덱 연동이 비활성화됩니다."); + } + } + + private void OnDestroy() + { + if (RawInput.IsRunning) + { + RawInput.OnKeyDown -= HandleRawKeyDown; + RawInput.Stop(); + } + } + + private void Update() + { + UpdateHotkeyRecording(); + HandleHotkeys(); + } + #endregion + + #region Initialization + public void InitializeItemGroups() + { + if (itemGroups == null) + { + itemGroups = new List(); + } + + if (autoFindGroups && itemGroups.Count == 0) + { + // "ItemGroup" 태그가 붙은 모든 오브젝트를 찾아서 그룹으로 등록 + var groupObjects = GameObject.FindGameObjectsWithTag(groupTag); + foreach (var groupObj in groupObjects) + { + var group = new ItemGroup(groupObj.name); + group.itemObjects = new GameObject[] { groupObj }; + itemGroups.Add(group); + } + Debug.Log($"[ItemController] {itemGroups.Count}개의 아이템 그룹을 자동으로 찾았습니다."); + } + + // 유효하지 않은 그룹 제거 + itemGroups.RemoveAll(group => group == null || group.itemObjects == null || group.itemObjects.Length == 0); + + Debug.Log($"[ItemController] 총 {itemGroups.Count}개의 아이템 그룹이 등록되었습니다."); + } + + private void InitializeRawInput() + { + if (!RawInput.IsRunning) + { + RawInput.Start(); + RawInput.WorkInBackground = true; + } + RawInput.OnKeyDown += HandleRawKeyDown; + } + #endregion + + #region Hotkey Management + private void UpdateHotkeyRecording() + { + foreach (var group in itemGroups) + { + if (group?.isRecording == true) + { + group.UpdateRecording(); + } + } + } + + private void HandleRawKeyDown(RawKey key) + { + // 핫키 레코딩 중이면 무시 + if (itemGroups.Any(g => g?.isRecording == true)) return; + } + + private void HandleHotkeys() + { + foreach (var group in itemGroups) + { + if (group?.IsTriggered() == true) + { + ToggleGroup(group); + Debug.Log($"[ItemController] 핫키로 그룹 토글: {group.groupName}"); + } + } + } + #endregion + + #region Public Methods + public void Set(int index) + { + if (index < 0 || index >= itemGroups.Count) + { + Debug.LogWarning($"[ItemController] 잘못된 인덱스: {index}, 유효 범위: 0-{itemGroups.Count - 1}"); + return; + } + + var oldGroup = currentGroup; + currentGroup = itemGroups[index]; + + // 모든 그룹 비활성화 후 현재 그룹만 활성화 + foreach (var group in itemGroups) + { + group.SetActive(false); + } + currentGroup.SetActive(true); + + // 이벤트 발생 + OnItemGroupChanged?.Invoke(oldGroup, currentGroup); + + // StreamDeck에 알림 + NotifyItemChanged(); + + Debug.Log($"[ItemController] 그룹 변경: {currentGroup.groupName} (인덱스: {index})"); + } + + public void ToggleGroup(int index) + { + if (index < 0 || index >= itemGroups.Count) + { + Debug.LogWarning($"[ItemController] 잘못된 인덱스: {index}, 유효 범위: 0-{itemGroups.Count - 1}"); + return; + } + + var group = itemGroups[index]; + bool newState = !group.IsActive(); + group.SetActive(newState); + + // StreamDeck에 알림 + NotifyItemChanged(); + + Debug.Log($"[ItemController] 그룹 토글: {group.groupName} -> {(newState ? "활성" : "비활성")}"); + } + + public void ToggleGroup(ItemGroup group) + { + if (group == null) return; + + int index = itemGroups.IndexOf(group); + if (index >= 0) + { + ToggleGroup(index); + } + } + + public void ToggleCurrentGroup() + { + if (currentGroup != null) + { + ToggleGroup(CurrentIndex); + } + } + + public void ActivateAllGroups() + { + foreach (var group in itemGroups) + { + group.SetActive(true); + } + NotifyItemChanged(); + Debug.Log("[ItemController] 모든 그룹 활성화"); + } + + public void DeactivateAllGroups() + { + foreach (var group in itemGroups) + { + group.SetActive(false); + } + NotifyItemChanged(); + Debug.Log("[ItemController] 모든 그룹 비활성화"); + } + + public void AddGroup(string groupName, GameObject[] objects = null) + { + var newGroup = new ItemGroup(groupName); + if (objects != null) + { + newGroup.itemObjects = objects; + } + + itemGroups.Add(newGroup); + NotifyItemChanged(); + Debug.Log($"[ItemController] 그룹 추가: {groupName}"); + } + + public void RemoveGroup(int index) + { + if (index < 0 || index >= itemGroups.Count) return; + + var removedGroup = itemGroups[index]; + itemGroups.RemoveAt(index); + + // 현재 그룹이 제거된 경우 첫 번째 그룹으로 변경 + if (removedGroup == currentGroup) + { + currentGroup = itemGroups.Count > 0 ? itemGroups[0] : null; + } + + NotifyItemChanged(); + Debug.Log($"[ItemController] 그룹 제거: {removedGroup.groupName}"); + } + + public void StartRecordingHotkey(int groupIndex) + { + if (groupIndex < 0 || groupIndex >= itemGroups.Count) return; + + var group = itemGroups[groupIndex]; + group.StartRecording(); + Debug.Log($"[ItemController] 핫키 레코딩 시작: {group.groupName}"); + } + + public void StopRecordingHotkey(int groupIndex) + { + if (groupIndex < 0 || groupIndex >= itemGroups.Count) return; + + var group = itemGroups[groupIndex]; + group.StopRecording(); + Debug.Log($"[ItemController] 핫키 레코딩 완료: {group.groupName} -> {group}"); + } + #endregion + + #region StreamDeck Integration + private void NotifyItemChanged() + { + if (streamDeckManager != null) + { + var updateMessage = new + { + type = "item_changed", + timestamp = System.DateTime.UtcNow.ToString("o"), + version = "1.0", + data = new + { + item_data = GetItemListData(), + current_item = GetCurrentItemState() + } + }; + + string json = JsonUtility.ToJson(updateMessage); + streamDeckManager.BroadcastMessage(json); + Debug.Log("[ItemController] 아이템 변경 알림 전송됨"); + } + } + + public ItemListData GetItemListData() + { + return new ItemListData + { + item_count = itemGroups.Count, + items = itemGroups.Select((g, i) => new ItemPresetData + { + index = i, + name = g.groupName, + isActive = g.IsActive(), + hotkey = g.ToString() + }).ToArray(), + current_index = CurrentIndex + }; + } + + public ItemStateData GetCurrentItemState() + { + if (currentGroup == null) return null; + + return new ItemStateData + { + current_index = CurrentIndex, + item_name = currentGroup.groupName, + isActive = currentGroup.IsActive(), + total_items = itemGroups.Count + }; + } + + public string GetItemListJson() + { + return JsonUtility.ToJson(GetItemListData()); + } + + public string GetItemStateJson() + { + return JsonUtility.ToJson(GetCurrentItemState()); + } + #endregion + + #region Data Classes + [System.Serializable] + public class ItemPresetData + { + public int index; + public string name; + public bool isActive; + public string hotkey; + } + + [System.Serializable] + public class ItemListData + { + public int item_count; + public ItemPresetData[] items; + public int current_index; + } + + [System.Serializable] + public class ItemStateData + { + public int current_index; + public string item_name; + public bool isActive; + public int total_items; + } + #endregion + + #region IController Implementation + public string GetControllerId() + { + return "item_controller"; + } + + public string GetControllerName() + { + return "Item Controller"; + } + + public object GetControllerData() + { + return GetItemListData(); + } + + public void ExecuteAction(string actionId, object parameters) + { + switch (actionId) + { + case "toggle_item": + { + if (parameters is int toggleIndex) + { + ToggleGroup(toggleIndex); + } + } + break; + case "set_item": + { + if (parameters is int setIndex) + { + Set(setIndex); + } + } + break; + case "activate_all": + ActivateAllGroups(); + break; + case "deactivate_all": + DeactivateAllGroups(); + break; + case "start_recording_hotkey": + if (parameters is int recordIndex) + { + StartRecordingHotkey(recordIndex); + } + break; + case "stop_recording_hotkey": + if (parameters is int stopIndex) + { + StopRecordingHotkey(stopIndex); + } + break; + default: + Debug.LogWarning($"[ItemController] 알 수 없는 액션: {actionId}"); + break; + } + } + #endregion +} \ No newline at end of file diff --git a/Assets/Scripts/Streamingle/StreamingleControl/Controllers/ItemController.cs.meta b/Assets/Scripts/Streamingle/StreamingleControl/Controllers/ItemController.cs.meta new file mode 100644 index 00000000..f4e8b133 --- /dev/null +++ b/Assets/Scripts/Streamingle/StreamingleControl/Controllers/ItemController.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 06c44b24cbacd394ca4e2c8c94a5efed \ No newline at end of file diff --git a/Assets/Scripts/Vrmtool.meta b/Assets/Scripts/Vrmtool.meta new file mode 100644 index 00000000..79c1bcb3 --- /dev/null +++ b/Assets/Scripts/Vrmtool.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 75da79b9f8bfe684fb4234e9fa0eaabc +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Vrmtool/ColliderAutoSetup.cs b/Assets/Scripts/Vrmtool/ColliderAutoSetup.cs new file mode 100644 index 00000000..254ce410 --- /dev/null +++ b/Assets/Scripts/Vrmtool/ColliderAutoSetup.cs @@ -0,0 +1,69 @@ +using UnityEngine; +using UnityEditor; +using VRM; +using System.Collections.Generic; + +public class ColliderAutoSetup : EditorWindow +{ + private GameObject springBoneObject; + private GameObject avatarRoot; + + //[MenuItem("Tools/Spring Bone Collider Setup")] + static void Init() + { + ColliderAutoSetup window = (ColliderAutoSetup)EditorWindow.GetWindow(typeof(ColliderAutoSetup)); + window.Show(); + } + + void OnGUI() + { + // ... 기존 코드 생략 ... + + EditorGUILayout.LabelField("스프링본 콜라이더 자동 설정", EditorStyles.boldLabel); + EditorGUILayout.Space(); + + springBoneObject = (GameObject)EditorGUILayout.ObjectField( + "스프링본이 있는 오브젝트", springBoneObject, typeof(GameObject), true); + + avatarRoot = (GameObject)EditorGUILayout.ObjectField( + "아바타 루트 오브젝트", avatarRoot, typeof(GameObject), true); + + EditorGUILayout.Space(); + + if (GUILayout.Button("콜라이더 설정")) + { + SetupColliders(); + } + } + + void SetupColliders() + { + if (springBoneObject == null || avatarRoot == null) + { + EditorUtility.DisplayDialog("오류", "스프링본 오브젝트와 아바타 루트를 모두 지정해주세요.", "확인"); + return; + } + + // 스프링본 컴포넌트들 가져오기 + VRMSpringBone[] springBones = springBoneObject.GetComponents(); + if (springBones.Length == 0) + { + EditorUtility.DisplayDialog("오류", "선택한 오브젝트에 VRMSpringBone 컴포넌트가 없습니다.", "확인"); + return; + } + + // 아바타 루트 아래의 모든 콜라이더 그룹 찾기 + VRMSpringBoneColliderGroup[] colliderGroups = avatarRoot.GetComponentsInChildren(true); + + // 각 스프링본 컴포넌트에 콜라이더 그룹 설정 + Undo.RecordObjects(springBones, "Setup Spring Bone Colliders"); + + foreach (var springBone in springBones) + { + springBone.ColliderGroups = colliderGroups; + } + + EditorUtility.SetDirty(springBoneObject); + Debug.Log($"콜라이더 설정 완료: {colliderGroups.Length}개의 콜라이더 그룹이 {springBones.Length}개의 스프링본에 설정되었습니다."); + } +} diff --git a/Assets/Scripts/Vrmtool/ColliderAutoSetup.cs.meta b/Assets/Scripts/Vrmtool/ColliderAutoSetup.cs.meta new file mode 100644 index 00000000..addd77f8 --- /dev/null +++ b/Assets/Scripts/Vrmtool/ColliderAutoSetup.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 1f34dc4244c9bdb438212c800b8c9673 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Vrmtool/ColliderMoveEditorWindow.cs b/Assets/Scripts/Vrmtool/ColliderMoveEditorWindow.cs new file mode 100644 index 00000000..a9ebf04d --- /dev/null +++ b/Assets/Scripts/Vrmtool/ColliderMoveEditorWindow.cs @@ -0,0 +1,38 @@ +// ColliderMoveEditorWindow.cs +using UnityEditor; +using UnityEngine; + +namespace Efitor +{ + public class ColliderMoveEditorWindow : EditorWindow + { + private GameObject originalPrefab; + private GameObject copyPrefab; + + //[MenuItem("Tools/Collider Move Tool")] + public static void ShowWindow() + { + GetWindow(typeof(ColliderMoveEditorWindow), false, "Collider Move Tool"); + } + + private void OnGUI() + { + GUILayout.Label("Collider Move Tool", EditorStyles.boldLabel); + + originalPrefab = (GameObject)EditorGUILayout.ObjectField("Original Prefab", originalPrefab, typeof(GameObject), true); + copyPrefab = (GameObject)EditorGUILayout.ObjectField("Copy Prefab", copyPrefab, typeof(GameObject), true); + + if (GUILayout.Button("Move Colliders")) + { + if (originalPrefab != null && copyPrefab != null) + { + Efitor.ColliderMoveTool.MoveColliders(originalPrefab, copyPrefab); + } + else + { + Debug.LogError("Original Prefab and Copy Prefab must be assigned!"); + } + } + } + } +} diff --git a/Assets/Scripts/Vrmtool/ColliderMoveEditorWindow.cs.meta b/Assets/Scripts/Vrmtool/ColliderMoveEditorWindow.cs.meta new file mode 100644 index 00000000..63ec36ce --- /dev/null +++ b/Assets/Scripts/Vrmtool/ColliderMoveEditorWindow.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8e6ea8b4e185d8442a5daea2d3e42c9d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Vrmtool/ColliderMoveTool.cs b/Assets/Scripts/Vrmtool/ColliderMoveTool.cs new file mode 100644 index 00000000..25030de5 --- /dev/null +++ b/Assets/Scripts/Vrmtool/ColliderMoveTool.cs @@ -0,0 +1,47 @@ +// ColliderMoveTool.cs +using System.Linq; +using UnityEngine; +using VRM; + +namespace Efitor +{ + public class ColliderMoveTool : MonoBehaviour + { + public static void MoveColliders(GameObject originalPrefab, GameObject copyPrefab) + { + VRMSpringBoneColliderGroup[] originalColliderGroups = originalPrefab.GetComponentsInChildren(); + Transform[] copyBones = copyPrefab.GetComponentsInChildren(); + + foreach (VRMSpringBoneColliderGroup originalColliderGroup in originalColliderGroups) + { + Transform correspondingCopyBone = FindCorrespondingBone(originalColliderGroup.transform.parent, copyBones); + + if (correspondingCopyBone != null) + { + // 원본 콜라이더 게임오브젝트를 복제 + GameObject copiedCollider = GameObject.Instantiate(originalColliderGroup.gameObject); + copiedCollider.name = originalColliderGroup.gameObject.name; + + // 복제된 콜라이더를 대상 본의 자식으로 설정 + copiedCollider.transform.SetParent(correspondingCopyBone); + + // 원본의 로컬 Transform 값을 그대로 적용 + copiedCollider.transform.localPosition = originalColliderGroup.transform.localPosition; + copiedCollider.transform.localRotation = originalColliderGroup.transform.localRotation; + copiedCollider.transform.localScale = originalColliderGroup.transform.localScale; + + Debug.Log($"Collider Group copied for: {correspondingCopyBone.name}"); + } + else + { + Debug.LogWarning("No corresponding bone found for: " + originalColliderGroup.transform.parent.name); + } + } + } + + private static Transform FindCorrespondingBone(Transform originalBone, Transform[] copyBones) + { + return System.Array.Find(copyBones, bone => bone.name == originalBone.name); + } + } +} diff --git a/Assets/Scripts/Vrmtool/ColliderMoveTool.cs.meta b/Assets/Scripts/Vrmtool/ColliderMoveTool.cs.meta new file mode 100644 index 00000000..54a6e95e --- /dev/null +++ b/Assets/Scripts/Vrmtool/ColliderMoveTool.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 0ac468aa4f507d5418cd9ea306883f95 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Vrmtool/RemoveInvalidVRMSpringBones.cs b/Assets/Scripts/Vrmtool/RemoveInvalidVRMSpringBones.cs new file mode 100644 index 00000000..5b12ef36 --- /dev/null +++ b/Assets/Scripts/Vrmtool/RemoveInvalidVRMSpringBones.cs @@ -0,0 +1,50 @@ +using UnityEngine; +using UnityEditor; +using System.Collections.Generic; +using System.Linq; +using VRM; + +public class RemoveInvalidVRMSpringBones : EditorWindow +{ + [SerializeField] private GameObject destinationPrefab; + + //[MenuItem("Tools/Remove Invalid VRM Spring Bones")] + private static void ShowWindow() + { + GetWindow("Remove Invalid VRM Spring Bones").Show(); + } + + private void OnGUI() + { + EditorGUILayout.LabelField("Remove Invalid VRM Spring Bones", EditorStyles.boldLabel); + + destinationPrefab = (GameObject)EditorGUILayout.ObjectField("Destination Prefab", destinationPrefab, typeof(GameObject), true); + + if (GUILayout.Button("Remove Invalid Bones")) + { + if (destinationPrefab == null) + { + EditorUtility.DisplayDialog("Error", "Destination Prefab must be set.", "OK"); + return; + } + + RemoveInvalidBones(); + } + } + + private void RemoveInvalidBones() + { + var springBones = destinationPrefab.GetComponentsInChildren(true).ToList(); + + foreach (var bone in springBones) + { + // Check if any of the root bones or collider groups are null (which indicates a missing reference) + if (bone.RootBones.Any(rb => rb == null) || bone.ColliderGroups.Any(cg => cg == null)) + { + DestroyImmediate(bone); + } + } + + Debug.Log("Invalid VRM Spring Bones have been removed."); + } +} \ No newline at end of file diff --git a/Assets/Scripts/Vrmtool/RemoveInvalidVRMSpringBones.cs.meta b/Assets/Scripts/Vrmtool/RemoveInvalidVRMSpringBones.cs.meta new file mode 100644 index 00000000..3045418e --- /dev/null +++ b/Assets/Scripts/Vrmtool/RemoveInvalidVRMSpringBones.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 4e6320d80b1084f4f9265ae8f4fbf42d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Vrmtool/VRMSpringBoneMoveTool.cs b/Assets/Scripts/Vrmtool/VRMSpringBoneMoveTool.cs new file mode 100644 index 00000000..5840cff8 --- /dev/null +++ b/Assets/Scripts/Vrmtool/VRMSpringBoneMoveTool.cs @@ -0,0 +1,234 @@ +using System.Collections.Generic; +using UnityEngine; +using UnityEditor; +using System.Linq; +using VRM; // VRMライブラリの名前空間を確認してください。これが正しいかどうかによっては変更が必要です。 + +public class VRMSpringBoneMoveTool : EditorWindow +{ + GameObject sourcePrefab; + GameObject destinationPrefab; + + //[MenuItem("Tools/Transfer VRM Spring Bones")] + static void ShowWindow() + { + GetWindow("VRM Spring Bone Move Tool"); + } + + void OnGUI() + { + GUILayout.Label("Transfer VRM Spring Bone Components", EditorStyles.boldLabel); + + sourcePrefab = EditorGUILayout.ObjectField("Source Prefab", sourcePrefab, typeof(GameObject), true) as GameObject; + destinationPrefab = EditorGUILayout.ObjectField("Destination Prefab", destinationPrefab, typeof(GameObject), true) as GameObject; + + if (GUILayout.Button("Transfer")) + { + if (sourcePrefab == null || destinationPrefab == null) + { + EditorUtility.DisplayDialog("Error", "Source and Destination Prefabs must be set.", "OK"); + return; + } + + TransferVRMSpringBone(); + } + } + + void TransferVRMSpringBone() + { + if (!EditorUtility.DisplayDialog("확인", "대상 프리팹의 모든 VRMSpringBone이 삭제됩니다. 계속하시겠습니까?", "예", "아니오")) + { + return; + } + + // Secondary 오브젝트 찾기 또는 생성 + Transform destSecondary = destinationPrefab.transform.Find("Secondary"); + if (destSecondary == null) + { + GameObject secondaryObj = new GameObject("Secondary"); + secondaryObj.transform.SetParent(destinationPrefab.transform, false); + destSecondary = secondaryObj.transform; + Debug.Log("Secondary 오브젝트가 생성되었습니다."); + } + + // 기존 VRMSpringBone 컴포넌트 제거 + var existingBones = destinationPrefab.GetComponentsInChildren(true); + foreach (var bone in existingBones) + { + DestroyImmediate(bone); + } + + int successCount = 0; + int failCount = 0; + + // 소스의 VRMSpringBone 컴포넌트 복사 + var springBones = sourcePrefab.GetComponentsInChildren(true); + foreach (var springBone in springBones) + { + try + { + // Secondary 오브젝트에 새로운 VRMSpringBone 컴포넌트 추가 + VRMSpringBone newSpringBone = destSecondary.gameObject.AddComponent(); + if (CopyVRMSpringBoneComponents(springBone, newSpringBone)) + { + successCount++; + } + else + { + DestroyImmediate(newSpringBone); + failCount++; + } + } + catch (System.Exception e) + { + Debug.LogError($"스프링본 복사 중 오류 발생: {e.Message}"); + failCount++; + } + } + + if (successCount > 0) + { + EditorUtility.SetDirty(destinationPrefab); + AssetDatabase.SaveAssets(); + } + + EditorUtility.DisplayDialog("작업 완료", + $"성공: {successCount}개\n실패: {failCount}개", "확인"); + } + + bool CopyVRMSpringBoneComponents(VRMSpringBone original, VRMSpringBone copy) + { + try + { + // 기본 속성 복사 + copy.m_comment = original.m_comment; + copy.m_stiffnessForce = original.m_stiffnessForce; + copy.m_gravityPower = original.m_gravityPower; + copy.m_gravityDir = original.m_gravityDir; + copy.m_dragForce = original.m_dragForce; + copy.m_hitRadius = original.m_hitRadius; + copy.m_updateType = original.m_updateType; + + // Center 본 찾기 및 설정 + if (original.m_center != null) + { + copy.m_center = FindCorrespondingTransform(original.m_center, destinationPrefab.transform); + if (copy.m_center == null) + { + Debug.LogError($"Center 본을 찾을 수 없습니다: {original.m_center.name}"); + return false; + } + } + + // Root 본들 찾기 및 설정 + List newRootBones = new List(); + foreach (var rootBone in original.RootBones) + { + if (rootBone == null) continue; + + var correspondingBone = FindCorrespondingTransform(rootBone, destinationPrefab.transform); + if (correspondingBone != null) + { + newRootBones.Add(correspondingBone); + } + else + { + Debug.LogError($"Root 본을 찾을 수 없습니다: {rootBone.name}"); + return false; + } + } + + if (newRootBones.Count == 0) + { + Debug.LogError("Root 본이 하나도 설정되지 않았습니다."); + return false; + } + copy.RootBones = newRootBones; + + // Collider Groups 찾기 및 설정 + if (original.ColliderGroups != null && original.ColliderGroups.Length > 0) + { + List newColliderGroups = new List(); + foreach (var colliderGroup in original.ColliderGroups) + { + if (colliderGroup == null) continue; + + var correspondingCollider = FindCorrespondingColliderGroup(colliderGroup, destinationPrefab.transform); + if (correspondingCollider != null) + { + newColliderGroups.Add(correspondingCollider); + } + else + { + Debug.LogWarning($"Collider Group을 찾을 수 없습니다: {colliderGroup.name}"); + } + } + copy.ColliderGroups = newColliderGroups.ToArray(); + } + + return true; + } + catch (System.Exception e) + { + Debug.LogError($"컴포넌트 복사 중 오류 발생: {e.Message}"); + return false; + } + } + + Transform FindCorrespondingTransform(Transform original, Transform searchRoot) + { + if (original == null) return null; + + // 먼저 전체 경로로 시도 + var path = GetTransformPath(original, sourcePrefab.transform); + var result = searchRoot.Find(path); + + // 경로로 찾지 못한 경우 이름으로 재시도 + if (result == null) + { + // 대상 프리팹에서 같은 이름을 가진 모든 Transform을 찾음 + var allTransforms = searchRoot.GetComponentsInChildren(true); + result = allTransforms.FirstOrDefault(t => t.name == original.name); + + if (result != null) + { + Debug.Log($"이름으로 매칭된 본 찾음: {original.name}"); + } + } + + return result; + } + + string GetTransformPath(Transform current, Transform root) + { + if (current == root) + { + return ""; // ルートに到達したら終了 + } + var path = current.name; + while (current.parent != null && current.parent != root) + { + current = current.parent; + path = current.name + "/" + path; // 親の名前をパスに追加 + } + return path; + } + + VRMSpringBoneColliderGroup FindCorrespondingColliderGroup(VRMSpringBoneColliderGroup original, Transform searchRoot) + { + if (original == null) + { + return null; + } + var path = GetTransformPath(original.transform, sourcePrefab.transform); + var correspondingTransform = searchRoot.Find(path); + return correspondingTransform != null ? correspondingTransform.GetComponent() : null; + } + + void SetTransform(Transform newTransform, Transform originalTransform) + { + newTransform.localPosition = originalTransform.localPosition; + newTransform.localRotation = originalTransform.localRotation; + newTransform.localScale = originalTransform.localScale; + } +} diff --git a/Assets/Scripts/Vrmtool/VRMSpringBoneMoveTool.cs.meta b/Assets/Scripts/Vrmtool/VRMSpringBoneMoveTool.cs.meta new file mode 100644 index 00000000..8919e58b --- /dev/null +++ b/Assets/Scripts/Vrmtool/VRMSpringBoneMoveTool.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b07b2fb6eb1dbc341a814a74a11b9ee6 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Vrmtool/VRMToolsWindow.cs b/Assets/Scripts/Vrmtool/VRMToolsWindow.cs new file mode 100644 index 00000000..8d776ed3 --- /dev/null +++ b/Assets/Scripts/Vrmtool/VRMToolsWindow.cs @@ -0,0 +1,626 @@ +using UnityEngine; +using UnityEditor; +using VRM; +using System.Linq; +using System.Collections.Generic; + +public class VRMUtilityWindow : EditorWindow +{ + private GameObject originalPrefab; + private GameObject destinationPrefab; + private GameObject springBoneObject; + private GameObject avatarRoot; + private Vector2 scrollPosition; + private int selectedTab = 0; + private readonly string[] tabNames = { "스프링본 이동", "콜라이더 이동", "콜라이더 설정", "잘못된 본 제거" }; + + [MenuItem("VRM0/VRM 유틸리티")] + static void ShowWindow() + { + GetWindow("VRM 유틸리티").Show(); + } + + void OnGUI() + { + // 탭 스타일 정의 + var tabStyle = new GUIStyle(EditorStyles.toolbarButton); + tabStyle.fixedHeight = 30; + tabStyle.fontSize = 12; + tabStyle.fontStyle = FontStyle.Bold; + tabStyle.alignment = TextAnchor.MiddleCenter; + + EditorGUILayout.Space(10); + + // 윈도우 너비에 맞춰 탭 버튼 크기 조정 + float windowWidth = position.width; + float minTabWidth = 100; // 최소 탭 너비 + float padding = 20; // 좌우 여백 + float availableWidth = windowWidth - (padding * 2); // 사용 가능한 전체 너비 + + // 탭 버튼들을 수평으로 배치 + using (new EditorGUILayout.HorizontalScope()) + { + GUILayout.FlexibleSpace(); + + // 사용 가능한 너비가 최소 필요 너비보다 작으면 버튼을 세로로 배치 + if (availableWidth < (minTabWidth * tabNames.Length)) + { + EditorGUILayout.EndHorizontal(); + for (int i = 0; i < tabNames.Length; i++) + { + using (new EditorGUILayout.HorizontalScope()) + { + GUILayout.FlexibleSpace(); + if (GUILayout.Toggle(selectedTab == i, tabNames[i], tabStyle, GUILayout.Width(availableWidth))) + { + selectedTab = i; + } + GUILayout.FlexibleSpace(); + } + } + } + else + { + // 충분한 너비가 있으면 가로로 배치 + float tabWidth = availableWidth / tabNames.Length; + for (int i = 0; i < tabNames.Length; i++) + { + if (GUILayout.Toggle(selectedTab == i, tabNames[i], tabStyle, GUILayout.Width(tabWidth))) + { + selectedTab = i; + } + } + GUILayout.FlexibleSpace(); + } + } + + EditorGUILayout.Space(10); + + scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition); + + // 공통 필드들을 탭에 따라 표시 + switch (selectedTab) + { + case 0: DrawSpringBoneTransferTab(); break; + case 1: DrawColliderMoveTab(); break; + case 2: DrawColliderSetupTab(); break; + case 3: DrawInvalidBonesTab(); break; + } + + EditorGUILayout.EndScrollView(); + } + + void DrawSpringBoneTransferTab() + { + DrawHeader("스프링본 이동"); + + EditorGUILayout.BeginVertical(EditorStyles.helpBox); + DrawSourceDestinationFields(); + EditorGUILayout.Space(10); + + using (new EditorGUILayout.HorizontalScope()) + { + GUILayout.FlexibleSpace(); + if (GUILayout.Button("스프링본 이동", GUILayout.Height(30), GUILayout.Width(200))) + { + if (ValidateSourceDestination()) + { + TransferVRMSpringBone(); + } + } + GUILayout.FlexibleSpace(); + } + + EditorGUILayout.Space(5); + DrawHelpBox("원본 아바타의 스프링본을 대상 아바타로 이동합니다.\n기존 스프링본은 선택적으로 제거할 수 있습니다."); + EditorGUILayout.EndVertical(); + } + + void DrawColliderMoveTab() + { + DrawHeader("콜라이더 이동"); + + EditorGUILayout.BeginVertical(EditorStyles.helpBox); + DrawSourceDestinationFields(); + EditorGUILayout.Space(10); + + using (new EditorGUILayout.HorizontalScope()) + { + GUILayout.FlexibleSpace(); + if (GUILayout.Button("콜라이더 이동", GUILayout.Height(30), GUILayout.Width(200))) + { + if (ValidateSourceDestination()) + { + MoveColliders(); + } + } + GUILayout.FlexibleSpace(); + } + + EditorGUILayout.Space(5); + DrawHelpBox("원본 아바타의 콜라이더를 대상 아바타의 동일한 본으로 이동합니다."); + EditorGUILayout.EndVertical(); + } + + void DrawColliderSetupTab() + { + DrawHeader("콜라이더 설정"); + + EditorGUILayout.BeginVertical(EditorStyles.helpBox); + EditorGUILayout.Space(5); + + springBoneObject = (GameObject)EditorGUILayout.ObjectField( + "스프링본 오브젝트", springBoneObject, typeof(GameObject), true); + EditorGUILayout.Space(2); + avatarRoot = (GameObject)EditorGUILayout.ObjectField( + "아바타 루트", avatarRoot, typeof(GameObject), true); + + EditorGUILayout.Space(10); + + using (new EditorGUILayout.HorizontalScope()) + { + GUILayout.FlexibleSpace(); + if (GUILayout.Button("콜라이더 설정", GUILayout.Height(30), GUILayout.Width(200))) + { + if (ValidateColliderSetup()) + { + SetupColliders(); + } + } + GUILayout.FlexibleSpace(); + } + + EditorGUILayout.Space(5); + DrawHelpBox("선택한 오브젝트의 모든 스프링본에 콜라이더를 자동으로 설정합니다."); + EditorGUILayout.EndVertical(); + } + + void DrawInvalidBonesTab() + { + DrawHeader("잘못된 본 제거"); + + EditorGUILayout.BeginVertical(EditorStyles.helpBox); + EditorGUILayout.Space(5); + + destinationPrefab = (GameObject)EditorGUILayout.ObjectField( + "대상 아바타", destinationPrefab, typeof(GameObject), true); + + EditorGUILayout.Space(10); + + using (new EditorGUILayout.HorizontalScope()) + { + GUILayout.FlexibleSpace(); + if (GUILayout.Button("잘못된 본 제거", GUILayout.Height(30), GUILayout.Width(200))) + { + if (ValidateDestination()) + { + RemoveInvalidBones(); + } + } + GUILayout.FlexibleSpace(); + } + + EditorGUILayout.Space(5); + DrawHelpBox("참조가 깨진 스프링본을 찾아서 제거합니다."); + EditorGUILayout.EndVertical(); + } + + void DrawSourceDestinationFields() + { + EditorGUILayout.Space(5); + originalPrefab = (GameObject)EditorGUILayout.ObjectField( + "원본 아바타", originalPrefab, typeof(GameObject), true); + EditorGUILayout.Space(2); + destinationPrefab = (GameObject)EditorGUILayout.ObjectField( + "대상 아바타", destinationPrefab, typeof(GameObject), true); + } + + void DrawHeader(string title) + { + EditorGUILayout.Space(5); + using (new EditorGUILayout.HorizontalScope()) + { + GUILayout.FlexibleSpace(); + EditorGUILayout.LabelField(title, EditorStyles.boldLabel, GUILayout.Width(200)); + GUILayout.FlexibleSpace(); + } + EditorGUILayout.Space(5); + } + + void DrawHelpBox(string message) + { + EditorGUILayout.HelpBox(message, MessageType.Info); + EditorGUILayout.Space(5); + } + + // 검증 메서드들 + bool ValidateSourceDestination() + { + if (originalPrefab == null || destinationPrefab == null) + { + EditorUtility.DisplayDialog("오류", "원본과 대상 아바타를 모두 지정해주세요.", "확인"); + return false; + } + return true; + } + + bool ValidateColliderSetup() + { + if (springBoneObject == null || avatarRoot == null) + { + EditorUtility.DisplayDialog("오류", "스프링본 오브젝트와 아바타 루트를 모두 지정해주세요.", "확인"); + return false; + } + return true; + } + + bool ValidateDestination() + { + if (destinationPrefab == null) + { + EditorUtility.DisplayDialog("오류", "대상 아바타를 지정해주세요.", "확인"); + return false; + } + return true; + } + + // 콜라이더 설정 + void SetupColliders() + { + if (springBoneObject == null || avatarRoot == null) + { + EditorUtility.DisplayDialog("오류", "스프링본 오브젝트와 아바타 루트를 모두 지정해주세요.", "확인"); + return; + } + + // 스프링본 컴포넌트들 가져오기 + VRMSpringBone[] springBones = springBoneObject.GetComponents(); + if (springBones.Length == 0) + { + EditorUtility.DisplayDialog("오류", "선택한 오브젝트에 VRMSpringBone 컴포넌트가 없습니다.", "확인"); + return; + } + + // 아바타 루트 아래의 모든 콜라이더 그룹 찾기 + VRMSpringBoneColliderGroup[] colliderGroups = avatarRoot.GetComponentsInChildren(true); + + // 각 스프링본 컴포넌트에 콜라이더 그룹 설정 + Undo.RecordObjects(springBones, "Setup Spring Bone Colliders"); + + foreach (var springBone in springBones) + { + springBone.ColliderGroups = colliderGroups; + } + + EditorUtility.SetDirty(springBoneObject); + Debug.Log($"콜라이더 설정 완료: {colliderGroups.Length}개의 콜라이더 그룹이 {springBones.Length}개의 스프링본에 설정되었습니다."); + } + + // 콜라이더 이동 + void MoveColliders() + { + VRMSpringBoneColliderGroup[] originalColliderGroups = originalPrefab.GetComponentsInChildren(); + int successCount = 0; + int failCount = 0; + int createdCount = 0; + + foreach (VRMSpringBoneColliderGroup originalColliderGroup in originalColliderGroups) + { + try + { + Transform correspondingTransform = FindCorrespondingTransform(originalColliderGroup.transform, destinationPrefab.transform); + + if (correspondingTransform == null) + { + // 원본 본의 부모 경로를 찾음 + Transform originalParent = originalColliderGroup.transform.parent; + Transform targetParent = destinationPrefab.transform; + + // 부모가 있는 경우, 대상 프리팹에서 대응하는 부모를 찾음 + if (originalParent != originalPrefab.transform) + { + Transform correspondingParent = FindCorrespondingTransform(originalParent, destinationPrefab.transform); + if (correspondingParent != null) + { + targetParent = correspondingParent; + } + else + { + Debug.LogWarning($"부모 본을 찾을 수 없습니다: {originalParent.name}. 루트에 생성합니다."); + } + } + + // 새로운 본 생성 + GameObject newBone = new GameObject(originalColliderGroup.transform.name); + newBone.transform.SetParent(targetParent, false); // false로 설정하여 로컬 트랜스폼 유지 + newBone.transform.localPosition = originalColliderGroup.transform.localPosition; + newBone.transform.localRotation = originalColliderGroup.transform.localRotation; + newBone.transform.localScale = originalColliderGroup.transform.localScale; + + correspondingTransform = newBone.transform; + createdCount++; + Debug.Log($"새로운 본 생성됨: {newBone.name} (부모: {targetParent.name})"); + } + + // 기존 콜라이더 그룹이 있다면 제거 + var existingCollider = correspondingTransform.GetComponent(); + if (existingCollider != null) + { + DestroyImmediate(existingCollider); + } + + // 새로운 콜라이더 그룹 추가 + var newColliderGroup = correspondingTransform.gameObject.AddComponent(); + + // 원본과 대상 본의 월드 회전 차이 계산 + Quaternion rotationDiff = Quaternion.Inverse(originalColliderGroup.transform.rotation) * correspondingTransform.rotation; + + // 콜라이더 설정 복사 및 회전 보정 + newColliderGroup.Colliders = new VRMSpringBoneColliderGroup.SphereCollider[originalColliderGroup.Colliders.Length]; + for (int i = 0; i < originalColliderGroup.Colliders.Length; i++) + { + var originalCollider = originalColliderGroup.Colliders[i]; + + // 원본 오프셋을 월드 공간으로 변환 + Vector3 worldOffset = originalColliderGroup.transform.TransformDirection(originalCollider.Offset); + // 대상 본의 로컬 공간으로 변환 + Vector3 newOffset = correspondingTransform.InverseTransformDirection(worldOffset); + + newColliderGroup.Colliders[i] = new VRMSpringBoneColliderGroup.SphereCollider + { + Offset = newOffset, + Radius = originalCollider.Radius + }; + } + + successCount++; + Debug.Log($"콜라이더 그룹 이동 완료: {correspondingTransform.name}"); + } + catch (System.Exception e) + { + failCount++; + Debug.LogError($"콜라이더 이동 중 오류 발생: {e.Message}"); + } + } + + if (successCount > 0 || createdCount > 0) + { + EditorUtility.SetDirty(destinationPrefab); + AssetDatabase.SaveAssets(); + } + + EditorUtility.DisplayDialog("작업 완료", + $"성공: {successCount}개\n실패: {failCount}개\n새로 생성된 본: {createdCount}개", "확인"); + } + + private Transform FindTransformByName(string name, Transform searchRoot) + { + // 대상 프리팹에서 같은 이름을 가진 모든 Transform을 찾음 + var allTransforms = searchRoot.GetComponentsInChildren(true); + return allTransforms.FirstOrDefault(t => t.name == name); + } + + private void CopyColliderGroupSettings(VRMSpringBoneColliderGroup source, VRMSpringBoneColliderGroup destination) + { + destination.Colliders = new VRMSpringBoneColliderGroup.SphereCollider[source.Colliders.Length]; + + for (int i = 0; i < source.Colliders.Length; i++) + { + destination.Colliders[i] = new VRMSpringBoneColliderGroup.SphereCollider + { + Offset = source.Colliders[i].Offset, + Radius = source.Colliders[i].Radius + }; + } + } + + // 스프링본 이동 + void TransferVRMSpringBone() + { + if (!EditorUtility.DisplayDialog("확인", "대상 프리팹의 모든 VRMSpringBone이 삭제됩니다. 계속하시겠습니까?", "예", "아니오")) + { + return; + } + + // Secondary 오브젝트 찾기 또는 생성 + Transform destSecondary = destinationPrefab.transform.Find("Secondary"); + if (destSecondary == null) + { + GameObject secondaryObj = new GameObject("Secondary"); + secondaryObj.transform.SetParent(destinationPrefab.transform, false); + destSecondary = secondaryObj.transform; + Debug.Log("Secondary 오브젝트가 생성되었습니다."); + } + + // 기존 VRMSpringBone 컴포넌트 제거 + var existingBones = destinationPrefab.GetComponentsInChildren(true); + foreach (var bone in existingBones) + { + DestroyImmediate(bone); + } + + // 소스의 VRMSpringBone 컴포넌트 복사 + var springBones = originalPrefab.GetComponentsInChildren(true); + int successCount = 0; + int failCount = 0; + + foreach (var springBone in springBones) + { + try + { + VRMSpringBone newSpringBone = destSecondary.gameObject.AddComponent(); + if (CopyVRMSpringBoneComponents(springBone, newSpringBone)) + { + successCount++; + } + else + { + DestroyImmediate(newSpringBone); + failCount++; + } + } + catch (System.Exception e) + { + Debug.LogError($"스프링본 복사 중 오류 발생: {e.Message}"); + failCount++; + } + } + + EditorUtility.SetDirty(destinationPrefab); + AssetDatabase.SaveAssets(); + EditorUtility.DisplayDialog("작업 완료", $"성공: {successCount}개\n실패: {failCount}개", "확인"); + } + + // 잘못된 본 제거 + void RemoveInvalidBones() + { + var springBones = destinationPrefab.GetComponentsInChildren(true); + int removedCount = 0; + + foreach (var bone in springBones) + { + if (bone.RootBones.Any(rb => rb == null) || bone.ColliderGroups.Any(cg => cg == null)) + { + DestroyImmediate(bone); + removedCount++; + } + } + + if (removedCount > 0) + { + EditorUtility.SetDirty(destinationPrefab); + AssetDatabase.SaveAssets(); + } + + EditorUtility.DisplayDialog("작업 완료", $"제거된 잘못된 스프링본: {removedCount}개", "확인"); + } + + // 유틸리티 메서드들 + private Transform FindCorrespondingBone(Transform originalBone, Transform[] copyBones) + { + return System.Array.Find(copyBones, bone => bone.name == originalBone.name); + } + + private bool CopyVRMSpringBoneComponents(VRMSpringBone original, VRMSpringBone copy) + { + try + { + copy.m_comment = original.m_comment; + copy.m_stiffnessForce = original.m_stiffnessForce; + copy.m_gravityPower = original.m_gravityPower; + copy.m_gravityDir = original.m_gravityDir; + copy.m_dragForce = original.m_dragForce; + copy.m_hitRadius = original.m_hitRadius; + copy.m_updateType = original.m_updateType; + + // Center 본 설정 + if (original.m_center != null) + { + copy.m_center = FindCorrespondingTransform(original.m_center, destinationPrefab.transform); + if (copy.m_center == null) + { + Debug.LogWarning($"Center 본을 찾을 수 없습니다: {original.m_center.name} - Center 본 없이 계속 진행합니다."); + } + } + + // Root 본들 설정 + List newRootBones = new List(); + foreach (var rootBone in original.RootBones) + { + if (rootBone == null) continue; + + var correspondingBone = FindCorrespondingTransform(rootBone, destinationPrefab.transform); + if (correspondingBone != null) + { + newRootBones.Add(correspondingBone); + } + else + { + Debug.LogWarning($"Root 본을 찾을 수 없습니다: {rootBone.name} - 이 본은 건너뜁니다."); + } + } + + // 찾은 본이 하나도 없으면 false 반환 + if (newRootBones.Count == 0) + { + Debug.LogError("유효한 Root 본을 하나도 찾을 수 없습니다."); + return false; + } + + copy.RootBones = newRootBones; + + // Collider Groups 설정 + if (original.ColliderGroups != null && original.ColliderGroups.Length > 0) + { + List newColliderGroups = new List(); + foreach (var colliderGroup in original.ColliderGroups) + { + if (colliderGroup == null) continue; + + var correspondingCollider = FindCorrespondingColliderGroup(colliderGroup); + if (correspondingCollider != null) + { + newColliderGroups.Add(correspondingCollider); + } + else + { + Debug.LogWarning($"콜라이더 그룹을 찾을 수 없습니다: {colliderGroup.name} - 이 콜라이더 그룹은 건너뜁니다."); + } + } + copy.ColliderGroups = newColliderGroups.ToArray(); + } + + return true; + } + catch (System.Exception e) + { + Debug.LogError($"컴포넌트 복사 중 오류 발생: {e.Message}"); + return false; + } + } + + private Transform FindCorrespondingTransform(Transform original, Transform searchRoot) + { + if (original == null) return null; + + string path = GetTransformPath(original); + Transform result = searchRoot.Find(path); + + if (result == null) + { + var allTransforms = searchRoot.GetComponentsInChildren(true); + result = allTransforms.FirstOrDefault(t => t.name == original.name); + } + + return result; + } + + private string GetTransformPath(Transform transform) + { + string path = transform.name; + Transform parent = transform.parent; + + while (parent != null && parent != originalPrefab.transform) + { + path = parent.name + "/" + path; + parent = parent.parent; + } + + return path; + } + + private VRMSpringBoneColliderGroup FindCorrespondingColliderGroup(VRMSpringBoneColliderGroup original) + { + if (original == null) return null; + + Transform correspondingTransform = FindCorrespondingTransform(original.transform, destinationPrefab.transform); + return correspondingTransform?.GetComponent(); + } + + // 최소 윈도우 크기 설정 + void OnEnable() + { + // 최소 윈도우 크기 설정 + minSize = new Vector2(250, 400); + } +} \ No newline at end of file diff --git a/Assets/Scripts/Vrmtool/VRMToolsWindow.cs.meta b/Assets/Scripts/Vrmtool/VRMToolsWindow.cs.meta new file mode 100644 index 00000000..5197e025 --- /dev/null +++ b/Assets/Scripts/Vrmtool/VRMToolsWindow.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 93cb3a1f85e5af94e82e3e2cb4b3a4c3 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/test.meta b/Assets/test.meta new file mode 100644 index 00000000..ee9567be --- /dev/null +++ b/Assets/test.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 8382ddc88ecd80645a30b123757143bd +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/test/GlobalVolumeProfile.asset b/Assets/test/GlobalVolumeProfile.asset new file mode 100644 index 00000000..35fb6a3e --- /dev/null +++ b/Assets/test/GlobalVolumeProfile.asset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1cc2dbaf06bb8b671bb71a1ce3e0ebee43cf90575e84fb49cc2d63903112351b +size 2385 diff --git a/Assets/test/GlobalVolumeProfile.asset.meta b/Assets/test/GlobalVolumeProfile.asset.meta new file mode 100644 index 00000000..8d5ea9b8 --- /dev/null +++ b/Assets/test/GlobalVolumeProfile.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: a78b1e50136052a43ba7b8a2261f8ba1 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/manifest.json b/Packages/manifest.json index a0e24e5b..5ef21835 100644 --- a/Packages/manifest.json +++ b/Packages/manifest.json @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0484a82face991d71f810636ba025c7171520043dc23f8c5d2fdcc129cffedf5 -size 2328 +oid sha256:1f8ad010ef2286143802246a021bf57f8156bf50abbcbfca90c56a162c30c309 +size 2376 diff --git a/Packages/packages-lock.json b/Packages/packages-lock.json index 55e115d3..e4fcfc89 100644 --- a/Packages/packages-lock.json +++ b/Packages/packages-lock.json @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5d052547d34d2f4f8f332de4c7da68e4613eb7ba7a1493754098e1e3c241029a -size 13867 +oid sha256:3abb83121663ba86e4f95c4eb8a816a77b0f5ae1406a13a2e153e8f6a1bd68be +size 14055 diff --git a/StreamDock-Plugin-SDK/Icon/unity.png b/StreamDock-Plugin-SDK/Icon/unity.png deleted file mode 100644 index 08a405a1..00000000 --- a/StreamDock-Plugin-SDK/Icon/unity.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:c6a70ce470c92e666d43f4491ca55dafccc7811e0e1672351eb23fcc6d910b8a -size 17016 diff --git a/StreamDock-Plugin-SDK/README_한국어.md b/StreamDock-Plugin-SDK/README_한국어.md deleted file mode 100644 index f46c7cdf..00000000 --- a/StreamDock-Plugin-SDK/README_한국어.md +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a0bf7c4f0672a785e5309d8fa4a10b6768c90e49c869f9174481b434c204fcbf -size 4757 diff --git a/StreamDock-Plugin-SDK/SDNodeJsSDK/com.mirabox.streamdock.demo.sdPlugin/package.json b/StreamDock-Plugin-SDK/SDNodeJsSDK/com.mirabox.streamdock.demo.sdPlugin/package.json deleted file mode 100644 index 239cbfed..00000000 --- a/StreamDock-Plugin-SDK/SDNodeJsSDK/com.mirabox.streamdock.demo.sdPlugin/package.json +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ae2850b8dcf1dcc9c3053cce726dd5031d4a499021b7a9e6ad40c558ed3c9925 -size 653 diff --git a/StreamDock-Plugin-SDK/SDNodeJsSDK/unity-communication-plugin/manifest.json b/StreamDock-Plugin-SDK/SDNodeJsSDK/unity-communication-plugin/manifest.json deleted file mode 100644 index 6344acf8..00000000 --- a/StreamDock-Plugin-SDK/SDNodeJsSDK/unity-communication-plugin/manifest.json +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:8777f19cf23aeae91083ccfae4e0f0d5bd6647dfe3ca2989c1e1d82ef4cbcf51 -size 1078 diff --git a/StreamDock-Plugin-SDK/SDNodeJsSDK/unity-communication-plugin/package.json b/StreamDock-Plugin-SDK/SDNodeJsSDK/unity-communication-plugin/package.json deleted file mode 100644 index 2a1a7fbb..00000000 --- a/StreamDock-Plugin-SDK/SDNodeJsSDK/unity-communication-plugin/package.json +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:0cfa62142e327e04cd21d255ba127678105570d4ffb4dae98c02e8ffa6dd05f5 -size 826 diff --git a/StreamDock-Plugin-SDK/SDNodeJsSDK/unity-communication-plugin/plugin/app.exe b/StreamDock-Plugin-SDK/SDNodeJsSDK/unity-communication-plugin/plugin/app.exe deleted file mode 100644 index eec58f08..00000000 Binary files a/StreamDock-Plugin-SDK/SDNodeJsSDK/unity-communication-plugin/plugin/app.exe and /dev/null differ diff --git a/StreamDock-Plugin-SDK/SDNodeJsSDK/unity-communication-plugin/plugin/index.js b/StreamDock-Plugin-SDK/SDNodeJsSDK/unity-communication-plugin/plugin/index.js deleted file mode 100644 index 6f9bf42f..00000000 --- a/StreamDock-Plugin-SDK/SDNodeJsSDK/unity-communication-plugin/plugin/index.js +++ /dev/null @@ -1,172 +0,0 @@ -const { Plugins, Actions, log } = require('./utils/plugin'); -const WebSocket = require('ws'); - -// 플러그인 인스턴스 -const plugin = new Plugins(); - -// Unity WebSocket 서버 -const WS_PORT = 15732; -let wss = null; -let unityConnections = new Set(); -let isUnityConnected = false; - -// Unity 서버 시작 -function startUnityServer() { - try { - wss = new WebSocket.Server({ port: WS_PORT }); - log.info(`Unity WebSocket 서버 시작: 포트 ${WS_PORT}`); - - // 서버 연결 시 기존 연결 확인 - setTimeout(() => { - if (wss.clients.size > 0) { - log.info(`기존 Unity 연결 발견: ${wss.clients.size}개`); - isUnityConnected = true; - wss.clients.forEach(ws => { - unityConnections.add(ws); - }); - updateConnectionStatus(); - } - }, 1000); - - wss.on('connection', (ws, req) => { - const clientIP = req.socket.remoteAddress; - log.info(`Unity 클라이언트 연결됨 - IP: ${clientIP}`); - unityConnections.add(ws); - isUnityConnected = true; - updateConnectionStatus(); - - // 연결 확인 메시지 전송 - ws.send(JSON.stringify({ - type: 'connection_confirmed', - data: { - message: '플러그인에 성공적으로 연결되었습니다', - timestamp: Date.now() - } - })); - - ws.on('message', (message) => { - log.info(`Unity에서 메시지 수신: ${message}`); - }); - - ws.on('close', () => { - log.info(`Unity 클라이언트 연결 해제 - IP: ${clientIP}`); - unityConnections.delete(ws); - - if (unityConnections.size === 0) { - isUnityConnected = false; - updateConnectionStatus(); - } - }); - - ws.on('error', (error) => { - log.error(`Unity 연결 오류: ${error.message}`); - }); - }); - - wss.on('error', (error) => { - log.error(`WebSocket 서버 오류: ${error.message}`); - }); - - } catch (error) { - log.error('WebSocket 서버 오류: ' + error.message); - } -} - -// Unity로 메시지 전송 -function sendToUnity(message) { - if (unityConnections.size === 0) { - log.warn('Unity 연결 없음'); - return false; - } - - const messageStr = JSON.stringify(message); - unityConnections.forEach(ws => { - if (ws.readyState === WebSocket.OPEN) { - ws.send(messageStr); - log.info('Unity로 전송: ' + messageStr); - } - }); - return true; -} - -// 연결 상태 업데이트 -function updateConnectionStatus() { - const status = isUnityConnected ? '연결됨' : '연결안됨'; - log.info(`Unity 연결 상태: ${status}`); - - // 모든 버튼의 제목 업데이트 - if (plugin.action1 && plugin.action1.currentContext) { - plugin.setTitle(plugin.action1.currentContext, isUnityConnected ? "Unity 버튼" : "연결안됨"); - } -} - -// 버튼 액션 -plugin.action1 = new Actions({ - currentContext: null, - - _willAppear({ context }) { - this.currentContext = context; - const title = isUnityConnected ? "Unity 버튼" : "연결안됨"; - plugin.setTitle(context, title); - log.info('Unity 버튼 로드됨'); - }, - - _keyUp({ context }) { - log.info('버튼 클릭됨!'); - - if (!isUnityConnected) { - plugin.setTitle(context, "연결없음"); - setTimeout(() => { - const title = isUnityConnected ? "Unity 버튼" : "연결안됨"; - plugin.setTitle(context, title); - }, 1000); - return; - } - - const success = sendToUnity({ - type: 'button_clicked', - data: { - message: 'StreamDock 버튼이 클릭되었습니다!', - timestamp: Date.now() - } - }); - - if (success) { - plugin.setTitle(context, "전송됨!"); - setTimeout(() => plugin.setTitle(context, "Unity 버튼"), 1000); - } else { - plugin.setTitle(context, "실패"); - setTimeout(() => plugin.setTitle(context, "Unity 버튼"), 1000); - } - }, - - _willDisappear({ context }) { - this.currentContext = null; - } -}); - -// 서버 시작 -startUnityServer(); - -// 주기적으로 연결 상태 확인 (5초마다) -setInterval(() => { - const actualConnections = wss ? wss.clients.size : 0; - const wasConnected = isUnityConnected; - - if (actualConnections > 0 && !isUnityConnected) { - log.info(`연결 상태 동기화: Unity 연결됨 (${actualConnections}개)`); - isUnityConnected = true; - updateConnectionStatus(); - } else if (actualConnections === 0 && isUnityConnected) { - log.info('연결 상태 동기화: Unity 연결 해제'); - isUnityConnected = false; - unityConnections.clear(); - updateConnectionStatus(); - } - - if (wasConnected !== isUnityConnected) { - log.info(`연결 상태 변경: ${wasConnected ? '연결됨' : '연결안됨'} → ${isUnityConnected ? '연결됨' : '연결안됨'}`); - } -}, 5000); - -log.info('Unity 통신 플러그인 시작됨'); \ No newline at end of file diff --git a/StreamDock-Plugin-SDK/SDNodeJsSDK/unity-communication-plugin/plugin/package.json b/StreamDock-Plugin-SDK/SDNodeJsSDK/unity-communication-plugin/plugin/package.json deleted file mode 100644 index 3cd6b89f..00000000 --- a/StreamDock-Plugin-SDK/SDNodeJsSDK/unity-communication-plugin/plugin/package.json +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:aa8df7d0d76e6c5f57e359387e760eed78baedb0af1def63c4b62ef17b3bb9a5 -size 344 diff --git a/StreamDock-Plugin-SDK/SDNodeJsSDK/unity-communication-plugin/propertyInspector/action1/index.html b/StreamDock-Plugin-SDK/SDNodeJsSDK/unity-communication-plugin/propertyInspector/action1/index.html deleted file mode 100644 index 0aed492a..00000000 --- a/StreamDock-Plugin-SDK/SDNodeJsSDK/unity-communication-plugin/propertyInspector/action1/index.html +++ /dev/null @@ -1,118 +0,0 @@ - - - - - - Unity 이벤트 전송 설정 - - - - -
-

Unity 이벤트 전송

- -
- - -
- -
- - -
- -
- 사용법:
- • Unity에서 StreamDockCommunicator 스크립트를 사용하세요
- • WebSocket 연결: ws://localhost:15732
- • 이 버튼을 누르면 Unity로 이벤트가 전송됩니다 -
-
- - - - - \ No newline at end of file diff --git a/StreamDock-Plugin-SDK/SDNodeJsSDK/unity-communication-plugin/propertyInspector/action1/index.js b/StreamDock-Plugin-SDK/SDNodeJsSDK/unity-communication-plugin/propertyInspector/action1/index.js deleted file mode 100644 index ff58dd01..00000000 --- a/StreamDock-Plugin-SDK/SDNodeJsSDK/unity-communication-plugin/propertyInspector/action1/index.js +++ /dev/null @@ -1,14 +0,0 @@ -/// -/// - -// $local 是否国际化 -// $back 是否自行决定回显时机 -// $dom 获取文档元素 - 不是动态的都写在这里面 -const $local = false, $back = false, $dom = { - main: $('.sdpi-wrapper') -}; - -const $propEvent = { - didReceiveSettings(data) { }, - sendToPropertyInspector(data) { } -}; \ No newline at end of file diff --git a/StreamDock-Plugin-SDK/SDNodeJsSDK/unity-communication-plugin/propertyInspector/action2/index.html b/StreamDock-Plugin-SDK/SDNodeJsSDK/unity-communication-plugin/propertyInspector/action2/index.html deleted file mode 100644 index d31b1cf0..00000000 --- a/StreamDock-Plugin-SDK/SDNodeJsSDK/unity-communication-plugin/propertyInspector/action2/index.html +++ /dev/null @@ -1,152 +0,0 @@ - - - - - Unity 상태 수신 설정 - - - -
-

Unity 상태 수신

- -
- - -
- -
- - -
- -
- 연결 상태:
- 연결 안됨

- - 기능:
- • Unity에서 게임 상태를 실시간으로 받아옵니다
- • 버튼을 누르면 최신 상태를 요청합니다
- • WebSocket: ws://localhost:15732 -
-
- - - - \ No newline at end of file diff --git a/StreamDock-Plugin-SDK/SDNodeJsSDK/unity-communication-plugin/readme.md b/StreamDock-Plugin-SDK/SDNodeJsSDK/unity-communication-plugin/readme.md deleted file mode 100644 index b5c84fdc..00000000 --- a/StreamDock-Plugin-SDK/SDNodeJsSDK/unity-communication-plugin/readme.md +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:e862724d53e761e916a709132de6f5f823abb0e8149f9115745ab26250d82cc7 -size 6416 diff --git a/StreamDock-Plugin-SDK/SDNodeJsSDK/unity-communication-plugin/static/@unity.png b/StreamDock-Plugin-SDK/SDNodeJsSDK/unity-communication-plugin/static/@unity.png deleted file mode 100644 index eb960787..00000000 --- a/StreamDock-Plugin-SDK/SDNodeJsSDK/unity-communication-plugin/static/@unity.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:f4eba213b920f94f5cc2927e6204a3bf23eec1b4458f3be14aaefaa97ed31f3b -size 5465 diff --git a/StreamDock-Plugin-SDK/SDNodeJsSDK/unity-communication-plugin/static/CH.png b/StreamDock-Plugin-SDK/SDNodeJsSDK/unity-communication-plugin/static/CH.png deleted file mode 100644 index eb960787..00000000 --- a/StreamDock-Plugin-SDK/SDNodeJsSDK/unity-communication-plugin/static/CH.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:f4eba213b920f94f5cc2927e6204a3bf23eec1b4458f3be14aaefaa97ed31f3b -size 5465 diff --git a/StreamDock-Plugin-SDK/SDNodeJsSDK/unity-communication-plugin/static/default.jpg b/StreamDock-Plugin-SDK/SDNodeJsSDK/unity-communication-plugin/static/default.jpg deleted file mode 100644 index 8061f9cf..00000000 --- a/StreamDock-Plugin-SDK/SDNodeJsSDK/unity-communication-plugin/static/default.jpg +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:eb5c4d9b90e10680cb8f13a6c12ad0f426601fd2ee4130494fa06b6e6bb1677c -size 16885 diff --git a/StreamDock-Plugin-SDK/SDNodeJsSDK/unity-communication-plugin/zh_CN.json b/StreamDock-Plugin-SDK/SDNodeJsSDK/unity-communication-plugin/zh_CN.json deleted file mode 100644 index b85b2571..00000000 --- a/StreamDock-Plugin-SDK/SDNodeJsSDK/unity-communication-plugin/zh_CN.json +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:686b48184fcd8189da15ab7bf71fb5529a033ef238298a074fb24513ca1681ab -size 231 diff --git a/StreamDock-Plugin-SDK/Unity_Scripts/SimpleEventExample.cs b/StreamDock-Plugin-SDK/Unity_Scripts/SimpleEventExample.cs deleted file mode 100644 index c34d111d..00000000 --- a/StreamDock-Plugin-SDK/Unity_Scripts/SimpleEventExample.cs +++ /dev/null @@ -1,101 +0,0 @@ -using UnityEngine; - -public class SimpleEventExample : MonoBehaviour -{ - [Header("StreamDock 통신")] - [SerializeField] private SimpleStreamDockCommunicator streamDock; - - void Start() - { - // StreamDock 이벤트 구독 - if (streamDock != null) - { - streamDock.OnStreamDockMessageReceived.AddListener(OnStreamDockMessage); - streamDock.OnConnected.AddListener(OnStreamDockConnected); - streamDock.OnDisconnected.AddListener(OnStreamDockDisconnected); - } - } - - /// - /// StreamDock에서 메시지 수신 시 처리 - /// - private void OnStreamDockMessage(string eventType, object data) - { - Debug.Log($"StreamDock 이벤트 수신: {eventType}"); - - switch (eventType) - { - case "button_clicked": - // 버튼 클릭 시 실행할 코드 - Debug.Log("버튼 클릭 이벤트 실행!"); - DoSomething(); - break; - - case "dial_rotate": - // 다이얼 회전 시 실행할 코드 - Debug.Log("다이얼 회전 이벤트 실행!"); - DoSomethingElse(); - break; - - case "dial_press": - // 다이얼 누름 시 실행할 코드 - Debug.Log("다이얼 누름 이벤트 실행!"); - DoAnotherThing(); - break; - } - } - - /// - /// StreamDock 연결 시 처리 - /// - private void OnStreamDockConnected() - { - Debug.Log("StreamDock에 연결되었습니다!"); - } - - /// - /// StreamDock 연결 해제 시 처리 - /// - private void OnStreamDockDisconnected() - { - Debug.Log("StreamDock 연결이 해제되었습니다."); - } - - // 여기에 원하는 동작들을 구현하세요 - private void DoSomething() - { - Debug.Log("버튼 클릭으로 실행된 동작!"); - // 예: 오브젝트 활성화/비활성화, 애니메이션 재생, 사운드 재생 등 - } - - private void DoSomethingElse() - { - Debug.Log("다이얼 회전으로 실행된 동작!"); - // 예: 볼륨 조절, 카메라 회전, 값 변경 등 - } - - private void DoAnotherThing() - { - Debug.Log("다이얼 누름으로 실행된 동작!"); - // 예: 특수 기능 실행, 모드 변경 등 - } - - // 공개 메서드들 (Inspector에서 호출 가능) - [ContextMenu("테스트 동작 1")] - public void TestAction1() - { - DoSomething(); - } - - [ContextMenu("테스트 동작 2")] - public void TestAction2() - { - DoSomethingElse(); - } - - [ContextMenu("테스트 동작 3")] - public void TestAction3() - { - DoAnotherThing(); - } -} \ No newline at end of file diff --git a/StreamDock-Plugin-SDK/Unity_Scripts/SimpleStreamDockCommunicator.cs b/StreamDock-Plugin-SDK/Unity_Scripts/SimpleStreamDockCommunicator.cs deleted file mode 100644 index 2de1421a..00000000 --- a/StreamDock-Plugin-SDK/Unity_Scripts/SimpleStreamDockCommunicator.cs +++ /dev/null @@ -1,282 +0,0 @@ -using UnityEngine; -using UnityEngine.Events; -using System; -using System.Collections; -using System.Text; -using System.Net.WebSockets; -using System.Threading; -using System.Threading.Tasks; - -[System.Serializable] -public class StreamDockEvent : UnityEvent { } - -public class SimpleStreamDockCommunicator : MonoBehaviour -{ - [Header("연결 설정")] - [SerializeField] private string serverUrl = "ws://localhost:15732"; - [SerializeField] private bool autoConnect = true; - [SerializeField] private float reconnectInterval = 5f; - - [Header("이벤트")] - public StreamDockEvent OnStreamDockMessageReceived; - public UnityEvent OnConnected; - public UnityEvent OnDisconnected; - - // 내부 변수 - private ClientWebSocket webSocket; - private CancellationTokenSource cancellationTokenSource; - private bool isConnecting = false; - private bool isConnected = false; - - // 프로퍼티 - public bool IsConnected => isConnected; - - void Start() - { - if (autoConnect) - { - ConnectToStreamDock(); - } - } - - void OnDestroy() - { - DisconnectFromStreamDock(); - } - - /// - /// StreamDock에 연결 - /// - public async void ConnectToStreamDock() - { - if (isConnecting || isConnected) return; - - isConnecting = true; - - try - { - webSocket = new ClientWebSocket(); - cancellationTokenSource = new CancellationTokenSource(); - - Debug.Log($"StreamDock에 연결 중... {serverUrl}"); - - await webSocket.ConnectAsync(new Uri(serverUrl), cancellationTokenSource.Token); - - isConnected = true; - isConnecting = false; - - Debug.Log("StreamDock에 연결되었습니다!"); - OnConnected?.Invoke(); - - // 메시지 수신 시작 - _ = ReceiveMessages(); - - } - catch (Exception e) - { - Debug.LogError($"StreamDock 연결 실패: {e.Message}"); - isConnecting = false; - OnDisconnected?.Invoke(); - - // 재연결 시도 - StartCoroutine(TryReconnect()); - } - } - - /// - /// StreamDock 연결 해제 - /// - public async void DisconnectFromStreamDock() - { - if (!isConnected) return; - - try - { - cancellationTokenSource?.Cancel(); - - if (webSocket != null && webSocket.State == WebSocketState.Open) - { - await webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Unity 종료", CancellationToken.None); - } - } - catch (Exception e) - { - Debug.LogError($"연결 해제 중 오류: {e.Message}"); - } - finally - { - isConnected = false; - webSocket?.Dispose(); - webSocket = null; - OnDisconnected?.Invoke(); - } - } - - /// - /// StreamDock으로 메시지 전송 - /// - public async void SendMessageToStreamDock(string eventType, object data = null) - { - if (!isConnected) - { - Debug.LogWarning("StreamDock에 연결되지 않았습니다."); - return; - } - - try - { - var message = new - { - type = eventType, - data = data, - timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() - }; - - string jsonMessage = JsonUtility.ToJson(message); - byte[] buffer = Encoding.UTF8.GetBytes(jsonMessage); - - await webSocket.SendAsync(new ArraySegment(buffer), WebSocketMessageType.Text, true, cancellationTokenSource.Token); - - Debug.Log($"StreamDock으로 메시지 전송: {eventType}"); - } - catch (Exception e) - { - Debug.LogError($"메시지 전송 실패: {e.Message}"); - } - } - - /// - /// 메시지 수신 처리 - /// - private async Task ReceiveMessages() - { - var buffer = new byte[4096]; - - try - { - while (webSocket.State == WebSocketState.Open) - { - var result = await webSocket.ReceiveAsync(new ArraySegment(buffer), cancellationTokenSource.Token); - - if (result.MessageType == WebSocketMessageType.Text) - { - string message = Encoding.UTF8.GetString(buffer, 0, result.Count); - ProcessReceivedMessage(message); - } - else if (result.MessageType == WebSocketMessageType.Close) - { - Debug.Log("StreamDock에서 연결을 종료했습니다."); - break; - } - } - } - catch (Exception e) - { - Debug.LogError($"메시지 수신 중 오류: {e.Message}"); - } - finally - { - isConnected = false; - OnDisconnected?.Invoke(); - StartCoroutine(TryReconnect()); - } - } - - /// - /// 수신된 메시지 처리 - /// - private void ProcessReceivedMessage(string message) - { - try - { - Debug.Log($"StreamDock에서 메시지 수신: {message}"); - - // JSON 파싱 (간단한 구조) - if (message.Contains("type")) - { - // UnityEvent 호출 - OnStreamDockMessageReceived?.Invoke("streamdock_message", message); - - // 특정 이벤트 타입 처리 - if (message.Contains("connection_confirmed")) - { - Debug.Log("StreamDock 플러그인 연결 확인됨!"); - } - else if (message.Contains("button_clicked")) - { - HandleButtonClick(message); - } - else if (message.Contains("streamdock_button_clicked")) - { - HandleButtonClick(message); - } - else if (message.Contains("dial_rotate")) - { - HandleDialRotate(message); - } - else if (message.Contains("dial_press")) - { - HandleDialPress(message); - } - } - } - catch (Exception e) - { - Debug.LogError($"메시지 처리 중 오류: {e.Message}"); - } - } - - /// - /// 버튼 클릭 이벤트 처리 - /// - private void HandleButtonClick(string message) - { - Debug.Log("StreamDock 버튼이 클릭되었습니다!"); - OnStreamDockMessageReceived?.Invoke("button_clicked", message); - } - - /// - /// 다이얼 회전 이벤트 처리 - /// - private void HandleDialRotate(string message) - { - Debug.Log("StreamDock 다이얼이 회전했습니다!"); - OnStreamDockMessageReceived?.Invoke("dial_rotate", message); - } - - /// - /// 다이얼 누름 이벤트 처리 - /// - private void HandleDialPress(string message) - { - Debug.Log("StreamDock 다이얼이 눌렸습니다!"); - OnStreamDockMessageReceived?.Invoke("dial_press", message); - } - - /// - /// 재연결 시도 - /// - private IEnumerator TryReconnect() - { - yield return new WaitForSeconds(reconnectInterval); - - if (!isConnected && !isConnecting) - { - Debug.Log("StreamDock 재연결 시도..."); - ConnectToStreamDock(); - } - } - - // 테스트용 메서드들 - [ContextMenu("테스트 메시지 전송")] - public void SendTestMessage() - { - SendMessageToStreamDock("test_message", new { message = "Unity에서 테스트 메시지" }); - } - - [ContextMenu("커스텀 이벤트 전송")] - public void SendCustomEvent() - { - SendMessageToStreamDock("custom_event", new { action = "test_action", value = 123 }); - } -} \ No newline at end of file diff --git a/StreamDock-Plugin-SDK/Unity_StreamDock_통신_가이드.md b/StreamDock-Plugin-SDK/Unity_StreamDock_통신_가이드.md deleted file mode 100644 index 27d50123..00000000 --- a/StreamDock-Plugin-SDK/Unity_StreamDock_통신_가이드.md +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:57dbf8a7ce2579a8d535fa455b29840944cd8dd76f74746cdfdfd9e88ff35790 -size 7438 diff --git a/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/.gitignore b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/.gitignore new file mode 100644 index 00000000..c2dd9a53 --- /dev/null +++ b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/.gitignore @@ -0,0 +1,3 @@ +node_modules +dist +.vscode \ No newline at end of file diff --git a/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/README.md b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/README.md new file mode 100644 index 00000000..e1e40550 --- /dev/null +++ b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/README.md @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bef78e9ad5535dcfee0896b32122098a46f50e636f6454c0bb48852e8e529d1d +size 205 diff --git a/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/ar.json b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/ar.json new file mode 100644 index 00000000..8f8c8bec --- /dev/null +++ b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/ar.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d5a44e5ffc451018d2bcca7136ee043a2746fafc7ba2048e0321479e5546bb49 +size 4062 diff --git a/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/de.json b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/de.json new file mode 100644 index 00000000..75f52f97 --- /dev/null +++ b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/de.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:85847389cbea6660ab29d2130057b060e65876773522af8fae159a29ea9b5d6d +size 3430 diff --git a/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/en.json b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/en.json new file mode 100644 index 00000000..8ad35314 --- /dev/null +++ b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/en.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c4ee156eebcab8f20f11a8420388e3201d1d4de08a47ee71a88ec2d8d9b963d8 +size 3121 diff --git a/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/es.json b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/es.json new file mode 100644 index 00000000..b058a1a3 --- /dev/null +++ b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/es.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5f7301e4594bdaec8c85aa9c445e2dbe878435c5530bb811eafcd48836ea1ce4 +size 3594 diff --git a/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/fr.json b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/fr.json new file mode 100644 index 00000000..71c1c1e7 --- /dev/null +++ b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/fr.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e942e491ef6f2ce45c9b834a7c338055f9d784029529021cfff873348421c741 +size 3506 diff --git a/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/it.json b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/it.json new file mode 100644 index 00000000..917037c3 --- /dev/null +++ b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/it.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b3793ff78e389f4e6bd3ee739c0b99ad4522c7689ea24c9c2d18c7cd9f1858fc +size 3519 diff --git a/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/ja.json b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/ja.json new file mode 100644 index 00000000..d18e030d --- /dev/null +++ b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/ja.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3f807e159869d830983ce381a51d6996bc8fb7ffbd236728786aaa49db26e72b +size 3329 diff --git a/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/ko.json b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/ko.json new file mode 100644 index 00000000..9a704643 --- /dev/null +++ b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/ko.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0ecee56922ee20b4305a943b68470765d998ba234276e0ce3de54b7901a41946 +size 3332 diff --git a/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/manifest.json b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/manifest.json new file mode 100644 index 00000000..f4eb0072 --- /dev/null +++ b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/manifest.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7b0db1832b2e22c06f70fa1f6b2293cec884a834d4ad673a6b398a71edf47a58 +size 1278 diff --git a/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/pl.json b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/pl.json new file mode 100644 index 00000000..089375b7 --- /dev/null +++ b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/pl.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1e1dd6d220b1775e21921d0d197d699297123e4263c41ebddb3b3b5204d6e48c +size 3472 diff --git a/StreamDock-Plugin-SDK/SDNodeJsSDK/unity-communication-plugin/plugin/autofile.js b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/plugin/autofile.js similarity index 70% rename from StreamDock-Plugin-SDK/SDNodeJsSDK/unity-communication-plugin/plugin/autofile.js rename to StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/plugin/autofile.js index 340d2954..cfe74798 100644 --- a/StreamDock-Plugin-SDK/SDNodeJsSDK/unity-communication-plugin/plugin/autofile.js +++ b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/plugin/autofile.js @@ -1,14 +1,15 @@ const path = require('path'); const fs = require('fs-extra'); - -console.log('开始执行自动化构建...'); +const manifest = require('../manifest.json'); +console.log('开始执行自动化构建...', manifest.PUUID); const currentDir = __dirname; -// 获取父文件夹的路径 -const parentDir = path.join(currentDir, '..'); -// 获取父文件夹的名称 -const PluginName = path.basename(parentDir); +// // 获取父文件夹的路径 +// const parentDir = path.join(currentDir, '..'); +// // 获取父文件夹的名称 +// const PluginName = path.basename(parentDir); +const PluginName = `com.mirabox.streamdock.${manifest.PUUID}.sdPlugin`; const PluginPath = path.join(process.env.APPDATA, 'HotSpot/StreamDock/plugins', PluginName); @@ -26,9 +27,10 @@ try { const relativePath = path.relative(path.resolve(__dirname, '..'), src); // 排除 'node_modules' 和 '.git' 目录及其子文件 return !relativePath.startsWith('plugin\\node_modules') - &&!relativePath.startsWith('plugin\\index.js') + // &&!relativePath.startsWith('plugin\\index.js') &&!relativePath.startsWith('plugin\\package.json') &&!relativePath.startsWith('plugin\\package-lock.json') + &&!relativePath.startsWith('plugin\\pnpm-lock.yaml') &&!relativePath.startsWith('plugin\\yarn.lock') &&!relativePath.startsWith('plugin\\build') &&!relativePath.startsWith('plugin\\log') @@ -37,7 +39,7 @@ try { } }); - fs.copySync( path.join(__dirname, "build"), path.join(PluginPath,'plugin')) + // fs.copySync( path.join(__dirname, "build"), path.join(PluginPath,'plugin')) console.log(`插件 "${PluginName}" 已成功复制到 "${PluginPath}"`); console.log('构建成功-------------'); diff --git a/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/plugin/enum.js b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/plugin/enum.js new file mode 100644 index 00000000..5f22a73a --- /dev/null +++ b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/plugin/enum.js @@ -0,0 +1,133 @@ +// 豪华绚丽枚举 +const LuxuryEnum = { + 100: "100", + 101: "101", + 102: "101", + 103: "101", + 104: "104", + 150: "150", + 151: "151", + 152: "151", + 153: "151", + 154: "151", + 300: "300", + 301: "300", + 302: "302", + 303: "302", + 304: "304", + 305: "305", + 306: "306", + 307: "307", + 308: "307", + 309: "305", + 310: "310", + 311: "311", + 312: "312", + 313: "313", + 314: "306", + 315: "307", + 316: "310", + 317: "311", + 318: "312", + 350: "350", + 351: "350", + 399: "305", + 400: "400", + 401: "401", + 402: "402", + 403: "403", + 404: "404", + 405: "404", + 406: "404", + 407: "407", + 408: "401", + 409: "402", + 410: "403", + 456: "404", + 457: "457", + 499: "400", + 500: "500", + 501: "500", + 502: "502", + 503: "503", + 504: "503", + 507: "503", + 508: "508", + 509: "500", + 510: "500", + 511: "502", + 512: "502", + 513: "502", + 514: "500", + 515: "502", + 900: "100", + 901: "401", + 999: "101" +} + +// 轻盈灵动枚举 +const dynamicEnum = { + 100: "100", + 101: "101", + 102: "101", + 103: "101", + 104: "104", + 150: "150", + 151: "151", + 152: "151", + 153: "151", + 154: "151", + 300: "300", + 301: "300", + 302: "302", + 303: "302", + 304: "304", + 305: "305", + 306: "306", + 307: "307", + 308: "307", + 309: "305", + 310: "310", + 311: "311", + 312: "312", + 313: "313", + 314: "306", + 315: "307", + 316: "310", + 317: "311", + 318: "312", + 350: "350", + 351: "350", + 399: "305", + 400: "400", + 401: "401", + 402: "402", + 403: "403", + 404: "404", + 405: "404", + 406: "404", + 407: "407", + 408: "401", + 409: "402", + 410: "403", + 456: "404", + 457: "457", + 499: "400", + 500: "500", + 501: "500", + 502: "502", + 503: "503", + 504: "503", + 507: "503", + 508: "508", + 509: "500", + 510: "500", + 511: "502", + 512: "502", + 513: "502", + 514: "500", + 515: "502", + 900: "100", + 901: "401", + 999: "101" +} \ No newline at end of file diff --git a/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/plugin/index.html b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/plugin/index.html new file mode 100644 index 00000000..c0d4874c --- /dev/null +++ b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/plugin/index.html @@ -0,0 +1,19 @@ + + + + + 天气查询 - 插件 + + + + + + + + + + + + + + \ No newline at end of file diff --git a/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/plugin/index.js b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/plugin/index.js new file mode 100644 index 00000000..7e4dc995 --- /dev/null +++ b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/plugin/index.js @@ -0,0 +1,322 @@ +/** + * 基础参数说明: + * @plugin 全局插件行动 - 策略模式 + * @plugin .default 行动默认数据 + * @plugin _willAppear 在 willAppear 方法之后立即执行 + * @plugin _willDisappear 在 willDisappear 方法之前立即执行 + * @common sendToPlugin keyUp propertyInspectorDidAppear + * =========================================================================> + */ +window.QWEATHER_API_KEY = 'bdd98ec1d87747f3a2e8b1741a5af796'; +window.WEATHERAPI_COM_API_KEY = 'c4aeca457d9e4a36a9982404252804'; +const $local = false, + $plugin = { + name: "weather", + action1: new Action({ + default: { + inputCity: "", // 输入框 + tempList: "0", // + cityId: "", + city: "", + title: "", + radio: "0", + radio2: "0", + radioUseApi: window.WeatherApiEnum.qweather, + searchList: [], + theme: "Modern", + wdata: { tmp: "20", code: "101" , name: '', img: '', cityName: ''}, + titleParameters: { + titleColor: "#ffffff", + }, + count: 0, + Localization: {} + }, + /** + * 1. 和风开发者KEY: 641403ded7f348bf88681308e648bdde + * 2. 和风官方地理位置KEY: bdd98ec1d87747f3a2e8b1741a5af796 + * 3. 和风官方网页查询数据: https://www.qweather.com/v2/current/condition/s/x-cityId.html + */ + async queryLocation(context, device) { + console.log("queryLocation"); + const data = this.data[context]; + const langMap = { + 'zh_CN': 'zh', 'en': 'en', 'ja': 'ja', 'fr': 'fr', + 'it': 'it', 'ru': 'ru', 'es': 'es', 'pt': 'pt', 'de': 'de' + }; + const lang = langMap[$lang] || 'en'; + const weatherService = window.WeatherServiceFactory.createWeatherService(data.radioUseApi); // 获取天气服务实例 + + try { + if (data.inputCity) { + // console.log("inputCity"); + $websocket.setTitle(context, "Loading"); + const [error, locationList] = await weatherService.queryLocation(data.inputCity, lang); + + if (error) { + console.error("queryLocation failed:", error); + data.searchList = []; + this.canvasFunc(context, device, "error"); + $websocket.setSettings(context, data); + } else if (locationList && locationList.length > 0) { + // **需要根据不同服务商的返回数据结构进行适配** + // 和风天气返回的是一个包含城市信息的数组 (res.data.location) + // WeatherAPI.com 返回的也是一个数组 (res.data) + data.searchList = locationList; + this.queryWeather(context, device); + data.count = 0; + } else { + data.searchList = []; + this.canvasFunc(context, device, "404"); + $websocket.setSettings(context, data); + } + } + } catch (e) { + console.error("queryLocation general error:", e); + if (++data.count <= 3) { + this.queryLocation(context, device); + $websocket.setTitle(context, "Try again"); + return; + } + this.canvasFunc(context, device, "error"); + } + }, + async queryWeather(context, device) { + const data = this.data[context]; + const weatherService = window.WeatherServiceFactory.createWeatherService(data.radioUseApi); // 获取天气服务实例 + const langMap = { + 'zh_CN': 'zh', 'en': 'en', 'ja': 'ja', 'fr': 'fr', + 'it': 'it', 'ru': 'ru', 'es': 'es', 'pt': 'pt', 'de': 'de' + }; + const lang = langMap[$lang] || 'en'; + try { + clearTimeout(data.timer); + console.log(data,data.cityId, data.searchList) + // 过滤出用户选择的城市 + data.cityId = ( + data.searchList.filter((item) => item.id === data.cityId)[0] || + data.searchList[0] + )?.id; + data.city = data.searchList.filter( + (item) => item.id === data.cityId + )[0]?.name; + + if (data.cityId) { + $websocket.setTitle(context, "Loading"); + const [error, weatherData] = await weatherService.queryWeather(data.cityId, lang); + + if (error) { + console.error("queryWeather failed:", error); + this.canvasFunc(context, device, "error"); + } else if (weatherData) { + // **需要根据不同服务商的返回数据结构进行适配** + let tmp, code, img, name, cityName; + if (weatherService instanceof window.QWeatherService) { + tmp = weatherData.tmp; + code = weatherData.code; // 需要查阅和风天气的 icon 对应关系 + } else if (weatherService instanceof window.WeatherApiComService) { + tmp = weatherData.current.temp_c; // 或 temp_f,根据需求 + code = weatherData.current.condition.code; // 需要查阅 WeatherAPI.com 的 code 对应关系 + img = 'https:' + weatherData.current.condition.icon; + name = weatherData.current.condition.text; + cityName = weatherData.location.name; + } + + data.wdata = { tmp, code, img, name, cityName}; + data.count = 0; + data.timer = setTimeout( + () => this.queryWeather(context, device), + 1000 * 60 * 60 * 1 + ); + $websocket.setSettings(context, data); + this.canvasFunc(context, device); + } else { + this.canvasFunc(context, device, "error"); + } + } + } catch (e) { + console.error("queryWeather general error:", e); + if (++data.count <= 3) { + this.queryWeather(context, device); + $websocket.setTitle(context, "Try again"); + return; + } + this.canvasFunc(context, device, "error"); + } + }, + // 绘制 + async canvasFunc(context, device, status = "success") { + if (status === "error") { + $websocket.setImage( + context, + this.data[context].isBackgroundHidden + ? "../static/img/tm.png" + : "../static/img/default.jpg" + ); + $websocket.setTitle(context, "Timeout"); + return; + } else if (status === "404") { + $websocket.setImage( + context, + this.data[context].isBackgroundHidden + ? "../static/img/tm.png" + : "../static/img/default.jpg" + ); + $websocket.setTitle(context, "Not found"); + return; + } + if (!this.data[context].cityId) return; + + // 主题配置 + const data = this.data[context]; + const { tmp, code, img, name, cityName } = data.wdata; + // 摄氏度/华氏度 + const unit = data.tempList === "0" ? "℃" : "℉"; + const tmps = data.tempList === "0" ? tmp : tmp * 1.8 + 32; + + // 设置gif背景 + // if (data.theme == "dynamic") { + // $websocket.setImage(context,`${dynamicEnum[101]}`, true); + // $websocket.setTitle( + // context, + // data.radio == "0" ? `${Number(tmps).toFixed(1)}${unit}\n${data.city}` : `${Number(tmps).toFixed(1)}${unit}\n${data.title}` + // ); + // return + // } + + // console.log(this.data[context].radioUseApi, window.WeatherApiEnum, data.wdata); + const image = new Image(); + switch(this.data[context].radioUseApi) { + case window.WeatherApiEnum.qweather: + if (data.theme == "Modern") + image.src = `../static/img/Modern/${code}-fill.svg`; + if (data.theme == "Luxury") + image.src = `../static/img/Luxury/${LuxuryEnum[code]}.png`; + break; + case window.WeatherApiEnum.weatherapi: + image.src = img + break; + default: + } + + + /* 加载完毕后开始 */ + image.onload = async function () { + let canvas = document.createElement("canvas"); + canvas.width = canvas.height = 512; + let ctx = canvas.getContext("2d"); + + // 是否隐藏背景 适配 296 + // if (!data.isBackgroundHidden) { + ctx.fillStyle = "rgba(0,0,0,0)"; + ctx.fillRect(0, 0, 512, 512); + ctx.save(); + // } + + if (data.theme == "Modern") ctx.drawImage(this, (512 - 260) / 2, 20, 260, 260); + if (data.theme == "Luxury") ctx.drawImage(this, -2, -2, 516, 516); + + // ctx.fillStyle = data.titleParameters.titleColor; + // ctx.font = `${data.titleParameters.fontStyle == "Regular" ? "" : data.titleParameters.fontStyle} ${data.titleParameters.fontSize + 10}px '${data.titleParameters.fontFamily}'`; + // ctx.shadowColor = "white"; + // ctx.shadowBlur = 1; + // ctx.shadowOffsetX = 1; + // ctx.shadowOffsetY = 1; + + + // ctx.fillText(`${Number(tmps).toFixed(1)}${unit}`, 14, 29); + + // if (data.titleParameters.fontUnderline) { + // let textMetrics = ctx.measureText(`${Number(tmps).toFixed(1)}${unit}`); + // let underlineHeight = 1; + // ctx.fillRect(14, 29 + 2, textMetrics.width, underlineHeight); + // } + + let weatherName; + switch(data.radioUseApi) { + case window.WeatherApiEnum.qweather: + weatherName = data.radio2 == "0" ? data.Localization[code == 154 ? 153 : code] + "\n" : ''; + data.Localization[code == 154 ? 153 : code] + "\n" + $websocket.setTitle( + context, + weatherName + (data.radio == "0" ? `${Number(tmps).toFixed(1)}${unit}\n${data.city}` : `${Number(tmps).toFixed(1)}${unit}\n${data.title}`) + ); + break; + case window.WeatherApiEnum.weatherapi: + weatherName = data.radio2 == "0" ? name + "\n" : ''; + $websocket.setTitle( + context, + weatherName + (data.radio == "0" ? `${Number(tmps).toFixed(1)}${unit}\n${cityName}` : `${Number(tmps).toFixed(1)}${unit}\n${data.title}`), + ); + break; + default: + } + + // 国际化json文件里面没有154,154与153是一样的夜间多云 + $websocket.setImage(context, canvas.toDataURL("image/png")); + }; + }, + titleParametersDidChange({ context, payload, device }) { + this.data[context].titleParameters = payload.titleParameters; + this.canvasFunc(context, device); + }, + didReceiveSettings({ context, payload, device }) { + this.data[context].isBackgroundHidden = payload.settings.isBackgroundHidden; + this.canvasFunc(context, device); + }, + async _willAppear({ context, device, payload }) { + console.log("willAppear", payload); + + this.data[context].Localization = await new Promise(resolve => { + const req = new XMLHttpRequest(); + req.open('GET', `../${$lang}.json`); + req.send(); + req.onreadystatechange = () => { + if (req.readyState === 4) { + resolve(JSON.parse(req.responseText).Localization) + } + }; + }) + const { radioUseApi } = payload.settings; + this.data[context].radioUseApi = radioUseApi !== undefined? window.WeatherApiEnum[radioUseApi]: window.WeatherApiEnum.qweather; + this.queryLocation(context, device); + }, + _willDisappear({ context}) { + clearTimeout(this.data[context].timer); + }, + sendToPlugin({ context, payload, device }) { + const data = this.data[context]; + const { inputCity, cityId, title, theme, radio, radio2, tempList, radioUseApi } = payload; + // 切换提供商 + if (radioUseApi !== undefined) { + data.radioUseApi = radioUseApi; + this.data[context].radioUseApi = window.WeatherApiEnum[radioUseApi]; + this.data[context].count = 0; + return this.queryLocation(context, device); + } + + // 输入城市 + if (inputCity !== undefined) { + data.inputCity = inputCity; + this.data[context].count = 0; + return this.queryLocation(context, device); + } + // 更新天气 + if (cityId !== undefined) { + data.cityId = cityId; + this.data[context].count = 0; + return this.queryWeather(context, device); + } + // 更新选项 + if (title !== undefined) data.title = title; + if (theme !== undefined) data.theme = theme; + if (radio !== undefined) data.radio = radio; + if (radio2 !== undefined) data.radio2 = radio2; + if (tempList !== undefined) data.tempList = tempList; + this.canvasFunc(context, device); + }, + keyUp({ context, device }) { + this.data[context].count = 0; + this.queryWeather(context, device); + }, + }), + }; diff --git a/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/plugin/package.json b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/plugin/package.json new file mode 100644 index 00000000..d709567b --- /dev/null +++ b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/plugin/package.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:87d6073c64af704ff952a111ede938a2b16bc9792a93d251a39285824cffeea5 +size 358 diff --git a/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/plugin/utils/weatherService.js b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/plugin/utils/weatherService.js new file mode 100644 index 00000000..e3646b85 --- /dev/null +++ b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/plugin/utils/weatherService.js @@ -0,0 +1,206 @@ +/** + * weatherService.js - 封装天气查询服务,支持多个天气提供商。 + * + * 该文件定义了天气服务接口和具体的天气提供商实现, + * 并提供了一个工厂函数用于创建天气服务实例。 + */ +var { pinyin } = pinyinPro; +// console.log(pinyin('呼和浩特',{"toneType": "none",separator: ''})); + +(function () { + const WeatherApiEnum = { + qweather: 'qweather', + weatherapi: 'weatherapi' + } + /** + * @class WeatherServiceInterface + * @classdesc 定义天气服务接口,所有天气提供商的服务类都应继承此类。 + */ + class WeatherServiceInterface { + /** + * @async + * @function WeatherServiceInterface#queryLocation + * @param {string} query - 查询关键字(如城市名称)。 + * @param {string} lang - 语言代码。 + * @returns {Promise<[Error | null, Array | null]>} - Promise,resolve 包含 [错误, 地区列表]。 + * @throws {Error} 如果子类没有实现该方法。 + */ + async queryLocation(query, lang) { + throw new Error("Method 'queryLocation' must be implemented."); + } + + /** + * @async + * @function WeatherServiceInterface#queryWeather + * @param {string} locationId - 地区ID。 + * @param {string} lang - 语言代码。 + * @returns {Promise<[Error | null, object | null]>} - Promise,resolve 包含 [错误, 天气数据]。 + * @throws {Error} 如果子类没有实现该方法。 + */ + async queryWeather(locationId, lang) { + throw new Error("Method 'queryWeather' must be implemented."); + } + } + + /** + * @class QWeatherService + * @classdesc 和风天气服务类,继承自 WeatherServiceInterface。 + */ + class QWeatherService extends WeatherServiceInterface { + /** + * @param {string} apiKey - 和风天气的 API 密钥。 + */ + constructor(apiKey) { + super(); + this.apiKey = apiKey; + } + + /** + * @async + * @function QWeatherService#queryLocation + * @inheritdoc + */ + async queryLocation(query, lang) { + try { + const res = await axios( + `https://geoapi.qweather.com/v2/city/lookup?location=${query}&key=${this.apiKey}&lang=${lang}`, + { timeout: 3000 } + ); + if (res.data.code === "200") { + return [null, res.data.location]; + } else { + return [new Error(`QWeather API Error: ${res.data.code}`), null]; + } + } catch (error) { + console.error("QWeatherService - queryLocation error:", error); + return [error, null]; + } + } + + /** + * @async + * @function QWeatherService#queryWeather + * @inheritdoc + */ + async queryWeather(locationId, lang) { + try { + const res = await axios( + `https://www.qweather.com/v2/current/condition/s/x-${locationId}.html`, + { timeout: 3000 } + ); + if (res.data.code === "0") { + return [null, res.data.data]; + } else { + return [new Error(`QWeather API Error: ${res.data.code}`), null]; + } + } catch (error) { + console.error("QWeatherService - queryWeather error:", error); + return [error, null]; + } + } + } + + /** + * @class WeatherApiComService + * @classdesc WeatherAPI.com 天气服务类,继承自 WeatherServiceInterface。 + */ + class WeatherApiComService extends WeatherServiceInterface { + /** + * @param {string} apiKey - WeatherAPI.com 的 API 密钥。 + */ + constructor(apiKey) { + super(); + this.apiKey = apiKey; + } + + /** + * @async + * @function WeatherApiComService#queryLocation + * @inheritdoc + */ + async queryLocation(query, lang) { + try { + let pinyinQuery = pinyin(query,{"toneType": "none",separator: ''}) + const res = await axios( + `http://api.weatherapi.com/v1/search.json?key=${this.apiKey}&q=${pinyinQuery}&lang=${lang}`, + { timeout: 3000 } + ); + let result = res.data.map(i => { + i.id = `${i.id}`; + return i; + }) + return [null, result]; // 根据 WeatherAPI.com 的响应结构调整 + } catch (error) { + console.error("WeatherApiComService - queryLocation error:", error); + return [error, null]; + } + } + + /** + * @async + * @function WeatherApiComService#queryWeather + * @inheritdoc + */ + async queryWeather(locationId, lang) { + try { + const res = await axios( + `http://api.weatherapi.com/v1/current.json?key=${this.apiKey}&q=id:${locationId}&lang=${lang}`, + { timeout: 3000 } + ); + return [null, res.data]; // 根据 WeatherAPI.com 的响应结构调整 + } catch (error) { + console.error("WeatherApiComService - queryWeather error:", error); + return [error, null]; + } + } + } + + /** + * @namespace WeatherServiceFactory + * @description 用于创建具体天气服务实例的工厂。 + */ + const WeatherServiceFactory = (function () { + const providers = { + qweather: (apiKey) => new QWeatherService(apiKey), + weatherapi: (apiKey) => new WeatherApiComService(apiKey), + // 可以添加更多的天气提供商 + }; + const apiKeys = { + qweather: () => window.QWEATHER_API_KEY || 'bdd98ec1d87747f3a2e8b1741a5af796', + weatherapi: () => window.WEATHERAPI_COM_API_KEY || 'c4aeca457d9e4a36a9982404252804', + // 配置其他提供商的 API 密钥 (使用函数延迟读取) + }; + + return { + /** + * @function WeatherServiceFactory.createWeatherService + * @returns {WeatherServiceInterface} - 当前配置的天气服务实例。 + * @throws {Error} 如果不支持指定的天气提供商或 API 密钥未配置。 + */ + createWeatherService: function (WEATHER_PROVIDER_TYPE) { + const WEATHER_PROVIDER = WEATHER_PROVIDER_TYPE || 'qweather'; // 每次调用时读取 + const providerName = WEATHER_PROVIDER.toLowerCase(); + const getApiKey = apiKeys[providerName]; + + if (!providers[providerName]) { + throw new Error(`Unsupported weather provider: ${providerName}`); + } + if (!getApiKey || typeof getApiKey !== 'function') { + throw new Error(`API key getter for ${providerName} is not configured.`); + } + const apiKey = getApiKey(); + if (!apiKey) { + throw new Error(`API key for ${providerName} is not set.`); + } + + return providers[providerName](apiKey); + } + }; + })(); + + // 将 WeatherServiceFactory 暴露到全局作用域 + window.WeatherServiceFactory = WeatherServiceFactory; + window.QWeatherService = QWeatherService; // 确保这行存在 + window.WeatherApiComService = WeatherApiComService; // 确保这行存在 + window.WeatherApiEnum = WeatherApiEnum; +})(); \ No newline at end of file diff --git a/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/propertyInspector/action1/index.html b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/propertyInspector/action1/index.html new file mode 100644 index 00000000..c006ba65 --- /dev/null +++ b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/propertyInspector/action1/index.html @@ -0,0 +1,106 @@ + + + + + + 天气查询 - 属性检查器 + + + + + + + + + + + + \ No newline at end of file diff --git a/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/propertyInspector/action1/index.js b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/propertyInspector/action1/index.js new file mode 100644 index 00000000..1114df96 --- /dev/null +++ b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/propertyInspector/action1/index.js @@ -0,0 +1,96 @@ +/** + * 基础参数说明: + * @local 是否国际化 + * @back 自主决定回显时机 + * @dom 保存需要的文档元素 + * @propEvent 软件回调事件 - 策略模式 + * ==================================================> + */ +const $local = true, $back = false, + $dom = { + main: $('.sdpi-wrapper'), + titleWrap: $('#titleWrap'), + tempList: $('#tempList'), + inputCity: $('#inputCity'), + inputTitle: $('#inputTitle'), + themeList: $('#themeList'), + searchBtn: $('#searchBtn'), + searchList: $('#searchList'), + radio: $('input[name="rdio"]', true), + radio2: $('input[name="rdio2"]', true), + radioUseApi: $('input[name="radioUseApi"]', true) + + }, + $propEvent = { + didReceiveSettings() { + $dom.searchList.innerHTML = ($settings.searchList || []).map(item => { + return ``; + }).join(''); + $dom.tempList.value = $settings.tempList || '0'; + $dom.inputCity.value = $settings.inputCity || ''; + $dom.inputTitle.value = $settings.title || ''; + $dom.searchList.value = $settings.cityId || ''; + $dom.themeList.value = $settings.theme || 'Modern'; + titleWrap.style.display = ($settings.radio || '0') == '0' ? 'none' : 'flex'; + $dom.radio.forEach(item => + item.checked = item.value === ($settings.radio || '0') ? true : false); + $dom.radio2.forEach(item => + item.checked = item.value === ($settings.radio2 || '0') ? true : false); + $dom.radioUseApi.forEach(item => + item.checked = item.value === ($settings.radioUseApi || 'qweather') ? true: false); + }, + sendToPropertyInspector(data) { } + }; + +// 从这里开始... +$dom.radioUseApi.forEach(item => item.on('change', function () { + $settings.radioUseApi = this.value; + $websocket.sendToPlugin({ radioUseApi: this.value }); +})); + +$dom.radio.forEach(item => item.on('change', function () { + $settings.radio = this.value; + $websocket.sendToPlugin({ radio: this.value }); + titleWrap.style.display = this.value == '0' ? 'none' : 'flex'; +})); + +$dom.radio2.forEach(item => item.on('change', function () { + $settings.radio2 = this.value; + $websocket.sendToPlugin({ radio2: this.value }); +})); + +// 防抖查询城市列表 +const searchFunc = $.debounce(() => { + $websocket.sendToPlugin({ inputCity: $dom.inputCity.value }); +}, 1000); +$dom.inputCity.on('input', () => { + searchFunc(); + $settings.inputCity = $dom.inputCity.value; +}); + +// 切换温度单位 +$dom.tempList.on('change', () => { + $settings.tempList = $dom.tempList.value; + $websocket.sendToPlugin({ tempList: $dom.tempList.value }); +}); + +// 搜索天气 +$dom.searchList.on('change', () => { + $settings.cityId = $dom.searchList.value; + $websocket.sendToPlugin({ cityId: $dom.searchList.value }); +}); +$dom.searchBtn.on('click', () => { + $websocket.sendToPlugin({ cityId: $dom.searchList.value }); +}); + +// 自定义标题 +$dom.inputTitle.on('input', () => { + $settings.title = $dom.inputTitle.value; + $websocket.sendToPlugin({ title: $dom.inputTitle.value }); +}); + +// 主题切换 +$dom.themeList.on('change', () => { + $settings.theme = $dom.themeList.value; + $websocket.sendToPlugin({ theme: $dom.themeList.value }); +}); \ No newline at end of file diff --git a/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/pt.json b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/pt.json new file mode 100644 index 00000000..42912f47 --- /dev/null +++ b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/pt.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c6da6c71212fe363b77717b1696132048f71aafdf237b113d8e56a67a81bfa2e +size 3389 diff --git a/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/ru.json b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/ru.json new file mode 100644 index 00000000..46ae217c --- /dev/null +++ b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/ru.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:01b58ca518b0c2c798dba443259d84b02c10b2f22c6b74c3472d6e81bd3ba5ce +size 4602 diff --git a/StreamDock-Plugin-SDK/SDNodeJsSDK/unity-communication-plugin/propertyInspector/utils/action.js b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/action.js similarity index 85% rename from StreamDock-Plugin-SDK/SDNodeJsSDK/unity-communication-plugin/propertyInspector/utils/action.js rename to StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/action.js index 76798ecb..8f0e0c12 100644 --- a/StreamDock-Plugin-SDK/SDNodeJsSDK/unity-communication-plugin/propertyInspector/utils/action.js +++ b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/action.js @@ -9,7 +9,7 @@ * ===== CJHONG ========================================== 2023.10.10 =====> */ -let $websocket, $uuid, $action, $context, $settings, $lang, $FileID = ''; +let $websocket, $uuid, $action, $context, $settings, $lang; // 与插件通信 WebSocket.prototype.sendToPlugin = function (payload) { @@ -19,7 +19,7 @@ WebSocket.prototype.sendToPlugin = function (payload) { context: $uuid, payload })); -}; +} // 设置状态 WebSocket.prototype.setState = function (state) { @@ -28,7 +28,7 @@ WebSocket.prototype.setState = function (state) { context: $context, payload: { state } })); -}; +} // 设置背景 WebSocket.prototype.setImage = function (url) { @@ -49,7 +49,7 @@ WebSocket.prototype.setImage = function (url) { } })); }; -}; +} // 打开网页 WebSocket.prototype.openUrl = function (url) { @@ -57,22 +57,22 @@ WebSocket.prototype.openUrl = function (url) { event: "openUrl", payload: { url } })); -}; +} // 保存持久化数据 WebSocket.prototype.saveData = $.debounce(function (payload) { this.send(JSON.stringify({ event: "setSettings", - context: $uuid, + context: $context, payload - })); -}); + })) +}, 0) // StreamDock 软件入口函数 +const connectSocket = connectElgatoStreamDeckSocket; async function connectElgatoStreamDeckSocket(port, uuid, event, app, info) { info = JSON.parse(info); - $uuid = uuid; $action = info.action; - $context = info.context; + $uuid = uuid; $action = info.action; $context = info.context; $websocket = new WebSocket('ws://127.0.0.1:' + port); $websocket.onopen = () => $websocket.send(JSON.stringify({ event, uuid })); @@ -102,30 +102,33 @@ async function connectElgatoStreamDeckSocket(port, uuid, event, app, info) { req.send(); req.onreadystatechange = () => { if (req.readyState === 4) { - resolve(JSON.parse(req.responseText).Localization); + // console.log(req.responseText); + resolve(JSON.parse(req.responseText).Localization) } }; - }); + }) // 遍历文本节点并翻译所有文本节点 const walker = document.createTreeWalker($dom.main, NodeFilter.SHOW_TEXT, (e) => { - return e.data.trim() && NodeFilter.FILTER_ACCEPT; + return e.data.trim() && NodeFilter.FILTER_ACCEPT }); while (walker.nextNode()) { console.log(walker.currentNode.data); - walker.currentNode.data = $lang[walker.currentNode.data]; + walker.currentNode.data = $lang[walker.currentNode.data] } // placeholder 特殊处理 const translate = item => { if (item.placeholder?.trim()) { console.log(item.placeholder); - item.placeholder = $lang[item.placeholder]; + item.placeholder = $lang[item.placeholder] } - }; - $('input', true).forEach(translate); - $('textarea', true).forEach(translate); + } + $('input', true).forEach(translate) + $('textarea', true).forEach(translate) } // StreamDock 文件路径回调 -Array.from($('input[type="file"]', true)).forEach(item => item.addEventListener('click', () => $FileID = item.id)); +let $FileID = ''; Array.from($('input[type="file"]', true)).forEach(item => { + item.addEventListener('click', () => $FileID = item.id); +}); const onFilePickerReturn = (url) => $emit.send(`File-${$FileID}`, JSON.parse(url)); \ No newline at end of file diff --git a/StreamDock-Plugin-SDK/SDNodeJsSDK/unity-communication-plugin/static/css/caret.svg b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/css/caret.svg similarity index 100% rename from StreamDock-Plugin-SDK/SDNodeJsSDK/unity-communication-plugin/static/css/caret.svg rename to StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/css/caret.svg diff --git a/StreamDock-Plugin-SDK/SDNodeJsSDK/unity-communication-plugin/static/css/check.png b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/css/check.png similarity index 100% rename from StreamDock-Plugin-SDK/SDNodeJsSDK/unity-communication-plugin/static/css/check.png rename to StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/css/check.png diff --git a/StreamDock-Plugin-SDK/SDNodeJsSDK/unity-communication-plugin/static/css/check.svg b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/css/check.svg similarity index 100% rename from StreamDock-Plugin-SDK/SDNodeJsSDK/unity-communication-plugin/static/css/check.svg rename to StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/css/check.svg diff --git a/StreamDock-Plugin-SDK/SDNodeJsSDK/unity-communication-plugin/static/css/elg_calendar.svg b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/css/elg_calendar.svg similarity index 100% rename from StreamDock-Plugin-SDK/SDNodeJsSDK/unity-communication-plugin/static/css/elg_calendar.svg rename to StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/css/elg_calendar.svg diff --git a/StreamDock-Plugin-SDK/SDNodeJsSDK/unity-communication-plugin/static/css/elg_calendar_inv.svg b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/css/elg_calendar_inv.svg similarity index 100% rename from StreamDock-Plugin-SDK/SDNodeJsSDK/unity-communication-plugin/static/css/elg_calendar_inv.svg rename to StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/css/elg_calendar_inv.svg diff --git a/StreamDock-Plugin-SDK/SDNodeJsSDK/unity-communication-plugin/static/css/g_d8d8d8.svg b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/css/g_d8d8d8.svg similarity index 100% rename from StreamDock-Plugin-SDK/SDNodeJsSDK/unity-communication-plugin/static/css/g_d8d8d8.svg rename to StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/css/g_d8d8d8.svg diff --git a/StreamDock-Plugin-SDK/SDNodeJsSDK/unity-communication-plugin/static/css/rcheck.svg b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/css/rcheck.svg similarity index 100% rename from StreamDock-Plugin-SDK/SDNodeJsSDK/unity-communication-plugin/static/css/rcheck.svg rename to StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/css/rcheck.svg diff --git a/StreamDock-Plugin-SDK/SDNodeJsSDK/unity-communication-plugin/static/css/sdpi.css b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/css/sdpi.css similarity index 100% rename from StreamDock-Plugin-SDK/SDNodeJsSDK/unity-communication-plugin/static/css/sdpi.css rename to StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/css/sdpi.css diff --git a/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Luxury/100.png b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Luxury/100.png new file mode 100644 index 00000000..8aae36f3 --- /dev/null +++ b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Luxury/100.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:13f57063c4bf55f5e8633b8d6c19a7dd079a412879c97db342dab779b3373b3e +size 61581 diff --git a/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Luxury/101.png b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Luxury/101.png new file mode 100644 index 00000000..aaed5143 --- /dev/null +++ b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Luxury/101.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ed186e1c83575d4bc04718610b392b8b5389c5ce18048a66a6dd56347b10d8ac +size 97377 diff --git a/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Luxury/104.png b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Luxury/104.png new file mode 100644 index 00000000..e17bd5ff --- /dev/null +++ b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Luxury/104.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:884991b43e99cfee5192c843e333b605aad9cd5ac1c433cd06d82d326f6e5268 +size 67420 diff --git a/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Luxury/150.png b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Luxury/150.png new file mode 100644 index 00000000..f7762191 --- /dev/null +++ b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Luxury/150.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d41f77f3133a8d634673e7439dd5c635072574170b700a65a291d5ca1f6a5c85 +size 95681 diff --git a/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Luxury/151.png b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Luxury/151.png new file mode 100644 index 00000000..7e57d05e --- /dev/null +++ b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Luxury/151.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2dae9178d19f0a3c0608ed82a39fc6b09d3f5cc6f3e0931b258c646d1c22e2dc +size 85640 diff --git a/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Luxury/300.png b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Luxury/300.png new file mode 100644 index 00000000..718c32a6 --- /dev/null +++ b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Luxury/300.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2b74aca7eff2532f91dec3a45bf1f9acc3bfc1374d409ddb4402ac50e3faa465 +size 102388 diff --git a/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Luxury/302.png b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Luxury/302.png new file mode 100644 index 00000000..b51c06b0 --- /dev/null +++ b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Luxury/302.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b6341c184afba1b4689bb4fc0184f3b41feef7712002b6e7c06f0b8c46609855 +size 84366 diff --git a/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Luxury/304.png b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Luxury/304.png new file mode 100644 index 00000000..f07005b6 --- /dev/null +++ b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Luxury/304.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fb6d24bb94c506c5722b2d78c987da594879ae054cc9fbaec1c7ae4c4308ecc4 +size 86733 diff --git a/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Luxury/305.png b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Luxury/305.png new file mode 100644 index 00000000..12a4c010 --- /dev/null +++ b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Luxury/305.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8d6c6cc25c078abe676a9268bd1860fa47a5ec7f18300fdc73127cd25021dbf9 +size 73853 diff --git a/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Luxury/306.png b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Luxury/306.png new file mode 100644 index 00000000..7e69964e --- /dev/null +++ b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Luxury/306.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d83440b3cf09336168ce1222ae53153683ec78c29ba2658b2aa40db15c19d837 +size 75712 diff --git a/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Luxury/307.png b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Luxury/307.png new file mode 100644 index 00000000..6a814369 --- /dev/null +++ b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Luxury/307.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cfa812f2019ea19747450c6af2977d625103f6ae7ef30af73ae467632864271b +size 84181 diff --git a/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Luxury/310.png b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Luxury/310.png new file mode 100644 index 00000000..b7e336e9 --- /dev/null +++ b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Luxury/310.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6340f02c8ccd13b455ee6193d3b50965cf8c302936df46d889f2d3a370e86f02 +size 76446 diff --git a/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Luxury/311.png b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Luxury/311.png new file mode 100644 index 00000000..aff4d609 --- /dev/null +++ b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Luxury/311.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:171d90bc2cfb3e808ccf321a591836c7a7200000e2a80dec0c0a28c2e40dafea +size 79421 diff --git a/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Luxury/312.png b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Luxury/312.png new file mode 100644 index 00000000..2aa2a710 --- /dev/null +++ b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Luxury/312.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ad072421d81774a1034b998d3f42e5079874ef4dbf149736bed2ac78a172628a +size 81068 diff --git a/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Luxury/313.png b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Luxury/313.png new file mode 100644 index 00000000..7c3f0b5d --- /dev/null +++ b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Luxury/313.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:88811dadfcef0a32b02e2a7c3c0a0004cbf3939409aeff4a46a192d6198d1b48 +size 86323 diff --git a/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Luxury/350.png b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Luxury/350.png new file mode 100644 index 00000000..17b6fcf9 --- /dev/null +++ b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Luxury/350.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2a4459c7dc2b5fce6288b993f7457df05e25d3ca6967a2aec5e5604aa20ede54 +size 77787 diff --git a/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Luxury/400.png b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Luxury/400.png new file mode 100644 index 00000000..82a285eb --- /dev/null +++ b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Luxury/400.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:611b2da5103b27edcd817e7d4fc5db74e95e5833db8568276e6791aad050dc3e +size 17045 diff --git a/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Luxury/401.png b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Luxury/401.png new file mode 100644 index 00000000..3336c3ba --- /dev/null +++ b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Luxury/401.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:773ede1a49a32718907e7388d03f694006862fb535a094819c8818a64cd117c7 +size 22254 diff --git a/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Luxury/402.png b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Luxury/402.png new file mode 100644 index 00000000..eb2ddbfe --- /dev/null +++ b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Luxury/402.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2d651f0ac23990731141242e2ade187047e6aa59944207fe6adfca5683fc4e20 +size 39741 diff --git a/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Luxury/403.png b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Luxury/403.png new file mode 100644 index 00000000..afc7745b --- /dev/null +++ b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Luxury/403.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:56ce25407db7a71190a39de5b6617c9dc219a14a69ea18e11ebc8183544cbb04 +size 44616 diff --git a/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Luxury/404.png b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Luxury/404.png new file mode 100644 index 00000000..5b8cec67 --- /dev/null +++ b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Luxury/404.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a3fb93d30f17a1504cb26090761afd4ccfbed3a56b2d6565891d37685836b948 +size 86707 diff --git a/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Luxury/407.png b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Luxury/407.png new file mode 100644 index 00000000..2353823c --- /dev/null +++ b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Luxury/407.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:24b84fd4302861227e97f68f6de110b09e0c9b43cecf0ef2f4f610a4b5d4c91a +size 109537 diff --git a/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Luxury/457.png b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Luxury/457.png new file mode 100644 index 00000000..6e02ead6 --- /dev/null +++ b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Luxury/457.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1701a8abe88847d9ac212f115c326c1b38b69bf720130740357923e80534064f +size 78870 diff --git a/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Luxury/500.png b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Luxury/500.png new file mode 100644 index 00000000..7e23267d --- /dev/null +++ b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Luxury/500.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8500ee127f3d2d9405a4990096d8311220822a2ca783810aa94c4d52e5ce92fc +size 85416 diff --git a/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Luxury/502.png b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Luxury/502.png new file mode 100644 index 00000000..379240db --- /dev/null +++ b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Luxury/502.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e13e70446e187cd2fabab225dec0a805315e636d6756b392454c475caa63ad8b +size 133931 diff --git a/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Luxury/503.png b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Luxury/503.png new file mode 100644 index 00000000..6400f827 --- /dev/null +++ b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Luxury/503.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f3c19717ca776e8680b7e0d1a02cd35fc9d35424a2cbd77dc439eb4f556aa6fc +size 144702 diff --git a/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Luxury/508.png b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Luxury/508.png new file mode 100644 index 00000000..875630ea --- /dev/null +++ b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Luxury/508.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:506cce5b1baefd0be9a918e63846708cedc1ee71dabdd1fa31314a5e1e4bd424 +size 95801 diff --git a/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/100-fill.svg b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/100-fill.svg new file mode 100644 index 00000000..4ecf5205 --- /dev/null +++ b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/100-fill.svg @@ -0,0 +1,18 @@ + + + + + + diff --git a/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/101-fill.svg b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/101-fill.svg new file mode 100644 index 00000000..ae2f7771 --- /dev/null +++ b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/101-fill.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + diff --git a/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/102-fill.svg b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/102-fill.svg new file mode 100644 index 00000000..ee145ccf --- /dev/null +++ b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/102-fill.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + diff --git a/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/103-fill.svg b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/103-fill.svg new file mode 100644 index 00000000..6b373d98 --- /dev/null +++ b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/103-fill.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + diff --git a/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/104-fill.svg b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/104-fill.svg new file mode 100644 index 00000000..f8985d79 --- /dev/null +++ b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/104-fill.svg @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/150-fill.svg b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/150-fill.svg new file mode 100644 index 00000000..0c3add1d --- /dev/null +++ b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/150-fill.svg @@ -0,0 +1,10 @@ + + + + + + diff --git a/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/151-fill.svg b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/151-fill.svg new file mode 100644 index 00000000..0b1f2069 --- /dev/null +++ b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/151-fill.svg @@ -0,0 +1,16 @@ + + + + + + + diff --git a/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/152-fill.svg b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/152-fill.svg new file mode 100644 index 00000000..40f61588 --- /dev/null +++ b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/152-fill.svg @@ -0,0 +1,17 @@ + + + + + + + diff --git a/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/153-fill.svg b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/153-fill.svg new file mode 100644 index 00000000..bd2947c5 --- /dev/null +++ b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/153-fill.svg @@ -0,0 +1,23 @@ + + + + + + + diff --git a/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/154-fill.svg b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/154-fill.svg new file mode 100644 index 00000000..bd2947c5 --- /dev/null +++ b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/154-fill.svg @@ -0,0 +1,23 @@ + + + + + + + diff --git a/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/300-fill.svg b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/300-fill.svg new file mode 100644 index 00000000..33ce789c --- /dev/null +++ b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/300-fill.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + diff --git a/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/301-fill.svg b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/301-fill.svg new file mode 100644 index 00000000..609e00dc --- /dev/null +++ b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/301-fill.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + diff --git a/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/302-fill.svg b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/302-fill.svg new file mode 100644 index 00000000..3981363a --- /dev/null +++ b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/302-fill.svg @@ -0,0 +1,16 @@ + + + + + + + + + diff --git a/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/303-fill.svg b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/303-fill.svg new file mode 100644 index 00000000..6924d362 --- /dev/null +++ b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/303-fill.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + diff --git a/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/304-fill.svg b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/304-fill.svg new file mode 100644 index 00000000..285b2865 --- /dev/null +++ b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/304-fill.svg @@ -0,0 +1,17 @@ + + + + + + + + + diff --git a/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/305-fill.svg b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/305-fill.svg new file mode 100644 index 00000000..6a6773ac --- /dev/null +++ b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/305-fill.svg @@ -0,0 +1,13 @@ + + + + + + + + diff --git a/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/306-fill.svg b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/306-fill.svg new file mode 100644 index 00000000..a5af7f9b --- /dev/null +++ b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/306-fill.svg @@ -0,0 +1,16 @@ + + + + + + + + + diff --git a/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/307-fill.svg b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/307-fill.svg new file mode 100644 index 00000000..ffe6a994 --- /dev/null +++ b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/307-fill.svg @@ -0,0 +1,16 @@ + + + + + + + + + + diff --git a/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/308-fill.svg b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/308-fill.svg new file mode 100644 index 00000000..22e681a1 --- /dev/null +++ b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/308-fill.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + diff --git a/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/309-fill.svg b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/309-fill.svg new file mode 100644 index 00000000..9909937a --- /dev/null +++ b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/309-fill.svg @@ -0,0 +1,17 @@ + + + + + + + + + diff --git a/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/310-fill.svg b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/310-fill.svg new file mode 100644 index 00000000..bf8ba2a5 --- /dev/null +++ b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/310-fill.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + diff --git a/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/311-fill.svg b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/311-fill.svg new file mode 100644 index 00000000..a0a82d34 --- /dev/null +++ b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/311-fill.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + diff --git a/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/312-fill.svg b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/312-fill.svg new file mode 100644 index 00000000..df66a697 --- /dev/null +++ b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/312-fill.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + diff --git a/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/313-fill.svg b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/313-fill.svg new file mode 100644 index 00000000..fe30b9f8 --- /dev/null +++ b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/313-fill.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + diff --git a/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/314-fill.svg b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/314-fill.svg new file mode 100644 index 00000000..fb8945f3 --- /dev/null +++ b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/314-fill.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + diff --git a/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/315-fill.svg b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/315-fill.svg new file mode 100644 index 00000000..3fac3b49 --- /dev/null +++ b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/315-fill.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + diff --git a/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/316-fill.svg b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/316-fill.svg new file mode 100644 index 00000000..ac897494 --- /dev/null +++ b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/316-fill.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + diff --git a/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/317-fill.svg b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/317-fill.svg new file mode 100644 index 00000000..c4360634 --- /dev/null +++ b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/317-fill.svg @@ -0,0 +1,19 @@ + + + + + + + diff --git a/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/318-fill.svg b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/318-fill.svg new file mode 100644 index 00000000..f3a29ee2 --- /dev/null +++ b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/318-fill.svg @@ -0,0 +1,21 @@ + + + + + + + diff --git a/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/350-fill.svg b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/350-fill.svg new file mode 100644 index 00000000..628a9769 --- /dev/null +++ b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/350-fill.svg @@ -0,0 +1,19 @@ + + + + + + + + + diff --git a/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/351-fill.svg b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/351-fill.svg new file mode 100644 index 00000000..e20a2e78 --- /dev/null +++ b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/351-fill.svg @@ -0,0 +1,22 @@ + + + + + + + + + + diff --git a/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/399-fill.svg b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/399-fill.svg new file mode 100644 index 00000000..a8f0256d --- /dev/null +++ b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/399-fill.svg @@ -0,0 +1,12 @@ + + + + + + + diff --git a/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/400-fill.svg b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/400-fill.svg new file mode 100644 index 00000000..d943de6e --- /dev/null +++ b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/400-fill.svg @@ -0,0 +1,19 @@ + + + + + + + + diff --git a/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/401-fill.svg b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/401-fill.svg new file mode 100644 index 00000000..06cbfc5b --- /dev/null +++ b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/401-fill.svg @@ -0,0 +1,23 @@ + + + + + + + + + diff --git a/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/402-fill.svg b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/402-fill.svg new file mode 100644 index 00000000..3f136541 --- /dev/null +++ b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/402-fill.svg @@ -0,0 +1,27 @@ + + + + + + + + + + diff --git a/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/403-fill.svg b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/403-fill.svg new file mode 100644 index 00000000..d21cf324 --- /dev/null +++ b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/403-fill.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + diff --git a/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/404-fill.svg b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/404-fill.svg new file mode 100644 index 00000000..4ffecfc5 --- /dev/null +++ b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/404-fill.svg @@ -0,0 +1,22 @@ + + + + + + + + + + diff --git a/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/405-fill.svg b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/405-fill.svg new file mode 100644 index 00000000..ec30051c --- /dev/null +++ b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/405-fill.svg @@ -0,0 +1,22 @@ + + + + + + + + + + diff --git a/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/406-fill.svg b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/406-fill.svg new file mode 100644 index 00000000..823dd957 --- /dev/null +++ b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/406-fill.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + diff --git a/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/407-fill.svg b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/407-fill.svg new file mode 100644 index 00000000..f247ecd1 --- /dev/null +++ b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/407-fill.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + diff --git a/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/408-fill.svg b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/408-fill.svg new file mode 100644 index 00000000..afa27d37 --- /dev/null +++ b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/408-fill.svg @@ -0,0 +1,25 @@ + + + + + + + diff --git a/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/409-fill.svg b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/409-fill.svg new file mode 100644 index 00000000..1886c7f5 --- /dev/null +++ b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/409-fill.svg @@ -0,0 +1,32 @@ + + + + + + + diff --git a/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/410-fill.svg b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/410-fill.svg new file mode 100644 index 00000000..930f0f0a --- /dev/null +++ b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/410-fill.svg @@ -0,0 +1,39 @@ + + + + + + + diff --git a/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/456-fill.svg b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/456-fill.svg new file mode 100644 index 00000000..ae6acc73 --- /dev/null +++ b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/456-fill.svg @@ -0,0 +1,23 @@ + + + + + + + + + diff --git a/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/457-fill.svg b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/457-fill.svg new file mode 100644 index 00000000..9a57ae98 --- /dev/null +++ b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/457-fill.svg @@ -0,0 +1,25 @@ + + + + + + + + + diff --git a/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/499-fill.svg b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/499-fill.svg new file mode 100644 index 00000000..3a29c93d --- /dev/null +++ b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/499-fill.svg @@ -0,0 +1,21 @@ + + + + + + diff --git a/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/500-fill.svg b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/500-fill.svg new file mode 100644 index 00000000..e252deb5 --- /dev/null +++ b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/500-fill.svg @@ -0,0 +1,13 @@ + + + + + + + + diff --git a/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/501-fill.svg b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/501-fill.svg new file mode 100644 index 00000000..7ada5f17 --- /dev/null +++ b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/501-fill.svg @@ -0,0 +1,15 @@ + + + + + + + + + + diff --git a/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/502-fill.svg b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/502-fill.svg new file mode 100644 index 00000000..1b8552e8 --- /dev/null +++ b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/502-fill.svg @@ -0,0 +1,10 @@ + + + + + + diff --git a/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/503-fill.svg b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/503-fill.svg new file mode 100644 index 00000000..b61be997 --- /dev/null +++ b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/503-fill.svg @@ -0,0 +1,19 @@ + + + + + + + diff --git a/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/504-fill.svg b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/504-fill.svg new file mode 100644 index 00000000..f821f7e6 --- /dev/null +++ b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/504-fill.svg @@ -0,0 +1,16 @@ + + + + + + + diff --git a/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/507-fill.svg b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/507-fill.svg new file mode 100644 index 00000000..3110cefa --- /dev/null +++ b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/507-fill.svg @@ -0,0 +1,16 @@ + + + + + + + diff --git a/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/508-fill.svg b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/508-fill.svg new file mode 100644 index 00000000..e16cdcd3 --- /dev/null +++ b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/508-fill.svg @@ -0,0 +1,17 @@ + + + + + + + diff --git a/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/509-fill.svg b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/509-fill.svg new file mode 100644 index 00000000..f273aa4e --- /dev/null +++ b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/509-fill.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + diff --git a/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/510-fill.svg b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/510-fill.svg new file mode 100644 index 00000000..d625a4e9 --- /dev/null +++ b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/510-fill.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + diff --git a/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/511-fill.svg b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/511-fill.svg new file mode 100644 index 00000000..0c59c6af --- /dev/null +++ b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/511-fill.svg @@ -0,0 +1,12 @@ + + + + + + diff --git a/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/512-fill.svg b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/512-fill.svg new file mode 100644 index 00000000..4af2ee3c --- /dev/null +++ b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/512-fill.svg @@ -0,0 +1,13 @@ + + + + + + diff --git a/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/513-fill.svg b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/513-fill.svg new file mode 100644 index 00000000..d8b2ba65 --- /dev/null +++ b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/513-fill.svg @@ -0,0 +1,14 @@ + + + + + + diff --git a/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/514-fill.svg b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/514-fill.svg new file mode 100644 index 00000000..0069b99a --- /dev/null +++ b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/514-fill.svg @@ -0,0 +1,17 @@ + + + + + + + + + + diff --git a/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/515-fill.svg b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/515-fill.svg new file mode 100644 index 00000000..7366b0a5 --- /dev/null +++ b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/515-fill.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + diff --git a/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/900-fill.svg b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/900-fill.svg new file mode 100644 index 00000000..21e59fe5 --- /dev/null +++ b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/900-fill.svg @@ -0,0 +1,14 @@ + + + + + + + diff --git a/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/901-fill.svg b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/901-fill.svg new file mode 100644 index 00000000..f7583ac6 --- /dev/null +++ b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/901-fill.svg @@ -0,0 +1,16 @@ + + + + + + + diff --git a/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/999-fill.svg b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/999-fill.svg new file mode 100644 index 00000000..5e0b56aa --- /dev/null +++ b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/Modern/999-fill.svg @@ -0,0 +1,15 @@ + + + + + + diff --git a/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/cate.png b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/cate.png new file mode 100644 index 00000000..fd015cf2 --- /dev/null +++ b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/cate.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8f0317b9c6394ad3024c842696f8a5a85a5fa620b34479e9323a0d1f45d87b74 +size 4635 diff --git a/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/default.jpg b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/default.jpg new file mode 100644 index 00000000..dff5cb7b --- /dev/null +++ b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/default.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9972b3d10eed2b1a4019efa170c8218fb775a4e8e2a923debca60a80871b2109 +size 5272 diff --git a/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/tm.png b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/tm.png new file mode 100644 index 00000000..4a39e991 --- /dev/null +++ b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/img/tm.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d6f33c67acd6b276e2c3405db8338297ce2b2005d3cc915a0c68a470c82a3021 +size 262913 diff --git a/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/plugin.js b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/plugin.js new file mode 100644 index 00000000..7aee4728 --- /dev/null +++ b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/plugin.js @@ -0,0 +1,151 @@ +/** + * Plugin 2.5.0 新特性 => + * + * 1 => 工具与主文件相分离 - 按需引入 + * 2 => $plugin - 全局插件配置对象 ※ + * 3 => 新增 Action 行动类 默认包含一些方法 + * 4 => 注意事项: 为了避免命名冲突,请勿使用 $ 相关的名称以及JQuery库 + * + * ===== CJHONG ========================================== 2023.10.11 =====> + */ + +let $websocket, $lang; + +class Timer { + constructor(task, interval, immediate = false) { + if (immediate) task() + this.worker = new Worker('../static/utils/worker.js'); + this.worker.postMessage(interval); + this.worker.onmessage = task; + } + stop() { + this.worker.terminate(); + } +} + +class Action { + constructor(data) { + this.data = {} + this.default = {} + Object.assign(this, data) + } + // 初始化数据 + willAppear(data) { + const { context, payload: { settings } } = data + this.data[context] = Object.assign({ ...this.default }, settings) + this._willAppear?.(data) + } + // 行动销毁 + willDisappear(data) { + this._willDisappear?.(data) + delete this.data[data.context]; + } + // 高精度定时器 + interval(task, interval, immediate = false) { + return new Timer(task, interval, immediate) + } +} + +// 打开网页 +WebSocket.prototype.openUrl = function (url) { + this.send(JSON.stringify({ + event: "openUrl", + payload: { url } + })) +} + +// 与属性检查器通信 +WebSocket.prototype.sendToPropertyInspector = function (action, context, payload) { + this.send(JSON.stringify({ + event: "sendToPropertyInspector", + action, context, payload + })) +} + +// 保存持久化数据 +WebSocket.prototype.setSettings = function (context, payload) { + this.send(JSON.stringify({ + event: "setSettings", + context, payload + })) +} + +// 设置背景 +WebSocket.prototype.setImage = function (context, url, isGif = false) { + if (isGif) { + this.send(JSON.stringify({ + event: "setImage", + context, payload: { + target: 0, + image: url + } + })) + } else { + const image = new Image(); + image.src = url; + image.onload = () => { + const canvas = document.createElement("canvas"); + canvas.width = image.naturalWidth; + canvas.height = image.naturalHeight; + const ctx = canvas.getContext("2d"); + ctx.drawImage(image, 0, 0); + this.send(JSON.stringify({ + event: "setImage", + context, payload: { + target: 0, + image: canvas.toDataURL("image/png") + } + })) + } + } +} + +// 设置标题 +WebSocket.prototype.setTitle = function (context, str, row = 0, num = 6) { + let newStr = ''; + if (row) { + let nowRow = 1, strArr = str.split(''); + strArr.forEach((item, index) => { + if (nowRow < row && index >= nowRow * num) { + nowRow++ + newStr += '\n' + } + if (nowRow <= row && index < nowRow * num) { + newStr += item + } + }) + if (strArr.length > row * num) { + newStr = newStr.substring(0, newStr.length - 1) + newStr += '..' + } + } + this.send(JSON.stringify({ + event: "setTitle", + context, payload: { + target: 0, + title: newStr || str + } + })) +} + +// 设置状态 +WebSocket.prototype.setState = function (context, state) { + this.send(JSON.stringify({ + event: "setState", + payload: { state }, + context + })); +} + +// StreamDock 软件入口函数 +const connectSocket = connectElgatoStreamDeckSocket; +async function connectElgatoStreamDeckSocket(port, uuid, event, info) { + $lang = JSON.parse(info).application.language + $websocket = new WebSocket("ws://127.0.0.1:" + port); + $websocket.onopen = () => $websocket.send(JSON.stringify({ uuid, event })) + $websocket.onmessage = e => { + const data = JSON.parse(e.data) + const action = data.action?.split(`com.hotspot.streamdock.${$plugin.name}.`)[1] + $plugin[action]?.[data.event]?.(data) + } +} \ No newline at end of file diff --git a/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/utils/axios.js b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/utils/axios.js new file mode 100644 index 00000000..78aa7b89 --- /dev/null +++ b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/utils/axios.js @@ -0,0 +1 @@ +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).axios=t()}(this,(function(){"use strict";function e(t){return e="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},e(t)}function t(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function n(e,t){for(var n=0;ne.length)&&(t=e.length);for(var n=0,r=new Array(t);n2&&void 0!==arguments[2]?arguments[2]:{},a=i.allOwnKeys,s=void 0!==a&&a;if(null!=t)if("object"!==e(t)&&(t=[t]),p(t))for(r=0,o=t.length;r0;)if(t===(n=r[o]).toLowerCase())return n;return null}var C="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:"undefined"!=typeof window?window:global,N=function(e){return!h(e)&&e!==C};var x,P=(x="undefined"!=typeof Uint8Array&&c(Uint8Array),function(e){return x&&e instanceof x}),k=l("HTMLFormElement"),U=function(e){var t=Object.prototype.hasOwnProperty;return function(e,n){return t.call(e,n)}}(),_=l("RegExp"),F=function(e,t){var n=Object.getOwnPropertyDescriptors(e),r={};T(n,(function(n,o){var i;!1!==(i=t(n,o,e))&&(r[o]=i||n)})),Object.defineProperties(e,r)},B="abcdefghijklmnopqrstuvwxyz",L="0123456789",D={DIGIT:L,ALPHA:B,ALPHA_DIGIT:B+B.toUpperCase()+L};var I=l("AsyncFunction"),q={isArray:p,isArrayBuffer:m,isBuffer:function(e){return null!==e&&!h(e)&&null!==e.constructor&&!h(e.constructor)&&y(e.constructor.isBuffer)&&e.constructor.isBuffer(e)},isFormData:function(e){var t;return e&&("function"==typeof FormData&&e instanceof FormData||y(e.append)&&("formdata"===(t=f(e))||"object"===t&&y(e.toString)&&"[object FormData]"===e.toString()))},isArrayBufferView:function(e){return"undefined"!=typeof ArrayBuffer&&ArrayBuffer.isView?ArrayBuffer.isView(e):e&&e.buffer&&m(e.buffer)},isString:v,isNumber:b,isBoolean:function(e){return!0===e||!1===e},isObject:g,isPlainObject:w,isUndefined:h,isDate:E,isFile:O,isBlob:S,isRegExp:_,isFunction:y,isStream:function(e){return g(e)&&y(e.pipe)},isURLSearchParams:A,isTypedArray:P,isFileList:R,forEach:T,merge:function e(){for(var t=N(this)&&this||{},n=t.caseless,r={},o=function(t,o){var i=n&&j(r,o)||o;w(r[i])&&w(t)?r[i]=e(r[i],t):w(t)?r[i]=e({},t):p(t)?r[i]=t.slice():r[i]=t},i=0,a=arguments.length;i3&&void 0!==arguments[3]?arguments[3]:{},o=r.allOwnKeys;return T(t,(function(t,r){n&&y(t)?e[r]=a(t,n):e[r]=t}),{allOwnKeys:o}),e},trim:function(e){return e.trim?e.trim():e.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,"")},stripBOM:function(e){return 65279===e.charCodeAt(0)&&(e=e.slice(1)),e},inherits:function(e,t,n,r){e.prototype=Object.create(t.prototype,r),e.prototype.constructor=e,Object.defineProperty(e,"super",{value:t.prototype}),n&&Object.assign(e.prototype,n)},toFlatObject:function(e,t,n,r){var o,i,a,s={};if(t=t||{},null==e)return t;do{for(i=(o=Object.getOwnPropertyNames(e)).length;i-- >0;)a=o[i],r&&!r(a,e,t)||s[a]||(t[a]=e[a],s[a]=!0);e=!1!==n&&c(e)}while(e&&(!n||n(e,t))&&e!==Object.prototype);return t},kindOf:f,kindOfTest:l,endsWith:function(e,t,n){e=String(e),(void 0===n||n>e.length)&&(n=e.length),n-=t.length;var r=e.indexOf(t,n);return-1!==r&&r===n},toArray:function(e){if(!e)return null;if(p(e))return e;var t=e.length;if(!b(t))return null;for(var n=new Array(t);t-- >0;)n[t]=e[t];return n},forEachEntry:function(e,t){for(var n,r=(e&&e[Symbol.iterator]).call(e);(n=r.next())&&!n.done;){var o=n.value;t.call(e,o[0],o[1])}},matchAll:function(e,t){for(var n,r=[];null!==(n=e.exec(t));)r.push(n);return r},isHTMLForm:k,hasOwnProperty:U,hasOwnProp:U,reduceDescriptors:F,freezeMethods:function(e){F(e,(function(t,n){if(y(e)&&-1!==["arguments","caller","callee"].indexOf(n))return!1;var r=e[n];y(r)&&(t.enumerable=!1,"writable"in t?t.writable=!1:t.set||(t.set=function(){throw Error("Can not rewrite read-only method '"+n+"'")}))}))},toObjectSet:function(e,t){var n={},r=function(e){e.forEach((function(e){n[e]=!0}))};return p(e)?r(e):r(String(e).split(t)),n},toCamelCase:function(e){return e.toLowerCase().replace(/[-_\s]([a-z\d])(\w*)/g,(function(e,t,n){return t.toUpperCase()+n}))},noop:function(){},toFiniteNumber:function(e,t){return e=+e,Number.isFinite(e)?e:t},findKey:j,global:C,isContextDefined:N,ALPHABET:D,generateString:function(){for(var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:16,t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:D.ALPHA_DIGIT,n="",r=t.length;e--;)n+=t[Math.random()*r|0];return n},isSpecCompliantForm:function(e){return!!(e&&y(e.append)&&"FormData"===e[Symbol.toStringTag]&&e[Symbol.iterator])},toJSONObject:function(e){var t=new Array(10);return function e(n,r){if(g(n)){if(t.indexOf(n)>=0)return;if(!("toJSON"in n)){t[r]=n;var o=p(n)?[]:{};return T(n,(function(t,n){var i=e(t,r+1);!h(i)&&(o[n]=i)})),t[r]=void 0,o}}return n}(e,0)},isAsyncFn:I,isThenable:function(e){return e&&(g(e)||y(e))&&y(e.then)&&y(e.catch)}};function M(e,t,n,r,o){Error.call(this),Error.captureStackTrace?Error.captureStackTrace(this,this.constructor):this.stack=(new Error).stack,this.message=e,this.name="AxiosError",t&&(this.code=t),n&&(this.config=n),r&&(this.request=r),o&&(this.response=o)}q.inherits(M,Error,{toJSON:function(){return{message:this.message,name:this.name,description:this.description,number:this.number,fileName:this.fileName,lineNumber:this.lineNumber,columnNumber:this.columnNumber,stack:this.stack,config:q.toJSONObject(this.config),code:this.code,status:this.response&&this.response.status?this.response.status:null}}});var z=M.prototype,H={};["ERR_BAD_OPTION_VALUE","ERR_BAD_OPTION","ECONNABORTED","ETIMEDOUT","ERR_NETWORK","ERR_FR_TOO_MANY_REDIRECTS","ERR_DEPRECATED","ERR_BAD_RESPONSE","ERR_BAD_REQUEST","ERR_CANCELED","ERR_NOT_SUPPORT","ERR_INVALID_URL"].forEach((function(e){H[e]={value:e}})),Object.defineProperties(M,H),Object.defineProperty(z,"isAxiosError",{value:!0}),M.from=function(e,t,n,r,o,i){var a=Object.create(z);return q.toFlatObject(e,a,(function(e){return e!==Error.prototype}),(function(e){return"isAxiosError"!==e})),M.call(a,e.message,t,n,r,o),a.cause=e,a.name=e.name,i&&Object.assign(a,i),a};function J(e){return q.isPlainObject(e)||q.isArray(e)}function W(e){return q.endsWith(e,"[]")?e.slice(0,-2):e}function K(e,t,n){return e?e.concat(t).map((function(e,t){return e=W(e),!n&&t?"["+e+"]":e})).join(n?".":""):t}var V=q.toFlatObject(q,{},null,(function(e){return/^is[A-Z]/.test(e)}));function G(t,n,r){if(!q.isObject(t))throw new TypeError("target must be an object");n=n||new FormData;var o=(r=q.toFlatObject(r,{metaTokens:!0,dots:!1,indexes:!1},!1,(function(e,t){return!q.isUndefined(t[e])}))).metaTokens,i=r.visitor||f,a=r.dots,s=r.indexes,u=(r.Blob||"undefined"!=typeof Blob&&Blob)&&q.isSpecCompliantForm(n);if(!q.isFunction(i))throw new TypeError("visitor must be a function");function c(e){if(null===e)return"";if(q.isDate(e))return e.toISOString();if(!u&&q.isBlob(e))throw new M("Blob is not supported. Use a Buffer instead.");return q.isArrayBuffer(e)||q.isTypedArray(e)?u&&"function"==typeof Blob?new Blob([e]):Buffer.from(e):e}function f(t,r,i){var u=t;if(t&&!i&&"object"===e(t))if(q.endsWith(r,"{}"))r=o?r:r.slice(0,-2),t=JSON.stringify(t);else if(q.isArray(t)&&function(e){return q.isArray(e)&&!e.some(J)}(t)||(q.isFileList(t)||q.endsWith(r,"[]"))&&(u=q.toArray(t)))return r=W(r),u.forEach((function(e,t){!q.isUndefined(e)&&null!==e&&n.append(!0===s?K([r],t,a):null===s?r:r+"[]",c(e))})),!1;return!!J(t)||(n.append(K(i,r,a),c(t)),!1)}var l=[],d=Object.assign(V,{defaultVisitor:f,convertValue:c,isVisitable:J});if(!q.isObject(t))throw new TypeError("data must be an object");return function e(t,r){if(!q.isUndefined(t)){if(-1!==l.indexOf(t))throw Error("Circular reference detected in "+r.join("."));l.push(t),q.forEach(t,(function(t,o){!0===(!(q.isUndefined(t)||null===t)&&i.call(n,t,q.isString(o)?o.trim():o,r,d))&&e(t,r?r.concat(o):[o])})),l.pop()}}(t),n}function $(e){var t={"!":"%21","'":"%27","(":"%28",")":"%29","~":"%7E","%20":"+","%00":"\0"};return encodeURIComponent(e).replace(/[!'()~]|%20|%00/g,(function(e){return t[e]}))}function X(e,t){this._pairs=[],e&&G(e,this,t)}var Q=X.prototype;function Z(e){return encodeURIComponent(e).replace(/%3A/gi,":").replace(/%24/g,"$").replace(/%2C/gi,",").replace(/%20/g,"+").replace(/%5B/gi,"[").replace(/%5D/gi,"]")}function Y(e,t,n){if(!t)return e;var r,o=n&&n.encode||Z,i=n&&n.serialize;if(r=i?i(t,n):q.isURLSearchParams(t)?t.toString():new X(t,n).toString(o)){var a=e.indexOf("#");-1!==a&&(e=e.slice(0,a)),e+=(-1===e.indexOf("?")?"?":"&")+r}return e}Q.append=function(e,t){this._pairs.push([e,t])},Q.toString=function(e){var t=e?function(t){return e.call(this,t,$)}:$;return this._pairs.map((function(e){return t(e[0])+"="+t(e[1])}),"").join("&")};var ee,te=function(){function e(){t(this,e),this.handlers=[]}return r(e,[{key:"use",value:function(e,t,n){return this.handlers.push({fulfilled:e,rejected:t,synchronous:!!n&&n.synchronous,runWhen:n?n.runWhen:null}),this.handlers.length-1}},{key:"eject",value:function(e){this.handlers[e]&&(this.handlers[e]=null)}},{key:"clear",value:function(){this.handlers&&(this.handlers=[])}},{key:"forEach",value:function(e){q.forEach(this.handlers,(function(t){null!==t&&e(t)}))}}]),e}(),ne={silentJSONParsing:!0,forcedJSONParsing:!0,clarifyTimeoutError:!1},re={isBrowser:!0,classes:{URLSearchParams:"undefined"!=typeof URLSearchParams?URLSearchParams:X,FormData:"undefined"!=typeof FormData?FormData:null,Blob:"undefined"!=typeof Blob?Blob:null},isStandardBrowserEnv:("undefined"==typeof navigator||"ReactNative"!==(ee=navigator.product)&&"NativeScript"!==ee&&"NS"!==ee)&&"undefined"!=typeof window&&"undefined"!=typeof document,isStandardBrowserWebWorkerEnv:"undefined"!=typeof WorkerGlobalScope&&self instanceof WorkerGlobalScope&&"function"==typeof self.importScripts,protocols:["http","https","file","blob","url","data"]};function oe(e){function t(e,n,r,o){var i=e[o++],a=Number.isFinite(+i),s=o>=e.length;return i=!i&&q.isArray(r)?r.length:i,s?(q.hasOwnProp(r,i)?r[i]=[r[i],n]:r[i]=n,!a):(r[i]&&q.isObject(r[i])||(r[i]=[]),t(e,n,r[i],o)&&q.isArray(r[i])&&(r[i]=function(e){var t,n,r={},o=Object.keys(e),i=o.length;for(t=0;t-1,i=q.isObject(e);if(i&&q.isHTMLForm(e)&&(e=new FormData(e)),q.isFormData(e))return o&&o?JSON.stringify(oe(e)):e;if(q.isArrayBuffer(e)||q.isBuffer(e)||q.isStream(e)||q.isFile(e)||q.isBlob(e))return e;if(q.isArrayBufferView(e))return e.buffer;if(q.isURLSearchParams(e))return t.setContentType("application/x-www-form-urlencoded;charset=utf-8",!1),e.toString();if(i){if(r.indexOf("application/x-www-form-urlencoded")>-1)return function(e,t){return G(e,new re.classes.URLSearchParams,Object.assign({visitor:function(e,t,n,r){return re.isNode&&q.isBuffer(e)?(this.append(t,e.toString("base64")),!1):r.defaultVisitor.apply(this,arguments)}},t))}(e,this.formSerializer).toString();if((n=q.isFileList(e))||r.indexOf("multipart/form-data")>-1){var a=this.env&&this.env.FormData;return G(n?{"files[]":e}:e,a&&new a,this.formSerializer)}}return i||o?(t.setContentType("application/json",!1),function(e,t,n){if(q.isString(e))try{return(t||JSON.parse)(e),q.trim(e)}catch(e){if("SyntaxError"!==e.name)throw e}return(n||JSON.stringify)(e)}(e)):e}],transformResponse:[function(e){var t=this.transitional||ie.transitional,n=t&&t.forcedJSONParsing,r="json"===this.responseType;if(e&&q.isString(e)&&(n&&!this.responseType||r)){var o=!(t&&t.silentJSONParsing)&&r;try{return JSON.parse(e)}catch(e){if(o){if("SyntaxError"===e.name)throw M.from(e,M.ERR_BAD_RESPONSE,this,null,this.response);throw e}}}return e}],timeout:0,xsrfCookieName:"XSRF-TOKEN",xsrfHeaderName:"X-XSRF-TOKEN",maxContentLength:-1,maxBodyLength:-1,env:{FormData:re.classes.FormData,Blob:re.classes.Blob},validateStatus:function(e){return e>=200&&e<300},headers:{common:{Accept:"application/json, text/plain, */*","Content-Type":void 0}}};q.forEach(["delete","get","head","post","put","patch"],(function(e){ie.headers[e]={}}));var ae=ie,se=q.toObjectSet(["age","authorization","content-length","content-type","etag","expires","from","host","if-modified-since","if-unmodified-since","last-modified","location","max-forwards","proxy-authorization","referer","retry-after","user-agent"]),ue=Symbol("internals");function ce(e){return e&&String(e).trim().toLowerCase()}function fe(e){return!1===e||null==e?e:q.isArray(e)?e.map(fe):String(e)}function le(e,t,n,r,o){return q.isFunction(r)?r.call(this,t,n):(o&&(t=n),q.isString(t)?q.isString(r)?-1!==t.indexOf(r):q.isRegExp(r)?r.test(t):void 0:void 0)}var de=function(e,n){function i(e){t(this,i),e&&this.set(e)}return r(i,[{key:"set",value:function(e,t,n){var r=this;function o(e,t,n){var o=ce(t);if(!o)throw new Error("header name must be a non-empty string");var i=q.findKey(r,o);(!i||void 0===r[i]||!0===n||void 0===n&&!1!==r[i])&&(r[i||t]=fe(e))}var i,a,s,u,c,f=function(e,t){return q.forEach(e,(function(e,n){return o(e,n,t)}))};return q.isPlainObject(e)||e instanceof this.constructor?f(e,t):q.isString(e)&&(e=e.trim())&&!/^[-_a-zA-Z0-9^`|~,!#$%&'*+.]+$/.test(e.trim())?f((c={},(i=e)&&i.split("\n").forEach((function(e){u=e.indexOf(":"),a=e.substring(0,u).trim().toLowerCase(),s=e.substring(u+1).trim(),!a||c[a]&&se[a]||("set-cookie"===a?c[a]?c[a].push(s):c[a]=[s]:c[a]=c[a]?c[a]+", "+s:s)})),c),t):null!=e&&o(t,e,n),this}},{key:"get",value:function(e,t){if(e=ce(e)){var n=q.findKey(this,e);if(n){var r=this[n];if(!t)return r;if(!0===t)return function(e){for(var t,n=Object.create(null),r=/([^\s,;=]+)\s*(?:=\s*([^,;]+))?/g;t=r.exec(e);)n[t[1]]=t[2];return n}(r);if(q.isFunction(t))return t.call(this,r,n);if(q.isRegExp(t))return t.exec(r);throw new TypeError("parser must be boolean|regexp|function")}}}},{key:"has",value:function(e,t){if(e=ce(e)){var n=q.findKey(this,e);return!(!n||void 0===this[n]||t&&!le(0,this[n],n,t))}return!1}},{key:"delete",value:function(e,t){var n=this,r=!1;function o(e){if(e=ce(e)){var o=q.findKey(n,e);!o||t&&!le(0,n[o],o,t)||(delete n[o],r=!0)}}return q.isArray(e)?e.forEach(o):o(e),r}},{key:"clear",value:function(e){for(var t=Object.keys(this),n=t.length,r=!1;n--;){var o=t[n];e&&!le(0,this[o],o,e,!0)||(delete this[o],r=!0)}return r}},{key:"normalize",value:function(e){var t=this,n={};return q.forEach(this,(function(r,o){var i=q.findKey(n,o);if(i)return t[i]=fe(r),void delete t[o];var a=e?function(e){return e.trim().toLowerCase().replace(/([a-z\d])(\w*)/g,(function(e,t,n){return t.toUpperCase()+n}))}(o):String(o).trim();a!==o&&delete t[o],t[a]=fe(r),n[a]=!0})),this}},{key:"concat",value:function(){for(var e,t=arguments.length,n=new Array(t),r=0;r1?n-1:0),o=1;o1?"since :\n"+u.map(Oe).join("\n"):" "+Oe(u[0]):"as no adapter specified"),"ERR_NOT_SUPPORT")}return n};function Ae(e){if(e.cancelToken&&e.cancelToken.throwIfRequested(),e.signal&&e.signal.aborted)throw new ve(null,e)}function Te(e){return Ae(e),e.headers=pe.from(e.headers),e.data=he.call(e,e.transformRequest),-1!==["post","put","patch"].indexOf(e.method)&&e.headers.setContentType("application/x-www-form-urlencoded",!1),Re(e.adapter||ae.adapter)(e).then((function(t){return Ae(e),t.data=he.call(e,e.transformResponse,t),t.headers=pe.from(t.headers),t}),(function(t){return me(t)||(Ae(e),t&&t.response&&(t.response.data=he.call(e,e.transformResponse,t.response),t.response.headers=pe.from(t.response.headers))),Promise.reject(t)}))}var je=function(e){return e instanceof pe?e.toJSON():e};function Ce(e,t){t=t||{};var n={};function r(e,t,n){return q.isPlainObject(e)&&q.isPlainObject(t)?q.merge.call({caseless:n},e,t):q.isPlainObject(t)?q.merge({},t):q.isArray(t)?t.slice():t}function o(e,t,n){return q.isUndefined(t)?q.isUndefined(e)?void 0:r(void 0,e,n):r(e,t,n)}function i(e,t){if(!q.isUndefined(t))return r(void 0,t)}function a(e,t){return q.isUndefined(t)?q.isUndefined(e)?void 0:r(void 0,e):r(void 0,t)}function s(n,o,i){return i in t?r(n,o):i in e?r(void 0,n):void 0}var u={url:i,method:i,data:i,baseURL:a,transformRequest:a,transformResponse:a,paramsSerializer:a,timeout:a,timeoutMessage:a,withCredentials:a,adapter:a,responseType:a,xsrfCookieName:a,xsrfHeaderName:a,onUploadProgress:a,onDownloadProgress:a,decompress:a,maxContentLength:a,maxBodyLength:a,beforeRedirect:a,transport:a,httpAgent:a,httpsAgent:a,cancelToken:a,socketPath:a,responseEncoding:a,validateStatus:s,headers:function(e,t){return o(je(e),je(t),!0)}};return q.forEach(Object.keys(Object.assign({},e,t)),(function(r){var i=u[r]||o,a=i(e[r],t[r],r);q.isUndefined(a)&&i!==s||(n[r]=a)})),n}var Ne="1.5.1",xe={};["object","boolean","number","function","string","symbol"].forEach((function(t,n){xe[t]=function(r){return e(r)===t||"a"+(n<1?"n ":" ")+t}}));var Pe={};xe.transitional=function(e,t,n){function r(e,t){return"[Axios v1.5.1] Transitional option '"+e+"'"+t+(n?". "+n:"")}return function(n,o,i){if(!1===e)throw new M(r(o," has been removed"+(t?" in "+t:"")),M.ERR_DEPRECATED);return t&&!Pe[o]&&(Pe[o]=!0,console.warn(r(o," has been deprecated since v"+t+" and will be removed in the near future"))),!e||e(n,o,i)}};var ke={assertOptions:function(t,n,r){if("object"!==e(t))throw new M("options must be an object",M.ERR_BAD_OPTION_VALUE);for(var o=Object.keys(t),i=o.length;i-- >0;){var a=o[i],s=n[a];if(s){var u=t[a],c=void 0===u||s(u,a,t);if(!0!==c)throw new M("option "+a+" must be "+c,M.ERR_BAD_OPTION_VALUE)}else if(!0!==r)throw new M("Unknown option "+a,M.ERR_BAD_OPTION)}},validators:xe},Ue=ke.validators,_e=function(){function e(n){t(this,e),this.defaults=n,this.interceptors={request:new te,response:new te}}return r(e,[{key:"request",value:function(e,t){"string"==typeof e?(t=t||{}).url=e:t=e||{};var n=t=Ce(this.defaults,t),r=n.transitional,o=n.paramsSerializer,i=n.headers;void 0!==r&&ke.assertOptions(r,{silentJSONParsing:Ue.transitional(Ue.boolean),forcedJSONParsing:Ue.transitional(Ue.boolean),clarifyTimeoutError:Ue.transitional(Ue.boolean)},!1),null!=o&&(q.isFunction(o)?t.paramsSerializer={serialize:o}:ke.assertOptions(o,{encode:Ue.function,serialize:Ue.function},!0)),t.method=(t.method||this.defaults.method||"get").toLowerCase();var a=i&&q.merge(i.common,i[t.method]);i&&q.forEach(["delete","get","head","post","put","patch","common"],(function(e){delete i[e]})),t.headers=pe.concat(a,i);var s=[],u=!0;this.interceptors.request.forEach((function(e){"function"==typeof e.runWhen&&!1===e.runWhen(t)||(u=u&&e.synchronous,s.unshift(e.fulfilled,e.rejected))}));var c,f=[];this.interceptors.response.forEach((function(e){f.push(e.fulfilled,e.rejected)}));var l,d=0;if(!u){var p=[Te.bind(this),void 0];for(p.unshift.apply(p,s),p.push.apply(p,f),l=p.length,c=Promise.resolve(t);d0;)o._listeners[t](e);o._listeners=null}})),this.promise.then=function(e){var t,n=new Promise((function(e){o.subscribe(e),t=e})).then(e);return n.cancel=function(){o.unsubscribe(t)},n},n((function(e,t,n){o.reason||(o.reason=new ve(e,t,n),r(o.reason))}))}return r(e,[{key:"throwIfRequested",value:function(){if(this.reason)throw this.reason}},{key:"subscribe",value:function(e){this.reason?e(this.reason):this._listeners?this._listeners.push(e):this._listeners=[e]}},{key:"unsubscribe",value:function(e){if(this._listeners){var t=this._listeners.indexOf(e);-1!==t&&this._listeners.splice(t,1)}}}],[{key:"source",value:function(){var t;return{token:new e((function(e){t=e})),cancel:t}}}]),e}();var Le={Continue:100,SwitchingProtocols:101,Processing:102,EarlyHints:103,Ok:200,Created:201,Accepted:202,NonAuthoritativeInformation:203,NoContent:204,ResetContent:205,PartialContent:206,MultiStatus:207,AlreadyReported:208,ImUsed:226,MultipleChoices:300,MovedPermanently:301,Found:302,SeeOther:303,NotModified:304,UseProxy:305,Unused:306,TemporaryRedirect:307,PermanentRedirect:308,BadRequest:400,Unauthorized:401,PaymentRequired:402,Forbidden:403,NotFound:404,MethodNotAllowed:405,NotAcceptable:406,ProxyAuthenticationRequired:407,RequestTimeout:408,Conflict:409,Gone:410,LengthRequired:411,PreconditionFailed:412,PayloadTooLarge:413,UriTooLong:414,UnsupportedMediaType:415,RangeNotSatisfiable:416,ExpectationFailed:417,ImATeapot:418,MisdirectedRequest:421,UnprocessableEntity:422,Locked:423,FailedDependency:424,TooEarly:425,UpgradeRequired:426,PreconditionRequired:428,TooManyRequests:429,RequestHeaderFieldsTooLarge:431,UnavailableForLegalReasons:451,InternalServerError:500,NotImplemented:501,BadGateway:502,ServiceUnavailable:503,GatewayTimeout:504,HttpVersionNotSupported:505,VariantAlsoNegotiates:506,InsufficientStorage:507,LoopDetected:508,NotExtended:510,NetworkAuthenticationRequired:511};Object.entries(Le).forEach((function(e){var t=o(e,2),n=t[0],r=t[1];Le[r]=n}));var De=Le;var Ie=function e(t){var n=new Fe(t),r=a(Fe.prototype.request,n);return q.extend(r,Fe.prototype,n,{allOwnKeys:!0}),q.extend(r,n,null,{allOwnKeys:!0}),r.create=function(n){return e(Ce(t,n))},r}(ae);return Ie.Axios=Fe,Ie.CanceledError=ve,Ie.CancelToken=Be,Ie.isCancel=me,Ie.VERSION=Ne,Ie.toFormData=G,Ie.AxiosError=M,Ie.Cancel=Ie.CanceledError,Ie.all=function(e){return Promise.all(e)},Ie.spread=function(e){return function(t){return e.apply(null,t)}},Ie.isAxiosError=function(e){return q.isObject(e)&&!0===e.isAxiosError},Ie.mergeConfig=Ce,Ie.AxiosHeaders=pe,Ie.formToJSON=function(e){return oe(q.isHTMLForm(e)?new FormData(e):e)},Ie.getAdapter=Re,Ie.HttpStatusCode=De,Ie.default=Ie,Ie})); \ No newline at end of file diff --git a/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/utils/color.js b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/utils/color.js new file mode 100644 index 00000000..52db3485 --- /dev/null +++ b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/utils/color.js @@ -0,0 +1,78 @@ +// 霓虹数组 +const $neonColorArr = [ + rgb(255, 0, 0), + rgb(255, 0, 255), + rgb(0, 0, 255), + rgb(0, 255, 255), + rgb(0, 255, 0), + rgb(255, 255, 0), + rgb(255, 0, 0) +] + +// RGB转16进制颜色值 +const $rgbToHex = (r, g, b) => { + return ((1 << 24) | (r << 16) | (g << 8) | b).toString(16).replace('1', '#'); +}; + +// 16进制颜色值转RGB +const $hexToRgb = (hex) => { + const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); + return { + r: parseInt(result[1], 16), + g: parseInt(result[2], 16), + b: parseInt(result[3], 16) + }; +}; + +// 根据色相转RGB +const $hueToRgb = (p, q, t) => { + if (t < 0) t += 1; + if (t > 1) t -= 1; + if (t < 1 / 6) return p + (q - p) * 6 * t; + if (t < 1 / 2) return q; + if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6; + return p; +}; + +// 根据RGB转色调 +const $rgbToHsl = (rgb) => { + const r = rgb[0] / 255; + const g = rgb[1] / 255; + const b = rgb[2] / 255; + const max = Math.max(r, g, b); + const min = Math.min(r, g, b); + + // 计算亮度饱和度 + let saturation = 0; + const lightness = (max + min) / 2; + if (max !== min) { + const delta = max - min; + saturation = delta / (1 - Math.abs(2 * lightness - 1)); + } + + // 计算色调 + let hue = 0; + if (max === r) hue = ((g - b) / (max - min)) % 6; + else if (max === g) hue = (b - r) / (max - min) + 2; + else hue = (r - g) / (max - min) + 4; hue *= 60; + if (hue < 0) hue += 360; + return [hue, saturation * 100, lightness * 100]; +}; + +// 根据色调转rgb +const $hslToRgb = (hsl) => { + const hue = hsl[0] / 360; + const saturation = hsl[1] / 100; + const lightness = hsl[2] / 100; + let r, g, b; + if (saturation === 0) { + r = g = b = lightness; + } else { + const q = lightness < 0.5 ? lightness * (1 + saturation) : lightness + saturation - lightness * saturation; + const p = 2 * lightness - q; + r = $hueToRgb(p, q, hue + 1 / 3); + g = $hueToRgb(p, q, hue); + b = $hueToRgb(p, q, hue - 1 / 3); + } + return [r * 255, g * 255, b * 255]; +}; \ No newline at end of file diff --git a/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/utils/common.js b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/utils/common.js new file mode 100644 index 00000000..7e50d09f --- /dev/null +++ b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/utils/common.js @@ -0,0 +1,84 @@ +// 自定义事件类 +class EventPlus { + constructor() { + this.event = new EventTarget(); + } + on(name, callback) { + this.event.addEventListener(name, e => callback(e.detail)); + } + send(name, data) { + this.event.dispatchEvent(new CustomEvent(name, { + detail: data, + bubbles: false, + cancelable: false + })); + } +} + +// 补零 +String.prototype.fill = function () { + return this >= 10 ? this : '0' + this +} + +// unicode编码转换字符串 +String.prototype.uTs = function () { + return eval('"' + Array.from(this).join('') + '"'); +}; + +// 字符串转换unicode编码 +String.prototype.sTu = function (str = '') { + Array.from(this).forEach(item => str += `\\u${item.charCodeAt(0).toString(16)}`); + return str; +}; + +// 全局变量/方法 +const $emit = new EventPlus(), $ = (selector, isAll = false) => { + const element = document.querySelector(selector), methods = { + on: function (event, callback) { + this.addEventListener(event, callback) + }, + attr: function (name, value = '') { + value && this.setAttribute(name, value); + return this; + } + } + if (!isAll && element) { + return Object.assign(element, methods) + } else if (!isAll && !element) { + throw `HTML没有 ${selector} 元素! 请检查是否拼写错误` + } + return Array.from(document.querySelectorAll(selector)).map(item => Object.assign(item, methods)) +} + +// 节流函数 +$.throttle = (fn, delay) => { + let Timer = null; + return function () { + if (Timer) return; + Timer = setTimeout(() => { + fn.apply(this, arguments); + Timer = null; + }, delay); + }; +}; + +// 防抖函数 +$.debounce = (fn, delay) => { + let Timer = null; + return function () { + clearTimeout(Timer); + Timer = setTimeout(() => fn.apply(this, arguments), delay); + }; +}; + +// 限制数字 +$.num = (selector) => { + if (!selector.value || /^\d+$/.test(selector.value)) return; + selector.value = selector.value.slice(0, -1); + $.num(selector); +}; + +// 绑定限制数字方法 +Array.from($('input[type="num"]', true)).forEach(item => { + item.addEventListener('input', () => $.num(item)); +}); \ No newline at end of file diff --git a/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/utils/worker.js b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/utils/worker.js new file mode 100644 index 00000000..b01b4f37 --- /dev/null +++ b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/static/utils/worker.js @@ -0,0 +1,5 @@ +self.onmessage = function (e) { + setInterval(() => { + self.postMessage(1) + }, e.data); +} \ No newline at end of file diff --git a/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/zh_CN.json b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/zh_CN.json new file mode 100644 index 00000000..0fcae7b4 --- /dev/null +++ b/StreamDock-Plugin-SDK/com.mirabox.streamdock.weather.sdPlugin/zh_CN.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b060e3cb4c4a070c0526e11c72429f475208864bc0406cd84ebe38725cba62d9 +size 2588 diff --git a/StreamDock-Plugin-SDK/개발환경_설정_가이드.md b/StreamDock-Plugin-SDK/개발환경_설정_가이드.md deleted file mode 100644 index b80b43c3..00000000 --- a/StreamDock-Plugin-SDK/개발환경_설정_가이드.md +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:02b74155abfe52c46e8b39cad2a7b629c49a93b28c22dfca83118dd29b39ad65 -size 3165 diff --git a/StreamDock-Plugin-SDK/샘플_플러그인_개발_가이드.md b/StreamDock-Plugin-SDK/샘플_플러그인_개발_가이드.md deleted file mode 100644 index 943e682e..00000000 --- a/StreamDock-Plugin-SDK/샘플_플러그인_개발_가이드.md +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:f5b9735aaac3c54383e2905f2f3b73575ec019adad3095dc59238f325341e653 -size 7973 diff --git a/Streamdeck/Implementation_Guide.md b/Streamdeck/Implementation_Guide.md new file mode 100644 index 00000000..b5afba2b --- /dev/null +++ b/Streamdeck/Implementation_Guide.md @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:119a2c3c7104904a506a531b989b147f4df9a558ae50d63047536e3998b0577f +size 6805 diff --git a/Streamdeck/JSON_Protocol_Specification.md b/Streamdeck/JSON_Protocol_Specification.md new file mode 100644 index 00000000..0aa8c6ab --- /dev/null +++ b/Streamdeck/JSON_Protocol_Specification.md @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3ea7000f2e799223fc73538cd26994f23f1147e3ffbff95735ad1de8bd629e06 +size 9184 diff --git a/Streamdeck/Streamingle_Plugin_Architecture.md b/Streamdeck/Streamingle_Plugin_Architecture.md new file mode 100644 index 00000000..92054859 --- /dev/null +++ b/Streamdeck/Streamingle_Plugin_Architecture.md @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:43b5bd7d68ce72fe98574e651db406bd90cd22f2d6bcb49d647c9b995ae2d584 +size 5830 diff --git a/Streamdeck/com.mirabox.streamingle.sdPlugin/README.md b/Streamdeck/com.mirabox.streamingle.sdPlugin/README.md new file mode 100644 index 00000000..910b61cf --- /dev/null +++ b/Streamdeck/com.mirabox.streamingle.sdPlugin/README.md @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b82b35887a538e416ea83662e653b0b0ff00cf70246f64f34e448efca02306be +size 4343 diff --git a/Streamdeck/com.mirabox.streamingle.sdPlugin/build.js b/Streamdeck/com.mirabox.streamingle.sdPlugin/build.js new file mode 100644 index 00000000..08a0f2e0 --- /dev/null +++ b/Streamdeck/com.mirabox.streamingle.sdPlugin/build.js @@ -0,0 +1,288 @@ +#!/usr/bin/env node + +/** + * Streamingle Camera Plugin Build Script + * StreamDock 플러그인 폴더에 자동 복사 기능 포함 + */ + +const fs = require('fs'); +const path = require('path'); + +// 설정 +const PLUGIN_NAME = 'com.mirabox.streamingle.sdPlugin'; +const STREAMDOCK_PLUGINS_PATH = 'C:\\Users\\qscft\\AppData\\Roaming\\HotSpot\\StreamDock\\plugins'; +const CURRENT_DIR = __dirname; + +// 색상 출력 함수 +function log(message, color = 'white') { + const colors = { + red: '\x1b[31m', + green: '\x1b[32m', + yellow: '\x1b[33m', + blue: '\x1b[34m', + magenta: '\x1b[35m', + cyan: '\x1b[36m', + white: '\x1b[37m', + reset: '\x1b[0m' + }; + console.log(`${colors[color]}${message}${colors.reset}`); +} + +// 디렉토리 복사 함수 (제외할 파일/폴더 필터링 포함) +function copyDirectory(src, dest) { + // 제외할 파일/폴더 목록 + const excludePatterns = [ + 'dist', + 'dist-dev', + 'node_modules', + '.git', + '.gitignore', + 'build.js', + 'build.log', + 'README.md' + // package.json과 package-lock.json은 제외하지 않음 (MiraBox StreamDock에서 필요할 수 있음) + ]; + + if (!fs.existsSync(dest)) { + fs.mkdirSync(dest, { recursive: true }); + } + + const entries = fs.readdirSync(src, { withFileTypes: true }); + + for (const entry of entries) { + // 제외할 파일/폴더 체크 + if (excludePatterns.includes(entry.name)) { + console.log(`⏭️ 제외: ${entry.name}`); + continue; + } + + const srcPath = path.join(src, entry.name); + const destPath = path.join(dest, entry.name); + + if (entry.isDirectory()) { + copyDirectory(srcPath, destPath); + } else { + fs.copyFileSync(srcPath, destPath); + } + } + } + + // 디렉토리 삭제 함수 +function removeDirectory(dirPath) { + if (fs.existsSync(dirPath)) { + fs.rmSync(dirPath, { recursive: true, force: true }); + } + } + +// 빌드 함수 - StreamDock 플러그인 폴더에 직접 빌드 +function build() { + log('🔨 Streamingle Camera Plugin 빌드 시작...', 'cyan'); + + const buildDir = path.join(STREAMDOCK_PLUGINS_PATH, PLUGIN_NAME); + const pluginDir = CURRENT_DIR; + + // StreamDock 플러그인 폴더 확인 + if (!fs.existsSync(STREAMDOCK_PLUGINS_PATH)) { + log('❌ StreamDock 플러그인 폴더를 찾을 수 없습니다', 'red'); + log(`📁 예상 경로: ${STREAMDOCK_PLUGINS_PATH}`, 'yellow'); + return false; + } + + // 기존 플러그인 삭제 + if (fs.existsSync(buildDir)) { + removeDirectory(buildDir); + log('🗑️ 기존 플러그인 삭제 완료', 'yellow'); + } + + // 빌드 폴더 생성 + fs.mkdirSync(buildDir, { recursive: true }); + + // 플러그인 폴더 복사 + if (fs.existsSync(pluginDir)) { + copyDirectory(pluginDir, buildDir); + log('✅ 플러그인 파일 복사 완료', 'green'); + } else { + log('❌ 플러그인 폴더를 찾을 수 없습니다', 'red'); + return false; + } + + log('🎉 빌드 완료!', 'green'); + log(`📁 빌드 위치: ${buildDir}`, 'blue'); + log('💡 StreamDock을 재시작하면 플러그인이 활성화됩니다', 'cyan'); + return true; + } + +// 개발 빌드 함수 +function buildDev() { + log('🔨 Streamingle Camera Plugin 개발 빌드 시작...', 'cyan'); + + const buildDir = path.join(CURRENT_DIR, 'dist-dev'); + const pluginDir = CURRENT_DIR; + + // 기존 빌드 폴더 정리 + if (fs.existsSync(buildDir)) { + removeDirectory(buildDir); + } + + // 빌드 폴더 생성 + fs.mkdirSync(buildDir, { recursive: true }); + + // 플러그인 폴더 복사 + if (fs.existsSync(pluginDir)) { + copyDirectory(pluginDir, path.join(buildDir, PLUGIN_NAME)); + log('✅ 플러그인 파일 복사 완료', 'green'); + } else { + log('❌ 플러그인 폴더를 찾을 수 없습니다', 'red'); + return false; + } + + log('🎉 개발 빌드 완료!', 'green'); + log(`📁 빌드 위치: ${buildDir}`, 'blue'); + return true; +} + +// StreamDock에 배포 함수 +function deploy() { + log('🚀 StreamDock에 플러그인 배포 시작...', 'magenta'); + + const pluginDir = CURRENT_DIR; + const streamdockPluginPath = path.join(STREAMDOCK_PLUGINS_PATH, PLUGIN_NAME); + + // StreamDock 플러그인 폴더 확인 + if (!fs.existsSync(STREAMDOCK_PLUGINS_PATH)) { + log('❌ StreamDock 플러그인 폴더를 찾을 수 없습니다', 'red'); + log(`📁 예상 경로: ${STREAMDOCK_PLUGINS_PATH}`, 'yellow'); + return false; + } + + // 기존 플러그인 삭제 + if (fs.existsSync(streamdockPluginPath)) { + removeDirectory(streamdockPluginPath); + log('🗑️ 기존 플러그인 삭제 완료', 'yellow'); + } + + // 새 플러그인 복사 + if (fs.existsSync(pluginDir)) { + copyDirectory(pluginDir, streamdockPluginPath); + log('✅ 플러그인 복사 완료', 'green'); + } else { + log('❌ 플러그인 폴더를 찾을 수 없습니다', 'red'); + return false; + } + + log('🎉 StreamDock 배포 완료!', 'green'); + log(`📁 배포 위치: ${streamdockPluginPath}`, 'blue'); + log('💡 StreamDock을 재시작하면 플러그인이 활성화됩니다', 'cyan'); + return true; + } + +// 개발용 배포 함수 (빌드 후 배포) +function deployDev() { + log('🚀 개발용 StreamDock 배포 시작...', 'magenta'); + + // 먼저 빌드 + if (!buildDev()) { + return false; + } + + const buildDir = path.join(CURRENT_DIR, 'dist-dev', PLUGIN_NAME); + const streamdockPluginPath = path.join(STREAMDOCK_PLUGINS_PATH, PLUGIN_NAME); + + // StreamDock 플러그인 폴더 확인 + if (!fs.existsSync(STREAMDOCK_PLUGINS_PATH)) { + log('❌ StreamDock 플러그인 폴더를 찾을 수 없습니다', 'red'); + log(`📁 예상 경로: ${STREAMDOCK_PLUGINS_PATH}`, 'yellow'); + return false; + } + + // 기존 플러그인 삭제 + if (fs.existsSync(streamdockPluginPath)) { + removeDirectory(streamdockPluginPath); + log('🗑️ 기존 플러그인 삭제 완료', 'yellow'); + } + + // 새 플러그인 복사 + if (fs.existsSync(buildDir)) { + copyDirectory(buildDir, streamdockPluginPath); + log('✅ 플러그인 복사 완료', 'green'); + } else { + log('❌ 빌드된 플러그인 폴더를 찾을 수 없습니다', 'red'); + return false; + } + + log('🎉 개발용 StreamDock 배포 완료!', 'green'); + log(`📁 배포 위치: ${streamdockPluginPath}`, 'blue'); + log('💡 StreamDock을 재시작하면 플러그인이 활성화됩니다', 'cyan'); + return true; +} + +// 정리 함수 +function clean() { + log('🧹 빌드 폴더 정리 시작...', 'yellow'); + + const buildDir = path.join(CURRENT_DIR, 'dist'); + const buildDevDir = path.join(CURRENT_DIR, 'dist-dev'); + + if (fs.existsSync(buildDir)) { + removeDirectory(buildDir); + log('✅ dist 폴더 삭제 완료', 'green'); + } + + if (fs.existsSync(buildDevDir)) { + removeDirectory(buildDevDir); + log('✅ dist-dev 폴더 삭제 완료', 'green'); + } + + log('🎉 정리 완료!', 'green'); +} + +// 도움말 함수 +function help() { + log('📖 Streamingle Camera Plugin 빌드 도구', 'cyan'); + log('', 'white'); + log('사용법:', 'yellow'); + log(' node build.js <명령>', 'white'); + log('', 'white'); + log('명령:', 'yellow'); + log(' build - 프로덕션 빌드 (dist 폴더에 생성)', 'white'); + log(' build:dev - 개발 빌드 (dist-dev 폴더에 생성)', 'white'); + log(' deploy - StreamDock에 직접 배포', 'white'); + log(' deploy:dev - 개발 빌드 후 StreamDock에 배포', 'white'); + log(' clean - 빌드 폴더 정리', 'white'); + log(' help - 이 도움말 표시', 'white'); + log('', 'white'); + log('예시:', 'yellow'); + log(' node build.js deploy # 바로 StreamDock에 배포', 'white'); + log(' node build.js deploy:dev # 개발 빌드 후 배포', 'white'); +} + +// 메인 실행 +const command = process.argv[2]; + + switch (command) { + case 'build': + build(); + break; + case 'build:dev': + buildDev(); + break; + case 'deploy': + deploy(); + break; + case 'deploy:dev': + deployDev(); + break; + case 'clean': + clean(); + break; + case 'help': + case '--help': + case '-h': + help(); + break; + default: + log('❌ 알 수 없는 명령입니다', 'red'); + log('', 'white'); + help(); + break; + } \ No newline at end of file diff --git a/Streamdeck/com.mirabox.streamingle.sdPlugin/debug.js b/Streamdeck/com.mirabox.streamingle.sdPlugin/debug.js new file mode 100644 index 00000000..aca3e6b2 --- /dev/null +++ b/Streamdeck/com.mirabox.streamingle.sdPlugin/debug.js @@ -0,0 +1,133 @@ +const fs = require('fs'); +const path = require('path'); + +console.log('=== Streamingle 플러그인 디버깅 도구 ==='); + +// 1. 플러그인 파일 확인 +console.log('\n1. 플러그인 파일 확인:'); +const pluginDir = __dirname; +const requiredFiles = [ + 'manifest.json', + 'plugin.js', + 'package.json', + 'propertyinspector.html' +]; + +requiredFiles.forEach(file => { + const filePath = path.join(pluginDir, file); + if (fs.existsSync(filePath)) { + const stats = fs.statSync(filePath); + console.log(`✅ ${file} - ${stats.size} bytes`); + } else { + console.log(`❌ ${file} - 파일 없음`); + } +}); + +// 2. manifest.json 내용 확인 +console.log('\n2. manifest.json 내용:'); +try { + const manifestPath = path.join(pluginDir, 'manifest.json'); + const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8')); + console.log('✅ manifest.json 파싱 성공'); + console.log(` SDK 버전: ${manifest.SDKVersion}`); + console.log(` 코드 경로: ${manifest.CodePath}`); + console.log(` 플러그인 이름: ${manifest.Name}`); + console.log(` 액션 UUID: ${manifest.Actions[0].UUID}`); +} catch (error) { + console.log(`❌ manifest.json 파싱 실패: ${error.message}`); +} + +// 3. package.json 내용 확인 +console.log('\n3. package.json 내용:'); +try { + const packagePath = path.join(pluginDir, 'package.json'); + const packageJson = JSON.parse(fs.readFileSync(packagePath, 'utf8')); + console.log('✅ package.json 파싱 성공'); + console.log(` 이름: ${packageJson.name}`); + console.log(` 버전: ${packageJson.version}`); + console.log(` 메인 파일: ${packageJson.main}`); + console.log(` 의존성: ${Object.keys(packageJson.dependencies || {}).join(', ')}`); +} catch (error) { + console.log(`❌ package.json 파싱 실패: ${error.message}`); +} + +// 4. WebSocket 연결 테스트 +console.log('\n4. WebSocket 연결 테스트:'); +const WebSocket = require('ws'); + +function testWebSocketConnection() { + return new Promise((resolve) => { + console.log(' 연결 시도 중...'); + + const ws = new WebSocket('ws://127.0.0.1:10701/'); + + const timeout = setTimeout(() => { + console.log(' ❌ 연결 타임아웃 (5초)'); + ws.close(); + resolve(false); + }, 5000); + + ws.on('open', () => { + console.log(' ✅ WebSocket 연결 성공!'); + clearTimeout(timeout); + ws.close(); + resolve(true); + }); + + ws.on('error', (error) => { + console.log(` ❌ WebSocket 연결 실패: ${error.message}`); + clearTimeout(timeout); + resolve(false); + }); + + ws.on('close', (code, reason) => { + console.log(` 🔌 연결 종료 - 코드: ${code}, 이유: ${reason || '알 수 없음'}`); + }); + }); +} + +testWebSocketConnection().then((connected) => { + console.log(`\n결과: ${connected ? '✅ 연결 가능' : '❌ 연결 불가'}`); + + // 5. 포트 상태 확인 + console.log('\n5. 포트 상태 확인:'); + const { exec } = require('child_process'); + + exec('netstat -an | findstr :10701', (error, stdout, stderr) => { + if (error) { + console.log(` ❌ netstat 실행 실패: ${error.message}`); + return; + } + + if (stdout.trim()) { + console.log(' ✅ 포트 10701 사용 중:'); + stdout.split('\n').forEach(line => { + if (line.trim()) { + console.log(` ${line.trim()}`); + } + }); + } else { + console.log(' ❌ 포트 10701에서 서비스 없음'); + } + }); +}); + +// 6. StreamDock 플러그인 경로 확인 +console.log('\n6. StreamDock 플러그인 경로:'); +const streamDockPath = 'C:\\Users\\qscft\\AppData\\Roaming\\HotSpot\\StreamDock\\plugins\\com.mirabox.streamingle.sdPlugin'; +if (fs.existsSync(streamDockPath)) { + console.log(` ✅ StreamDock 플러그인 폴더 존재: ${streamDockPath}`); + + const files = fs.readdirSync(streamDockPath); + console.log(` 📁 파일 개수: ${files.length}개`); + files.forEach(file => { + const filePath = path.join(streamDockPath, file); + const stats = fs.statSync(filePath); + const type = stats.isDirectory() ? '📁' : '📄'; + console.log(` ${type} ${file} - ${stats.size} bytes`); + }); +} else { + console.log(` ❌ StreamDock 플러그인 폴더 없음: ${streamDockPath}`); +} + +console.log('\n=== 디버깅 완료 ==='); \ No newline at end of file diff --git a/Streamdeck/com.mirabox.streamingle.sdPlugin/dist-dev/com.mirabox.streamingle.sdPlugin/debug.js b/Streamdeck/com.mirabox.streamingle.sdPlugin/dist-dev/com.mirabox.streamingle.sdPlugin/debug.js new file mode 100644 index 00000000..aca3e6b2 --- /dev/null +++ b/Streamdeck/com.mirabox.streamingle.sdPlugin/dist-dev/com.mirabox.streamingle.sdPlugin/debug.js @@ -0,0 +1,133 @@ +const fs = require('fs'); +const path = require('path'); + +console.log('=== Streamingle 플러그인 디버깅 도구 ==='); + +// 1. 플러그인 파일 확인 +console.log('\n1. 플러그인 파일 확인:'); +const pluginDir = __dirname; +const requiredFiles = [ + 'manifest.json', + 'plugin.js', + 'package.json', + 'propertyinspector.html' +]; + +requiredFiles.forEach(file => { + const filePath = path.join(pluginDir, file); + if (fs.existsSync(filePath)) { + const stats = fs.statSync(filePath); + console.log(`✅ ${file} - ${stats.size} bytes`); + } else { + console.log(`❌ ${file} - 파일 없음`); + } +}); + +// 2. manifest.json 내용 확인 +console.log('\n2. manifest.json 내용:'); +try { + const manifestPath = path.join(pluginDir, 'manifest.json'); + const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8')); + console.log('✅ manifest.json 파싱 성공'); + console.log(` SDK 버전: ${manifest.SDKVersion}`); + console.log(` 코드 경로: ${manifest.CodePath}`); + console.log(` 플러그인 이름: ${manifest.Name}`); + console.log(` 액션 UUID: ${manifest.Actions[0].UUID}`); +} catch (error) { + console.log(`❌ manifest.json 파싱 실패: ${error.message}`); +} + +// 3. package.json 내용 확인 +console.log('\n3. package.json 내용:'); +try { + const packagePath = path.join(pluginDir, 'package.json'); + const packageJson = JSON.parse(fs.readFileSync(packagePath, 'utf8')); + console.log('✅ package.json 파싱 성공'); + console.log(` 이름: ${packageJson.name}`); + console.log(` 버전: ${packageJson.version}`); + console.log(` 메인 파일: ${packageJson.main}`); + console.log(` 의존성: ${Object.keys(packageJson.dependencies || {}).join(', ')}`); +} catch (error) { + console.log(`❌ package.json 파싱 실패: ${error.message}`); +} + +// 4. WebSocket 연결 테스트 +console.log('\n4. WebSocket 연결 테스트:'); +const WebSocket = require('ws'); + +function testWebSocketConnection() { + return new Promise((resolve) => { + console.log(' 연결 시도 중...'); + + const ws = new WebSocket('ws://127.0.0.1:10701/'); + + const timeout = setTimeout(() => { + console.log(' ❌ 연결 타임아웃 (5초)'); + ws.close(); + resolve(false); + }, 5000); + + ws.on('open', () => { + console.log(' ✅ WebSocket 연결 성공!'); + clearTimeout(timeout); + ws.close(); + resolve(true); + }); + + ws.on('error', (error) => { + console.log(` ❌ WebSocket 연결 실패: ${error.message}`); + clearTimeout(timeout); + resolve(false); + }); + + ws.on('close', (code, reason) => { + console.log(` 🔌 연결 종료 - 코드: ${code}, 이유: ${reason || '알 수 없음'}`); + }); + }); +} + +testWebSocketConnection().then((connected) => { + console.log(`\n결과: ${connected ? '✅ 연결 가능' : '❌ 연결 불가'}`); + + // 5. 포트 상태 확인 + console.log('\n5. 포트 상태 확인:'); + const { exec } = require('child_process'); + + exec('netstat -an | findstr :10701', (error, stdout, stderr) => { + if (error) { + console.log(` ❌ netstat 실행 실패: ${error.message}`); + return; + } + + if (stdout.trim()) { + console.log(' ✅ 포트 10701 사용 중:'); + stdout.split('\n').forEach(line => { + if (line.trim()) { + console.log(` ${line.trim()}`); + } + }); + } else { + console.log(' ❌ 포트 10701에서 서비스 없음'); + } + }); +}); + +// 6. StreamDock 플러그인 경로 확인 +console.log('\n6. StreamDock 플러그인 경로:'); +const streamDockPath = 'C:\\Users\\qscft\\AppData\\Roaming\\HotSpot\\StreamDock\\plugins\\com.mirabox.streamingle.sdPlugin'; +if (fs.existsSync(streamDockPath)) { + console.log(` ✅ StreamDock 플러그인 폴더 존재: ${streamDockPath}`); + + const files = fs.readdirSync(streamDockPath); + console.log(` 📁 파일 개수: ${files.length}개`); + files.forEach(file => { + const filePath = path.join(streamDockPath, file); + const stats = fs.statSync(filePath); + const type = stats.isDirectory() ? '📁' : '📄'; + console.log(` ${type} ${file} - ${stats.size} bytes`); + }); +} else { + console.log(` ❌ StreamDock 플러그인 폴더 없음: ${streamDockPath}`); +} + +console.log('\n=== 디버깅 완료 ==='); \ No newline at end of file diff --git a/Streamdeck/com.mirabox.streamingle.sdPlugin/dist-dev/com.mirabox.streamingle.sdPlugin/images/action.png b/Streamdeck/com.mirabox.streamingle.sdPlugin/dist-dev/com.mirabox.streamingle.sdPlugin/images/action.png new file mode 100644 index 00000000..45b93e4e --- /dev/null +++ b/Streamdeck/com.mirabox.streamingle.sdPlugin/dist-dev/com.mirabox.streamingle.sdPlugin/images/action.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:62da15b1333bcf15bb1ef33c05fb246406b76a8bf9710a4df6e7bd0a16ef62b5 +size 42151 diff --git a/Streamdeck/com.mirabox.streamingle.sdPlugin/dist-dev/com.mirabox.streamingle.sdPlugin/manifest.json b/Streamdeck/com.mirabox.streamingle.sdPlugin/dist-dev/com.mirabox.streamingle.sdPlugin/manifest.json new file mode 100644 index 00000000..fcc7d856 --- /dev/null +++ b/Streamdeck/com.mirabox.streamingle.sdPlugin/dist-dev/com.mirabox.streamingle.sdPlugin/manifest.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f646099cafd148b8c2b4c8dc1922ab6e98354df7194717b6d82812b05af66f43 +size 1353 diff --git a/Streamdeck/com.mirabox.streamingle.sdPlugin/dist-dev/com.mirabox.streamingle.sdPlugin/plugin/index.html b/Streamdeck/com.mirabox.streamingle.sdPlugin/dist-dev/com.mirabox.streamingle.sdPlugin/plugin/index.html new file mode 100644 index 00000000..84e63f28 --- /dev/null +++ b/Streamdeck/com.mirabox.streamingle.sdPlugin/dist-dev/com.mirabox.streamingle.sdPlugin/plugin/index.html @@ -0,0 +1,314 @@ + + + + + Streamingle Plugin Main + + +
+ 플러그인 메인 - 버튼 클릭 처리
+ Property Inspector에서 설정 관리 +
+ + + + \ No newline at end of file diff --git a/Streamdeck/com.mirabox.streamingle.sdPlugin/dist-dev/com.mirabox.streamingle.sdPlugin/plugin/index.js b/Streamdeck/com.mirabox.streamingle.sdPlugin/dist-dev/com.mirabox.streamingle.sdPlugin/plugin/index.js new file mode 100644 index 00000000..e32ba79d --- /dev/null +++ b/Streamdeck/com.mirabox.streamingle.sdPlugin/dist-dev/com.mirabox.streamingle.sdPlugin/plugin/index.js @@ -0,0 +1,664 @@ +/** + * Streamingle Camera Controller Plugin for StreamDeck + * 외부 패키지 없이 동작하도록 단순화 + */ + +// Global variables +let websocket = null; +let uuid = null; +let actionInfo = {}; +let connectionInfo = {}; + +// Context 관리 +const buttonContexts = new Map(); + +// Unity 연결 관리자 +let unityManager = null; + +// Unity 연결 관리 클래스 (내장 WebSocket만 사용) +class UnityConnectionManager { + constructor() { + this.isConnected = false; + this.socket = null; + this.reconnectInterval = null; + this.healthCheckInterval = null; + this.connectionAttempts = 0; + this.maxConsecutiveFailures = 10; + this.isShuttingDown = false; + this.cameraData = null; + this.currentCamera = 0; + this.eventListeners = new Map(); + this.startHealthCheck(); + } + + on(event, callback) { + if (!this.eventListeners.has(event)) { + this.eventListeners.set(event, []); + } + this.eventListeners.get(event).push(callback); + } + + emit(event, data) { + if (this.eventListeners.has(event)) { + this.eventListeners.get(event).forEach(callback => { + try { + callback(data); + } catch (error) { + console.log(`❌ Event listener error (${event}): ${error.message}`); + } + }); + } + } + + startHealthCheck() { + if (this.healthCheckInterval) { + clearInterval(this.healthCheckInterval); + } + this.healthCheckInterval = setInterval(() => { + console.log(`🩺 연결 상태 확인 - Unity 연결됨: ${this.isConnected}, 종료 중: ${this.isShuttingDown}, 재연결 중: ${!!this.reconnectInterval}`); + if (!this.isConnected && !this.isShuttingDown && !this.reconnectInterval) { + console.log('🩺 연결 상태 확인 - Unity 서버 재연결 시도'); + this.connectionAttempts = 0; + this.connect(); + } + }, 30000); + console.log('🩺 Unity 연결 상태 주기적 확인 시작 (30초 간격)'); + } + + connect() { + if (this.isShuttingDown) { + console.log('🛑 종료 중이므로 연결 시도 안함'); + return; + } + if (this.socket && this.socket.readyState === 0) { + console.log('⏳ 이미 연결 시도 중...'); + return; + } + if (this.socket) { + console.log('🔌 기존 소켓 닫기'); + this.socket.close(); + } + try { + console.log('🔌 Unity WebSocket 생성 시도: ws://localhost:10701'); + this.socket = new window.WebSocket('ws://localhost:10701'); + console.log(`🔌 Unity 연결 시도... (시도 ${this.connectionAttempts + 1}회)`); + this.socket.onopen = () => { + this.isConnected = true; + this.connectionAttempts = 0; + console.log('✅ Unity 연결 성공! WebSocket 연결됨'); + if (this.reconnectInterval) { + clearTimeout(this.reconnectInterval); + this.reconnectInterval = null; + } + this.emit('connectionChange', this.isConnected); + + // 연결 상태 변경을 모든 Property Inspector에 알림 + broadcastToPropertyInspectors('connection_status', { + connected: true + }); + + this.requestCameraList(); + }; + this.socket.onmessage = (event) => { + try { + console.log('📨 Unity 원본 메시지:', event.data); + const data = JSON.parse(event.data); + console.log(`📨 Unity 메시지 파싱 완료: ${data.type || 'unknown'}`); + this.handleUnityMessage(data); + } catch (error) { + console.log(`❌ Unity 메시지 파싱 오류: ${error.message}`); + console.log(`❌ 원본 메시지: ${event.data}`); + } + }; + this.socket.onclose = (event) => { + const wasConnected = this.isConnected; + this.isConnected = false; + if (wasConnected) { + console.log(`❌ Unity 연결 끊어짐 (코드: ${event.code}, 이유: ${event.reason || '알 수 없음'})`); + this.emit('connectionChange', this.isConnected); + } else { + console.log(`❌ Unity 연결 실패 (코드: ${event.code}, 이유: ${event.reason || '알 수 없음'})`); + } + if (!this.isShuttingDown) { + this.scheduleReconnect(); + } + }; + this.socket.onerror = (error) => { + console.log(`❌ Unity 연결 오류: ${error}`); + }; + } catch (error) { + console.log(`❌ Unity 연결 설정 오류: ${error.message}`); + if (!this.isShuttingDown) { + this.scheduleReconnect(); + } + } + } + + scheduleReconnect() { + if (this.isShuttingDown || this.reconnectInterval) return; + this.connectionAttempts++; + let delay; + if (this.connectionAttempts <= 3) { + delay = 2000; + } else if (this.connectionAttempts <= 10) { + delay = 5000; + } else { + delay = 10000; + } + console.log(`🔄 ${delay/1000}초 후 Unity 재연결 시도 (${this.connectionAttempts}/${this.maxConsecutiveFailures})`); + this.reconnectInterval = setTimeout(() => { + this.reconnectInterval = null; + this.connect(); + }, delay); + } + + handleUnityMessage(data) { + console.log('📨 Unity 메시지 수신:', data); + + switch (data.type) { + case 'cameraList': + this.cameraData = data; + this.currentCamera = data.currentCamera || 0; + console.log(`📹 카메라 목록 수신: ${data.cameras?.length || 0}개`); + console.log('📹 카메라 데이터 상세:', data); + + // 카메라 목록을 받았다는 것은 Unity 연결이 성공했다는 의미 + if (!this.isConnected) { + console.log('🔄 Unity 연결 상태 업데이트: 연결됨 (카메라 목록 수신으로 확인)'); + this.isConnected = true; + this.connectionAttempts = 0; + this.emit('connectionChange', this.isConnected); + } + + // Property Inspector들에게 카메라 데이터 전송 + console.log('📤 Property Inspector들에게 카메라 데이터 전송'); + broadcastToPropertyInspectors('camera_data', { + camera_data: data, + current_camera: this.currentCamera + }); + updateAllButtons(); + break; + case 'cameraSwitched': + this.currentCamera = data.cameraIndex; + console.log(`📹 카메라 전환 완료: ${data.cameraIndex}`); + break; + default: + console.log(`📨 알 수 없는 Unity 메시지 타입: ${data.type}`); + } + } + + requestCameraList() { + if (!this.isConnected || !this.socket) { + console.log('❌ Unity 연결 안됨 - 카메라 목록 요청 불가'); + console.log(`❌ 연결 상태: ${this.isConnected}, 소켓 상태: ${this.socket ? this.socket.readyState : 'null'}`); + return; + } + try { + const message = { + type: 'getCameraList', + requestId: Date.now().toString() + }; + console.log('📤 Unity에 카메라 목록 요청:', message); + this.socket.send(JSON.stringify(message)); + console.log('✅ 카메라 목록 요청 전송 완료'); + } catch (error) { + console.log(`❌ 카메라 목록 요청 실패: ${error.message}`); + } + } + + switchCamera(cameraIndex) { + if (!this.isConnected) { + console.log('❌ Unity 연결 안됨 - 카메라 전환 불가'); + return false; + } + try { + const message = { + type: 'switchCamera', + cameraIndex: cameraIndex + }; + this.socket.send(JSON.stringify(message)); + console.log(`📤 Unity에 카메라 전환 요청: ${cameraIndex}`); + return true; + } catch (error) { + console.log(`❌ 카메라 전환 요청 실패: ${error.message}`); + return false; + } + } + + forceReconnect() { + console.log('🔄 수동 재연결 요청'); + this.connectionAttempts = 0; + if (this.reconnectInterval) { + clearTimeout(this.reconnectInterval); + this.reconnectInterval = null; + } + this.connect(); + } + + getConnectionStatus() { + return { + isConnected: this.isConnected, + cameraData: this.cameraData, + currentCamera: this.currentCamera + }; + } + + disconnect() { + this.isShuttingDown = true; + if (this.healthCheckInterval) { + clearInterval(this.healthCheckInterval); + this.healthCheckInterval = null; + } + if (this.reconnectInterval) { + clearTimeout(this.reconnectInterval); + this.reconnectInterval = null; + } + if (this.socket) { + this.socket.close(); + this.socket = null; + } + this.isConnected = false; + console.log('🔌 Unity 연결 종료'); + } +} + +// Property Inspector로 메시지 전송 (단순화) +function sendToPropertyInspector(context, type, data) { + if (!websocket || !context) { + console.log(`⚠️ Property Inspector 전송 실패: ${!websocket ? 'websocket 없음' : 'context 없음'}`); + return; + } + try { + const message = { + type: type, + ...data + }; + websocket.send(JSON.stringify({ + event: 'sendToPropertyInspector', + context: context, + payload: message + })); + console.log(`📤 Property Inspector 전송: ${type} -> ${context.substring(0, 8)}...`); + } catch (error) { + console.log(`❌ Property Inspector 전송 실패: ${error.message}`); + } +} + +function broadcastToPropertyInspectors(type, data) { + buttonContexts.forEach((buttonData, context) => { + sendToPropertyInspector(context, type, data); + }); +} + +function updateButtonTitle(context, cameraName = null, cameraIndex = null) { + if (!websocket || !context) return; + if (!cameraName) { + const buttonData = buttonContexts.get(context); + if (buttonData && buttonData.settings && buttonData.settings.cameraList) { + const index = cameraIndex !== null ? cameraIndex : buttonData.settings.cameraIndex; + if (typeof index === 'number' && buttonData.settings.cameraList[index]) { + cameraName = buttonData.settings.cameraList[index].name; + } + } + } + let title = cameraName || '카메라\n선택'; + if (title.length > 8) { + const underscoreIndex = title.indexOf('_'); + if (underscoreIndex !== -1 && underscoreIndex > 0) { + const firstLine = title.substring(0, underscoreIndex); + const secondLine = title.substring(underscoreIndex + 1); + const maxLineLength = 8; + let line1 = firstLine.length > maxLineLength ? firstLine.substring(0, maxLineLength - 1) + '.' : firstLine; + let line2 = secondLine.length > maxLineLength ? secondLine.substring(0, maxLineLength - 1) + '.' : secondLine; + title = line1 + '\n' + line2; + } else { + const midPoint = Math.ceil(title.length / 2); + const firstLine = title.substring(0, midPoint); + const secondLine = title.substring(midPoint); + title = firstLine + '\n' + secondLine; + } + } + websocket.send(JSON.stringify({ + event: 'setTitle', + context: context, + payload: { + title: title, + target: 0, + titleParameters: { + fontSize: 18, + showTitle: true, + titleAlignment: "middle" + } + } + })); + console.log(`🏷️ 버튼 제목 업데이트: "${title.replace('\n', '\\n')}"`); +} + +function updateButtonUI(context, cameraIndex, isConnected) { + if (!websocket) return; + try { + const svgData = createSvgIcon(cameraIndex, isConnected); + const base64Data = btoa(svgData); + websocket.send(JSON.stringify({ + event: 'setImage', + context: context, + payload: { + image: `data:image/svg+xml;base64,${base64Data}`, + target: 0, + state: 0 + } + })); + updateButtonTitle(context, null, cameraIndex); + } catch (error) { + console.log(`❌ 버튼 UI 업데이트 실패: ${error.message}`); + } +} + +function createSvgIcon(cameraIndex, isConnected) { + const bgColor = isConnected ? '#4CAF50' : '#F44336'; + const textColor = '#FFFFFF'; + return ` + + + + ${cameraIndex + 1} + `; +} + +function updateAllButtons() { + buttonContexts.forEach((buttonData, context) => { + updateButtonUI(context, buttonData.cameraIndex, unityManager ? unityManager.isConnected : false); + }); +} + +const streamDeckEventHandlers = { + willAppear: function(data) { + const { context, payload } = data; + const settings = payload.settings || {}; + const cameraIndex = settings.cameraIndex || 0; + buttonContexts.set(context, { + cameraIndex: cameraIndex, + settings: settings + }); + console.log(`📱 버튼 추가됨: context=${context.substring(0, 8)}..., camera=${cameraIndex}`); + updateButtonUI(context, cameraIndex, unityManager ? unityManager.isConnected : false); + if (unityManager) { + const status = unityManager.getConnectionStatus(); + sendToPropertyInspector(context, 'connection_status', { + connected: status.isConnected + }); + if (status.cameraData) { + sendToPropertyInspector(context, 'camera_data', { + camera_data: status.cameraData, + current_camera: status.currentCamera + }); + } + } + }, + willDisappear: function(data) { + const { context } = data; + buttonContexts.delete(context); + console.log(`👋 버튼 제거됨: ${context.substring(0, 8)}... (남은 버튼: ${buttonContexts.size}개)`); + }, + keyUp: function(data) { + const { context, payload } = data; + const buttonData = buttonContexts.get(context); + if (!buttonData) { + console.log(`❌ 버튼 데이터 없음: ${context.substring(0, 8)}...`); + return; + } + const cameraIndex = buttonData.settings.cameraIndex || 0; + console.log(`🔘 버튼 클릭: context=${context.substring(0, 8)}..., camera=${cameraIndex}`); + if (!unityManager || !unityManager.isConnected) { + console.log('❌ Unity 연결 안됨 - 카메라 전환 불가'); + return; + } + const success = unityManager.switchCamera(cameraIndex); + if (success) { + console.log(`✅ 카메라 전환 요청 성공: ${cameraIndex}`); + } else { + console.log(`❌ 카메라 전환 요청 실패: ${cameraIndex}`); + } + }, + didReceiveSettings: function(data) { + const { context, payload } = data; + const settings = payload.settings || {}; + console.log(`📥 설정 수신: context=${context.substring(0, 8)}..., cameraIndex=${settings.cameraIndex}`); + + const buttonData = buttonContexts.get(context); + if (buttonData) { + // 기존 설정 유지하면서 카메라 인덱스만 업데이트 + const updatedSettings = { ...buttonData.settings }; + + // 카메라 인덱스 업데이트 + if (typeof settings.cameraIndex === 'number') { + updatedSettings.cameraIndex = settings.cameraIndex; + buttonData.cameraIndex = settings.cameraIndex; + console.log(`🎯 카메라 인덱스 업데이트: ${settings.cameraIndex}`); + } + + // 카메라 목록 업데이트 (있는 경우만) + if (settings.cameraList && settings.cameraList.length > 0) { + updatedSettings.cameraList = settings.cameraList; + console.log(`📹 카메라 목록 업데이트: ${settings.cameraList.length}개`); + } + + buttonData.settings = updatedSettings; + + // 버튼 UI 업데이트 + updateButtonUI(context, buttonData.cameraIndex, unityManager ? unityManager.isConnected : false); + + console.log(`✅ 버튼 설정 업데이트 완료: cameraIndex=${buttonData.cameraIndex}`); + } + }, + sendToPlugin: function(data) { + const { context, payload } = data; + try { + const message = typeof payload === 'string' ? JSON.parse(payload) : payload; + console.log(`📨 Property Inspector 메시지: ${message.command || message.type}`); + switch (message.command) { + case 'requestCameraList': + if (unityManager && unityManager.isConnected) { + unityManager.requestCameraList(); + } else { + console.log('❌ Unity 연결 안됨 - 카메라 목록 요청 불가'); + } + break; + case 'forceReconnect': + if (unityManager) { + unityManager.forceReconnect(); + } + break; + case 'getInitialSettings': + console.log('📥 Property Inspector에서 초기 설정 요청'); + // 현재 Unity 연결 상태와 카메라 데이터를 Property Inspector에 전송 + const isConnected = unityManager ? unityManager.isConnected : false; + sendToPropertyInspector(context, 'connection_status', { + connected: isConnected + }); + + if (unityManager && isConnected) { + const status = unityManager.getConnectionStatus(); + if (status.cameraData) { + sendToPropertyInspector(context, 'camera_data', { + camera_data: status.cameraData, + current_camera: status.currentCamera + }); + } + } + break; + default: + console.log(`❓ 알 수 없는 Property Inspector 명령: ${message.command}`); + } + } catch (error) { + console.log(`❌ Property Inspector 메시지 파싱 오류: ${error.message}`); + } + }, + + // Property Inspector가 열릴 때 자동으로 호출되는 핵심 이벤트 + propertyInspectorDidAppear: function(data) { + const { context } = data; + console.log(`📋 Property Inspector 열림: ${context.substring(0, 8)}...`); + + // 현재 버튼의 설정을 PI로 전송 + const buttonData = buttonContexts.get(context); + if (buttonData) { + console.log(`📤 PI에 현재 설정 전송: cameraIndex=${buttonData.settings.cameraIndex}`); + + // Unity 연결 강제 확인 및 시도 + if (unityManager) { + console.log(`🔍 Unity 연결 상태 강제 확인`); + const currentStatus = unityManager.getConnectionStatus(); + console.log(`🔍 현재 Unity 연결 상태: ${currentStatus.isConnected}`); + + // 연결되지 않았다면 연결 시도 + if (!currentStatus.isConnected) { + console.log(`🔄 Unity 연결 시도 중...`); + unityManager.connect(); + } + } + + // Unity 연결 상태 확인 + const isUnityConnected = unityManager ? unityManager.isConnected : false; + console.log(`🔍 Unity 연결 상태 확인: ${isUnityConnected}`); + + // 현재 설정 전송 (Unity 연결 상태 포함) + sendToPropertyInspector(context, 'current_settings', { + cameraIndex: buttonData.settings.cameraIndex || 0, + cameraList: buttonData.settings.cameraList || [], + isUnityConnected: isUnityConnected + }); + + // Unity 연결 상태 별도 전송 + sendToPropertyInspector(context, 'connection_status', { + connected: isUnityConnected + }); + + // Unity가 연결되어 있고 카메라 데이터가 있으면 전송 + if (unityManager && isUnityConnected) { + const status = unityManager.getConnectionStatus(); + if (status.cameraData) { + sendToPropertyInspector(context, 'camera_data', { + camera_data: status.cameraData, + current_camera: status.currentCamera + }); + } + } + } else { + console.log(`❌ 버튼 데이터 없음: ${context.substring(0, 8)}...`); + } + }, + + // Property Inspector가 닫힐 때 호출 + propertyInspectorDidDisappear: function(data) { + const { context } = data; + console.log(`📋 Property Inspector 닫힘: ${context.substring(0, 8)}...`); + }, + + // getSettings 이벤트 처리 (PI에서 설정 요청 시) + getSettings: function(data) { + const { context } = data; + console.log(`📥 설정 요청: ${context.substring(0, 8)}...`); + + const buttonData = buttonContexts.get(context); + if (buttonData) { + // 현재 설정을 PI로 전송 + sendToPropertyInspector(context, 'current_settings', { + cameraIndex: buttonData.settings.cameraIndex || 0, + cameraList: buttonData.settings.cameraList || [], + isUnityConnected: unityManager ? unityManager.isConnected : false + }); + } + } +}; + +function setupUnityEventListeners() { + if (!unityManager) return; + unityManager.on('connectionChange', (isConnected) => { + console.log(`🔄 Unity 연결 상태 변경: ${isConnected}`); + updateAllButtons(); + + // Property Inspector들에게 연결 상태 변경 알림 + broadcastToPropertyInspectors('connection_status', { + connected: isConnected + }); + + // 연결이 되면 카메라 목록 요청 + if (isConnected) { + console.log('📹 Unity 연결됨 - 카메라 목록 요청'); + unityManager.requestCameraList(); + } + }); +} + +function connectElgatoStreamDeckSocket(inPort, inUUID, inEvent, inInfo, inActionInfo) { + uuid = inUUID; + actionInfo = inActionInfo; + connectionInfo = inInfo; + console.log(`🔌 StreamDeck 소켓 연결: port=${inPort}, uuid=${uuid.substring(0, 8)}...`); + websocket = new window.WebSocket('ws://localhost:' + inPort); + websocket.onopen = function() { + console.log('✅ StreamDeck 소켓 연결 성공'); + const json = { + event: inEvent, + uuid: inUUID + }; + websocket.send(JSON.stringify(json)); + if (!unityManager) { + unityManager = new UnityConnectionManager(); + setupUnityEventListeners(); + // 즉시 Unity 연결 시도 + console.log('🚀 StreamDeck 연결 완료 - Unity 연결 시도'); + unityManager.connect(); + } + }; + websocket.onmessage = function(evt) { + try { + const jsonObj = JSON.parse(evt.data); + const event = jsonObj.event; + const context = jsonObj.context; + console.log(`📨 StreamDeck 이벤트: ${event} (context: ${context ? context.substring(0, 8) + '...' : 'N/A'})`); + console.log(`📨 전체 메시지:`, jsonObj); + + if (streamDeckEventHandlers[event]) { + streamDeckEventHandlers[event](jsonObj); + } else { + console.log(`❓ 알 수 없는 StreamDeck 이벤트: ${event}`); + } + } catch (error) { + console.log(`❌ StreamDeck 메시지 파싱 오류: ${error.message}`); + console.log(`❌ 원본 메시지: ${evt.data}`); + } + }; + websocket.onerror = function(evt) { + console.log(`❌ StreamDeck 소켓 오류: ${evt}`); + }; + websocket.onclose = function(evt) { + console.log(`🔌 StreamDeck 소켓 연결 종료: ${evt.code} - ${evt.reason}`); + if (unityManager) { + unityManager.disconnect(); + } + }; +} + +// 프로세스 종료 시 정리 +if (typeof process !== 'undefined' && process.on) { + process.on('SIGINT', function() { + console.log('🛑 프로세스 종료 신호 수신'); + if (unityManager) { + unityManager.disconnect(); + } + if (websocket) { + websocket.close(); + } + process.exit(0); +}); +} + +// 모듈 내보내기 (테스트/호환용) +if (typeof module !== 'undefined') { + module.exports = { + connectElgatoStreamDeckSocket + }; +} diff --git a/Streamdeck/com.mirabox.streamingle.sdPlugin/dist-dev/com.mirabox.streamingle.sdPlugin/plugin/log/streamingle_plugin b/Streamdeck/com.mirabox.streamingle.sdPlugin/dist-dev/com.mirabox.streamingle.sdPlugin/plugin/log/streamingle_plugin new file mode 100644 index 00000000..e69de29b diff --git a/StreamDock-Plugin-SDK/SDNodeJsSDK/unity-communication-plugin/plugin/utils/plugin.js b/Streamdeck/com.mirabox.streamingle.sdPlugin/dist-dev/com.mirabox.streamingle.sdPlugin/plugin/utils/plugin.js similarity index 69% rename from StreamDock-Plugin-SDK/SDNodeJsSDK/unity-communication-plugin/plugin/utils/plugin.js rename to Streamdeck/com.mirabox.streamingle.sdPlugin/dist-dev/com.mirabox.streamingle.sdPlugin/plugin/utils/plugin.js index 0451860e..b7a9fcb1 100644 --- a/StreamDock-Plugin-SDK/SDNodeJsSDK/unity-communication-plugin/plugin/utils/plugin.js +++ b/Streamdeck/com.mirabox.streamingle.sdPlugin/dist-dev/com.mirabox.streamingle.sdPlugin/plugin/utils/plugin.js @@ -9,54 +9,45 @@ const log = require('log4js').configure({ } }).getLogger(); -// 전역 예외 처리 +//################################################## +//##################全局异常捕获##################### process.on('uncaughtException', (error) => { log.error('Uncaught Exception:', error); }); process.on('unhandledRejection', (reason) => { log.error('Unhandled Rejection:', reason); }); +//################################################## +//################################################## -// 플러그인 클래스 + +// 插件类 const ws = require('ws'); class Plugins { static language = process.argv[9] ? JSON.parse(process.argv[9]).application.language : 'en'; static globalSettings = {}; getGlobalSettingsFlag = true; - constructor() { if (Plugins.instance) { return Plugins.instance; } - - log.info("process.argv", process.argv); + // log.info("process.argv", process.argv); this.ws = new ws("ws://127.0.0.1:" + process.argv[3]); this.ws.on('open', () => this.ws.send(JSON.stringify({ uuid: process.argv[5], event: process.argv[7] }))); this.ws.on('close', process.exit); this.ws.on('message', e => { if (this.getGlobalSettingsFlag) { - // 한 번만 가져오기 + // 只获取一次 this.getGlobalSettingsFlag = false; this.getGlobalSettings(); } const data = JSON.parse(e.toString()); - log.info('Received event:', data); - const action = data.action?.split('.').pop(); - - // 액션별 이벤트 처리 - if (this[action] && this[action][data.event]) { - log.info(`Calling ${action}.${data.event}`); - this[action][data.event](data); - } - - // 전역 이벤트 처리 + this[action]?.[data.event]?.(data); if (data.event === 'didReceiveGlobalSettings') { Plugins.globalSettings = data.payload.settings; } - if (this[data.event]) { - this[data.event](data); - } + this[data.event]?.(data); }); Plugins.instance = this; } @@ -65,8 +56,7 @@ class Plugins { Plugins.globalSettings = payload; this.ws.send(JSON.stringify({ event: "setGlobalSettings", - context: process.argv[5], - payload + context: process.argv[5], payload })); } @@ -76,8 +66,7 @@ class Plugins { context: process.argv[5], })); } - - // 제목 설정 + // 设置标题 setTitle(context, str, row = 0, num = 6) { let newStr = null; if (row && str) { @@ -90,45 +79,38 @@ class Plugins { } this.ws.send(JSON.stringify({ event: "setTitle", - context, - payload: { + context, payload: { target: 0, title: newStr || str + '' } })); } - - // 이미지 설정 + // 设置背景 setImage(context, url) { this.ws.send(JSON.stringify({ event: "setImage", - context, - payload: { + context, payload: { target: 0, image: url } })); } - - // 상태 설정 + // 设置状态 setState(context, state) { this.ws.send(JSON.stringify({ event: "setState", - context, - payload: { state } + context, payload: { state } })); } - - // 설정 저장 + // 保存持久化数据 setSettings(context, payload) { this.ws.send(JSON.stringify({ event: "setSettings", - context, - payload + context, payload })); } - // 경고 표시 + // 在按键上展示警告 showAlert(context) { this.ws.send(JSON.stringify({ event: "showAlert", @@ -136,56 +118,50 @@ class Plugins { })); } - // 성공 표시 + // 在按键上展示成功 showOk(context) { this.ws.send(JSON.stringify({ event: "showOk", context })); } - - // 속성 검사기로 전송 + // 发送给属性检测器 sendToPropertyInspector(payload) { this.ws.send(JSON.stringify({ action: Actions.currentAction, context: Actions.currentContext, - payload, - event: "sendToPropertyInspector" + payload, event: "sendToPropertyInspector" })); } - - // URL 열기 + // 用默认浏览器打开网页 openUrl(url) { this.ws.send(JSON.stringify({ event: "openUrl", payload: { url } })); } -} +}; -// 액션 클래스 +// 操作类 class Actions { constructor(data) { this.data = {}; this.default = {}; Object.assign(this, data); } - - // 속성 검사기 관련 + // 属性检查器显示时 static currentAction = null; static currentContext = null; static actions = {}; - propertyInspectorDidAppear(data) { Actions.currentAction = data.action; Actions.currentContext = data.context; this._propertyInspectorDidAppear?.(data); } - - // 액션 초기화 + // 初始化数据 willAppear(data) { Plugins.globalContext = data.context; - Actions.actions[data.context] = data.action; + Actions.actions[data.context] = data.action const { context, payload: { settings } } = data; this.data[context] = Object.assign({ ...this.default }, settings); this._willAppear?.(data); @@ -195,46 +171,43 @@ class Actions { this.data[data.context] = data.payload.settings; this._didReceiveSettings?.(data); } - - // 키 이벤트 - keyUp(data) { - log.info('keyUp called with data:', data); - if (typeof this._keyUp === 'function') { - this._keyUp(data); - } - } - - keyDown(data) { - log.info('keyDown called with data:', data); - if (typeof this._keyDown === 'function') { - this._keyDown(data); - } - } - - // 다이얼 이벤트 - dialRotate(data) { - log.info('dialRotate called with data:', data); - if (typeof this._dialRotate === 'function') { - this._dialRotate(data); - } - } - - dialDown(data) { - log.info('dialDown called with data:', data); - if (typeof this._dialDown === 'function') { - this._dialDown(data); - } - } - - // 액션 해제 + // 行动销毁 willDisappear(data) { this._willDisappear?.(data); delete this.data[data.context]; } } +class EventEmitter { + constructor() { + this.events = {}; + } + + // 订阅事件 + subscribe(event, listener) { + if (!this.events[event]) { + this.events[event] = []; + } + this.events[event].push(listener); + } + + // 取消订阅 + unsubscribe(event, listenerToRemove) { + if (!this.events[event]) return; + + this.events[event] = this.events[event].filter(listener => listener !== listenerToRemove); + } + + // 发布事件 + emit(event, data) { + if (!this.events[event]) return; + this.events[event].forEach(listener => listener(data)); + } +} + module.exports = { log, Plugins, Actions, + EventEmitter }; \ No newline at end of file diff --git a/Streamdeck/com.mirabox.streamingle.sdPlugin/dist-dev/com.mirabox.streamingle.sdPlugin/propertyInspector/camera/index.html b/Streamdeck/com.mirabox.streamingle.sdPlugin/dist-dev/com.mirabox.streamingle.sdPlugin/propertyInspector/camera/index.html new file mode 100644 index 00000000..0e2f2161 --- /dev/null +++ b/Streamdeck/com.mirabox.streamingle.sdPlugin/dist-dev/com.mirabox.streamingle.sdPlugin/propertyInspector/camera/index.html @@ -0,0 +1,187 @@ + + + + + Streamingle Camera Inspector + + + + +
+
+ Unity 연결 안됨 +
+ + +
+ +
+ +
+
현재 카메라: -
+
+ + +
+ + +
+ + +
+ +
+ + +
+ +
+
+ + + + + \ No newline at end of file diff --git a/Streamdeck/com.mirabox.streamingle.sdPlugin/dist-dev/com.mirabox.streamingle.sdPlugin/propertyInspector/camera/index.js b/Streamdeck/com.mirabox.streamingle.sdPlugin/dist-dev/com.mirabox.streamingle.sdPlugin/propertyInspector/camera/index.js new file mode 100644 index 00000000..74a97451 --- /dev/null +++ b/Streamdeck/com.mirabox.streamingle.sdPlugin/dist-dev/com.mirabox.streamingle.sdPlugin/propertyInspector/camera/index.js @@ -0,0 +1,778 @@ +/** + * Streamingle Camera Controller - Property Inspector + * 엘가토 공식 구조 기반 단순화 버전 + */ + +// Global variables +let websocket = null; +let uuid = null; +let actionContext = null; // 현재 액션의 컨텍스트 +let settings = {}; + +// Context별 설정 관리 +const contextSettings = new Map(); +let currentActionContext = null; + +// Unity 연결 상태 (Plugin Main에서 받아옴) +let isUnityConnected = false; +let cameraData = []; +let currentCamera = 0; + +// DOM elements +let statusDot = null; +let connectionStatus = null; +let cameraSelect = null; +let currentCameraDisplay = null; +let refreshButton = null; + +// 화면에 로그를 표시하는 함수 +function logToScreen(msg, color = "#fff") { + let logDiv = document.getElementById('logArea'); + if (!logDiv) { + logDiv = document.createElement('div'); + logDiv.id = 'logArea'; + logDiv.style.background = '#111'; + logDiv.style.color = '#fff'; + logDiv.style.fontSize = '11px'; + logDiv.style.padding = '8px'; + logDiv.style.marginTop = '16px'; + logDiv.style.height = '120px'; + logDiv.style.overflowY = 'auto'; + document.body.appendChild(logDiv); + } + const line = document.createElement('div'); + line.style.color = color; + line.textContent = `[${new Date().toLocaleTimeString()}] ${msg}`; + logDiv.appendChild(line); + logDiv.scrollTop = logDiv.scrollHeight; +} + +// 기존 console.log/console.error를 화면에도 출력 +const origLog = console.log; +console.log = function(...args) { + origLog.apply(console, args); + logToScreen(args.map(a => (typeof a === 'object' ? JSON.stringify(a) : a)).join(' '), '#0f0'); +}; +const origErr = console.error; +console.error = function(...args) { + origErr.apply(console, args); + logToScreen(args.map(a => (typeof a === 'object' ? JSON.stringify(a) : a)).join(' '), '#f55'); +}; + +console.log('🔧 Property Inspector script loaded'); + +// Initialize when DOM is loaded +document.addEventListener('DOMContentLoaded', function() { + console.log('📋 Property Inspector 초기화'); + initializePropertyInspector(); +}); + +// Initialize Property Inspector +function initializePropertyInspector() { + // Get DOM elements + statusDot = document.getElementById('statusDot'); + connectionStatus = document.getElementById('connection-status'); + cameraSelect = document.getElementById('camera-select'); + currentCameraDisplay = document.getElementById('current-camera'); + refreshButton = document.getElementById('refresh-button'); + + // Setup event listeners + if (cameraSelect) { + cameraSelect.addEventListener('change', onCameraSelectionChanged); + } + + if (refreshButton) { + refreshButton.addEventListener('click', onRefreshClicked); + } + + console.log('✅ Property Inspector 준비 완료'); +} + +// Send message to plugin +function sendToPlugin(command, data = {}) { + if (!websocket) { + console.error('❌ WebSocket not available'); + return; + } + + try { + const message = { + command: command, + context: uuid, + ...data + }; + + // StreamDeck SDK 표준 방식 - sendToPlugin 이벤트 사용 + const payload = { + event: 'sendToPlugin', + context: uuid, + payload: message + }; + + websocket.send(JSON.stringify(payload)); + console.log('📤 Message sent to plugin:', command, data); + } catch (error) { + console.error('❌ Failed to send message to plugin:', error); + } +} + +// Update connection status display +function updateConnectionStatus(isConnected) { + console.log('🔄 Connection status update:', isConnected); + + // 전역 변수도 업데이트 + isUnityConnected = isConnected; + + if (statusDot) { + statusDot.className = `dot ${isConnected ? 'green' : 'red'}`; + } + + if (connectionStatus) { + connectionStatus.textContent = isConnected ? 'Unity 연결됨' : 'Unity 연결 안됨'; + connectionStatus.className = isConnected ? 'connected' : 'disconnected'; + } + + if (cameraSelect) { + cameraSelect.disabled = !isConnected; + } + + if (refreshButton) { + refreshButton.disabled = !isConnected; + } +} + +// Update camera data display +function updateCameraData(cameraDataParam, currentCamera) { + console.log('📹 Camera data update:', cameraDataParam, currentCamera); + + if (cameraSelect && cameraDataParam) { + // Clear existing options + cameraSelect.innerHTML = ''; + + // cameraDataParam이 직접 배열인지 확인 + let cameras = cameraDataParam; + if (cameraDataParam.cameras) { + cameras = cameraDataParam.cameras; + } else if (Array.isArray(cameraDataParam)) { + cameras = cameraDataParam; + } + + console.log('📹 처리할 카메라 배열:', cameras); + + if (cameras && cameras.length > 0) { + // 전역 변수에 카메라 데이터 저장 + cameraData = cameras; + console.log('💾 전역 cameraData 저장됨:', cameraData.length + '개'); + + // Add camera options + cameras.forEach((camera, index) => { + const option = document.createElement('option'); + option.value = index; + option.textContent = `카메라 ${index + 1}`; + if (camera.name) { + option.textContent += ` (${camera.name})`; + } + cameraSelect.appendChild(option); + }); + + // Set current selection + if (typeof currentCamera === 'number') { + cameraSelect.value = currentCamera; + } + + cameraSelect.disabled = false; + console.log('✅ 카메라 목록 업데이트 완료:', cameras.length + '개'); + } else { + console.log('⚠️ 카메라 데이터가 없거나 빈 배열'); + cameraSelect.disabled = true; + } + } + + // Update current camera display + updateCurrentCameraDisplay(currentCamera); +} + +// Update current camera display +function updateCurrentCameraDisplay(currentCamera) { + if (currentCameraDisplay) { + if (typeof currentCamera === 'number') { + currentCameraDisplay.textContent = `현재 카메라: ${currentCamera + 1}`; + } else { + currentCameraDisplay.textContent = '현재 카메라: -'; + } + } +} + +// Handle camera selection change +function onCameraSelectionChanged() { + if (!cameraSelect || !currentActionContext) return; + + const selectedIndex = parseInt(cameraSelect.value, 10); + if (isNaN(selectedIndex)) return; + + console.log('🎯 카메라 선택 변경:', selectedIndex); + console.log('📋 현재 cameraData:', cameraData); + console.log('📋 cameraData 길이:', cameraData ? cameraData.length : 'undefined'); + + // StreamDeck에 설정 저장 (Plugin Main에서 didReceiveSettings 이벤트 발생) + if (websocket) { + const setSettingsMessage = { + event: 'setSettings', + context: currentActionContext, + payload: { + cameraIndex: selectedIndex, + cameraList: cameraData // 현재 카메라 목록 포함 + } + }; + + console.log('📤 Plugin Main으로 전송할 데이터:', setSettingsMessage.payload); + websocket.send(JSON.stringify(setSettingsMessage)); + console.log('💾 설정 저장됨 - Plugin Main에서 버튼 제목 업데이트됨'); + } + + // UI 업데이트 + updateCurrentCameraDisplay(selectedIndex); +} + +// Handle refresh button click +function onRefreshClicked() { + console.log('🔄 새로고침 버튼 클릭 - Plugin Main에 카메라 목록 요청'); + + // Plugin Main에 카메라 목록 요청 + sendToPlugin('requestCameraList'); +} + +// Unity 연결은 Plugin Main에서만 처리 +function startUnityAutoReconnect() { + console.log('🩺 Property Inspector에서는 Unity 연결을 직접 관리하지 않음 - Plugin Main에서 처리됨'); +} + +// Unity 재연결 시도 (제거) +function attemptUnityReconnect() { + if (isShuttingDown || isConnecting || unityReconnectInterval) return; + + unityConnectionAttempts++; + + // 재연결 간격 조정 + let delay; + if (unityConnectionAttempts <= 3) { + delay = 2000; // 처음 3번은 2초 간격 + } else if (unityConnectionAttempts <= 10) { + delay = 5000; // 4-10번은 5초 간격 + } else { + delay = 30000; // 그 이후는 30초 간격 + } + + console.log(`🔄 [Property Inspector] ${delay/1000}초 후 Unity 재연결 시도... (${unityConnectionAttempts}번째 시도)`); + + unityReconnectInterval = setTimeout(() => { + unityReconnectInterval = null; + connectToUnity().catch(error => { + console.error(`❌ [Property Inspector] Unity 재연결 실패:`, error); + // 실패해도 계속 시도 + if (!isShuttingDown) { + attemptUnityReconnect(); + } + }); + }, delay); +} + +// Unity WebSocket 연결 (개선된 버전) +function connectToUnity() { + return new Promise((resolve, reject) => { + // 글로벌 상태 확인 + if (window.sharedUnityConnected && window.sharedUnitySocket) { + console.log('✅ [Property Inspector] 기존 Unity 연결 재사용'); + isUnityConnected = true; + unitySocket = window.sharedUnitySocket; + updateConnectionStatus(true); + resolve(); + return; + } + + if (isUnityConnected) { + console.log('✅ [Property Inspector] Unity 이미 연결됨'); + resolve(); + return; + } + + if (isConnecting) { + console.log('⏳ [Property Inspector] Unity 연결 중... 대기'); + reject(new Error('이미 연결 중')); + return; + } + + isConnecting = true; + window.sharedIsConnecting = true; + console.log(`🔌 [Property Inspector] Unity 연결 시도... (시도 ${unityConnectionAttempts + 1}회)`); + + try { + unitySocket = new WebSocket('ws://localhost:10701'); + + const connectionTimeout = setTimeout(() => { + isConnecting = false; + window.sharedIsConnecting = false; + console.log('⏰ [Property Inspector] Unity 연결 타임아웃'); + if (unitySocket) { + unitySocket.close(); + } + reject(new Error('연결 타임아웃')); + }, 5000); + + unitySocket.onopen = function() { + clearTimeout(connectionTimeout); + isConnecting = false; + isUnityConnected = true; + unityConnectionAttempts = 0; // 성공 시 재시도 카운터 리셋 + + // 재연결 타이머 정리 + if (unityReconnectInterval) { + clearTimeout(unityReconnectInterval); + unityReconnectInterval = null; + } + + // 글로벌 상태 저장 + window.sharedUnitySocket = unitySocket; + window.sharedUnityConnected = true; + window.sharedIsConnecting = false; + + console.log('✅ [Property Inspector] Unity 연결 성공!'); + updateConnectionStatus(true); + resolve(); + }; + + unitySocket.onmessage = function(event) { + try { + const message = JSON.parse(event.data); + handleUnityMessage(message); + } catch (error) { + console.error('❌ [Property Inspector] Unity 메시지 파싱 오류:', error); + } + }; + + unitySocket.onclose = function(event) { + clearTimeout(connectionTimeout); + const wasConnected = isUnityConnected; + isConnecting = false; + isUnityConnected = false; + + // 글로벌 상태 정리 + window.sharedUnitySocket = null; + window.sharedUnityConnected = false; + window.sharedIsConnecting = false; + + if (wasConnected) { + console.log(`❌ [Property Inspector] Unity 연결 끊어짐 (코드: ${event.code}, 이유: ${event.reason || '알 수 없음'})`); + } + updateConnectionStatus(false); + unitySocket = null; + + if (!event.wasClean) { + reject(new Error('연결 실패')); + } + + // 자동 재연결 시도 + if (!isShuttingDown) { + attemptUnityReconnect(); + } + }; + + unitySocket.onerror = function(error) { + clearTimeout(connectionTimeout); + isConnecting = false; + window.sharedIsConnecting = false; + console.error('❌ [Property Inspector] Unity 연결 오류:', error); + isUnityConnected = false; + updateConnectionStatus(false); + reject(error); + }; + + } catch (error) { + isConnecting = false; + window.sharedIsConnecting = false; + console.error('❌ [Property Inspector] Unity WebSocket 생성 실패:', error); + reject(error); + } + }); +} + +// Unity 메시지 처리 +function handleUnityMessage(message) { + const messageType = message.type; + + if (messageType === 'connection_established') { + console.log('🎉 Unity 연결 확인됨'); + if (message.data && message.data.camera_data) { + console.log('📹 연결 시 카메라 데이터 수신 (초기 로드)'); + updateCameraUI(message.data.camera_data.presets, message.data.camera_data.current_index); + // 이미 카메라 데이터를 받았으므로 추가 요청하지 않음 + cameraData = message.data.camera_data.presets; // 글로벌 변수에 저장 + window.sharedCameraData = cameraData; // 브라우저 세션에서 공유 + } + } else if (messageType === 'camera_list_response') { + console.log('📹 카메라 목록 응답 수신 (요청에 대한 응답)'); + if (message.data && message.data.camera_data) { + updateCameraUI(message.data.camera_data.presets, message.data.camera_data.current_index); + cameraData = message.data.camera_data.presets; // 글로벌 변수에 저장 + window.sharedCameraData = cameraData; // 브라우저 세션에서 공유 + } + } else if (messageType === 'camera_changed') { + console.log('📹 카메라 변경 알림 수신'); + if (message.data && typeof message.data.camera_index === 'number') { + updateCurrentCamera(message.data.camera_index); + } + } +} + +function updateCameraUI(cameras, currentIndex) { + if (!cameras || !Array.isArray(cameras)) { + console.error('❌ 잘못된 카메라 데이터'); + return; + } + + console.log('📹 카메라 UI 업데이트:', cameras.length + '개'); + + const cameraSelect = document.getElementById('camera-select'); + const currentCameraDisplay = document.getElementById('current-camera'); + + if (!cameraSelect) { + console.error('❌ camera-select 요소를 찾을 수 없음'); + return; + } + + // 카메라 목록 업데이트 + cameraSelect.innerHTML = ''; + + cameras.forEach((camera, index) => { + const option = document.createElement('option'); + option.value = index; + option.textContent = `${index + 1}. ${camera.name}`; + cameraSelect.appendChild(option); + }); + + // 현재 컨텍스트의 설정 가져오기 + const currentSettings = getContextSettings(currentActionContext); + + // 카메라 목록 저장 + const newSettings = { + ...currentSettings, + cameraList: cameras + }; + saveContextSettings(currentActionContext, newSettings); + + // 이미 설정된 카메라가 있으면 선택 + if (currentSettings && typeof currentSettings.cameraIndex === 'number') { + cameraSelect.value = currentSettings.cameraIndex; + + // 설정된 카메라의 이름으로 현재 표시만 업데이트 (버튼 제목은 Plugin Main에서 처리) + const selectedCamera = cameras[currentSettings.cameraIndex]; + if (selectedCamera) { + currentCameraDisplay.textContent = `현재: ${selectedCamera.name}`; + console.log('📋 기존 카메라 설정 복원:', selectedCamera.name); + } + } else { + // 설정이 없으면 Unity의 현재 카메라 사용 + if (typeof currentIndex === 'number' && currentIndex >= 0 && cameras[currentIndex]) { + cameraSelect.value = currentIndex; + currentCameraDisplay.textContent = `현재: ${cameras[currentIndex].name}`; + } else { + currentCameraDisplay.textContent = '현재: 없음'; + } + } + + console.log('✅ 카메라 UI 업데이트 완료'); +} + +// Unity에서 카메라 목록 요청 +function requestCameraListFromUnity() { + if (isUnityConnected && unitySocket) { + const message = JSON.stringify({ type: 'get_camera_list' }); + unitySocket.send(message); + console.log('📤 Unity에 카메라 목록 요청:', message); + } +} + +// Unity에서 카메라 전환 +function switchCameraInUnity(cameraIndex) { + if (isUnityConnected && unitySocket) { + const message = JSON.stringify({ + type: 'switch_camera', + data: { + camera_index: cameraIndex + } + }); + unitySocket.send(message); + console.log('📤 Unity에 카메라 전환 요청:', message); + } +} + +// Unity에서 받은 카메라 데이터로 UI 업데이트 +function updateCameraDataFromUnity() { + console.log('📹 Unity 카메라 데이터로 UI 업데이트:', cameraData); + + if (cameraSelect && cameraData && cameraData.length > 0) { + // Clear existing options + cameraSelect.innerHTML = ''; + + // Add camera options + cameraData.forEach((camera, index) => { + const option = document.createElement('option'); + option.value = index; + option.textContent = camera.name || `카메라 ${index + 1}`; + cameraSelect.appendChild(option); + }); + + // Set current selection + cameraSelect.value = currentCamera; + cameraSelect.disabled = false; + + console.log('✅ 카메라 선택 목록 업데이트 완료'); + } + + // Update current camera display + updateCurrentCameraDisplay(currentCamera); +} + +// Handle messages from plugin +function handleMessage(jsonObj) { + console.log('📨 Message received:', jsonObj); + + try { + if (jsonObj.event === 'sendToPropertyInspector') { + // payload는 직접 객체로 전달됨 + const payload = jsonObj.payload; + + console.log('📋 Parsed payload:', payload); + + switch (payload.type || payload.command) { + case 'connection_status': + console.log('📡 연결 상태 업데이트:', payload.connected); + updateConnectionStatus(payload.connected); + break; + + case 'camera_data': + console.log('📹 카메라 데이터 업데이트 수신:', payload); + console.log('📹 카메라 데이터 상세:', payload.camera_data); + updateCameraData(payload.camera_data, payload.current_camera); + break; + + case 'current_settings': + console.log('⚙️ 현재 설정 수신:', payload); + // 현재 설정을 컨텍스트에 저장 + if (currentActionContext) { + const newSettings = { + cameraIndex: payload.cameraIndex || 0, + cameraList: payload.cameraList || [], + isUnityConnected: payload.isUnityConnected || false + }; + saveContextSettings(currentActionContext, newSettings); + + // UI 업데이트 + updateConnectionStatus(payload.isUnityConnected); + if (payload.cameraList && payload.cameraList.length > 0) { + updateCameraData({ cameras: payload.cameraList }, payload.cameraIndex); + } + } + break; + + case 'camera_changed': + console.log('📹 카메라 변경:', payload.current_camera); + updateCurrentCameraDisplay(payload.current_camera); + break; + + default: + console.log('❓ Unknown message type:', payload.type || payload.command); + } + } else if (jsonObj.event === 'didReceiveSettings') { + // didReceiveSettings 이벤트 처리 추가 + console.log('⚙️ didReceiveSettings 이벤트 수신:', jsonObj); + + const settings = jsonObj.payload.settings || {}; + console.log('📋 설정 데이터:', settings); + + // 컨텍스트에 설정 저장 + if (currentActionContext) { + saveContextSettings(currentActionContext, settings); + + // UI 업데이트 + if (settings.cameraList && settings.cameraList.length > 0) { + console.log('📹 카메라 목록으로 UI 업데이트:', settings.cameraList.length + '개'); + updateCameraData(settings.cameraList, settings.cameraIndex || 0); + + // 카메라 목록이 있다면 Unity가 연결되어 있다고 판단 + console.log('🔍 카메라 목록 존재 - Unity 연결됨으로 판단'); + updateConnectionStatus(true); + } + + // 연결 상태 업데이트 (카메라 목록이 없을 때만 설정값 사용) + if (typeof settings.isUnityConnected === 'boolean' && (!settings.cameraList || settings.cameraList.length === 0)) { + updateConnectionStatus(settings.isUnityConnected); + } + } + } + } catch (error) { + console.error('❌ Failed to handle message:', error); + } +} + +// StreamDeck SDK connection +function connectElgatoStreamDeckSocket(inPort, inPropertyInspectorUUID, inRegisterEvent, inInfo, inActionInfo) { + uuid = inPropertyInspectorUUID; + + console.log('🔌 StreamDeck 연결 중...'); + + // Parse info + try { + const info = JSON.parse(inInfo); + const actionInfo = JSON.parse(inActionInfo); + actionContext = actionInfo.context; // 액션 컨텍스트 저장 + currentActionContext = actionInfo.context; // 현재 액션 컨텍스트 설정 + settings = actionInfo.payload.settings || {}; + + // 컨텍스트별 설정 초기화 + saveContextSettings(currentActionContext, settings); + + console.log('📋 컨텍스트 설정 완료:', currentActionContext); + } catch (error) { + console.error('❌ 정보 파싱 실패:', error); + } + + // Connect to StreamDock + websocket = new WebSocket('ws://127.0.0.1:' + inPort); + + websocket.onopen = function() { + console.log('✅ StreamDeck 연결됨'); + + // Register + const json = { + event: inRegisterEvent, + uuid: uuid + }; + + websocket.send(JSON.stringify(json)); + + // Plugin Main에 초기 설정 요청 + console.log('📤 Plugin Main에 초기 설정 요청'); + sendToPlugin('getInitialSettings'); + }; + + websocket.onmessage = function(evt) { + try { + const jsonObj = JSON.parse(evt.data); + handleMessage(jsonObj); + } catch (error) { + console.error('❌ Failed to parse message:', error); + } + }; + + websocket.onclose = function() { + console.log('❌ StreamDeck WebSocket closed'); + isShuttingDown = true; // 종료 플래그 설정 + + // Unity 자동 재연결 정리 + if (unityReconnectInterval) { + clearTimeout(unityReconnectInterval); + unityReconnectInterval = null; + } + if (unityHealthCheckInterval) { + clearInterval(unityHealthCheckInterval); + unityHealthCheckInterval = null; + } + + websocket = null; + }; + + websocket.onerror = function(error) { + console.error('❌ StreamDeck WebSocket error:', error); + }; +} + +// 컨텍스트별 설정 관리 함수들 +function getContextSettings(context) { + if (!context) return {}; + return contextSettings.get(context) || {}; +} + +function saveContextSettings(context, newSettings) { + if (!context) return; + contextSettings.set(context, { ...newSettings }); + console.log('💾 컨텍스트 설정 저장:', context, newSettings); +} + +// 현재 컨텍스트의 설정에서 카메라 이름을 가져오는 공통 함수 +function getCurrentCameraName(context, cameraIndex = null) { + if (!context) return '카메라\n선택'; + + const settings = getContextSettings(context); + if (!settings || !settings.cameraList) return '카메라\n선택'; + + // cameraIndex가 제공되면 그것을 사용, 아니면 설정에서 가져옴 + const index = cameraIndex !== null ? cameraIndex : settings.cameraIndex; + if (typeof index !== 'number' || !settings.cameraList[index]) return '카메라\n선택'; + + return settings.cameraList[index].name || '카메라\n선택'; +} + +// 버튼 제목 업데이트 공통 함수 (Plugin Main과 동일한 로직) +function updateButtonTitle(context, cameraName = null, cameraIndex = null) { + if (!websocket || !context) return; + + // cameraName이 제공되지 않으면 현재 설정에서 가져옴 + if (!cameraName) { + cameraName = getCurrentCameraName(context, cameraIndex); + console.log(`🔍 [Property Inspector] getCurrentCameraName 결과: "${cameraName}"`); + + // 디버깅을 위해 현재 설정 상태도 출력 + const settings = getContextSettings(context); + console.log(`🔍 [Property Inspector] 컨텍스트 설정:`, settings); + console.log(`🔍 [Property Inspector] 카메라 인덱스: ${cameraIndex !== null ? cameraIndex : settings.cameraIndex}, 목록 길이: ${settings.cameraList ? settings.cameraList.length : 0}`); + } else { + console.log(`🔍 [Property Inspector] 직접 제공된 카메라 이름: "${cameraName}"`); + } + + // 기본값 설정 + let title = cameraName || '카메라\n선택'; + console.log(`🔍 [Property Inspector] 최종 사용할 제목 (가공 전): "${title}"`); + + // 긴 텍스트를 두 줄로 나누기 (Plugin Main과 동일한 로직) + if (title.length > 8) { + const underscoreIndex = title.indexOf('_'); + + if (underscoreIndex !== -1 && underscoreIndex > 0) { + // 언더스코어가 있으면 그 위치에서 분할하고 언더스코어는 제거 + const firstLine = title.substring(0, underscoreIndex); + const secondLine = title.substring(underscoreIndex + 1); // +1로 언더스코어 제거 + + // 각 줄이 너무 길면 적절히 자르기 + const maxLineLength = 8; + let line1 = firstLine.length > maxLineLength ? firstLine.substring(0, maxLineLength - 1) + '.' : firstLine; + let line2 = secondLine.length > maxLineLength ? secondLine.substring(0, maxLineLength - 1) + '.' : secondLine; + + title = line1 + '\n' + line2; + } else { + // 언더스코어가 없으면 중간 지점에서 분할 + const midPoint = Math.ceil(title.length / 2); + const firstLine = title.substring(0, midPoint); + const secondLine = title.substring(midPoint); + title = firstLine + '\n' + secondLine; + } + } + + // 버튼 제목 설정 (Plugin Main과 완전히 동일한 매개변수) + const message = { + event: 'setTitle', + context: context, + payload: { + title: title, + target: 0, // 하드웨어와 소프트웨어 모두 + titleParameters: { + fontSize: 18, + showTitle: true, + titleAlignment: "middle" + } + } + }; + + websocket.send(JSON.stringify(message)); + console.log('🏷️ [Property Inspector] 버튼 제목 업데이트:', title.replace('\n', '\\n')); +} \ No newline at end of file diff --git a/Streamdeck/com.mirabox.streamingle.sdPlugin/dist-dev/com.mirabox.streamingle.sdPlugin/propertyInspector/utils/action.js b/Streamdeck/com.mirabox.streamingle.sdPlugin/dist-dev/com.mirabox.streamingle.sdPlugin/propertyInspector/utils/action.js new file mode 100644 index 00000000..7cc9620a --- /dev/null +++ b/Streamdeck/com.mirabox.streamingle.sdPlugin/dist-dev/com.mirabox.streamingle.sdPlugin/propertyInspector/utils/action.js @@ -0,0 +1,157 @@ +let $websocket, $uuid, $action, $context, $settings, $lang, $FileID = ''; + +WebSocket.prototype.setGlobalSettings = function(payload) { + this.send(JSON.stringify({ + event: "setGlobalSettings", + context: $uuid, payload + })); +} + +WebSocket.prototype.getGlobalSettings = function() { + this.send(JSON.stringify({ + event: "getGlobalSettings", + context: $uuid, + })); +} + +// 与插件通信 +WebSocket.prototype.sendToPlugin = function (payload) { + this.send(JSON.stringify({ + event: "sendToPlugin", + action: $action, + context: $uuid, + payload + })); +}; + +//设置标题 +WebSocket.prototype.setTitle = function (str, row = 0, num = 6) { + console.log(str); + let newStr = ''; + if (row) { + let nowRow = 1, strArr = str.split(''); + strArr.forEach((item, index) => { + if (nowRow < row && index >= nowRow * num) { nowRow++; newStr += '\n'; } + if (nowRow <= row && index < nowRow * num) { newStr += item; } + }); + if (strArr.length > row * num) { newStr = newStr.substring(0, newStr.length - 1); newStr += '..'; } + } + this.send(JSON.stringify({ + event: "setTitle", + context: $context, + payload: { + target: 0, + title: newStr || str + } + })); +} + +// 设置状态 +WebSocket.prototype.setState = function (state) { + this.send(JSON.stringify({ + event: "setState", + context: $context, + payload: { state } + })); +}; + +// 设置背景 +WebSocket.prototype.setImage = function (url) { + let image = new Image(); + image.src = url; + image.onload = () => { + let canvas = document.createElement("canvas"); + canvas.width = image.naturalWidth; + canvas.height = image.naturalHeight; + let ctx = canvas.getContext("2d"); + ctx.drawImage(image, 0, 0); + this.send(JSON.stringify({ + event: "setImage", + context: $context, + payload: { + target: 0, + image: canvas.toDataURL("image/png") + } + })); + }; +}; + +// 打开网页 +WebSocket.prototype.openUrl = function (url) { + this.send(JSON.stringify({ + event: "openUrl", + payload: { url } + })); +}; + +// 保存持久化数据 +WebSocket.prototype.saveData = $.debounce(function (payload) { + this.send(JSON.stringify({ + event: "setSettings", + context: $uuid, + payload + })); +}); + +// StreamDock 软件入口函数 +const connectSocket = connectElgatoStreamDeckSocket; +async function connectElgatoStreamDeckSocket(port, uuid, event, app, info) { + info = JSON.parse(info); + $uuid = uuid; $action = info.action; + $context = info.context; + $websocket = new WebSocket('ws://127.0.0.1:' + port); + $websocket.onopen = () => $websocket.send(JSON.stringify({ event, uuid })); + + // 持久数据代理 + $websocket.onmessage = e => { + let data = JSON.parse(e.data); + if (data.event === 'didReceiveSettings') { + $settings = new Proxy(data.payload.settings, { + get(target, property) { + return target[property]; + }, + set(target, property, value) { + target[property] = value; + $websocket.saveData(data.payload.settings); + } + }); + if (!$back) $dom.main.style.display = 'block'; + } + $propEvent[data.event]?.(data.payload); + }; + + // 自动翻译页面 + if (!$local) return; + $lang = await new Promise(resolve => { + const req = new XMLHttpRequest(); + req.open('GET', `../../${JSON.parse(app).application.language}.json`); + req.send(); + req.onreadystatechange = () => { + if (req.readyState === 4) { + resolve(JSON.parse(req.responseText).Localization); + } + }; + }); + + // 遍历文本节点并翻译所有文本节点 + const walker = document.createTreeWalker($dom.main, NodeFilter.SHOW_TEXT, (e) => { + return e.data.trim() && NodeFilter.FILTER_ACCEPT; + }); + while (walker.nextNode()) { + console.log(walker.currentNode.data); + walker.currentNode.data = $lang[walker.currentNode.data]; + } + // placeholder 特殊处理 + const translate = item => { + if (item.placeholder?.trim()) { + console.log(item.placeholder); + item.placeholder = $lang[item.placeholder]; + } + }; + $('input', true).forEach(translate); + $('textarea', true).forEach(translate); +} + +// StreamDock 文件路径回调 +Array.from($('input[type="file"]', true)).forEach(item => item.addEventListener('click', () => $FileID = item.id)); +const onFilePickerReturn = (url) => $emit.send(`File-${$FileID}`, JSON.parse(url)); \ No newline at end of file diff --git a/Streamdeck/com.mirabox.streamingle.sdPlugin/dist-dev/com.mirabox.streamingle.sdPlugin/propertyInspector/utils/axios.js b/Streamdeck/com.mirabox.streamingle.sdPlugin/dist-dev/com.mirabox.streamingle.sdPlugin/propertyInspector/utils/axios.js new file mode 100644 index 00000000..78aa7b89 --- /dev/null +++ b/Streamdeck/com.mirabox.streamingle.sdPlugin/dist-dev/com.mirabox.streamingle.sdPlugin/propertyInspector/utils/axios.js @@ -0,0 +1 @@ +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).axios=t()}(this,(function(){"use strict";function e(t){return e="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},e(t)}function t(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function n(e,t){for(var n=0;ne.length)&&(t=e.length);for(var n=0,r=new Array(t);n2&&void 0!==arguments[2]?arguments[2]:{},a=i.allOwnKeys,s=void 0!==a&&a;if(null!=t)if("object"!==e(t)&&(t=[t]),p(t))for(r=0,o=t.length;r0;)if(t===(n=r[o]).toLowerCase())return n;return null}var C="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:"undefined"!=typeof window?window:global,N=function(e){return!h(e)&&e!==C};var x,P=(x="undefined"!=typeof Uint8Array&&c(Uint8Array),function(e){return x&&e instanceof x}),k=l("HTMLFormElement"),U=function(e){var t=Object.prototype.hasOwnProperty;return function(e,n){return t.call(e,n)}}(),_=l("RegExp"),F=function(e,t){var n=Object.getOwnPropertyDescriptors(e),r={};T(n,(function(n,o){var i;!1!==(i=t(n,o,e))&&(r[o]=i||n)})),Object.defineProperties(e,r)},B="abcdefghijklmnopqrstuvwxyz",L="0123456789",D={DIGIT:L,ALPHA:B,ALPHA_DIGIT:B+B.toUpperCase()+L};var I=l("AsyncFunction"),q={isArray:p,isArrayBuffer:m,isBuffer:function(e){return null!==e&&!h(e)&&null!==e.constructor&&!h(e.constructor)&&y(e.constructor.isBuffer)&&e.constructor.isBuffer(e)},isFormData:function(e){var t;return e&&("function"==typeof FormData&&e instanceof FormData||y(e.append)&&("formdata"===(t=f(e))||"object"===t&&y(e.toString)&&"[object FormData]"===e.toString()))},isArrayBufferView:function(e){return"undefined"!=typeof ArrayBuffer&&ArrayBuffer.isView?ArrayBuffer.isView(e):e&&e.buffer&&m(e.buffer)},isString:v,isNumber:b,isBoolean:function(e){return!0===e||!1===e},isObject:g,isPlainObject:w,isUndefined:h,isDate:E,isFile:O,isBlob:S,isRegExp:_,isFunction:y,isStream:function(e){return g(e)&&y(e.pipe)},isURLSearchParams:A,isTypedArray:P,isFileList:R,forEach:T,merge:function e(){for(var t=N(this)&&this||{},n=t.caseless,r={},o=function(t,o){var i=n&&j(r,o)||o;w(r[i])&&w(t)?r[i]=e(r[i],t):w(t)?r[i]=e({},t):p(t)?r[i]=t.slice():r[i]=t},i=0,a=arguments.length;i3&&void 0!==arguments[3]?arguments[3]:{},o=r.allOwnKeys;return T(t,(function(t,r){n&&y(t)?e[r]=a(t,n):e[r]=t}),{allOwnKeys:o}),e},trim:function(e){return e.trim?e.trim():e.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,"")},stripBOM:function(e){return 65279===e.charCodeAt(0)&&(e=e.slice(1)),e},inherits:function(e,t,n,r){e.prototype=Object.create(t.prototype,r),e.prototype.constructor=e,Object.defineProperty(e,"super",{value:t.prototype}),n&&Object.assign(e.prototype,n)},toFlatObject:function(e,t,n,r){var o,i,a,s={};if(t=t||{},null==e)return t;do{for(i=(o=Object.getOwnPropertyNames(e)).length;i-- >0;)a=o[i],r&&!r(a,e,t)||s[a]||(t[a]=e[a],s[a]=!0);e=!1!==n&&c(e)}while(e&&(!n||n(e,t))&&e!==Object.prototype);return t},kindOf:f,kindOfTest:l,endsWith:function(e,t,n){e=String(e),(void 0===n||n>e.length)&&(n=e.length),n-=t.length;var r=e.indexOf(t,n);return-1!==r&&r===n},toArray:function(e){if(!e)return null;if(p(e))return e;var t=e.length;if(!b(t))return null;for(var n=new Array(t);t-- >0;)n[t]=e[t];return n},forEachEntry:function(e,t){for(var n,r=(e&&e[Symbol.iterator]).call(e);(n=r.next())&&!n.done;){var o=n.value;t.call(e,o[0],o[1])}},matchAll:function(e,t){for(var n,r=[];null!==(n=e.exec(t));)r.push(n);return r},isHTMLForm:k,hasOwnProperty:U,hasOwnProp:U,reduceDescriptors:F,freezeMethods:function(e){F(e,(function(t,n){if(y(e)&&-1!==["arguments","caller","callee"].indexOf(n))return!1;var r=e[n];y(r)&&(t.enumerable=!1,"writable"in t?t.writable=!1:t.set||(t.set=function(){throw Error("Can not rewrite read-only method '"+n+"'")}))}))},toObjectSet:function(e,t){var n={},r=function(e){e.forEach((function(e){n[e]=!0}))};return p(e)?r(e):r(String(e).split(t)),n},toCamelCase:function(e){return e.toLowerCase().replace(/[-_\s]([a-z\d])(\w*)/g,(function(e,t,n){return t.toUpperCase()+n}))},noop:function(){},toFiniteNumber:function(e,t){return e=+e,Number.isFinite(e)?e:t},findKey:j,global:C,isContextDefined:N,ALPHABET:D,generateString:function(){for(var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:16,t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:D.ALPHA_DIGIT,n="",r=t.length;e--;)n+=t[Math.random()*r|0];return n},isSpecCompliantForm:function(e){return!!(e&&y(e.append)&&"FormData"===e[Symbol.toStringTag]&&e[Symbol.iterator])},toJSONObject:function(e){var t=new Array(10);return function e(n,r){if(g(n)){if(t.indexOf(n)>=0)return;if(!("toJSON"in n)){t[r]=n;var o=p(n)?[]:{};return T(n,(function(t,n){var i=e(t,r+1);!h(i)&&(o[n]=i)})),t[r]=void 0,o}}return n}(e,0)},isAsyncFn:I,isThenable:function(e){return e&&(g(e)||y(e))&&y(e.then)&&y(e.catch)}};function M(e,t,n,r,o){Error.call(this),Error.captureStackTrace?Error.captureStackTrace(this,this.constructor):this.stack=(new Error).stack,this.message=e,this.name="AxiosError",t&&(this.code=t),n&&(this.config=n),r&&(this.request=r),o&&(this.response=o)}q.inherits(M,Error,{toJSON:function(){return{message:this.message,name:this.name,description:this.description,number:this.number,fileName:this.fileName,lineNumber:this.lineNumber,columnNumber:this.columnNumber,stack:this.stack,config:q.toJSONObject(this.config),code:this.code,status:this.response&&this.response.status?this.response.status:null}}});var z=M.prototype,H={};["ERR_BAD_OPTION_VALUE","ERR_BAD_OPTION","ECONNABORTED","ETIMEDOUT","ERR_NETWORK","ERR_FR_TOO_MANY_REDIRECTS","ERR_DEPRECATED","ERR_BAD_RESPONSE","ERR_BAD_REQUEST","ERR_CANCELED","ERR_NOT_SUPPORT","ERR_INVALID_URL"].forEach((function(e){H[e]={value:e}})),Object.defineProperties(M,H),Object.defineProperty(z,"isAxiosError",{value:!0}),M.from=function(e,t,n,r,o,i){var a=Object.create(z);return q.toFlatObject(e,a,(function(e){return e!==Error.prototype}),(function(e){return"isAxiosError"!==e})),M.call(a,e.message,t,n,r,o),a.cause=e,a.name=e.name,i&&Object.assign(a,i),a};function J(e){return q.isPlainObject(e)||q.isArray(e)}function W(e){return q.endsWith(e,"[]")?e.slice(0,-2):e}function K(e,t,n){return e?e.concat(t).map((function(e,t){return e=W(e),!n&&t?"["+e+"]":e})).join(n?".":""):t}var V=q.toFlatObject(q,{},null,(function(e){return/^is[A-Z]/.test(e)}));function G(t,n,r){if(!q.isObject(t))throw new TypeError("target must be an object");n=n||new FormData;var o=(r=q.toFlatObject(r,{metaTokens:!0,dots:!1,indexes:!1},!1,(function(e,t){return!q.isUndefined(t[e])}))).metaTokens,i=r.visitor||f,a=r.dots,s=r.indexes,u=(r.Blob||"undefined"!=typeof Blob&&Blob)&&q.isSpecCompliantForm(n);if(!q.isFunction(i))throw new TypeError("visitor must be a function");function c(e){if(null===e)return"";if(q.isDate(e))return e.toISOString();if(!u&&q.isBlob(e))throw new M("Blob is not supported. Use a Buffer instead.");return q.isArrayBuffer(e)||q.isTypedArray(e)?u&&"function"==typeof Blob?new Blob([e]):Buffer.from(e):e}function f(t,r,i){var u=t;if(t&&!i&&"object"===e(t))if(q.endsWith(r,"{}"))r=o?r:r.slice(0,-2),t=JSON.stringify(t);else if(q.isArray(t)&&function(e){return q.isArray(e)&&!e.some(J)}(t)||(q.isFileList(t)||q.endsWith(r,"[]"))&&(u=q.toArray(t)))return r=W(r),u.forEach((function(e,t){!q.isUndefined(e)&&null!==e&&n.append(!0===s?K([r],t,a):null===s?r:r+"[]",c(e))})),!1;return!!J(t)||(n.append(K(i,r,a),c(t)),!1)}var l=[],d=Object.assign(V,{defaultVisitor:f,convertValue:c,isVisitable:J});if(!q.isObject(t))throw new TypeError("data must be an object");return function e(t,r){if(!q.isUndefined(t)){if(-1!==l.indexOf(t))throw Error("Circular reference detected in "+r.join("."));l.push(t),q.forEach(t,(function(t,o){!0===(!(q.isUndefined(t)||null===t)&&i.call(n,t,q.isString(o)?o.trim():o,r,d))&&e(t,r?r.concat(o):[o])})),l.pop()}}(t),n}function $(e){var t={"!":"%21","'":"%27","(":"%28",")":"%29","~":"%7E","%20":"+","%00":"\0"};return encodeURIComponent(e).replace(/[!'()~]|%20|%00/g,(function(e){return t[e]}))}function X(e,t){this._pairs=[],e&&G(e,this,t)}var Q=X.prototype;function Z(e){return encodeURIComponent(e).replace(/%3A/gi,":").replace(/%24/g,"$").replace(/%2C/gi,",").replace(/%20/g,"+").replace(/%5B/gi,"[").replace(/%5D/gi,"]")}function Y(e,t,n){if(!t)return e;var r,o=n&&n.encode||Z,i=n&&n.serialize;if(r=i?i(t,n):q.isURLSearchParams(t)?t.toString():new X(t,n).toString(o)){var a=e.indexOf("#");-1!==a&&(e=e.slice(0,a)),e+=(-1===e.indexOf("?")?"?":"&")+r}return e}Q.append=function(e,t){this._pairs.push([e,t])},Q.toString=function(e){var t=e?function(t){return e.call(this,t,$)}:$;return this._pairs.map((function(e){return t(e[0])+"="+t(e[1])}),"").join("&")};var ee,te=function(){function e(){t(this,e),this.handlers=[]}return r(e,[{key:"use",value:function(e,t,n){return this.handlers.push({fulfilled:e,rejected:t,synchronous:!!n&&n.synchronous,runWhen:n?n.runWhen:null}),this.handlers.length-1}},{key:"eject",value:function(e){this.handlers[e]&&(this.handlers[e]=null)}},{key:"clear",value:function(){this.handlers&&(this.handlers=[])}},{key:"forEach",value:function(e){q.forEach(this.handlers,(function(t){null!==t&&e(t)}))}}]),e}(),ne={silentJSONParsing:!0,forcedJSONParsing:!0,clarifyTimeoutError:!1},re={isBrowser:!0,classes:{URLSearchParams:"undefined"!=typeof URLSearchParams?URLSearchParams:X,FormData:"undefined"!=typeof FormData?FormData:null,Blob:"undefined"!=typeof Blob?Blob:null},isStandardBrowserEnv:("undefined"==typeof navigator||"ReactNative"!==(ee=navigator.product)&&"NativeScript"!==ee&&"NS"!==ee)&&"undefined"!=typeof window&&"undefined"!=typeof document,isStandardBrowserWebWorkerEnv:"undefined"!=typeof WorkerGlobalScope&&self instanceof WorkerGlobalScope&&"function"==typeof self.importScripts,protocols:["http","https","file","blob","url","data"]};function oe(e){function t(e,n,r,o){var i=e[o++],a=Number.isFinite(+i),s=o>=e.length;return i=!i&&q.isArray(r)?r.length:i,s?(q.hasOwnProp(r,i)?r[i]=[r[i],n]:r[i]=n,!a):(r[i]&&q.isObject(r[i])||(r[i]=[]),t(e,n,r[i],o)&&q.isArray(r[i])&&(r[i]=function(e){var t,n,r={},o=Object.keys(e),i=o.length;for(t=0;t-1,i=q.isObject(e);if(i&&q.isHTMLForm(e)&&(e=new FormData(e)),q.isFormData(e))return o&&o?JSON.stringify(oe(e)):e;if(q.isArrayBuffer(e)||q.isBuffer(e)||q.isStream(e)||q.isFile(e)||q.isBlob(e))return e;if(q.isArrayBufferView(e))return e.buffer;if(q.isURLSearchParams(e))return t.setContentType("application/x-www-form-urlencoded;charset=utf-8",!1),e.toString();if(i){if(r.indexOf("application/x-www-form-urlencoded")>-1)return function(e,t){return G(e,new re.classes.URLSearchParams,Object.assign({visitor:function(e,t,n,r){return re.isNode&&q.isBuffer(e)?(this.append(t,e.toString("base64")),!1):r.defaultVisitor.apply(this,arguments)}},t))}(e,this.formSerializer).toString();if((n=q.isFileList(e))||r.indexOf("multipart/form-data")>-1){var a=this.env&&this.env.FormData;return G(n?{"files[]":e}:e,a&&new a,this.formSerializer)}}return i||o?(t.setContentType("application/json",!1),function(e,t,n){if(q.isString(e))try{return(t||JSON.parse)(e),q.trim(e)}catch(e){if("SyntaxError"!==e.name)throw e}return(n||JSON.stringify)(e)}(e)):e}],transformResponse:[function(e){var t=this.transitional||ie.transitional,n=t&&t.forcedJSONParsing,r="json"===this.responseType;if(e&&q.isString(e)&&(n&&!this.responseType||r)){var o=!(t&&t.silentJSONParsing)&&r;try{return JSON.parse(e)}catch(e){if(o){if("SyntaxError"===e.name)throw M.from(e,M.ERR_BAD_RESPONSE,this,null,this.response);throw e}}}return e}],timeout:0,xsrfCookieName:"XSRF-TOKEN",xsrfHeaderName:"X-XSRF-TOKEN",maxContentLength:-1,maxBodyLength:-1,env:{FormData:re.classes.FormData,Blob:re.classes.Blob},validateStatus:function(e){return e>=200&&e<300},headers:{common:{Accept:"application/json, text/plain, */*","Content-Type":void 0}}};q.forEach(["delete","get","head","post","put","patch"],(function(e){ie.headers[e]={}}));var ae=ie,se=q.toObjectSet(["age","authorization","content-length","content-type","etag","expires","from","host","if-modified-since","if-unmodified-since","last-modified","location","max-forwards","proxy-authorization","referer","retry-after","user-agent"]),ue=Symbol("internals");function ce(e){return e&&String(e).trim().toLowerCase()}function fe(e){return!1===e||null==e?e:q.isArray(e)?e.map(fe):String(e)}function le(e,t,n,r,o){return q.isFunction(r)?r.call(this,t,n):(o&&(t=n),q.isString(t)?q.isString(r)?-1!==t.indexOf(r):q.isRegExp(r)?r.test(t):void 0:void 0)}var de=function(e,n){function i(e){t(this,i),e&&this.set(e)}return r(i,[{key:"set",value:function(e,t,n){var r=this;function o(e,t,n){var o=ce(t);if(!o)throw new Error("header name must be a non-empty string");var i=q.findKey(r,o);(!i||void 0===r[i]||!0===n||void 0===n&&!1!==r[i])&&(r[i||t]=fe(e))}var i,a,s,u,c,f=function(e,t){return q.forEach(e,(function(e,n){return o(e,n,t)}))};return q.isPlainObject(e)||e instanceof this.constructor?f(e,t):q.isString(e)&&(e=e.trim())&&!/^[-_a-zA-Z0-9^`|~,!#$%&'*+.]+$/.test(e.trim())?f((c={},(i=e)&&i.split("\n").forEach((function(e){u=e.indexOf(":"),a=e.substring(0,u).trim().toLowerCase(),s=e.substring(u+1).trim(),!a||c[a]&&se[a]||("set-cookie"===a?c[a]?c[a].push(s):c[a]=[s]:c[a]=c[a]?c[a]+", "+s:s)})),c),t):null!=e&&o(t,e,n),this}},{key:"get",value:function(e,t){if(e=ce(e)){var n=q.findKey(this,e);if(n){var r=this[n];if(!t)return r;if(!0===t)return function(e){for(var t,n=Object.create(null),r=/([^\s,;=]+)\s*(?:=\s*([^,;]+))?/g;t=r.exec(e);)n[t[1]]=t[2];return n}(r);if(q.isFunction(t))return t.call(this,r,n);if(q.isRegExp(t))return t.exec(r);throw new TypeError("parser must be boolean|regexp|function")}}}},{key:"has",value:function(e,t){if(e=ce(e)){var n=q.findKey(this,e);return!(!n||void 0===this[n]||t&&!le(0,this[n],n,t))}return!1}},{key:"delete",value:function(e,t){var n=this,r=!1;function o(e){if(e=ce(e)){var o=q.findKey(n,e);!o||t&&!le(0,n[o],o,t)||(delete n[o],r=!0)}}return q.isArray(e)?e.forEach(o):o(e),r}},{key:"clear",value:function(e){for(var t=Object.keys(this),n=t.length,r=!1;n--;){var o=t[n];e&&!le(0,this[o],o,e,!0)||(delete this[o],r=!0)}return r}},{key:"normalize",value:function(e){var t=this,n={};return q.forEach(this,(function(r,o){var i=q.findKey(n,o);if(i)return t[i]=fe(r),void delete t[o];var a=e?function(e){return e.trim().toLowerCase().replace(/([a-z\d])(\w*)/g,(function(e,t,n){return t.toUpperCase()+n}))}(o):String(o).trim();a!==o&&delete t[o],t[a]=fe(r),n[a]=!0})),this}},{key:"concat",value:function(){for(var e,t=arguments.length,n=new Array(t),r=0;r1?n-1:0),o=1;o1?"since :\n"+u.map(Oe).join("\n"):" "+Oe(u[0]):"as no adapter specified"),"ERR_NOT_SUPPORT")}return n};function Ae(e){if(e.cancelToken&&e.cancelToken.throwIfRequested(),e.signal&&e.signal.aborted)throw new ve(null,e)}function Te(e){return Ae(e),e.headers=pe.from(e.headers),e.data=he.call(e,e.transformRequest),-1!==["post","put","patch"].indexOf(e.method)&&e.headers.setContentType("application/x-www-form-urlencoded",!1),Re(e.adapter||ae.adapter)(e).then((function(t){return Ae(e),t.data=he.call(e,e.transformResponse,t),t.headers=pe.from(t.headers),t}),(function(t){return me(t)||(Ae(e),t&&t.response&&(t.response.data=he.call(e,e.transformResponse,t.response),t.response.headers=pe.from(t.response.headers))),Promise.reject(t)}))}var je=function(e){return e instanceof pe?e.toJSON():e};function Ce(e,t){t=t||{};var n={};function r(e,t,n){return q.isPlainObject(e)&&q.isPlainObject(t)?q.merge.call({caseless:n},e,t):q.isPlainObject(t)?q.merge({},t):q.isArray(t)?t.slice():t}function o(e,t,n){return q.isUndefined(t)?q.isUndefined(e)?void 0:r(void 0,e,n):r(e,t,n)}function i(e,t){if(!q.isUndefined(t))return r(void 0,t)}function a(e,t){return q.isUndefined(t)?q.isUndefined(e)?void 0:r(void 0,e):r(void 0,t)}function s(n,o,i){return i in t?r(n,o):i in e?r(void 0,n):void 0}var u={url:i,method:i,data:i,baseURL:a,transformRequest:a,transformResponse:a,paramsSerializer:a,timeout:a,timeoutMessage:a,withCredentials:a,adapter:a,responseType:a,xsrfCookieName:a,xsrfHeaderName:a,onUploadProgress:a,onDownloadProgress:a,decompress:a,maxContentLength:a,maxBodyLength:a,beforeRedirect:a,transport:a,httpAgent:a,httpsAgent:a,cancelToken:a,socketPath:a,responseEncoding:a,validateStatus:s,headers:function(e,t){return o(je(e),je(t),!0)}};return q.forEach(Object.keys(Object.assign({},e,t)),(function(r){var i=u[r]||o,a=i(e[r],t[r],r);q.isUndefined(a)&&i!==s||(n[r]=a)})),n}var Ne="1.5.1",xe={};["object","boolean","number","function","string","symbol"].forEach((function(t,n){xe[t]=function(r){return e(r)===t||"a"+(n<1?"n ":" ")+t}}));var Pe={};xe.transitional=function(e,t,n){function r(e,t){return"[Axios v1.5.1] Transitional option '"+e+"'"+t+(n?". "+n:"")}return function(n,o,i){if(!1===e)throw new M(r(o," has been removed"+(t?" in "+t:"")),M.ERR_DEPRECATED);return t&&!Pe[o]&&(Pe[o]=!0,console.warn(r(o," has been deprecated since v"+t+" and will be removed in the near future"))),!e||e(n,o,i)}};var ke={assertOptions:function(t,n,r){if("object"!==e(t))throw new M("options must be an object",M.ERR_BAD_OPTION_VALUE);for(var o=Object.keys(t),i=o.length;i-- >0;){var a=o[i],s=n[a];if(s){var u=t[a],c=void 0===u||s(u,a,t);if(!0!==c)throw new M("option "+a+" must be "+c,M.ERR_BAD_OPTION_VALUE)}else if(!0!==r)throw new M("Unknown option "+a,M.ERR_BAD_OPTION)}},validators:xe},Ue=ke.validators,_e=function(){function e(n){t(this,e),this.defaults=n,this.interceptors={request:new te,response:new te}}return r(e,[{key:"request",value:function(e,t){"string"==typeof e?(t=t||{}).url=e:t=e||{};var n=t=Ce(this.defaults,t),r=n.transitional,o=n.paramsSerializer,i=n.headers;void 0!==r&&ke.assertOptions(r,{silentJSONParsing:Ue.transitional(Ue.boolean),forcedJSONParsing:Ue.transitional(Ue.boolean),clarifyTimeoutError:Ue.transitional(Ue.boolean)},!1),null!=o&&(q.isFunction(o)?t.paramsSerializer={serialize:o}:ke.assertOptions(o,{encode:Ue.function,serialize:Ue.function},!0)),t.method=(t.method||this.defaults.method||"get").toLowerCase();var a=i&&q.merge(i.common,i[t.method]);i&&q.forEach(["delete","get","head","post","put","patch","common"],(function(e){delete i[e]})),t.headers=pe.concat(a,i);var s=[],u=!0;this.interceptors.request.forEach((function(e){"function"==typeof e.runWhen&&!1===e.runWhen(t)||(u=u&&e.synchronous,s.unshift(e.fulfilled,e.rejected))}));var c,f=[];this.interceptors.response.forEach((function(e){f.push(e.fulfilled,e.rejected)}));var l,d=0;if(!u){var p=[Te.bind(this),void 0];for(p.unshift.apply(p,s),p.push.apply(p,f),l=p.length,c=Promise.resolve(t);d0;)o._listeners[t](e);o._listeners=null}})),this.promise.then=function(e){var t,n=new Promise((function(e){o.subscribe(e),t=e})).then(e);return n.cancel=function(){o.unsubscribe(t)},n},n((function(e,t,n){o.reason||(o.reason=new ve(e,t,n),r(o.reason))}))}return r(e,[{key:"throwIfRequested",value:function(){if(this.reason)throw this.reason}},{key:"subscribe",value:function(e){this.reason?e(this.reason):this._listeners?this._listeners.push(e):this._listeners=[e]}},{key:"unsubscribe",value:function(e){if(this._listeners){var t=this._listeners.indexOf(e);-1!==t&&this._listeners.splice(t,1)}}}],[{key:"source",value:function(){var t;return{token:new e((function(e){t=e})),cancel:t}}}]),e}();var Le={Continue:100,SwitchingProtocols:101,Processing:102,EarlyHints:103,Ok:200,Created:201,Accepted:202,NonAuthoritativeInformation:203,NoContent:204,ResetContent:205,PartialContent:206,MultiStatus:207,AlreadyReported:208,ImUsed:226,MultipleChoices:300,MovedPermanently:301,Found:302,SeeOther:303,NotModified:304,UseProxy:305,Unused:306,TemporaryRedirect:307,PermanentRedirect:308,BadRequest:400,Unauthorized:401,PaymentRequired:402,Forbidden:403,NotFound:404,MethodNotAllowed:405,NotAcceptable:406,ProxyAuthenticationRequired:407,RequestTimeout:408,Conflict:409,Gone:410,LengthRequired:411,PreconditionFailed:412,PayloadTooLarge:413,UriTooLong:414,UnsupportedMediaType:415,RangeNotSatisfiable:416,ExpectationFailed:417,ImATeapot:418,MisdirectedRequest:421,UnprocessableEntity:422,Locked:423,FailedDependency:424,TooEarly:425,UpgradeRequired:426,PreconditionRequired:428,TooManyRequests:429,RequestHeaderFieldsTooLarge:431,UnavailableForLegalReasons:451,InternalServerError:500,NotImplemented:501,BadGateway:502,ServiceUnavailable:503,GatewayTimeout:504,HttpVersionNotSupported:505,VariantAlsoNegotiates:506,InsufficientStorage:507,LoopDetected:508,NotExtended:510,NetworkAuthenticationRequired:511};Object.entries(Le).forEach((function(e){var t=o(e,2),n=t[0],r=t[1];Le[r]=n}));var De=Le;var Ie=function e(t){var n=new Fe(t),r=a(Fe.prototype.request,n);return q.extend(r,Fe.prototype,n,{allOwnKeys:!0}),q.extend(r,n,null,{allOwnKeys:!0}),r.create=function(n){return e(Ce(t,n))},r}(ae);return Ie.Axios=Fe,Ie.CanceledError=ve,Ie.CancelToken=Be,Ie.isCancel=me,Ie.VERSION=Ne,Ie.toFormData=G,Ie.AxiosError=M,Ie.Cancel=Ie.CanceledError,Ie.all=function(e){return Promise.all(e)},Ie.spread=function(e){return function(t){return e.apply(null,t)}},Ie.isAxiosError=function(e){return q.isObject(e)&&!0===e.isAxiosError},Ie.mergeConfig=Ce,Ie.AxiosHeaders=pe,Ie.formToJSON=function(e){return oe(q.isHTMLForm(e)?new FormData(e):e)},Ie.getAdapter=Re,Ie.HttpStatusCode=De,Ie.default=Ie,Ie})); \ No newline at end of file diff --git a/Streamdeck/com.mirabox.streamingle.sdPlugin/dist-dev/com.mirabox.streamingle.sdPlugin/propertyInspector/utils/bootstrap-icons.css b/Streamdeck/com.mirabox.streamingle.sdPlugin/dist-dev/com.mirabox.streamingle.sdPlugin/propertyInspector/utils/bootstrap-icons.css new file mode 100644 index 00000000..3c1537f1 --- /dev/null +++ b/Streamdeck/com.mirabox.streamingle.sdPlugin/dist-dev/com.mirabox.streamingle.sdPlugin/propertyInspector/utils/bootstrap-icons.css @@ -0,0 +1,1556 @@ +@font-face { + font-family: "bootstrap-icons"; + src: url("./fonts/bootstrap-icons.woff2?30af91bf14e37666a085fb8a161ff36d") format("woff2"), +url("./fonts/bootstrap-icons.woff?30af91bf14e37666a085fb8a161ff36d") format("woff"); +} + +.bi::before, +[class^="bi-"]::before, +[class*=" bi-"]::before { + display: inline-block; + font-family: bootstrap-icons !important; + font-style: normal; + font-weight: normal !important; + font-variant: normal; + text-transform: none; + line-height: 1; + vertical-align: -.125em; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.bi-123::before { content: "\f67f"; } +.bi-alarm-fill::before { content: "\f101"; } +.bi-alarm::before { content: "\f102"; } +.bi-align-bottom::before { content: "\f103"; } +.bi-align-center::before { content: "\f104"; } +.bi-align-end::before { content: "\f105"; } +.bi-align-middle::before { content: "\f106"; } +.bi-align-start::before { content: "\f107"; } +.bi-align-top::before { content: "\f108"; } +.bi-alt::before { content: "\f109"; } +.bi-app-indicator::before { content: "\f10a"; } +.bi-app::before { content: "\f10b"; } +.bi-archive-fill::before { content: "\f10c"; } +.bi-archive::before { content: "\f10d"; } +.bi-arrow-90deg-down::before { content: "\f10e"; } +.bi-arrow-90deg-left::before { content: "\f10f"; } +.bi-arrow-90deg-right::before { content: "\f110"; } +.bi-arrow-90deg-up::before { content: "\f111"; } +.bi-arrow-bar-down::before { content: "\f112"; } +.bi-arrow-bar-left::before { content: "\f113"; } +.bi-arrow-bar-right::before { content: "\f114"; } +.bi-arrow-bar-up::before { content: "\f115"; } +.bi-arrow-clockwise::before { content: "\f116"; } +.bi-arrow-counterclockwise::before { content: "\f117"; } +.bi-arrow-down-circle-fill::before { content: "\f118"; } +.bi-arrow-down-circle::before { content: "\f119"; } +.bi-arrow-down-left-circle-fill::before { content: "\f11a"; } +.bi-arrow-down-left-circle::before { content: "\f11b"; } +.bi-arrow-down-left-square-fill::before { content: "\f11c"; } +.bi-arrow-down-left-square::before { content: "\f11d"; } +.bi-arrow-down-left::before { content: "\f11e"; } +.bi-arrow-down-right-circle-fill::before { content: "\f11f"; } +.bi-arrow-down-right-circle::before { content: "\f120"; } +.bi-arrow-down-right-square-fill::before { content: "\f121"; } +.bi-arrow-down-right-square::before { content: "\f122"; } +.bi-arrow-down-right::before { content: "\f123"; } +.bi-arrow-down-short::before { content: "\f124"; } +.bi-arrow-down-square-fill::before { content: "\f125"; } +.bi-arrow-down-square::before { content: "\f126"; } +.bi-arrow-down-up::before { content: "\f127"; } +.bi-arrow-down::before { content: "\f128"; } +.bi-arrow-left-circle-fill::before { content: "\f129"; } +.bi-arrow-left-circle::before { content: "\f12a"; } +.bi-arrow-left-right::before { content: "\f12b"; } +.bi-arrow-left-short::before { content: "\f12c"; } +.bi-arrow-left-square-fill::before { content: "\f12d"; } +.bi-arrow-left-square::before { content: "\f12e"; } +.bi-arrow-left::before { content: "\f12f"; } +.bi-arrow-repeat::before { content: "\f130"; } +.bi-arrow-return-left::before { content: "\f131"; } +.bi-arrow-return-right::before { content: "\f132"; } +.bi-arrow-right-circle-fill::before { content: "\f133"; } +.bi-arrow-right-circle::before { content: "\f134"; } +.bi-arrow-right-short::before { content: "\f135"; } +.bi-arrow-right-square-fill::before { content: "\f136"; } +.bi-arrow-right-square::before { content: "\f137"; } +.bi-arrow-right::before { content: "\f138"; } +.bi-arrow-up-circle-fill::before { content: "\f139"; } +.bi-arrow-up-circle::before { content: "\f13a"; } +.bi-arrow-up-left-circle-fill::before { content: "\f13b"; } +.bi-arrow-up-left-circle::before { content: "\f13c"; } +.bi-arrow-up-left-square-fill::before { content: "\f13d"; } +.bi-arrow-up-left-square::before { content: "\f13e"; } +.bi-arrow-up-left::before { content: "\f13f"; } +.bi-arrow-up-right-circle-fill::before { content: "\f140"; } +.bi-arrow-up-right-circle::before { content: "\f141"; } +.bi-arrow-up-right-square-fill::before { content: "\f142"; } +.bi-arrow-up-right-square::before { content: "\f143"; } +.bi-arrow-up-right::before { content: "\f144"; } +.bi-arrow-up-short::before { content: "\f145"; } +.bi-arrow-up-square-fill::before { content: "\f146"; } +.bi-arrow-up-square::before { content: "\f147"; } +.bi-arrow-up::before { content: "\f148"; } +.bi-arrows-angle-contract::before { content: "\f149"; } +.bi-arrows-angle-expand::before { content: "\f14a"; } +.bi-arrows-collapse::before { content: "\f14b"; } +.bi-arrows-expand::before { content: "\f14c"; } +.bi-arrows-fullscreen::before { content: "\f14d"; } +.bi-arrows-move::before { content: "\f14e"; } +.bi-aspect-ratio-fill::before { content: "\f14f"; } +.bi-aspect-ratio::before { content: "\f150"; } +.bi-asterisk::before { content: "\f151"; } +.bi-at::before { content: "\f152"; } +.bi-award-fill::before { content: "\f153"; } +.bi-award::before { content: "\f154"; } +.bi-back::before { content: "\f155"; } +.bi-backspace-fill::before { content: "\f156"; } +.bi-backspace-reverse-fill::before { content: "\f157"; } +.bi-backspace-reverse::before { content: "\f158"; } +.bi-backspace::before { content: "\f159"; } +.bi-badge-3d-fill::before { content: "\f15a"; } +.bi-badge-3d::before { content: "\f15b"; } +.bi-badge-4k-fill::before { content: "\f15c"; } +.bi-badge-4k::before { content: "\f15d"; } +.bi-badge-8k-fill::before { content: "\f15e"; } +.bi-badge-8k::before { content: "\f15f"; } +.bi-badge-ad-fill::before { content: "\f160"; } +.bi-badge-ad::before { content: "\f161"; } +.bi-badge-ar-fill::before { content: "\f162"; } +.bi-badge-ar::before { content: "\f163"; } +.bi-badge-cc-fill::before { content: "\f164"; } +.bi-badge-cc::before { content: "\f165"; } +.bi-badge-hd-fill::before { content: "\f166"; } +.bi-badge-hd::before { content: "\f167"; } +.bi-badge-tm-fill::before { content: "\f168"; } +.bi-badge-tm::before { content: "\f169"; } +.bi-badge-vo-fill::before { content: "\f16a"; } +.bi-badge-vo::before { content: "\f16b"; } +.bi-badge-vr-fill::before { content: "\f16c"; } +.bi-badge-vr::before { content: "\f16d"; } +.bi-badge-wc-fill::before { content: "\f16e"; } +.bi-badge-wc::before { content: "\f16f"; } +.bi-bag-check-fill::before { content: "\f170"; } +.bi-bag-check::before { content: "\f171"; } +.bi-bag-dash-fill::before { content: "\f172"; } +.bi-bag-dash::before { content: "\f173"; } +.bi-bag-fill::before { content: "\f174"; } +.bi-bag-plus-fill::before { content: "\f175"; } +.bi-bag-plus::before { content: "\f176"; } +.bi-bag-x-fill::before { content: "\f177"; } +.bi-bag-x::before { content: "\f178"; } +.bi-bag::before { content: "\f179"; } +.bi-bar-chart-fill::before { content: "\f17a"; } +.bi-bar-chart-line-fill::before { content: "\f17b"; } +.bi-bar-chart-line::before { content: "\f17c"; } +.bi-bar-chart-steps::before { content: "\f17d"; } +.bi-bar-chart::before { content: "\f17e"; } +.bi-basket-fill::before { content: "\f17f"; } +.bi-basket::before { content: "\f180"; } +.bi-basket2-fill::before { content: "\f181"; } +.bi-basket2::before { content: "\f182"; } +.bi-basket3-fill::before { content: "\f183"; } +.bi-basket3::before { content: "\f184"; } +.bi-battery-charging::before { content: "\f185"; } +.bi-battery-full::before { content: "\f186"; } +.bi-battery-half::before { content: "\f187"; } +.bi-battery::before { content: "\f188"; } +.bi-bell-fill::before { content: "\f189"; } +.bi-bell::before { content: "\f18a"; } +.bi-bezier::before { content: "\f18b"; } +.bi-bezier2::before { content: "\f18c"; } +.bi-bicycle::before { content: "\f18d"; } +.bi-binoculars-fill::before { content: "\f18e"; } +.bi-binoculars::before { content: "\f18f"; } +.bi-blockquote-left::before { content: "\f190"; } +.bi-blockquote-right::before { content: "\f191"; } +.bi-book-fill::before { content: "\f192"; } +.bi-book-half::before { content: "\f193"; } +.bi-book::before { content: "\f194"; } +.bi-bookmark-check-fill::before { content: "\f195"; } +.bi-bookmark-check::before { content: "\f196"; } +.bi-bookmark-dash-fill::before { content: "\f197"; } +.bi-bookmark-dash::before { content: "\f198"; } +.bi-bookmark-fill::before { content: "\f199"; } +.bi-bookmark-heart-fill::before { content: "\f19a"; } +.bi-bookmark-heart::before { content: "\f19b"; } +.bi-bookmark-plus-fill::before { content: "\f19c"; } +.bi-bookmark-plus::before { content: "\f19d"; } +.bi-bookmark-star-fill::before { content: "\f19e"; } +.bi-bookmark-star::before { content: "\f19f"; } +.bi-bookmark-x-fill::before { content: "\f1a0"; } +.bi-bookmark-x::before { content: "\f1a1"; } +.bi-bookmark::before { content: "\f1a2"; } +.bi-bookmarks-fill::before { content: "\f1a3"; } +.bi-bookmarks::before { content: "\f1a4"; } +.bi-bookshelf::before { content: "\f1a5"; } +.bi-bootstrap-fill::before { content: "\f1a6"; } +.bi-bootstrap-reboot::before { content: "\f1a7"; } +.bi-bootstrap::before { content: "\f1a8"; } +.bi-border-all::before { content: "\f1a9"; } +.bi-border-bottom::before { content: "\f1aa"; } +.bi-border-center::before { content: "\f1ab"; } +.bi-border-inner::before { content: "\f1ac"; } +.bi-border-left::before { content: "\f1ad"; } +.bi-border-middle::before { content: "\f1ae"; } +.bi-border-outer::before { content: "\f1af"; } +.bi-border-right::before { content: "\f1b0"; } +.bi-border-style::before { content: "\f1b1"; } +.bi-border-top::before { content: "\f1b2"; } +.bi-border-width::before { content: "\f1b3"; } +.bi-border::before { content: "\f1b4"; } +.bi-bounding-box-circles::before { content: "\f1b5"; } +.bi-bounding-box::before { content: "\f1b6"; } +.bi-box-arrow-down-left::before { content: "\f1b7"; } +.bi-box-arrow-down-right::before { content: "\f1b8"; } +.bi-box-arrow-down::before { content: "\f1b9"; } +.bi-box-arrow-in-down-left::before { content: "\f1ba"; } +.bi-box-arrow-in-down-right::before { content: "\f1bb"; } +.bi-box-arrow-in-down::before { content: "\f1bc"; } +.bi-box-arrow-in-left::before { content: "\f1bd"; } +.bi-box-arrow-in-right::before { content: "\f1be"; } +.bi-box-arrow-in-up-left::before { content: "\f1bf"; } +.bi-box-arrow-in-up-right::before { content: "\f1c0"; } +.bi-box-arrow-in-up::before { content: "\f1c1"; } +.bi-box-arrow-left::before { content: "\f1c2"; } +.bi-box-arrow-right::before { content: "\f1c3"; } +.bi-box-arrow-up-left::before { content: "\f1c4"; } +.bi-box-arrow-up-right::before { content: "\f1c5"; } +.bi-box-arrow-up::before { content: "\f1c6"; } +.bi-box-seam::before { content: "\f1c7"; } +.bi-box::before { content: "\f1c8"; } +.bi-braces::before { content: "\f1c9"; } +.bi-bricks::before { content: "\f1ca"; } +.bi-briefcase-fill::before { content: "\f1cb"; } +.bi-briefcase::before { content: "\f1cc"; } +.bi-brightness-alt-high-fill::before { content: "\f1cd"; } +.bi-brightness-alt-high::before { content: "\f1ce"; } +.bi-brightness-alt-low-fill::before { content: "\f1cf"; } +.bi-brightness-alt-low::before { content: "\f1d0"; } +.bi-brightness-high-fill::before { content: "\f1d1"; } +.bi-brightness-high::before { content: "\f1d2"; } +.bi-brightness-low-fill::before { content: "\f1d3"; } +.bi-brightness-low::before { content: "\f1d4"; } +.bi-broadcast-pin::before { content: "\f1d5"; } +.bi-broadcast::before { content: "\f1d6"; } +.bi-brush-fill::before { content: "\f1d7"; } +.bi-brush::before { content: "\f1d8"; } +.bi-bucket-fill::before { content: "\f1d9"; } +.bi-bucket::before { content: "\f1da"; } +.bi-bug-fill::before { content: "\f1db"; } +.bi-bug::before { content: "\f1dc"; } +.bi-building::before { content: "\f1dd"; } +.bi-bullseye::before { content: "\f1de"; } +.bi-calculator-fill::before { content: "\f1df"; } +.bi-calculator::before { content: "\f1e0"; } +.bi-calendar-check-fill::before { content: "\f1e1"; } +.bi-calendar-check::before { content: "\f1e2"; } +.bi-calendar-date-fill::before { content: "\f1e3"; } +.bi-calendar-date::before { content: "\f1e4"; } +.bi-calendar-day-fill::before { content: "\f1e5"; } +.bi-calendar-day::before { content: "\f1e6"; } +.bi-calendar-event-fill::before { content: "\f1e7"; } +.bi-calendar-event::before { content: "\f1e8"; } +.bi-calendar-fill::before { content: "\f1e9"; } +.bi-calendar-minus-fill::before { content: "\f1ea"; } +.bi-calendar-minus::before { content: "\f1eb"; } +.bi-calendar-month-fill::before { content: "\f1ec"; } +.bi-calendar-month::before { content: "\f1ed"; } +.bi-calendar-plus-fill::before { content: "\f1ee"; } +.bi-calendar-plus::before { content: "\f1ef"; } +.bi-calendar-range-fill::before { content: "\f1f0"; } +.bi-calendar-range::before { content: "\f1f1"; } +.bi-calendar-week-fill::before { content: "\f1f2"; } +.bi-calendar-week::before { content: "\f1f3"; } +.bi-calendar-x-fill::before { content: "\f1f4"; } +.bi-calendar-x::before { content: "\f1f5"; } +.bi-calendar::before { content: "\f1f6"; } +.bi-calendar2-check-fill::before { content: "\f1f7"; } +.bi-calendar2-check::before { content: "\f1f8"; } +.bi-calendar2-date-fill::before { content: "\f1f9"; } +.bi-calendar2-date::before { content: "\f1fa"; } +.bi-calendar2-day-fill::before { content: "\f1fb"; } +.bi-calendar2-day::before { content: "\f1fc"; } +.bi-calendar2-event-fill::before { content: "\f1fd"; } +.bi-calendar2-event::before { content: "\f1fe"; } +.bi-calendar2-fill::before { content: "\f1ff"; } +.bi-calendar2-minus-fill::before { content: "\f200"; } +.bi-calendar2-minus::before { content: "\f201"; } +.bi-calendar2-month-fill::before { content: "\f202"; } +.bi-calendar2-month::before { content: "\f203"; } +.bi-calendar2-plus-fill::before { content: "\f204"; } +.bi-calendar2-plus::before { content: "\f205"; } +.bi-calendar2-range-fill::before { content: "\f206"; } +.bi-calendar2-range::before { content: "\f207"; } +.bi-calendar2-week-fill::before { content: "\f208"; } +.bi-calendar2-week::before { content: "\f209"; } +.bi-calendar2-x-fill::before { content: "\f20a"; } +.bi-calendar2-x::before { content: "\f20b"; } +.bi-calendar2::before { content: "\f20c"; } +.bi-calendar3-event-fill::before { content: "\f20d"; } +.bi-calendar3-event::before { content: "\f20e"; } +.bi-calendar3-fill::before { content: "\f20f"; } +.bi-calendar3-range-fill::before { content: "\f210"; } +.bi-calendar3-range::before { content: "\f211"; } +.bi-calendar3-week-fill::before { content: "\f212"; } +.bi-calendar3-week::before { content: "\f213"; } +.bi-calendar3::before { content: "\f214"; } +.bi-calendar4-event::before { content: "\f215"; } +.bi-calendar4-range::before { content: "\f216"; } +.bi-calendar4-week::before { content: "\f217"; } +.bi-calendar4::before { content: "\f218"; } +.bi-camera-fill::before { content: "\f219"; } +.bi-camera-reels-fill::before { content: "\f21a"; } +.bi-camera-reels::before { content: "\f21b"; } +.bi-camera-video-fill::before { content: "\f21c"; } +.bi-camera-video-off-fill::before { content: "\f21d"; } +.bi-camera-video-off::before { content: "\f21e"; } +.bi-camera-video::before { content: "\f21f"; } +.bi-camera::before { content: "\f220"; } +.bi-camera2::before { content: "\f221"; } +.bi-capslock-fill::before { content: "\f222"; } +.bi-capslock::before { content: "\f223"; } +.bi-card-checklist::before { content: "\f224"; } +.bi-card-heading::before { content: "\f225"; } +.bi-card-image::before { content: "\f226"; } +.bi-card-list::before { content: "\f227"; } +.bi-card-text::before { content: "\f228"; } +.bi-caret-down-fill::before { content: "\f229"; } +.bi-caret-down-square-fill::before { content: "\f22a"; } +.bi-caret-down-square::before { content: "\f22b"; } +.bi-caret-down::before { content: "\f22c"; } +.bi-caret-left-fill::before { content: "\f22d"; } +.bi-caret-left-square-fill::before { content: "\f22e"; } +.bi-caret-left-square::before { content: "\f22f"; } +.bi-caret-left::before { content: "\f230"; } +.bi-caret-right-fill::before { content: "\f231"; } +.bi-caret-right-square-fill::before { content: "\f232"; } +.bi-caret-right-square::before { content: "\f233"; } +.bi-caret-right::before { content: "\f234"; } +.bi-caret-up-fill::before { content: "\f235"; } +.bi-caret-up-square-fill::before { content: "\f236"; } +.bi-caret-up-square::before { content: "\f237"; } +.bi-caret-up::before { content: "\f238"; } +.bi-cart-check-fill::before { content: "\f239"; } +.bi-cart-check::before { content: "\f23a"; } +.bi-cart-dash-fill::before { content: "\f23b"; } +.bi-cart-dash::before { content: "\f23c"; } +.bi-cart-fill::before { content: "\f23d"; } +.bi-cart-plus-fill::before { content: "\f23e"; } +.bi-cart-plus::before { content: "\f23f"; } +.bi-cart-x-fill::before { content: "\f240"; } +.bi-cart-x::before { content: "\f241"; } +.bi-cart::before { content: "\f242"; } +.bi-cart2::before { content: "\f243"; } +.bi-cart3::before { content: "\f244"; } +.bi-cart4::before { content: "\f245"; } +.bi-cash-stack::before { content: "\f246"; } +.bi-cash::before { content: "\f247"; } +.bi-cast::before { content: "\f248"; } +.bi-chat-dots-fill::before { content: "\f249"; } +.bi-chat-dots::before { content: "\f24a"; } +.bi-chat-fill::before { content: "\f24b"; } +.bi-chat-left-dots-fill::before { content: "\f24c"; } +.bi-chat-left-dots::before { content: "\f24d"; } +.bi-chat-left-fill::before { content: "\f24e"; } +.bi-chat-left-quote-fill::before { content: "\f24f"; } +.bi-chat-left-quote::before { content: "\f250"; } +.bi-chat-left-text-fill::before { content: "\f251"; } +.bi-chat-left-text::before { content: "\f252"; } +.bi-chat-left::before { content: "\f253"; } +.bi-chat-quote-fill::before { content: "\f254"; } +.bi-chat-quote::before { content: "\f255"; } +.bi-chat-right-dots-fill::before { content: "\f256"; } +.bi-chat-right-dots::before { content: "\f257"; } +.bi-chat-right-fill::before { content: "\f258"; } +.bi-chat-right-quote-fill::before { content: "\f259"; } +.bi-chat-right-quote::before { content: "\f25a"; } +.bi-chat-right-text-fill::before { content: "\f25b"; } +.bi-chat-right-text::before { content: "\f25c"; } +.bi-chat-right::before { content: "\f25d"; } +.bi-chat-square-dots-fill::before { content: "\f25e"; } +.bi-chat-square-dots::before { content: "\f25f"; } +.bi-chat-square-fill::before { content: "\f260"; } +.bi-chat-square-quote-fill::before { content: "\f261"; } +.bi-chat-square-quote::before { content: "\f262"; } +.bi-chat-square-text-fill::before { content: "\f263"; } +.bi-chat-square-text::before { content: "\f264"; } +.bi-chat-square::before { content: "\f265"; } +.bi-chat-text-fill::before { content: "\f266"; } +.bi-chat-text::before { content: "\f267"; } +.bi-chat::before { content: "\f268"; } +.bi-check-all::before { content: "\f269"; } +.bi-check-circle-fill::before { content: "\f26a"; } +.bi-check-circle::before { content: "\f26b"; } +.bi-check-square-fill::before { content: "\f26c"; } +.bi-check-square::before { content: "\f26d"; } +.bi-check::before { content: "\f26e"; } +.bi-check2-all::before { content: "\f26f"; } +.bi-check2-circle::before { content: "\f270"; } +.bi-check2-square::before { content: "\f271"; } +.bi-check2::before { content: "\f272"; } +.bi-chevron-bar-contract::before { content: "\f273"; } +.bi-chevron-bar-down::before { content: "\f274"; } +.bi-chevron-bar-expand::before { content: "\f275"; } +.bi-chevron-bar-left::before { content: "\f276"; } +.bi-chevron-bar-right::before { content: "\f277"; } +.bi-chevron-bar-up::before { content: "\f278"; } +.bi-chevron-compact-down::before { content: "\f279"; } +.bi-chevron-compact-left::before { content: "\f27a"; } +.bi-chevron-compact-right::before { content: "\f27b"; } +.bi-chevron-compact-up::before { content: "\f27c"; } +.bi-chevron-contract::before { content: "\f27d"; } +.bi-chevron-double-down::before { content: "\f27e"; } +.bi-chevron-double-left::before { content: "\f27f"; } +.bi-chevron-double-right::before { content: "\f280"; } +.bi-chevron-double-up::before { content: "\f281"; } +.bi-chevron-down::before { content: "\f282"; } +.bi-chevron-expand::before { content: "\f283"; } +.bi-chevron-left::before { content: "\f284"; } +.bi-chevron-right::before { content: "\f285"; } +.bi-chevron-up::before { content: "\f286"; } +.bi-circle-fill::before { content: "\f287"; } +.bi-circle-half::before { content: "\f288"; } +.bi-circle-square::before { content: "\f289"; } +.bi-circle::before { content: "\f28a"; } +.bi-clipboard-check::before { content: "\f28b"; } +.bi-clipboard-data::before { content: "\f28c"; } +.bi-clipboard-minus::before { content: "\f28d"; } +.bi-clipboard-plus::before { content: "\f28e"; } +.bi-clipboard-x::before { content: "\f28f"; } +.bi-clipboard::before { content: "\f290"; } +.bi-clock-fill::before { content: "\f291"; } +.bi-clock-history::before { content: "\f292"; } +.bi-clock::before { content: "\f293"; } +.bi-cloud-arrow-down-fill::before { content: "\f294"; } +.bi-cloud-arrow-down::before { content: "\f295"; } +.bi-cloud-arrow-up-fill::before { content: "\f296"; } +.bi-cloud-arrow-up::before { content: "\f297"; } +.bi-cloud-check-fill::before { content: "\f298"; } +.bi-cloud-check::before { content: "\f299"; } +.bi-cloud-download-fill::before { content: "\f29a"; } +.bi-cloud-download::before { content: "\f29b"; } +.bi-cloud-drizzle-fill::before { content: "\f29c"; } +.bi-cloud-drizzle::before { content: "\f29d"; } +.bi-cloud-fill::before { content: "\f29e"; } +.bi-cloud-fog-fill::before { content: "\f29f"; } +.bi-cloud-fog::before { content: "\f2a0"; } +.bi-cloud-fog2-fill::before { content: "\f2a1"; } +.bi-cloud-fog2::before { content: "\f2a2"; } +.bi-cloud-hail-fill::before { content: "\f2a3"; } +.bi-cloud-hail::before { content: "\f2a4"; } +.bi-cloud-haze-1::before { content: "\f2a5"; } +.bi-cloud-haze-fill::before { content: "\f2a6"; } +.bi-cloud-haze::before { content: "\f2a7"; } +.bi-cloud-haze2-fill::before { content: "\f2a8"; } +.bi-cloud-lightning-fill::before { content: "\f2a9"; } +.bi-cloud-lightning-rain-fill::before { content: "\f2aa"; } +.bi-cloud-lightning-rain::before { content: "\f2ab"; } +.bi-cloud-lightning::before { content: "\f2ac"; } +.bi-cloud-minus-fill::before { content: "\f2ad"; } +.bi-cloud-minus::before { content: "\f2ae"; } +.bi-cloud-moon-fill::before { content: "\f2af"; } +.bi-cloud-moon::before { content: "\f2b0"; } +.bi-cloud-plus-fill::before { content: "\f2b1"; } +.bi-cloud-plus::before { content: "\f2b2"; } +.bi-cloud-rain-fill::before { content: "\f2b3"; } +.bi-cloud-rain-heavy-fill::before { content: "\f2b4"; } +.bi-cloud-rain-heavy::before { content: "\f2b5"; } +.bi-cloud-rain::before { content: "\f2b6"; } +.bi-cloud-slash-fill::before { content: "\f2b7"; } +.bi-cloud-slash::before { content: "\f2b8"; } +.bi-cloud-sleet-fill::before { content: "\f2b9"; } +.bi-cloud-sleet::before { content: "\f2ba"; } +.bi-cloud-snow-fill::before { content: "\f2bb"; } +.bi-cloud-snow::before { content: "\f2bc"; } +.bi-cloud-sun-fill::before { content: "\f2bd"; } +.bi-cloud-sun::before { content: "\f2be"; } +.bi-cloud-upload-fill::before { content: "\f2bf"; } +.bi-cloud-upload::before { content: "\f2c0"; } +.bi-cloud::before { content: "\f2c1"; } +.bi-clouds-fill::before { content: "\f2c2"; } +.bi-clouds::before { content: "\f2c3"; } +.bi-cloudy-fill::before { content: "\f2c4"; } +.bi-cloudy::before { content: "\f2c5"; } +.bi-code-slash::before { content: "\f2c6"; } +.bi-code-square::before { content: "\f2c7"; } +.bi-code::before { content: "\f2c8"; } +.bi-collection-fill::before { content: "\f2c9"; } +.bi-collection-play-fill::before { content: "\f2ca"; } +.bi-collection-play::before { content: "\f2cb"; } +.bi-collection::before { content: "\f2cc"; } +.bi-columns-gap::before { content: "\f2cd"; } +.bi-columns::before { content: "\f2ce"; } +.bi-command::before { content: "\f2cf"; } +.bi-compass-fill::before { content: "\f2d0"; } +.bi-compass::before { content: "\f2d1"; } +.bi-cone-striped::before { content: "\f2d2"; } +.bi-cone::before { content: "\f2d3"; } +.bi-controller::before { content: "\f2d4"; } +.bi-cpu-fill::before { content: "\f2d5"; } +.bi-cpu::before { content: "\f2d6"; } +.bi-credit-card-2-back-fill::before { content: "\f2d7"; } +.bi-credit-card-2-back::before { content: "\f2d8"; } +.bi-credit-card-2-front-fill::before { content: "\f2d9"; } +.bi-credit-card-2-front::before { content: "\f2da"; } +.bi-credit-card-fill::before { content: "\f2db"; } +.bi-credit-card::before { content: "\f2dc"; } +.bi-crop::before { content: "\f2dd"; } +.bi-cup-fill::before { content: "\f2de"; } +.bi-cup-straw::before { content: "\f2df"; } +.bi-cup::before { content: "\f2e0"; } +.bi-cursor-fill::before { content: "\f2e1"; } +.bi-cursor-text::before { content: "\f2e2"; } +.bi-cursor::before { content: "\f2e3"; } +.bi-dash-circle-dotted::before { content: "\f2e4"; } +.bi-dash-circle-fill::before { content: "\f2e5"; } +.bi-dash-circle::before { content: "\f2e6"; } +.bi-dash-square-dotted::before { content: "\f2e7"; } +.bi-dash-square-fill::before { content: "\f2e8"; } +.bi-dash-square::before { content: "\f2e9"; } +.bi-dash::before { content: "\f2ea"; } +.bi-diagram-2-fill::before { content: "\f2eb"; } +.bi-diagram-2::before { content: "\f2ec"; } +.bi-diagram-3-fill::before { content: "\f2ed"; } +.bi-diagram-3::before { content: "\f2ee"; } +.bi-diamond-fill::before { content: "\f2ef"; } +.bi-diamond-half::before { content: "\f2f0"; } +.bi-diamond::before { content: "\f2f1"; } +.bi-dice-1-fill::before { content: "\f2f2"; } +.bi-dice-1::before { content: "\f2f3"; } +.bi-dice-2-fill::before { content: "\f2f4"; } +.bi-dice-2::before { content: "\f2f5"; } +.bi-dice-3-fill::before { content: "\f2f6"; } +.bi-dice-3::before { content: "\f2f7"; } +.bi-dice-4-fill::before { content: "\f2f8"; } +.bi-dice-4::before { content: "\f2f9"; } +.bi-dice-5-fill::before { content: "\f2fa"; } +.bi-dice-5::before { content: "\f2fb"; } +.bi-dice-6-fill::before { content: "\f2fc"; } +.bi-dice-6::before { content: "\f2fd"; } +.bi-disc-fill::before { content: "\f2fe"; } +.bi-disc::before { content: "\f2ff"; } +.bi-discord::before { content: "\f300"; } +.bi-display-fill::before { content: "\f301"; } +.bi-display::before { content: "\f302"; } +.bi-distribute-horizontal::before { content: "\f303"; } +.bi-distribute-vertical::before { content: "\f304"; } +.bi-door-closed-fill::before { content: "\f305"; } +.bi-door-closed::before { content: "\f306"; } +.bi-door-open-fill::before { content: "\f307"; } +.bi-door-open::before { content: "\f308"; } +.bi-dot::before { content: "\f309"; } +.bi-download::before { content: "\f30a"; } +.bi-droplet-fill::before { content: "\f30b"; } +.bi-droplet-half::before { content: "\f30c"; } +.bi-droplet::before { content: "\f30d"; } +.bi-earbuds::before { content: "\f30e"; } +.bi-easel-fill::before { content: "\f30f"; } +.bi-easel::before { content: "\f310"; } +.bi-egg-fill::before { content: "\f311"; } +.bi-egg-fried::before { content: "\f312"; } +.bi-egg::before { content: "\f313"; } +.bi-eject-fill::before { content: "\f314"; } +.bi-eject::before { content: "\f315"; } +.bi-emoji-angry-fill::before { content: "\f316"; } +.bi-emoji-angry::before { content: "\f317"; } +.bi-emoji-dizzy-fill::before { content: "\f318"; } +.bi-emoji-dizzy::before { content: "\f319"; } +.bi-emoji-expressionless-fill::before { content: "\f31a"; } +.bi-emoji-expressionless::before { content: "\f31b"; } +.bi-emoji-frown-fill::before { content: "\f31c"; } +.bi-emoji-frown::before { content: "\f31d"; } +.bi-emoji-heart-eyes-fill::before { content: "\f31e"; } +.bi-emoji-heart-eyes::before { content: "\f31f"; } +.bi-emoji-laughing-fill::before { content: "\f320"; } +.bi-emoji-laughing::before { content: "\f321"; } +.bi-emoji-neutral-fill::before { content: "\f322"; } +.bi-emoji-neutral::before { content: "\f323"; } +.bi-emoji-smile-fill::before { content: "\f324"; } +.bi-emoji-smile-upside-down-fill::before { content: "\f325"; } +.bi-emoji-smile-upside-down::before { content: "\f326"; } +.bi-emoji-smile::before { content: "\f327"; } +.bi-emoji-sunglasses-fill::before { content: "\f328"; } +.bi-emoji-sunglasses::before { content: "\f329"; } +.bi-emoji-wink-fill::before { content: "\f32a"; } +.bi-emoji-wink::before { content: "\f32b"; } +.bi-envelope-fill::before { content: "\f32c"; } +.bi-envelope-open-fill::before { content: "\f32d"; } +.bi-envelope-open::before { content: "\f32e"; } +.bi-envelope::before { content: "\f32f"; } +.bi-eraser-fill::before { content: "\f330"; } +.bi-eraser::before { content: "\f331"; } +.bi-exclamation-circle-fill::before { content: "\f332"; } +.bi-exclamation-circle::before { content: "\f333"; } +.bi-exclamation-diamond-fill::before { content: "\f334"; } +.bi-exclamation-diamond::before { content: "\f335"; } +.bi-exclamation-octagon-fill::before { content: "\f336"; } +.bi-exclamation-octagon::before { content: "\f337"; } +.bi-exclamation-square-fill::before { content: "\f338"; } +.bi-exclamation-square::before { content: "\f339"; } +.bi-exclamation-triangle-fill::before { content: "\f33a"; } +.bi-exclamation-triangle::before { content: "\f33b"; } +.bi-exclamation::before { content: "\f33c"; } +.bi-exclude::before { content: "\f33d"; } +.bi-eye-fill::before { content: "\f33e"; } +.bi-eye-slash-fill::before { content: "\f33f"; } +.bi-eye-slash::before { content: "\f340"; } +.bi-eye::before { content: "\f341"; } +.bi-eyedropper::before { content: "\f342"; } +.bi-eyeglasses::before { content: "\f343"; } +.bi-facebook::before { content: "\f344"; } +.bi-file-arrow-down-fill::before { content: "\f345"; } +.bi-file-arrow-down::before { content: "\f346"; } +.bi-file-arrow-up-fill::before { content: "\f347"; } +.bi-file-arrow-up::before { content: "\f348"; } +.bi-file-bar-graph-fill::before { content: "\f349"; } +.bi-file-bar-graph::before { content: "\f34a"; } +.bi-file-binary-fill::before { content: "\f34b"; } +.bi-file-binary::before { content: "\f34c"; } +.bi-file-break-fill::before { content: "\f34d"; } +.bi-file-break::before { content: "\f34e"; } +.bi-file-check-fill::before { content: "\f34f"; } +.bi-file-check::before { content: "\f350"; } +.bi-file-code-fill::before { content: "\f351"; } +.bi-file-code::before { content: "\f352"; } +.bi-file-diff-fill::before { content: "\f353"; } +.bi-file-diff::before { content: "\f354"; } +.bi-file-earmark-arrow-down-fill::before { content: "\f355"; } +.bi-file-earmark-arrow-down::before { content: "\f356"; } +.bi-file-earmark-arrow-up-fill::before { content: "\f357"; } +.bi-file-earmark-arrow-up::before { content: "\f358"; } +.bi-file-earmark-bar-graph-fill::before { content: "\f359"; } +.bi-file-earmark-bar-graph::before { content: "\f35a"; } +.bi-file-earmark-binary-fill::before { content: "\f35b"; } +.bi-file-earmark-binary::before { content: "\f35c"; } +.bi-file-earmark-break-fill::before { content: "\f35d"; } +.bi-file-earmark-break::before { content: "\f35e"; } +.bi-file-earmark-check-fill::before { content: "\f35f"; } +.bi-file-earmark-check::before { content: "\f360"; } +.bi-file-earmark-code-fill::before { content: "\f361"; } +.bi-file-earmark-code::before { content: "\f362"; } +.bi-file-earmark-diff-fill::before { content: "\f363"; } +.bi-file-earmark-diff::before { content: "\f364"; } +.bi-file-earmark-easel-fill::before { content: "\f365"; } +.bi-file-earmark-easel::before { content: "\f366"; } +.bi-file-earmark-excel-fill::before { content: "\f367"; } +.bi-file-earmark-excel::before { content: "\f368"; } +.bi-file-earmark-fill::before { content: "\f369"; } +.bi-file-earmark-font-fill::before { content: "\f36a"; } +.bi-file-earmark-font::before { content: "\f36b"; } +.bi-file-earmark-image-fill::before { content: "\f36c"; } +.bi-file-earmark-image::before { content: "\f36d"; } +.bi-file-earmark-lock-fill::before { content: "\f36e"; } +.bi-file-earmark-lock::before { content: "\f36f"; } +.bi-file-earmark-lock2-fill::before { content: "\f370"; } +.bi-file-earmark-lock2::before { content: "\f371"; } +.bi-file-earmark-medical-fill::before { content: "\f372"; } +.bi-file-earmark-medical::before { content: "\f373"; } +.bi-file-earmark-minus-fill::before { content: "\f374"; } +.bi-file-earmark-minus::before { content: "\f375"; } +.bi-file-earmark-music-fill::before { content: "\f376"; } +.bi-file-earmark-music::before { content: "\f377"; } +.bi-file-earmark-person-fill::before { content: "\f378"; } +.bi-file-earmark-person::before { content: "\f379"; } +.bi-file-earmark-play-fill::before { content: "\f37a"; } +.bi-file-earmark-play::before { content: "\f37b"; } +.bi-file-earmark-plus-fill::before { content: "\f37c"; } +.bi-file-earmark-plus::before { content: "\f37d"; } +.bi-file-earmark-post-fill::before { content: "\f37e"; } +.bi-file-earmark-post::before { content: "\f37f"; } +.bi-file-earmark-ppt-fill::before { content: "\f380"; } +.bi-file-earmark-ppt::before { content: "\f381"; } +.bi-file-earmark-richtext-fill::before { content: "\f382"; } +.bi-file-earmark-richtext::before { content: "\f383"; } +.bi-file-earmark-ruled-fill::before { content: "\f384"; } +.bi-file-earmark-ruled::before { content: "\f385"; } +.bi-file-earmark-slides-fill::before { content: "\f386"; } +.bi-file-earmark-slides::before { content: "\f387"; } +.bi-file-earmark-spreadsheet-fill::before { content: "\f388"; } +.bi-file-earmark-spreadsheet::before { content: "\f389"; } +.bi-file-earmark-text-fill::before { content: "\f38a"; } +.bi-file-earmark-text::before { content: "\f38b"; } +.bi-file-earmark-word-fill::before { content: "\f38c"; } +.bi-file-earmark-word::before { content: "\f38d"; } +.bi-file-earmark-x-fill::before { content: "\f38e"; } +.bi-file-earmark-x::before { content: "\f38f"; } +.bi-file-earmark-zip-fill::before { content: "\f390"; } +.bi-file-earmark-zip::before { content: "\f391"; } +.bi-file-earmark::before { content: "\f392"; } +.bi-file-easel-fill::before { content: "\f393"; } +.bi-file-easel::before { content: "\f394"; } +.bi-file-excel-fill::before { content: "\f395"; } +.bi-file-excel::before { content: "\f396"; } +.bi-file-fill::before { content: "\f397"; } +.bi-file-font-fill::before { content: "\f398"; } +.bi-file-font::before { content: "\f399"; } +.bi-file-image-fill::before { content: "\f39a"; } +.bi-file-image::before { content: "\f39b"; } +.bi-file-lock-fill::before { content: "\f39c"; } +.bi-file-lock::before { content: "\f39d"; } +.bi-file-lock2-fill::before { content: "\f39e"; } +.bi-file-lock2::before { content: "\f39f"; } +.bi-file-medical-fill::before { content: "\f3a0"; } +.bi-file-medical::before { content: "\f3a1"; } +.bi-file-minus-fill::before { content: "\f3a2"; } +.bi-file-minus::before { content: "\f3a3"; } +.bi-file-music-fill::before { content: "\f3a4"; } +.bi-file-music::before { content: "\f3a5"; } +.bi-file-person-fill::before { content: "\f3a6"; } +.bi-file-person::before { content: "\f3a7"; } +.bi-file-play-fill::before { content: "\f3a8"; } +.bi-file-play::before { content: "\f3a9"; } +.bi-file-plus-fill::before { content: "\f3aa"; } +.bi-file-plus::before { content: "\f3ab"; } +.bi-file-post-fill::before { content: "\f3ac"; } +.bi-file-post::before { content: "\f3ad"; } +.bi-file-ppt-fill::before { content: "\f3ae"; } +.bi-file-ppt::before { content: "\f3af"; } +.bi-file-richtext-fill::before { content: "\f3b0"; } +.bi-file-richtext::before { content: "\f3b1"; } +.bi-file-ruled-fill::before { content: "\f3b2"; } +.bi-file-ruled::before { content: "\f3b3"; } +.bi-file-slides-fill::before { content: "\f3b4"; } +.bi-file-slides::before { content: "\f3b5"; } +.bi-file-spreadsheet-fill::before { content: "\f3b6"; } +.bi-file-spreadsheet::before { content: "\f3b7"; } +.bi-file-text-fill::before { content: "\f3b8"; } +.bi-file-text::before { content: "\f3b9"; } +.bi-file-word-fill::before { content: "\f3ba"; } +.bi-file-word::before { content: "\f3bb"; } +.bi-file-x-fill::before { content: "\f3bc"; } +.bi-file-x::before { content: "\f3bd"; } +.bi-file-zip-fill::before { content: "\f3be"; } +.bi-file-zip::before { content: "\f3bf"; } +.bi-file::before { content: "\f3c0"; } +.bi-files-alt::before { content: "\f3c1"; } +.bi-files::before { content: "\f3c2"; } +.bi-film::before { content: "\f3c3"; } +.bi-filter-circle-fill::before { content: "\f3c4"; } +.bi-filter-circle::before { content: "\f3c5"; } +.bi-filter-left::before { content: "\f3c6"; } +.bi-filter-right::before { content: "\f3c7"; } +.bi-filter-square-fill::before { content: "\f3c8"; } +.bi-filter-square::before { content: "\f3c9"; } +.bi-filter::before { content: "\f3ca"; } +.bi-flag-fill::before { content: "\f3cb"; } +.bi-flag::before { content: "\f3cc"; } +.bi-flower1::before { content: "\f3cd"; } +.bi-flower2::before { content: "\f3ce"; } +.bi-flower3::before { content: "\f3cf"; } +.bi-folder-check::before { content: "\f3d0"; } +.bi-folder-fill::before { content: "\f3d1"; } +.bi-folder-minus::before { content: "\f3d2"; } +.bi-folder-plus::before { content: "\f3d3"; } +.bi-folder-symlink-fill::before { content: "\f3d4"; } +.bi-folder-symlink::before { content: "\f3d5"; } +.bi-folder-x::before { content: "\f3d6"; } +.bi-folder::before { content: "\f3d7"; } +.bi-folder2-open::before { content: "\f3d8"; } +.bi-folder2::before { content: "\f3d9"; } +.bi-fonts::before { content: "\f3da"; } +.bi-forward-fill::before { content: "\f3db"; } +.bi-forward::before { content: "\f3dc"; } +.bi-front::before { content: "\f3dd"; } +.bi-fullscreen-exit::before { content: "\f3de"; } +.bi-fullscreen::before { content: "\f3df"; } +.bi-funnel-fill::before { content: "\f3e0"; } +.bi-funnel::before { content: "\f3e1"; } +.bi-gear-fill::before { content: "\f3e2"; } +.bi-gear-wide-connected::before { content: "\f3e3"; } +.bi-gear-wide::before { content: "\f3e4"; } +.bi-gear::before { content: "\f3e5"; } +.bi-gem::before { content: "\f3e6"; } +.bi-geo-alt-fill::before { content: "\f3e7"; } +.bi-geo-alt::before { content: "\f3e8"; } +.bi-geo-fill::before { content: "\f3e9"; } +.bi-geo::before { content: "\f3ea"; } +.bi-gift-fill::before { content: "\f3eb"; } +.bi-gift::before { content: "\f3ec"; } +.bi-github::before { content: "\f3ed"; } +.bi-globe::before { content: "\f3ee"; } +.bi-globe2::before { content: "\f3ef"; } +.bi-google::before { content: "\f3f0"; } +.bi-graph-down::before { content: "\f3f1"; } +.bi-graph-up::before { content: "\f3f2"; } +.bi-grid-1x2-fill::before { content: "\f3f3"; } +.bi-grid-1x2::before { content: "\f3f4"; } +.bi-grid-3x2-gap-fill::before { content: "\f3f5"; } +.bi-grid-3x2-gap::before { content: "\f3f6"; } +.bi-grid-3x2::before { content: "\f3f7"; } +.bi-grid-3x3-gap-fill::before { content: "\f3f8"; } +.bi-grid-3x3-gap::before { content: "\f3f9"; } +.bi-grid-3x3::before { content: "\f3fa"; } +.bi-grid-fill::before { content: "\f3fb"; } +.bi-grid::before { content: "\f3fc"; } +.bi-grip-horizontal::before { content: "\f3fd"; } +.bi-grip-vertical::before { content: "\f3fe"; } +.bi-hammer::before { content: "\f3ff"; } +.bi-hand-index-fill::before { content: "\f400"; } +.bi-hand-index-thumb-fill::before { content: "\f401"; } +.bi-hand-index-thumb::before { content: "\f402"; } +.bi-hand-index::before { content: "\f403"; } +.bi-hand-thumbs-down-fill::before { content: "\f404"; } +.bi-hand-thumbs-down::before { content: "\f405"; } +.bi-hand-thumbs-up-fill::before { content: "\f406"; } +.bi-hand-thumbs-up::before { content: "\f407"; } +.bi-handbag-fill::before { content: "\f408"; } +.bi-handbag::before { content: "\f409"; } +.bi-hash::before { content: "\f40a"; } +.bi-hdd-fill::before { content: "\f40b"; } +.bi-hdd-network-fill::before { content: "\f40c"; } +.bi-hdd-network::before { content: "\f40d"; } +.bi-hdd-rack-fill::before { content: "\f40e"; } +.bi-hdd-rack::before { content: "\f40f"; } +.bi-hdd-stack-fill::before { content: "\f410"; } +.bi-hdd-stack::before { content: "\f411"; } +.bi-hdd::before { content: "\f412"; } +.bi-headphones::before { content: "\f413"; } +.bi-headset::before { content: "\f414"; } +.bi-heart-fill::before { content: "\f415"; } +.bi-heart-half::before { content: "\f416"; } +.bi-heart::before { content: "\f417"; } +.bi-heptagon-fill::before { content: "\f418"; } +.bi-heptagon-half::before { content: "\f419"; } +.bi-heptagon::before { content: "\f41a"; } +.bi-hexagon-fill::before { content: "\f41b"; } +.bi-hexagon-half::before { content: "\f41c"; } +.bi-hexagon::before { content: "\f41d"; } +.bi-hourglass-bottom::before { content: "\f41e"; } +.bi-hourglass-split::before { content: "\f41f"; } +.bi-hourglass-top::before { content: "\f420"; } +.bi-hourglass::before { content: "\f421"; } +.bi-house-door-fill::before { content: "\f422"; } +.bi-house-door::before { content: "\f423"; } +.bi-house-fill::before { content: "\f424"; } +.bi-house::before { content: "\f425"; } +.bi-hr::before { content: "\f426"; } +.bi-hurricane::before { content: "\f427"; } +.bi-image-alt::before { content: "\f428"; } +.bi-image-fill::before { content: "\f429"; } +.bi-image::before { content: "\f42a"; } +.bi-images::before { content: "\f42b"; } +.bi-inbox-fill::before { content: "\f42c"; } +.bi-inbox::before { content: "\f42d"; } +.bi-inboxes-fill::before { content: "\f42e"; } +.bi-inboxes::before { content: "\f42f"; } +.bi-info-circle-fill::before { content: "\f430"; } +.bi-info-circle::before { content: "\f431"; } +.bi-info-square-fill::before { content: "\f432"; } +.bi-info-square::before { content: "\f433"; } +.bi-info::before { content: "\f434"; } +.bi-input-cursor-text::before { content: "\f435"; } +.bi-input-cursor::before { content: "\f436"; } +.bi-instagram::before { content: "\f437"; } +.bi-intersect::before { content: "\f438"; } +.bi-journal-album::before { content: "\f439"; } +.bi-journal-arrow-down::before { content: "\f43a"; } +.bi-journal-arrow-up::before { content: "\f43b"; } +.bi-journal-bookmark-fill::before { content: "\f43c"; } +.bi-journal-bookmark::before { content: "\f43d"; } +.bi-journal-check::before { content: "\f43e"; } +.bi-journal-code::before { content: "\f43f"; } +.bi-journal-medical::before { content: "\f440"; } +.bi-journal-minus::before { content: "\f441"; } +.bi-journal-plus::before { content: "\f442"; } +.bi-journal-richtext::before { content: "\f443"; } +.bi-journal-text::before { content: "\f444"; } +.bi-journal-x::before { content: "\f445"; } +.bi-journal::before { content: "\f446"; } +.bi-journals::before { content: "\f447"; } +.bi-joystick::before { content: "\f448"; } +.bi-justify-left::before { content: "\f449"; } +.bi-justify-right::before { content: "\f44a"; } +.bi-justify::before { content: "\f44b"; } +.bi-kanban-fill::before { content: "\f44c"; } +.bi-kanban::before { content: "\f44d"; } +.bi-key-fill::before { content: "\f44e"; } +.bi-key::before { content: "\f44f"; } +.bi-keyboard-fill::before { content: "\f450"; } +.bi-keyboard::before { content: "\f451"; } +.bi-ladder::before { content: "\f452"; } +.bi-lamp-fill::before { content: "\f453"; } +.bi-lamp::before { content: "\f454"; } +.bi-laptop-fill::before { content: "\f455"; } +.bi-laptop::before { content: "\f456"; } +.bi-layer-backward::before { content: "\f457"; } +.bi-layer-forward::before { content: "\f458"; } +.bi-layers-fill::before { content: "\f459"; } +.bi-layers-half::before { content: "\f45a"; } +.bi-layers::before { content: "\f45b"; } +.bi-layout-sidebar-inset-reverse::before { content: "\f45c"; } +.bi-layout-sidebar-inset::before { content: "\f45d"; } +.bi-layout-sidebar-reverse::before { content: "\f45e"; } +.bi-layout-sidebar::before { content: "\f45f"; } +.bi-layout-split::before { content: "\f460"; } +.bi-layout-text-sidebar-reverse::before { content: "\f461"; } +.bi-layout-text-sidebar::before { content: "\f462"; } +.bi-layout-text-window-reverse::before { content: "\f463"; } +.bi-layout-text-window::before { content: "\f464"; } +.bi-layout-three-columns::before { content: "\f465"; } +.bi-layout-wtf::before { content: "\f466"; } +.bi-life-preserver::before { content: "\f467"; } +.bi-lightbulb-fill::before { content: "\f468"; } +.bi-lightbulb-off-fill::before { content: "\f469"; } +.bi-lightbulb-off::before { content: "\f46a"; } +.bi-lightbulb::before { content: "\f46b"; } +.bi-lightning-charge-fill::before { content: "\f46c"; } +.bi-lightning-charge::before { content: "\f46d"; } +.bi-lightning-fill::before { content: "\f46e"; } +.bi-lightning::before { content: "\f46f"; } +.bi-link-45deg::before { content: "\f470"; } +.bi-link::before { content: "\f471"; } +.bi-linkedin::before { content: "\f472"; } +.bi-list-check::before { content: "\f473"; } +.bi-list-nested::before { content: "\f474"; } +.bi-list-ol::before { content: "\f475"; } +.bi-list-stars::before { content: "\f476"; } +.bi-list-task::before { content: "\f477"; } +.bi-list-ul::before { content: "\f478"; } +.bi-list::before { content: "\f479"; } +.bi-lock-fill::before { content: "\f47a"; } +.bi-lock::before { content: "\f47b"; } +.bi-mailbox::before { content: "\f47c"; } +.bi-mailbox2::before { content: "\f47d"; } +.bi-map-fill::before { content: "\f47e"; } +.bi-map::before { content: "\f47f"; } +.bi-markdown-fill::before { content: "\f480"; } +.bi-markdown::before { content: "\f481"; } +.bi-mask::before { content: "\f482"; } +.bi-megaphone-fill::before { content: "\f483"; } +.bi-megaphone::before { content: "\f484"; } +.bi-menu-app-fill::before { content: "\f485"; } +.bi-menu-app::before { content: "\f486"; } +.bi-menu-button-fill::before { content: "\f487"; } +.bi-menu-button-wide-fill::before { content: "\f488"; } +.bi-menu-button-wide::before { content: "\f489"; } +.bi-menu-button::before { content: "\f48a"; } +.bi-menu-down::before { content: "\f48b"; } +.bi-menu-up::before { content: "\f48c"; } +.bi-mic-fill::before { content: "\f48d"; } +.bi-mic-mute-fill::before { content: "\f48e"; } +.bi-mic-mute::before { content: "\f48f"; } +.bi-mic::before { content: "\f490"; } +.bi-minecart-loaded::before { content: "\f491"; } +.bi-minecart::before { content: "\f492"; } +.bi-moisture::before { content: "\f493"; } +.bi-moon-fill::before { content: "\f494"; } +.bi-moon-stars-fill::before { content: "\f495"; } +.bi-moon-stars::before { content: "\f496"; } +.bi-moon::before { content: "\f497"; } +.bi-mouse-fill::before { content: "\f498"; } +.bi-mouse::before { content: "\f499"; } +.bi-mouse2-fill::before { content: "\f49a"; } +.bi-mouse2::before { content: "\f49b"; } +.bi-mouse3-fill::before { content: "\f49c"; } +.bi-mouse3::before { content: "\f49d"; } +.bi-music-note-beamed::before { content: "\f49e"; } +.bi-music-note-list::before { content: "\f49f"; } +.bi-music-note::before { content: "\f4a0"; } +.bi-music-player-fill::before { content: "\f4a1"; } +.bi-music-player::before { content: "\f4a2"; } +.bi-newspaper::before { content: "\f4a3"; } +.bi-node-minus-fill::before { content: "\f4a4"; } +.bi-node-minus::before { content: "\f4a5"; } +.bi-node-plus-fill::before { content: "\f4a6"; } +.bi-node-plus::before { content: "\f4a7"; } +.bi-nut-fill::before { content: "\f4a8"; } +.bi-nut::before { content: "\f4a9"; } +.bi-octagon-fill::before { content: "\f4aa"; } +.bi-octagon-half::before { content: "\f4ab"; } +.bi-octagon::before { content: "\f4ac"; } +.bi-option::before { content: "\f4ad"; } +.bi-outlet::before { content: "\f4ae"; } +.bi-paint-bucket::before { content: "\f4af"; } +.bi-palette-fill::before { content: "\f4b0"; } +.bi-palette::before { content: "\f4b1"; } +.bi-palette2::before { content: "\f4b2"; } +.bi-paperclip::before { content: "\f4b3"; } +.bi-paragraph::before { content: "\f4b4"; } +.bi-patch-check-fill::before { content: "\f4b5"; } +.bi-patch-check::before { content: "\f4b6"; } +.bi-patch-exclamation-fill::before { content: "\f4b7"; } +.bi-patch-exclamation::before { content: "\f4b8"; } +.bi-patch-minus-fill::before { content: "\f4b9"; } +.bi-patch-minus::before { content: "\f4ba"; } +.bi-patch-plus-fill::before { content: "\f4bb"; } +.bi-patch-plus::before { content: "\f4bc"; } +.bi-patch-question-fill::before { content: "\f4bd"; } +.bi-patch-question::before { content: "\f4be"; } +.bi-pause-btn-fill::before { content: "\f4bf"; } +.bi-pause-btn::before { content: "\f4c0"; } +.bi-pause-circle-fill::before { content: "\f4c1"; } +.bi-pause-circle::before { content: "\f4c2"; } +.bi-pause-fill::before { content: "\f4c3"; } +.bi-pause::before { content: "\f4c4"; } +.bi-peace-fill::before { content: "\f4c5"; } +.bi-peace::before { content: "\f4c6"; } +.bi-pen-fill::before { content: "\f4c7"; } +.bi-pen::before { content: "\f4c8"; } +.bi-pencil-fill::before { content: "\f4c9"; } +.bi-pencil-square::before { content: "\f4ca"; } +.bi-pencil::before { content: "\f4cb"; } +.bi-pentagon-fill::before { content: "\f4cc"; } +.bi-pentagon-half::before { content: "\f4cd"; } +.bi-pentagon::before { content: "\f4ce"; } +.bi-people-fill::before { content: "\f4cf"; } +.bi-people::before { content: "\f4d0"; } +.bi-percent::before { content: "\f4d1"; } +.bi-person-badge-fill::before { content: "\f4d2"; } +.bi-person-badge::before { content: "\f4d3"; } +.bi-person-bounding-box::before { content: "\f4d4"; } +.bi-person-check-fill::before { content: "\f4d5"; } +.bi-person-check::before { content: "\f4d6"; } +.bi-person-circle::before { content: "\f4d7"; } +.bi-person-dash-fill::before { content: "\f4d8"; } +.bi-person-dash::before { content: "\f4d9"; } +.bi-person-fill::before { content: "\f4da"; } +.bi-person-lines-fill::before { content: "\f4db"; } +.bi-person-plus-fill::before { content: "\f4dc"; } +.bi-person-plus::before { content: "\f4dd"; } +.bi-person-square::before { content: "\f4de"; } +.bi-person-x-fill::before { content: "\f4df"; } +.bi-person-x::before { content: "\f4e0"; } +.bi-person::before { content: "\f4e1"; } +.bi-phone-fill::before { content: "\f4e2"; } +.bi-phone-landscape-fill::before { content: "\f4e3"; } +.bi-phone-landscape::before { content: "\f4e4"; } +.bi-phone-vibrate-fill::before { content: "\f4e5"; } +.bi-phone-vibrate::before { content: "\f4e6"; } +.bi-phone::before { content: "\f4e7"; } +.bi-pie-chart-fill::before { content: "\f4e8"; } +.bi-pie-chart::before { content: "\f4e9"; } +.bi-pin-angle-fill::before { content: "\f4ea"; } +.bi-pin-angle::before { content: "\f4eb"; } +.bi-pin-fill::before { content: "\f4ec"; } +.bi-pin::before { content: "\f4ed"; } +.bi-pip-fill::before { content: "\f4ee"; } +.bi-pip::before { content: "\f4ef"; } +.bi-play-btn-fill::before { content: "\f4f0"; } +.bi-play-btn::before { content: "\f4f1"; } +.bi-play-circle-fill::before { content: "\f4f2"; } +.bi-play-circle::before { content: "\f4f3"; } +.bi-play-fill::before { content: "\f4f4"; } +.bi-play::before { content: "\f4f5"; } +.bi-plug-fill::before { content: "\f4f6"; } +.bi-plug::before { content: "\f4f7"; } +.bi-plus-circle-dotted::before { content: "\f4f8"; } +.bi-plus-circle-fill::before { content: "\f4f9"; } +.bi-plus-circle::before { content: "\f4fa"; } +.bi-plus-square-dotted::before { content: "\f4fb"; } +.bi-plus-square-fill::before { content: "\f4fc"; } +.bi-plus-square::before { content: "\f4fd"; } +.bi-plus::before { content: "\f4fe"; } +.bi-power::before { content: "\f4ff"; } +.bi-printer-fill::before { content: "\f500"; } +.bi-printer::before { content: "\f501"; } +.bi-puzzle-fill::before { content: "\f502"; } +.bi-puzzle::before { content: "\f503"; } +.bi-question-circle-fill::before { content: "\f504"; } +.bi-question-circle::before { content: "\f505"; } +.bi-question-diamond-fill::before { content: "\f506"; } +.bi-question-diamond::before { content: "\f507"; } +.bi-question-octagon-fill::before { content: "\f508"; } +.bi-question-octagon::before { content: "\f509"; } +.bi-question-square-fill::before { content: "\f50a"; } +.bi-question-square::before { content: "\f50b"; } +.bi-question::before { content: "\f50c"; } +.bi-rainbow::before { content: "\f50d"; } +.bi-receipt-cutoff::before { content: "\f50e"; } +.bi-receipt::before { content: "\f50f"; } +.bi-reception-0::before { content: "\f510"; } +.bi-reception-1::before { content: "\f511"; } +.bi-reception-2::before { content: "\f512"; } +.bi-reception-3::before { content: "\f513"; } +.bi-reception-4::before { content: "\f514"; } +.bi-record-btn-fill::before { content: "\f515"; } +.bi-record-btn::before { content: "\f516"; } +.bi-record-circle-fill::before { content: "\f517"; } +.bi-record-circle::before { content: "\f518"; } +.bi-record-fill::before { content: "\f519"; } +.bi-record::before { content: "\f51a"; } +.bi-record2-fill::before { content: "\f51b"; } +.bi-record2::before { content: "\f51c"; } +.bi-reply-all-fill::before { content: "\f51d"; } +.bi-reply-all::before { content: "\f51e"; } +.bi-reply-fill::before { content: "\f51f"; } +.bi-reply::before { content: "\f520"; } +.bi-rss-fill::before { content: "\f521"; } +.bi-rss::before { content: "\f522"; } +.bi-rulers::before { content: "\f523"; } +.bi-save-fill::before { content: "\f524"; } +.bi-save::before { content: "\f525"; } +.bi-save2-fill::before { content: "\f526"; } +.bi-save2::before { content: "\f527"; } +.bi-scissors::before { content: "\f528"; } +.bi-screwdriver::before { content: "\f529"; } +.bi-search::before { content: "\f52a"; } +.bi-segmented-nav::before { content: "\f52b"; } +.bi-server::before { content: "\f52c"; } +.bi-share-fill::before { content: "\f52d"; } +.bi-share::before { content: "\f52e"; } +.bi-shield-check::before { content: "\f52f"; } +.bi-shield-exclamation::before { content: "\f530"; } +.bi-shield-fill-check::before { content: "\f531"; } +.bi-shield-fill-exclamation::before { content: "\f532"; } +.bi-shield-fill-minus::before { content: "\f533"; } +.bi-shield-fill-plus::before { content: "\f534"; } +.bi-shield-fill-x::before { content: "\f535"; } +.bi-shield-fill::before { content: "\f536"; } +.bi-shield-lock-fill::before { content: "\f537"; } +.bi-shield-lock::before { content: "\f538"; } +.bi-shield-minus::before { content: "\f539"; } +.bi-shield-plus::before { content: "\f53a"; } +.bi-shield-shaded::before { content: "\f53b"; } +.bi-shield-slash-fill::before { content: "\f53c"; } +.bi-shield-slash::before { content: "\f53d"; } +.bi-shield-x::before { content: "\f53e"; } +.bi-shield::before { content: "\f53f"; } +.bi-shift-fill::before { content: "\f540"; } +.bi-shift::before { content: "\f541"; } +.bi-shop-window::before { content: "\f542"; } +.bi-shop::before { content: "\f543"; } +.bi-shuffle::before { content: "\f544"; } +.bi-signpost-2-fill::before { content: "\f545"; } +.bi-signpost-2::before { content: "\f546"; } +.bi-signpost-fill::before { content: "\f547"; } +.bi-signpost-split-fill::before { content: "\f548"; } +.bi-signpost-split::before { content: "\f549"; } +.bi-signpost::before { content: "\f54a"; } +.bi-sim-fill::before { content: "\f54b"; } +.bi-sim::before { content: "\f54c"; } +.bi-skip-backward-btn-fill::before { content: "\f54d"; } +.bi-skip-backward-btn::before { content: "\f54e"; } +.bi-skip-backward-circle-fill::before { content: "\f54f"; } +.bi-skip-backward-circle::before { content: "\f550"; } +.bi-skip-backward-fill::before { content: "\f551"; } +.bi-skip-backward::before { content: "\f552"; } +.bi-skip-end-btn-fill::before { content: "\f553"; } +.bi-skip-end-btn::before { content: "\f554"; } +.bi-skip-end-circle-fill::before { content: "\f555"; } +.bi-skip-end-circle::before { content: "\f556"; } +.bi-skip-end-fill::before { content: "\f557"; } +.bi-skip-end::before { content: "\f558"; } +.bi-skip-forward-btn-fill::before { content: "\f559"; } +.bi-skip-forward-btn::before { content: "\f55a"; } +.bi-skip-forward-circle-fill::before { content: "\f55b"; } +.bi-skip-forward-circle::before { content: "\f55c"; } +.bi-skip-forward-fill::before { content: "\f55d"; } +.bi-skip-forward::before { content: "\f55e"; } +.bi-skip-start-btn-fill::before { content: "\f55f"; } +.bi-skip-start-btn::before { content: "\f560"; } +.bi-skip-start-circle-fill::before { content: "\f561"; } +.bi-skip-start-circle::before { content: "\f562"; } +.bi-skip-start-fill::before { content: "\f563"; } +.bi-skip-start::before { content: "\f564"; } +.bi-slack::before { content: "\f565"; } +.bi-slash-circle-fill::before { content: "\f566"; } +.bi-slash-circle::before { content: "\f567"; } +.bi-slash-square-fill::before { content: "\f568"; } +.bi-slash-square::before { content: "\f569"; } +.bi-slash::before { content: "\f56a"; } +.bi-sliders::before { content: "\f56b"; } +.bi-smartwatch::before { content: "\f56c"; } +.bi-snow::before { content: "\f56d"; } +.bi-snow2::before { content: "\f56e"; } +.bi-snow3::before { content: "\f56f"; } +.bi-sort-alpha-down-alt::before { content: "\f570"; } +.bi-sort-alpha-down::before { content: "\f571"; } +.bi-sort-alpha-up-alt::before { content: "\f572"; } +.bi-sort-alpha-up::before { content: "\f573"; } +.bi-sort-down-alt::before { content: "\f574"; } +.bi-sort-down::before { content: "\f575"; } +.bi-sort-numeric-down-alt::before { content: "\f576"; } +.bi-sort-numeric-down::before { content: "\f577"; } +.bi-sort-numeric-up-alt::before { content: "\f578"; } +.bi-sort-numeric-up::before { content: "\f579"; } +.bi-sort-up-alt::before { content: "\f57a"; } +.bi-sort-up::before { content: "\f57b"; } +.bi-soundwave::before { content: "\f57c"; } +.bi-speaker-fill::before { content: "\f57d"; } +.bi-speaker::before { content: "\f57e"; } +.bi-speedometer::before { content: "\f57f"; } +.bi-speedometer2::before { content: "\f580"; } +.bi-spellcheck::before { content: "\f581"; } +.bi-square-fill::before { content: "\f582"; } +.bi-square-half::before { content: "\f583"; } +.bi-square::before { content: "\f584"; } +.bi-stack::before { content: "\f585"; } +.bi-star-fill::before { content: "\f586"; } +.bi-star-half::before { content: "\f587"; } +.bi-star::before { content: "\f588"; } +.bi-stars::before { content: "\f589"; } +.bi-stickies-fill::before { content: "\f58a"; } +.bi-stickies::before { content: "\f58b"; } +.bi-sticky-fill::before { content: "\f58c"; } +.bi-sticky::before { content: "\f58d"; } +.bi-stop-btn-fill::before { content: "\f58e"; } +.bi-stop-btn::before { content: "\f58f"; } +.bi-stop-circle-fill::before { content: "\f590"; } +.bi-stop-circle::before { content: "\f591"; } +.bi-stop-fill::before { content: "\f592"; } +.bi-stop::before { content: "\f593"; } +.bi-stoplights-fill::before { content: "\f594"; } +.bi-stoplights::before { content: "\f595"; } +.bi-stopwatch-fill::before { content: "\f596"; } +.bi-stopwatch::before { content: "\f597"; } +.bi-subtract::before { content: "\f598"; } +.bi-suit-club-fill::before { content: "\f599"; } +.bi-suit-club::before { content: "\f59a"; } +.bi-suit-diamond-fill::before { content: "\f59b"; } +.bi-suit-diamond::before { content: "\f59c"; } +.bi-suit-heart-fill::before { content: "\f59d"; } +.bi-suit-heart::before { content: "\f59e"; } +.bi-suit-spade-fill::before { content: "\f59f"; } +.bi-suit-spade::before { content: "\f5a0"; } +.bi-sun-fill::before { content: "\f5a1"; } +.bi-sun::before { content: "\f5a2"; } +.bi-sunglasses::before { content: "\f5a3"; } +.bi-sunrise-fill::before { content: "\f5a4"; } +.bi-sunrise::before { content: "\f5a5"; } +.bi-sunset-fill::before { content: "\f5a6"; } +.bi-sunset::before { content: "\f5a7"; } +.bi-symmetry-horizontal::before { content: "\f5a8"; } +.bi-symmetry-vertical::before { content: "\f5a9"; } +.bi-table::before { content: "\f5aa"; } +.bi-tablet-fill::before { content: "\f5ab"; } +.bi-tablet-landscape-fill::before { content: "\f5ac"; } +.bi-tablet-landscape::before { content: "\f5ad"; } +.bi-tablet::before { content: "\f5ae"; } +.bi-tag-fill::before { content: "\f5af"; } +.bi-tag::before { content: "\f5b0"; } +.bi-tags-fill::before { content: "\f5b1"; } +.bi-tags::before { content: "\f5b2"; } +.bi-telegram::before { content: "\f5b3"; } +.bi-telephone-fill::before { content: "\f5b4"; } +.bi-telephone-forward-fill::before { content: "\f5b5"; } +.bi-telephone-forward::before { content: "\f5b6"; } +.bi-telephone-inbound-fill::before { content: "\f5b7"; } +.bi-telephone-inbound::before { content: "\f5b8"; } +.bi-telephone-minus-fill::before { content: "\f5b9"; } +.bi-telephone-minus::before { content: "\f5ba"; } +.bi-telephone-outbound-fill::before { content: "\f5bb"; } +.bi-telephone-outbound::before { content: "\f5bc"; } +.bi-telephone-plus-fill::before { content: "\f5bd"; } +.bi-telephone-plus::before { content: "\f5be"; } +.bi-telephone-x-fill::before { content: "\f5bf"; } +.bi-telephone-x::before { content: "\f5c0"; } +.bi-telephone::before { content: "\f5c1"; } +.bi-terminal-fill::before { content: "\f5c2"; } +.bi-terminal::before { content: "\f5c3"; } +.bi-text-center::before { content: "\f5c4"; } +.bi-text-indent-left::before { content: "\f5c5"; } +.bi-text-indent-right::before { content: "\f5c6"; } +.bi-text-left::before { content: "\f5c7"; } +.bi-text-paragraph::before { content: "\f5c8"; } +.bi-text-right::before { content: "\f5c9"; } +.bi-textarea-resize::before { content: "\f5ca"; } +.bi-textarea-t::before { content: "\f5cb"; } +.bi-textarea::before { content: "\f5cc"; } +.bi-thermometer-half::before { content: "\f5cd"; } +.bi-thermometer-high::before { content: "\f5ce"; } +.bi-thermometer-low::before { content: "\f5cf"; } +.bi-thermometer-snow::before { content: "\f5d0"; } +.bi-thermometer-sun::before { content: "\f5d1"; } +.bi-thermometer::before { content: "\f5d2"; } +.bi-three-dots-vertical::before { content: "\f5d3"; } +.bi-three-dots::before { content: "\f5d4"; } +.bi-toggle-off::before { content: "\f5d5"; } +.bi-toggle-on::before { content: "\f5d6"; } +.bi-toggle2-off::before { content: "\f5d7"; } +.bi-toggle2-on::before { content: "\f5d8"; } +.bi-toggles::before { content: "\f5d9"; } +.bi-toggles2::before { content: "\f5da"; } +.bi-tools::before { content: "\f5db"; } +.bi-tornado::before { content: "\f5dc"; } +.bi-trash-fill::before { content: "\f5dd"; } +.bi-trash::before { content: "\f5de"; } +.bi-trash2-fill::before { content: "\f5df"; } +.bi-trash2::before { content: "\f5e0"; } +.bi-tree-fill::before { content: "\f5e1"; } +.bi-tree::before { content: "\f5e2"; } +.bi-triangle-fill::before { content: "\f5e3"; } +.bi-triangle-half::before { content: "\f5e4"; } +.bi-triangle::before { content: "\f5e5"; } +.bi-trophy-fill::before { content: "\f5e6"; } +.bi-trophy::before { content: "\f5e7"; } +.bi-tropical-storm::before { content: "\f5e8"; } +.bi-truck-flatbed::before { content: "\f5e9"; } +.bi-truck::before { content: "\f5ea"; } +.bi-tsunami::before { content: "\f5eb"; } +.bi-tv-fill::before { content: "\f5ec"; } +.bi-tv::before { content: "\f5ed"; } +.bi-twitch::before { content: "\f5ee"; } +.bi-twitter::before { content: "\f5ef"; } +.bi-type-bold::before { content: "\f5f0"; } +.bi-type-h1::before { content: "\f5f1"; } +.bi-type-h2::before { content: "\f5f2"; } +.bi-type-h3::before { content: "\f5f3"; } +.bi-type-italic::before { content: "\f5f4"; } +.bi-type-strikethrough::before { content: "\f5f5"; } +.bi-type-underline::before { content: "\f5f6"; } +.bi-type::before { content: "\f5f7"; } +.bi-ui-checks-grid::before { content: "\f5f8"; } +.bi-ui-checks::before { content: "\f5f9"; } +.bi-ui-radios-grid::before { content: "\f5fa"; } +.bi-ui-radios::before { content: "\f5fb"; } +.bi-umbrella-fill::before { content: "\f5fc"; } +.bi-umbrella::before { content: "\f5fd"; } +.bi-union::before { content: "\f5fe"; } +.bi-unlock-fill::before { content: "\f5ff"; } +.bi-unlock::before { content: "\f600"; } +.bi-upc-scan::before { content: "\f601"; } +.bi-upc::before { content: "\f602"; } +.bi-upload::before { content: "\f603"; } +.bi-vector-pen::before { content: "\f604"; } +.bi-view-list::before { content: "\f605"; } +.bi-view-stacked::before { content: "\f606"; } +.bi-vinyl-fill::before { content: "\f607"; } +.bi-vinyl::before { content: "\f608"; } +.bi-voicemail::before { content: "\f609"; } +.bi-volume-down-fill::before { content: "\f60a"; } +.bi-volume-down::before { content: "\f60b"; } +.bi-volume-mute-fill::before { content: "\f60c"; } +.bi-volume-mute::before { content: "\f60d"; } +.bi-volume-off-fill::before { content: "\f60e"; } +.bi-volume-off::before { content: "\f60f"; } +.bi-volume-up-fill::before { content: "\f610"; } +.bi-volume-up::before { content: "\f611"; } +.bi-vr::before { content: "\f612"; } +.bi-wallet-fill::before { content: "\f613"; } +.bi-wallet::before { content: "\f614"; } +.bi-wallet2::before { content: "\f615"; } +.bi-watch::before { content: "\f616"; } +.bi-water::before { content: "\f617"; } +.bi-whatsapp::before { content: "\f618"; } +.bi-wifi-1::before { content: "\f619"; } +.bi-wifi-2::before { content: "\f61a"; } +.bi-wifi-off::before { content: "\f61b"; } +.bi-wifi::before { content: "\f61c"; } +.bi-wind::before { content: "\f61d"; } +.bi-window-dock::before { content: "\f61e"; } +.bi-window-sidebar::before { content: "\f61f"; } +.bi-window::before { content: "\f620"; } +.bi-wrench::before { content: "\f621"; } +.bi-x-circle-fill::before { content: "\f622"; } +.bi-x-circle::before { content: "\f623"; } +.bi-x-diamond-fill::before { content: "\f624"; } +.bi-x-diamond::before { content: "\f625"; } +.bi-x-octagon-fill::before { content: "\f626"; } +.bi-x-octagon::before { content: "\f627"; } +.bi-x-square-fill::before { content: "\f628"; } +.bi-x-square::before { content: "\f629"; } +.bi-x::before { content: "\f62a"; } +.bi-youtube::before { content: "\f62b"; } +.bi-zoom-in::before { content: "\f62c"; } +.bi-zoom-out::before { content: "\f62d"; } +.bi-bank::before { content: "\f62e"; } +.bi-bank2::before { content: "\f62f"; } +.bi-bell-slash-fill::before { content: "\f630"; } +.bi-bell-slash::before { content: "\f631"; } +.bi-cash-coin::before { content: "\f632"; } +.bi-check-lg::before { content: "\f633"; } +.bi-coin::before { content: "\f634"; } +.bi-currency-bitcoin::before { content: "\f635"; } +.bi-currency-dollar::before { content: "\f636"; } +.bi-currency-euro::before { content: "\f637"; } +.bi-currency-exchange::before { content: "\f638"; } +.bi-currency-pound::before { content: "\f639"; } +.bi-currency-yen::before { content: "\f63a"; } +.bi-dash-lg::before { content: "\f63b"; } +.bi-exclamation-lg::before { content: "\f63c"; } +.bi-file-earmark-pdf-fill::before { content: "\f63d"; } +.bi-file-earmark-pdf::before { content: "\f63e"; } +.bi-file-pdf-fill::before { content: "\f63f"; } +.bi-file-pdf::before { content: "\f640"; } +.bi-gender-ambiguous::before { content: "\f641"; } +.bi-gender-female::before { content: "\f642"; } +.bi-gender-male::before { content: "\f643"; } +.bi-gender-trans::before { content: "\f644"; } +.bi-headset-vr::before { content: "\f645"; } +.bi-info-lg::before { content: "\f646"; } +.bi-mastodon::before { content: "\f647"; } +.bi-messenger::before { content: "\f648"; } +.bi-piggy-bank-fill::before { content: "\f649"; } +.bi-piggy-bank::before { content: "\f64a"; } +.bi-pin-map-fill::before { content: "\f64b"; } +.bi-pin-map::before { content: "\f64c"; } +.bi-plus-lg::before { content: "\f64d"; } +.bi-question-lg::before { content: "\f64e"; } +.bi-recycle::before { content: "\f64f"; } +.bi-reddit::before { content: "\f650"; } +.bi-safe-fill::before { content: "\f651"; } +.bi-safe2-fill::before { content: "\f652"; } +.bi-safe2::before { content: "\f653"; } +.bi-sd-card-fill::before { content: "\f654"; } +.bi-sd-card::before { content: "\f655"; } +.bi-skype::before { content: "\f656"; } +.bi-slash-lg::before { content: "\f657"; } +.bi-translate::before { content: "\f658"; } +.bi-x-lg::before { content: "\f659"; } +.bi-safe::before { content: "\f65a"; } +.bi-apple::before { content: "\f65b"; } +.bi-microsoft::before { content: "\f65d"; } +.bi-windows::before { content: "\f65e"; } +.bi-behance::before { content: "\f65c"; } +.bi-dribbble::before { content: "\f65f"; } +.bi-line::before { content: "\f660"; } +.bi-medium::before { content: "\f661"; } +.bi-paypal::before { content: "\f662"; } +.bi-pinterest::before { content: "\f663"; } +.bi-signal::before { content: "\f664"; } +.bi-snapchat::before { content: "\f665"; } +.bi-spotify::before { content: "\f666"; } +.bi-stack-overflow::before { content: "\f667"; } +.bi-strava::before { content: "\f668"; } +.bi-wordpress::before { content: "\f669"; } +.bi-vimeo::before { content: "\f66a"; } +.bi-activity::before { content: "\f66b"; } +.bi-easel2-fill::before { content: "\f66c"; } +.bi-easel2::before { content: "\f66d"; } +.bi-easel3-fill::before { content: "\f66e"; } +.bi-easel3::before { content: "\f66f"; } +.bi-fan::before { content: "\f670"; } +.bi-fingerprint::before { content: "\f671"; } +.bi-graph-down-arrow::before { content: "\f672"; } +.bi-graph-up-arrow::before { content: "\f673"; } +.bi-hypnotize::before { content: "\f674"; } +.bi-magic::before { content: "\f675"; } +.bi-person-rolodex::before { content: "\f676"; } +.bi-person-video::before { content: "\f677"; } +.bi-person-video2::before { content: "\f678"; } +.bi-person-video3::before { content: "\f679"; } +.bi-person-workspace::before { content: "\f67a"; } +.bi-radioactive::before { content: "\f67b"; } +.bi-webcam-fill::before { content: "\f67c"; } +.bi-webcam::before { content: "\f67d"; } +.bi-yin-yang::before { content: "\f67e"; } +.bi-bandaid-fill::before { content: "\f680"; } +.bi-bandaid::before { content: "\f681"; } +.bi-bluetooth::before { content: "\f682"; } +.bi-body-text::before { content: "\f683"; } +.bi-boombox::before { content: "\f684"; } +.bi-boxes::before { content: "\f685"; } +.bi-dpad-fill::before { content: "\f686"; } +.bi-dpad::before { content: "\f687"; } +.bi-ear-fill::before { content: "\f688"; } +.bi-ear::before { content: "\f689"; } +.bi-envelope-check-1::before { content: "\f68a"; } +.bi-envelope-check-fill::before { content: "\f68b"; } +.bi-envelope-check::before { content: "\f68c"; } +.bi-envelope-dash-1::before { content: "\f68d"; } +.bi-envelope-dash-fill::before { content: "\f68e"; } +.bi-envelope-dash::before { content: "\f68f"; } +.bi-envelope-exclamation-1::before { content: "\f690"; } +.bi-envelope-exclamation-fill::before { content: "\f691"; } +.bi-envelope-exclamation::before { content: "\f692"; } +.bi-envelope-plus-fill::before { content: "\f693"; } +.bi-envelope-plus::before { content: "\f694"; } +.bi-envelope-slash-1::before { content: "\f695"; } +.bi-envelope-slash-fill::before { content: "\f696"; } +.bi-envelope-slash::before { content: "\f697"; } +.bi-envelope-x-1::before { content: "\f698"; } +.bi-envelope-x-fill::before { content: "\f699"; } +.bi-envelope-x::before { content: "\f69a"; } +.bi-explicit-fill::before { content: "\f69b"; } +.bi-explicit::before { content: "\f69c"; } +.bi-git::before { content: "\f69d"; } +.bi-infinity::before { content: "\f69e"; } +.bi-list-columns-reverse::before { content: "\f69f"; } +.bi-list-columns::before { content: "\f6a0"; } +.bi-meta::before { content: "\f6a1"; } +.bi-mortorboard-fill::before { content: "\f6a2"; } +.bi-mortorboard::before { content: "\f6a3"; } +.bi-nintendo-switch::before { content: "\f6a4"; } +.bi-pc-display-horizontal::before { content: "\f6a5"; } +.bi-pc-display::before { content: "\f6a6"; } +.bi-pc-horizontal::before { content: "\f6a7"; } +.bi-pc::before { content: "\f6a8"; } +.bi-playstation::before { content: "\f6a9"; } +.bi-plus-slash-minus::before { content: "\f6aa"; } +.bi-projector-fill::before { content: "\f6ab"; } +.bi-projector::before { content: "\f6ac"; } +.bi-qr-code-scan::before { content: "\f6ad"; } +.bi-qr-code::before { content: "\f6ae"; } +.bi-quora::before { content: "\f6af"; } +.bi-quote::before { content: "\f6b0"; } +.bi-robot::before { content: "\f6b1"; } +.bi-send-check-fill::before { content: "\f6b2"; } +.bi-send-check::before { content: "\f6b3"; } +.bi-send-dash-fill::before { content: "\f6b4"; } +.bi-send-dash::before { content: "\f6b5"; } +.bi-send-exclamation-1::before { content: "\f6b6"; } +.bi-send-exclamation-fill::before { content: "\f6b7"; } +.bi-send-exclamation::before { content: "\f6b8"; } +.bi-send-fill::before { content: "\f6b9"; } +.bi-send-plus-fill::before { content: "\f6ba"; } +.bi-send-plus::before { content: "\f6bb"; } +.bi-send-slash-fill::before { content: "\f6bc"; } +.bi-send-slash::before { content: "\f6bd"; } +.bi-send-x-fill::before { content: "\f6be"; } +.bi-send-x::before { content: "\f6bf"; } +.bi-send::before { content: "\f6c0"; } +.bi-steam::before { content: "\f6c1"; } +.bi-terminal-dash-1::before { content: "\f6c2"; } +.bi-terminal-dash::before { content: "\f6c3"; } +.bi-terminal-plus::before { content: "\f6c4"; } +.bi-terminal-split::before { content: "\f6c5"; } +.bi-ticket-detailed-fill::before { content: "\f6c6"; } +.bi-ticket-detailed::before { content: "\f6c7"; } +.bi-ticket-fill::before { content: "\f6c8"; } +.bi-ticket-perforated-fill::before { content: "\f6c9"; } +.bi-ticket-perforated::before { content: "\f6ca"; } +.bi-ticket::before { content: "\f6cb"; } +.bi-tiktok::before { content: "\f6cc"; } +.bi-window-dash::before { content: "\f6cd"; } +.bi-window-desktop::before { content: "\f6ce"; } +.bi-window-fullscreen::before { content: "\f6cf"; } +.bi-window-plus::before { content: "\f6d0"; } +.bi-window-split::before { content: "\f6d1"; } +.bi-window-stack::before { content: "\f6d2"; } +.bi-window-x::before { content: "\f6d3"; } +.bi-xbox::before { content: "\f6d4"; } +.bi-ethernet::before { content: "\f6d5"; } +.bi-hdmi-fill::before { content: "\f6d6"; } +.bi-hdmi::before { content: "\f6d7"; } +.bi-usb-c-fill::before { content: "\f6d8"; } +.bi-usb-c::before { content: "\f6d9"; } +.bi-usb-fill::before { content: "\f6da"; } +.bi-usb-plug-fill::before { content: "\f6db"; } +.bi-usb-plug::before { content: "\f6dc"; } +.bi-usb-symbol::before { content: "\f6dd"; } +.bi-usb::before { content: "\f6de"; } +.bi-boombox-fill::before { content: "\f6df"; } +.bi-displayport-1::before { content: "\f6e0"; } +.bi-displayport::before { content: "\f6e1"; } +.bi-gpu-card::before { content: "\f6e2"; } +.bi-memory::before { content: "\f6e3"; } +.bi-modem-fill::before { content: "\f6e4"; } +.bi-modem::before { content: "\f6e5"; } +.bi-motherboard-fill::before { content: "\f6e6"; } +.bi-motherboard::before { content: "\f6e7"; } +.bi-optical-audio-fill::before { content: "\f6e8"; } +.bi-optical-audio::before { content: "\f6e9"; } +.bi-pci-card::before { content: "\f6ea"; } +.bi-router-fill::before { content: "\f6eb"; } +.bi-router::before { content: "\f6ec"; } +.bi-ssd-fill::before { content: "\f6ed"; } +.bi-ssd::before { content: "\f6ee"; } +.bi-thunderbolt-fill::before { content: "\f6ef"; } +.bi-thunderbolt::before { content: "\f6f0"; } +.bi-usb-drive-fill::before { content: "\f6f1"; } +.bi-usb-drive::before { content: "\f6f2"; } +.bi-usb-micro-fill::before { content: "\f6f3"; } +.bi-usb-micro::before { content: "\f6f4"; } +.bi-usb-mini-fill::before { content: "\f6f5"; } +.bi-usb-mini::before { content: "\f6f6"; } +.bi-cloud-haze2::before { content: "\f6f7"; } +.bi-device-hdd-fill::before { content: "\f6f8"; } +.bi-device-hdd::before { content: "\f6f9"; } +.bi-device-ssd-fill::before { content: "\f6fa"; } +.bi-device-ssd::before { content: "\f6fb"; } +.bi-displayport-fill::before { content: "\f6fc"; } +.bi-mortarboard-fill::before { content: "\f6fd"; } +.bi-mortarboard::before { content: "\f6fe"; } +.bi-terminal-x::before { content: "\f6ff"; } diff --git a/Streamdeck/com.mirabox.streamingle.sdPlugin/dist-dev/com.mirabox.streamingle.sdPlugin/propertyInspector/utils/bootstrap.min.css b/Streamdeck/com.mirabox.streamingle.sdPlugin/dist-dev/com.mirabox.streamingle.sdPlugin/propertyInspector/utils/bootstrap.min.css new file mode 100644 index 00000000..1472dec0 --- /dev/null +++ b/Streamdeck/com.mirabox.streamingle.sdPlugin/dist-dev/com.mirabox.streamingle.sdPlugin/propertyInspector/utils/bootstrap.min.css @@ -0,0 +1,7 @@ +@charset "UTF-8";/*! + * Bootstrap v5.1.3 (https://getbootstrap.com/) + * Copyright 2011-2021 The Bootstrap Authors + * Copyright 2011-2021 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + */:root{--bs-blue:#0d6efd;--bs-indigo:#6610f2;--bs-purple:#6f42c1;--bs-pink:#d63384;--bs-red:#dc3545;--bs-orange:#fd7e14;--bs-yellow:#ffc107;--bs-green:#198754;--bs-teal:#20c997;--bs-cyan:#0dcaf0;--bs-white:#fff;--bs-gray:#6c757d;--bs-gray-dark:#343a40;--bs-gray-100:#f8f9fa;--bs-gray-200:#e9ecef;--bs-gray-300:#dee2e6;--bs-gray-400:#ced4da;--bs-gray-500:#adb5bd;--bs-gray-600:#6c757d;--bs-gray-700:#495057;--bs-gray-800:#343a40;--bs-gray-900:#212529;--bs-primary:#0d6efd;--bs-secondary:#6c757d;--bs-success:#198754;--bs-info:#0dcaf0;--bs-warning:#ffc107;--bs-danger:#dc3545;--bs-light:#f8f9fa;--bs-dark:#212529;--bs-primary-rgb:13,110,253;--bs-secondary-rgb:108,117,125;--bs-success-rgb:25,135,84;--bs-info-rgb:13,202,240;--bs-warning-rgb:255,193,7;--bs-danger-rgb:220,53,69;--bs-light-rgb:248,249,250;--bs-dark-rgb:33,37,41;--bs-white-rgb:255,255,255;--bs-black-rgb:0,0,0;--bs-body-color-rgb:33,37,41;--bs-body-bg-rgb:255,255,255;--bs-font-sans-serif:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans","Liberation Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--bs-font-monospace:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--bs-gradient:linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));--bs-body-font-family:var(--bs-font-sans-serif);--bs-body-font-size:1rem;--bs-body-font-weight:400;--bs-body-line-height:1.5;--bs-body-color:#212529;--bs-body-bg:#fff}*,::after,::before{box-sizing:border-box}@media (prefers-reduced-motion:no-preference){:root{scroll-behavior:smooth}}body{margin:0;font-family:var(--bs-body-font-family);font-size:var(--bs-body-font-size);font-weight:var(--bs-body-font-weight);line-height:var(--bs-body-line-height);color:var(--bs-body-color);text-align:var(--bs-body-text-align);background-color:var(--bs-body-bg);-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}hr{margin:1rem 0;color:inherit;background-color:currentColor;border:0;opacity:.25}hr:not([size]){height:1px}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem;font-weight:500;line-height:1.2}.h1,h1{font-size:calc(1.375rem + 1.5vw)}@media (min-width:1200px){.h1,h1{font-size:2.5rem}}.h2,h2{font-size:calc(1.325rem + .9vw)}@media (min-width:1200px){.h2,h2{font-size:2rem}}.h3,h3{font-size:calc(1.3rem + .6vw)}@media (min-width:1200px){.h3,h3{font-size:1.75rem}}.h4,h4{font-size:calc(1.275rem + .3vw)}@media (min-width:1200px){.h4,h4{font-size:1.5rem}}.h5,h5{font-size:1.25rem}.h6,h6{font-size:1rem}p{margin-top:0;margin-bottom:1rem}abbr[data-bs-original-title],abbr[title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}ol,ul{padding-left:2rem}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}.small,small{font-size:.875em}.mark,mark{padding:.2em;background-color:#fcf8e3}sub,sup{position:relative;font-size:.75em;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#0d6efd;text-decoration:underline}a:hover{color:#0a58ca}a:not([href]):not([class]),a:not([href]):not([class]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:var(--bs-font-monospace);font-size:1em;direction:ltr;unicode-bidi:bidi-override}pre{display:block;margin-top:0;margin-bottom:1rem;overflow:auto;font-size:.875em}pre code{font-size:inherit;color:inherit;word-break:normal}code{font-size:.875em;color:#d63384;word-wrap:break-word}a>code{color:inherit}kbd{padding:.2rem .4rem;font-size:.875em;color:#fff;background-color:#212529;border-radius:.2rem}kbd kbd{padding:0;font-size:1em;font-weight:700}figure{margin:0 0 1rem}img,svg{vertical-align:middle}table{caption-side:bottom;border-collapse:collapse}caption{padding-top:.5rem;padding-bottom:.5rem;color:#6c757d;text-align:left}th{text-align:inherit;text-align:-webkit-match-parent}tbody,td,tfoot,th,thead,tr{border-color:inherit;border-style:solid;border-width:0}label{display:inline-block}button{border-radius:0}button:focus:not(:focus-visible){outline:0}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}select:disabled{opacity:1}[list]::-webkit-calendar-picker-indicator{display:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}::-moz-focus-inner{padding:0;border-style:none}textarea{resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{float:left;width:100%;padding:0;margin-bottom:.5rem;font-size:calc(1.275rem + .3vw);line-height:inherit}@media (min-width:1200px){legend{font-size:1.5rem}}legend+*{clear:left}::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-fields-wrapper,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-text,::-webkit-datetime-edit-year-field{padding:0}::-webkit-inner-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:textfield}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-color-swatch-wrapper{padding:0}::-webkit-file-upload-button{font:inherit}::file-selector-button{font:inherit}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}iframe{border:0}summary{display:list-item;cursor:pointer}progress{vertical-align:baseline}[hidden]{display:none!important}.lead{font-size:1.25rem;font-weight:300}.display-1{font-size:calc(1.625rem + 4.5vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-1{font-size:5rem}}.display-2{font-size:calc(1.575rem + 3.9vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-2{font-size:4.5rem}}.display-3{font-size:calc(1.525rem + 3.3vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-3{font-size:4rem}}.display-4{font-size:calc(1.475rem + 2.7vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-4{font-size:3.5rem}}.display-5{font-size:calc(1.425rem + 2.1vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-5{font-size:3rem}}.display-6{font-size:calc(1.375rem + 1.5vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-6{font-size:2.5rem}}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none}.list-inline-item{display:inline-block}.list-inline-item:not(:last-child){margin-right:.5rem}.initialism{font-size:.875em;text-transform:uppercase}.blockquote{margin-bottom:1rem;font-size:1.25rem}.blockquote>:last-child{margin-bottom:0}.blockquote-footer{margin-top:-1rem;margin-bottom:1rem;font-size:.875em;color:#6c757d}.blockquote-footer::before{content:"— "}.img-fluid{max-width:100%;height:auto}.img-thumbnail{padding:.25rem;background-color:#fff;border:1px solid #dee2e6;border-radius:.25rem;max-width:100%;height:auto}.figure{display:inline-block}.figure-img{margin-bottom:.5rem;line-height:1}.figure-caption{font-size:.875em;color:#6c757d}.container,.container-fluid,.container-lg,.container-md,.container-sm,.container-xl,.container-xxl{width:100%;padding-right:var(--bs-gutter-x,.75rem);padding-left:var(--bs-gutter-x,.75rem);margin-right:auto;margin-left:auto}@media (min-width:576px){.container,.container-sm{max-width:540px}}@media (min-width:768px){.container,.container-md,.container-sm{max-width:720px}}@media (min-width:992px){.container,.container-lg,.container-md,.container-sm{max-width:960px}}@media (min-width:1200px){.container,.container-lg,.container-md,.container-sm,.container-xl{max-width:1140px}}@media (min-width:1400px){.container,.container-lg,.container-md,.container-sm,.container-xl,.container-xxl{max-width:1320px}}.row{--bs-gutter-x:1.5rem;--bs-gutter-y:0;display:flex;flex-wrap:wrap;margin-top:calc(-1 * var(--bs-gutter-y));margin-right:calc(-.5 * var(--bs-gutter-x));margin-left:calc(-.5 * var(--bs-gutter-x))}.row>*{flex-shrink:0;width:100%;max-width:100%;padding-right:calc(var(--bs-gutter-x) * .5);padding-left:calc(var(--bs-gutter-x) * .5);margin-top:var(--bs-gutter-y)}.col{flex:1 0 0%}.row-cols-auto>*{flex:0 0 auto;width:auto}.row-cols-1>*{flex:0 0 auto;width:100%}.row-cols-2>*{flex:0 0 auto;width:50%}.row-cols-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-4>*{flex:0 0 auto;width:25%}.row-cols-5>*{flex:0 0 auto;width:20%}.row-cols-6>*{flex:0 0 auto;width:16.6666666667%}.col-auto{flex:0 0 auto;width:auto}.col-1{flex:0 0 auto;width:8.33333333%}.col-2{flex:0 0 auto;width:16.66666667%}.col-3{flex:0 0 auto;width:25%}.col-4{flex:0 0 auto;width:33.33333333%}.col-5{flex:0 0 auto;width:41.66666667%}.col-6{flex:0 0 auto;width:50%}.col-7{flex:0 0 auto;width:58.33333333%}.col-8{flex:0 0 auto;width:66.66666667%}.col-9{flex:0 0 auto;width:75%}.col-10{flex:0 0 auto;width:83.33333333%}.col-11{flex:0 0 auto;width:91.66666667%}.col-12{flex:0 0 auto;width:100%}.offset-1{margin-left:8.33333333%}.offset-2{margin-left:16.66666667%}.offset-3{margin-left:25%}.offset-4{margin-left:33.33333333%}.offset-5{margin-left:41.66666667%}.offset-6{margin-left:50%}.offset-7{margin-left:58.33333333%}.offset-8{margin-left:66.66666667%}.offset-9{margin-left:75%}.offset-10{margin-left:83.33333333%}.offset-11{margin-left:91.66666667%}.g-0,.gx-0{--bs-gutter-x:0}.g-0,.gy-0{--bs-gutter-y:0}.g-1,.gx-1{--bs-gutter-x:0.25rem}.g-1,.gy-1{--bs-gutter-y:0.25rem}.g-2,.gx-2{--bs-gutter-x:0.5rem}.g-2,.gy-2{--bs-gutter-y:0.5rem}.g-3,.gx-3{--bs-gutter-x:1rem}.g-3,.gy-3{--bs-gutter-y:1rem}.g-4,.gx-4{--bs-gutter-x:1.5rem}.g-4,.gy-4{--bs-gutter-y:1.5rem}.g-5,.gx-5{--bs-gutter-x:3rem}.g-5,.gy-5{--bs-gutter-y:3rem}@media (min-width:576px){.col-sm{flex:1 0 0%}.row-cols-sm-auto>*{flex:0 0 auto;width:auto}.row-cols-sm-1>*{flex:0 0 auto;width:100%}.row-cols-sm-2>*{flex:0 0 auto;width:50%}.row-cols-sm-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-sm-4>*{flex:0 0 auto;width:25%}.row-cols-sm-5>*{flex:0 0 auto;width:20%}.row-cols-sm-6>*{flex:0 0 auto;width:16.6666666667%}.col-sm-auto{flex:0 0 auto;width:auto}.col-sm-1{flex:0 0 auto;width:8.33333333%}.col-sm-2{flex:0 0 auto;width:16.66666667%}.col-sm-3{flex:0 0 auto;width:25%}.col-sm-4{flex:0 0 auto;width:33.33333333%}.col-sm-5{flex:0 0 auto;width:41.66666667%}.col-sm-6{flex:0 0 auto;width:50%}.col-sm-7{flex:0 0 auto;width:58.33333333%}.col-sm-8{flex:0 0 auto;width:66.66666667%}.col-sm-9{flex:0 0 auto;width:75%}.col-sm-10{flex:0 0 auto;width:83.33333333%}.col-sm-11{flex:0 0 auto;width:91.66666667%}.col-sm-12{flex:0 0 auto;width:100%}.offset-sm-0{margin-left:0}.offset-sm-1{margin-left:8.33333333%}.offset-sm-2{margin-left:16.66666667%}.offset-sm-3{margin-left:25%}.offset-sm-4{margin-left:33.33333333%}.offset-sm-5{margin-left:41.66666667%}.offset-sm-6{margin-left:50%}.offset-sm-7{margin-left:58.33333333%}.offset-sm-8{margin-left:66.66666667%}.offset-sm-9{margin-left:75%}.offset-sm-10{margin-left:83.33333333%}.offset-sm-11{margin-left:91.66666667%}.g-sm-0,.gx-sm-0{--bs-gutter-x:0}.g-sm-0,.gy-sm-0{--bs-gutter-y:0}.g-sm-1,.gx-sm-1{--bs-gutter-x:0.25rem}.g-sm-1,.gy-sm-1{--bs-gutter-y:0.25rem}.g-sm-2,.gx-sm-2{--bs-gutter-x:0.5rem}.g-sm-2,.gy-sm-2{--bs-gutter-y:0.5rem}.g-sm-3,.gx-sm-3{--bs-gutter-x:1rem}.g-sm-3,.gy-sm-3{--bs-gutter-y:1rem}.g-sm-4,.gx-sm-4{--bs-gutter-x:1.5rem}.g-sm-4,.gy-sm-4{--bs-gutter-y:1.5rem}.g-sm-5,.gx-sm-5{--bs-gutter-x:3rem}.g-sm-5,.gy-sm-5{--bs-gutter-y:3rem}}@media (min-width:768px){.col-md{flex:1 0 0%}.row-cols-md-auto>*{flex:0 0 auto;width:auto}.row-cols-md-1>*{flex:0 0 auto;width:100%}.row-cols-md-2>*{flex:0 0 auto;width:50%}.row-cols-md-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-md-4>*{flex:0 0 auto;width:25%}.row-cols-md-5>*{flex:0 0 auto;width:20%}.row-cols-md-6>*{flex:0 0 auto;width:16.6666666667%}.col-md-auto{flex:0 0 auto;width:auto}.col-md-1{flex:0 0 auto;width:8.33333333%}.col-md-2{flex:0 0 auto;width:16.66666667%}.col-md-3{flex:0 0 auto;width:25%}.col-md-4{flex:0 0 auto;width:33.33333333%}.col-md-5{flex:0 0 auto;width:41.66666667%}.col-md-6{flex:0 0 auto;width:50%}.col-md-7{flex:0 0 auto;width:58.33333333%}.col-md-8{flex:0 0 auto;width:66.66666667%}.col-md-9{flex:0 0 auto;width:75%}.col-md-10{flex:0 0 auto;width:83.33333333%}.col-md-11{flex:0 0 auto;width:91.66666667%}.col-md-12{flex:0 0 auto;width:100%}.offset-md-0{margin-left:0}.offset-md-1{margin-left:8.33333333%}.offset-md-2{margin-left:16.66666667%}.offset-md-3{margin-left:25%}.offset-md-4{margin-left:33.33333333%}.offset-md-5{margin-left:41.66666667%}.offset-md-6{margin-left:50%}.offset-md-7{margin-left:58.33333333%}.offset-md-8{margin-left:66.66666667%}.offset-md-9{margin-left:75%}.offset-md-10{margin-left:83.33333333%}.offset-md-11{margin-left:91.66666667%}.g-md-0,.gx-md-0{--bs-gutter-x:0}.g-md-0,.gy-md-0{--bs-gutter-y:0}.g-md-1,.gx-md-1{--bs-gutter-x:0.25rem}.g-md-1,.gy-md-1{--bs-gutter-y:0.25rem}.g-md-2,.gx-md-2{--bs-gutter-x:0.5rem}.g-md-2,.gy-md-2{--bs-gutter-y:0.5rem}.g-md-3,.gx-md-3{--bs-gutter-x:1rem}.g-md-3,.gy-md-3{--bs-gutter-y:1rem}.g-md-4,.gx-md-4{--bs-gutter-x:1.5rem}.g-md-4,.gy-md-4{--bs-gutter-y:1.5rem}.g-md-5,.gx-md-5{--bs-gutter-x:3rem}.g-md-5,.gy-md-5{--bs-gutter-y:3rem}}@media (min-width:992px){.col-lg{flex:1 0 0%}.row-cols-lg-auto>*{flex:0 0 auto;width:auto}.row-cols-lg-1>*{flex:0 0 auto;width:100%}.row-cols-lg-2>*{flex:0 0 auto;width:50%}.row-cols-lg-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-lg-4>*{flex:0 0 auto;width:25%}.row-cols-lg-5>*{flex:0 0 auto;width:20%}.row-cols-lg-6>*{flex:0 0 auto;width:16.6666666667%}.col-lg-auto{flex:0 0 auto;width:auto}.col-lg-1{flex:0 0 auto;width:8.33333333%}.col-lg-2{flex:0 0 auto;width:16.66666667%}.col-lg-3{flex:0 0 auto;width:25%}.col-lg-4{flex:0 0 auto;width:33.33333333%}.col-lg-5{flex:0 0 auto;width:41.66666667%}.col-lg-6{flex:0 0 auto;width:50%}.col-lg-7{flex:0 0 auto;width:58.33333333%}.col-lg-8{flex:0 0 auto;width:66.66666667%}.col-lg-9{flex:0 0 auto;width:75%}.col-lg-10{flex:0 0 auto;width:83.33333333%}.col-lg-11{flex:0 0 auto;width:91.66666667%}.col-lg-12{flex:0 0 auto;width:100%}.offset-lg-0{margin-left:0}.offset-lg-1{margin-left:8.33333333%}.offset-lg-2{margin-left:16.66666667%}.offset-lg-3{margin-left:25%}.offset-lg-4{margin-left:33.33333333%}.offset-lg-5{margin-left:41.66666667%}.offset-lg-6{margin-left:50%}.offset-lg-7{margin-left:58.33333333%}.offset-lg-8{margin-left:66.66666667%}.offset-lg-9{margin-left:75%}.offset-lg-10{margin-left:83.33333333%}.offset-lg-11{margin-left:91.66666667%}.g-lg-0,.gx-lg-0{--bs-gutter-x:0}.g-lg-0,.gy-lg-0{--bs-gutter-y:0}.g-lg-1,.gx-lg-1{--bs-gutter-x:0.25rem}.g-lg-1,.gy-lg-1{--bs-gutter-y:0.25rem}.g-lg-2,.gx-lg-2{--bs-gutter-x:0.5rem}.g-lg-2,.gy-lg-2{--bs-gutter-y:0.5rem}.g-lg-3,.gx-lg-3{--bs-gutter-x:1rem}.g-lg-3,.gy-lg-3{--bs-gutter-y:1rem}.g-lg-4,.gx-lg-4{--bs-gutter-x:1.5rem}.g-lg-4,.gy-lg-4{--bs-gutter-y:1.5rem}.g-lg-5,.gx-lg-5{--bs-gutter-x:3rem}.g-lg-5,.gy-lg-5{--bs-gutter-y:3rem}}@media (min-width:1200px){.col-xl{flex:1 0 0%}.row-cols-xl-auto>*{flex:0 0 auto;width:auto}.row-cols-xl-1>*{flex:0 0 auto;width:100%}.row-cols-xl-2>*{flex:0 0 auto;width:50%}.row-cols-xl-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-xl-4>*{flex:0 0 auto;width:25%}.row-cols-xl-5>*{flex:0 0 auto;width:20%}.row-cols-xl-6>*{flex:0 0 auto;width:16.6666666667%}.col-xl-auto{flex:0 0 auto;width:auto}.col-xl-1{flex:0 0 auto;width:8.33333333%}.col-xl-2{flex:0 0 auto;width:16.66666667%}.col-xl-3{flex:0 0 auto;width:25%}.col-xl-4{flex:0 0 auto;width:33.33333333%}.col-xl-5{flex:0 0 auto;width:41.66666667%}.col-xl-6{flex:0 0 auto;width:50%}.col-xl-7{flex:0 0 auto;width:58.33333333%}.col-xl-8{flex:0 0 auto;width:66.66666667%}.col-xl-9{flex:0 0 auto;width:75%}.col-xl-10{flex:0 0 auto;width:83.33333333%}.col-xl-11{flex:0 0 auto;width:91.66666667%}.col-xl-12{flex:0 0 auto;width:100%}.offset-xl-0{margin-left:0}.offset-xl-1{margin-left:8.33333333%}.offset-xl-2{margin-left:16.66666667%}.offset-xl-3{margin-left:25%}.offset-xl-4{margin-left:33.33333333%}.offset-xl-5{margin-left:41.66666667%}.offset-xl-6{margin-left:50%}.offset-xl-7{margin-left:58.33333333%}.offset-xl-8{margin-left:66.66666667%}.offset-xl-9{margin-left:75%}.offset-xl-10{margin-left:83.33333333%}.offset-xl-11{margin-left:91.66666667%}.g-xl-0,.gx-xl-0{--bs-gutter-x:0}.g-xl-0,.gy-xl-0{--bs-gutter-y:0}.g-xl-1,.gx-xl-1{--bs-gutter-x:0.25rem}.g-xl-1,.gy-xl-1{--bs-gutter-y:0.25rem}.g-xl-2,.gx-xl-2{--bs-gutter-x:0.5rem}.g-xl-2,.gy-xl-2{--bs-gutter-y:0.5rem}.g-xl-3,.gx-xl-3{--bs-gutter-x:1rem}.g-xl-3,.gy-xl-3{--bs-gutter-y:1rem}.g-xl-4,.gx-xl-4{--bs-gutter-x:1.5rem}.g-xl-4,.gy-xl-4{--bs-gutter-y:1.5rem}.g-xl-5,.gx-xl-5{--bs-gutter-x:3rem}.g-xl-5,.gy-xl-5{--bs-gutter-y:3rem}}@media (min-width:1400px){.col-xxl{flex:1 0 0%}.row-cols-xxl-auto>*{flex:0 0 auto;width:auto}.row-cols-xxl-1>*{flex:0 0 auto;width:100%}.row-cols-xxl-2>*{flex:0 0 auto;width:50%}.row-cols-xxl-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-xxl-4>*{flex:0 0 auto;width:25%}.row-cols-xxl-5>*{flex:0 0 auto;width:20%}.row-cols-xxl-6>*{flex:0 0 auto;width:16.6666666667%}.col-xxl-auto{flex:0 0 auto;width:auto}.col-xxl-1{flex:0 0 auto;width:8.33333333%}.col-xxl-2{flex:0 0 auto;width:16.66666667%}.col-xxl-3{flex:0 0 auto;width:25%}.col-xxl-4{flex:0 0 auto;width:33.33333333%}.col-xxl-5{flex:0 0 auto;width:41.66666667%}.col-xxl-6{flex:0 0 auto;width:50%}.col-xxl-7{flex:0 0 auto;width:58.33333333%}.col-xxl-8{flex:0 0 auto;width:66.66666667%}.col-xxl-9{flex:0 0 auto;width:75%}.col-xxl-10{flex:0 0 auto;width:83.33333333%}.col-xxl-11{flex:0 0 auto;width:91.66666667%}.col-xxl-12{flex:0 0 auto;width:100%}.offset-xxl-0{margin-left:0}.offset-xxl-1{margin-left:8.33333333%}.offset-xxl-2{margin-left:16.66666667%}.offset-xxl-3{margin-left:25%}.offset-xxl-4{margin-left:33.33333333%}.offset-xxl-5{margin-left:41.66666667%}.offset-xxl-6{margin-left:50%}.offset-xxl-7{margin-left:58.33333333%}.offset-xxl-8{margin-left:66.66666667%}.offset-xxl-9{margin-left:75%}.offset-xxl-10{margin-left:83.33333333%}.offset-xxl-11{margin-left:91.66666667%}.g-xxl-0,.gx-xxl-0{--bs-gutter-x:0}.g-xxl-0,.gy-xxl-0{--bs-gutter-y:0}.g-xxl-1,.gx-xxl-1{--bs-gutter-x:0.25rem}.g-xxl-1,.gy-xxl-1{--bs-gutter-y:0.25rem}.g-xxl-2,.gx-xxl-2{--bs-gutter-x:0.5rem}.g-xxl-2,.gy-xxl-2{--bs-gutter-y:0.5rem}.g-xxl-3,.gx-xxl-3{--bs-gutter-x:1rem}.g-xxl-3,.gy-xxl-3{--bs-gutter-y:1rem}.g-xxl-4,.gx-xxl-4{--bs-gutter-x:1.5rem}.g-xxl-4,.gy-xxl-4{--bs-gutter-y:1.5rem}.g-xxl-5,.gx-xxl-5{--bs-gutter-x:3rem}.g-xxl-5,.gy-xxl-5{--bs-gutter-y:3rem}}.table{--bs-table-bg:transparent;--bs-table-accent-bg:transparent;--bs-table-striped-color:#212529;--bs-table-striped-bg:rgba(0, 0, 0, 0.05);--bs-table-active-color:#212529;--bs-table-active-bg:rgba(0, 0, 0, 0.1);--bs-table-hover-color:#212529;--bs-table-hover-bg:rgba(0, 0, 0, 0.075);width:100%;margin-bottom:1rem;color:#212529;vertical-align:top;border-color:#dee2e6}.table>:not(caption)>*>*{padding:.5rem .5rem;background-color:var(--bs-table-bg);border-bottom-width:1px;box-shadow:inset 0 0 0 9999px var(--bs-table-accent-bg)}.table>tbody{vertical-align:inherit}.table>thead{vertical-align:bottom}.table>:not(:first-child){border-top:2px solid currentColor}.caption-top{caption-side:top}.table-sm>:not(caption)>*>*{padding:.25rem .25rem}.table-bordered>:not(caption)>*{border-width:1px 0}.table-bordered>:not(caption)>*>*{border-width:0 1px}.table-borderless>:not(caption)>*>*{border-bottom-width:0}.table-borderless>:not(:first-child){border-top-width:0}.table-striped>tbody>tr:nth-of-type(odd)>*{--bs-table-accent-bg:var(--bs-table-striped-bg);color:var(--bs-table-striped-color)}.table-active{--bs-table-accent-bg:var(--bs-table-active-bg);color:var(--bs-table-active-color)}.table-hover>tbody>tr:hover>*{--bs-table-accent-bg:var(--bs-table-hover-bg);color:var(--bs-table-hover-color)}.table-primary{--bs-table-bg:#cfe2ff;--bs-table-striped-bg:#c5d7f2;--bs-table-striped-color:#000;--bs-table-active-bg:#bacbe6;--bs-table-active-color:#000;--bs-table-hover-bg:#bfd1ec;--bs-table-hover-color:#000;color:#000;border-color:#bacbe6}.table-secondary{--bs-table-bg:#e2e3e5;--bs-table-striped-bg:#d7d8da;--bs-table-striped-color:#000;--bs-table-active-bg:#cbccce;--bs-table-active-color:#000;--bs-table-hover-bg:#d1d2d4;--bs-table-hover-color:#000;color:#000;border-color:#cbccce}.table-success{--bs-table-bg:#d1e7dd;--bs-table-striped-bg:#c7dbd2;--bs-table-striped-color:#000;--bs-table-active-bg:#bcd0c7;--bs-table-active-color:#000;--bs-table-hover-bg:#c1d6cc;--bs-table-hover-color:#000;color:#000;border-color:#bcd0c7}.table-info{--bs-table-bg:#cff4fc;--bs-table-striped-bg:#c5e8ef;--bs-table-striped-color:#000;--bs-table-active-bg:#badce3;--bs-table-active-color:#000;--bs-table-hover-bg:#bfe2e9;--bs-table-hover-color:#000;color:#000;border-color:#badce3}.table-warning{--bs-table-bg:#fff3cd;--bs-table-striped-bg:#f2e7c3;--bs-table-striped-color:#000;--bs-table-active-bg:#e6dbb9;--bs-table-active-color:#000;--bs-table-hover-bg:#ece1be;--bs-table-hover-color:#000;color:#000;border-color:#e6dbb9}.table-danger{--bs-table-bg:#f8d7da;--bs-table-striped-bg:#eccccf;--bs-table-striped-color:#000;--bs-table-active-bg:#dfc2c4;--bs-table-active-color:#000;--bs-table-hover-bg:#e5c7ca;--bs-table-hover-color:#000;color:#000;border-color:#dfc2c4}.table-light{--bs-table-bg:#f8f9fa;--bs-table-striped-bg:#ecedee;--bs-table-striped-color:#000;--bs-table-active-bg:#dfe0e1;--bs-table-active-color:#000;--bs-table-hover-bg:#e5e6e7;--bs-table-hover-color:#000;color:#000;border-color:#dfe0e1}.table-dark{--bs-table-bg:#212529;--bs-table-striped-bg:#2c3034;--bs-table-striped-color:#fff;--bs-table-active-bg:#373b3e;--bs-table-active-color:#fff;--bs-table-hover-bg:#323539;--bs-table-hover-color:#fff;color:#fff;border-color:#373b3e}.table-responsive{overflow-x:auto;-webkit-overflow-scrolling:touch}@media (max-width:575.98px){.table-responsive-sm{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:767.98px){.table-responsive-md{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:991.98px){.table-responsive-lg{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:1199.98px){.table-responsive-xl{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:1399.98px){.table-responsive-xxl{overflow-x:auto;-webkit-overflow-scrolling:touch}}.form-label{margin-bottom:.5rem}.col-form-label{padding-top:calc(.375rem + 1px);padding-bottom:calc(.375rem + 1px);margin-bottom:0;font-size:inherit;line-height:1.5}.col-form-label-lg{padding-top:calc(.5rem + 1px);padding-bottom:calc(.5rem + 1px);font-size:1.25rem}.col-form-label-sm{padding-top:calc(.25rem + 1px);padding-bottom:calc(.25rem + 1px);font-size:.875rem}.form-text{margin-top:.25rem;font-size:.875em;color:#6c757d}.form-control{display:block;width:100%;padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#212529;background-color:#fff;background-clip:padding-box;border:1px solid #ced4da;-webkit-appearance:none;-moz-appearance:none;appearance:none;border-radius:.25rem;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control{transition:none}}.form-control[type=file]{overflow:hidden}.form-control[type=file]:not(:disabled):not([readonly]){cursor:pointer}.form-control:focus{color:#212529;background-color:#fff;border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.form-control::-webkit-date-and-time-value{height:1.5em}.form-control::-moz-placeholder{color:#6c757d;opacity:1}.form-control::placeholder{color:#6c757d;opacity:1}.form-control:disabled,.form-control[readonly]{background-color:#e9ecef;opacity:1}.form-control::-webkit-file-upload-button{padding:.375rem .75rem;margin:-.375rem -.75rem;-webkit-margin-end:.75rem;margin-inline-end:.75rem;color:#212529;background-color:#e9ecef;pointer-events:none;border-color:inherit;border-style:solid;border-width:0;border-inline-end-width:1px;border-radius:0;-webkit-transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}.form-control::file-selector-button{padding:.375rem .75rem;margin:-.375rem -.75rem;-webkit-margin-end:.75rem;margin-inline-end:.75rem;color:#212529;background-color:#e9ecef;pointer-events:none;border-color:inherit;border-style:solid;border-width:0;border-inline-end-width:1px;border-radius:0;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control::-webkit-file-upload-button{-webkit-transition:none;transition:none}.form-control::file-selector-button{transition:none}}.form-control:hover:not(:disabled):not([readonly])::-webkit-file-upload-button{background-color:#dde0e3}.form-control:hover:not(:disabled):not([readonly])::file-selector-button{background-color:#dde0e3}.form-control::-webkit-file-upload-button{padding:.375rem .75rem;margin:-.375rem -.75rem;-webkit-margin-end:.75rem;margin-inline-end:.75rem;color:#212529;background-color:#e9ecef;pointer-events:none;border-color:inherit;border-style:solid;border-width:0;border-inline-end-width:1px;border-radius:0;-webkit-transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control::-webkit-file-upload-button{-webkit-transition:none;transition:none}}.form-control:hover:not(:disabled):not([readonly])::-webkit-file-upload-button{background-color:#dde0e3}.form-control-plaintext{display:block;width:100%;padding:.375rem 0;margin-bottom:0;line-height:1.5;color:#212529;background-color:transparent;border:solid transparent;border-width:1px 0}.form-control-plaintext.form-control-lg,.form-control-plaintext.form-control-sm{padding-right:0;padding-left:0}.form-control-sm{min-height:calc(1.5em + .5rem + 2px);padding:.25rem .5rem;font-size:.875rem;border-radius:.2rem}.form-control-sm::-webkit-file-upload-button{padding:.25rem .5rem;margin:-.25rem -.5rem;-webkit-margin-end:.5rem;margin-inline-end:.5rem}.form-control-sm::file-selector-button{padding:.25rem .5rem;margin:-.25rem -.5rem;-webkit-margin-end:.5rem;margin-inline-end:.5rem}.form-control-sm::-webkit-file-upload-button{padding:.25rem .5rem;margin:-.25rem -.5rem;-webkit-margin-end:.5rem;margin-inline-end:.5rem}.form-control-lg{min-height:calc(1.5em + 1rem + 2px);padding:.5rem 1rem;font-size:1.25rem;border-radius:.3rem}.form-control-lg::-webkit-file-upload-button{padding:.5rem 1rem;margin:-.5rem -1rem;-webkit-margin-end:1rem;margin-inline-end:1rem}.form-control-lg::file-selector-button{padding:.5rem 1rem;margin:-.5rem -1rem;-webkit-margin-end:1rem;margin-inline-end:1rem}.form-control-lg::-webkit-file-upload-button{padding:.5rem 1rem;margin:-.5rem -1rem;-webkit-margin-end:1rem;margin-inline-end:1rem}textarea.form-control{min-height:calc(1.5em + .75rem + 2px)}textarea.form-control-sm{min-height:calc(1.5em + .5rem + 2px)}textarea.form-control-lg{min-height:calc(1.5em + 1rem + 2px)}.form-control-color{width:3rem;height:auto;padding:.375rem}.form-control-color:not(:disabled):not([readonly]){cursor:pointer}.form-control-color::-moz-color-swatch{height:1.5em;border-radius:.25rem}.form-control-color::-webkit-color-swatch{height:1.5em;border-radius:.25rem}.form-select{display:block;width:100%;padding:.375rem 2.25rem .375rem .75rem;-moz-padding-start:calc(0.75rem - 3px);font-size:1rem;font-weight:400;line-height:1.5;color:#212529;background-color:#fff;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right .75rem center;background-size:16px 12px;border:1px solid #ced4da;border-radius:.25rem;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out;-webkit-appearance:none;-moz-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.form-select{transition:none}}.form-select:focus{border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.form-select[multiple],.form-select[size]:not([size="1"]){padding-right:.75rem;background-image:none}.form-select:disabled{background-color:#e9ecef}.form-select:-moz-focusring{color:transparent;text-shadow:0 0 0 #212529}.form-select-sm{padding-top:.25rem;padding-bottom:.25rem;padding-left:.5rem;font-size:.875rem;border-radius:.2rem}.form-select-lg{padding-top:.5rem;padding-bottom:.5rem;padding-left:1rem;font-size:1.25rem;border-radius:.3rem}.form-check{display:block;min-height:1.5rem;padding-left:1.5em;margin-bottom:.125rem}.form-check .form-check-input{float:left;margin-left:-1.5em}.form-check-input{width:1em;height:1em;margin-top:.25em;vertical-align:top;background-color:#fff;background-repeat:no-repeat;background-position:center;background-size:contain;border:1px solid rgba(0,0,0,.25);-webkit-appearance:none;-moz-appearance:none;appearance:none;-webkit-print-color-adjust:exact;color-adjust:exact}.form-check-input[type=checkbox]{border-radius:.25em}.form-check-input[type=radio]{border-radius:50%}.form-check-input:active{filter:brightness(90%)}.form-check-input:focus{border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.form-check-input:checked{background-color:#0d6efd;border-color:#0d6efd}.form-check-input:checked[type=checkbox]{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10l3 3l6-6'/%3e%3c/svg%3e")}.form-check-input:checked[type=radio]{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='2' fill='%23fff'/%3e%3c/svg%3e")}.form-check-input[type=checkbox]:indeterminate{background-color:#0d6efd;border-color:#0d6efd;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10h8'/%3e%3c/svg%3e")}.form-check-input:disabled{pointer-events:none;filter:none;opacity:.5}.form-check-input:disabled~.form-check-label,.form-check-input[disabled]~.form-check-label{opacity:.5}.form-switch{padding-left:2.5em}.form-switch .form-check-input{width:2em;margin-left:-2.5em;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%280, 0, 0, 0.25%29'/%3e%3c/svg%3e");background-position:left center;border-radius:2em;transition:background-position .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-switch .form-check-input{transition:none}}.form-switch .form-check-input:focus{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%2386b7fe'/%3e%3c/svg%3e")}.form-switch .form-check-input:checked{background-position:right center;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e")}.form-check-inline{display:inline-block;margin-right:1rem}.btn-check{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.btn-check:disabled+.btn,.btn-check[disabled]+.btn{pointer-events:none;filter:none;opacity:.65}.form-range{width:100%;height:1.5rem;padding:0;background-color:transparent;-webkit-appearance:none;-moz-appearance:none;appearance:none}.form-range:focus{outline:0}.form-range:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .25rem rgba(13,110,253,.25)}.form-range:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .25rem rgba(13,110,253,.25)}.form-range::-moz-focus-outer{border:0}.form-range::-webkit-slider-thumb{width:1rem;height:1rem;margin-top:-.25rem;background-color:#0d6efd;border:0;border-radius:1rem;-webkit-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;-webkit-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.form-range::-webkit-slider-thumb{-webkit-transition:none;transition:none}}.form-range::-webkit-slider-thumb:active{background-color:#b6d4fe}.form-range::-webkit-slider-runnable-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.form-range::-moz-range-thumb{width:1rem;height:1rem;background-color:#0d6efd;border:0;border-radius:1rem;-moz-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;-moz-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.form-range::-moz-range-thumb{-moz-transition:none;transition:none}}.form-range::-moz-range-thumb:active{background-color:#b6d4fe}.form-range::-moz-range-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.form-range:disabled{pointer-events:none}.form-range:disabled::-webkit-slider-thumb{background-color:#adb5bd}.form-range:disabled::-moz-range-thumb{background-color:#adb5bd}.form-floating{position:relative}.form-floating>.form-control,.form-floating>.form-select{height:calc(3.5rem + 2px);line-height:1.25}.form-floating>label{position:absolute;top:0;left:0;height:100%;padding:1rem .75rem;pointer-events:none;border:1px solid transparent;transform-origin:0 0;transition:opacity .1s ease-in-out,transform .1s ease-in-out}@media (prefers-reduced-motion:reduce){.form-floating>label{transition:none}}.form-floating>.form-control{padding:1rem .75rem}.form-floating>.form-control::-moz-placeholder{color:transparent}.form-floating>.form-control::placeholder{color:transparent}.form-floating>.form-control:not(:-moz-placeholder-shown){padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control:focus,.form-floating>.form-control:not(:placeholder-shown){padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control:-webkit-autofill{padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-select{padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control:not(:-moz-placeholder-shown)~label{opacity:.65;transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.form-floating>.form-control:focus~label,.form-floating>.form-control:not(:placeholder-shown)~label,.form-floating>.form-select~label{opacity:.65;transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.form-floating>.form-control:-webkit-autofill~label{opacity:.65;transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.input-group{position:relative;display:flex;flex-wrap:wrap;align-items:stretch;width:100%}.input-group>.form-control,.input-group>.form-select{position:relative;flex:1 1 auto;width:1%;min-width:0}.input-group>.form-control:focus,.input-group>.form-select:focus{z-index:3}.input-group .btn{position:relative;z-index:2}.input-group .btn:focus{z-index:3}.input-group-text{display:flex;align-items:center;padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:center;white-space:nowrap;background-color:#e9ecef;border:1px solid #ced4da;border-radius:.25rem}.input-group-lg>.btn,.input-group-lg>.form-control,.input-group-lg>.form-select,.input-group-lg>.input-group-text{padding:.5rem 1rem;font-size:1.25rem;border-radius:.3rem}.input-group-sm>.btn,.input-group-sm>.form-control,.input-group-sm>.form-select,.input-group-sm>.input-group-text{padding:.25rem .5rem;font-size:.875rem;border-radius:.2rem}.input-group-lg>.form-select,.input-group-sm>.form-select{padding-right:3rem}.input-group:not(.has-validation)>.dropdown-toggle:nth-last-child(n+3),.input-group:not(.has-validation)>:not(:last-child):not(.dropdown-toggle):not(.dropdown-menu){border-top-right-radius:0;border-bottom-right-radius:0}.input-group.has-validation>.dropdown-toggle:nth-last-child(n+4),.input-group.has-validation>:nth-last-child(n+3):not(.dropdown-toggle):not(.dropdown-menu){border-top-right-radius:0;border-bottom-right-radius:0}.input-group>:not(:first-child):not(.dropdown-menu):not(.valid-tooltip):not(.valid-feedback):not(.invalid-tooltip):not(.invalid-feedback){margin-left:-1px;border-top-left-radius:0;border-bottom-left-radius:0}.valid-feedback{display:none;width:100%;margin-top:.25rem;font-size:.875em;color:#198754}.valid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;color:#fff;background-color:rgba(25,135,84,.9);border-radius:.25rem}.is-valid~.valid-feedback,.is-valid~.valid-tooltip,.was-validated :valid~.valid-feedback,.was-validated :valid~.valid-tooltip{display:block}.form-control.is-valid,.was-validated .form-control:valid{border-color:#198754;padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-valid:focus,.was-validated .form-control:valid:focus{border-color:#198754;box-shadow:0 0 0 .25rem rgba(25,135,84,.25)}.was-validated textarea.form-control:valid,textarea.form-control.is-valid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.form-select.is-valid,.was-validated .form-select:valid{border-color:#198754}.form-select.is-valid:not([multiple]):not([size]),.form-select.is-valid:not([multiple])[size="1"],.was-validated .form-select:valid:not([multiple]):not([size]),.was-validated .form-select:valid:not([multiple])[size="1"]{padding-right:4.125rem;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e"),url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");background-position:right .75rem center,center right 2.25rem;background-size:16px 12px,calc(.75em + .375rem) calc(.75em + .375rem)}.form-select.is-valid:focus,.was-validated .form-select:valid:focus{border-color:#198754;box-shadow:0 0 0 .25rem rgba(25,135,84,.25)}.form-check-input.is-valid,.was-validated .form-check-input:valid{border-color:#198754}.form-check-input.is-valid:checked,.was-validated .form-check-input:valid:checked{background-color:#198754}.form-check-input.is-valid:focus,.was-validated .form-check-input:valid:focus{box-shadow:0 0 0 .25rem rgba(25,135,84,.25)}.form-check-input.is-valid~.form-check-label,.was-validated .form-check-input:valid~.form-check-label{color:#198754}.form-check-inline .form-check-input~.valid-feedback{margin-left:.5em}.input-group .form-control.is-valid,.input-group .form-select.is-valid,.was-validated .input-group .form-control:valid,.was-validated .input-group .form-select:valid{z-index:1}.input-group .form-control.is-valid:focus,.input-group .form-select.is-valid:focus,.was-validated .input-group .form-control:valid:focus,.was-validated .input-group .form-select:valid:focus{z-index:3}.invalid-feedback{display:none;width:100%;margin-top:.25rem;font-size:.875em;color:#dc3545}.invalid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;color:#fff;background-color:rgba(220,53,69,.9);border-radius:.25rem}.is-invalid~.invalid-feedback,.is-invalid~.invalid-tooltip,.was-validated :invalid~.invalid-feedback,.was-validated :invalid~.invalid-tooltip{display:block}.form-control.is-invalid,.was-validated .form-control:invalid{border-color:#dc3545;padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-invalid:focus,.was-validated .form-control:invalid:focus{border-color:#dc3545;box-shadow:0 0 0 .25rem rgba(220,53,69,.25)}.was-validated textarea.form-control:invalid,textarea.form-control.is-invalid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.form-select.is-invalid,.was-validated .form-select:invalid{border-color:#dc3545}.form-select.is-invalid:not([multiple]):not([size]),.form-select.is-invalid:not([multiple])[size="1"],.was-validated .form-select:invalid:not([multiple]):not([size]),.was-validated .form-select:invalid:not([multiple])[size="1"]{padding-right:4.125rem;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e"),url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e");background-position:right .75rem center,center right 2.25rem;background-size:16px 12px,calc(.75em + .375rem) calc(.75em + .375rem)}.form-select.is-invalid:focus,.was-validated .form-select:invalid:focus{border-color:#dc3545;box-shadow:0 0 0 .25rem rgba(220,53,69,.25)}.form-check-input.is-invalid,.was-validated .form-check-input:invalid{border-color:#dc3545}.form-check-input.is-invalid:checked,.was-validated .form-check-input:invalid:checked{background-color:#dc3545}.form-check-input.is-invalid:focus,.was-validated .form-check-input:invalid:focus{box-shadow:0 0 0 .25rem rgba(220,53,69,.25)}.form-check-input.is-invalid~.form-check-label,.was-validated .form-check-input:invalid~.form-check-label{color:#dc3545}.form-check-inline .form-check-input~.invalid-feedback{margin-left:.5em}.input-group .form-control.is-invalid,.input-group .form-select.is-invalid,.was-validated .input-group .form-control:invalid,.was-validated .input-group .form-select:invalid{z-index:2}.input-group .form-control.is-invalid:focus,.input-group .form-select.is-invalid:focus,.was-validated .input-group .form-control:invalid:focus,.was-validated .input-group .form-select:invalid:focus{z-index:3}.btn{display:inline-block;font-weight:400;line-height:1.5;color:#212529;text-align:center;text-decoration:none;vertical-align:middle;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;user-select:none;background-color:transparent;border:1px solid transparent;padding:.375rem .75rem;font-size:1rem;border-radius:.25rem;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.btn{transition:none}}.btn:hover{color:#212529}.btn-check:focus+.btn,.btn:focus{outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.btn.disabled,.btn:disabled,fieldset:disabled .btn{pointer-events:none;opacity:.65}.btn-primary{color:#fff;background-color:#0d6efd;border-color:#0d6efd}.btn-primary:hover{color:#fff;background-color:#0b5ed7;border-color:#0a58ca}.btn-check:focus+.btn-primary,.btn-primary:focus{color:#fff;background-color:#0b5ed7;border-color:#0a58ca;box-shadow:0 0 0 .25rem rgba(49,132,253,.5)}.btn-check:active+.btn-primary,.btn-check:checked+.btn-primary,.btn-primary.active,.btn-primary:active,.show>.btn-primary.dropdown-toggle{color:#fff;background-color:#0a58ca;border-color:#0a53be}.btn-check:active+.btn-primary:focus,.btn-check:checked+.btn-primary:focus,.btn-primary.active:focus,.btn-primary:active:focus,.show>.btn-primary.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(49,132,253,.5)}.btn-primary.disabled,.btn-primary:disabled{color:#fff;background-color:#0d6efd;border-color:#0d6efd}.btn-secondary{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-secondary:hover{color:#fff;background-color:#5c636a;border-color:#565e64}.btn-check:focus+.btn-secondary,.btn-secondary:focus{color:#fff;background-color:#5c636a;border-color:#565e64;box-shadow:0 0 0 .25rem rgba(130,138,145,.5)}.btn-check:active+.btn-secondary,.btn-check:checked+.btn-secondary,.btn-secondary.active,.btn-secondary:active,.show>.btn-secondary.dropdown-toggle{color:#fff;background-color:#565e64;border-color:#51585e}.btn-check:active+.btn-secondary:focus,.btn-check:checked+.btn-secondary:focus,.btn-secondary.active:focus,.btn-secondary:active:focus,.show>.btn-secondary.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(130,138,145,.5)}.btn-secondary.disabled,.btn-secondary:disabled{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-success{color:#fff;background-color:#198754;border-color:#198754}.btn-success:hover{color:#fff;background-color:#157347;border-color:#146c43}.btn-check:focus+.btn-success,.btn-success:focus{color:#fff;background-color:#157347;border-color:#146c43;box-shadow:0 0 0 .25rem rgba(60,153,110,.5)}.btn-check:active+.btn-success,.btn-check:checked+.btn-success,.btn-success.active,.btn-success:active,.show>.btn-success.dropdown-toggle{color:#fff;background-color:#146c43;border-color:#13653f}.btn-check:active+.btn-success:focus,.btn-check:checked+.btn-success:focus,.btn-success.active:focus,.btn-success:active:focus,.show>.btn-success.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(60,153,110,.5)}.btn-success.disabled,.btn-success:disabled{color:#fff;background-color:#198754;border-color:#198754}.btn-info{color:#000;background-color:#0dcaf0;border-color:#0dcaf0}.btn-info:hover{color:#000;background-color:#31d2f2;border-color:#25cff2}.btn-check:focus+.btn-info,.btn-info:focus{color:#000;background-color:#31d2f2;border-color:#25cff2;box-shadow:0 0 0 .25rem rgba(11,172,204,.5)}.btn-check:active+.btn-info,.btn-check:checked+.btn-info,.btn-info.active,.btn-info:active,.show>.btn-info.dropdown-toggle{color:#000;background-color:#3dd5f3;border-color:#25cff2}.btn-check:active+.btn-info:focus,.btn-check:checked+.btn-info:focus,.btn-info.active:focus,.btn-info:active:focus,.show>.btn-info.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(11,172,204,.5)}.btn-info.disabled,.btn-info:disabled{color:#000;background-color:#0dcaf0;border-color:#0dcaf0}.btn-warning{color:#000;background-color:#ffc107;border-color:#ffc107}.btn-warning:hover{color:#000;background-color:#ffca2c;border-color:#ffc720}.btn-check:focus+.btn-warning,.btn-warning:focus{color:#000;background-color:#ffca2c;border-color:#ffc720;box-shadow:0 0 0 .25rem rgba(217,164,6,.5)}.btn-check:active+.btn-warning,.btn-check:checked+.btn-warning,.btn-warning.active,.btn-warning:active,.show>.btn-warning.dropdown-toggle{color:#000;background-color:#ffcd39;border-color:#ffc720}.btn-check:active+.btn-warning:focus,.btn-check:checked+.btn-warning:focus,.btn-warning.active:focus,.btn-warning:active:focus,.show>.btn-warning.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(217,164,6,.5)}.btn-warning.disabled,.btn-warning:disabled{color:#000;background-color:#ffc107;border-color:#ffc107}.btn-danger{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-danger:hover{color:#fff;background-color:#bb2d3b;border-color:#b02a37}.btn-check:focus+.btn-danger,.btn-danger:focus{color:#fff;background-color:#bb2d3b;border-color:#b02a37;box-shadow:0 0 0 .25rem rgba(225,83,97,.5)}.btn-check:active+.btn-danger,.btn-check:checked+.btn-danger,.btn-danger.active,.btn-danger:active,.show>.btn-danger.dropdown-toggle{color:#fff;background-color:#b02a37;border-color:#a52834}.btn-check:active+.btn-danger:focus,.btn-check:checked+.btn-danger:focus,.btn-danger.active:focus,.btn-danger:active:focus,.show>.btn-danger.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(225,83,97,.5)}.btn-danger.disabled,.btn-danger:disabled{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-light{color:#000;background-color:#f8f9fa;border-color:#f8f9fa}.btn-light:hover{color:#000;background-color:#f9fafb;border-color:#f9fafb}.btn-check:focus+.btn-light,.btn-light:focus{color:#000;background-color:#f9fafb;border-color:#f9fafb;box-shadow:0 0 0 .25rem rgba(211,212,213,.5)}.btn-check:active+.btn-light,.btn-check:checked+.btn-light,.btn-light.active,.btn-light:active,.show>.btn-light.dropdown-toggle{color:#000;background-color:#f9fafb;border-color:#f9fafb}.btn-check:active+.btn-light:focus,.btn-check:checked+.btn-light:focus,.btn-light.active:focus,.btn-light:active:focus,.show>.btn-light.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(211,212,213,.5)}.btn-light.disabled,.btn-light:disabled{color:#000;background-color:#f8f9fa;border-color:#f8f9fa}.btn-dark{color:#fff;background-color:#212529;border-color:#212529}.btn-dark:hover{color:#fff;background-color:#1c1f23;border-color:#1a1e21}.btn-check:focus+.btn-dark,.btn-dark:focus{color:#fff;background-color:#1c1f23;border-color:#1a1e21;box-shadow:0 0 0 .25rem rgba(66,70,73,.5)}.btn-check:active+.btn-dark,.btn-check:checked+.btn-dark,.btn-dark.active,.btn-dark:active,.show>.btn-dark.dropdown-toggle{color:#fff;background-color:#1a1e21;border-color:#191c1f}.btn-check:active+.btn-dark:focus,.btn-check:checked+.btn-dark:focus,.btn-dark.active:focus,.btn-dark:active:focus,.show>.btn-dark.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(66,70,73,.5)}.btn-dark.disabled,.btn-dark:disabled{color:#fff;background-color:#212529;border-color:#212529}.btn-outline-primary{color:#0d6efd;border-color:#0d6efd}.btn-outline-primary:hover{color:#fff;background-color:#0d6efd;border-color:#0d6efd}.btn-check:focus+.btn-outline-primary,.btn-outline-primary:focus{box-shadow:0 0 0 .25rem rgba(13,110,253,.5)}.btn-check:active+.btn-outline-primary,.btn-check:checked+.btn-outline-primary,.btn-outline-primary.active,.btn-outline-primary.dropdown-toggle.show,.btn-outline-primary:active{color:#fff;background-color:#0d6efd;border-color:#0d6efd}.btn-check:active+.btn-outline-primary:focus,.btn-check:checked+.btn-outline-primary:focus,.btn-outline-primary.active:focus,.btn-outline-primary.dropdown-toggle.show:focus,.btn-outline-primary:active:focus{box-shadow:0 0 0 .25rem rgba(13,110,253,.5)}.btn-outline-primary.disabled,.btn-outline-primary:disabled{color:#0d6efd;background-color:transparent}.btn-outline-secondary{color:#6c757d;border-color:#6c757d}.btn-outline-secondary:hover{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-check:focus+.btn-outline-secondary,.btn-outline-secondary:focus{box-shadow:0 0 0 .25rem rgba(108,117,125,.5)}.btn-check:active+.btn-outline-secondary,.btn-check:checked+.btn-outline-secondary,.btn-outline-secondary.active,.btn-outline-secondary.dropdown-toggle.show,.btn-outline-secondary:active{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-check:active+.btn-outline-secondary:focus,.btn-check:checked+.btn-outline-secondary:focus,.btn-outline-secondary.active:focus,.btn-outline-secondary.dropdown-toggle.show:focus,.btn-outline-secondary:active:focus{box-shadow:0 0 0 .25rem rgba(108,117,125,.5)}.btn-outline-secondary.disabled,.btn-outline-secondary:disabled{color:#6c757d;background-color:transparent}.btn-outline-success{color:#198754;border-color:#198754}.btn-outline-success:hover{color:#fff;background-color:#198754;border-color:#198754}.btn-check:focus+.btn-outline-success,.btn-outline-success:focus{box-shadow:0 0 0 .25rem rgba(25,135,84,.5)}.btn-check:active+.btn-outline-success,.btn-check:checked+.btn-outline-success,.btn-outline-success.active,.btn-outline-success.dropdown-toggle.show,.btn-outline-success:active{color:#fff;background-color:#198754;border-color:#198754}.btn-check:active+.btn-outline-success:focus,.btn-check:checked+.btn-outline-success:focus,.btn-outline-success.active:focus,.btn-outline-success.dropdown-toggle.show:focus,.btn-outline-success:active:focus{box-shadow:0 0 0 .25rem rgba(25,135,84,.5)}.btn-outline-success.disabled,.btn-outline-success:disabled{color:#198754;background-color:transparent}.btn-outline-info{color:#0dcaf0;border-color:#0dcaf0}.btn-outline-info:hover{color:#000;background-color:#0dcaf0;border-color:#0dcaf0}.btn-check:focus+.btn-outline-info,.btn-outline-info:focus{box-shadow:0 0 0 .25rem rgba(13,202,240,.5)}.btn-check:active+.btn-outline-info,.btn-check:checked+.btn-outline-info,.btn-outline-info.active,.btn-outline-info.dropdown-toggle.show,.btn-outline-info:active{color:#000;background-color:#0dcaf0;border-color:#0dcaf0}.btn-check:active+.btn-outline-info:focus,.btn-check:checked+.btn-outline-info:focus,.btn-outline-info.active:focus,.btn-outline-info.dropdown-toggle.show:focus,.btn-outline-info:active:focus{box-shadow:0 0 0 .25rem rgba(13,202,240,.5)}.btn-outline-info.disabled,.btn-outline-info:disabled{color:#0dcaf0;background-color:transparent}.btn-outline-warning{color:#ffc107;border-color:#ffc107}.btn-outline-warning:hover{color:#000;background-color:#ffc107;border-color:#ffc107}.btn-check:focus+.btn-outline-warning,.btn-outline-warning:focus{box-shadow:0 0 0 .25rem rgba(255,193,7,.5)}.btn-check:active+.btn-outline-warning,.btn-check:checked+.btn-outline-warning,.btn-outline-warning.active,.btn-outline-warning.dropdown-toggle.show,.btn-outline-warning:active{color:#000;background-color:#ffc107;border-color:#ffc107}.btn-check:active+.btn-outline-warning:focus,.btn-check:checked+.btn-outline-warning:focus,.btn-outline-warning.active:focus,.btn-outline-warning.dropdown-toggle.show:focus,.btn-outline-warning:active:focus{box-shadow:0 0 0 .25rem rgba(255,193,7,.5)}.btn-outline-warning.disabled,.btn-outline-warning:disabled{color:#ffc107;background-color:transparent}.btn-outline-danger{color:#dc3545;border-color:#dc3545}.btn-outline-danger:hover{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-check:focus+.btn-outline-danger,.btn-outline-danger:focus{box-shadow:0 0 0 .25rem rgba(220,53,69,.5)}.btn-check:active+.btn-outline-danger,.btn-check:checked+.btn-outline-danger,.btn-outline-danger.active,.btn-outline-danger.dropdown-toggle.show,.btn-outline-danger:active{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-check:active+.btn-outline-danger:focus,.btn-check:checked+.btn-outline-danger:focus,.btn-outline-danger.active:focus,.btn-outline-danger.dropdown-toggle.show:focus,.btn-outline-danger:active:focus{box-shadow:0 0 0 .25rem rgba(220,53,69,.5)}.btn-outline-danger.disabled,.btn-outline-danger:disabled{color:#dc3545;background-color:transparent}.btn-outline-light{color:#f8f9fa;border-color:#f8f9fa}.btn-outline-light:hover{color:#000;background-color:#f8f9fa;border-color:#f8f9fa}.btn-check:focus+.btn-outline-light,.btn-outline-light:focus{box-shadow:0 0 0 .25rem rgba(248,249,250,.5)}.btn-check:active+.btn-outline-light,.btn-check:checked+.btn-outline-light,.btn-outline-light.active,.btn-outline-light.dropdown-toggle.show,.btn-outline-light:active{color:#000;background-color:#f8f9fa;border-color:#f8f9fa}.btn-check:active+.btn-outline-light:focus,.btn-check:checked+.btn-outline-light:focus,.btn-outline-light.active:focus,.btn-outline-light.dropdown-toggle.show:focus,.btn-outline-light:active:focus{box-shadow:0 0 0 .25rem rgba(248,249,250,.5)}.btn-outline-light.disabled,.btn-outline-light:disabled{color:#f8f9fa;background-color:transparent}.btn-outline-dark{color:#212529;border-color:#212529}.btn-outline-dark:hover{color:#fff;background-color:#212529;border-color:#212529}.btn-check:focus+.btn-outline-dark,.btn-outline-dark:focus{box-shadow:0 0 0 .25rem rgba(33,37,41,.5)}.btn-check:active+.btn-outline-dark,.btn-check:checked+.btn-outline-dark,.btn-outline-dark.active,.btn-outline-dark.dropdown-toggle.show,.btn-outline-dark:active{color:#fff;background-color:#212529;border-color:#212529}.btn-check:active+.btn-outline-dark:focus,.btn-check:checked+.btn-outline-dark:focus,.btn-outline-dark.active:focus,.btn-outline-dark.dropdown-toggle.show:focus,.btn-outline-dark:active:focus{box-shadow:0 0 0 .25rem rgba(33,37,41,.5)}.btn-outline-dark.disabled,.btn-outline-dark:disabled{color:#212529;background-color:transparent}.btn-link{font-weight:400;color:#0d6efd;text-decoration:underline}.btn-link:hover{color:#0a58ca}.btn-link.disabled,.btn-link:disabled{color:#6c757d}.btn-group-lg>.btn,.btn-lg{padding:.5rem 1rem;font-size:1.25rem;border-radius:.3rem}.btn-group-sm>.btn,.btn-sm{padding:.25rem .5rem;font-size:.875rem;border-radius:.2rem}.fade{transition:opacity .15s linear}@media (prefers-reduced-motion:reduce){.fade{transition:none}}.fade:not(.show){opacity:0}.collapse:not(.show){display:none}.collapsing{height:0;overflow:hidden;transition:height .35s ease}@media (prefers-reduced-motion:reduce){.collapsing{transition:none}}.collapsing.collapse-horizontal{width:0;height:auto;transition:width .35s ease}@media (prefers-reduced-motion:reduce){.collapsing.collapse-horizontal{transition:none}}.dropdown,.dropend,.dropstart,.dropup{position:relative}.dropdown-toggle{white-space:nowrap}.dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid;border-right:.3em solid transparent;border-bottom:0;border-left:.3em solid transparent}.dropdown-toggle:empty::after{margin-left:0}.dropdown-menu{position:absolute;z-index:1000;display:none;min-width:10rem;padding:.5rem 0;margin:0;font-size:1rem;color:#212529;text-align:left;list-style:none;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.15);border-radius:.25rem}.dropdown-menu[data-bs-popper]{top:100%;left:0;margin-top:.125rem}.dropdown-menu-start{--bs-position:start}.dropdown-menu-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-end{--bs-position:end}.dropdown-menu-end[data-bs-popper]{right:0;left:auto}@media (min-width:576px){.dropdown-menu-sm-start{--bs-position:start}.dropdown-menu-sm-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-sm-end{--bs-position:end}.dropdown-menu-sm-end[data-bs-popper]{right:0;left:auto}}@media (min-width:768px){.dropdown-menu-md-start{--bs-position:start}.dropdown-menu-md-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-md-end{--bs-position:end}.dropdown-menu-md-end[data-bs-popper]{right:0;left:auto}}@media (min-width:992px){.dropdown-menu-lg-start{--bs-position:start}.dropdown-menu-lg-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-lg-end{--bs-position:end}.dropdown-menu-lg-end[data-bs-popper]{right:0;left:auto}}@media (min-width:1200px){.dropdown-menu-xl-start{--bs-position:start}.dropdown-menu-xl-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-xl-end{--bs-position:end}.dropdown-menu-xl-end[data-bs-popper]{right:0;left:auto}}@media (min-width:1400px){.dropdown-menu-xxl-start{--bs-position:start}.dropdown-menu-xxl-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-xxl-end{--bs-position:end}.dropdown-menu-xxl-end[data-bs-popper]{right:0;left:auto}}.dropup .dropdown-menu[data-bs-popper]{top:auto;bottom:100%;margin-top:0;margin-bottom:.125rem}.dropup .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:0;border-right:.3em solid transparent;border-bottom:.3em solid;border-left:.3em solid transparent}.dropup .dropdown-toggle:empty::after{margin-left:0}.dropend .dropdown-menu[data-bs-popper]{top:0;right:auto;left:100%;margin-top:0;margin-left:.125rem}.dropend .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:0;border-bottom:.3em solid transparent;border-left:.3em solid}.dropend .dropdown-toggle:empty::after{margin-left:0}.dropend .dropdown-toggle::after{vertical-align:0}.dropstart .dropdown-menu[data-bs-popper]{top:0;right:100%;left:auto;margin-top:0;margin-right:.125rem}.dropstart .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:""}.dropstart .dropdown-toggle::after{display:none}.dropstart .dropdown-toggle::before{display:inline-block;margin-right:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:.3em solid;border-bottom:.3em solid transparent}.dropstart .dropdown-toggle:empty::after{margin-left:0}.dropstart .dropdown-toggle::before{vertical-align:0}.dropdown-divider{height:0;margin:.5rem 0;overflow:hidden;border-top:1px solid rgba(0,0,0,.15)}.dropdown-item{display:block;width:100%;padding:.25rem 1rem;clear:both;font-weight:400;color:#212529;text-align:inherit;text-decoration:none;white-space:nowrap;background-color:transparent;border:0}.dropdown-item:focus,.dropdown-item:hover{color:#1e2125;background-color:#e9ecef}.dropdown-item.active,.dropdown-item:active{color:#fff;text-decoration:none;background-color:#0d6efd}.dropdown-item.disabled,.dropdown-item:disabled{color:#adb5bd;pointer-events:none;background-color:transparent}.dropdown-menu.show{display:block}.dropdown-header{display:block;padding:.5rem 1rem;margin-bottom:0;font-size:.875rem;color:#6c757d;white-space:nowrap}.dropdown-item-text{display:block;padding:.25rem 1rem;color:#212529}.dropdown-menu-dark{color:#dee2e6;background-color:#343a40;border-color:rgba(0,0,0,.15)}.dropdown-menu-dark .dropdown-item{color:#dee2e6}.dropdown-menu-dark .dropdown-item:focus,.dropdown-menu-dark .dropdown-item:hover{color:#fff;background-color:rgba(255,255,255,.15)}.dropdown-menu-dark .dropdown-item.active,.dropdown-menu-dark .dropdown-item:active{color:#fff;background-color:#0d6efd}.dropdown-menu-dark .dropdown-item.disabled,.dropdown-menu-dark .dropdown-item:disabled{color:#adb5bd}.dropdown-menu-dark .dropdown-divider{border-color:rgba(0,0,0,.15)}.dropdown-menu-dark .dropdown-item-text{color:#dee2e6}.dropdown-menu-dark .dropdown-header{color:#adb5bd}.btn-group,.btn-group-vertical{position:relative;display:inline-flex;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;flex:1 1 auto}.btn-group-vertical>.btn-check:checked+.btn,.btn-group-vertical>.btn-check:focus+.btn,.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:hover,.btn-group>.btn-check:checked+.btn,.btn-group>.btn-check:focus+.btn,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus,.btn-group>.btn:hover{z-index:1}.btn-toolbar{display:flex;flex-wrap:wrap;justify-content:flex-start}.btn-toolbar .input-group{width:auto}.btn-group>.btn-group:not(:first-child),.btn-group>.btn:not(:first-child){margin-left:-1px}.btn-group>.btn-group:not(:last-child)>.btn,.btn-group>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:not(:first-child)>.btn,.btn-group>.btn:nth-child(n+3),.btn-group>:not(.btn-check)+.btn{border-top-left-radius:0;border-bottom-left-radius:0}.dropdown-toggle-split{padding-right:.5625rem;padding-left:.5625rem}.dropdown-toggle-split::after,.dropend .dropdown-toggle-split::after,.dropup .dropdown-toggle-split::after{margin-left:0}.dropstart .dropdown-toggle-split::before{margin-right:0}.btn-group-sm>.btn+.dropdown-toggle-split,.btn-sm+.dropdown-toggle-split{padding-right:.375rem;padding-left:.375rem}.btn-group-lg>.btn+.dropdown-toggle-split,.btn-lg+.dropdown-toggle-split{padding-right:.75rem;padding-left:.75rem}.btn-group-vertical{flex-direction:column;align-items:flex-start;justify-content:center}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group{width:100%}.btn-group-vertical>.btn-group:not(:first-child),.btn-group-vertical>.btn:not(:first-child){margin-top:-1px}.btn-group-vertical>.btn-group:not(:last-child)>.btn,.btn-group-vertical>.btn:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:not(:first-child)>.btn,.btn-group-vertical>.btn~.btn{border-top-left-radius:0;border-top-right-radius:0}.nav{display:flex;flex-wrap:wrap;padding-left:0;margin-bottom:0;list-style:none}.nav-link{display:block;padding:.5rem 1rem;color:#0d6efd;text-decoration:none;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out}@media (prefers-reduced-motion:reduce){.nav-link{transition:none}}.nav-link:focus,.nav-link:hover{color:#0a58ca}.nav-link.disabled{color:#6c757d;pointer-events:none;cursor:default}.nav-tabs{border-bottom:1px solid #dee2e6}.nav-tabs .nav-link{margin-bottom:-1px;background:0 0;border:1px solid transparent;border-top-left-radius:.25rem;border-top-right-radius:.25rem}.nav-tabs .nav-link:focus,.nav-tabs .nav-link:hover{border-color:#e9ecef #e9ecef #dee2e6;isolation:isolate}.nav-tabs .nav-link.disabled{color:#6c757d;background-color:transparent;border-color:transparent}.nav-tabs .nav-item.show .nav-link,.nav-tabs .nav-link.active{color:#495057;background-color:#fff;border-color:#dee2e6 #dee2e6 #fff}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-left-radius:0;border-top-right-radius:0}.nav-pills .nav-link{background:0 0;border:0;border-radius:.25rem}.nav-pills .nav-link.active,.nav-pills .show>.nav-link{color:#fff;background-color:#0d6efd}.nav-fill .nav-item,.nav-fill>.nav-link{flex:1 1 auto;text-align:center}.nav-justified .nav-item,.nav-justified>.nav-link{flex-basis:0;flex-grow:1;text-align:center}.nav-fill .nav-item .nav-link,.nav-justified .nav-item .nav-link{width:100%}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.navbar{position:relative;display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;padding-top:.5rem;padding-bottom:.5rem}.navbar>.container,.navbar>.container-fluid,.navbar>.container-lg,.navbar>.container-md,.navbar>.container-sm,.navbar>.container-xl,.navbar>.container-xxl{display:flex;flex-wrap:inherit;align-items:center;justify-content:space-between}.navbar-brand{padding-top:.3125rem;padding-bottom:.3125rem;margin-right:1rem;font-size:1.25rem;text-decoration:none;white-space:nowrap}.navbar-nav{display:flex;flex-direction:column;padding-left:0;margin-bottom:0;list-style:none}.navbar-nav .nav-link{padding-right:0;padding-left:0}.navbar-nav .dropdown-menu{position:static}.navbar-text{padding-top:.5rem;padding-bottom:.5rem}.navbar-collapse{flex-basis:100%;flex-grow:1;align-items:center}.navbar-toggler{padding:.25rem .75rem;font-size:1.25rem;line-height:1;background-color:transparent;border:1px solid transparent;border-radius:.25rem;transition:box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.navbar-toggler{transition:none}}.navbar-toggler:hover{text-decoration:none}.navbar-toggler:focus{text-decoration:none;outline:0;box-shadow:0 0 0 .25rem}.navbar-toggler-icon{display:inline-block;width:1.5em;height:1.5em;vertical-align:middle;background-repeat:no-repeat;background-position:center;background-size:100%}.navbar-nav-scroll{max-height:var(--bs-scroll-height,75vh);overflow-y:auto}@media (min-width:576px){.navbar-expand-sm{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-sm .navbar-nav{flex-direction:row}.navbar-expand-sm .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-sm .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-sm .navbar-nav-scroll{overflow:visible}.navbar-expand-sm .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-sm .navbar-toggler{display:none}.navbar-expand-sm .offcanvas-header{display:none}.navbar-expand-sm .offcanvas{position:inherit;bottom:0;z-index:1000;flex-grow:1;visibility:visible!important;background-color:transparent;border-right:0;border-left:0;transition:none;transform:none}.navbar-expand-sm .offcanvas-bottom,.navbar-expand-sm .offcanvas-top{height:auto;border-top:0;border-bottom:0}.navbar-expand-sm .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:768px){.navbar-expand-md{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-md .navbar-nav{flex-direction:row}.navbar-expand-md .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-md .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-md .navbar-nav-scroll{overflow:visible}.navbar-expand-md .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-md .navbar-toggler{display:none}.navbar-expand-md .offcanvas-header{display:none}.navbar-expand-md .offcanvas{position:inherit;bottom:0;z-index:1000;flex-grow:1;visibility:visible!important;background-color:transparent;border-right:0;border-left:0;transition:none;transform:none}.navbar-expand-md .offcanvas-bottom,.navbar-expand-md .offcanvas-top{height:auto;border-top:0;border-bottom:0}.navbar-expand-md .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:992px){.navbar-expand-lg{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-lg .navbar-nav{flex-direction:row}.navbar-expand-lg .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-lg .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-lg .navbar-nav-scroll{overflow:visible}.navbar-expand-lg .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-lg .navbar-toggler{display:none}.navbar-expand-lg .offcanvas-header{display:none}.navbar-expand-lg .offcanvas{position:inherit;bottom:0;z-index:1000;flex-grow:1;visibility:visible!important;background-color:transparent;border-right:0;border-left:0;transition:none;transform:none}.navbar-expand-lg .offcanvas-bottom,.navbar-expand-lg .offcanvas-top{height:auto;border-top:0;border-bottom:0}.navbar-expand-lg .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:1200px){.navbar-expand-xl{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-xl .navbar-nav{flex-direction:row}.navbar-expand-xl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xl .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-xl .navbar-nav-scroll{overflow:visible}.navbar-expand-xl .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-xl .navbar-toggler{display:none}.navbar-expand-xl .offcanvas-header{display:none}.navbar-expand-xl .offcanvas{position:inherit;bottom:0;z-index:1000;flex-grow:1;visibility:visible!important;background-color:transparent;border-right:0;border-left:0;transition:none;transform:none}.navbar-expand-xl .offcanvas-bottom,.navbar-expand-xl .offcanvas-top{height:auto;border-top:0;border-bottom:0}.navbar-expand-xl .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:1400px){.navbar-expand-xxl{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-xxl .navbar-nav{flex-direction:row}.navbar-expand-xxl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xxl .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-xxl .navbar-nav-scroll{overflow:visible}.navbar-expand-xxl .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-xxl .navbar-toggler{display:none}.navbar-expand-xxl .offcanvas-header{display:none}.navbar-expand-xxl .offcanvas{position:inherit;bottom:0;z-index:1000;flex-grow:1;visibility:visible!important;background-color:transparent;border-right:0;border-left:0;transition:none;transform:none}.navbar-expand-xxl .offcanvas-bottom,.navbar-expand-xxl .offcanvas-top{height:auto;border-top:0;border-bottom:0}.navbar-expand-xxl .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}.navbar-expand{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand .navbar-nav{flex-direction:row}.navbar-expand .navbar-nav .dropdown-menu{position:absolute}.navbar-expand .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand .navbar-nav-scroll{overflow:visible}.navbar-expand .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand .navbar-toggler{display:none}.navbar-expand .offcanvas-header{display:none}.navbar-expand .offcanvas{position:inherit;bottom:0;z-index:1000;flex-grow:1;visibility:visible!important;background-color:transparent;border-right:0;border-left:0;transition:none;transform:none}.navbar-expand .offcanvas-bottom,.navbar-expand .offcanvas-top{height:auto;border-top:0;border-bottom:0}.navbar-expand .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}.navbar-light .navbar-brand{color:rgba(0,0,0,.9)}.navbar-light .navbar-brand:focus,.navbar-light .navbar-brand:hover{color:rgba(0,0,0,.9)}.navbar-light .navbar-nav .nav-link{color:rgba(0,0,0,.55)}.navbar-light .navbar-nav .nav-link:focus,.navbar-light .navbar-nav .nav-link:hover{color:rgba(0,0,0,.7)}.navbar-light .navbar-nav .nav-link.disabled{color:rgba(0,0,0,.3)}.navbar-light .navbar-nav .nav-link.active,.navbar-light .navbar-nav .show>.nav-link{color:rgba(0,0,0,.9)}.navbar-light .navbar-toggler{color:rgba(0,0,0,.55);border-color:rgba(0,0,0,.1)}.navbar-light .navbar-toggler-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%280, 0, 0, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.navbar-light .navbar-text{color:rgba(0,0,0,.55)}.navbar-light .navbar-text a,.navbar-light .navbar-text a:focus,.navbar-light .navbar-text a:hover{color:rgba(0,0,0,.9)}.navbar-dark .navbar-brand{color:#fff}.navbar-dark .navbar-brand:focus,.navbar-dark .navbar-brand:hover{color:#fff}.navbar-dark .navbar-nav .nav-link{color:rgba(255,255,255,.55)}.navbar-dark .navbar-nav .nav-link:focus,.navbar-dark .navbar-nav .nav-link:hover{color:rgba(255,255,255,.75)}.navbar-dark .navbar-nav .nav-link.disabled{color:rgba(255,255,255,.25)}.navbar-dark .navbar-nav .nav-link.active,.navbar-dark .navbar-nav .show>.nav-link{color:#fff}.navbar-dark .navbar-toggler{color:rgba(255,255,255,.55);border-color:rgba(255,255,255,.1)}.navbar-dark .navbar-toggler-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.navbar-dark .navbar-text{color:rgba(255,255,255,.55)}.navbar-dark .navbar-text a,.navbar-dark .navbar-text a:focus,.navbar-dark .navbar-text a:hover{color:#fff}.card{position:relative;display:flex;flex-direction:column;min-width:0;word-wrap:break-word;background-color:#fff;background-clip:border-box;border:1px solid rgba(0,0,0,.125);border-radius:.25rem}.card>hr{margin-right:0;margin-left:0}.card>.list-group{border-top:inherit;border-bottom:inherit}.card>.list-group:first-child{border-top-width:0;border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.card>.list-group:last-child{border-bottom-width:0;border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.card>.card-header+.list-group,.card>.list-group+.card-footer{border-top:0}.card-body{flex:1 1 auto;padding:1rem 1rem}.card-title{margin-bottom:.5rem}.card-subtitle{margin-top:-.25rem;margin-bottom:0}.card-text:last-child{margin-bottom:0}.card-link+.card-link{margin-left:1rem}.card-header{padding:.5rem 1rem;margin-bottom:0;background-color:rgba(0,0,0,.03);border-bottom:1px solid rgba(0,0,0,.125)}.card-header:first-child{border-radius:calc(.25rem - 1px) calc(.25rem - 1px) 0 0}.card-footer{padding:.5rem 1rem;background-color:rgba(0,0,0,.03);border-top:1px solid rgba(0,0,0,.125)}.card-footer:last-child{border-radius:0 0 calc(.25rem - 1px) calc(.25rem - 1px)}.card-header-tabs{margin-right:-.5rem;margin-bottom:-.5rem;margin-left:-.5rem;border-bottom:0}.card-header-pills{margin-right:-.5rem;margin-left:-.5rem}.card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:1rem;border-radius:calc(.25rem - 1px)}.card-img,.card-img-bottom,.card-img-top{width:100%}.card-img,.card-img-top{border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.card-img,.card-img-bottom{border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.card-group>.card{margin-bottom:.75rem}@media (min-width:576px){.card-group{display:flex;flex-flow:row wrap}.card-group>.card{flex:1 0 0%;margin-bottom:0}.card-group>.card+.card{margin-left:0;border-left:0}.card-group>.card:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.card-group>.card:not(:last-child) .card-header,.card-group>.card:not(:last-child) .card-img-top{border-top-right-radius:0}.card-group>.card:not(:last-child) .card-footer,.card-group>.card:not(:last-child) .card-img-bottom{border-bottom-right-radius:0}.card-group>.card:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.card-group>.card:not(:first-child) .card-header,.card-group>.card:not(:first-child) .card-img-top{border-top-left-radius:0}.card-group>.card:not(:first-child) .card-footer,.card-group>.card:not(:first-child) .card-img-bottom{border-bottom-left-radius:0}}.accordion-button{position:relative;display:flex;align-items:center;width:100%;padding:1rem 1.25rem;font-size:1rem;color:#212529;text-align:left;background-color:#fff;border:0;border-radius:0;overflow-anchor:none;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out,border-radius .15s ease}@media (prefers-reduced-motion:reduce){.accordion-button{transition:none}}.accordion-button:not(.collapsed){color:#0c63e4;background-color:#e7f1ff;box-shadow:inset 0 -1px 0 rgba(0,0,0,.125)}.accordion-button:not(.collapsed)::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%230c63e4'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");transform:rotate(-180deg)}.accordion-button::after{flex-shrink:0;width:1.25rem;height:1.25rem;margin-left:auto;content:"";background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23212529'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");background-repeat:no-repeat;background-size:1.25rem;transition:transform .2s ease-in-out}@media (prefers-reduced-motion:reduce){.accordion-button::after{transition:none}}.accordion-button:hover{z-index:2}.accordion-button:focus{z-index:3;border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.accordion-header{margin-bottom:0}.accordion-item{background-color:#fff;border:1px solid rgba(0,0,0,.125)}.accordion-item:first-of-type{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.accordion-item:first-of-type .accordion-button{border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.accordion-item:not(:first-of-type){border-top:0}.accordion-item:last-of-type{border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.accordion-item:last-of-type .accordion-button.collapsed{border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.accordion-item:last-of-type .accordion-collapse{border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.accordion-body{padding:1rem 1.25rem}.accordion-flush .accordion-collapse{border-width:0}.accordion-flush .accordion-item{border-right:0;border-left:0;border-radius:0}.accordion-flush .accordion-item:first-child{border-top:0}.accordion-flush .accordion-item:last-child{border-bottom:0}.accordion-flush .accordion-item .accordion-button{border-radius:0}.breadcrumb{display:flex;flex-wrap:wrap;padding:0 0;margin-bottom:1rem;list-style:none}.breadcrumb-item+.breadcrumb-item{padding-left:.5rem}.breadcrumb-item+.breadcrumb-item::before{float:left;padding-right:.5rem;color:#6c757d;content:var(--bs-breadcrumb-divider, "/")}.breadcrumb-item.active{color:#6c757d}.pagination{display:flex;padding-left:0;list-style:none}.page-link{position:relative;display:block;color:#0d6efd;text-decoration:none;background-color:#fff;border:1px solid #dee2e6;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.page-link{transition:none}}.page-link:hover{z-index:2;color:#0a58ca;background-color:#e9ecef;border-color:#dee2e6}.page-link:focus{z-index:3;color:#0a58ca;background-color:#e9ecef;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.page-item:not(:first-child) .page-link{margin-left:-1px}.page-item.active .page-link{z-index:3;color:#fff;background-color:#0d6efd;border-color:#0d6efd}.page-item.disabled .page-link{color:#6c757d;pointer-events:none;background-color:#fff;border-color:#dee2e6}.page-link{padding:.375rem .75rem}.page-item:first-child .page-link{border-top-left-radius:.25rem;border-bottom-left-radius:.25rem}.page-item:last-child .page-link{border-top-right-radius:.25rem;border-bottom-right-radius:.25rem}.pagination-lg .page-link{padding:.75rem 1.5rem;font-size:1.25rem}.pagination-lg .page-item:first-child .page-link{border-top-left-radius:.3rem;border-bottom-left-radius:.3rem}.pagination-lg .page-item:last-child .page-link{border-top-right-radius:.3rem;border-bottom-right-radius:.3rem}.pagination-sm .page-link{padding:.25rem .5rem;font-size:.875rem}.pagination-sm .page-item:first-child .page-link{border-top-left-radius:.2rem;border-bottom-left-radius:.2rem}.pagination-sm .page-item:last-child .page-link{border-top-right-radius:.2rem;border-bottom-right-radius:.2rem}.badge{display:inline-block;padding:.35em .65em;font-size:.75em;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25rem}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.alert{position:relative;padding:1rem 1rem;margin-bottom:1rem;border:1px solid transparent;border-radius:.25rem}.alert-heading{color:inherit}.alert-link{font-weight:700}.alert-dismissible{padding-right:3rem}.alert-dismissible .btn-close{position:absolute;top:0;right:0;z-index:2;padding:1.25rem 1rem}.alert-primary{color:#084298;background-color:#cfe2ff;border-color:#b6d4fe}.alert-primary .alert-link{color:#06357a}.alert-secondary{color:#41464b;background-color:#e2e3e5;border-color:#d3d6d8}.alert-secondary .alert-link{color:#34383c}.alert-success{color:#0f5132;background-color:#d1e7dd;border-color:#badbcc}.alert-success .alert-link{color:#0c4128}.alert-info{color:#055160;background-color:#cff4fc;border-color:#b6effb}.alert-info .alert-link{color:#04414d}.alert-warning{color:#664d03;background-color:#fff3cd;border-color:#ffecb5}.alert-warning .alert-link{color:#523e02}.alert-danger{color:#842029;background-color:#f8d7da;border-color:#f5c2c7}.alert-danger .alert-link{color:#6a1a21}.alert-light{color:#636464;background-color:#fefefe;border-color:#fdfdfe}.alert-light .alert-link{color:#4f5050}.alert-dark{color:#141619;background-color:#d3d3d4;border-color:#bcbebf}.alert-dark .alert-link{color:#101214}@-webkit-keyframes progress-bar-stripes{0%{background-position-x:1rem}}@keyframes progress-bar-stripes{0%{background-position-x:1rem}}.progress{display:flex;height:1rem;overflow:hidden;font-size:.75rem;background-color:#e9ecef;border-radius:.25rem}.progress-bar{display:flex;flex-direction:column;justify-content:center;overflow:hidden;color:#fff;text-align:center;white-space:nowrap;background-color:#0d6efd;transition:width .6s ease}@media (prefers-reduced-motion:reduce){.progress-bar{transition:none}}.progress-bar-striped{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-size:1rem 1rem}.progress-bar-animated{-webkit-animation:1s linear infinite progress-bar-stripes;animation:1s linear infinite progress-bar-stripes}@media (prefers-reduced-motion:reduce){.progress-bar-animated{-webkit-animation:none;animation:none}}.list-group{display:flex;flex-direction:column;padding-left:0;margin-bottom:0;border-radius:.25rem}.list-group-numbered{list-style-type:none;counter-reset:section}.list-group-numbered>li::before{content:counters(section, ".") ". ";counter-increment:section}.list-group-item-action{width:100%;color:#495057;text-align:inherit}.list-group-item-action:focus,.list-group-item-action:hover{z-index:1;color:#495057;text-decoration:none;background-color:#f8f9fa}.list-group-item-action:active{color:#212529;background-color:#e9ecef}.list-group-item{position:relative;display:block;padding:.5rem 1rem;color:#212529;text-decoration:none;background-color:#fff;border:1px solid rgba(0,0,0,.125)}.list-group-item:first-child{border-top-left-radius:inherit;border-top-right-radius:inherit}.list-group-item:last-child{border-bottom-right-radius:inherit;border-bottom-left-radius:inherit}.list-group-item.disabled,.list-group-item:disabled{color:#6c757d;pointer-events:none;background-color:#fff}.list-group-item.active{z-index:2;color:#fff;background-color:#0d6efd;border-color:#0d6efd}.list-group-item+.list-group-item{border-top-width:0}.list-group-item+.list-group-item.active{margin-top:-1px;border-top-width:1px}.list-group-horizontal{flex-direction:row}.list-group-horizontal>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal>.list-group-item.active{margin-top:0}.list-group-horizontal>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}@media (min-width:576px){.list-group-horizontal-sm{flex-direction:row}.list-group-horizontal-sm>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-sm>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-sm>.list-group-item.active{margin-top:0}.list-group-horizontal-sm>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-sm>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:768px){.list-group-horizontal-md{flex-direction:row}.list-group-horizontal-md>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-md>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-md>.list-group-item.active{margin-top:0}.list-group-horizontal-md>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-md>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:992px){.list-group-horizontal-lg{flex-direction:row}.list-group-horizontal-lg>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-lg>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-lg>.list-group-item.active{margin-top:0}.list-group-horizontal-lg>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-lg>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:1200px){.list-group-horizontal-xl{flex-direction:row}.list-group-horizontal-xl>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-xl>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-xl>.list-group-item.active{margin-top:0}.list-group-horizontal-xl>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-xl>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:1400px){.list-group-horizontal-xxl{flex-direction:row}.list-group-horizontal-xxl>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-xxl>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-xxl>.list-group-item.active{margin-top:0}.list-group-horizontal-xxl>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-xxl>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}.list-group-flush{border-radius:0}.list-group-flush>.list-group-item{border-width:0 0 1px}.list-group-flush>.list-group-item:last-child{border-bottom-width:0}.list-group-item-primary{color:#084298;background-color:#cfe2ff}.list-group-item-primary.list-group-item-action:focus,.list-group-item-primary.list-group-item-action:hover{color:#084298;background-color:#bacbe6}.list-group-item-primary.list-group-item-action.active{color:#fff;background-color:#084298;border-color:#084298}.list-group-item-secondary{color:#41464b;background-color:#e2e3e5}.list-group-item-secondary.list-group-item-action:focus,.list-group-item-secondary.list-group-item-action:hover{color:#41464b;background-color:#cbccce}.list-group-item-secondary.list-group-item-action.active{color:#fff;background-color:#41464b;border-color:#41464b}.list-group-item-success{color:#0f5132;background-color:#d1e7dd}.list-group-item-success.list-group-item-action:focus,.list-group-item-success.list-group-item-action:hover{color:#0f5132;background-color:#bcd0c7}.list-group-item-success.list-group-item-action.active{color:#fff;background-color:#0f5132;border-color:#0f5132}.list-group-item-info{color:#055160;background-color:#cff4fc}.list-group-item-info.list-group-item-action:focus,.list-group-item-info.list-group-item-action:hover{color:#055160;background-color:#badce3}.list-group-item-info.list-group-item-action.active{color:#fff;background-color:#055160;border-color:#055160}.list-group-item-warning{color:#664d03;background-color:#fff3cd}.list-group-item-warning.list-group-item-action:focus,.list-group-item-warning.list-group-item-action:hover{color:#664d03;background-color:#e6dbb9}.list-group-item-warning.list-group-item-action.active{color:#fff;background-color:#664d03;border-color:#664d03}.list-group-item-danger{color:#842029;background-color:#f8d7da}.list-group-item-danger.list-group-item-action:focus,.list-group-item-danger.list-group-item-action:hover{color:#842029;background-color:#dfc2c4}.list-group-item-danger.list-group-item-action.active{color:#fff;background-color:#842029;border-color:#842029}.list-group-item-light{color:#636464;background-color:#fefefe}.list-group-item-light.list-group-item-action:focus,.list-group-item-light.list-group-item-action:hover{color:#636464;background-color:#e5e5e5}.list-group-item-light.list-group-item-action.active{color:#fff;background-color:#636464;border-color:#636464}.list-group-item-dark{color:#141619;background-color:#d3d3d4}.list-group-item-dark.list-group-item-action:focus,.list-group-item-dark.list-group-item-action:hover{color:#141619;background-color:#bebebf}.list-group-item-dark.list-group-item-action.active{color:#fff;background-color:#141619;border-color:#141619}.btn-close{box-sizing:content-box;width:1em;height:1em;padding:.25em .25em;color:#000;background:transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'%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") center/1em auto no-repeat;border:0;border-radius:.25rem;opacity:.5}.btn-close:hover{color:#000;text-decoration:none;opacity:.75}.btn-close:focus{outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25);opacity:1}.btn-close.disabled,.btn-close:disabled{pointer-events:none;-webkit-user-select:none;-moz-user-select:none;user-select:none;opacity:.25}.btn-close-white{filter:invert(1) grayscale(100%) brightness(200%)}.toast{width:350px;max-width:100%;font-size:.875rem;pointer-events:auto;background-color:rgba(255,255,255,.85);background-clip:padding-box;border:1px solid rgba(0,0,0,.1);box-shadow:0 .5rem 1rem rgba(0,0,0,.15);border-radius:.25rem}.toast.showing{opacity:0}.toast:not(.show){display:none}.toast-container{width:-webkit-max-content;width:-moz-max-content;width:max-content;max-width:100%;pointer-events:none}.toast-container>:not(:last-child){margin-bottom:.75rem}.toast-header{display:flex;align-items:center;padding:.5rem .75rem;color:#6c757d;background-color:rgba(255,255,255,.85);background-clip:padding-box;border-bottom:1px solid rgba(0,0,0,.05);border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.toast-header .btn-close{margin-right:-.375rem;margin-left:.75rem}.toast-body{padding:.75rem;word-wrap:break-word}.modal{position:fixed;top:0;left:0;z-index:1055;display:none;width:100%;height:100%;overflow-x:hidden;overflow-y:auto;outline:0}.modal-dialog{position:relative;width:auto;margin:.5rem;pointer-events:none}.modal.fade .modal-dialog{transition:transform .3s ease-out;transform:translate(0,-50px)}@media (prefers-reduced-motion:reduce){.modal.fade .modal-dialog{transition:none}}.modal.show .modal-dialog{transform:none}.modal.modal-static .modal-dialog{transform:scale(1.02)}.modal-dialog-scrollable{height:calc(100% - 1rem)}.modal-dialog-scrollable .modal-content{max-height:100%;overflow:hidden}.modal-dialog-scrollable .modal-body{overflow-y:auto}.modal-dialog-centered{display:flex;align-items:center;min-height:calc(100% - 1rem)}.modal-content{position:relative;display:flex;flex-direction:column;width:100%;pointer-events:auto;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem;outline:0}.modal-backdrop{position:fixed;top:0;left:0;z-index:1050;width:100vw;height:100vh;background-color:#000}.modal-backdrop.fade{opacity:0}.modal-backdrop.show{opacity:.5}.modal-header{display:flex;flex-shrink:0;align-items:center;justify-content:space-between;padding:1rem 1rem;border-bottom:1px solid #dee2e6;border-top-left-radius:calc(.3rem - 1px);border-top-right-radius:calc(.3rem - 1px)}.modal-header .btn-close{padding:.5rem .5rem;margin:-.5rem -.5rem -.5rem auto}.modal-title{margin-bottom:0;line-height:1.5}.modal-body{position:relative;flex:1 1 auto;padding:1rem}.modal-footer{display:flex;flex-wrap:wrap;flex-shrink:0;align-items:center;justify-content:flex-end;padding:.75rem;border-top:1px solid #dee2e6;border-bottom-right-radius:calc(.3rem - 1px);border-bottom-left-radius:calc(.3rem - 1px)}.modal-footer>*{margin:.25rem}@media (min-width:576px){.modal-dialog{max-width:500px;margin:1.75rem auto}.modal-dialog-scrollable{height:calc(100% - 3.5rem)}.modal-dialog-centered{min-height:calc(100% - 3.5rem)}.modal-sm{max-width:300px}}@media (min-width:992px){.modal-lg,.modal-xl{max-width:800px}}@media (min-width:1200px){.modal-xl{max-width:1140px}}.modal-fullscreen{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen .modal-header{border-radius:0}.modal-fullscreen .modal-body{overflow-y:auto}.modal-fullscreen .modal-footer{border-radius:0}@media (max-width:575.98px){.modal-fullscreen-sm-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-sm-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-sm-down .modal-header{border-radius:0}.modal-fullscreen-sm-down .modal-body{overflow-y:auto}.modal-fullscreen-sm-down .modal-footer{border-radius:0}}@media (max-width:767.98px){.modal-fullscreen-md-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-md-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-md-down .modal-header{border-radius:0}.modal-fullscreen-md-down .modal-body{overflow-y:auto}.modal-fullscreen-md-down .modal-footer{border-radius:0}}@media (max-width:991.98px){.modal-fullscreen-lg-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-lg-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-lg-down .modal-header{border-radius:0}.modal-fullscreen-lg-down .modal-body{overflow-y:auto}.modal-fullscreen-lg-down .modal-footer{border-radius:0}}@media (max-width:1199.98px){.modal-fullscreen-xl-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-xl-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-xl-down .modal-header{border-radius:0}.modal-fullscreen-xl-down .modal-body{overflow-y:auto}.modal-fullscreen-xl-down .modal-footer{border-radius:0}}@media (max-width:1399.98px){.modal-fullscreen-xxl-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-xxl-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-xxl-down .modal-header{border-radius:0}.modal-fullscreen-xxl-down .modal-body{overflow-y:auto}.modal-fullscreen-xxl-down .modal-footer{border-radius:0}}.tooltip{position:absolute;z-index:1080;display:block;margin:0;font-family:var(--bs-font-sans-serif);font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;opacity:0}.tooltip.show{opacity:.9}.tooltip .tooltip-arrow{position:absolute;display:block;width:.8rem;height:.4rem}.tooltip .tooltip-arrow::before{position:absolute;content:"";border-color:transparent;border-style:solid}.bs-tooltip-auto[data-popper-placement^=top],.bs-tooltip-top{padding:.4rem 0}.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow,.bs-tooltip-top .tooltip-arrow{bottom:0}.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow::before,.bs-tooltip-top .tooltip-arrow::before{top:-1px;border-width:.4rem .4rem 0;border-top-color:#000}.bs-tooltip-auto[data-popper-placement^=right],.bs-tooltip-end{padding:0 .4rem}.bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow,.bs-tooltip-end .tooltip-arrow{left:0;width:.4rem;height:.8rem}.bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow::before,.bs-tooltip-end .tooltip-arrow::before{right:-1px;border-width:.4rem .4rem .4rem 0;border-right-color:#000}.bs-tooltip-auto[data-popper-placement^=bottom],.bs-tooltip-bottom{padding:.4rem 0}.bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow,.bs-tooltip-bottom .tooltip-arrow{top:0}.bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow::before,.bs-tooltip-bottom .tooltip-arrow::before{bottom:-1px;border-width:0 .4rem .4rem;border-bottom-color:#000}.bs-tooltip-auto[data-popper-placement^=left],.bs-tooltip-start{padding:0 .4rem}.bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow,.bs-tooltip-start .tooltip-arrow{right:0;width:.4rem;height:.8rem}.bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow::before,.bs-tooltip-start .tooltip-arrow::before{left:-1px;border-width:.4rem 0 .4rem .4rem;border-left-color:#000}.tooltip-inner{max-width:200px;padding:.25rem .5rem;color:#fff;text-align:center;background-color:#000;border-radius:.25rem}.popover{position:absolute;top:0;left:0;z-index:1070;display:block;max-width:276px;font-family:var(--bs-font-sans-serif);font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem}.popover .popover-arrow{position:absolute;display:block;width:1rem;height:.5rem}.popover .popover-arrow::after,.popover .popover-arrow::before{position:absolute;display:block;content:"";border-color:transparent;border-style:solid}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow,.bs-popover-top>.popover-arrow{bottom:calc(-.5rem - 1px)}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::before,.bs-popover-top>.popover-arrow::before{bottom:0;border-width:.5rem .5rem 0;border-top-color:rgba(0,0,0,.25)}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::after,.bs-popover-top>.popover-arrow::after{bottom:1px;border-width:.5rem .5rem 0;border-top-color:#fff}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow,.bs-popover-end>.popover-arrow{left:calc(-.5rem - 1px);width:.5rem;height:1rem}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::before,.bs-popover-end>.popover-arrow::before{left:0;border-width:.5rem .5rem .5rem 0;border-right-color:rgba(0,0,0,.25)}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::after,.bs-popover-end>.popover-arrow::after{left:1px;border-width:.5rem .5rem .5rem 0;border-right-color:#fff}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow,.bs-popover-bottom>.popover-arrow{top:calc(-.5rem - 1px)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::before,.bs-popover-bottom>.popover-arrow::before{top:0;border-width:0 .5rem .5rem .5rem;border-bottom-color:rgba(0,0,0,.25)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::after,.bs-popover-bottom>.popover-arrow::after{top:1px;border-width:0 .5rem .5rem .5rem;border-bottom-color:#fff}.bs-popover-auto[data-popper-placement^=bottom] .popover-header::before,.bs-popover-bottom .popover-header::before{position:absolute;top:0;left:50%;display:block;width:1rem;margin-left:-.5rem;content:"";border-bottom:1px solid #f0f0f0}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow,.bs-popover-start>.popover-arrow{right:calc(-.5rem - 1px);width:.5rem;height:1rem}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::before,.bs-popover-start>.popover-arrow::before{right:0;border-width:.5rem 0 .5rem .5rem;border-left-color:rgba(0,0,0,.25)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::after,.bs-popover-start>.popover-arrow::after{right:1px;border-width:.5rem 0 .5rem .5rem;border-left-color:#fff}.popover-header{padding:.5rem 1rem;margin-bottom:0;font-size:1rem;background-color:#f0f0f0;border-bottom:1px solid rgba(0,0,0,.2);border-top-left-radius:calc(.3rem - 1px);border-top-right-radius:calc(.3rem - 1px)}.popover-header:empty{display:none}.popover-body{padding:1rem 1rem;color:#212529}.carousel{position:relative}.carousel.pointer-event{touch-action:pan-y}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner::after{display:block;clear:both;content:""}.carousel-item{position:relative;display:none;float:left;width:100%;margin-right:-100%;-webkit-backface-visibility:hidden;backface-visibility:hidden;transition:transform .6s ease-in-out}@media (prefers-reduced-motion:reduce){.carousel-item{transition:none}}.carousel-item-next,.carousel-item-prev,.carousel-item.active{display:block}.active.carousel-item-end,.carousel-item-next:not(.carousel-item-start){transform:translateX(100%)}.active.carousel-item-start,.carousel-item-prev:not(.carousel-item-end){transform:translateX(-100%)}.carousel-fade .carousel-item{opacity:0;transition-property:opacity;transform:none}.carousel-fade .carousel-item-next.carousel-item-start,.carousel-fade .carousel-item-prev.carousel-item-end,.carousel-fade .carousel-item.active{z-index:1;opacity:1}.carousel-fade .active.carousel-item-end,.carousel-fade .active.carousel-item-start{z-index:0;opacity:0;transition:opacity 0s .6s}@media (prefers-reduced-motion:reduce){.carousel-fade .active.carousel-item-end,.carousel-fade .active.carousel-item-start{transition:none}}.carousel-control-next,.carousel-control-prev{position:absolute;top:0;bottom:0;z-index:1;display:flex;align-items:center;justify-content:center;width:15%;padding:0;color:#fff;text-align:center;background:0 0;border:0;opacity:.5;transition:opacity .15s ease}@media (prefers-reduced-motion:reduce){.carousel-control-next,.carousel-control-prev{transition:none}}.carousel-control-next:focus,.carousel-control-next:hover,.carousel-control-prev:focus,.carousel-control-prev:hover{color:#fff;text-decoration:none;outline:0;opacity:.9}.carousel-control-prev{left:0}.carousel-control-next{right:0}.carousel-control-next-icon,.carousel-control-prev-icon{display:inline-block;width:2rem;height:2rem;background-repeat:no-repeat;background-position:50%;background-size:100% 100%}.carousel-control-prev-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z'/%3e%3c/svg%3e")}.carousel-control-next-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e")}.carousel-indicators{position:absolute;right:0;bottom:0;left:0;z-index:2;display:flex;justify-content:center;padding:0;margin-right:15%;margin-bottom:1rem;margin-left:15%;list-style:none}.carousel-indicators [data-bs-target]{box-sizing:content-box;flex:0 1 auto;width:30px;height:3px;padding:0;margin-right:3px;margin-left:3px;text-indent:-999px;cursor:pointer;background-color:#fff;background-clip:padding-box;border:0;border-top:10px solid transparent;border-bottom:10px solid transparent;opacity:.5;transition:opacity .6s ease}@media (prefers-reduced-motion:reduce){.carousel-indicators [data-bs-target]{transition:none}}.carousel-indicators .active{opacity:1}.carousel-caption{position:absolute;right:15%;bottom:1.25rem;left:15%;padding-top:1.25rem;padding-bottom:1.25rem;color:#fff;text-align:center}.carousel-dark .carousel-control-next-icon,.carousel-dark .carousel-control-prev-icon{filter:invert(1) grayscale(100)}.carousel-dark .carousel-indicators [data-bs-target]{background-color:#000}.carousel-dark .carousel-caption{color:#000}@-webkit-keyframes spinner-border{to{transform:rotate(360deg)}}@keyframes spinner-border{to{transform:rotate(360deg)}}.spinner-border{display:inline-block;width:2rem;height:2rem;vertical-align:-.125em;border:.25em solid currentColor;border-right-color:transparent;border-radius:50%;-webkit-animation:.75s linear infinite spinner-border;animation:.75s linear infinite spinner-border}.spinner-border-sm{width:1rem;height:1rem;border-width:.2em}@-webkit-keyframes spinner-grow{0%{transform:scale(0)}50%{opacity:1;transform:none}}@keyframes spinner-grow{0%{transform:scale(0)}50%{opacity:1;transform:none}}.spinner-grow{display:inline-block;width:2rem;height:2rem;vertical-align:-.125em;background-color:currentColor;border-radius:50%;opacity:0;-webkit-animation:.75s linear infinite spinner-grow;animation:.75s linear infinite spinner-grow}.spinner-grow-sm{width:1rem;height:1rem}@media (prefers-reduced-motion:reduce){.spinner-border,.spinner-grow{-webkit-animation-duration:1.5s;animation-duration:1.5s}}.offcanvas{position:fixed;bottom:0;z-index:1045;display:flex;flex-direction:column;max-width:100%;visibility:hidden;background-color:#fff;background-clip:padding-box;outline:0;transition:transform .3s ease-in-out}@media (prefers-reduced-motion:reduce){.offcanvas{transition:none}}.offcanvas-backdrop{position:fixed;top:0;left:0;z-index:1040;width:100vw;height:100vh;background-color:#000}.offcanvas-backdrop.fade{opacity:0}.offcanvas-backdrop.show{opacity:.5}.offcanvas-header{display:flex;align-items:center;justify-content:space-between;padding:1rem 1rem}.offcanvas-header .btn-close{padding:.5rem .5rem;margin-top:-.5rem;margin-right:-.5rem;margin-bottom:-.5rem}.offcanvas-title{margin-bottom:0;line-height:1.5}.offcanvas-body{flex-grow:1;padding:1rem 1rem;overflow-y:auto}.offcanvas-start{top:0;left:0;width:400px;border-right:1px solid rgba(0,0,0,.2);transform:translateX(-100%)}.offcanvas-end{top:0;right:0;width:400px;border-left:1px solid rgba(0,0,0,.2);transform:translateX(100%)}.offcanvas-top{top:0;right:0;left:0;height:30vh;max-height:100%;border-bottom:1px solid rgba(0,0,0,.2);transform:translateY(-100%)}.offcanvas-bottom{right:0;left:0;height:30vh;max-height:100%;border-top:1px solid rgba(0,0,0,.2);transform:translateY(100%)}.offcanvas.show{transform:none}.placeholder{display:inline-block;min-height:1em;vertical-align:middle;cursor:wait;background-color:currentColor;opacity:.5}.placeholder.btn::before{display:inline-block;content:""}.placeholder-xs{min-height:.6em}.placeholder-sm{min-height:.8em}.placeholder-lg{min-height:1.2em}.placeholder-glow .placeholder{-webkit-animation:placeholder-glow 2s ease-in-out infinite;animation:placeholder-glow 2s ease-in-out infinite}@-webkit-keyframes placeholder-glow{50%{opacity:.2}}@keyframes placeholder-glow{50%{opacity:.2}}.placeholder-wave{-webkit-mask-image:linear-gradient(130deg,#000 55%,rgba(0,0,0,0.8) 75%,#000 95%);mask-image:linear-gradient(130deg,#000 55%,rgba(0,0,0,0.8) 75%,#000 95%);-webkit-mask-size:200% 100%;mask-size:200% 100%;-webkit-animation:placeholder-wave 2s linear infinite;animation:placeholder-wave 2s linear infinite}@-webkit-keyframes placeholder-wave{100%{-webkit-mask-position:-200% 0%;mask-position:-200% 0%}}@keyframes placeholder-wave{100%{-webkit-mask-position:-200% 0%;mask-position:-200% 0%}}.clearfix::after{display:block;clear:both;content:""}.link-primary{color:#0d6efd}.link-primary:focus,.link-primary:hover{color:#0a58ca}.link-secondary{color:#6c757d}.link-secondary:focus,.link-secondary:hover{color:#565e64}.link-success{color:#198754}.link-success:focus,.link-success:hover{color:#146c43}.link-info{color:#0dcaf0}.link-info:focus,.link-info:hover{color:#3dd5f3}.link-warning{color:#ffc107}.link-warning:focus,.link-warning:hover{color:#ffcd39}.link-danger{color:#dc3545}.link-danger:focus,.link-danger:hover{color:#b02a37}.link-light{color:#f8f9fa}.link-light:focus,.link-light:hover{color:#f9fafb}.link-dark{color:#212529}.link-dark:focus,.link-dark:hover{color:#1a1e21}.ratio{position:relative;width:100%}.ratio::before{display:block;padding-top:var(--bs-aspect-ratio);content:""}.ratio>*{position:absolute;top:0;left:0;width:100%;height:100%}.ratio-1x1{--bs-aspect-ratio:100%}.ratio-4x3{--bs-aspect-ratio:75%}.ratio-16x9{--bs-aspect-ratio:56.25%}.ratio-21x9{--bs-aspect-ratio:42.8571428571%}.fixed-top{position:fixed;top:0;right:0;left:0;z-index:1030}.fixed-bottom{position:fixed;right:0;bottom:0;left:0;z-index:1030}.sticky-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}@media (min-width:576px){.sticky-sm-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}@media (min-width:768px){.sticky-md-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}@media (min-width:992px){.sticky-lg-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}@media (min-width:1200px){.sticky-xl-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}@media (min-width:1400px){.sticky-xxl-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}.hstack{display:flex;flex-direction:row;align-items:center;align-self:stretch}.vstack{display:flex;flex:1 1 auto;flex-direction:column;align-self:stretch}.visually-hidden,.visually-hidden-focusable:not(:focus):not(:focus-within){position:absolute!important;width:1px!important;height:1px!important;padding:0!important;margin:-1px!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;white-space:nowrap!important;border:0!important}.stretched-link::after{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;content:""}.text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.vr{display:inline-block;align-self:stretch;width:1px;min-height:1em;background-color:currentColor;opacity:.25}.align-baseline{vertical-align:baseline!important}.align-top{vertical-align:top!important}.align-middle{vertical-align:middle!important}.align-bottom{vertical-align:bottom!important}.align-text-bottom{vertical-align:text-bottom!important}.align-text-top{vertical-align:text-top!important}.float-start{float:left!important}.float-end{float:right!important}.float-none{float:none!important}.opacity-0{opacity:0!important}.opacity-25{opacity:.25!important}.opacity-50{opacity:.5!important}.opacity-75{opacity:.75!important}.opacity-100{opacity:1!important}.overflow-auto{overflow:auto!important}.overflow-hidden{overflow:hidden!important}.overflow-visible{overflow:visible!important}.overflow-scroll{overflow:scroll!important}.d-inline{display:inline!important}.d-inline-block{display:inline-block!important}.d-block{display:block!important}.d-grid{display:grid!important}.d-table{display:table!important}.d-table-row{display:table-row!important}.d-table-cell{display:table-cell!important}.d-flex{display:flex!important}.d-inline-flex{display:inline-flex!important}.d-none{display:none!important}.shadow{box-shadow:0 .5rem 1rem rgba(0,0,0,.15)!important}.shadow-sm{box-shadow:0 .125rem .25rem rgba(0,0,0,.075)!important}.shadow-lg{box-shadow:0 1rem 3rem rgba(0,0,0,.175)!important}.shadow-none{box-shadow:none!important}.position-static{position:static!important}.position-relative{position:relative!important}.position-absolute{position:absolute!important}.position-fixed{position:fixed!important}.position-sticky{position:-webkit-sticky!important;position:sticky!important}.top-0{top:0!important}.top-50{top:50%!important}.top-100{top:100%!important}.bottom-0{bottom:0!important}.bottom-50{bottom:50%!important}.bottom-100{bottom:100%!important}.start-0{left:0!important}.start-50{left:50%!important}.start-100{left:100%!important}.end-0{right:0!important}.end-50{right:50%!important}.end-100{right:100%!important}.translate-middle{transform:translate(-50%,-50%)!important}.translate-middle-x{transform:translateX(-50%)!important}.translate-middle-y{transform:translateY(-50%)!important}.border{border:1px solid #dee2e6!important}.border-0{border:0!important}.border-top{border-top:1px solid #dee2e6!important}.border-top-0{border-top:0!important}.border-end{border-right:1px solid #dee2e6!important}.border-end-0{border-right:0!important}.border-bottom{border-bottom:1px solid #dee2e6!important}.border-bottom-0{border-bottom:0!important}.border-start{border-left:1px solid #dee2e6!important}.border-start-0{border-left:0!important}.border-primary{border-color:#0d6efd!important}.border-secondary{border-color:#6c757d!important}.border-success{border-color:#198754!important}.border-info{border-color:#0dcaf0!important}.border-warning{border-color:#ffc107!important}.border-danger{border-color:#dc3545!important}.border-light{border-color:#f8f9fa!important}.border-dark{border-color:#212529!important}.border-white{border-color:#fff!important}.border-1{border-width:1px!important}.border-2{border-width:2px!important}.border-3{border-width:3px!important}.border-4{border-width:4px!important}.border-5{border-width:5px!important}.w-25{width:25%!important}.w-50{width:50%!important}.w-75{width:75%!important}.w-100{width:100%!important}.w-auto{width:auto!important}.mw-100{max-width:100%!important}.vw-100{width:100vw!important}.min-vw-100{min-width:100vw!important}.h-25{height:25%!important}.h-50{height:50%!important}.h-75{height:75%!important}.h-100{height:100%!important}.h-auto{height:auto!important}.mh-100{max-height:100%!important}.vh-100{height:100vh!important}.min-vh-100{min-height:100vh!important}.flex-fill{flex:1 1 auto!important}.flex-row{flex-direction:row!important}.flex-column{flex-direction:column!important}.flex-row-reverse{flex-direction:row-reverse!important}.flex-column-reverse{flex-direction:column-reverse!important}.flex-grow-0{flex-grow:0!important}.flex-grow-1{flex-grow:1!important}.flex-shrink-0{flex-shrink:0!important}.flex-shrink-1{flex-shrink:1!important}.flex-wrap{flex-wrap:wrap!important}.flex-nowrap{flex-wrap:nowrap!important}.flex-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-0{gap:0!important}.gap-1{gap:.25rem!important}.gap-2{gap:.5rem!important}.gap-3{gap:1rem!important}.gap-4{gap:1.5rem!important}.gap-5{gap:3rem!important}.justify-content-start{justify-content:flex-start!important}.justify-content-end{justify-content:flex-end!important}.justify-content-center{justify-content:center!important}.justify-content-between{justify-content:space-between!important}.justify-content-around{justify-content:space-around!important}.justify-content-evenly{justify-content:space-evenly!important}.align-items-start{align-items:flex-start!important}.align-items-end{align-items:flex-end!important}.align-items-center{align-items:center!important}.align-items-baseline{align-items:baseline!important}.align-items-stretch{align-items:stretch!important}.align-content-start{align-content:flex-start!important}.align-content-end{align-content:flex-end!important}.align-content-center{align-content:center!important}.align-content-between{align-content:space-between!important}.align-content-around{align-content:space-around!important}.align-content-stretch{align-content:stretch!important}.align-self-auto{align-self:auto!important}.align-self-start{align-self:flex-start!important}.align-self-end{align-self:flex-end!important}.align-self-center{align-self:center!important}.align-self-baseline{align-self:baseline!important}.align-self-stretch{align-self:stretch!important}.order-first{order:-1!important}.order-0{order:0!important}.order-1{order:1!important}.order-2{order:2!important}.order-3{order:3!important}.order-4{order:4!important}.order-5{order:5!important}.order-last{order:6!important}.m-0{margin:0!important}.m-1{margin:.25rem!important}.m-2{margin:.5rem!important}.m-3{margin:1rem!important}.m-4{margin:1.5rem!important}.m-5{margin:3rem!important}.m-auto{margin:auto!important}.mx-0{margin-right:0!important;margin-left:0!important}.mx-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-3{margin-right:1rem!important;margin-left:1rem!important}.mx-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-5{margin-right:3rem!important;margin-left:3rem!important}.mx-auto{margin-right:auto!important;margin-left:auto!important}.my-0{margin-top:0!important;margin-bottom:0!important}.my-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-0{margin-top:0!important}.mt-1{margin-top:.25rem!important}.mt-2{margin-top:.5rem!important}.mt-3{margin-top:1rem!important}.mt-4{margin-top:1.5rem!important}.mt-5{margin-top:3rem!important}.mt-auto{margin-top:auto!important}.me-0{margin-right:0!important}.me-1{margin-right:.25rem!important}.me-2{margin-right:.5rem!important}.me-3{margin-right:1rem!important}.me-4{margin-right:1.5rem!important}.me-5{margin-right:3rem!important}.me-auto{margin-right:auto!important}.mb-0{margin-bottom:0!important}.mb-1{margin-bottom:.25rem!important}.mb-2{margin-bottom:.5rem!important}.mb-3{margin-bottom:1rem!important}.mb-4{margin-bottom:1.5rem!important}.mb-5{margin-bottom:3rem!important}.mb-auto{margin-bottom:auto!important}.ms-0{margin-left:0!important}.ms-1{margin-left:.25rem!important}.ms-2{margin-left:.5rem!important}.ms-3{margin-left:1rem!important}.ms-4{margin-left:1.5rem!important}.ms-5{margin-left:3rem!important}.ms-auto{margin-left:auto!important}.p-0{padding:0!important}.p-1{padding:.25rem!important}.p-2{padding:.5rem!important}.p-3{padding:1rem!important}.p-4{padding:1.5rem!important}.p-5{padding:3rem!important}.px-0{padding-right:0!important;padding-left:0!important}.px-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-3{padding-right:1rem!important;padding-left:1rem!important}.px-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-5{padding-right:3rem!important;padding-left:3rem!important}.py-0{padding-top:0!important;padding-bottom:0!important}.py-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-0{padding-top:0!important}.pt-1{padding-top:.25rem!important}.pt-2{padding-top:.5rem!important}.pt-3{padding-top:1rem!important}.pt-4{padding-top:1.5rem!important}.pt-5{padding-top:3rem!important}.pe-0{padding-right:0!important}.pe-1{padding-right:.25rem!important}.pe-2{padding-right:.5rem!important}.pe-3{padding-right:1rem!important}.pe-4{padding-right:1.5rem!important}.pe-5{padding-right:3rem!important}.pb-0{padding-bottom:0!important}.pb-1{padding-bottom:.25rem!important}.pb-2{padding-bottom:.5rem!important}.pb-3{padding-bottom:1rem!important}.pb-4{padding-bottom:1.5rem!important}.pb-5{padding-bottom:3rem!important}.ps-0{padding-left:0!important}.ps-1{padding-left:.25rem!important}.ps-2{padding-left:.5rem!important}.ps-3{padding-left:1rem!important}.ps-4{padding-left:1.5rem!important}.ps-5{padding-left:3rem!important}.font-monospace{font-family:var(--bs-font-monospace)!important}.fs-1{font-size:calc(1.375rem + 1.5vw)!important}.fs-2{font-size:calc(1.325rem + .9vw)!important}.fs-3{font-size:calc(1.3rem + .6vw)!important}.fs-4{font-size:calc(1.275rem + .3vw)!important}.fs-5{font-size:1.25rem!important}.fs-6{font-size:1rem!important}.fst-italic{font-style:italic!important}.fst-normal{font-style:normal!important}.fw-light{font-weight:300!important}.fw-lighter{font-weight:lighter!important}.fw-normal{font-weight:400!important}.fw-bold{font-weight:700!important}.fw-bolder{font-weight:bolder!important}.lh-1{line-height:1!important}.lh-sm{line-height:1.25!important}.lh-base{line-height:1.5!important}.lh-lg{line-height:2!important}.text-start{text-align:left!important}.text-end{text-align:right!important}.text-center{text-align:center!important}.text-decoration-none{text-decoration:none!important}.text-decoration-underline{text-decoration:underline!important}.text-decoration-line-through{text-decoration:line-through!important}.text-lowercase{text-transform:lowercase!important}.text-uppercase{text-transform:uppercase!important}.text-capitalize{text-transform:capitalize!important}.text-wrap{white-space:normal!important}.text-nowrap{white-space:nowrap!important}.text-break{word-wrap:break-word!important;word-break:break-word!important}.text-primary{--bs-text-opacity:1;color:rgba(var(--bs-primary-rgb),var(--bs-text-opacity))!important}.text-secondary{--bs-text-opacity:1;color:rgba(var(--bs-secondary-rgb),var(--bs-text-opacity))!important}.text-success{--bs-text-opacity:1;color:rgba(var(--bs-success-rgb),var(--bs-text-opacity))!important}.text-info{--bs-text-opacity:1;color:rgba(var(--bs-info-rgb),var(--bs-text-opacity))!important}.text-warning{--bs-text-opacity:1;color:rgba(var(--bs-warning-rgb),var(--bs-text-opacity))!important}.text-danger{--bs-text-opacity:1;color:rgba(var(--bs-danger-rgb),var(--bs-text-opacity))!important}.text-light{--bs-text-opacity:1;color:rgba(var(--bs-light-rgb),var(--bs-text-opacity))!important}.text-dark{--bs-text-opacity:1;color:rgba(var(--bs-dark-rgb),var(--bs-text-opacity))!important}.text-black{--bs-text-opacity:1;color:rgba(var(--bs-black-rgb),var(--bs-text-opacity))!important}.text-white{--bs-text-opacity:1;color:rgba(var(--bs-white-rgb),var(--bs-text-opacity))!important}.text-body{--bs-text-opacity:1;color:rgba(var(--bs-body-color-rgb),var(--bs-text-opacity))!important}.text-muted{--bs-text-opacity:1;color:#6c757d!important}.text-black-50{--bs-text-opacity:1;color:rgba(0,0,0,.5)!important}.text-white-50{--bs-text-opacity:1;color:rgba(255,255,255,.5)!important}.text-reset{--bs-text-opacity:1;color:inherit!important}.text-opacity-25{--bs-text-opacity:0.25}.text-opacity-50{--bs-text-opacity:0.5}.text-opacity-75{--bs-text-opacity:0.75}.text-opacity-100{--bs-text-opacity:1}.bg-primary{--bs-bg-opacity:1;background-color:rgba(var(--bs-primary-rgb),var(--bs-bg-opacity))!important}.bg-secondary{--bs-bg-opacity:1;background-color:rgba(var(--bs-secondary-rgb),var(--bs-bg-opacity))!important}.bg-success{--bs-bg-opacity:1;background-color:rgba(var(--bs-success-rgb),var(--bs-bg-opacity))!important}.bg-info{--bs-bg-opacity:1;background-color:rgba(var(--bs-info-rgb),var(--bs-bg-opacity))!important}.bg-warning{--bs-bg-opacity:1;background-color:rgba(var(--bs-warning-rgb),var(--bs-bg-opacity))!important}.bg-danger{--bs-bg-opacity:1;background-color:rgba(var(--bs-danger-rgb),var(--bs-bg-opacity))!important}.bg-light{--bs-bg-opacity:1;background-color:rgba(var(--bs-light-rgb),var(--bs-bg-opacity))!important}.bg-dark{--bs-bg-opacity:1;background-color:rgba(var(--bs-dark-rgb),var(--bs-bg-opacity))!important}.bg-black{--bs-bg-opacity:1;background-color:rgba(var(--bs-black-rgb),var(--bs-bg-opacity))!important}.bg-white{--bs-bg-opacity:1;background-color:rgba(var(--bs-white-rgb),var(--bs-bg-opacity))!important}.bg-body{--bs-bg-opacity:1;background-color:rgba(var(--bs-body-bg-rgb),var(--bs-bg-opacity))!important}.bg-transparent{--bs-bg-opacity:1;background-color:transparent!important}.bg-opacity-10{--bs-bg-opacity:0.1}.bg-opacity-25{--bs-bg-opacity:0.25}.bg-opacity-50{--bs-bg-opacity:0.5}.bg-opacity-75{--bs-bg-opacity:0.75}.bg-opacity-100{--bs-bg-opacity:1}.bg-gradient{background-image:var(--bs-gradient)!important}.user-select-all{-webkit-user-select:all!important;-moz-user-select:all!important;user-select:all!important}.user-select-auto{-webkit-user-select:auto!important;-moz-user-select:auto!important;user-select:auto!important}.user-select-none{-webkit-user-select:none!important;-moz-user-select:none!important;user-select:none!important}.pe-none{pointer-events:none!important}.pe-auto{pointer-events:auto!important}.rounded{border-radius:.25rem!important}.rounded-0{border-radius:0!important}.rounded-1{border-radius:.2rem!important}.rounded-2{border-radius:.25rem!important}.rounded-3{border-radius:.3rem!important}.rounded-circle{border-radius:50%!important}.rounded-pill{border-radius:50rem!important}.rounded-top{border-top-left-radius:.25rem!important;border-top-right-radius:.25rem!important}.rounded-end{border-top-right-radius:.25rem!important;border-bottom-right-radius:.25rem!important}.rounded-bottom{border-bottom-right-radius:.25rem!important;border-bottom-left-radius:.25rem!important}.rounded-start{border-bottom-left-radius:.25rem!important;border-top-left-radius:.25rem!important}.visible{visibility:visible!important}.invisible{visibility:hidden!important}@media (min-width:576px){.float-sm-start{float:left!important}.float-sm-end{float:right!important}.float-sm-none{float:none!important}.d-sm-inline{display:inline!important}.d-sm-inline-block{display:inline-block!important}.d-sm-block{display:block!important}.d-sm-grid{display:grid!important}.d-sm-table{display:table!important}.d-sm-table-row{display:table-row!important}.d-sm-table-cell{display:table-cell!important}.d-sm-flex{display:flex!important}.d-sm-inline-flex{display:inline-flex!important}.d-sm-none{display:none!important}.flex-sm-fill{flex:1 1 auto!important}.flex-sm-row{flex-direction:row!important}.flex-sm-column{flex-direction:column!important}.flex-sm-row-reverse{flex-direction:row-reverse!important}.flex-sm-column-reverse{flex-direction:column-reverse!important}.flex-sm-grow-0{flex-grow:0!important}.flex-sm-grow-1{flex-grow:1!important}.flex-sm-shrink-0{flex-shrink:0!important}.flex-sm-shrink-1{flex-shrink:1!important}.flex-sm-wrap{flex-wrap:wrap!important}.flex-sm-nowrap{flex-wrap:nowrap!important}.flex-sm-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-sm-0{gap:0!important}.gap-sm-1{gap:.25rem!important}.gap-sm-2{gap:.5rem!important}.gap-sm-3{gap:1rem!important}.gap-sm-4{gap:1.5rem!important}.gap-sm-5{gap:3rem!important}.justify-content-sm-start{justify-content:flex-start!important}.justify-content-sm-end{justify-content:flex-end!important}.justify-content-sm-center{justify-content:center!important}.justify-content-sm-between{justify-content:space-between!important}.justify-content-sm-around{justify-content:space-around!important}.justify-content-sm-evenly{justify-content:space-evenly!important}.align-items-sm-start{align-items:flex-start!important}.align-items-sm-end{align-items:flex-end!important}.align-items-sm-center{align-items:center!important}.align-items-sm-baseline{align-items:baseline!important}.align-items-sm-stretch{align-items:stretch!important}.align-content-sm-start{align-content:flex-start!important}.align-content-sm-end{align-content:flex-end!important}.align-content-sm-center{align-content:center!important}.align-content-sm-between{align-content:space-between!important}.align-content-sm-around{align-content:space-around!important}.align-content-sm-stretch{align-content:stretch!important}.align-self-sm-auto{align-self:auto!important}.align-self-sm-start{align-self:flex-start!important}.align-self-sm-end{align-self:flex-end!important}.align-self-sm-center{align-self:center!important}.align-self-sm-baseline{align-self:baseline!important}.align-self-sm-stretch{align-self:stretch!important}.order-sm-first{order:-1!important}.order-sm-0{order:0!important}.order-sm-1{order:1!important}.order-sm-2{order:2!important}.order-sm-3{order:3!important}.order-sm-4{order:4!important}.order-sm-5{order:5!important}.order-sm-last{order:6!important}.m-sm-0{margin:0!important}.m-sm-1{margin:.25rem!important}.m-sm-2{margin:.5rem!important}.m-sm-3{margin:1rem!important}.m-sm-4{margin:1.5rem!important}.m-sm-5{margin:3rem!important}.m-sm-auto{margin:auto!important}.mx-sm-0{margin-right:0!important;margin-left:0!important}.mx-sm-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-sm-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-sm-3{margin-right:1rem!important;margin-left:1rem!important}.mx-sm-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-sm-5{margin-right:3rem!important;margin-left:3rem!important}.mx-sm-auto{margin-right:auto!important;margin-left:auto!important}.my-sm-0{margin-top:0!important;margin-bottom:0!important}.my-sm-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-sm-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-sm-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-sm-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-sm-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-sm-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-sm-0{margin-top:0!important}.mt-sm-1{margin-top:.25rem!important}.mt-sm-2{margin-top:.5rem!important}.mt-sm-3{margin-top:1rem!important}.mt-sm-4{margin-top:1.5rem!important}.mt-sm-5{margin-top:3rem!important}.mt-sm-auto{margin-top:auto!important}.me-sm-0{margin-right:0!important}.me-sm-1{margin-right:.25rem!important}.me-sm-2{margin-right:.5rem!important}.me-sm-3{margin-right:1rem!important}.me-sm-4{margin-right:1.5rem!important}.me-sm-5{margin-right:3rem!important}.me-sm-auto{margin-right:auto!important}.mb-sm-0{margin-bottom:0!important}.mb-sm-1{margin-bottom:.25rem!important}.mb-sm-2{margin-bottom:.5rem!important}.mb-sm-3{margin-bottom:1rem!important}.mb-sm-4{margin-bottom:1.5rem!important}.mb-sm-5{margin-bottom:3rem!important}.mb-sm-auto{margin-bottom:auto!important}.ms-sm-0{margin-left:0!important}.ms-sm-1{margin-left:.25rem!important}.ms-sm-2{margin-left:.5rem!important}.ms-sm-3{margin-left:1rem!important}.ms-sm-4{margin-left:1.5rem!important}.ms-sm-5{margin-left:3rem!important}.ms-sm-auto{margin-left:auto!important}.p-sm-0{padding:0!important}.p-sm-1{padding:.25rem!important}.p-sm-2{padding:.5rem!important}.p-sm-3{padding:1rem!important}.p-sm-4{padding:1.5rem!important}.p-sm-5{padding:3rem!important}.px-sm-0{padding-right:0!important;padding-left:0!important}.px-sm-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-sm-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-sm-3{padding-right:1rem!important;padding-left:1rem!important}.px-sm-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-sm-5{padding-right:3rem!important;padding-left:3rem!important}.py-sm-0{padding-top:0!important;padding-bottom:0!important}.py-sm-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-sm-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-sm-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-sm-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-sm-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-sm-0{padding-top:0!important}.pt-sm-1{padding-top:.25rem!important}.pt-sm-2{padding-top:.5rem!important}.pt-sm-3{padding-top:1rem!important}.pt-sm-4{padding-top:1.5rem!important}.pt-sm-5{padding-top:3rem!important}.pe-sm-0{padding-right:0!important}.pe-sm-1{padding-right:.25rem!important}.pe-sm-2{padding-right:.5rem!important}.pe-sm-3{padding-right:1rem!important}.pe-sm-4{padding-right:1.5rem!important}.pe-sm-5{padding-right:3rem!important}.pb-sm-0{padding-bottom:0!important}.pb-sm-1{padding-bottom:.25rem!important}.pb-sm-2{padding-bottom:.5rem!important}.pb-sm-3{padding-bottom:1rem!important}.pb-sm-4{padding-bottom:1.5rem!important}.pb-sm-5{padding-bottom:3rem!important}.ps-sm-0{padding-left:0!important}.ps-sm-1{padding-left:.25rem!important}.ps-sm-2{padding-left:.5rem!important}.ps-sm-3{padding-left:1rem!important}.ps-sm-4{padding-left:1.5rem!important}.ps-sm-5{padding-left:3rem!important}.text-sm-start{text-align:left!important}.text-sm-end{text-align:right!important}.text-sm-center{text-align:center!important}}@media (min-width:768px){.float-md-start{float:left!important}.float-md-end{float:right!important}.float-md-none{float:none!important}.d-md-inline{display:inline!important}.d-md-inline-block{display:inline-block!important}.d-md-block{display:block!important}.d-md-grid{display:grid!important}.d-md-table{display:table!important}.d-md-table-row{display:table-row!important}.d-md-table-cell{display:table-cell!important}.d-md-flex{display:flex!important}.d-md-inline-flex{display:inline-flex!important}.d-md-none{display:none!important}.flex-md-fill{flex:1 1 auto!important}.flex-md-row{flex-direction:row!important}.flex-md-column{flex-direction:column!important}.flex-md-row-reverse{flex-direction:row-reverse!important}.flex-md-column-reverse{flex-direction:column-reverse!important}.flex-md-grow-0{flex-grow:0!important}.flex-md-grow-1{flex-grow:1!important}.flex-md-shrink-0{flex-shrink:0!important}.flex-md-shrink-1{flex-shrink:1!important}.flex-md-wrap{flex-wrap:wrap!important}.flex-md-nowrap{flex-wrap:nowrap!important}.flex-md-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-md-0{gap:0!important}.gap-md-1{gap:.25rem!important}.gap-md-2{gap:.5rem!important}.gap-md-3{gap:1rem!important}.gap-md-4{gap:1.5rem!important}.gap-md-5{gap:3rem!important}.justify-content-md-start{justify-content:flex-start!important}.justify-content-md-end{justify-content:flex-end!important}.justify-content-md-center{justify-content:center!important}.justify-content-md-between{justify-content:space-between!important}.justify-content-md-around{justify-content:space-around!important}.justify-content-md-evenly{justify-content:space-evenly!important}.align-items-md-start{align-items:flex-start!important}.align-items-md-end{align-items:flex-end!important}.align-items-md-center{align-items:center!important}.align-items-md-baseline{align-items:baseline!important}.align-items-md-stretch{align-items:stretch!important}.align-content-md-start{align-content:flex-start!important}.align-content-md-end{align-content:flex-end!important}.align-content-md-center{align-content:center!important}.align-content-md-between{align-content:space-between!important}.align-content-md-around{align-content:space-around!important}.align-content-md-stretch{align-content:stretch!important}.align-self-md-auto{align-self:auto!important}.align-self-md-start{align-self:flex-start!important}.align-self-md-end{align-self:flex-end!important}.align-self-md-center{align-self:center!important}.align-self-md-baseline{align-self:baseline!important}.align-self-md-stretch{align-self:stretch!important}.order-md-first{order:-1!important}.order-md-0{order:0!important}.order-md-1{order:1!important}.order-md-2{order:2!important}.order-md-3{order:3!important}.order-md-4{order:4!important}.order-md-5{order:5!important}.order-md-last{order:6!important}.m-md-0{margin:0!important}.m-md-1{margin:.25rem!important}.m-md-2{margin:.5rem!important}.m-md-3{margin:1rem!important}.m-md-4{margin:1.5rem!important}.m-md-5{margin:3rem!important}.m-md-auto{margin:auto!important}.mx-md-0{margin-right:0!important;margin-left:0!important}.mx-md-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-md-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-md-3{margin-right:1rem!important;margin-left:1rem!important}.mx-md-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-md-5{margin-right:3rem!important;margin-left:3rem!important}.mx-md-auto{margin-right:auto!important;margin-left:auto!important}.my-md-0{margin-top:0!important;margin-bottom:0!important}.my-md-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-md-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-md-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-md-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-md-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-md-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-md-0{margin-top:0!important}.mt-md-1{margin-top:.25rem!important}.mt-md-2{margin-top:.5rem!important}.mt-md-3{margin-top:1rem!important}.mt-md-4{margin-top:1.5rem!important}.mt-md-5{margin-top:3rem!important}.mt-md-auto{margin-top:auto!important}.me-md-0{margin-right:0!important}.me-md-1{margin-right:.25rem!important}.me-md-2{margin-right:.5rem!important}.me-md-3{margin-right:1rem!important}.me-md-4{margin-right:1.5rem!important}.me-md-5{margin-right:3rem!important}.me-md-auto{margin-right:auto!important}.mb-md-0{margin-bottom:0!important}.mb-md-1{margin-bottom:.25rem!important}.mb-md-2{margin-bottom:.5rem!important}.mb-md-3{margin-bottom:1rem!important}.mb-md-4{margin-bottom:1.5rem!important}.mb-md-5{margin-bottom:3rem!important}.mb-md-auto{margin-bottom:auto!important}.ms-md-0{margin-left:0!important}.ms-md-1{margin-left:.25rem!important}.ms-md-2{margin-left:.5rem!important}.ms-md-3{margin-left:1rem!important}.ms-md-4{margin-left:1.5rem!important}.ms-md-5{margin-left:3rem!important}.ms-md-auto{margin-left:auto!important}.p-md-0{padding:0!important}.p-md-1{padding:.25rem!important}.p-md-2{padding:.5rem!important}.p-md-3{padding:1rem!important}.p-md-4{padding:1.5rem!important}.p-md-5{padding:3rem!important}.px-md-0{padding-right:0!important;padding-left:0!important}.px-md-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-md-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-md-3{padding-right:1rem!important;padding-left:1rem!important}.px-md-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-md-5{padding-right:3rem!important;padding-left:3rem!important}.py-md-0{padding-top:0!important;padding-bottom:0!important}.py-md-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-md-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-md-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-md-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-md-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-md-0{padding-top:0!important}.pt-md-1{padding-top:.25rem!important}.pt-md-2{padding-top:.5rem!important}.pt-md-3{padding-top:1rem!important}.pt-md-4{padding-top:1.5rem!important}.pt-md-5{padding-top:3rem!important}.pe-md-0{padding-right:0!important}.pe-md-1{padding-right:.25rem!important}.pe-md-2{padding-right:.5rem!important}.pe-md-3{padding-right:1rem!important}.pe-md-4{padding-right:1.5rem!important}.pe-md-5{padding-right:3rem!important}.pb-md-0{padding-bottom:0!important}.pb-md-1{padding-bottom:.25rem!important}.pb-md-2{padding-bottom:.5rem!important}.pb-md-3{padding-bottom:1rem!important}.pb-md-4{padding-bottom:1.5rem!important}.pb-md-5{padding-bottom:3rem!important}.ps-md-0{padding-left:0!important}.ps-md-1{padding-left:.25rem!important}.ps-md-2{padding-left:.5rem!important}.ps-md-3{padding-left:1rem!important}.ps-md-4{padding-left:1.5rem!important}.ps-md-5{padding-left:3rem!important}.text-md-start{text-align:left!important}.text-md-end{text-align:right!important}.text-md-center{text-align:center!important}}@media (min-width:992px){.float-lg-start{float:left!important}.float-lg-end{float:right!important}.float-lg-none{float:none!important}.d-lg-inline{display:inline!important}.d-lg-inline-block{display:inline-block!important}.d-lg-block{display:block!important}.d-lg-grid{display:grid!important}.d-lg-table{display:table!important}.d-lg-table-row{display:table-row!important}.d-lg-table-cell{display:table-cell!important}.d-lg-flex{display:flex!important}.d-lg-inline-flex{display:inline-flex!important}.d-lg-none{display:none!important}.flex-lg-fill{flex:1 1 auto!important}.flex-lg-row{flex-direction:row!important}.flex-lg-column{flex-direction:column!important}.flex-lg-row-reverse{flex-direction:row-reverse!important}.flex-lg-column-reverse{flex-direction:column-reverse!important}.flex-lg-grow-0{flex-grow:0!important}.flex-lg-grow-1{flex-grow:1!important}.flex-lg-shrink-0{flex-shrink:0!important}.flex-lg-shrink-1{flex-shrink:1!important}.flex-lg-wrap{flex-wrap:wrap!important}.flex-lg-nowrap{flex-wrap:nowrap!important}.flex-lg-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-lg-0{gap:0!important}.gap-lg-1{gap:.25rem!important}.gap-lg-2{gap:.5rem!important}.gap-lg-3{gap:1rem!important}.gap-lg-4{gap:1.5rem!important}.gap-lg-5{gap:3rem!important}.justify-content-lg-start{justify-content:flex-start!important}.justify-content-lg-end{justify-content:flex-end!important}.justify-content-lg-center{justify-content:center!important}.justify-content-lg-between{justify-content:space-between!important}.justify-content-lg-around{justify-content:space-around!important}.justify-content-lg-evenly{justify-content:space-evenly!important}.align-items-lg-start{align-items:flex-start!important}.align-items-lg-end{align-items:flex-end!important}.align-items-lg-center{align-items:center!important}.align-items-lg-baseline{align-items:baseline!important}.align-items-lg-stretch{align-items:stretch!important}.align-content-lg-start{align-content:flex-start!important}.align-content-lg-end{align-content:flex-end!important}.align-content-lg-center{align-content:center!important}.align-content-lg-between{align-content:space-between!important}.align-content-lg-around{align-content:space-around!important}.align-content-lg-stretch{align-content:stretch!important}.align-self-lg-auto{align-self:auto!important}.align-self-lg-start{align-self:flex-start!important}.align-self-lg-end{align-self:flex-end!important}.align-self-lg-center{align-self:center!important}.align-self-lg-baseline{align-self:baseline!important}.align-self-lg-stretch{align-self:stretch!important}.order-lg-first{order:-1!important}.order-lg-0{order:0!important}.order-lg-1{order:1!important}.order-lg-2{order:2!important}.order-lg-3{order:3!important}.order-lg-4{order:4!important}.order-lg-5{order:5!important}.order-lg-last{order:6!important}.m-lg-0{margin:0!important}.m-lg-1{margin:.25rem!important}.m-lg-2{margin:.5rem!important}.m-lg-3{margin:1rem!important}.m-lg-4{margin:1.5rem!important}.m-lg-5{margin:3rem!important}.m-lg-auto{margin:auto!important}.mx-lg-0{margin-right:0!important;margin-left:0!important}.mx-lg-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-lg-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-lg-3{margin-right:1rem!important;margin-left:1rem!important}.mx-lg-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-lg-5{margin-right:3rem!important;margin-left:3rem!important}.mx-lg-auto{margin-right:auto!important;margin-left:auto!important}.my-lg-0{margin-top:0!important;margin-bottom:0!important}.my-lg-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-lg-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-lg-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-lg-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-lg-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-lg-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-lg-0{margin-top:0!important}.mt-lg-1{margin-top:.25rem!important}.mt-lg-2{margin-top:.5rem!important}.mt-lg-3{margin-top:1rem!important}.mt-lg-4{margin-top:1.5rem!important}.mt-lg-5{margin-top:3rem!important}.mt-lg-auto{margin-top:auto!important}.me-lg-0{margin-right:0!important}.me-lg-1{margin-right:.25rem!important}.me-lg-2{margin-right:.5rem!important}.me-lg-3{margin-right:1rem!important}.me-lg-4{margin-right:1.5rem!important}.me-lg-5{margin-right:3rem!important}.me-lg-auto{margin-right:auto!important}.mb-lg-0{margin-bottom:0!important}.mb-lg-1{margin-bottom:.25rem!important}.mb-lg-2{margin-bottom:.5rem!important}.mb-lg-3{margin-bottom:1rem!important}.mb-lg-4{margin-bottom:1.5rem!important}.mb-lg-5{margin-bottom:3rem!important}.mb-lg-auto{margin-bottom:auto!important}.ms-lg-0{margin-left:0!important}.ms-lg-1{margin-left:.25rem!important}.ms-lg-2{margin-left:.5rem!important}.ms-lg-3{margin-left:1rem!important}.ms-lg-4{margin-left:1.5rem!important}.ms-lg-5{margin-left:3rem!important}.ms-lg-auto{margin-left:auto!important}.p-lg-0{padding:0!important}.p-lg-1{padding:.25rem!important}.p-lg-2{padding:.5rem!important}.p-lg-3{padding:1rem!important}.p-lg-4{padding:1.5rem!important}.p-lg-5{padding:3rem!important}.px-lg-0{padding-right:0!important;padding-left:0!important}.px-lg-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-lg-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-lg-3{padding-right:1rem!important;padding-left:1rem!important}.px-lg-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-lg-5{padding-right:3rem!important;padding-left:3rem!important}.py-lg-0{padding-top:0!important;padding-bottom:0!important}.py-lg-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-lg-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-lg-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-lg-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-lg-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-lg-0{padding-top:0!important}.pt-lg-1{padding-top:.25rem!important}.pt-lg-2{padding-top:.5rem!important}.pt-lg-3{padding-top:1rem!important}.pt-lg-4{padding-top:1.5rem!important}.pt-lg-5{padding-top:3rem!important}.pe-lg-0{padding-right:0!important}.pe-lg-1{padding-right:.25rem!important}.pe-lg-2{padding-right:.5rem!important}.pe-lg-3{padding-right:1rem!important}.pe-lg-4{padding-right:1.5rem!important}.pe-lg-5{padding-right:3rem!important}.pb-lg-0{padding-bottom:0!important}.pb-lg-1{padding-bottom:.25rem!important}.pb-lg-2{padding-bottom:.5rem!important}.pb-lg-3{padding-bottom:1rem!important}.pb-lg-4{padding-bottom:1.5rem!important}.pb-lg-5{padding-bottom:3rem!important}.ps-lg-0{padding-left:0!important}.ps-lg-1{padding-left:.25rem!important}.ps-lg-2{padding-left:.5rem!important}.ps-lg-3{padding-left:1rem!important}.ps-lg-4{padding-left:1.5rem!important}.ps-lg-5{padding-left:3rem!important}.text-lg-start{text-align:left!important}.text-lg-end{text-align:right!important}.text-lg-center{text-align:center!important}}@media (min-width:1200px){.float-xl-start{float:left!important}.float-xl-end{float:right!important}.float-xl-none{float:none!important}.d-xl-inline{display:inline!important}.d-xl-inline-block{display:inline-block!important}.d-xl-block{display:block!important}.d-xl-grid{display:grid!important}.d-xl-table{display:table!important}.d-xl-table-row{display:table-row!important}.d-xl-table-cell{display:table-cell!important}.d-xl-flex{display:flex!important}.d-xl-inline-flex{display:inline-flex!important}.d-xl-none{display:none!important}.flex-xl-fill{flex:1 1 auto!important}.flex-xl-row{flex-direction:row!important}.flex-xl-column{flex-direction:column!important}.flex-xl-row-reverse{flex-direction:row-reverse!important}.flex-xl-column-reverse{flex-direction:column-reverse!important}.flex-xl-grow-0{flex-grow:0!important}.flex-xl-grow-1{flex-grow:1!important}.flex-xl-shrink-0{flex-shrink:0!important}.flex-xl-shrink-1{flex-shrink:1!important}.flex-xl-wrap{flex-wrap:wrap!important}.flex-xl-nowrap{flex-wrap:nowrap!important}.flex-xl-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-xl-0{gap:0!important}.gap-xl-1{gap:.25rem!important}.gap-xl-2{gap:.5rem!important}.gap-xl-3{gap:1rem!important}.gap-xl-4{gap:1.5rem!important}.gap-xl-5{gap:3rem!important}.justify-content-xl-start{justify-content:flex-start!important}.justify-content-xl-end{justify-content:flex-end!important}.justify-content-xl-center{justify-content:center!important}.justify-content-xl-between{justify-content:space-between!important}.justify-content-xl-around{justify-content:space-around!important}.justify-content-xl-evenly{justify-content:space-evenly!important}.align-items-xl-start{align-items:flex-start!important}.align-items-xl-end{align-items:flex-end!important}.align-items-xl-center{align-items:center!important}.align-items-xl-baseline{align-items:baseline!important}.align-items-xl-stretch{align-items:stretch!important}.align-content-xl-start{align-content:flex-start!important}.align-content-xl-end{align-content:flex-end!important}.align-content-xl-center{align-content:center!important}.align-content-xl-between{align-content:space-between!important}.align-content-xl-around{align-content:space-around!important}.align-content-xl-stretch{align-content:stretch!important}.align-self-xl-auto{align-self:auto!important}.align-self-xl-start{align-self:flex-start!important}.align-self-xl-end{align-self:flex-end!important}.align-self-xl-center{align-self:center!important}.align-self-xl-baseline{align-self:baseline!important}.align-self-xl-stretch{align-self:stretch!important}.order-xl-first{order:-1!important}.order-xl-0{order:0!important}.order-xl-1{order:1!important}.order-xl-2{order:2!important}.order-xl-3{order:3!important}.order-xl-4{order:4!important}.order-xl-5{order:5!important}.order-xl-last{order:6!important}.m-xl-0{margin:0!important}.m-xl-1{margin:.25rem!important}.m-xl-2{margin:.5rem!important}.m-xl-3{margin:1rem!important}.m-xl-4{margin:1.5rem!important}.m-xl-5{margin:3rem!important}.m-xl-auto{margin:auto!important}.mx-xl-0{margin-right:0!important;margin-left:0!important}.mx-xl-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-xl-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-xl-3{margin-right:1rem!important;margin-left:1rem!important}.mx-xl-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-xl-5{margin-right:3rem!important;margin-left:3rem!important}.mx-xl-auto{margin-right:auto!important;margin-left:auto!important}.my-xl-0{margin-top:0!important;margin-bottom:0!important}.my-xl-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-xl-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-xl-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-xl-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-xl-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-xl-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-xl-0{margin-top:0!important}.mt-xl-1{margin-top:.25rem!important}.mt-xl-2{margin-top:.5rem!important}.mt-xl-3{margin-top:1rem!important}.mt-xl-4{margin-top:1.5rem!important}.mt-xl-5{margin-top:3rem!important}.mt-xl-auto{margin-top:auto!important}.me-xl-0{margin-right:0!important}.me-xl-1{margin-right:.25rem!important}.me-xl-2{margin-right:.5rem!important}.me-xl-3{margin-right:1rem!important}.me-xl-4{margin-right:1.5rem!important}.me-xl-5{margin-right:3rem!important}.me-xl-auto{margin-right:auto!important}.mb-xl-0{margin-bottom:0!important}.mb-xl-1{margin-bottom:.25rem!important}.mb-xl-2{margin-bottom:.5rem!important}.mb-xl-3{margin-bottom:1rem!important}.mb-xl-4{margin-bottom:1.5rem!important}.mb-xl-5{margin-bottom:3rem!important}.mb-xl-auto{margin-bottom:auto!important}.ms-xl-0{margin-left:0!important}.ms-xl-1{margin-left:.25rem!important}.ms-xl-2{margin-left:.5rem!important}.ms-xl-3{margin-left:1rem!important}.ms-xl-4{margin-left:1.5rem!important}.ms-xl-5{margin-left:3rem!important}.ms-xl-auto{margin-left:auto!important}.p-xl-0{padding:0!important}.p-xl-1{padding:.25rem!important}.p-xl-2{padding:.5rem!important}.p-xl-3{padding:1rem!important}.p-xl-4{padding:1.5rem!important}.p-xl-5{padding:3rem!important}.px-xl-0{padding-right:0!important;padding-left:0!important}.px-xl-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-xl-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-xl-3{padding-right:1rem!important;padding-left:1rem!important}.px-xl-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-xl-5{padding-right:3rem!important;padding-left:3rem!important}.py-xl-0{padding-top:0!important;padding-bottom:0!important}.py-xl-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-xl-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-xl-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-xl-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-xl-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-xl-0{padding-top:0!important}.pt-xl-1{padding-top:.25rem!important}.pt-xl-2{padding-top:.5rem!important}.pt-xl-3{padding-top:1rem!important}.pt-xl-4{padding-top:1.5rem!important}.pt-xl-5{padding-top:3rem!important}.pe-xl-0{padding-right:0!important}.pe-xl-1{padding-right:.25rem!important}.pe-xl-2{padding-right:.5rem!important}.pe-xl-3{padding-right:1rem!important}.pe-xl-4{padding-right:1.5rem!important}.pe-xl-5{padding-right:3rem!important}.pb-xl-0{padding-bottom:0!important}.pb-xl-1{padding-bottom:.25rem!important}.pb-xl-2{padding-bottom:.5rem!important}.pb-xl-3{padding-bottom:1rem!important}.pb-xl-4{padding-bottom:1.5rem!important}.pb-xl-5{padding-bottom:3rem!important}.ps-xl-0{padding-left:0!important}.ps-xl-1{padding-left:.25rem!important}.ps-xl-2{padding-left:.5rem!important}.ps-xl-3{padding-left:1rem!important}.ps-xl-4{padding-left:1.5rem!important}.ps-xl-5{padding-left:3rem!important}.text-xl-start{text-align:left!important}.text-xl-end{text-align:right!important}.text-xl-center{text-align:center!important}}@media (min-width:1400px){.float-xxl-start{float:left!important}.float-xxl-end{float:right!important}.float-xxl-none{float:none!important}.d-xxl-inline{display:inline!important}.d-xxl-inline-block{display:inline-block!important}.d-xxl-block{display:block!important}.d-xxl-grid{display:grid!important}.d-xxl-table{display:table!important}.d-xxl-table-row{display:table-row!important}.d-xxl-table-cell{display:table-cell!important}.d-xxl-flex{display:flex!important}.d-xxl-inline-flex{display:inline-flex!important}.d-xxl-none{display:none!important}.flex-xxl-fill{flex:1 1 auto!important}.flex-xxl-row{flex-direction:row!important}.flex-xxl-column{flex-direction:column!important}.flex-xxl-row-reverse{flex-direction:row-reverse!important}.flex-xxl-column-reverse{flex-direction:column-reverse!important}.flex-xxl-grow-0{flex-grow:0!important}.flex-xxl-grow-1{flex-grow:1!important}.flex-xxl-shrink-0{flex-shrink:0!important}.flex-xxl-shrink-1{flex-shrink:1!important}.flex-xxl-wrap{flex-wrap:wrap!important}.flex-xxl-nowrap{flex-wrap:nowrap!important}.flex-xxl-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-xxl-0{gap:0!important}.gap-xxl-1{gap:.25rem!important}.gap-xxl-2{gap:.5rem!important}.gap-xxl-3{gap:1rem!important}.gap-xxl-4{gap:1.5rem!important}.gap-xxl-5{gap:3rem!important}.justify-content-xxl-start{justify-content:flex-start!important}.justify-content-xxl-end{justify-content:flex-end!important}.justify-content-xxl-center{justify-content:center!important}.justify-content-xxl-between{justify-content:space-between!important}.justify-content-xxl-around{justify-content:space-around!important}.justify-content-xxl-evenly{justify-content:space-evenly!important}.align-items-xxl-start{align-items:flex-start!important}.align-items-xxl-end{align-items:flex-end!important}.align-items-xxl-center{align-items:center!important}.align-items-xxl-baseline{align-items:baseline!important}.align-items-xxl-stretch{align-items:stretch!important}.align-content-xxl-start{align-content:flex-start!important}.align-content-xxl-end{align-content:flex-end!important}.align-content-xxl-center{align-content:center!important}.align-content-xxl-between{align-content:space-between!important}.align-content-xxl-around{align-content:space-around!important}.align-content-xxl-stretch{align-content:stretch!important}.align-self-xxl-auto{align-self:auto!important}.align-self-xxl-start{align-self:flex-start!important}.align-self-xxl-end{align-self:flex-end!important}.align-self-xxl-center{align-self:center!important}.align-self-xxl-baseline{align-self:baseline!important}.align-self-xxl-stretch{align-self:stretch!important}.order-xxl-first{order:-1!important}.order-xxl-0{order:0!important}.order-xxl-1{order:1!important}.order-xxl-2{order:2!important}.order-xxl-3{order:3!important}.order-xxl-4{order:4!important}.order-xxl-5{order:5!important}.order-xxl-last{order:6!important}.m-xxl-0{margin:0!important}.m-xxl-1{margin:.25rem!important}.m-xxl-2{margin:.5rem!important}.m-xxl-3{margin:1rem!important}.m-xxl-4{margin:1.5rem!important}.m-xxl-5{margin:3rem!important}.m-xxl-auto{margin:auto!important}.mx-xxl-0{margin-right:0!important;margin-left:0!important}.mx-xxl-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-xxl-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-xxl-3{margin-right:1rem!important;margin-left:1rem!important}.mx-xxl-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-xxl-5{margin-right:3rem!important;margin-left:3rem!important}.mx-xxl-auto{margin-right:auto!important;margin-left:auto!important}.my-xxl-0{margin-top:0!important;margin-bottom:0!important}.my-xxl-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-xxl-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-xxl-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-xxl-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-xxl-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-xxl-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-xxl-0{margin-top:0!important}.mt-xxl-1{margin-top:.25rem!important}.mt-xxl-2{margin-top:.5rem!important}.mt-xxl-3{margin-top:1rem!important}.mt-xxl-4{margin-top:1.5rem!important}.mt-xxl-5{margin-top:3rem!important}.mt-xxl-auto{margin-top:auto!important}.me-xxl-0{margin-right:0!important}.me-xxl-1{margin-right:.25rem!important}.me-xxl-2{margin-right:.5rem!important}.me-xxl-3{margin-right:1rem!important}.me-xxl-4{margin-right:1.5rem!important}.me-xxl-5{margin-right:3rem!important}.me-xxl-auto{margin-right:auto!important}.mb-xxl-0{margin-bottom:0!important}.mb-xxl-1{margin-bottom:.25rem!important}.mb-xxl-2{margin-bottom:.5rem!important}.mb-xxl-3{margin-bottom:1rem!important}.mb-xxl-4{margin-bottom:1.5rem!important}.mb-xxl-5{margin-bottom:3rem!important}.mb-xxl-auto{margin-bottom:auto!important}.ms-xxl-0{margin-left:0!important}.ms-xxl-1{margin-left:.25rem!important}.ms-xxl-2{margin-left:.5rem!important}.ms-xxl-3{margin-left:1rem!important}.ms-xxl-4{margin-left:1.5rem!important}.ms-xxl-5{margin-left:3rem!important}.ms-xxl-auto{margin-left:auto!important}.p-xxl-0{padding:0!important}.p-xxl-1{padding:.25rem!important}.p-xxl-2{padding:.5rem!important}.p-xxl-3{padding:1rem!important}.p-xxl-4{padding:1.5rem!important}.p-xxl-5{padding:3rem!important}.px-xxl-0{padding-right:0!important;padding-left:0!important}.px-xxl-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-xxl-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-xxl-3{padding-right:1rem!important;padding-left:1rem!important}.px-xxl-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-xxl-5{padding-right:3rem!important;padding-left:3rem!important}.py-xxl-0{padding-top:0!important;padding-bottom:0!important}.py-xxl-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-xxl-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-xxl-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-xxl-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-xxl-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-xxl-0{padding-top:0!important}.pt-xxl-1{padding-top:.25rem!important}.pt-xxl-2{padding-top:.5rem!important}.pt-xxl-3{padding-top:1rem!important}.pt-xxl-4{padding-top:1.5rem!important}.pt-xxl-5{padding-top:3rem!important}.pe-xxl-0{padding-right:0!important}.pe-xxl-1{padding-right:.25rem!important}.pe-xxl-2{padding-right:.5rem!important}.pe-xxl-3{padding-right:1rem!important}.pe-xxl-4{padding-right:1.5rem!important}.pe-xxl-5{padding-right:3rem!important}.pb-xxl-0{padding-bottom:0!important}.pb-xxl-1{padding-bottom:.25rem!important}.pb-xxl-2{padding-bottom:.5rem!important}.pb-xxl-3{padding-bottom:1rem!important}.pb-xxl-4{padding-bottom:1.5rem!important}.pb-xxl-5{padding-bottom:3rem!important}.ps-xxl-0{padding-left:0!important}.ps-xxl-1{padding-left:.25rem!important}.ps-xxl-2{padding-left:.5rem!important}.ps-xxl-3{padding-left:1rem!important}.ps-xxl-4{padding-left:1.5rem!important}.ps-xxl-5{padding-left:3rem!important}.text-xxl-start{text-align:left!important}.text-xxl-end{text-align:right!important}.text-xxl-center{text-align:center!important}}@media (min-width:1200px){.fs-1{font-size:2.5rem!important}.fs-2{font-size:2rem!important}.fs-3{font-size:1.75rem!important}.fs-4{font-size:1.5rem!important}}@media print{.d-print-inline{display:inline!important}.d-print-inline-block{display:inline-block!important}.d-print-block{display:block!important}.d-print-grid{display:grid!important}.d-print-table{display:table!important}.d-print-table-row{display:table-row!important}.d-print-table-cell{display:table-cell!important}.d-print-flex{display:flex!important}.d-print-inline-flex{display:inline-flex!important}.d-print-none{display:none!important}} +/*# sourceMappingURL=bootstrap.min.css.map */ \ No newline at end of file diff --git a/StreamDock-Plugin-SDK/SDNodeJsSDK/unity-communication-plugin/propertyInspector/utils/common.js b/Streamdeck/com.mirabox.streamingle.sdPlugin/dist-dev/com.mirabox.streamingle.sdPlugin/propertyInspector/utils/common.js similarity index 100% rename from StreamDock-Plugin-SDK/SDNodeJsSDK/unity-communication-plugin/propertyInspector/utils/common.js rename to Streamdeck/com.mirabox.streamingle.sdPlugin/dist-dev/com.mirabox.streamingle.sdPlugin/propertyInspector/utils/common.js diff --git a/Streamdeck/com.mirabox.streamingle.sdPlugin/dist-dev/com.mirabox.streamingle.sdPlugin/propertyInspector/utils/fonts/bootstrap-icons.woff b/Streamdeck/com.mirabox.streamingle.sdPlugin/dist-dev/com.mirabox.streamingle.sdPlugin/propertyInspector/utils/fonts/bootstrap-icons.woff new file mode 100644 index 00000000..1f5d5430 Binary files /dev/null and b/Streamdeck/com.mirabox.streamingle.sdPlugin/dist-dev/com.mirabox.streamingle.sdPlugin/propertyInspector/utils/fonts/bootstrap-icons.woff differ diff --git a/Streamdeck/com.mirabox.streamingle.sdPlugin/dist-dev/com.mirabox.streamingle.sdPlugin/propertyInspector/utils/fonts/bootstrap-icons.woff2 b/Streamdeck/com.mirabox.streamingle.sdPlugin/dist-dev/com.mirabox.streamingle.sdPlugin/propertyInspector/utils/fonts/bootstrap-icons.woff2 new file mode 100644 index 00000000..b3897eff Binary files /dev/null and b/Streamdeck/com.mirabox.streamingle.sdPlugin/dist-dev/com.mirabox.streamingle.sdPlugin/propertyInspector/utils/fonts/bootstrap-icons.woff2 differ diff --git a/Streamdeck/com.mirabox.streamingle.sdPlugin/dist-dev/com.mirabox.streamingle.sdPlugin/streamingle_plugin b/Streamdeck/com.mirabox.streamingle.sdPlugin/dist-dev/com.mirabox.streamingle.sdPlugin/streamingle_plugin new file mode 100644 index 00000000..04440e92 --- /dev/null +++ b/Streamdeck/com.mirabox.streamingle.sdPlugin/dist-dev/com.mirabox.streamingle.sdPlugin/streamingle_plugin @@ -0,0 +1,9 @@ +[2025-07-03T00:52:25.811] [INFO] default - 🚀 Streamingle Camera Controller 플러그인 시작됨 +[2025-07-03T00:58:26.107] [INFO] default - 🚀 Streamingle Camera Controller 플러그인 시작됨 +[2025-07-03T00:58:37.479] [INFO] default - 🚀 Streamingle Camera Controller 플러그인 시작됨 +[2025-07-03T00:58:50.582] [INFO] default - 🚀 Streamingle Camera Controller 플러그인 시작됨 +[2025-07-03T00:59:04.912] [INFO] default - 🚀 Streamingle Camera Controller 플러그인 시작됨 +[2025-07-03T01:00:19.924] [INFO] default - 🚀 Streamingle Camera Controller 플러그인 시작됨 +[2025-07-03T01:00:32.402] [INFO] default - 🚀 Streamingle Camera Controller 플러그인 시작됨 +[2025-07-03T01:01:32.400] [INFO] default - 🚀 Streamingle Camera Controller 플러그인 시작됨 +[2025-07-03T01:02:32.414] [INFO] default - 🚀 Streamingle Camera Controller 플러그인 시작됨 diff --git a/Streamdeck/com.mirabox.streamingle.sdPlugin/dist-dev/com.mirabox.streamingle.sdPlugin/test.js b/Streamdeck/com.mirabox.streamingle.sdPlugin/dist-dev/com.mirabox.streamingle.sdPlugin/test.js new file mode 100644 index 00000000..0f6e2a5f --- /dev/null +++ b/Streamdeck/com.mirabox.streamingle.sdPlugin/dist-dev/com.mirabox.streamingle.sdPlugin/test.js @@ -0,0 +1,65 @@ +const StreaminglePlugin = require('./plugin.js'); + +console.log('=== Streamingle 플러그인 테스트 시작 ==='); + +const plugin = new StreaminglePlugin(); + +// 연결 상태 모니터링 +let connectionCheckInterval = setInterval(() => { + const status = plugin.getStatus(); + console.log(`📊 연결 상태: ${status.isConnected ? '✅ 연결됨' : '❌ 연결 안됨'}`); + + if (status.isConnected) { + console.log(`📷 카메라 개수: ${status.cameraCount}개`); + console.log(`🎯 현재 카메라: ${status.currentCamera >= 0 ? status.currentCamera : '없음'}`); + + if (status.cameraList && status.cameraList.length > 0) { + console.log('📋 카메라 목록:'); + status.cameraList.forEach((camera, index) => { + console.log(` ${index}: ${camera.name} ${camera.isActive ? '[활성]' : '[비활성]'}`); + }); + } + } + + console.log('---'); +}, 5000); + +// 3초 후 카메라 목록 요청 +setTimeout(() => { + console.log('🔍 카메라 목록 요청...'); + plugin.requestCameraList(); +}, 3000); + +// 8초 후 첫 번째 카메라로 전환 +setTimeout(() => { + console.log('🎬 첫 번째 카메라로 전환...'); + plugin.switchCamera(0); +}, 8000); + +// 13초 후 두 번째 카메라로 전환 +setTimeout(() => { + console.log('🎬 두 번째 카메라로 전환...'); + plugin.switchCamera(1); +}, 13000); + +// 18초 후 세 번째 카메라로 전환 (있다면) +setTimeout(() => { + console.log('🎬 세 번째 카메라로 전환...'); + plugin.switchCamera(2); +}, 18000); + +// 25초 후 종료 +setTimeout(() => { + console.log('🛑 테스트 종료...'); + clearInterval(connectionCheckInterval); + plugin.disconnect(); + process.exit(0); +}, 25000); + +// 프로세스 종료 시 정리 +process.on('SIGINT', () => { + console.log('🛑 테스트 중단...'); + clearInterval(connectionCheckInterval); + plugin.disconnect(); + process.exit(0); +}); \ No newline at end of file diff --git a/Streamdeck/com.mirabox.streamingle.sdPlugin/dist/com.mirabox.streamingle.sdPlugin/debug.js b/Streamdeck/com.mirabox.streamingle.sdPlugin/dist/com.mirabox.streamingle.sdPlugin/debug.js new file mode 100644 index 00000000..aca3e6b2 --- /dev/null +++ b/Streamdeck/com.mirabox.streamingle.sdPlugin/dist/com.mirabox.streamingle.sdPlugin/debug.js @@ -0,0 +1,133 @@ +const fs = require('fs'); +const path = require('path'); + +console.log('=== Streamingle 플러그인 디버깅 도구 ==='); + +// 1. 플러그인 파일 확인 +console.log('\n1. 플러그인 파일 확인:'); +const pluginDir = __dirname; +const requiredFiles = [ + 'manifest.json', + 'plugin.js', + 'package.json', + 'propertyinspector.html' +]; + +requiredFiles.forEach(file => { + const filePath = path.join(pluginDir, file); + if (fs.existsSync(filePath)) { + const stats = fs.statSync(filePath); + console.log(`✅ ${file} - ${stats.size} bytes`); + } else { + console.log(`❌ ${file} - 파일 없음`); + } +}); + +// 2. manifest.json 내용 확인 +console.log('\n2. manifest.json 내용:'); +try { + const manifestPath = path.join(pluginDir, 'manifest.json'); + const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8')); + console.log('✅ manifest.json 파싱 성공'); + console.log(` SDK 버전: ${manifest.SDKVersion}`); + console.log(` 코드 경로: ${manifest.CodePath}`); + console.log(` 플러그인 이름: ${manifest.Name}`); + console.log(` 액션 UUID: ${manifest.Actions[0].UUID}`); +} catch (error) { + console.log(`❌ manifest.json 파싱 실패: ${error.message}`); +} + +// 3. package.json 내용 확인 +console.log('\n3. package.json 내용:'); +try { + const packagePath = path.join(pluginDir, 'package.json'); + const packageJson = JSON.parse(fs.readFileSync(packagePath, 'utf8')); + console.log('✅ package.json 파싱 성공'); + console.log(` 이름: ${packageJson.name}`); + console.log(` 버전: ${packageJson.version}`); + console.log(` 메인 파일: ${packageJson.main}`); + console.log(` 의존성: ${Object.keys(packageJson.dependencies || {}).join(', ')}`); +} catch (error) { + console.log(`❌ package.json 파싱 실패: ${error.message}`); +} + +// 4. WebSocket 연결 테스트 +console.log('\n4. WebSocket 연결 테스트:'); +const WebSocket = require('ws'); + +function testWebSocketConnection() { + return new Promise((resolve) => { + console.log(' 연결 시도 중...'); + + const ws = new WebSocket('ws://127.0.0.1:10701/'); + + const timeout = setTimeout(() => { + console.log(' ❌ 연결 타임아웃 (5초)'); + ws.close(); + resolve(false); + }, 5000); + + ws.on('open', () => { + console.log(' ✅ WebSocket 연결 성공!'); + clearTimeout(timeout); + ws.close(); + resolve(true); + }); + + ws.on('error', (error) => { + console.log(` ❌ WebSocket 연결 실패: ${error.message}`); + clearTimeout(timeout); + resolve(false); + }); + + ws.on('close', (code, reason) => { + console.log(` 🔌 연결 종료 - 코드: ${code}, 이유: ${reason || '알 수 없음'}`); + }); + }); +} + +testWebSocketConnection().then((connected) => { + console.log(`\n결과: ${connected ? '✅ 연결 가능' : '❌ 연결 불가'}`); + + // 5. 포트 상태 확인 + console.log('\n5. 포트 상태 확인:'); + const { exec } = require('child_process'); + + exec('netstat -an | findstr :10701', (error, stdout, stderr) => { + if (error) { + console.log(` ❌ netstat 실행 실패: ${error.message}`); + return; + } + + if (stdout.trim()) { + console.log(' ✅ 포트 10701 사용 중:'); + stdout.split('\n').forEach(line => { + if (line.trim()) { + console.log(` ${line.trim()}`); + } + }); + } else { + console.log(' ❌ 포트 10701에서 서비스 없음'); + } + }); +}); + +// 6. StreamDock 플러그인 경로 확인 +console.log('\n6. StreamDock 플러그인 경로:'); +const streamDockPath = 'C:\\Users\\qscft\\AppData\\Roaming\\HotSpot\\StreamDock\\plugins\\com.mirabox.streamingle.sdPlugin'; +if (fs.existsSync(streamDockPath)) { + console.log(` ✅ StreamDock 플러그인 폴더 존재: ${streamDockPath}`); + + const files = fs.readdirSync(streamDockPath); + console.log(` 📁 파일 개수: ${files.length}개`); + files.forEach(file => { + const filePath = path.join(streamDockPath, file); + const stats = fs.statSync(filePath); + const type = stats.isDirectory() ? '📁' : '📄'; + console.log(` ${type} ${file} - ${stats.size} bytes`); + }); +} else { + console.log(` ❌ StreamDock 플러그인 폴더 없음: ${streamDockPath}`); +} + +console.log('\n=== 디버깅 완료 ==='); \ No newline at end of file diff --git a/Streamdeck/com.mirabox.streamingle.sdPlugin/dist/com.mirabox.streamingle.sdPlugin/images/action.png b/Streamdeck/com.mirabox.streamingle.sdPlugin/dist/com.mirabox.streamingle.sdPlugin/images/action.png new file mode 100644 index 00000000..45b93e4e --- /dev/null +++ b/Streamdeck/com.mirabox.streamingle.sdPlugin/dist/com.mirabox.streamingle.sdPlugin/images/action.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:62da15b1333bcf15bb1ef33c05fb246406b76a8bf9710a4df6e7bd0a16ef62b5 +size 42151 diff --git a/Streamdeck/com.mirabox.streamingle.sdPlugin/dist/com.mirabox.streamingle.sdPlugin/manifest.json b/Streamdeck/com.mirabox.streamingle.sdPlugin/dist/com.mirabox.streamingle.sdPlugin/manifest.json new file mode 100644 index 00000000..fcc7d856 --- /dev/null +++ b/Streamdeck/com.mirabox.streamingle.sdPlugin/dist/com.mirabox.streamingle.sdPlugin/manifest.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f646099cafd148b8c2b4c8dc1922ab6e98354df7194717b6d82812b05af66f43 +size 1353 diff --git a/Streamdeck/com.mirabox.streamingle.sdPlugin/dist/com.mirabox.streamingle.sdPlugin/plugin/index.html b/Streamdeck/com.mirabox.streamingle.sdPlugin/dist/com.mirabox.streamingle.sdPlugin/plugin/index.html new file mode 100644 index 00000000..84e63f28 --- /dev/null +++ b/Streamdeck/com.mirabox.streamingle.sdPlugin/dist/com.mirabox.streamingle.sdPlugin/plugin/index.html @@ -0,0 +1,314 @@ + + + + + Streamingle Plugin Main + + +
+ 플러그인 메인 - 버튼 클릭 처리
+ Property Inspector에서 설정 관리 +
+ + + + \ No newline at end of file diff --git a/Streamdeck/com.mirabox.streamingle.sdPlugin/dist/com.mirabox.streamingle.sdPlugin/plugin/index.js b/Streamdeck/com.mirabox.streamingle.sdPlugin/dist/com.mirabox.streamingle.sdPlugin/plugin/index.js new file mode 100644 index 00000000..ee08eef3 --- /dev/null +++ b/Streamdeck/com.mirabox.streamingle.sdPlugin/dist/com.mirabox.streamingle.sdPlugin/plugin/index.js @@ -0,0 +1,228 @@ +/** + * Streamingle Camera Controller Plugin for StreamDeck + * StreamDeck SDK 표준 구조로 수정 + */ + +const { Plugins, Actions, log } = require('./utils/plugin'); +const WebSocket = require('ws'); + +// 플러그인 인스턴스 생성 +const plugin = new Plugins('streamingle'); + +// Unity WebSocket 연결 관리 +let unityWebSocket = null; +let reconnectTimer = null; +let isUnityConnected = false; +let cameraData = null; +let currentCamera = 0; + +// 로그 함수 +function writeLog(message) { + const timestamp = new Date().toISOString(); + log.info(`[${timestamp}] ${message}`); +} + +// Unity WebSocket 서버 연결 함수 +function connectToUnity() { + if (unityWebSocket) { + unityWebSocket.close(); + } + + writeLog('🔌 Unity WebSocket 연결 시도: ws://localhost:10701'); + unityWebSocket = new WebSocket('ws://localhost:10701'); + + unityWebSocket.on('open', function() { + writeLog('✅ Unity WebSocket 서버에 연결됨'); + isUnityConnected = true; + requestCameraList(); + }); + + unityWebSocket.on('message', function(data) { + try { + writeLog('📨 Unity 메시지 수신: ' + data.toString()); + const message = JSON.parse(data.toString()); + handleUnityMessage(message); + } catch (error) { + writeLog('❌ Unity 메시지 파싱 오류: ' + error.message); + } + }); + + unityWebSocket.on('error', function(err) { + writeLog('❌ Unity WebSocket 연결 오류: ' + err.message); + isUnityConnected = false; + scheduleReconnect(); + }); + + unityWebSocket.on('close', function() { + writeLog('🔌 Unity WebSocket 연결 종료'); + isUnityConnected = false; + scheduleReconnect(); + }); +} + +// Unity 메시지 처리 +function handleUnityMessage(message) { + switch (message.type) { + case 'cameraList': + cameraData = message; + currentCamera = message.currentCamera || 0; + writeLog('📹 카메라 목록 수신: ' + JSON.stringify(message)); + break; + case 'cameraSwitched': + currentCamera = message.cameraIndex; + writeLog('📹 카메라 전환 완료: ' + message.cameraIndex); + break; + default: + writeLog('📨 알 수 없는 Unity 메시지: ' + message.type); + } +} + +// 카메라 목록 요청 +function requestCameraList() { + if (!isUnityConnected || !unityWebSocket) { + writeLog('❌ Unity 연결 안됨 - 카메라 목록 요청 불가'); + return; + } + + const message = { + type: 'getCameraList', + requestId: Date.now().toString() + }; + + writeLog('📤 Unity에 카메라 목록 요청: ' + JSON.stringify(message)); + unityWebSocket.send(JSON.stringify(message)); +} + +// 카메라 전환 +function switchCamera(cameraIndex) { + if (!isUnityConnected || !unityWebSocket) { + writeLog('❌ Unity 연결 안됨 - 카메라 전환 불가'); + return false; + } + + const message = { + type: 'switchCamera', + cameraIndex: cameraIndex + }; + + writeLog('📤 Unity에 카메라 전환 요청: ' + JSON.stringify(message)); + unityWebSocket.send(JSON.stringify(message)); + return true; +} + +// 재연결 스케줄링 +function scheduleReconnect() { + if (reconnectTimer) { + clearTimeout(reconnectTimer); + } + + writeLog('🔄 3초 후 Unity 재연결 시도'); + reconnectTimer = setTimeout(() => { + reconnectTimer = null; + connectToUnity(); + }, 3000); +} + +// 플러그인 설정 수신 시 Unity 연결 +plugin.didReceiveGlobalSettings = ({ payload: { settings } }) => { + log.info('didReceiveGlobalSettings', settings); + writeLog('플러그인 설정 수신됨'); + + // 플러그인 시작 시 Unity 연결 + connectToUnity(); +}; + +// Streamingle 액션 클래스 +plugin.streamingle = new Actions({ + default: { + cameraIndex: 0, + autoSwitch: true, + isUnityConnected: false, + cameraList: [], + currentCamera: null + }, + + // 버튼이 나타날 때 + async _willAppear({ context, payload }) { + writeLog(`🎯 버튼 나타남: ${context.substring(0, 8)}...`); + + // Unity 연결 시도 (첫 번째 버튼에서만) + if (!unityWebSocket) { + writeLog('🚀 첫 번째 버튼 - Unity 연결 시도'); + connectToUnity(); + } + }, + + // 버튼이 사라질 때 + _willDisappear({ context }) { + writeLog(`🎯 버튼 사라짐: ${context.substring(0, 8)}...`); + }, + + // Property Inspector가 열릴 때 + _propertyInspectorDidAppear({ context }) { + writeLog(`📋 Property Inspector 열림: ${context.substring(0, 8)}...`); + + // Unity 연결 강제 확인 + if (!isUnityConnected) { + writeLog('🔄 Unity 연결 시도 중...'); + connectToUnity(); + } + }, + + // Property Inspector에서 메시지 수신 + sendToPlugin({ payload, context }) { + writeLog(`📨 Property Inspector 메시지 수신: ${context.substring(0, 8)}...`); + + try { + const message = JSON.parse(payload); + writeLog('📨 파싱된 메시지: ' + JSON.stringify(message)); + + switch (message.command) { + case 'refreshCameraList': + requestCameraList(); + break; + case 'forceReconnect': + connectToUnity(); + break; + case 'getInitialSettings': + writeLog('📥 Property Inspector에서 초기 설정 요청'); + plugin.sendToPropertyInspector({ + type: 'connection_status', + connected: isUnityConnected + }); + + if (isUnityConnected && cameraData) { + plugin.sendToPropertyInspector({ + type: 'camera_data', + camera_data: cameraData, + current_camera: currentCamera + }); + } + break; + default: + writeLog('❓ 알 수 없는 Property Inspector 명령: ' + message.command); + } + } catch (error) { + writeLog('❌ Property Inspector 메시지 파싱 오류: ' + error.message); + } + }, + + // 버튼 클릭 시 + keyUp({ context, payload }) { + writeLog(`🎯 버튼 클릭: ${context.substring(0, 8)}...`); + + // 설정에서 카메라 인덱스 가져오기 + const settings = this.data[context] || {}; + const cameraIndex = settings.cameraIndex || 0; + + writeLog(`📹 카메라 전환 시도: ${cameraIndex}`); + + if (switchCamera(cameraIndex)) { + writeLog(`✅ 카메라 전환 요청 완료: ${cameraIndex}`); + } else { + writeLog(`❌ 카메라 전환 요청 실패: ${cameraIndex}`); + } + } +}); + +writeLog('🚀 Streamingle 플러그인 초기화 완료'); diff --git a/Streamdeck/com.mirabox.streamingle.sdPlugin/dist/com.mirabox.streamingle.sdPlugin/plugin/log/streamingle_plugin b/Streamdeck/com.mirabox.streamingle.sdPlugin/dist/com.mirabox.streamingle.sdPlugin/plugin/log/streamingle_plugin new file mode 100644 index 00000000..e69de29b diff --git a/Streamdeck/com.mirabox.streamingle.sdPlugin/dist/com.mirabox.streamingle.sdPlugin/plugin/utils/plugin.js b/Streamdeck/com.mirabox.streamingle.sdPlugin/dist/com.mirabox.streamingle.sdPlugin/plugin/utils/plugin.js new file mode 100644 index 00000000..60eac675 --- /dev/null +++ b/Streamdeck/com.mirabox.streamingle.sdPlugin/dist/com.mirabox.streamingle.sdPlugin/plugin/utils/plugin.js @@ -0,0 +1,223 @@ +// 配置日志文件 +const now = new Date(); +const log = require('log4js').configure({ + appenders: { + file: { type: 'file', filename: `./log/${now.getFullYear()}.${now.getMonth() + 1}.${now.getDate()}.log` } + }, + categories: { + default: { appenders: ['file'], level: 'info' } + } +}).getLogger(); + +//################################################## +//##################全局异常捕获##################### +process.on('uncaughtException', (error) => { + log.error('Uncaught Exception:', error); +}); +process.on('unhandledRejection', (reason) => { + log.error('Unhandled Rejection:', reason); +}); +//################################################## +//################################################## + + +// 插件类 +const ws = require('ws'); +class Plugins { + static language = (() => { + try { + if (process.argv[9] && process.argv[9] !== 'undefined') { + const parsed = JSON.parse(process.argv[9]); + return parsed.application?.language || 'en'; + } + return 'en'; + } catch (error) { + return 'en'; + } + })(); + static globalSettings = {}; + getGlobalSettingsFlag = true; + constructor() { + if (Plugins.instance) { + return Plugins.instance; + } + // log.info("process.argv", process.argv); + this.ws = new ws("ws://127.0.0.1:" + process.argv[3]); + this.ws.on('open', () => this.ws.send(JSON.stringify({ uuid: process.argv[5], event: process.argv[7] }))); + this.ws.on('close', process.exit); + this.ws.on('message', e => { + if (this.getGlobalSettingsFlag) { + // 只获取一次 + this.getGlobalSettingsFlag = false; + this.getGlobalSettings(); + } + const data = JSON.parse(e.toString()); + const action = data.action?.split('.').pop(); + this[action]?.[data.event]?.(data); + if (data.event === 'didReceiveGlobalSettings') { + Plugins.globalSettings = data.payload.settings; + } + this[data.event]?.(data); + }); + Plugins.instance = this; + } + + setGlobalSettings(payload) { + Plugins.globalSettings = payload; + this.ws.send(JSON.stringify({ + event: "setGlobalSettings", + context: process.argv[5], payload + })); + } + + getGlobalSettings() { + this.ws.send(JSON.stringify({ + event: "getGlobalSettings", + context: process.argv[5], + })); + } + // 设置标题 + setTitle(context, str, row = 0, num = 6) { + let newStr = null; + if (row && str) { + let nowRow = 1, strArr = str.split(''); + strArr.forEach((item, index) => { + if (nowRow < row && index >= nowRow * num) { nowRow++; newStr += '\n'; } + if (nowRow <= row && index < nowRow * num) { newStr += item; } + }); + if (strArr.length > row * num) { newStr = newStr.substring(0, newStr.length - 1); newStr += '..'; } + } + this.ws.send(JSON.stringify({ + event: "setTitle", + context, payload: { + target: 0, + title: newStr || str + '' + } + })); + } + // 设置背景 + setImage(context, url) { + this.ws.send(JSON.stringify({ + event: "setImage", + context, payload: { + target: 0, + image: url + } + })); + } + // 设置状态 + setState(context, state) { + this.ws.send(JSON.stringify({ + event: "setState", + context, payload: { state } + })); + } + // 保存持久化数据 + setSettings(context, payload) { + this.ws.send(JSON.stringify({ + event: "setSettings", + context, payload + })); + } + + // 在按键上展示警告 + showAlert(context) { + this.ws.send(JSON.stringify({ + event: "showAlert", + context + })); + } + + // 在按键上展示成功 + showOk(context) { + this.ws.send(JSON.stringify({ + event: "showOk", + context + })); + } + // 发送给属性检测器 + sendToPropertyInspector(payload) { + this.ws.send(JSON.stringify({ + action: Actions.currentAction, + context: Actions.currentContext, + payload, event: "sendToPropertyInspector" + })); + } + // 用默认浏览器打开网页 + openUrl(url) { + this.ws.send(JSON.stringify({ + event: "openUrl", + payload: { url } + })); + } +}; + +// 操作类 +class Actions { + constructor(data) { + this.data = {}; + this.default = {}; + Object.assign(this, data); + } + // 属性检查器显示时 + static currentAction = null; + static currentContext = null; + static actions = {}; + propertyInspectorDidAppear(data) { + Actions.currentAction = data.action; + Actions.currentContext = data.context; + this._propertyInspectorDidAppear?.(data); + } + // 初始化数据 + willAppear(data) { + Plugins.globalContext = data.context; + Actions.actions[data.context] = data.action + const { context, payload: { settings } } = data; + this.data[context] = Object.assign({ ...this.default }, settings); + this._willAppear?.(data); + } + + didReceiveSettings(data) { + this.data[data.context] = data.payload.settings; + this._didReceiveSettings?.(data); + } + // 行动销毁 + willDisappear(data) { + this._willDisappear?.(data); + delete this.data[data.context]; + } +} + +class EventEmitter { + constructor() { + this.events = {}; + } + + // 订阅事件 + subscribe(event, listener) { + if (!this.events[event]) { + this.events[event] = []; + } + this.events[event].push(listener); + } + + // 取消订阅 + unsubscribe(event, listenerToRemove) { + if (!this.events[event]) return; + + this.events[event] = this.events[event].filter(listener => listener !== listenerToRemove); + } + + // 发布事件 + emit(event, data) { + if (!this.events[event]) return; + this.events[event].forEach(listener => listener(data)); + } +} + +module.exports = { + log, + Plugins, + Actions, + EventEmitter +}; \ No newline at end of file diff --git a/Streamdeck/com.mirabox.streamingle.sdPlugin/dist/com.mirabox.streamingle.sdPlugin/propertyInspector/camera/index.html b/Streamdeck/com.mirabox.streamingle.sdPlugin/dist/com.mirabox.streamingle.sdPlugin/propertyInspector/camera/index.html new file mode 100644 index 00000000..0e2f2161 --- /dev/null +++ b/Streamdeck/com.mirabox.streamingle.sdPlugin/dist/com.mirabox.streamingle.sdPlugin/propertyInspector/camera/index.html @@ -0,0 +1,187 @@ + + + + + Streamingle Camera Inspector + + + + +
+
+ Unity 연결 안됨 +
+ + +
+ +
+ +
+
현재 카메라: -
+
+ + +
+ + +
+ + +
+ +
+ + +
+ +
+
+ + + + + \ No newline at end of file diff --git a/Streamdeck/com.mirabox.streamingle.sdPlugin/dist/com.mirabox.streamingle.sdPlugin/propertyInspector/camera/index.js b/Streamdeck/com.mirabox.streamingle.sdPlugin/dist/com.mirabox.streamingle.sdPlugin/propertyInspector/camera/index.js new file mode 100644 index 00000000..74a97451 --- /dev/null +++ b/Streamdeck/com.mirabox.streamingle.sdPlugin/dist/com.mirabox.streamingle.sdPlugin/propertyInspector/camera/index.js @@ -0,0 +1,778 @@ +/** + * Streamingle Camera Controller - Property Inspector + * 엘가토 공식 구조 기반 단순화 버전 + */ + +// Global variables +let websocket = null; +let uuid = null; +let actionContext = null; // 현재 액션의 컨텍스트 +let settings = {}; + +// Context별 설정 관리 +const contextSettings = new Map(); +let currentActionContext = null; + +// Unity 연결 상태 (Plugin Main에서 받아옴) +let isUnityConnected = false; +let cameraData = []; +let currentCamera = 0; + +// DOM elements +let statusDot = null; +let connectionStatus = null; +let cameraSelect = null; +let currentCameraDisplay = null; +let refreshButton = null; + +// 화면에 로그를 표시하는 함수 +function logToScreen(msg, color = "#fff") { + let logDiv = document.getElementById('logArea'); + if (!logDiv) { + logDiv = document.createElement('div'); + logDiv.id = 'logArea'; + logDiv.style.background = '#111'; + logDiv.style.color = '#fff'; + logDiv.style.fontSize = '11px'; + logDiv.style.padding = '8px'; + logDiv.style.marginTop = '16px'; + logDiv.style.height = '120px'; + logDiv.style.overflowY = 'auto'; + document.body.appendChild(logDiv); + } + const line = document.createElement('div'); + line.style.color = color; + line.textContent = `[${new Date().toLocaleTimeString()}] ${msg}`; + logDiv.appendChild(line); + logDiv.scrollTop = logDiv.scrollHeight; +} + +// 기존 console.log/console.error를 화면에도 출력 +const origLog = console.log; +console.log = function(...args) { + origLog.apply(console, args); + logToScreen(args.map(a => (typeof a === 'object' ? JSON.stringify(a) : a)).join(' '), '#0f0'); +}; +const origErr = console.error; +console.error = function(...args) { + origErr.apply(console, args); + logToScreen(args.map(a => (typeof a === 'object' ? JSON.stringify(a) : a)).join(' '), '#f55'); +}; + +console.log('🔧 Property Inspector script loaded'); + +// Initialize when DOM is loaded +document.addEventListener('DOMContentLoaded', function() { + console.log('📋 Property Inspector 초기화'); + initializePropertyInspector(); +}); + +// Initialize Property Inspector +function initializePropertyInspector() { + // Get DOM elements + statusDot = document.getElementById('statusDot'); + connectionStatus = document.getElementById('connection-status'); + cameraSelect = document.getElementById('camera-select'); + currentCameraDisplay = document.getElementById('current-camera'); + refreshButton = document.getElementById('refresh-button'); + + // Setup event listeners + if (cameraSelect) { + cameraSelect.addEventListener('change', onCameraSelectionChanged); + } + + if (refreshButton) { + refreshButton.addEventListener('click', onRefreshClicked); + } + + console.log('✅ Property Inspector 준비 완료'); +} + +// Send message to plugin +function sendToPlugin(command, data = {}) { + if (!websocket) { + console.error('❌ WebSocket not available'); + return; + } + + try { + const message = { + command: command, + context: uuid, + ...data + }; + + // StreamDeck SDK 표준 방식 - sendToPlugin 이벤트 사용 + const payload = { + event: 'sendToPlugin', + context: uuid, + payload: message + }; + + websocket.send(JSON.stringify(payload)); + console.log('📤 Message sent to plugin:', command, data); + } catch (error) { + console.error('❌ Failed to send message to plugin:', error); + } +} + +// Update connection status display +function updateConnectionStatus(isConnected) { + console.log('🔄 Connection status update:', isConnected); + + // 전역 변수도 업데이트 + isUnityConnected = isConnected; + + if (statusDot) { + statusDot.className = `dot ${isConnected ? 'green' : 'red'}`; + } + + if (connectionStatus) { + connectionStatus.textContent = isConnected ? 'Unity 연결됨' : 'Unity 연결 안됨'; + connectionStatus.className = isConnected ? 'connected' : 'disconnected'; + } + + if (cameraSelect) { + cameraSelect.disabled = !isConnected; + } + + if (refreshButton) { + refreshButton.disabled = !isConnected; + } +} + +// Update camera data display +function updateCameraData(cameraDataParam, currentCamera) { + console.log('📹 Camera data update:', cameraDataParam, currentCamera); + + if (cameraSelect && cameraDataParam) { + // Clear existing options + cameraSelect.innerHTML = ''; + + // cameraDataParam이 직접 배열인지 확인 + let cameras = cameraDataParam; + if (cameraDataParam.cameras) { + cameras = cameraDataParam.cameras; + } else if (Array.isArray(cameraDataParam)) { + cameras = cameraDataParam; + } + + console.log('📹 처리할 카메라 배열:', cameras); + + if (cameras && cameras.length > 0) { + // 전역 변수에 카메라 데이터 저장 + cameraData = cameras; + console.log('💾 전역 cameraData 저장됨:', cameraData.length + '개'); + + // Add camera options + cameras.forEach((camera, index) => { + const option = document.createElement('option'); + option.value = index; + option.textContent = `카메라 ${index + 1}`; + if (camera.name) { + option.textContent += ` (${camera.name})`; + } + cameraSelect.appendChild(option); + }); + + // Set current selection + if (typeof currentCamera === 'number') { + cameraSelect.value = currentCamera; + } + + cameraSelect.disabled = false; + console.log('✅ 카메라 목록 업데이트 완료:', cameras.length + '개'); + } else { + console.log('⚠️ 카메라 데이터가 없거나 빈 배열'); + cameraSelect.disabled = true; + } + } + + // Update current camera display + updateCurrentCameraDisplay(currentCamera); +} + +// Update current camera display +function updateCurrentCameraDisplay(currentCamera) { + if (currentCameraDisplay) { + if (typeof currentCamera === 'number') { + currentCameraDisplay.textContent = `현재 카메라: ${currentCamera + 1}`; + } else { + currentCameraDisplay.textContent = '현재 카메라: -'; + } + } +} + +// Handle camera selection change +function onCameraSelectionChanged() { + if (!cameraSelect || !currentActionContext) return; + + const selectedIndex = parseInt(cameraSelect.value, 10); + if (isNaN(selectedIndex)) return; + + console.log('🎯 카메라 선택 변경:', selectedIndex); + console.log('📋 현재 cameraData:', cameraData); + console.log('📋 cameraData 길이:', cameraData ? cameraData.length : 'undefined'); + + // StreamDeck에 설정 저장 (Plugin Main에서 didReceiveSettings 이벤트 발생) + if (websocket) { + const setSettingsMessage = { + event: 'setSettings', + context: currentActionContext, + payload: { + cameraIndex: selectedIndex, + cameraList: cameraData // 현재 카메라 목록 포함 + } + }; + + console.log('📤 Plugin Main으로 전송할 데이터:', setSettingsMessage.payload); + websocket.send(JSON.stringify(setSettingsMessage)); + console.log('💾 설정 저장됨 - Plugin Main에서 버튼 제목 업데이트됨'); + } + + // UI 업데이트 + updateCurrentCameraDisplay(selectedIndex); +} + +// Handle refresh button click +function onRefreshClicked() { + console.log('🔄 새로고침 버튼 클릭 - Plugin Main에 카메라 목록 요청'); + + // Plugin Main에 카메라 목록 요청 + sendToPlugin('requestCameraList'); +} + +// Unity 연결은 Plugin Main에서만 처리 +function startUnityAutoReconnect() { + console.log('🩺 Property Inspector에서는 Unity 연결을 직접 관리하지 않음 - Plugin Main에서 처리됨'); +} + +// Unity 재연결 시도 (제거) +function attemptUnityReconnect() { + if (isShuttingDown || isConnecting || unityReconnectInterval) return; + + unityConnectionAttempts++; + + // 재연결 간격 조정 + let delay; + if (unityConnectionAttempts <= 3) { + delay = 2000; // 처음 3번은 2초 간격 + } else if (unityConnectionAttempts <= 10) { + delay = 5000; // 4-10번은 5초 간격 + } else { + delay = 30000; // 그 이후는 30초 간격 + } + + console.log(`🔄 [Property Inspector] ${delay/1000}초 후 Unity 재연결 시도... (${unityConnectionAttempts}번째 시도)`); + + unityReconnectInterval = setTimeout(() => { + unityReconnectInterval = null; + connectToUnity().catch(error => { + console.error(`❌ [Property Inspector] Unity 재연결 실패:`, error); + // 실패해도 계속 시도 + if (!isShuttingDown) { + attemptUnityReconnect(); + } + }); + }, delay); +} + +// Unity WebSocket 연결 (개선된 버전) +function connectToUnity() { + return new Promise((resolve, reject) => { + // 글로벌 상태 확인 + if (window.sharedUnityConnected && window.sharedUnitySocket) { + console.log('✅ [Property Inspector] 기존 Unity 연결 재사용'); + isUnityConnected = true; + unitySocket = window.sharedUnitySocket; + updateConnectionStatus(true); + resolve(); + return; + } + + if (isUnityConnected) { + console.log('✅ [Property Inspector] Unity 이미 연결됨'); + resolve(); + return; + } + + if (isConnecting) { + console.log('⏳ [Property Inspector] Unity 연결 중... 대기'); + reject(new Error('이미 연결 중')); + return; + } + + isConnecting = true; + window.sharedIsConnecting = true; + console.log(`🔌 [Property Inspector] Unity 연결 시도... (시도 ${unityConnectionAttempts + 1}회)`); + + try { + unitySocket = new WebSocket('ws://localhost:10701'); + + const connectionTimeout = setTimeout(() => { + isConnecting = false; + window.sharedIsConnecting = false; + console.log('⏰ [Property Inspector] Unity 연결 타임아웃'); + if (unitySocket) { + unitySocket.close(); + } + reject(new Error('연결 타임아웃')); + }, 5000); + + unitySocket.onopen = function() { + clearTimeout(connectionTimeout); + isConnecting = false; + isUnityConnected = true; + unityConnectionAttempts = 0; // 성공 시 재시도 카운터 리셋 + + // 재연결 타이머 정리 + if (unityReconnectInterval) { + clearTimeout(unityReconnectInterval); + unityReconnectInterval = null; + } + + // 글로벌 상태 저장 + window.sharedUnitySocket = unitySocket; + window.sharedUnityConnected = true; + window.sharedIsConnecting = false; + + console.log('✅ [Property Inspector] Unity 연결 성공!'); + updateConnectionStatus(true); + resolve(); + }; + + unitySocket.onmessage = function(event) { + try { + const message = JSON.parse(event.data); + handleUnityMessage(message); + } catch (error) { + console.error('❌ [Property Inspector] Unity 메시지 파싱 오류:', error); + } + }; + + unitySocket.onclose = function(event) { + clearTimeout(connectionTimeout); + const wasConnected = isUnityConnected; + isConnecting = false; + isUnityConnected = false; + + // 글로벌 상태 정리 + window.sharedUnitySocket = null; + window.sharedUnityConnected = false; + window.sharedIsConnecting = false; + + if (wasConnected) { + console.log(`❌ [Property Inspector] Unity 연결 끊어짐 (코드: ${event.code}, 이유: ${event.reason || '알 수 없음'})`); + } + updateConnectionStatus(false); + unitySocket = null; + + if (!event.wasClean) { + reject(new Error('연결 실패')); + } + + // 자동 재연결 시도 + if (!isShuttingDown) { + attemptUnityReconnect(); + } + }; + + unitySocket.onerror = function(error) { + clearTimeout(connectionTimeout); + isConnecting = false; + window.sharedIsConnecting = false; + console.error('❌ [Property Inspector] Unity 연결 오류:', error); + isUnityConnected = false; + updateConnectionStatus(false); + reject(error); + }; + + } catch (error) { + isConnecting = false; + window.sharedIsConnecting = false; + console.error('❌ [Property Inspector] Unity WebSocket 생성 실패:', error); + reject(error); + } + }); +} + +// Unity 메시지 처리 +function handleUnityMessage(message) { + const messageType = message.type; + + if (messageType === 'connection_established') { + console.log('🎉 Unity 연결 확인됨'); + if (message.data && message.data.camera_data) { + console.log('📹 연결 시 카메라 데이터 수신 (초기 로드)'); + updateCameraUI(message.data.camera_data.presets, message.data.camera_data.current_index); + // 이미 카메라 데이터를 받았으므로 추가 요청하지 않음 + cameraData = message.data.camera_data.presets; // 글로벌 변수에 저장 + window.sharedCameraData = cameraData; // 브라우저 세션에서 공유 + } + } else if (messageType === 'camera_list_response') { + console.log('📹 카메라 목록 응답 수신 (요청에 대한 응답)'); + if (message.data && message.data.camera_data) { + updateCameraUI(message.data.camera_data.presets, message.data.camera_data.current_index); + cameraData = message.data.camera_data.presets; // 글로벌 변수에 저장 + window.sharedCameraData = cameraData; // 브라우저 세션에서 공유 + } + } else if (messageType === 'camera_changed') { + console.log('📹 카메라 변경 알림 수신'); + if (message.data && typeof message.data.camera_index === 'number') { + updateCurrentCamera(message.data.camera_index); + } + } +} + +function updateCameraUI(cameras, currentIndex) { + if (!cameras || !Array.isArray(cameras)) { + console.error('❌ 잘못된 카메라 데이터'); + return; + } + + console.log('📹 카메라 UI 업데이트:', cameras.length + '개'); + + const cameraSelect = document.getElementById('camera-select'); + const currentCameraDisplay = document.getElementById('current-camera'); + + if (!cameraSelect) { + console.error('❌ camera-select 요소를 찾을 수 없음'); + return; + } + + // 카메라 목록 업데이트 + cameraSelect.innerHTML = ''; + + cameras.forEach((camera, index) => { + const option = document.createElement('option'); + option.value = index; + option.textContent = `${index + 1}. ${camera.name}`; + cameraSelect.appendChild(option); + }); + + // 현재 컨텍스트의 설정 가져오기 + const currentSettings = getContextSettings(currentActionContext); + + // 카메라 목록 저장 + const newSettings = { + ...currentSettings, + cameraList: cameras + }; + saveContextSettings(currentActionContext, newSettings); + + // 이미 설정된 카메라가 있으면 선택 + if (currentSettings && typeof currentSettings.cameraIndex === 'number') { + cameraSelect.value = currentSettings.cameraIndex; + + // 설정된 카메라의 이름으로 현재 표시만 업데이트 (버튼 제목은 Plugin Main에서 처리) + const selectedCamera = cameras[currentSettings.cameraIndex]; + if (selectedCamera) { + currentCameraDisplay.textContent = `현재: ${selectedCamera.name}`; + console.log('📋 기존 카메라 설정 복원:', selectedCamera.name); + } + } else { + // 설정이 없으면 Unity의 현재 카메라 사용 + if (typeof currentIndex === 'number' && currentIndex >= 0 && cameras[currentIndex]) { + cameraSelect.value = currentIndex; + currentCameraDisplay.textContent = `현재: ${cameras[currentIndex].name}`; + } else { + currentCameraDisplay.textContent = '현재: 없음'; + } + } + + console.log('✅ 카메라 UI 업데이트 완료'); +} + +// Unity에서 카메라 목록 요청 +function requestCameraListFromUnity() { + if (isUnityConnected && unitySocket) { + const message = JSON.stringify({ type: 'get_camera_list' }); + unitySocket.send(message); + console.log('📤 Unity에 카메라 목록 요청:', message); + } +} + +// Unity에서 카메라 전환 +function switchCameraInUnity(cameraIndex) { + if (isUnityConnected && unitySocket) { + const message = JSON.stringify({ + type: 'switch_camera', + data: { + camera_index: cameraIndex + } + }); + unitySocket.send(message); + console.log('📤 Unity에 카메라 전환 요청:', message); + } +} + +// Unity에서 받은 카메라 데이터로 UI 업데이트 +function updateCameraDataFromUnity() { + console.log('📹 Unity 카메라 데이터로 UI 업데이트:', cameraData); + + if (cameraSelect && cameraData && cameraData.length > 0) { + // Clear existing options + cameraSelect.innerHTML = ''; + + // Add camera options + cameraData.forEach((camera, index) => { + const option = document.createElement('option'); + option.value = index; + option.textContent = camera.name || `카메라 ${index + 1}`; + cameraSelect.appendChild(option); + }); + + // Set current selection + cameraSelect.value = currentCamera; + cameraSelect.disabled = false; + + console.log('✅ 카메라 선택 목록 업데이트 완료'); + } + + // Update current camera display + updateCurrentCameraDisplay(currentCamera); +} + +// Handle messages from plugin +function handleMessage(jsonObj) { + console.log('📨 Message received:', jsonObj); + + try { + if (jsonObj.event === 'sendToPropertyInspector') { + // payload는 직접 객체로 전달됨 + const payload = jsonObj.payload; + + console.log('📋 Parsed payload:', payload); + + switch (payload.type || payload.command) { + case 'connection_status': + console.log('📡 연결 상태 업데이트:', payload.connected); + updateConnectionStatus(payload.connected); + break; + + case 'camera_data': + console.log('📹 카메라 데이터 업데이트 수신:', payload); + console.log('📹 카메라 데이터 상세:', payload.camera_data); + updateCameraData(payload.camera_data, payload.current_camera); + break; + + case 'current_settings': + console.log('⚙️ 현재 설정 수신:', payload); + // 현재 설정을 컨텍스트에 저장 + if (currentActionContext) { + const newSettings = { + cameraIndex: payload.cameraIndex || 0, + cameraList: payload.cameraList || [], + isUnityConnected: payload.isUnityConnected || false + }; + saveContextSettings(currentActionContext, newSettings); + + // UI 업데이트 + updateConnectionStatus(payload.isUnityConnected); + if (payload.cameraList && payload.cameraList.length > 0) { + updateCameraData({ cameras: payload.cameraList }, payload.cameraIndex); + } + } + break; + + case 'camera_changed': + console.log('📹 카메라 변경:', payload.current_camera); + updateCurrentCameraDisplay(payload.current_camera); + break; + + default: + console.log('❓ Unknown message type:', payload.type || payload.command); + } + } else if (jsonObj.event === 'didReceiveSettings') { + // didReceiveSettings 이벤트 처리 추가 + console.log('⚙️ didReceiveSettings 이벤트 수신:', jsonObj); + + const settings = jsonObj.payload.settings || {}; + console.log('📋 설정 데이터:', settings); + + // 컨텍스트에 설정 저장 + if (currentActionContext) { + saveContextSettings(currentActionContext, settings); + + // UI 업데이트 + if (settings.cameraList && settings.cameraList.length > 0) { + console.log('📹 카메라 목록으로 UI 업데이트:', settings.cameraList.length + '개'); + updateCameraData(settings.cameraList, settings.cameraIndex || 0); + + // 카메라 목록이 있다면 Unity가 연결되어 있다고 판단 + console.log('🔍 카메라 목록 존재 - Unity 연결됨으로 판단'); + updateConnectionStatus(true); + } + + // 연결 상태 업데이트 (카메라 목록이 없을 때만 설정값 사용) + if (typeof settings.isUnityConnected === 'boolean' && (!settings.cameraList || settings.cameraList.length === 0)) { + updateConnectionStatus(settings.isUnityConnected); + } + } + } + } catch (error) { + console.error('❌ Failed to handle message:', error); + } +} + +// StreamDeck SDK connection +function connectElgatoStreamDeckSocket(inPort, inPropertyInspectorUUID, inRegisterEvent, inInfo, inActionInfo) { + uuid = inPropertyInspectorUUID; + + console.log('🔌 StreamDeck 연결 중...'); + + // Parse info + try { + const info = JSON.parse(inInfo); + const actionInfo = JSON.parse(inActionInfo); + actionContext = actionInfo.context; // 액션 컨텍스트 저장 + currentActionContext = actionInfo.context; // 현재 액션 컨텍스트 설정 + settings = actionInfo.payload.settings || {}; + + // 컨텍스트별 설정 초기화 + saveContextSettings(currentActionContext, settings); + + console.log('📋 컨텍스트 설정 완료:', currentActionContext); + } catch (error) { + console.error('❌ 정보 파싱 실패:', error); + } + + // Connect to StreamDock + websocket = new WebSocket('ws://127.0.0.1:' + inPort); + + websocket.onopen = function() { + console.log('✅ StreamDeck 연결됨'); + + // Register + const json = { + event: inRegisterEvent, + uuid: uuid + }; + + websocket.send(JSON.stringify(json)); + + // Plugin Main에 초기 설정 요청 + console.log('📤 Plugin Main에 초기 설정 요청'); + sendToPlugin('getInitialSettings'); + }; + + websocket.onmessage = function(evt) { + try { + const jsonObj = JSON.parse(evt.data); + handleMessage(jsonObj); + } catch (error) { + console.error('❌ Failed to parse message:', error); + } + }; + + websocket.onclose = function() { + console.log('❌ StreamDeck WebSocket closed'); + isShuttingDown = true; // 종료 플래그 설정 + + // Unity 자동 재연결 정리 + if (unityReconnectInterval) { + clearTimeout(unityReconnectInterval); + unityReconnectInterval = null; + } + if (unityHealthCheckInterval) { + clearInterval(unityHealthCheckInterval); + unityHealthCheckInterval = null; + } + + websocket = null; + }; + + websocket.onerror = function(error) { + console.error('❌ StreamDeck WebSocket error:', error); + }; +} + +// 컨텍스트별 설정 관리 함수들 +function getContextSettings(context) { + if (!context) return {}; + return contextSettings.get(context) || {}; +} + +function saveContextSettings(context, newSettings) { + if (!context) return; + contextSettings.set(context, { ...newSettings }); + console.log('💾 컨텍스트 설정 저장:', context, newSettings); +} + +// 현재 컨텍스트의 설정에서 카메라 이름을 가져오는 공통 함수 +function getCurrentCameraName(context, cameraIndex = null) { + if (!context) return '카메라\n선택'; + + const settings = getContextSettings(context); + if (!settings || !settings.cameraList) return '카메라\n선택'; + + // cameraIndex가 제공되면 그것을 사용, 아니면 설정에서 가져옴 + const index = cameraIndex !== null ? cameraIndex : settings.cameraIndex; + if (typeof index !== 'number' || !settings.cameraList[index]) return '카메라\n선택'; + + return settings.cameraList[index].name || '카메라\n선택'; +} + +// 버튼 제목 업데이트 공통 함수 (Plugin Main과 동일한 로직) +function updateButtonTitle(context, cameraName = null, cameraIndex = null) { + if (!websocket || !context) return; + + // cameraName이 제공되지 않으면 현재 설정에서 가져옴 + if (!cameraName) { + cameraName = getCurrentCameraName(context, cameraIndex); + console.log(`🔍 [Property Inspector] getCurrentCameraName 결과: "${cameraName}"`); + + // 디버깅을 위해 현재 설정 상태도 출력 + const settings = getContextSettings(context); + console.log(`🔍 [Property Inspector] 컨텍스트 설정:`, settings); + console.log(`🔍 [Property Inspector] 카메라 인덱스: ${cameraIndex !== null ? cameraIndex : settings.cameraIndex}, 목록 길이: ${settings.cameraList ? settings.cameraList.length : 0}`); + } else { + console.log(`🔍 [Property Inspector] 직접 제공된 카메라 이름: "${cameraName}"`); + } + + // 기본값 설정 + let title = cameraName || '카메라\n선택'; + console.log(`🔍 [Property Inspector] 최종 사용할 제목 (가공 전): "${title}"`); + + // 긴 텍스트를 두 줄로 나누기 (Plugin Main과 동일한 로직) + if (title.length > 8) { + const underscoreIndex = title.indexOf('_'); + + if (underscoreIndex !== -1 && underscoreIndex > 0) { + // 언더스코어가 있으면 그 위치에서 분할하고 언더스코어는 제거 + const firstLine = title.substring(0, underscoreIndex); + const secondLine = title.substring(underscoreIndex + 1); // +1로 언더스코어 제거 + + // 각 줄이 너무 길면 적절히 자르기 + const maxLineLength = 8; + let line1 = firstLine.length > maxLineLength ? firstLine.substring(0, maxLineLength - 1) + '.' : firstLine; + let line2 = secondLine.length > maxLineLength ? secondLine.substring(0, maxLineLength - 1) + '.' : secondLine; + + title = line1 + '\n' + line2; + } else { + // 언더스코어가 없으면 중간 지점에서 분할 + const midPoint = Math.ceil(title.length / 2); + const firstLine = title.substring(0, midPoint); + const secondLine = title.substring(midPoint); + title = firstLine + '\n' + secondLine; + } + } + + // 버튼 제목 설정 (Plugin Main과 완전히 동일한 매개변수) + const message = { + event: 'setTitle', + context: context, + payload: { + title: title, + target: 0, // 하드웨어와 소프트웨어 모두 + titleParameters: { + fontSize: 18, + showTitle: true, + titleAlignment: "middle" + } + } + }; + + websocket.send(JSON.stringify(message)); + console.log('🏷️ [Property Inspector] 버튼 제목 업데이트:', title.replace('\n', '\\n')); +} \ No newline at end of file diff --git a/Streamdeck/com.mirabox.streamingle.sdPlugin/dist/com.mirabox.streamingle.sdPlugin/propertyInspector/utils/action.js b/Streamdeck/com.mirabox.streamingle.sdPlugin/dist/com.mirabox.streamingle.sdPlugin/propertyInspector/utils/action.js new file mode 100644 index 00000000..7cc9620a --- /dev/null +++ b/Streamdeck/com.mirabox.streamingle.sdPlugin/dist/com.mirabox.streamingle.sdPlugin/propertyInspector/utils/action.js @@ -0,0 +1,157 @@ +let $websocket, $uuid, $action, $context, $settings, $lang, $FileID = ''; + +WebSocket.prototype.setGlobalSettings = function(payload) { + this.send(JSON.stringify({ + event: "setGlobalSettings", + context: $uuid, payload + })); +} + +WebSocket.prototype.getGlobalSettings = function() { + this.send(JSON.stringify({ + event: "getGlobalSettings", + context: $uuid, + })); +} + +// 与插件通信 +WebSocket.prototype.sendToPlugin = function (payload) { + this.send(JSON.stringify({ + event: "sendToPlugin", + action: $action, + context: $uuid, + payload + })); +}; + +//设置标题 +WebSocket.prototype.setTitle = function (str, row = 0, num = 6) { + console.log(str); + let newStr = ''; + if (row) { + let nowRow = 1, strArr = str.split(''); + strArr.forEach((item, index) => { + if (nowRow < row && index >= nowRow * num) { nowRow++; newStr += '\n'; } + if (nowRow <= row && index < nowRow * num) { newStr += item; } + }); + if (strArr.length > row * num) { newStr = newStr.substring(0, newStr.length - 1); newStr += '..'; } + } + this.send(JSON.stringify({ + event: "setTitle", + context: $context, + payload: { + target: 0, + title: newStr || str + } + })); +} + +// 设置状态 +WebSocket.prototype.setState = function (state) { + this.send(JSON.stringify({ + event: "setState", + context: $context, + payload: { state } + })); +}; + +// 设置背景 +WebSocket.prototype.setImage = function (url) { + let image = new Image(); + image.src = url; + image.onload = () => { + let canvas = document.createElement("canvas"); + canvas.width = image.naturalWidth; + canvas.height = image.naturalHeight; + let ctx = canvas.getContext("2d"); + ctx.drawImage(image, 0, 0); + this.send(JSON.stringify({ + event: "setImage", + context: $context, + payload: { + target: 0, + image: canvas.toDataURL("image/png") + } + })); + }; +}; + +// 打开网页 +WebSocket.prototype.openUrl = function (url) { + this.send(JSON.stringify({ + event: "openUrl", + payload: { url } + })); +}; + +// 保存持久化数据 +WebSocket.prototype.saveData = $.debounce(function (payload) { + this.send(JSON.stringify({ + event: "setSettings", + context: $uuid, + payload + })); +}); + +// StreamDock 软件入口函数 +const connectSocket = connectElgatoStreamDeckSocket; +async function connectElgatoStreamDeckSocket(port, uuid, event, app, info) { + info = JSON.parse(info); + $uuid = uuid; $action = info.action; + $context = info.context; + $websocket = new WebSocket('ws://127.0.0.1:' + port); + $websocket.onopen = () => $websocket.send(JSON.stringify({ event, uuid })); + + // 持久数据代理 + $websocket.onmessage = e => { + let data = JSON.parse(e.data); + if (data.event === 'didReceiveSettings') { + $settings = new Proxy(data.payload.settings, { + get(target, property) { + return target[property]; + }, + set(target, property, value) { + target[property] = value; + $websocket.saveData(data.payload.settings); + } + }); + if (!$back) $dom.main.style.display = 'block'; + } + $propEvent[data.event]?.(data.payload); + }; + + // 自动翻译页面 + if (!$local) return; + $lang = await new Promise(resolve => { + const req = new XMLHttpRequest(); + req.open('GET', `../../${JSON.parse(app).application.language}.json`); + req.send(); + req.onreadystatechange = () => { + if (req.readyState === 4) { + resolve(JSON.parse(req.responseText).Localization); + } + }; + }); + + // 遍历文本节点并翻译所有文本节点 + const walker = document.createTreeWalker($dom.main, NodeFilter.SHOW_TEXT, (e) => { + return e.data.trim() && NodeFilter.FILTER_ACCEPT; + }); + while (walker.nextNode()) { + console.log(walker.currentNode.data); + walker.currentNode.data = $lang[walker.currentNode.data]; + } + // placeholder 特殊处理 + const translate = item => { + if (item.placeholder?.trim()) { + console.log(item.placeholder); + item.placeholder = $lang[item.placeholder]; + } + }; + $('input', true).forEach(translate); + $('textarea', true).forEach(translate); +} + +// StreamDock 文件路径回调 +Array.from($('input[type="file"]', true)).forEach(item => item.addEventListener('click', () => $FileID = item.id)); +const onFilePickerReturn = (url) => $emit.send(`File-${$FileID}`, JSON.parse(url)); \ No newline at end of file diff --git a/Streamdeck/com.mirabox.streamingle.sdPlugin/dist/com.mirabox.streamingle.sdPlugin/propertyInspector/utils/axios.js b/Streamdeck/com.mirabox.streamingle.sdPlugin/dist/com.mirabox.streamingle.sdPlugin/propertyInspector/utils/axios.js new file mode 100644 index 00000000..78aa7b89 --- /dev/null +++ b/Streamdeck/com.mirabox.streamingle.sdPlugin/dist/com.mirabox.streamingle.sdPlugin/propertyInspector/utils/axios.js @@ -0,0 +1 @@ +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).axios=t()}(this,(function(){"use strict";function e(t){return e="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},e(t)}function t(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function n(e,t){for(var n=0;ne.length)&&(t=e.length);for(var n=0,r=new Array(t);n2&&void 0!==arguments[2]?arguments[2]:{},a=i.allOwnKeys,s=void 0!==a&&a;if(null!=t)if("object"!==e(t)&&(t=[t]),p(t))for(r=0,o=t.length;r0;)if(t===(n=r[o]).toLowerCase())return n;return null}var C="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:"undefined"!=typeof window?window:global,N=function(e){return!h(e)&&e!==C};var x,P=(x="undefined"!=typeof Uint8Array&&c(Uint8Array),function(e){return x&&e instanceof x}),k=l("HTMLFormElement"),U=function(e){var t=Object.prototype.hasOwnProperty;return function(e,n){return t.call(e,n)}}(),_=l("RegExp"),F=function(e,t){var n=Object.getOwnPropertyDescriptors(e),r={};T(n,(function(n,o){var i;!1!==(i=t(n,o,e))&&(r[o]=i||n)})),Object.defineProperties(e,r)},B="abcdefghijklmnopqrstuvwxyz",L="0123456789",D={DIGIT:L,ALPHA:B,ALPHA_DIGIT:B+B.toUpperCase()+L};var I=l("AsyncFunction"),q={isArray:p,isArrayBuffer:m,isBuffer:function(e){return null!==e&&!h(e)&&null!==e.constructor&&!h(e.constructor)&&y(e.constructor.isBuffer)&&e.constructor.isBuffer(e)},isFormData:function(e){var t;return e&&("function"==typeof FormData&&e instanceof FormData||y(e.append)&&("formdata"===(t=f(e))||"object"===t&&y(e.toString)&&"[object FormData]"===e.toString()))},isArrayBufferView:function(e){return"undefined"!=typeof ArrayBuffer&&ArrayBuffer.isView?ArrayBuffer.isView(e):e&&e.buffer&&m(e.buffer)},isString:v,isNumber:b,isBoolean:function(e){return!0===e||!1===e},isObject:g,isPlainObject:w,isUndefined:h,isDate:E,isFile:O,isBlob:S,isRegExp:_,isFunction:y,isStream:function(e){return g(e)&&y(e.pipe)},isURLSearchParams:A,isTypedArray:P,isFileList:R,forEach:T,merge:function e(){for(var t=N(this)&&this||{},n=t.caseless,r={},o=function(t,o){var i=n&&j(r,o)||o;w(r[i])&&w(t)?r[i]=e(r[i],t):w(t)?r[i]=e({},t):p(t)?r[i]=t.slice():r[i]=t},i=0,a=arguments.length;i3&&void 0!==arguments[3]?arguments[3]:{},o=r.allOwnKeys;return T(t,(function(t,r){n&&y(t)?e[r]=a(t,n):e[r]=t}),{allOwnKeys:o}),e},trim:function(e){return e.trim?e.trim():e.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,"")},stripBOM:function(e){return 65279===e.charCodeAt(0)&&(e=e.slice(1)),e},inherits:function(e,t,n,r){e.prototype=Object.create(t.prototype,r),e.prototype.constructor=e,Object.defineProperty(e,"super",{value:t.prototype}),n&&Object.assign(e.prototype,n)},toFlatObject:function(e,t,n,r){var o,i,a,s={};if(t=t||{},null==e)return t;do{for(i=(o=Object.getOwnPropertyNames(e)).length;i-- >0;)a=o[i],r&&!r(a,e,t)||s[a]||(t[a]=e[a],s[a]=!0);e=!1!==n&&c(e)}while(e&&(!n||n(e,t))&&e!==Object.prototype);return t},kindOf:f,kindOfTest:l,endsWith:function(e,t,n){e=String(e),(void 0===n||n>e.length)&&(n=e.length),n-=t.length;var r=e.indexOf(t,n);return-1!==r&&r===n},toArray:function(e){if(!e)return null;if(p(e))return e;var t=e.length;if(!b(t))return null;for(var n=new Array(t);t-- >0;)n[t]=e[t];return n},forEachEntry:function(e,t){for(var n,r=(e&&e[Symbol.iterator]).call(e);(n=r.next())&&!n.done;){var o=n.value;t.call(e,o[0],o[1])}},matchAll:function(e,t){for(var n,r=[];null!==(n=e.exec(t));)r.push(n);return r},isHTMLForm:k,hasOwnProperty:U,hasOwnProp:U,reduceDescriptors:F,freezeMethods:function(e){F(e,(function(t,n){if(y(e)&&-1!==["arguments","caller","callee"].indexOf(n))return!1;var r=e[n];y(r)&&(t.enumerable=!1,"writable"in t?t.writable=!1:t.set||(t.set=function(){throw Error("Can not rewrite read-only method '"+n+"'")}))}))},toObjectSet:function(e,t){var n={},r=function(e){e.forEach((function(e){n[e]=!0}))};return p(e)?r(e):r(String(e).split(t)),n},toCamelCase:function(e){return e.toLowerCase().replace(/[-_\s]([a-z\d])(\w*)/g,(function(e,t,n){return t.toUpperCase()+n}))},noop:function(){},toFiniteNumber:function(e,t){return e=+e,Number.isFinite(e)?e:t},findKey:j,global:C,isContextDefined:N,ALPHABET:D,generateString:function(){for(var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:16,t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:D.ALPHA_DIGIT,n="",r=t.length;e--;)n+=t[Math.random()*r|0];return n},isSpecCompliantForm:function(e){return!!(e&&y(e.append)&&"FormData"===e[Symbol.toStringTag]&&e[Symbol.iterator])},toJSONObject:function(e){var t=new Array(10);return function e(n,r){if(g(n)){if(t.indexOf(n)>=0)return;if(!("toJSON"in n)){t[r]=n;var o=p(n)?[]:{};return T(n,(function(t,n){var i=e(t,r+1);!h(i)&&(o[n]=i)})),t[r]=void 0,o}}return n}(e,0)},isAsyncFn:I,isThenable:function(e){return e&&(g(e)||y(e))&&y(e.then)&&y(e.catch)}};function M(e,t,n,r,o){Error.call(this),Error.captureStackTrace?Error.captureStackTrace(this,this.constructor):this.stack=(new Error).stack,this.message=e,this.name="AxiosError",t&&(this.code=t),n&&(this.config=n),r&&(this.request=r),o&&(this.response=o)}q.inherits(M,Error,{toJSON:function(){return{message:this.message,name:this.name,description:this.description,number:this.number,fileName:this.fileName,lineNumber:this.lineNumber,columnNumber:this.columnNumber,stack:this.stack,config:q.toJSONObject(this.config),code:this.code,status:this.response&&this.response.status?this.response.status:null}}});var z=M.prototype,H={};["ERR_BAD_OPTION_VALUE","ERR_BAD_OPTION","ECONNABORTED","ETIMEDOUT","ERR_NETWORK","ERR_FR_TOO_MANY_REDIRECTS","ERR_DEPRECATED","ERR_BAD_RESPONSE","ERR_BAD_REQUEST","ERR_CANCELED","ERR_NOT_SUPPORT","ERR_INVALID_URL"].forEach((function(e){H[e]={value:e}})),Object.defineProperties(M,H),Object.defineProperty(z,"isAxiosError",{value:!0}),M.from=function(e,t,n,r,o,i){var a=Object.create(z);return q.toFlatObject(e,a,(function(e){return e!==Error.prototype}),(function(e){return"isAxiosError"!==e})),M.call(a,e.message,t,n,r,o),a.cause=e,a.name=e.name,i&&Object.assign(a,i),a};function J(e){return q.isPlainObject(e)||q.isArray(e)}function W(e){return q.endsWith(e,"[]")?e.slice(0,-2):e}function K(e,t,n){return e?e.concat(t).map((function(e,t){return e=W(e),!n&&t?"["+e+"]":e})).join(n?".":""):t}var V=q.toFlatObject(q,{},null,(function(e){return/^is[A-Z]/.test(e)}));function G(t,n,r){if(!q.isObject(t))throw new TypeError("target must be an object");n=n||new FormData;var o=(r=q.toFlatObject(r,{metaTokens:!0,dots:!1,indexes:!1},!1,(function(e,t){return!q.isUndefined(t[e])}))).metaTokens,i=r.visitor||f,a=r.dots,s=r.indexes,u=(r.Blob||"undefined"!=typeof Blob&&Blob)&&q.isSpecCompliantForm(n);if(!q.isFunction(i))throw new TypeError("visitor must be a function");function c(e){if(null===e)return"";if(q.isDate(e))return e.toISOString();if(!u&&q.isBlob(e))throw new M("Blob is not supported. Use a Buffer instead.");return q.isArrayBuffer(e)||q.isTypedArray(e)?u&&"function"==typeof Blob?new Blob([e]):Buffer.from(e):e}function f(t,r,i){var u=t;if(t&&!i&&"object"===e(t))if(q.endsWith(r,"{}"))r=o?r:r.slice(0,-2),t=JSON.stringify(t);else if(q.isArray(t)&&function(e){return q.isArray(e)&&!e.some(J)}(t)||(q.isFileList(t)||q.endsWith(r,"[]"))&&(u=q.toArray(t)))return r=W(r),u.forEach((function(e,t){!q.isUndefined(e)&&null!==e&&n.append(!0===s?K([r],t,a):null===s?r:r+"[]",c(e))})),!1;return!!J(t)||(n.append(K(i,r,a),c(t)),!1)}var l=[],d=Object.assign(V,{defaultVisitor:f,convertValue:c,isVisitable:J});if(!q.isObject(t))throw new TypeError("data must be an object");return function e(t,r){if(!q.isUndefined(t)){if(-1!==l.indexOf(t))throw Error("Circular reference detected in "+r.join("."));l.push(t),q.forEach(t,(function(t,o){!0===(!(q.isUndefined(t)||null===t)&&i.call(n,t,q.isString(o)?o.trim():o,r,d))&&e(t,r?r.concat(o):[o])})),l.pop()}}(t),n}function $(e){var t={"!":"%21","'":"%27","(":"%28",")":"%29","~":"%7E","%20":"+","%00":"\0"};return encodeURIComponent(e).replace(/[!'()~]|%20|%00/g,(function(e){return t[e]}))}function X(e,t){this._pairs=[],e&&G(e,this,t)}var Q=X.prototype;function Z(e){return encodeURIComponent(e).replace(/%3A/gi,":").replace(/%24/g,"$").replace(/%2C/gi,",").replace(/%20/g,"+").replace(/%5B/gi,"[").replace(/%5D/gi,"]")}function Y(e,t,n){if(!t)return e;var r,o=n&&n.encode||Z,i=n&&n.serialize;if(r=i?i(t,n):q.isURLSearchParams(t)?t.toString():new X(t,n).toString(o)){var a=e.indexOf("#");-1!==a&&(e=e.slice(0,a)),e+=(-1===e.indexOf("?")?"?":"&")+r}return e}Q.append=function(e,t){this._pairs.push([e,t])},Q.toString=function(e){var t=e?function(t){return e.call(this,t,$)}:$;return this._pairs.map((function(e){return t(e[0])+"="+t(e[1])}),"").join("&")};var ee,te=function(){function e(){t(this,e),this.handlers=[]}return r(e,[{key:"use",value:function(e,t,n){return this.handlers.push({fulfilled:e,rejected:t,synchronous:!!n&&n.synchronous,runWhen:n?n.runWhen:null}),this.handlers.length-1}},{key:"eject",value:function(e){this.handlers[e]&&(this.handlers[e]=null)}},{key:"clear",value:function(){this.handlers&&(this.handlers=[])}},{key:"forEach",value:function(e){q.forEach(this.handlers,(function(t){null!==t&&e(t)}))}}]),e}(),ne={silentJSONParsing:!0,forcedJSONParsing:!0,clarifyTimeoutError:!1},re={isBrowser:!0,classes:{URLSearchParams:"undefined"!=typeof URLSearchParams?URLSearchParams:X,FormData:"undefined"!=typeof FormData?FormData:null,Blob:"undefined"!=typeof Blob?Blob:null},isStandardBrowserEnv:("undefined"==typeof navigator||"ReactNative"!==(ee=navigator.product)&&"NativeScript"!==ee&&"NS"!==ee)&&"undefined"!=typeof window&&"undefined"!=typeof document,isStandardBrowserWebWorkerEnv:"undefined"!=typeof WorkerGlobalScope&&self instanceof WorkerGlobalScope&&"function"==typeof self.importScripts,protocols:["http","https","file","blob","url","data"]};function oe(e){function t(e,n,r,o){var i=e[o++],a=Number.isFinite(+i),s=o>=e.length;return i=!i&&q.isArray(r)?r.length:i,s?(q.hasOwnProp(r,i)?r[i]=[r[i],n]:r[i]=n,!a):(r[i]&&q.isObject(r[i])||(r[i]=[]),t(e,n,r[i],o)&&q.isArray(r[i])&&(r[i]=function(e){var t,n,r={},o=Object.keys(e),i=o.length;for(t=0;t-1,i=q.isObject(e);if(i&&q.isHTMLForm(e)&&(e=new FormData(e)),q.isFormData(e))return o&&o?JSON.stringify(oe(e)):e;if(q.isArrayBuffer(e)||q.isBuffer(e)||q.isStream(e)||q.isFile(e)||q.isBlob(e))return e;if(q.isArrayBufferView(e))return e.buffer;if(q.isURLSearchParams(e))return t.setContentType("application/x-www-form-urlencoded;charset=utf-8",!1),e.toString();if(i){if(r.indexOf("application/x-www-form-urlencoded")>-1)return function(e,t){return G(e,new re.classes.URLSearchParams,Object.assign({visitor:function(e,t,n,r){return re.isNode&&q.isBuffer(e)?(this.append(t,e.toString("base64")),!1):r.defaultVisitor.apply(this,arguments)}},t))}(e,this.formSerializer).toString();if((n=q.isFileList(e))||r.indexOf("multipart/form-data")>-1){var a=this.env&&this.env.FormData;return G(n?{"files[]":e}:e,a&&new a,this.formSerializer)}}return i||o?(t.setContentType("application/json",!1),function(e,t,n){if(q.isString(e))try{return(t||JSON.parse)(e),q.trim(e)}catch(e){if("SyntaxError"!==e.name)throw e}return(n||JSON.stringify)(e)}(e)):e}],transformResponse:[function(e){var t=this.transitional||ie.transitional,n=t&&t.forcedJSONParsing,r="json"===this.responseType;if(e&&q.isString(e)&&(n&&!this.responseType||r)){var o=!(t&&t.silentJSONParsing)&&r;try{return JSON.parse(e)}catch(e){if(o){if("SyntaxError"===e.name)throw M.from(e,M.ERR_BAD_RESPONSE,this,null,this.response);throw e}}}return e}],timeout:0,xsrfCookieName:"XSRF-TOKEN",xsrfHeaderName:"X-XSRF-TOKEN",maxContentLength:-1,maxBodyLength:-1,env:{FormData:re.classes.FormData,Blob:re.classes.Blob},validateStatus:function(e){return e>=200&&e<300},headers:{common:{Accept:"application/json, text/plain, */*","Content-Type":void 0}}};q.forEach(["delete","get","head","post","put","patch"],(function(e){ie.headers[e]={}}));var ae=ie,se=q.toObjectSet(["age","authorization","content-length","content-type","etag","expires","from","host","if-modified-since","if-unmodified-since","last-modified","location","max-forwards","proxy-authorization","referer","retry-after","user-agent"]),ue=Symbol("internals");function ce(e){return e&&String(e).trim().toLowerCase()}function fe(e){return!1===e||null==e?e:q.isArray(e)?e.map(fe):String(e)}function le(e,t,n,r,o){return q.isFunction(r)?r.call(this,t,n):(o&&(t=n),q.isString(t)?q.isString(r)?-1!==t.indexOf(r):q.isRegExp(r)?r.test(t):void 0:void 0)}var de=function(e,n){function i(e){t(this,i),e&&this.set(e)}return r(i,[{key:"set",value:function(e,t,n){var r=this;function o(e,t,n){var o=ce(t);if(!o)throw new Error("header name must be a non-empty string");var i=q.findKey(r,o);(!i||void 0===r[i]||!0===n||void 0===n&&!1!==r[i])&&(r[i||t]=fe(e))}var i,a,s,u,c,f=function(e,t){return q.forEach(e,(function(e,n){return o(e,n,t)}))};return q.isPlainObject(e)||e instanceof this.constructor?f(e,t):q.isString(e)&&(e=e.trim())&&!/^[-_a-zA-Z0-9^`|~,!#$%&'*+.]+$/.test(e.trim())?f((c={},(i=e)&&i.split("\n").forEach((function(e){u=e.indexOf(":"),a=e.substring(0,u).trim().toLowerCase(),s=e.substring(u+1).trim(),!a||c[a]&&se[a]||("set-cookie"===a?c[a]?c[a].push(s):c[a]=[s]:c[a]=c[a]?c[a]+", "+s:s)})),c),t):null!=e&&o(t,e,n),this}},{key:"get",value:function(e,t){if(e=ce(e)){var n=q.findKey(this,e);if(n){var r=this[n];if(!t)return r;if(!0===t)return function(e){for(var t,n=Object.create(null),r=/([^\s,;=]+)\s*(?:=\s*([^,;]+))?/g;t=r.exec(e);)n[t[1]]=t[2];return n}(r);if(q.isFunction(t))return t.call(this,r,n);if(q.isRegExp(t))return t.exec(r);throw new TypeError("parser must be boolean|regexp|function")}}}},{key:"has",value:function(e,t){if(e=ce(e)){var n=q.findKey(this,e);return!(!n||void 0===this[n]||t&&!le(0,this[n],n,t))}return!1}},{key:"delete",value:function(e,t){var n=this,r=!1;function o(e){if(e=ce(e)){var o=q.findKey(n,e);!o||t&&!le(0,n[o],o,t)||(delete n[o],r=!0)}}return q.isArray(e)?e.forEach(o):o(e),r}},{key:"clear",value:function(e){for(var t=Object.keys(this),n=t.length,r=!1;n--;){var o=t[n];e&&!le(0,this[o],o,e,!0)||(delete this[o],r=!0)}return r}},{key:"normalize",value:function(e){var t=this,n={};return q.forEach(this,(function(r,o){var i=q.findKey(n,o);if(i)return t[i]=fe(r),void delete t[o];var a=e?function(e){return e.trim().toLowerCase().replace(/([a-z\d])(\w*)/g,(function(e,t,n){return t.toUpperCase()+n}))}(o):String(o).trim();a!==o&&delete t[o],t[a]=fe(r),n[a]=!0})),this}},{key:"concat",value:function(){for(var e,t=arguments.length,n=new Array(t),r=0;r1?n-1:0),o=1;o1?"since :\n"+u.map(Oe).join("\n"):" "+Oe(u[0]):"as no adapter specified"),"ERR_NOT_SUPPORT")}return n};function Ae(e){if(e.cancelToken&&e.cancelToken.throwIfRequested(),e.signal&&e.signal.aborted)throw new ve(null,e)}function Te(e){return Ae(e),e.headers=pe.from(e.headers),e.data=he.call(e,e.transformRequest),-1!==["post","put","patch"].indexOf(e.method)&&e.headers.setContentType("application/x-www-form-urlencoded",!1),Re(e.adapter||ae.adapter)(e).then((function(t){return Ae(e),t.data=he.call(e,e.transformResponse,t),t.headers=pe.from(t.headers),t}),(function(t){return me(t)||(Ae(e),t&&t.response&&(t.response.data=he.call(e,e.transformResponse,t.response),t.response.headers=pe.from(t.response.headers))),Promise.reject(t)}))}var je=function(e){return e instanceof pe?e.toJSON():e};function Ce(e,t){t=t||{};var n={};function r(e,t,n){return q.isPlainObject(e)&&q.isPlainObject(t)?q.merge.call({caseless:n},e,t):q.isPlainObject(t)?q.merge({},t):q.isArray(t)?t.slice():t}function o(e,t,n){return q.isUndefined(t)?q.isUndefined(e)?void 0:r(void 0,e,n):r(e,t,n)}function i(e,t){if(!q.isUndefined(t))return r(void 0,t)}function a(e,t){return q.isUndefined(t)?q.isUndefined(e)?void 0:r(void 0,e):r(void 0,t)}function s(n,o,i){return i in t?r(n,o):i in e?r(void 0,n):void 0}var u={url:i,method:i,data:i,baseURL:a,transformRequest:a,transformResponse:a,paramsSerializer:a,timeout:a,timeoutMessage:a,withCredentials:a,adapter:a,responseType:a,xsrfCookieName:a,xsrfHeaderName:a,onUploadProgress:a,onDownloadProgress:a,decompress:a,maxContentLength:a,maxBodyLength:a,beforeRedirect:a,transport:a,httpAgent:a,httpsAgent:a,cancelToken:a,socketPath:a,responseEncoding:a,validateStatus:s,headers:function(e,t){return o(je(e),je(t),!0)}};return q.forEach(Object.keys(Object.assign({},e,t)),(function(r){var i=u[r]||o,a=i(e[r],t[r],r);q.isUndefined(a)&&i!==s||(n[r]=a)})),n}var Ne="1.5.1",xe={};["object","boolean","number","function","string","symbol"].forEach((function(t,n){xe[t]=function(r){return e(r)===t||"a"+(n<1?"n ":" ")+t}}));var Pe={};xe.transitional=function(e,t,n){function r(e,t){return"[Axios v1.5.1] Transitional option '"+e+"'"+t+(n?". "+n:"")}return function(n,o,i){if(!1===e)throw new M(r(o," has been removed"+(t?" in "+t:"")),M.ERR_DEPRECATED);return t&&!Pe[o]&&(Pe[o]=!0,console.warn(r(o," has been deprecated since v"+t+" and will be removed in the near future"))),!e||e(n,o,i)}};var ke={assertOptions:function(t,n,r){if("object"!==e(t))throw new M("options must be an object",M.ERR_BAD_OPTION_VALUE);for(var o=Object.keys(t),i=o.length;i-- >0;){var a=o[i],s=n[a];if(s){var u=t[a],c=void 0===u||s(u,a,t);if(!0!==c)throw new M("option "+a+" must be "+c,M.ERR_BAD_OPTION_VALUE)}else if(!0!==r)throw new M("Unknown option "+a,M.ERR_BAD_OPTION)}},validators:xe},Ue=ke.validators,_e=function(){function e(n){t(this,e),this.defaults=n,this.interceptors={request:new te,response:new te}}return r(e,[{key:"request",value:function(e,t){"string"==typeof e?(t=t||{}).url=e:t=e||{};var n=t=Ce(this.defaults,t),r=n.transitional,o=n.paramsSerializer,i=n.headers;void 0!==r&&ke.assertOptions(r,{silentJSONParsing:Ue.transitional(Ue.boolean),forcedJSONParsing:Ue.transitional(Ue.boolean),clarifyTimeoutError:Ue.transitional(Ue.boolean)},!1),null!=o&&(q.isFunction(o)?t.paramsSerializer={serialize:o}:ke.assertOptions(o,{encode:Ue.function,serialize:Ue.function},!0)),t.method=(t.method||this.defaults.method||"get").toLowerCase();var a=i&&q.merge(i.common,i[t.method]);i&&q.forEach(["delete","get","head","post","put","patch","common"],(function(e){delete i[e]})),t.headers=pe.concat(a,i);var s=[],u=!0;this.interceptors.request.forEach((function(e){"function"==typeof e.runWhen&&!1===e.runWhen(t)||(u=u&&e.synchronous,s.unshift(e.fulfilled,e.rejected))}));var c,f=[];this.interceptors.response.forEach((function(e){f.push(e.fulfilled,e.rejected)}));var l,d=0;if(!u){var p=[Te.bind(this),void 0];for(p.unshift.apply(p,s),p.push.apply(p,f),l=p.length,c=Promise.resolve(t);d0;)o._listeners[t](e);o._listeners=null}})),this.promise.then=function(e){var t,n=new Promise((function(e){o.subscribe(e),t=e})).then(e);return n.cancel=function(){o.unsubscribe(t)},n},n((function(e,t,n){o.reason||(o.reason=new ve(e,t,n),r(o.reason))}))}return r(e,[{key:"throwIfRequested",value:function(){if(this.reason)throw this.reason}},{key:"subscribe",value:function(e){this.reason?e(this.reason):this._listeners?this._listeners.push(e):this._listeners=[e]}},{key:"unsubscribe",value:function(e){if(this._listeners){var t=this._listeners.indexOf(e);-1!==t&&this._listeners.splice(t,1)}}}],[{key:"source",value:function(){var t;return{token:new e((function(e){t=e})),cancel:t}}}]),e}();var Le={Continue:100,SwitchingProtocols:101,Processing:102,EarlyHints:103,Ok:200,Created:201,Accepted:202,NonAuthoritativeInformation:203,NoContent:204,ResetContent:205,PartialContent:206,MultiStatus:207,AlreadyReported:208,ImUsed:226,MultipleChoices:300,MovedPermanently:301,Found:302,SeeOther:303,NotModified:304,UseProxy:305,Unused:306,TemporaryRedirect:307,PermanentRedirect:308,BadRequest:400,Unauthorized:401,PaymentRequired:402,Forbidden:403,NotFound:404,MethodNotAllowed:405,NotAcceptable:406,ProxyAuthenticationRequired:407,RequestTimeout:408,Conflict:409,Gone:410,LengthRequired:411,PreconditionFailed:412,PayloadTooLarge:413,UriTooLong:414,UnsupportedMediaType:415,RangeNotSatisfiable:416,ExpectationFailed:417,ImATeapot:418,MisdirectedRequest:421,UnprocessableEntity:422,Locked:423,FailedDependency:424,TooEarly:425,UpgradeRequired:426,PreconditionRequired:428,TooManyRequests:429,RequestHeaderFieldsTooLarge:431,UnavailableForLegalReasons:451,InternalServerError:500,NotImplemented:501,BadGateway:502,ServiceUnavailable:503,GatewayTimeout:504,HttpVersionNotSupported:505,VariantAlsoNegotiates:506,InsufficientStorage:507,LoopDetected:508,NotExtended:510,NetworkAuthenticationRequired:511};Object.entries(Le).forEach((function(e){var t=o(e,2),n=t[0],r=t[1];Le[r]=n}));var De=Le;var Ie=function e(t){var n=new Fe(t),r=a(Fe.prototype.request,n);return q.extend(r,Fe.prototype,n,{allOwnKeys:!0}),q.extend(r,n,null,{allOwnKeys:!0}),r.create=function(n){return e(Ce(t,n))},r}(ae);return Ie.Axios=Fe,Ie.CanceledError=ve,Ie.CancelToken=Be,Ie.isCancel=me,Ie.VERSION=Ne,Ie.toFormData=G,Ie.AxiosError=M,Ie.Cancel=Ie.CanceledError,Ie.all=function(e){return Promise.all(e)},Ie.spread=function(e){return function(t){return e.apply(null,t)}},Ie.isAxiosError=function(e){return q.isObject(e)&&!0===e.isAxiosError},Ie.mergeConfig=Ce,Ie.AxiosHeaders=pe,Ie.formToJSON=function(e){return oe(q.isHTMLForm(e)?new FormData(e):e)},Ie.getAdapter=Re,Ie.HttpStatusCode=De,Ie.default=Ie,Ie})); \ No newline at end of file diff --git a/Streamdeck/com.mirabox.streamingle.sdPlugin/dist/com.mirabox.streamingle.sdPlugin/propertyInspector/utils/bootstrap-icons.css b/Streamdeck/com.mirabox.streamingle.sdPlugin/dist/com.mirabox.streamingle.sdPlugin/propertyInspector/utils/bootstrap-icons.css new file mode 100644 index 00000000..3c1537f1 --- /dev/null +++ b/Streamdeck/com.mirabox.streamingle.sdPlugin/dist/com.mirabox.streamingle.sdPlugin/propertyInspector/utils/bootstrap-icons.css @@ -0,0 +1,1556 @@ +@font-face { + font-family: "bootstrap-icons"; + src: url("./fonts/bootstrap-icons.woff2?30af91bf14e37666a085fb8a161ff36d") format("woff2"), +url("./fonts/bootstrap-icons.woff?30af91bf14e37666a085fb8a161ff36d") format("woff"); +} + +.bi::before, +[class^="bi-"]::before, +[class*=" bi-"]::before { + display: inline-block; + font-family: bootstrap-icons !important; + font-style: normal; + font-weight: normal !important; + font-variant: normal; + text-transform: none; + line-height: 1; + vertical-align: -.125em; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.bi-123::before { content: "\f67f"; } +.bi-alarm-fill::before { content: "\f101"; } +.bi-alarm::before { content: "\f102"; } +.bi-align-bottom::before { content: "\f103"; } +.bi-align-center::before { content: "\f104"; } +.bi-align-end::before { content: "\f105"; } +.bi-align-middle::before { content: "\f106"; } +.bi-align-start::before { content: "\f107"; } +.bi-align-top::before { content: "\f108"; } +.bi-alt::before { content: "\f109"; } +.bi-app-indicator::before { content: "\f10a"; } +.bi-app::before { content: "\f10b"; } +.bi-archive-fill::before { content: "\f10c"; } +.bi-archive::before { content: "\f10d"; } +.bi-arrow-90deg-down::before { content: "\f10e"; } +.bi-arrow-90deg-left::before { content: "\f10f"; } +.bi-arrow-90deg-right::before { content: "\f110"; } +.bi-arrow-90deg-up::before { content: "\f111"; } +.bi-arrow-bar-down::before { content: "\f112"; } +.bi-arrow-bar-left::before { content: "\f113"; } +.bi-arrow-bar-right::before { content: "\f114"; } +.bi-arrow-bar-up::before { content: "\f115"; } +.bi-arrow-clockwise::before { content: "\f116"; } +.bi-arrow-counterclockwise::before { content: "\f117"; } +.bi-arrow-down-circle-fill::before { content: "\f118"; } +.bi-arrow-down-circle::before { content: "\f119"; } +.bi-arrow-down-left-circle-fill::before { content: "\f11a"; } +.bi-arrow-down-left-circle::before { content: "\f11b"; } +.bi-arrow-down-left-square-fill::before { content: "\f11c"; } +.bi-arrow-down-left-square::before { content: "\f11d"; } +.bi-arrow-down-left::before { content: "\f11e"; } +.bi-arrow-down-right-circle-fill::before { content: "\f11f"; } +.bi-arrow-down-right-circle::before { content: "\f120"; } +.bi-arrow-down-right-square-fill::before { content: "\f121"; } +.bi-arrow-down-right-square::before { content: "\f122"; } +.bi-arrow-down-right::before { content: "\f123"; } +.bi-arrow-down-short::before { content: "\f124"; } +.bi-arrow-down-square-fill::before { content: "\f125"; } +.bi-arrow-down-square::before { content: "\f126"; } +.bi-arrow-down-up::before { content: "\f127"; } +.bi-arrow-down::before { content: "\f128"; } +.bi-arrow-left-circle-fill::before { content: "\f129"; } +.bi-arrow-left-circle::before { content: "\f12a"; } +.bi-arrow-left-right::before { content: "\f12b"; } +.bi-arrow-left-short::before { content: "\f12c"; } +.bi-arrow-left-square-fill::before { content: "\f12d"; } +.bi-arrow-left-square::before { content: "\f12e"; } +.bi-arrow-left::before { content: "\f12f"; } +.bi-arrow-repeat::before { content: "\f130"; } +.bi-arrow-return-left::before { content: "\f131"; } +.bi-arrow-return-right::before { content: "\f132"; } +.bi-arrow-right-circle-fill::before { content: "\f133"; } +.bi-arrow-right-circle::before { content: "\f134"; } +.bi-arrow-right-short::before { content: "\f135"; } +.bi-arrow-right-square-fill::before { content: "\f136"; } +.bi-arrow-right-square::before { content: "\f137"; } +.bi-arrow-right::before { content: "\f138"; } +.bi-arrow-up-circle-fill::before { content: "\f139"; } +.bi-arrow-up-circle::before { content: "\f13a"; } +.bi-arrow-up-left-circle-fill::before { content: "\f13b"; } +.bi-arrow-up-left-circle::before { content: "\f13c"; } +.bi-arrow-up-left-square-fill::before { content: "\f13d"; } +.bi-arrow-up-left-square::before { content: "\f13e"; } +.bi-arrow-up-left::before { content: "\f13f"; } +.bi-arrow-up-right-circle-fill::before { content: "\f140"; } +.bi-arrow-up-right-circle::before { content: "\f141"; } +.bi-arrow-up-right-square-fill::before { content: "\f142"; } +.bi-arrow-up-right-square::before { content: "\f143"; } +.bi-arrow-up-right::before { content: "\f144"; } +.bi-arrow-up-short::before { content: "\f145"; } +.bi-arrow-up-square-fill::before { content: "\f146"; } +.bi-arrow-up-square::before { content: "\f147"; } +.bi-arrow-up::before { content: "\f148"; } +.bi-arrows-angle-contract::before { content: "\f149"; } +.bi-arrows-angle-expand::before { content: "\f14a"; } +.bi-arrows-collapse::before { content: "\f14b"; } +.bi-arrows-expand::before { content: "\f14c"; } +.bi-arrows-fullscreen::before { content: "\f14d"; } +.bi-arrows-move::before { content: "\f14e"; } +.bi-aspect-ratio-fill::before { content: "\f14f"; } +.bi-aspect-ratio::before { content: "\f150"; } +.bi-asterisk::before { content: "\f151"; } +.bi-at::before { content: "\f152"; } +.bi-award-fill::before { content: "\f153"; } +.bi-award::before { content: "\f154"; } +.bi-back::before { content: "\f155"; } +.bi-backspace-fill::before { content: "\f156"; } +.bi-backspace-reverse-fill::before { content: "\f157"; } +.bi-backspace-reverse::before { content: "\f158"; } +.bi-backspace::before { content: "\f159"; } +.bi-badge-3d-fill::before { content: "\f15a"; } +.bi-badge-3d::before { content: "\f15b"; } +.bi-badge-4k-fill::before { content: "\f15c"; } +.bi-badge-4k::before { content: "\f15d"; } +.bi-badge-8k-fill::before { content: "\f15e"; } +.bi-badge-8k::before { content: "\f15f"; } +.bi-badge-ad-fill::before { content: "\f160"; } +.bi-badge-ad::before { content: "\f161"; } +.bi-badge-ar-fill::before { content: "\f162"; } +.bi-badge-ar::before { content: "\f163"; } +.bi-badge-cc-fill::before { content: "\f164"; } +.bi-badge-cc::before { content: "\f165"; } +.bi-badge-hd-fill::before { content: "\f166"; } +.bi-badge-hd::before { content: "\f167"; } +.bi-badge-tm-fill::before { content: "\f168"; } +.bi-badge-tm::before { content: "\f169"; } +.bi-badge-vo-fill::before { content: "\f16a"; } +.bi-badge-vo::before { content: "\f16b"; } +.bi-badge-vr-fill::before { content: "\f16c"; } +.bi-badge-vr::before { content: "\f16d"; } +.bi-badge-wc-fill::before { content: "\f16e"; } +.bi-badge-wc::before { content: "\f16f"; } +.bi-bag-check-fill::before { content: "\f170"; } +.bi-bag-check::before { content: "\f171"; } +.bi-bag-dash-fill::before { content: "\f172"; } +.bi-bag-dash::before { content: "\f173"; } +.bi-bag-fill::before { content: "\f174"; } +.bi-bag-plus-fill::before { content: "\f175"; } +.bi-bag-plus::before { content: "\f176"; } +.bi-bag-x-fill::before { content: "\f177"; } +.bi-bag-x::before { content: "\f178"; } +.bi-bag::before { content: "\f179"; } +.bi-bar-chart-fill::before { content: "\f17a"; } +.bi-bar-chart-line-fill::before { content: "\f17b"; } +.bi-bar-chart-line::before { content: "\f17c"; } +.bi-bar-chart-steps::before { content: "\f17d"; } +.bi-bar-chart::before { content: "\f17e"; } +.bi-basket-fill::before { content: "\f17f"; } +.bi-basket::before { content: "\f180"; } +.bi-basket2-fill::before { content: "\f181"; } +.bi-basket2::before { content: "\f182"; } +.bi-basket3-fill::before { content: "\f183"; } +.bi-basket3::before { content: "\f184"; } +.bi-battery-charging::before { content: "\f185"; } +.bi-battery-full::before { content: "\f186"; } +.bi-battery-half::before { content: "\f187"; } +.bi-battery::before { content: "\f188"; } +.bi-bell-fill::before { content: "\f189"; } +.bi-bell::before { content: "\f18a"; } +.bi-bezier::before { content: "\f18b"; } +.bi-bezier2::before { content: "\f18c"; } +.bi-bicycle::before { content: "\f18d"; } +.bi-binoculars-fill::before { content: "\f18e"; } +.bi-binoculars::before { content: "\f18f"; } +.bi-blockquote-left::before { content: "\f190"; } +.bi-blockquote-right::before { content: "\f191"; } +.bi-book-fill::before { content: "\f192"; } +.bi-book-half::before { content: "\f193"; } +.bi-book::before { content: "\f194"; } +.bi-bookmark-check-fill::before { content: "\f195"; } +.bi-bookmark-check::before { content: "\f196"; } +.bi-bookmark-dash-fill::before { content: "\f197"; } +.bi-bookmark-dash::before { content: "\f198"; } +.bi-bookmark-fill::before { content: "\f199"; } +.bi-bookmark-heart-fill::before { content: "\f19a"; } +.bi-bookmark-heart::before { content: "\f19b"; } +.bi-bookmark-plus-fill::before { content: "\f19c"; } +.bi-bookmark-plus::before { content: "\f19d"; } +.bi-bookmark-star-fill::before { content: "\f19e"; } +.bi-bookmark-star::before { content: "\f19f"; } +.bi-bookmark-x-fill::before { content: "\f1a0"; } +.bi-bookmark-x::before { content: "\f1a1"; } +.bi-bookmark::before { content: "\f1a2"; } +.bi-bookmarks-fill::before { content: "\f1a3"; } +.bi-bookmarks::before { content: "\f1a4"; } +.bi-bookshelf::before { content: "\f1a5"; } +.bi-bootstrap-fill::before { content: "\f1a6"; } +.bi-bootstrap-reboot::before { content: "\f1a7"; } +.bi-bootstrap::before { content: "\f1a8"; } +.bi-border-all::before { content: "\f1a9"; } +.bi-border-bottom::before { content: "\f1aa"; } +.bi-border-center::before { content: "\f1ab"; } +.bi-border-inner::before { content: "\f1ac"; } +.bi-border-left::before { content: "\f1ad"; } +.bi-border-middle::before { content: "\f1ae"; } +.bi-border-outer::before { content: "\f1af"; } +.bi-border-right::before { content: "\f1b0"; } +.bi-border-style::before { content: "\f1b1"; } +.bi-border-top::before { content: "\f1b2"; } +.bi-border-width::before { content: "\f1b3"; } +.bi-border::before { content: "\f1b4"; } +.bi-bounding-box-circles::before { content: "\f1b5"; } +.bi-bounding-box::before { content: "\f1b6"; } +.bi-box-arrow-down-left::before { content: "\f1b7"; } +.bi-box-arrow-down-right::before { content: "\f1b8"; } +.bi-box-arrow-down::before { content: "\f1b9"; } +.bi-box-arrow-in-down-left::before { content: "\f1ba"; } +.bi-box-arrow-in-down-right::before { content: "\f1bb"; } +.bi-box-arrow-in-down::before { content: "\f1bc"; } +.bi-box-arrow-in-left::before { content: "\f1bd"; } +.bi-box-arrow-in-right::before { content: "\f1be"; } +.bi-box-arrow-in-up-left::before { content: "\f1bf"; } +.bi-box-arrow-in-up-right::before { content: "\f1c0"; } +.bi-box-arrow-in-up::before { content: "\f1c1"; } +.bi-box-arrow-left::before { content: "\f1c2"; } +.bi-box-arrow-right::before { content: "\f1c3"; } +.bi-box-arrow-up-left::before { content: "\f1c4"; } +.bi-box-arrow-up-right::before { content: "\f1c5"; } +.bi-box-arrow-up::before { content: "\f1c6"; } +.bi-box-seam::before { content: "\f1c7"; } +.bi-box::before { content: "\f1c8"; } +.bi-braces::before { content: "\f1c9"; } +.bi-bricks::before { content: "\f1ca"; } +.bi-briefcase-fill::before { content: "\f1cb"; } +.bi-briefcase::before { content: "\f1cc"; } +.bi-brightness-alt-high-fill::before { content: "\f1cd"; } +.bi-brightness-alt-high::before { content: "\f1ce"; } +.bi-brightness-alt-low-fill::before { content: "\f1cf"; } +.bi-brightness-alt-low::before { content: "\f1d0"; } +.bi-brightness-high-fill::before { content: "\f1d1"; } +.bi-brightness-high::before { content: "\f1d2"; } +.bi-brightness-low-fill::before { content: "\f1d3"; } +.bi-brightness-low::before { content: "\f1d4"; } +.bi-broadcast-pin::before { content: "\f1d5"; } +.bi-broadcast::before { content: "\f1d6"; } +.bi-brush-fill::before { content: "\f1d7"; } +.bi-brush::before { content: "\f1d8"; } +.bi-bucket-fill::before { content: "\f1d9"; } +.bi-bucket::before { content: "\f1da"; } +.bi-bug-fill::before { content: "\f1db"; } +.bi-bug::before { content: "\f1dc"; } +.bi-building::before { content: "\f1dd"; } +.bi-bullseye::before { content: "\f1de"; } +.bi-calculator-fill::before { content: "\f1df"; } +.bi-calculator::before { content: "\f1e0"; } +.bi-calendar-check-fill::before { content: "\f1e1"; } +.bi-calendar-check::before { content: "\f1e2"; } +.bi-calendar-date-fill::before { content: "\f1e3"; } +.bi-calendar-date::before { content: "\f1e4"; } +.bi-calendar-day-fill::before { content: "\f1e5"; } +.bi-calendar-day::before { content: "\f1e6"; } +.bi-calendar-event-fill::before { content: "\f1e7"; } +.bi-calendar-event::before { content: "\f1e8"; } +.bi-calendar-fill::before { content: "\f1e9"; } +.bi-calendar-minus-fill::before { content: "\f1ea"; } +.bi-calendar-minus::before { content: "\f1eb"; } +.bi-calendar-month-fill::before { content: "\f1ec"; } +.bi-calendar-month::before { content: "\f1ed"; } +.bi-calendar-plus-fill::before { content: "\f1ee"; } +.bi-calendar-plus::before { content: "\f1ef"; } +.bi-calendar-range-fill::before { content: "\f1f0"; } +.bi-calendar-range::before { content: "\f1f1"; } +.bi-calendar-week-fill::before { content: "\f1f2"; } +.bi-calendar-week::before { content: "\f1f3"; } +.bi-calendar-x-fill::before { content: "\f1f4"; } +.bi-calendar-x::before { content: "\f1f5"; } +.bi-calendar::before { content: "\f1f6"; } +.bi-calendar2-check-fill::before { content: "\f1f7"; } +.bi-calendar2-check::before { content: "\f1f8"; } +.bi-calendar2-date-fill::before { content: "\f1f9"; } +.bi-calendar2-date::before { content: "\f1fa"; } +.bi-calendar2-day-fill::before { content: "\f1fb"; } +.bi-calendar2-day::before { content: "\f1fc"; } +.bi-calendar2-event-fill::before { content: "\f1fd"; } +.bi-calendar2-event::before { content: "\f1fe"; } +.bi-calendar2-fill::before { content: "\f1ff"; } +.bi-calendar2-minus-fill::before { content: "\f200"; } +.bi-calendar2-minus::before { content: "\f201"; } +.bi-calendar2-month-fill::before { content: "\f202"; } +.bi-calendar2-month::before { content: "\f203"; } +.bi-calendar2-plus-fill::before { content: "\f204"; } +.bi-calendar2-plus::before { content: "\f205"; } +.bi-calendar2-range-fill::before { content: "\f206"; } +.bi-calendar2-range::before { content: "\f207"; } +.bi-calendar2-week-fill::before { content: "\f208"; } +.bi-calendar2-week::before { content: "\f209"; } +.bi-calendar2-x-fill::before { content: "\f20a"; } +.bi-calendar2-x::before { content: "\f20b"; } +.bi-calendar2::before { content: "\f20c"; } +.bi-calendar3-event-fill::before { content: "\f20d"; } +.bi-calendar3-event::before { content: "\f20e"; } +.bi-calendar3-fill::before { content: "\f20f"; } +.bi-calendar3-range-fill::before { content: "\f210"; } +.bi-calendar3-range::before { content: "\f211"; } +.bi-calendar3-week-fill::before { content: "\f212"; } +.bi-calendar3-week::before { content: "\f213"; } +.bi-calendar3::before { content: "\f214"; } +.bi-calendar4-event::before { content: "\f215"; } +.bi-calendar4-range::before { content: "\f216"; } +.bi-calendar4-week::before { content: "\f217"; } +.bi-calendar4::before { content: "\f218"; } +.bi-camera-fill::before { content: "\f219"; } +.bi-camera-reels-fill::before { content: "\f21a"; } +.bi-camera-reels::before { content: "\f21b"; } +.bi-camera-video-fill::before { content: "\f21c"; } +.bi-camera-video-off-fill::before { content: "\f21d"; } +.bi-camera-video-off::before { content: "\f21e"; } +.bi-camera-video::before { content: "\f21f"; } +.bi-camera::before { content: "\f220"; } +.bi-camera2::before { content: "\f221"; } +.bi-capslock-fill::before { content: "\f222"; } +.bi-capslock::before { content: "\f223"; } +.bi-card-checklist::before { content: "\f224"; } +.bi-card-heading::before { content: "\f225"; } +.bi-card-image::before { content: "\f226"; } +.bi-card-list::before { content: "\f227"; } +.bi-card-text::before { content: "\f228"; } +.bi-caret-down-fill::before { content: "\f229"; } +.bi-caret-down-square-fill::before { content: "\f22a"; } +.bi-caret-down-square::before { content: "\f22b"; } +.bi-caret-down::before { content: "\f22c"; } +.bi-caret-left-fill::before { content: "\f22d"; } +.bi-caret-left-square-fill::before { content: "\f22e"; } +.bi-caret-left-square::before { content: "\f22f"; } +.bi-caret-left::before { content: "\f230"; } +.bi-caret-right-fill::before { content: "\f231"; } +.bi-caret-right-square-fill::before { content: "\f232"; } +.bi-caret-right-square::before { content: "\f233"; } +.bi-caret-right::before { content: "\f234"; } +.bi-caret-up-fill::before { content: "\f235"; } +.bi-caret-up-square-fill::before { content: "\f236"; } +.bi-caret-up-square::before { content: "\f237"; } +.bi-caret-up::before { content: "\f238"; } +.bi-cart-check-fill::before { content: "\f239"; } +.bi-cart-check::before { content: "\f23a"; } +.bi-cart-dash-fill::before { content: "\f23b"; } +.bi-cart-dash::before { content: "\f23c"; } +.bi-cart-fill::before { content: "\f23d"; } +.bi-cart-plus-fill::before { content: "\f23e"; } +.bi-cart-plus::before { content: "\f23f"; } +.bi-cart-x-fill::before { content: "\f240"; } +.bi-cart-x::before { content: "\f241"; } +.bi-cart::before { content: "\f242"; } +.bi-cart2::before { content: "\f243"; } +.bi-cart3::before { content: "\f244"; } +.bi-cart4::before { content: "\f245"; } +.bi-cash-stack::before { content: "\f246"; } +.bi-cash::before { content: "\f247"; } +.bi-cast::before { content: "\f248"; } +.bi-chat-dots-fill::before { content: "\f249"; } +.bi-chat-dots::before { content: "\f24a"; } +.bi-chat-fill::before { content: "\f24b"; } +.bi-chat-left-dots-fill::before { content: "\f24c"; } +.bi-chat-left-dots::before { content: "\f24d"; } +.bi-chat-left-fill::before { content: "\f24e"; } +.bi-chat-left-quote-fill::before { content: "\f24f"; } +.bi-chat-left-quote::before { content: "\f250"; } +.bi-chat-left-text-fill::before { content: "\f251"; } +.bi-chat-left-text::before { content: "\f252"; } +.bi-chat-left::before { content: "\f253"; } +.bi-chat-quote-fill::before { content: "\f254"; } +.bi-chat-quote::before { content: "\f255"; } +.bi-chat-right-dots-fill::before { content: "\f256"; } +.bi-chat-right-dots::before { content: "\f257"; } +.bi-chat-right-fill::before { content: "\f258"; } +.bi-chat-right-quote-fill::before { content: "\f259"; } +.bi-chat-right-quote::before { content: "\f25a"; } +.bi-chat-right-text-fill::before { content: "\f25b"; } +.bi-chat-right-text::before { content: "\f25c"; } +.bi-chat-right::before { content: "\f25d"; } +.bi-chat-square-dots-fill::before { content: "\f25e"; } +.bi-chat-square-dots::before { content: "\f25f"; } +.bi-chat-square-fill::before { content: "\f260"; } +.bi-chat-square-quote-fill::before { content: "\f261"; } +.bi-chat-square-quote::before { content: "\f262"; } +.bi-chat-square-text-fill::before { content: "\f263"; } +.bi-chat-square-text::before { content: "\f264"; } +.bi-chat-square::before { content: "\f265"; } +.bi-chat-text-fill::before { content: "\f266"; } +.bi-chat-text::before { content: "\f267"; } +.bi-chat::before { content: "\f268"; } +.bi-check-all::before { content: "\f269"; } +.bi-check-circle-fill::before { content: "\f26a"; } +.bi-check-circle::before { content: "\f26b"; } +.bi-check-square-fill::before { content: "\f26c"; } +.bi-check-square::before { content: "\f26d"; } +.bi-check::before { content: "\f26e"; } +.bi-check2-all::before { content: "\f26f"; } +.bi-check2-circle::before { content: "\f270"; } +.bi-check2-square::before { content: "\f271"; } +.bi-check2::before { content: "\f272"; } +.bi-chevron-bar-contract::before { content: "\f273"; } +.bi-chevron-bar-down::before { content: "\f274"; } +.bi-chevron-bar-expand::before { content: "\f275"; } +.bi-chevron-bar-left::before { content: "\f276"; } +.bi-chevron-bar-right::before { content: "\f277"; } +.bi-chevron-bar-up::before { content: "\f278"; } +.bi-chevron-compact-down::before { content: "\f279"; } +.bi-chevron-compact-left::before { content: "\f27a"; } +.bi-chevron-compact-right::before { content: "\f27b"; } +.bi-chevron-compact-up::before { content: "\f27c"; } +.bi-chevron-contract::before { content: "\f27d"; } +.bi-chevron-double-down::before { content: "\f27e"; } +.bi-chevron-double-left::before { content: "\f27f"; } +.bi-chevron-double-right::before { content: "\f280"; } +.bi-chevron-double-up::before { content: "\f281"; } +.bi-chevron-down::before { content: "\f282"; } +.bi-chevron-expand::before { content: "\f283"; } +.bi-chevron-left::before { content: "\f284"; } +.bi-chevron-right::before { content: "\f285"; } +.bi-chevron-up::before { content: "\f286"; } +.bi-circle-fill::before { content: "\f287"; } +.bi-circle-half::before { content: "\f288"; } +.bi-circle-square::before { content: "\f289"; } +.bi-circle::before { content: "\f28a"; } +.bi-clipboard-check::before { content: "\f28b"; } +.bi-clipboard-data::before { content: "\f28c"; } +.bi-clipboard-minus::before { content: "\f28d"; } +.bi-clipboard-plus::before { content: "\f28e"; } +.bi-clipboard-x::before { content: "\f28f"; } +.bi-clipboard::before { content: "\f290"; } +.bi-clock-fill::before { content: "\f291"; } +.bi-clock-history::before { content: "\f292"; } +.bi-clock::before { content: "\f293"; } +.bi-cloud-arrow-down-fill::before { content: "\f294"; } +.bi-cloud-arrow-down::before { content: "\f295"; } +.bi-cloud-arrow-up-fill::before { content: "\f296"; } +.bi-cloud-arrow-up::before { content: "\f297"; } +.bi-cloud-check-fill::before { content: "\f298"; } +.bi-cloud-check::before { content: "\f299"; } +.bi-cloud-download-fill::before { content: "\f29a"; } +.bi-cloud-download::before { content: "\f29b"; } +.bi-cloud-drizzle-fill::before { content: "\f29c"; } +.bi-cloud-drizzle::before { content: "\f29d"; } +.bi-cloud-fill::before { content: "\f29e"; } +.bi-cloud-fog-fill::before { content: "\f29f"; } +.bi-cloud-fog::before { content: "\f2a0"; } +.bi-cloud-fog2-fill::before { content: "\f2a1"; } +.bi-cloud-fog2::before { content: "\f2a2"; } +.bi-cloud-hail-fill::before { content: "\f2a3"; } +.bi-cloud-hail::before { content: "\f2a4"; } +.bi-cloud-haze-1::before { content: "\f2a5"; } +.bi-cloud-haze-fill::before { content: "\f2a6"; } +.bi-cloud-haze::before { content: "\f2a7"; } +.bi-cloud-haze2-fill::before { content: "\f2a8"; } +.bi-cloud-lightning-fill::before { content: "\f2a9"; } +.bi-cloud-lightning-rain-fill::before { content: "\f2aa"; } +.bi-cloud-lightning-rain::before { content: "\f2ab"; } +.bi-cloud-lightning::before { content: "\f2ac"; } +.bi-cloud-minus-fill::before { content: "\f2ad"; } +.bi-cloud-minus::before { content: "\f2ae"; } +.bi-cloud-moon-fill::before { content: "\f2af"; } +.bi-cloud-moon::before { content: "\f2b0"; } +.bi-cloud-plus-fill::before { content: "\f2b1"; } +.bi-cloud-plus::before { content: "\f2b2"; } +.bi-cloud-rain-fill::before { content: "\f2b3"; } +.bi-cloud-rain-heavy-fill::before { content: "\f2b4"; } +.bi-cloud-rain-heavy::before { content: "\f2b5"; } +.bi-cloud-rain::before { content: "\f2b6"; } +.bi-cloud-slash-fill::before { content: "\f2b7"; } +.bi-cloud-slash::before { content: "\f2b8"; } +.bi-cloud-sleet-fill::before { content: "\f2b9"; } +.bi-cloud-sleet::before { content: "\f2ba"; } +.bi-cloud-snow-fill::before { content: "\f2bb"; } +.bi-cloud-snow::before { content: "\f2bc"; } +.bi-cloud-sun-fill::before { content: "\f2bd"; } +.bi-cloud-sun::before { content: "\f2be"; } +.bi-cloud-upload-fill::before { content: "\f2bf"; } +.bi-cloud-upload::before { content: "\f2c0"; } +.bi-cloud::before { content: "\f2c1"; } +.bi-clouds-fill::before { content: "\f2c2"; } +.bi-clouds::before { content: "\f2c3"; } +.bi-cloudy-fill::before { content: "\f2c4"; } +.bi-cloudy::before { content: "\f2c5"; } +.bi-code-slash::before { content: "\f2c6"; } +.bi-code-square::before { content: "\f2c7"; } +.bi-code::before { content: "\f2c8"; } +.bi-collection-fill::before { content: "\f2c9"; } +.bi-collection-play-fill::before { content: "\f2ca"; } +.bi-collection-play::before { content: "\f2cb"; } +.bi-collection::before { content: "\f2cc"; } +.bi-columns-gap::before { content: "\f2cd"; } +.bi-columns::before { content: "\f2ce"; } +.bi-command::before { content: "\f2cf"; } +.bi-compass-fill::before { content: "\f2d0"; } +.bi-compass::before { content: "\f2d1"; } +.bi-cone-striped::before { content: "\f2d2"; } +.bi-cone::before { content: "\f2d3"; } +.bi-controller::before { content: "\f2d4"; } +.bi-cpu-fill::before { content: "\f2d5"; } +.bi-cpu::before { content: "\f2d6"; } +.bi-credit-card-2-back-fill::before { content: "\f2d7"; } +.bi-credit-card-2-back::before { content: "\f2d8"; } +.bi-credit-card-2-front-fill::before { content: "\f2d9"; } +.bi-credit-card-2-front::before { content: "\f2da"; } +.bi-credit-card-fill::before { content: "\f2db"; } +.bi-credit-card::before { content: "\f2dc"; } +.bi-crop::before { content: "\f2dd"; } +.bi-cup-fill::before { content: "\f2de"; } +.bi-cup-straw::before { content: "\f2df"; } +.bi-cup::before { content: "\f2e0"; } +.bi-cursor-fill::before { content: "\f2e1"; } +.bi-cursor-text::before { content: "\f2e2"; } +.bi-cursor::before { content: "\f2e3"; } +.bi-dash-circle-dotted::before { content: "\f2e4"; } +.bi-dash-circle-fill::before { content: "\f2e5"; } +.bi-dash-circle::before { content: "\f2e6"; } +.bi-dash-square-dotted::before { content: "\f2e7"; } +.bi-dash-square-fill::before { content: "\f2e8"; } +.bi-dash-square::before { content: "\f2e9"; } +.bi-dash::before { content: "\f2ea"; } +.bi-diagram-2-fill::before { content: "\f2eb"; } +.bi-diagram-2::before { content: "\f2ec"; } +.bi-diagram-3-fill::before { content: "\f2ed"; } +.bi-diagram-3::before { content: "\f2ee"; } +.bi-diamond-fill::before { content: "\f2ef"; } +.bi-diamond-half::before { content: "\f2f0"; } +.bi-diamond::before { content: "\f2f1"; } +.bi-dice-1-fill::before { content: "\f2f2"; } +.bi-dice-1::before { content: "\f2f3"; } +.bi-dice-2-fill::before { content: "\f2f4"; } +.bi-dice-2::before { content: "\f2f5"; } +.bi-dice-3-fill::before { content: "\f2f6"; } +.bi-dice-3::before { content: "\f2f7"; } +.bi-dice-4-fill::before { content: "\f2f8"; } +.bi-dice-4::before { content: "\f2f9"; } +.bi-dice-5-fill::before { content: "\f2fa"; } +.bi-dice-5::before { content: "\f2fb"; } +.bi-dice-6-fill::before { content: "\f2fc"; } +.bi-dice-6::before { content: "\f2fd"; } +.bi-disc-fill::before { content: "\f2fe"; } +.bi-disc::before { content: "\f2ff"; } +.bi-discord::before { content: "\f300"; } +.bi-display-fill::before { content: "\f301"; } +.bi-display::before { content: "\f302"; } +.bi-distribute-horizontal::before { content: "\f303"; } +.bi-distribute-vertical::before { content: "\f304"; } +.bi-door-closed-fill::before { content: "\f305"; } +.bi-door-closed::before { content: "\f306"; } +.bi-door-open-fill::before { content: "\f307"; } +.bi-door-open::before { content: "\f308"; } +.bi-dot::before { content: "\f309"; } +.bi-download::before { content: "\f30a"; } +.bi-droplet-fill::before { content: "\f30b"; } +.bi-droplet-half::before { content: "\f30c"; } +.bi-droplet::before { content: "\f30d"; } +.bi-earbuds::before { content: "\f30e"; } +.bi-easel-fill::before { content: "\f30f"; } +.bi-easel::before { content: "\f310"; } +.bi-egg-fill::before { content: "\f311"; } +.bi-egg-fried::before { content: "\f312"; } +.bi-egg::before { content: "\f313"; } +.bi-eject-fill::before { content: "\f314"; } +.bi-eject::before { content: "\f315"; } +.bi-emoji-angry-fill::before { content: "\f316"; } +.bi-emoji-angry::before { content: "\f317"; } +.bi-emoji-dizzy-fill::before { content: "\f318"; } +.bi-emoji-dizzy::before { content: "\f319"; } +.bi-emoji-expressionless-fill::before { content: "\f31a"; } +.bi-emoji-expressionless::before { content: "\f31b"; } +.bi-emoji-frown-fill::before { content: "\f31c"; } +.bi-emoji-frown::before { content: "\f31d"; } +.bi-emoji-heart-eyes-fill::before { content: "\f31e"; } +.bi-emoji-heart-eyes::before { content: "\f31f"; } +.bi-emoji-laughing-fill::before { content: "\f320"; } +.bi-emoji-laughing::before { content: "\f321"; } +.bi-emoji-neutral-fill::before { content: "\f322"; } +.bi-emoji-neutral::before { content: "\f323"; } +.bi-emoji-smile-fill::before { content: "\f324"; } +.bi-emoji-smile-upside-down-fill::before { content: "\f325"; } +.bi-emoji-smile-upside-down::before { content: "\f326"; } +.bi-emoji-smile::before { content: "\f327"; } +.bi-emoji-sunglasses-fill::before { content: "\f328"; } +.bi-emoji-sunglasses::before { content: "\f329"; } +.bi-emoji-wink-fill::before { content: "\f32a"; } +.bi-emoji-wink::before { content: "\f32b"; } +.bi-envelope-fill::before { content: "\f32c"; } +.bi-envelope-open-fill::before { content: "\f32d"; } +.bi-envelope-open::before { content: "\f32e"; } +.bi-envelope::before { content: "\f32f"; } +.bi-eraser-fill::before { content: "\f330"; } +.bi-eraser::before { content: "\f331"; } +.bi-exclamation-circle-fill::before { content: "\f332"; } +.bi-exclamation-circle::before { content: "\f333"; } +.bi-exclamation-diamond-fill::before { content: "\f334"; } +.bi-exclamation-diamond::before { content: "\f335"; } +.bi-exclamation-octagon-fill::before { content: "\f336"; } +.bi-exclamation-octagon::before { content: "\f337"; } +.bi-exclamation-square-fill::before { content: "\f338"; } +.bi-exclamation-square::before { content: "\f339"; } +.bi-exclamation-triangle-fill::before { content: "\f33a"; } +.bi-exclamation-triangle::before { content: "\f33b"; } +.bi-exclamation::before { content: "\f33c"; } +.bi-exclude::before { content: "\f33d"; } +.bi-eye-fill::before { content: "\f33e"; } +.bi-eye-slash-fill::before { content: "\f33f"; } +.bi-eye-slash::before { content: "\f340"; } +.bi-eye::before { content: "\f341"; } +.bi-eyedropper::before { content: "\f342"; } +.bi-eyeglasses::before { content: "\f343"; } +.bi-facebook::before { content: "\f344"; } +.bi-file-arrow-down-fill::before { content: "\f345"; } +.bi-file-arrow-down::before { content: "\f346"; } +.bi-file-arrow-up-fill::before { content: "\f347"; } +.bi-file-arrow-up::before { content: "\f348"; } +.bi-file-bar-graph-fill::before { content: "\f349"; } +.bi-file-bar-graph::before { content: "\f34a"; } +.bi-file-binary-fill::before { content: "\f34b"; } +.bi-file-binary::before { content: "\f34c"; } +.bi-file-break-fill::before { content: "\f34d"; } +.bi-file-break::before { content: "\f34e"; } +.bi-file-check-fill::before { content: "\f34f"; } +.bi-file-check::before { content: "\f350"; } +.bi-file-code-fill::before { content: "\f351"; } +.bi-file-code::before { content: "\f352"; } +.bi-file-diff-fill::before { content: "\f353"; } +.bi-file-diff::before { content: "\f354"; } +.bi-file-earmark-arrow-down-fill::before { content: "\f355"; } +.bi-file-earmark-arrow-down::before { content: "\f356"; } +.bi-file-earmark-arrow-up-fill::before { content: "\f357"; } +.bi-file-earmark-arrow-up::before { content: "\f358"; } +.bi-file-earmark-bar-graph-fill::before { content: "\f359"; } +.bi-file-earmark-bar-graph::before { content: "\f35a"; } +.bi-file-earmark-binary-fill::before { content: "\f35b"; } +.bi-file-earmark-binary::before { content: "\f35c"; } +.bi-file-earmark-break-fill::before { content: "\f35d"; } +.bi-file-earmark-break::before { content: "\f35e"; } +.bi-file-earmark-check-fill::before { content: "\f35f"; } +.bi-file-earmark-check::before { content: "\f360"; } +.bi-file-earmark-code-fill::before { content: "\f361"; } +.bi-file-earmark-code::before { content: "\f362"; } +.bi-file-earmark-diff-fill::before { content: "\f363"; } +.bi-file-earmark-diff::before { content: "\f364"; } +.bi-file-earmark-easel-fill::before { content: "\f365"; } +.bi-file-earmark-easel::before { content: "\f366"; } +.bi-file-earmark-excel-fill::before { content: "\f367"; } +.bi-file-earmark-excel::before { content: "\f368"; } +.bi-file-earmark-fill::before { content: "\f369"; } +.bi-file-earmark-font-fill::before { content: "\f36a"; } +.bi-file-earmark-font::before { content: "\f36b"; } +.bi-file-earmark-image-fill::before { content: "\f36c"; } +.bi-file-earmark-image::before { content: "\f36d"; } +.bi-file-earmark-lock-fill::before { content: "\f36e"; } +.bi-file-earmark-lock::before { content: "\f36f"; } +.bi-file-earmark-lock2-fill::before { content: "\f370"; } +.bi-file-earmark-lock2::before { content: "\f371"; } +.bi-file-earmark-medical-fill::before { content: "\f372"; } +.bi-file-earmark-medical::before { content: "\f373"; } +.bi-file-earmark-minus-fill::before { content: "\f374"; } +.bi-file-earmark-minus::before { content: "\f375"; } +.bi-file-earmark-music-fill::before { content: "\f376"; } +.bi-file-earmark-music::before { content: "\f377"; } +.bi-file-earmark-person-fill::before { content: "\f378"; } +.bi-file-earmark-person::before { content: "\f379"; } +.bi-file-earmark-play-fill::before { content: "\f37a"; } +.bi-file-earmark-play::before { content: "\f37b"; } +.bi-file-earmark-plus-fill::before { content: "\f37c"; } +.bi-file-earmark-plus::before { content: "\f37d"; } +.bi-file-earmark-post-fill::before { content: "\f37e"; } +.bi-file-earmark-post::before { content: "\f37f"; } +.bi-file-earmark-ppt-fill::before { content: "\f380"; } +.bi-file-earmark-ppt::before { content: "\f381"; } +.bi-file-earmark-richtext-fill::before { content: "\f382"; } +.bi-file-earmark-richtext::before { content: "\f383"; } +.bi-file-earmark-ruled-fill::before { content: "\f384"; } +.bi-file-earmark-ruled::before { content: "\f385"; } +.bi-file-earmark-slides-fill::before { content: "\f386"; } +.bi-file-earmark-slides::before { content: "\f387"; } +.bi-file-earmark-spreadsheet-fill::before { content: "\f388"; } +.bi-file-earmark-spreadsheet::before { content: "\f389"; } +.bi-file-earmark-text-fill::before { content: "\f38a"; } +.bi-file-earmark-text::before { content: "\f38b"; } +.bi-file-earmark-word-fill::before { content: "\f38c"; } +.bi-file-earmark-word::before { content: "\f38d"; } +.bi-file-earmark-x-fill::before { content: "\f38e"; } +.bi-file-earmark-x::before { content: "\f38f"; } +.bi-file-earmark-zip-fill::before { content: "\f390"; } +.bi-file-earmark-zip::before { content: "\f391"; } +.bi-file-earmark::before { content: "\f392"; } +.bi-file-easel-fill::before { content: "\f393"; } +.bi-file-easel::before { content: "\f394"; } +.bi-file-excel-fill::before { content: "\f395"; } +.bi-file-excel::before { content: "\f396"; } +.bi-file-fill::before { content: "\f397"; } +.bi-file-font-fill::before { content: "\f398"; } +.bi-file-font::before { content: "\f399"; } +.bi-file-image-fill::before { content: "\f39a"; } +.bi-file-image::before { content: "\f39b"; } +.bi-file-lock-fill::before { content: "\f39c"; } +.bi-file-lock::before { content: "\f39d"; } +.bi-file-lock2-fill::before { content: "\f39e"; } +.bi-file-lock2::before { content: "\f39f"; } +.bi-file-medical-fill::before { content: "\f3a0"; } +.bi-file-medical::before { content: "\f3a1"; } +.bi-file-minus-fill::before { content: "\f3a2"; } +.bi-file-minus::before { content: "\f3a3"; } +.bi-file-music-fill::before { content: "\f3a4"; } +.bi-file-music::before { content: "\f3a5"; } +.bi-file-person-fill::before { content: "\f3a6"; } +.bi-file-person::before { content: "\f3a7"; } +.bi-file-play-fill::before { content: "\f3a8"; } +.bi-file-play::before { content: "\f3a9"; } +.bi-file-plus-fill::before { content: "\f3aa"; } +.bi-file-plus::before { content: "\f3ab"; } +.bi-file-post-fill::before { content: "\f3ac"; } +.bi-file-post::before { content: "\f3ad"; } +.bi-file-ppt-fill::before { content: "\f3ae"; } +.bi-file-ppt::before { content: "\f3af"; } +.bi-file-richtext-fill::before { content: "\f3b0"; } +.bi-file-richtext::before { content: "\f3b1"; } +.bi-file-ruled-fill::before { content: "\f3b2"; } +.bi-file-ruled::before { content: "\f3b3"; } +.bi-file-slides-fill::before { content: "\f3b4"; } +.bi-file-slides::before { content: "\f3b5"; } +.bi-file-spreadsheet-fill::before { content: "\f3b6"; } +.bi-file-spreadsheet::before { content: "\f3b7"; } +.bi-file-text-fill::before { content: "\f3b8"; } +.bi-file-text::before { content: "\f3b9"; } +.bi-file-word-fill::before { content: "\f3ba"; } +.bi-file-word::before { content: "\f3bb"; } +.bi-file-x-fill::before { content: "\f3bc"; } +.bi-file-x::before { content: "\f3bd"; } +.bi-file-zip-fill::before { content: "\f3be"; } +.bi-file-zip::before { content: "\f3bf"; } +.bi-file::before { content: "\f3c0"; } +.bi-files-alt::before { content: "\f3c1"; } +.bi-files::before { content: "\f3c2"; } +.bi-film::before { content: "\f3c3"; } +.bi-filter-circle-fill::before { content: "\f3c4"; } +.bi-filter-circle::before { content: "\f3c5"; } +.bi-filter-left::before { content: "\f3c6"; } +.bi-filter-right::before { content: "\f3c7"; } +.bi-filter-square-fill::before { content: "\f3c8"; } +.bi-filter-square::before { content: "\f3c9"; } +.bi-filter::before { content: "\f3ca"; } +.bi-flag-fill::before { content: "\f3cb"; } +.bi-flag::before { content: "\f3cc"; } +.bi-flower1::before { content: "\f3cd"; } +.bi-flower2::before { content: "\f3ce"; } +.bi-flower3::before { content: "\f3cf"; } +.bi-folder-check::before { content: "\f3d0"; } +.bi-folder-fill::before { content: "\f3d1"; } +.bi-folder-minus::before { content: "\f3d2"; } +.bi-folder-plus::before { content: "\f3d3"; } +.bi-folder-symlink-fill::before { content: "\f3d4"; } +.bi-folder-symlink::before { content: "\f3d5"; } +.bi-folder-x::before { content: "\f3d6"; } +.bi-folder::before { content: "\f3d7"; } +.bi-folder2-open::before { content: "\f3d8"; } +.bi-folder2::before { content: "\f3d9"; } +.bi-fonts::before { content: "\f3da"; } +.bi-forward-fill::before { content: "\f3db"; } +.bi-forward::before { content: "\f3dc"; } +.bi-front::before { content: "\f3dd"; } +.bi-fullscreen-exit::before { content: "\f3de"; } +.bi-fullscreen::before { content: "\f3df"; } +.bi-funnel-fill::before { content: "\f3e0"; } +.bi-funnel::before { content: "\f3e1"; } +.bi-gear-fill::before { content: "\f3e2"; } +.bi-gear-wide-connected::before { content: "\f3e3"; } +.bi-gear-wide::before { content: "\f3e4"; } +.bi-gear::before { content: "\f3e5"; } +.bi-gem::before { content: "\f3e6"; } +.bi-geo-alt-fill::before { content: "\f3e7"; } +.bi-geo-alt::before { content: "\f3e8"; } +.bi-geo-fill::before { content: "\f3e9"; } +.bi-geo::before { content: "\f3ea"; } +.bi-gift-fill::before { content: "\f3eb"; } +.bi-gift::before { content: "\f3ec"; } +.bi-github::before { content: "\f3ed"; } +.bi-globe::before { content: "\f3ee"; } +.bi-globe2::before { content: "\f3ef"; } +.bi-google::before { content: "\f3f0"; } +.bi-graph-down::before { content: "\f3f1"; } +.bi-graph-up::before { content: "\f3f2"; } +.bi-grid-1x2-fill::before { content: "\f3f3"; } +.bi-grid-1x2::before { content: "\f3f4"; } +.bi-grid-3x2-gap-fill::before { content: "\f3f5"; } +.bi-grid-3x2-gap::before { content: "\f3f6"; } +.bi-grid-3x2::before { content: "\f3f7"; } +.bi-grid-3x3-gap-fill::before { content: "\f3f8"; } +.bi-grid-3x3-gap::before { content: "\f3f9"; } +.bi-grid-3x3::before { content: "\f3fa"; } +.bi-grid-fill::before { content: "\f3fb"; } +.bi-grid::before { content: "\f3fc"; } +.bi-grip-horizontal::before { content: "\f3fd"; } +.bi-grip-vertical::before { content: "\f3fe"; } +.bi-hammer::before { content: "\f3ff"; } +.bi-hand-index-fill::before { content: "\f400"; } +.bi-hand-index-thumb-fill::before { content: "\f401"; } +.bi-hand-index-thumb::before { content: "\f402"; } +.bi-hand-index::before { content: "\f403"; } +.bi-hand-thumbs-down-fill::before { content: "\f404"; } +.bi-hand-thumbs-down::before { content: "\f405"; } +.bi-hand-thumbs-up-fill::before { content: "\f406"; } +.bi-hand-thumbs-up::before { content: "\f407"; } +.bi-handbag-fill::before { content: "\f408"; } +.bi-handbag::before { content: "\f409"; } +.bi-hash::before { content: "\f40a"; } +.bi-hdd-fill::before { content: "\f40b"; } +.bi-hdd-network-fill::before { content: "\f40c"; } +.bi-hdd-network::before { content: "\f40d"; } +.bi-hdd-rack-fill::before { content: "\f40e"; } +.bi-hdd-rack::before { content: "\f40f"; } +.bi-hdd-stack-fill::before { content: "\f410"; } +.bi-hdd-stack::before { content: "\f411"; } +.bi-hdd::before { content: "\f412"; } +.bi-headphones::before { content: "\f413"; } +.bi-headset::before { content: "\f414"; } +.bi-heart-fill::before { content: "\f415"; } +.bi-heart-half::before { content: "\f416"; } +.bi-heart::before { content: "\f417"; } +.bi-heptagon-fill::before { content: "\f418"; } +.bi-heptagon-half::before { content: "\f419"; } +.bi-heptagon::before { content: "\f41a"; } +.bi-hexagon-fill::before { content: "\f41b"; } +.bi-hexagon-half::before { content: "\f41c"; } +.bi-hexagon::before { content: "\f41d"; } +.bi-hourglass-bottom::before { content: "\f41e"; } +.bi-hourglass-split::before { content: "\f41f"; } +.bi-hourglass-top::before { content: "\f420"; } +.bi-hourglass::before { content: "\f421"; } +.bi-house-door-fill::before { content: "\f422"; } +.bi-house-door::before { content: "\f423"; } +.bi-house-fill::before { content: "\f424"; } +.bi-house::before { content: "\f425"; } +.bi-hr::before { content: "\f426"; } +.bi-hurricane::before { content: "\f427"; } +.bi-image-alt::before { content: "\f428"; } +.bi-image-fill::before { content: "\f429"; } +.bi-image::before { content: "\f42a"; } +.bi-images::before { content: "\f42b"; } +.bi-inbox-fill::before { content: "\f42c"; } +.bi-inbox::before { content: "\f42d"; } +.bi-inboxes-fill::before { content: "\f42e"; } +.bi-inboxes::before { content: "\f42f"; } +.bi-info-circle-fill::before { content: "\f430"; } +.bi-info-circle::before { content: "\f431"; } +.bi-info-square-fill::before { content: "\f432"; } +.bi-info-square::before { content: "\f433"; } +.bi-info::before { content: "\f434"; } +.bi-input-cursor-text::before { content: "\f435"; } +.bi-input-cursor::before { content: "\f436"; } +.bi-instagram::before { content: "\f437"; } +.bi-intersect::before { content: "\f438"; } +.bi-journal-album::before { content: "\f439"; } +.bi-journal-arrow-down::before { content: "\f43a"; } +.bi-journal-arrow-up::before { content: "\f43b"; } +.bi-journal-bookmark-fill::before { content: "\f43c"; } +.bi-journal-bookmark::before { content: "\f43d"; } +.bi-journal-check::before { content: "\f43e"; } +.bi-journal-code::before { content: "\f43f"; } +.bi-journal-medical::before { content: "\f440"; } +.bi-journal-minus::before { content: "\f441"; } +.bi-journal-plus::before { content: "\f442"; } +.bi-journal-richtext::before { content: "\f443"; } +.bi-journal-text::before { content: "\f444"; } +.bi-journal-x::before { content: "\f445"; } +.bi-journal::before { content: "\f446"; } +.bi-journals::before { content: "\f447"; } +.bi-joystick::before { content: "\f448"; } +.bi-justify-left::before { content: "\f449"; } +.bi-justify-right::before { content: "\f44a"; } +.bi-justify::before { content: "\f44b"; } +.bi-kanban-fill::before { content: "\f44c"; } +.bi-kanban::before { content: "\f44d"; } +.bi-key-fill::before { content: "\f44e"; } +.bi-key::before { content: "\f44f"; } +.bi-keyboard-fill::before { content: "\f450"; } +.bi-keyboard::before { content: "\f451"; } +.bi-ladder::before { content: "\f452"; } +.bi-lamp-fill::before { content: "\f453"; } +.bi-lamp::before { content: "\f454"; } +.bi-laptop-fill::before { content: "\f455"; } +.bi-laptop::before { content: "\f456"; } +.bi-layer-backward::before { content: "\f457"; } +.bi-layer-forward::before { content: "\f458"; } +.bi-layers-fill::before { content: "\f459"; } +.bi-layers-half::before { content: "\f45a"; } +.bi-layers::before { content: "\f45b"; } +.bi-layout-sidebar-inset-reverse::before { content: "\f45c"; } +.bi-layout-sidebar-inset::before { content: "\f45d"; } +.bi-layout-sidebar-reverse::before { content: "\f45e"; } +.bi-layout-sidebar::before { content: "\f45f"; } +.bi-layout-split::before { content: "\f460"; } +.bi-layout-text-sidebar-reverse::before { content: "\f461"; } +.bi-layout-text-sidebar::before { content: "\f462"; } +.bi-layout-text-window-reverse::before { content: "\f463"; } +.bi-layout-text-window::before { content: "\f464"; } +.bi-layout-three-columns::before { content: "\f465"; } +.bi-layout-wtf::before { content: "\f466"; } +.bi-life-preserver::before { content: "\f467"; } +.bi-lightbulb-fill::before { content: "\f468"; } +.bi-lightbulb-off-fill::before { content: "\f469"; } +.bi-lightbulb-off::before { content: "\f46a"; } +.bi-lightbulb::before { content: "\f46b"; } +.bi-lightning-charge-fill::before { content: "\f46c"; } +.bi-lightning-charge::before { content: "\f46d"; } +.bi-lightning-fill::before { content: "\f46e"; } +.bi-lightning::before { content: "\f46f"; } +.bi-link-45deg::before { content: "\f470"; } +.bi-link::before { content: "\f471"; } +.bi-linkedin::before { content: "\f472"; } +.bi-list-check::before { content: "\f473"; } +.bi-list-nested::before { content: "\f474"; } +.bi-list-ol::before { content: "\f475"; } +.bi-list-stars::before { content: "\f476"; } +.bi-list-task::before { content: "\f477"; } +.bi-list-ul::before { content: "\f478"; } +.bi-list::before { content: "\f479"; } +.bi-lock-fill::before { content: "\f47a"; } +.bi-lock::before { content: "\f47b"; } +.bi-mailbox::before { content: "\f47c"; } +.bi-mailbox2::before { content: "\f47d"; } +.bi-map-fill::before { content: "\f47e"; } +.bi-map::before { content: "\f47f"; } +.bi-markdown-fill::before { content: "\f480"; } +.bi-markdown::before { content: "\f481"; } +.bi-mask::before { content: "\f482"; } +.bi-megaphone-fill::before { content: "\f483"; } +.bi-megaphone::before { content: "\f484"; } +.bi-menu-app-fill::before { content: "\f485"; } +.bi-menu-app::before { content: "\f486"; } +.bi-menu-button-fill::before { content: "\f487"; } +.bi-menu-button-wide-fill::before { content: "\f488"; } +.bi-menu-button-wide::before { content: "\f489"; } +.bi-menu-button::before { content: "\f48a"; } +.bi-menu-down::before { content: "\f48b"; } +.bi-menu-up::before { content: "\f48c"; } +.bi-mic-fill::before { content: "\f48d"; } +.bi-mic-mute-fill::before { content: "\f48e"; } +.bi-mic-mute::before { content: "\f48f"; } +.bi-mic::before { content: "\f490"; } +.bi-minecart-loaded::before { content: "\f491"; } +.bi-minecart::before { content: "\f492"; } +.bi-moisture::before { content: "\f493"; } +.bi-moon-fill::before { content: "\f494"; } +.bi-moon-stars-fill::before { content: "\f495"; } +.bi-moon-stars::before { content: "\f496"; } +.bi-moon::before { content: "\f497"; } +.bi-mouse-fill::before { content: "\f498"; } +.bi-mouse::before { content: "\f499"; } +.bi-mouse2-fill::before { content: "\f49a"; } +.bi-mouse2::before { content: "\f49b"; } +.bi-mouse3-fill::before { content: "\f49c"; } +.bi-mouse3::before { content: "\f49d"; } +.bi-music-note-beamed::before { content: "\f49e"; } +.bi-music-note-list::before { content: "\f49f"; } +.bi-music-note::before { content: "\f4a0"; } +.bi-music-player-fill::before { content: "\f4a1"; } +.bi-music-player::before { content: "\f4a2"; } +.bi-newspaper::before { content: "\f4a3"; } +.bi-node-minus-fill::before { content: "\f4a4"; } +.bi-node-minus::before { content: "\f4a5"; } +.bi-node-plus-fill::before { content: "\f4a6"; } +.bi-node-plus::before { content: "\f4a7"; } +.bi-nut-fill::before { content: "\f4a8"; } +.bi-nut::before { content: "\f4a9"; } +.bi-octagon-fill::before { content: "\f4aa"; } +.bi-octagon-half::before { content: "\f4ab"; } +.bi-octagon::before { content: "\f4ac"; } +.bi-option::before { content: "\f4ad"; } +.bi-outlet::before { content: "\f4ae"; } +.bi-paint-bucket::before { content: "\f4af"; } +.bi-palette-fill::before { content: "\f4b0"; } +.bi-palette::before { content: "\f4b1"; } +.bi-palette2::before { content: "\f4b2"; } +.bi-paperclip::before { content: "\f4b3"; } +.bi-paragraph::before { content: "\f4b4"; } +.bi-patch-check-fill::before { content: "\f4b5"; } +.bi-patch-check::before { content: "\f4b6"; } +.bi-patch-exclamation-fill::before { content: "\f4b7"; } +.bi-patch-exclamation::before { content: "\f4b8"; } +.bi-patch-minus-fill::before { content: "\f4b9"; } +.bi-patch-minus::before { content: "\f4ba"; } +.bi-patch-plus-fill::before { content: "\f4bb"; } +.bi-patch-plus::before { content: "\f4bc"; } +.bi-patch-question-fill::before { content: "\f4bd"; } +.bi-patch-question::before { content: "\f4be"; } +.bi-pause-btn-fill::before { content: "\f4bf"; } +.bi-pause-btn::before { content: "\f4c0"; } +.bi-pause-circle-fill::before { content: "\f4c1"; } +.bi-pause-circle::before { content: "\f4c2"; } +.bi-pause-fill::before { content: "\f4c3"; } +.bi-pause::before { content: "\f4c4"; } +.bi-peace-fill::before { content: "\f4c5"; } +.bi-peace::before { content: "\f4c6"; } +.bi-pen-fill::before { content: "\f4c7"; } +.bi-pen::before { content: "\f4c8"; } +.bi-pencil-fill::before { content: "\f4c9"; } +.bi-pencil-square::before { content: "\f4ca"; } +.bi-pencil::before { content: "\f4cb"; } +.bi-pentagon-fill::before { content: "\f4cc"; } +.bi-pentagon-half::before { content: "\f4cd"; } +.bi-pentagon::before { content: "\f4ce"; } +.bi-people-fill::before { content: "\f4cf"; } +.bi-people::before { content: "\f4d0"; } +.bi-percent::before { content: "\f4d1"; } +.bi-person-badge-fill::before { content: "\f4d2"; } +.bi-person-badge::before { content: "\f4d3"; } +.bi-person-bounding-box::before { content: "\f4d4"; } +.bi-person-check-fill::before { content: "\f4d5"; } +.bi-person-check::before { content: "\f4d6"; } +.bi-person-circle::before { content: "\f4d7"; } +.bi-person-dash-fill::before { content: "\f4d8"; } +.bi-person-dash::before { content: "\f4d9"; } +.bi-person-fill::before { content: "\f4da"; } +.bi-person-lines-fill::before { content: "\f4db"; } +.bi-person-plus-fill::before { content: "\f4dc"; } +.bi-person-plus::before { content: "\f4dd"; } +.bi-person-square::before { content: "\f4de"; } +.bi-person-x-fill::before { content: "\f4df"; } +.bi-person-x::before { content: "\f4e0"; } +.bi-person::before { content: "\f4e1"; } +.bi-phone-fill::before { content: "\f4e2"; } +.bi-phone-landscape-fill::before { content: "\f4e3"; } +.bi-phone-landscape::before { content: "\f4e4"; } +.bi-phone-vibrate-fill::before { content: "\f4e5"; } +.bi-phone-vibrate::before { content: "\f4e6"; } +.bi-phone::before { content: "\f4e7"; } +.bi-pie-chart-fill::before { content: "\f4e8"; } +.bi-pie-chart::before { content: "\f4e9"; } +.bi-pin-angle-fill::before { content: "\f4ea"; } +.bi-pin-angle::before { content: "\f4eb"; } +.bi-pin-fill::before { content: "\f4ec"; } +.bi-pin::before { content: "\f4ed"; } +.bi-pip-fill::before { content: "\f4ee"; } +.bi-pip::before { content: "\f4ef"; } +.bi-play-btn-fill::before { content: "\f4f0"; } +.bi-play-btn::before { content: "\f4f1"; } +.bi-play-circle-fill::before { content: "\f4f2"; } +.bi-play-circle::before { content: "\f4f3"; } +.bi-play-fill::before { content: "\f4f4"; } +.bi-play::before { content: "\f4f5"; } +.bi-plug-fill::before { content: "\f4f6"; } +.bi-plug::before { content: "\f4f7"; } +.bi-plus-circle-dotted::before { content: "\f4f8"; } +.bi-plus-circle-fill::before { content: "\f4f9"; } +.bi-plus-circle::before { content: "\f4fa"; } +.bi-plus-square-dotted::before { content: "\f4fb"; } +.bi-plus-square-fill::before { content: "\f4fc"; } +.bi-plus-square::before { content: "\f4fd"; } +.bi-plus::before { content: "\f4fe"; } +.bi-power::before { content: "\f4ff"; } +.bi-printer-fill::before { content: "\f500"; } +.bi-printer::before { content: "\f501"; } +.bi-puzzle-fill::before { content: "\f502"; } +.bi-puzzle::before { content: "\f503"; } +.bi-question-circle-fill::before { content: "\f504"; } +.bi-question-circle::before { content: "\f505"; } +.bi-question-diamond-fill::before { content: "\f506"; } +.bi-question-diamond::before { content: "\f507"; } +.bi-question-octagon-fill::before { content: "\f508"; } +.bi-question-octagon::before { content: "\f509"; } +.bi-question-square-fill::before { content: "\f50a"; } +.bi-question-square::before { content: "\f50b"; } +.bi-question::before { content: "\f50c"; } +.bi-rainbow::before { content: "\f50d"; } +.bi-receipt-cutoff::before { content: "\f50e"; } +.bi-receipt::before { content: "\f50f"; } +.bi-reception-0::before { content: "\f510"; } +.bi-reception-1::before { content: "\f511"; } +.bi-reception-2::before { content: "\f512"; } +.bi-reception-3::before { content: "\f513"; } +.bi-reception-4::before { content: "\f514"; } +.bi-record-btn-fill::before { content: "\f515"; } +.bi-record-btn::before { content: "\f516"; } +.bi-record-circle-fill::before { content: "\f517"; } +.bi-record-circle::before { content: "\f518"; } +.bi-record-fill::before { content: "\f519"; } +.bi-record::before { content: "\f51a"; } +.bi-record2-fill::before { content: "\f51b"; } +.bi-record2::before { content: "\f51c"; } +.bi-reply-all-fill::before { content: "\f51d"; } +.bi-reply-all::before { content: "\f51e"; } +.bi-reply-fill::before { content: "\f51f"; } +.bi-reply::before { content: "\f520"; } +.bi-rss-fill::before { content: "\f521"; } +.bi-rss::before { content: "\f522"; } +.bi-rulers::before { content: "\f523"; } +.bi-save-fill::before { content: "\f524"; } +.bi-save::before { content: "\f525"; } +.bi-save2-fill::before { content: "\f526"; } +.bi-save2::before { content: "\f527"; } +.bi-scissors::before { content: "\f528"; } +.bi-screwdriver::before { content: "\f529"; } +.bi-search::before { content: "\f52a"; } +.bi-segmented-nav::before { content: "\f52b"; } +.bi-server::before { content: "\f52c"; } +.bi-share-fill::before { content: "\f52d"; } +.bi-share::before { content: "\f52e"; } +.bi-shield-check::before { content: "\f52f"; } +.bi-shield-exclamation::before { content: "\f530"; } +.bi-shield-fill-check::before { content: "\f531"; } +.bi-shield-fill-exclamation::before { content: "\f532"; } +.bi-shield-fill-minus::before { content: "\f533"; } +.bi-shield-fill-plus::before { content: "\f534"; } +.bi-shield-fill-x::before { content: "\f535"; } +.bi-shield-fill::before { content: "\f536"; } +.bi-shield-lock-fill::before { content: "\f537"; } +.bi-shield-lock::before { content: "\f538"; } +.bi-shield-minus::before { content: "\f539"; } +.bi-shield-plus::before { content: "\f53a"; } +.bi-shield-shaded::before { content: "\f53b"; } +.bi-shield-slash-fill::before { content: "\f53c"; } +.bi-shield-slash::before { content: "\f53d"; } +.bi-shield-x::before { content: "\f53e"; } +.bi-shield::before { content: "\f53f"; } +.bi-shift-fill::before { content: "\f540"; } +.bi-shift::before { content: "\f541"; } +.bi-shop-window::before { content: "\f542"; } +.bi-shop::before { content: "\f543"; } +.bi-shuffle::before { content: "\f544"; } +.bi-signpost-2-fill::before { content: "\f545"; } +.bi-signpost-2::before { content: "\f546"; } +.bi-signpost-fill::before { content: "\f547"; } +.bi-signpost-split-fill::before { content: "\f548"; } +.bi-signpost-split::before { content: "\f549"; } +.bi-signpost::before { content: "\f54a"; } +.bi-sim-fill::before { content: "\f54b"; } +.bi-sim::before { content: "\f54c"; } +.bi-skip-backward-btn-fill::before { content: "\f54d"; } +.bi-skip-backward-btn::before { content: "\f54e"; } +.bi-skip-backward-circle-fill::before { content: "\f54f"; } +.bi-skip-backward-circle::before { content: "\f550"; } +.bi-skip-backward-fill::before { content: "\f551"; } +.bi-skip-backward::before { content: "\f552"; } +.bi-skip-end-btn-fill::before { content: "\f553"; } +.bi-skip-end-btn::before { content: "\f554"; } +.bi-skip-end-circle-fill::before { content: "\f555"; } +.bi-skip-end-circle::before { content: "\f556"; } +.bi-skip-end-fill::before { content: "\f557"; } +.bi-skip-end::before { content: "\f558"; } +.bi-skip-forward-btn-fill::before { content: "\f559"; } +.bi-skip-forward-btn::before { content: "\f55a"; } +.bi-skip-forward-circle-fill::before { content: "\f55b"; } +.bi-skip-forward-circle::before { content: "\f55c"; } +.bi-skip-forward-fill::before { content: "\f55d"; } +.bi-skip-forward::before { content: "\f55e"; } +.bi-skip-start-btn-fill::before { content: "\f55f"; } +.bi-skip-start-btn::before { content: "\f560"; } +.bi-skip-start-circle-fill::before { content: "\f561"; } +.bi-skip-start-circle::before { content: "\f562"; } +.bi-skip-start-fill::before { content: "\f563"; } +.bi-skip-start::before { content: "\f564"; } +.bi-slack::before { content: "\f565"; } +.bi-slash-circle-fill::before { content: "\f566"; } +.bi-slash-circle::before { content: "\f567"; } +.bi-slash-square-fill::before { content: "\f568"; } +.bi-slash-square::before { content: "\f569"; } +.bi-slash::before { content: "\f56a"; } +.bi-sliders::before { content: "\f56b"; } +.bi-smartwatch::before { content: "\f56c"; } +.bi-snow::before { content: "\f56d"; } +.bi-snow2::before { content: "\f56e"; } +.bi-snow3::before { content: "\f56f"; } +.bi-sort-alpha-down-alt::before { content: "\f570"; } +.bi-sort-alpha-down::before { content: "\f571"; } +.bi-sort-alpha-up-alt::before { content: "\f572"; } +.bi-sort-alpha-up::before { content: "\f573"; } +.bi-sort-down-alt::before { content: "\f574"; } +.bi-sort-down::before { content: "\f575"; } +.bi-sort-numeric-down-alt::before { content: "\f576"; } +.bi-sort-numeric-down::before { content: "\f577"; } +.bi-sort-numeric-up-alt::before { content: "\f578"; } +.bi-sort-numeric-up::before { content: "\f579"; } +.bi-sort-up-alt::before { content: "\f57a"; } +.bi-sort-up::before { content: "\f57b"; } +.bi-soundwave::before { content: "\f57c"; } +.bi-speaker-fill::before { content: "\f57d"; } +.bi-speaker::before { content: "\f57e"; } +.bi-speedometer::before { content: "\f57f"; } +.bi-speedometer2::before { content: "\f580"; } +.bi-spellcheck::before { content: "\f581"; } +.bi-square-fill::before { content: "\f582"; } +.bi-square-half::before { content: "\f583"; } +.bi-square::before { content: "\f584"; } +.bi-stack::before { content: "\f585"; } +.bi-star-fill::before { content: "\f586"; } +.bi-star-half::before { content: "\f587"; } +.bi-star::before { content: "\f588"; } +.bi-stars::before { content: "\f589"; } +.bi-stickies-fill::before { content: "\f58a"; } +.bi-stickies::before { content: "\f58b"; } +.bi-sticky-fill::before { content: "\f58c"; } +.bi-sticky::before { content: "\f58d"; } +.bi-stop-btn-fill::before { content: "\f58e"; } +.bi-stop-btn::before { content: "\f58f"; } +.bi-stop-circle-fill::before { content: "\f590"; } +.bi-stop-circle::before { content: "\f591"; } +.bi-stop-fill::before { content: "\f592"; } +.bi-stop::before { content: "\f593"; } +.bi-stoplights-fill::before { content: "\f594"; } +.bi-stoplights::before { content: "\f595"; } +.bi-stopwatch-fill::before { content: "\f596"; } +.bi-stopwatch::before { content: "\f597"; } +.bi-subtract::before { content: "\f598"; } +.bi-suit-club-fill::before { content: "\f599"; } +.bi-suit-club::before { content: "\f59a"; } +.bi-suit-diamond-fill::before { content: "\f59b"; } +.bi-suit-diamond::before { content: "\f59c"; } +.bi-suit-heart-fill::before { content: "\f59d"; } +.bi-suit-heart::before { content: "\f59e"; } +.bi-suit-spade-fill::before { content: "\f59f"; } +.bi-suit-spade::before { content: "\f5a0"; } +.bi-sun-fill::before { content: "\f5a1"; } +.bi-sun::before { content: "\f5a2"; } +.bi-sunglasses::before { content: "\f5a3"; } +.bi-sunrise-fill::before { content: "\f5a4"; } +.bi-sunrise::before { content: "\f5a5"; } +.bi-sunset-fill::before { content: "\f5a6"; } +.bi-sunset::before { content: "\f5a7"; } +.bi-symmetry-horizontal::before { content: "\f5a8"; } +.bi-symmetry-vertical::before { content: "\f5a9"; } +.bi-table::before { content: "\f5aa"; } +.bi-tablet-fill::before { content: "\f5ab"; } +.bi-tablet-landscape-fill::before { content: "\f5ac"; } +.bi-tablet-landscape::before { content: "\f5ad"; } +.bi-tablet::before { content: "\f5ae"; } +.bi-tag-fill::before { content: "\f5af"; } +.bi-tag::before { content: "\f5b0"; } +.bi-tags-fill::before { content: "\f5b1"; } +.bi-tags::before { content: "\f5b2"; } +.bi-telegram::before { content: "\f5b3"; } +.bi-telephone-fill::before { content: "\f5b4"; } +.bi-telephone-forward-fill::before { content: "\f5b5"; } +.bi-telephone-forward::before { content: "\f5b6"; } +.bi-telephone-inbound-fill::before { content: "\f5b7"; } +.bi-telephone-inbound::before { content: "\f5b8"; } +.bi-telephone-minus-fill::before { content: "\f5b9"; } +.bi-telephone-minus::before { content: "\f5ba"; } +.bi-telephone-outbound-fill::before { content: "\f5bb"; } +.bi-telephone-outbound::before { content: "\f5bc"; } +.bi-telephone-plus-fill::before { content: "\f5bd"; } +.bi-telephone-plus::before { content: "\f5be"; } +.bi-telephone-x-fill::before { content: "\f5bf"; } +.bi-telephone-x::before { content: "\f5c0"; } +.bi-telephone::before { content: "\f5c1"; } +.bi-terminal-fill::before { content: "\f5c2"; } +.bi-terminal::before { content: "\f5c3"; } +.bi-text-center::before { content: "\f5c4"; } +.bi-text-indent-left::before { content: "\f5c5"; } +.bi-text-indent-right::before { content: "\f5c6"; } +.bi-text-left::before { content: "\f5c7"; } +.bi-text-paragraph::before { content: "\f5c8"; } +.bi-text-right::before { content: "\f5c9"; } +.bi-textarea-resize::before { content: "\f5ca"; } +.bi-textarea-t::before { content: "\f5cb"; } +.bi-textarea::before { content: "\f5cc"; } +.bi-thermometer-half::before { content: "\f5cd"; } +.bi-thermometer-high::before { content: "\f5ce"; } +.bi-thermometer-low::before { content: "\f5cf"; } +.bi-thermometer-snow::before { content: "\f5d0"; } +.bi-thermometer-sun::before { content: "\f5d1"; } +.bi-thermometer::before { content: "\f5d2"; } +.bi-three-dots-vertical::before { content: "\f5d3"; } +.bi-three-dots::before { content: "\f5d4"; } +.bi-toggle-off::before { content: "\f5d5"; } +.bi-toggle-on::before { content: "\f5d6"; } +.bi-toggle2-off::before { content: "\f5d7"; } +.bi-toggle2-on::before { content: "\f5d8"; } +.bi-toggles::before { content: "\f5d9"; } +.bi-toggles2::before { content: "\f5da"; } +.bi-tools::before { content: "\f5db"; } +.bi-tornado::before { content: "\f5dc"; } +.bi-trash-fill::before { content: "\f5dd"; } +.bi-trash::before { content: "\f5de"; } +.bi-trash2-fill::before { content: "\f5df"; } +.bi-trash2::before { content: "\f5e0"; } +.bi-tree-fill::before { content: "\f5e1"; } +.bi-tree::before { content: "\f5e2"; } +.bi-triangle-fill::before { content: "\f5e3"; } +.bi-triangle-half::before { content: "\f5e4"; } +.bi-triangle::before { content: "\f5e5"; } +.bi-trophy-fill::before { content: "\f5e6"; } +.bi-trophy::before { content: "\f5e7"; } +.bi-tropical-storm::before { content: "\f5e8"; } +.bi-truck-flatbed::before { content: "\f5e9"; } +.bi-truck::before { content: "\f5ea"; } +.bi-tsunami::before { content: "\f5eb"; } +.bi-tv-fill::before { content: "\f5ec"; } +.bi-tv::before { content: "\f5ed"; } +.bi-twitch::before { content: "\f5ee"; } +.bi-twitter::before { content: "\f5ef"; } +.bi-type-bold::before { content: "\f5f0"; } +.bi-type-h1::before { content: "\f5f1"; } +.bi-type-h2::before { content: "\f5f2"; } +.bi-type-h3::before { content: "\f5f3"; } +.bi-type-italic::before { content: "\f5f4"; } +.bi-type-strikethrough::before { content: "\f5f5"; } +.bi-type-underline::before { content: "\f5f6"; } +.bi-type::before { content: "\f5f7"; } +.bi-ui-checks-grid::before { content: "\f5f8"; } +.bi-ui-checks::before { content: "\f5f9"; } +.bi-ui-radios-grid::before { content: "\f5fa"; } +.bi-ui-radios::before { content: "\f5fb"; } +.bi-umbrella-fill::before { content: "\f5fc"; } +.bi-umbrella::before { content: "\f5fd"; } +.bi-union::before { content: "\f5fe"; } +.bi-unlock-fill::before { content: "\f5ff"; } +.bi-unlock::before { content: "\f600"; } +.bi-upc-scan::before { content: "\f601"; } +.bi-upc::before { content: "\f602"; } +.bi-upload::before { content: "\f603"; } +.bi-vector-pen::before { content: "\f604"; } +.bi-view-list::before { content: "\f605"; } +.bi-view-stacked::before { content: "\f606"; } +.bi-vinyl-fill::before { content: "\f607"; } +.bi-vinyl::before { content: "\f608"; } +.bi-voicemail::before { content: "\f609"; } +.bi-volume-down-fill::before { content: "\f60a"; } +.bi-volume-down::before { content: "\f60b"; } +.bi-volume-mute-fill::before { content: "\f60c"; } +.bi-volume-mute::before { content: "\f60d"; } +.bi-volume-off-fill::before { content: "\f60e"; } +.bi-volume-off::before { content: "\f60f"; } +.bi-volume-up-fill::before { content: "\f610"; } +.bi-volume-up::before { content: "\f611"; } +.bi-vr::before { content: "\f612"; } +.bi-wallet-fill::before { content: "\f613"; } +.bi-wallet::before { content: "\f614"; } +.bi-wallet2::before { content: "\f615"; } +.bi-watch::before { content: "\f616"; } +.bi-water::before { content: "\f617"; } +.bi-whatsapp::before { content: "\f618"; } +.bi-wifi-1::before { content: "\f619"; } +.bi-wifi-2::before { content: "\f61a"; } +.bi-wifi-off::before { content: "\f61b"; } +.bi-wifi::before { content: "\f61c"; } +.bi-wind::before { content: "\f61d"; } +.bi-window-dock::before { content: "\f61e"; } +.bi-window-sidebar::before { content: "\f61f"; } +.bi-window::before { content: "\f620"; } +.bi-wrench::before { content: "\f621"; } +.bi-x-circle-fill::before { content: "\f622"; } +.bi-x-circle::before { content: "\f623"; } +.bi-x-diamond-fill::before { content: "\f624"; } +.bi-x-diamond::before { content: "\f625"; } +.bi-x-octagon-fill::before { content: "\f626"; } +.bi-x-octagon::before { content: "\f627"; } +.bi-x-square-fill::before { content: "\f628"; } +.bi-x-square::before { content: "\f629"; } +.bi-x::before { content: "\f62a"; } +.bi-youtube::before { content: "\f62b"; } +.bi-zoom-in::before { content: "\f62c"; } +.bi-zoom-out::before { content: "\f62d"; } +.bi-bank::before { content: "\f62e"; } +.bi-bank2::before { content: "\f62f"; } +.bi-bell-slash-fill::before { content: "\f630"; } +.bi-bell-slash::before { content: "\f631"; } +.bi-cash-coin::before { content: "\f632"; } +.bi-check-lg::before { content: "\f633"; } +.bi-coin::before { content: "\f634"; } +.bi-currency-bitcoin::before { content: "\f635"; } +.bi-currency-dollar::before { content: "\f636"; } +.bi-currency-euro::before { content: "\f637"; } +.bi-currency-exchange::before { content: "\f638"; } +.bi-currency-pound::before { content: "\f639"; } +.bi-currency-yen::before { content: "\f63a"; } +.bi-dash-lg::before { content: "\f63b"; } +.bi-exclamation-lg::before { content: "\f63c"; } +.bi-file-earmark-pdf-fill::before { content: "\f63d"; } +.bi-file-earmark-pdf::before { content: "\f63e"; } +.bi-file-pdf-fill::before { content: "\f63f"; } +.bi-file-pdf::before { content: "\f640"; } +.bi-gender-ambiguous::before { content: "\f641"; } +.bi-gender-female::before { content: "\f642"; } +.bi-gender-male::before { content: "\f643"; } +.bi-gender-trans::before { content: "\f644"; } +.bi-headset-vr::before { content: "\f645"; } +.bi-info-lg::before { content: "\f646"; } +.bi-mastodon::before { content: "\f647"; } +.bi-messenger::before { content: "\f648"; } +.bi-piggy-bank-fill::before { content: "\f649"; } +.bi-piggy-bank::before { content: "\f64a"; } +.bi-pin-map-fill::before { content: "\f64b"; } +.bi-pin-map::before { content: "\f64c"; } +.bi-plus-lg::before { content: "\f64d"; } +.bi-question-lg::before { content: "\f64e"; } +.bi-recycle::before { content: "\f64f"; } +.bi-reddit::before { content: "\f650"; } +.bi-safe-fill::before { content: "\f651"; } +.bi-safe2-fill::before { content: "\f652"; } +.bi-safe2::before { content: "\f653"; } +.bi-sd-card-fill::before { content: "\f654"; } +.bi-sd-card::before { content: "\f655"; } +.bi-skype::before { content: "\f656"; } +.bi-slash-lg::before { content: "\f657"; } +.bi-translate::before { content: "\f658"; } +.bi-x-lg::before { content: "\f659"; } +.bi-safe::before { content: "\f65a"; } +.bi-apple::before { content: "\f65b"; } +.bi-microsoft::before { content: "\f65d"; } +.bi-windows::before { content: "\f65e"; } +.bi-behance::before { content: "\f65c"; } +.bi-dribbble::before { content: "\f65f"; } +.bi-line::before { content: "\f660"; } +.bi-medium::before { content: "\f661"; } +.bi-paypal::before { content: "\f662"; } +.bi-pinterest::before { content: "\f663"; } +.bi-signal::before { content: "\f664"; } +.bi-snapchat::before { content: "\f665"; } +.bi-spotify::before { content: "\f666"; } +.bi-stack-overflow::before { content: "\f667"; } +.bi-strava::before { content: "\f668"; } +.bi-wordpress::before { content: "\f669"; } +.bi-vimeo::before { content: "\f66a"; } +.bi-activity::before { content: "\f66b"; } +.bi-easel2-fill::before { content: "\f66c"; } +.bi-easel2::before { content: "\f66d"; } +.bi-easel3-fill::before { content: "\f66e"; } +.bi-easel3::before { content: "\f66f"; } +.bi-fan::before { content: "\f670"; } +.bi-fingerprint::before { content: "\f671"; } +.bi-graph-down-arrow::before { content: "\f672"; } +.bi-graph-up-arrow::before { content: "\f673"; } +.bi-hypnotize::before { content: "\f674"; } +.bi-magic::before { content: "\f675"; } +.bi-person-rolodex::before { content: "\f676"; } +.bi-person-video::before { content: "\f677"; } +.bi-person-video2::before { content: "\f678"; } +.bi-person-video3::before { content: "\f679"; } +.bi-person-workspace::before { content: "\f67a"; } +.bi-radioactive::before { content: "\f67b"; } +.bi-webcam-fill::before { content: "\f67c"; } +.bi-webcam::before { content: "\f67d"; } +.bi-yin-yang::before { content: "\f67e"; } +.bi-bandaid-fill::before { content: "\f680"; } +.bi-bandaid::before { content: "\f681"; } +.bi-bluetooth::before { content: "\f682"; } +.bi-body-text::before { content: "\f683"; } +.bi-boombox::before { content: "\f684"; } +.bi-boxes::before { content: "\f685"; } +.bi-dpad-fill::before { content: "\f686"; } +.bi-dpad::before { content: "\f687"; } +.bi-ear-fill::before { content: "\f688"; } +.bi-ear::before { content: "\f689"; } +.bi-envelope-check-1::before { content: "\f68a"; } +.bi-envelope-check-fill::before { content: "\f68b"; } +.bi-envelope-check::before { content: "\f68c"; } +.bi-envelope-dash-1::before { content: "\f68d"; } +.bi-envelope-dash-fill::before { content: "\f68e"; } +.bi-envelope-dash::before { content: "\f68f"; } +.bi-envelope-exclamation-1::before { content: "\f690"; } +.bi-envelope-exclamation-fill::before { content: "\f691"; } +.bi-envelope-exclamation::before { content: "\f692"; } +.bi-envelope-plus-fill::before { content: "\f693"; } +.bi-envelope-plus::before { content: "\f694"; } +.bi-envelope-slash-1::before { content: "\f695"; } +.bi-envelope-slash-fill::before { content: "\f696"; } +.bi-envelope-slash::before { content: "\f697"; } +.bi-envelope-x-1::before { content: "\f698"; } +.bi-envelope-x-fill::before { content: "\f699"; } +.bi-envelope-x::before { content: "\f69a"; } +.bi-explicit-fill::before { content: "\f69b"; } +.bi-explicit::before { content: "\f69c"; } +.bi-git::before { content: "\f69d"; } +.bi-infinity::before { content: "\f69e"; } +.bi-list-columns-reverse::before { content: "\f69f"; } +.bi-list-columns::before { content: "\f6a0"; } +.bi-meta::before { content: "\f6a1"; } +.bi-mortorboard-fill::before { content: "\f6a2"; } +.bi-mortorboard::before { content: "\f6a3"; } +.bi-nintendo-switch::before { content: "\f6a4"; } +.bi-pc-display-horizontal::before { content: "\f6a5"; } +.bi-pc-display::before { content: "\f6a6"; } +.bi-pc-horizontal::before { content: "\f6a7"; } +.bi-pc::before { content: "\f6a8"; } +.bi-playstation::before { content: "\f6a9"; } +.bi-plus-slash-minus::before { content: "\f6aa"; } +.bi-projector-fill::before { content: "\f6ab"; } +.bi-projector::before { content: "\f6ac"; } +.bi-qr-code-scan::before { content: "\f6ad"; } +.bi-qr-code::before { content: "\f6ae"; } +.bi-quora::before { content: "\f6af"; } +.bi-quote::before { content: "\f6b0"; } +.bi-robot::before { content: "\f6b1"; } +.bi-send-check-fill::before { content: "\f6b2"; } +.bi-send-check::before { content: "\f6b3"; } +.bi-send-dash-fill::before { content: "\f6b4"; } +.bi-send-dash::before { content: "\f6b5"; } +.bi-send-exclamation-1::before { content: "\f6b6"; } +.bi-send-exclamation-fill::before { content: "\f6b7"; } +.bi-send-exclamation::before { content: "\f6b8"; } +.bi-send-fill::before { content: "\f6b9"; } +.bi-send-plus-fill::before { content: "\f6ba"; } +.bi-send-plus::before { content: "\f6bb"; } +.bi-send-slash-fill::before { content: "\f6bc"; } +.bi-send-slash::before { content: "\f6bd"; } +.bi-send-x-fill::before { content: "\f6be"; } +.bi-send-x::before { content: "\f6bf"; } +.bi-send::before { content: "\f6c0"; } +.bi-steam::before { content: "\f6c1"; } +.bi-terminal-dash-1::before { content: "\f6c2"; } +.bi-terminal-dash::before { content: "\f6c3"; } +.bi-terminal-plus::before { content: "\f6c4"; } +.bi-terminal-split::before { content: "\f6c5"; } +.bi-ticket-detailed-fill::before { content: "\f6c6"; } +.bi-ticket-detailed::before { content: "\f6c7"; } +.bi-ticket-fill::before { content: "\f6c8"; } +.bi-ticket-perforated-fill::before { content: "\f6c9"; } +.bi-ticket-perforated::before { content: "\f6ca"; } +.bi-ticket::before { content: "\f6cb"; } +.bi-tiktok::before { content: "\f6cc"; } +.bi-window-dash::before { content: "\f6cd"; } +.bi-window-desktop::before { content: "\f6ce"; } +.bi-window-fullscreen::before { content: "\f6cf"; } +.bi-window-plus::before { content: "\f6d0"; } +.bi-window-split::before { content: "\f6d1"; } +.bi-window-stack::before { content: "\f6d2"; } +.bi-window-x::before { content: "\f6d3"; } +.bi-xbox::before { content: "\f6d4"; } +.bi-ethernet::before { content: "\f6d5"; } +.bi-hdmi-fill::before { content: "\f6d6"; } +.bi-hdmi::before { content: "\f6d7"; } +.bi-usb-c-fill::before { content: "\f6d8"; } +.bi-usb-c::before { content: "\f6d9"; } +.bi-usb-fill::before { content: "\f6da"; } +.bi-usb-plug-fill::before { content: "\f6db"; } +.bi-usb-plug::before { content: "\f6dc"; } +.bi-usb-symbol::before { content: "\f6dd"; } +.bi-usb::before { content: "\f6de"; } +.bi-boombox-fill::before { content: "\f6df"; } +.bi-displayport-1::before { content: "\f6e0"; } +.bi-displayport::before { content: "\f6e1"; } +.bi-gpu-card::before { content: "\f6e2"; } +.bi-memory::before { content: "\f6e3"; } +.bi-modem-fill::before { content: "\f6e4"; } +.bi-modem::before { content: "\f6e5"; } +.bi-motherboard-fill::before { content: "\f6e6"; } +.bi-motherboard::before { content: "\f6e7"; } +.bi-optical-audio-fill::before { content: "\f6e8"; } +.bi-optical-audio::before { content: "\f6e9"; } +.bi-pci-card::before { content: "\f6ea"; } +.bi-router-fill::before { content: "\f6eb"; } +.bi-router::before { content: "\f6ec"; } +.bi-ssd-fill::before { content: "\f6ed"; } +.bi-ssd::before { content: "\f6ee"; } +.bi-thunderbolt-fill::before { content: "\f6ef"; } +.bi-thunderbolt::before { content: "\f6f0"; } +.bi-usb-drive-fill::before { content: "\f6f1"; } +.bi-usb-drive::before { content: "\f6f2"; } +.bi-usb-micro-fill::before { content: "\f6f3"; } +.bi-usb-micro::before { content: "\f6f4"; } +.bi-usb-mini-fill::before { content: "\f6f5"; } +.bi-usb-mini::before { content: "\f6f6"; } +.bi-cloud-haze2::before { content: "\f6f7"; } +.bi-device-hdd-fill::before { content: "\f6f8"; } +.bi-device-hdd::before { content: "\f6f9"; } +.bi-device-ssd-fill::before { content: "\f6fa"; } +.bi-device-ssd::before { content: "\f6fb"; } +.bi-displayport-fill::before { content: "\f6fc"; } +.bi-mortarboard-fill::before { content: "\f6fd"; } +.bi-mortarboard::before { content: "\f6fe"; } +.bi-terminal-x::before { content: "\f6ff"; } diff --git a/Streamdeck/com.mirabox.streamingle.sdPlugin/dist/com.mirabox.streamingle.sdPlugin/propertyInspector/utils/bootstrap.min.css b/Streamdeck/com.mirabox.streamingle.sdPlugin/dist/com.mirabox.streamingle.sdPlugin/propertyInspector/utils/bootstrap.min.css new file mode 100644 index 00000000..1472dec0 --- /dev/null +++ b/Streamdeck/com.mirabox.streamingle.sdPlugin/dist/com.mirabox.streamingle.sdPlugin/propertyInspector/utils/bootstrap.min.css @@ -0,0 +1,7 @@ +@charset "UTF-8";/*! + * Bootstrap v5.1.3 (https://getbootstrap.com/) + * Copyright 2011-2021 The Bootstrap Authors + * Copyright 2011-2021 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + */:root{--bs-blue:#0d6efd;--bs-indigo:#6610f2;--bs-purple:#6f42c1;--bs-pink:#d63384;--bs-red:#dc3545;--bs-orange:#fd7e14;--bs-yellow:#ffc107;--bs-green:#198754;--bs-teal:#20c997;--bs-cyan:#0dcaf0;--bs-white:#fff;--bs-gray:#6c757d;--bs-gray-dark:#343a40;--bs-gray-100:#f8f9fa;--bs-gray-200:#e9ecef;--bs-gray-300:#dee2e6;--bs-gray-400:#ced4da;--bs-gray-500:#adb5bd;--bs-gray-600:#6c757d;--bs-gray-700:#495057;--bs-gray-800:#343a40;--bs-gray-900:#212529;--bs-primary:#0d6efd;--bs-secondary:#6c757d;--bs-success:#198754;--bs-info:#0dcaf0;--bs-warning:#ffc107;--bs-danger:#dc3545;--bs-light:#f8f9fa;--bs-dark:#212529;--bs-primary-rgb:13,110,253;--bs-secondary-rgb:108,117,125;--bs-success-rgb:25,135,84;--bs-info-rgb:13,202,240;--bs-warning-rgb:255,193,7;--bs-danger-rgb:220,53,69;--bs-light-rgb:248,249,250;--bs-dark-rgb:33,37,41;--bs-white-rgb:255,255,255;--bs-black-rgb:0,0,0;--bs-body-color-rgb:33,37,41;--bs-body-bg-rgb:255,255,255;--bs-font-sans-serif:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans","Liberation Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--bs-font-monospace:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--bs-gradient:linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));--bs-body-font-family:var(--bs-font-sans-serif);--bs-body-font-size:1rem;--bs-body-font-weight:400;--bs-body-line-height:1.5;--bs-body-color:#212529;--bs-body-bg:#fff}*,::after,::before{box-sizing:border-box}@media (prefers-reduced-motion:no-preference){:root{scroll-behavior:smooth}}body{margin:0;font-family:var(--bs-body-font-family);font-size:var(--bs-body-font-size);font-weight:var(--bs-body-font-weight);line-height:var(--bs-body-line-height);color:var(--bs-body-color);text-align:var(--bs-body-text-align);background-color:var(--bs-body-bg);-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}hr{margin:1rem 0;color:inherit;background-color:currentColor;border:0;opacity:.25}hr:not([size]){height:1px}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem;font-weight:500;line-height:1.2}.h1,h1{font-size:calc(1.375rem + 1.5vw)}@media (min-width:1200px){.h1,h1{font-size:2.5rem}}.h2,h2{font-size:calc(1.325rem + .9vw)}@media (min-width:1200px){.h2,h2{font-size:2rem}}.h3,h3{font-size:calc(1.3rem + .6vw)}@media (min-width:1200px){.h3,h3{font-size:1.75rem}}.h4,h4{font-size:calc(1.275rem + .3vw)}@media (min-width:1200px){.h4,h4{font-size:1.5rem}}.h5,h5{font-size:1.25rem}.h6,h6{font-size:1rem}p{margin-top:0;margin-bottom:1rem}abbr[data-bs-original-title],abbr[title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}ol,ul{padding-left:2rem}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}.small,small{font-size:.875em}.mark,mark{padding:.2em;background-color:#fcf8e3}sub,sup{position:relative;font-size:.75em;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#0d6efd;text-decoration:underline}a:hover{color:#0a58ca}a:not([href]):not([class]),a:not([href]):not([class]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:var(--bs-font-monospace);font-size:1em;direction:ltr;unicode-bidi:bidi-override}pre{display:block;margin-top:0;margin-bottom:1rem;overflow:auto;font-size:.875em}pre code{font-size:inherit;color:inherit;word-break:normal}code{font-size:.875em;color:#d63384;word-wrap:break-word}a>code{color:inherit}kbd{padding:.2rem .4rem;font-size:.875em;color:#fff;background-color:#212529;border-radius:.2rem}kbd kbd{padding:0;font-size:1em;font-weight:700}figure{margin:0 0 1rem}img,svg{vertical-align:middle}table{caption-side:bottom;border-collapse:collapse}caption{padding-top:.5rem;padding-bottom:.5rem;color:#6c757d;text-align:left}th{text-align:inherit;text-align:-webkit-match-parent}tbody,td,tfoot,th,thead,tr{border-color:inherit;border-style:solid;border-width:0}label{display:inline-block}button{border-radius:0}button:focus:not(:focus-visible){outline:0}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}select:disabled{opacity:1}[list]::-webkit-calendar-picker-indicator{display:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}::-moz-focus-inner{padding:0;border-style:none}textarea{resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{float:left;width:100%;padding:0;margin-bottom:.5rem;font-size:calc(1.275rem + .3vw);line-height:inherit}@media (min-width:1200px){legend{font-size:1.5rem}}legend+*{clear:left}::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-fields-wrapper,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-text,::-webkit-datetime-edit-year-field{padding:0}::-webkit-inner-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:textfield}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-color-swatch-wrapper{padding:0}::-webkit-file-upload-button{font:inherit}::file-selector-button{font:inherit}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}iframe{border:0}summary{display:list-item;cursor:pointer}progress{vertical-align:baseline}[hidden]{display:none!important}.lead{font-size:1.25rem;font-weight:300}.display-1{font-size:calc(1.625rem + 4.5vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-1{font-size:5rem}}.display-2{font-size:calc(1.575rem + 3.9vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-2{font-size:4.5rem}}.display-3{font-size:calc(1.525rem + 3.3vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-3{font-size:4rem}}.display-4{font-size:calc(1.475rem + 2.7vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-4{font-size:3.5rem}}.display-5{font-size:calc(1.425rem + 2.1vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-5{font-size:3rem}}.display-6{font-size:calc(1.375rem + 1.5vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-6{font-size:2.5rem}}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none}.list-inline-item{display:inline-block}.list-inline-item:not(:last-child){margin-right:.5rem}.initialism{font-size:.875em;text-transform:uppercase}.blockquote{margin-bottom:1rem;font-size:1.25rem}.blockquote>:last-child{margin-bottom:0}.blockquote-footer{margin-top:-1rem;margin-bottom:1rem;font-size:.875em;color:#6c757d}.blockquote-footer::before{content:"— "}.img-fluid{max-width:100%;height:auto}.img-thumbnail{padding:.25rem;background-color:#fff;border:1px solid #dee2e6;border-radius:.25rem;max-width:100%;height:auto}.figure{display:inline-block}.figure-img{margin-bottom:.5rem;line-height:1}.figure-caption{font-size:.875em;color:#6c757d}.container,.container-fluid,.container-lg,.container-md,.container-sm,.container-xl,.container-xxl{width:100%;padding-right:var(--bs-gutter-x,.75rem);padding-left:var(--bs-gutter-x,.75rem);margin-right:auto;margin-left:auto}@media (min-width:576px){.container,.container-sm{max-width:540px}}@media (min-width:768px){.container,.container-md,.container-sm{max-width:720px}}@media (min-width:992px){.container,.container-lg,.container-md,.container-sm{max-width:960px}}@media (min-width:1200px){.container,.container-lg,.container-md,.container-sm,.container-xl{max-width:1140px}}@media (min-width:1400px){.container,.container-lg,.container-md,.container-sm,.container-xl,.container-xxl{max-width:1320px}}.row{--bs-gutter-x:1.5rem;--bs-gutter-y:0;display:flex;flex-wrap:wrap;margin-top:calc(-1 * var(--bs-gutter-y));margin-right:calc(-.5 * var(--bs-gutter-x));margin-left:calc(-.5 * var(--bs-gutter-x))}.row>*{flex-shrink:0;width:100%;max-width:100%;padding-right:calc(var(--bs-gutter-x) * .5);padding-left:calc(var(--bs-gutter-x) * .5);margin-top:var(--bs-gutter-y)}.col{flex:1 0 0%}.row-cols-auto>*{flex:0 0 auto;width:auto}.row-cols-1>*{flex:0 0 auto;width:100%}.row-cols-2>*{flex:0 0 auto;width:50%}.row-cols-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-4>*{flex:0 0 auto;width:25%}.row-cols-5>*{flex:0 0 auto;width:20%}.row-cols-6>*{flex:0 0 auto;width:16.6666666667%}.col-auto{flex:0 0 auto;width:auto}.col-1{flex:0 0 auto;width:8.33333333%}.col-2{flex:0 0 auto;width:16.66666667%}.col-3{flex:0 0 auto;width:25%}.col-4{flex:0 0 auto;width:33.33333333%}.col-5{flex:0 0 auto;width:41.66666667%}.col-6{flex:0 0 auto;width:50%}.col-7{flex:0 0 auto;width:58.33333333%}.col-8{flex:0 0 auto;width:66.66666667%}.col-9{flex:0 0 auto;width:75%}.col-10{flex:0 0 auto;width:83.33333333%}.col-11{flex:0 0 auto;width:91.66666667%}.col-12{flex:0 0 auto;width:100%}.offset-1{margin-left:8.33333333%}.offset-2{margin-left:16.66666667%}.offset-3{margin-left:25%}.offset-4{margin-left:33.33333333%}.offset-5{margin-left:41.66666667%}.offset-6{margin-left:50%}.offset-7{margin-left:58.33333333%}.offset-8{margin-left:66.66666667%}.offset-9{margin-left:75%}.offset-10{margin-left:83.33333333%}.offset-11{margin-left:91.66666667%}.g-0,.gx-0{--bs-gutter-x:0}.g-0,.gy-0{--bs-gutter-y:0}.g-1,.gx-1{--bs-gutter-x:0.25rem}.g-1,.gy-1{--bs-gutter-y:0.25rem}.g-2,.gx-2{--bs-gutter-x:0.5rem}.g-2,.gy-2{--bs-gutter-y:0.5rem}.g-3,.gx-3{--bs-gutter-x:1rem}.g-3,.gy-3{--bs-gutter-y:1rem}.g-4,.gx-4{--bs-gutter-x:1.5rem}.g-4,.gy-4{--bs-gutter-y:1.5rem}.g-5,.gx-5{--bs-gutter-x:3rem}.g-5,.gy-5{--bs-gutter-y:3rem}@media (min-width:576px){.col-sm{flex:1 0 0%}.row-cols-sm-auto>*{flex:0 0 auto;width:auto}.row-cols-sm-1>*{flex:0 0 auto;width:100%}.row-cols-sm-2>*{flex:0 0 auto;width:50%}.row-cols-sm-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-sm-4>*{flex:0 0 auto;width:25%}.row-cols-sm-5>*{flex:0 0 auto;width:20%}.row-cols-sm-6>*{flex:0 0 auto;width:16.6666666667%}.col-sm-auto{flex:0 0 auto;width:auto}.col-sm-1{flex:0 0 auto;width:8.33333333%}.col-sm-2{flex:0 0 auto;width:16.66666667%}.col-sm-3{flex:0 0 auto;width:25%}.col-sm-4{flex:0 0 auto;width:33.33333333%}.col-sm-5{flex:0 0 auto;width:41.66666667%}.col-sm-6{flex:0 0 auto;width:50%}.col-sm-7{flex:0 0 auto;width:58.33333333%}.col-sm-8{flex:0 0 auto;width:66.66666667%}.col-sm-9{flex:0 0 auto;width:75%}.col-sm-10{flex:0 0 auto;width:83.33333333%}.col-sm-11{flex:0 0 auto;width:91.66666667%}.col-sm-12{flex:0 0 auto;width:100%}.offset-sm-0{margin-left:0}.offset-sm-1{margin-left:8.33333333%}.offset-sm-2{margin-left:16.66666667%}.offset-sm-3{margin-left:25%}.offset-sm-4{margin-left:33.33333333%}.offset-sm-5{margin-left:41.66666667%}.offset-sm-6{margin-left:50%}.offset-sm-7{margin-left:58.33333333%}.offset-sm-8{margin-left:66.66666667%}.offset-sm-9{margin-left:75%}.offset-sm-10{margin-left:83.33333333%}.offset-sm-11{margin-left:91.66666667%}.g-sm-0,.gx-sm-0{--bs-gutter-x:0}.g-sm-0,.gy-sm-0{--bs-gutter-y:0}.g-sm-1,.gx-sm-1{--bs-gutter-x:0.25rem}.g-sm-1,.gy-sm-1{--bs-gutter-y:0.25rem}.g-sm-2,.gx-sm-2{--bs-gutter-x:0.5rem}.g-sm-2,.gy-sm-2{--bs-gutter-y:0.5rem}.g-sm-3,.gx-sm-3{--bs-gutter-x:1rem}.g-sm-3,.gy-sm-3{--bs-gutter-y:1rem}.g-sm-4,.gx-sm-4{--bs-gutter-x:1.5rem}.g-sm-4,.gy-sm-4{--bs-gutter-y:1.5rem}.g-sm-5,.gx-sm-5{--bs-gutter-x:3rem}.g-sm-5,.gy-sm-5{--bs-gutter-y:3rem}}@media (min-width:768px){.col-md{flex:1 0 0%}.row-cols-md-auto>*{flex:0 0 auto;width:auto}.row-cols-md-1>*{flex:0 0 auto;width:100%}.row-cols-md-2>*{flex:0 0 auto;width:50%}.row-cols-md-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-md-4>*{flex:0 0 auto;width:25%}.row-cols-md-5>*{flex:0 0 auto;width:20%}.row-cols-md-6>*{flex:0 0 auto;width:16.6666666667%}.col-md-auto{flex:0 0 auto;width:auto}.col-md-1{flex:0 0 auto;width:8.33333333%}.col-md-2{flex:0 0 auto;width:16.66666667%}.col-md-3{flex:0 0 auto;width:25%}.col-md-4{flex:0 0 auto;width:33.33333333%}.col-md-5{flex:0 0 auto;width:41.66666667%}.col-md-6{flex:0 0 auto;width:50%}.col-md-7{flex:0 0 auto;width:58.33333333%}.col-md-8{flex:0 0 auto;width:66.66666667%}.col-md-9{flex:0 0 auto;width:75%}.col-md-10{flex:0 0 auto;width:83.33333333%}.col-md-11{flex:0 0 auto;width:91.66666667%}.col-md-12{flex:0 0 auto;width:100%}.offset-md-0{margin-left:0}.offset-md-1{margin-left:8.33333333%}.offset-md-2{margin-left:16.66666667%}.offset-md-3{margin-left:25%}.offset-md-4{margin-left:33.33333333%}.offset-md-5{margin-left:41.66666667%}.offset-md-6{margin-left:50%}.offset-md-7{margin-left:58.33333333%}.offset-md-8{margin-left:66.66666667%}.offset-md-9{margin-left:75%}.offset-md-10{margin-left:83.33333333%}.offset-md-11{margin-left:91.66666667%}.g-md-0,.gx-md-0{--bs-gutter-x:0}.g-md-0,.gy-md-0{--bs-gutter-y:0}.g-md-1,.gx-md-1{--bs-gutter-x:0.25rem}.g-md-1,.gy-md-1{--bs-gutter-y:0.25rem}.g-md-2,.gx-md-2{--bs-gutter-x:0.5rem}.g-md-2,.gy-md-2{--bs-gutter-y:0.5rem}.g-md-3,.gx-md-3{--bs-gutter-x:1rem}.g-md-3,.gy-md-3{--bs-gutter-y:1rem}.g-md-4,.gx-md-4{--bs-gutter-x:1.5rem}.g-md-4,.gy-md-4{--bs-gutter-y:1.5rem}.g-md-5,.gx-md-5{--bs-gutter-x:3rem}.g-md-5,.gy-md-5{--bs-gutter-y:3rem}}@media (min-width:992px){.col-lg{flex:1 0 0%}.row-cols-lg-auto>*{flex:0 0 auto;width:auto}.row-cols-lg-1>*{flex:0 0 auto;width:100%}.row-cols-lg-2>*{flex:0 0 auto;width:50%}.row-cols-lg-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-lg-4>*{flex:0 0 auto;width:25%}.row-cols-lg-5>*{flex:0 0 auto;width:20%}.row-cols-lg-6>*{flex:0 0 auto;width:16.6666666667%}.col-lg-auto{flex:0 0 auto;width:auto}.col-lg-1{flex:0 0 auto;width:8.33333333%}.col-lg-2{flex:0 0 auto;width:16.66666667%}.col-lg-3{flex:0 0 auto;width:25%}.col-lg-4{flex:0 0 auto;width:33.33333333%}.col-lg-5{flex:0 0 auto;width:41.66666667%}.col-lg-6{flex:0 0 auto;width:50%}.col-lg-7{flex:0 0 auto;width:58.33333333%}.col-lg-8{flex:0 0 auto;width:66.66666667%}.col-lg-9{flex:0 0 auto;width:75%}.col-lg-10{flex:0 0 auto;width:83.33333333%}.col-lg-11{flex:0 0 auto;width:91.66666667%}.col-lg-12{flex:0 0 auto;width:100%}.offset-lg-0{margin-left:0}.offset-lg-1{margin-left:8.33333333%}.offset-lg-2{margin-left:16.66666667%}.offset-lg-3{margin-left:25%}.offset-lg-4{margin-left:33.33333333%}.offset-lg-5{margin-left:41.66666667%}.offset-lg-6{margin-left:50%}.offset-lg-7{margin-left:58.33333333%}.offset-lg-8{margin-left:66.66666667%}.offset-lg-9{margin-left:75%}.offset-lg-10{margin-left:83.33333333%}.offset-lg-11{margin-left:91.66666667%}.g-lg-0,.gx-lg-0{--bs-gutter-x:0}.g-lg-0,.gy-lg-0{--bs-gutter-y:0}.g-lg-1,.gx-lg-1{--bs-gutter-x:0.25rem}.g-lg-1,.gy-lg-1{--bs-gutter-y:0.25rem}.g-lg-2,.gx-lg-2{--bs-gutter-x:0.5rem}.g-lg-2,.gy-lg-2{--bs-gutter-y:0.5rem}.g-lg-3,.gx-lg-3{--bs-gutter-x:1rem}.g-lg-3,.gy-lg-3{--bs-gutter-y:1rem}.g-lg-4,.gx-lg-4{--bs-gutter-x:1.5rem}.g-lg-4,.gy-lg-4{--bs-gutter-y:1.5rem}.g-lg-5,.gx-lg-5{--bs-gutter-x:3rem}.g-lg-5,.gy-lg-5{--bs-gutter-y:3rem}}@media (min-width:1200px){.col-xl{flex:1 0 0%}.row-cols-xl-auto>*{flex:0 0 auto;width:auto}.row-cols-xl-1>*{flex:0 0 auto;width:100%}.row-cols-xl-2>*{flex:0 0 auto;width:50%}.row-cols-xl-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-xl-4>*{flex:0 0 auto;width:25%}.row-cols-xl-5>*{flex:0 0 auto;width:20%}.row-cols-xl-6>*{flex:0 0 auto;width:16.6666666667%}.col-xl-auto{flex:0 0 auto;width:auto}.col-xl-1{flex:0 0 auto;width:8.33333333%}.col-xl-2{flex:0 0 auto;width:16.66666667%}.col-xl-3{flex:0 0 auto;width:25%}.col-xl-4{flex:0 0 auto;width:33.33333333%}.col-xl-5{flex:0 0 auto;width:41.66666667%}.col-xl-6{flex:0 0 auto;width:50%}.col-xl-7{flex:0 0 auto;width:58.33333333%}.col-xl-8{flex:0 0 auto;width:66.66666667%}.col-xl-9{flex:0 0 auto;width:75%}.col-xl-10{flex:0 0 auto;width:83.33333333%}.col-xl-11{flex:0 0 auto;width:91.66666667%}.col-xl-12{flex:0 0 auto;width:100%}.offset-xl-0{margin-left:0}.offset-xl-1{margin-left:8.33333333%}.offset-xl-2{margin-left:16.66666667%}.offset-xl-3{margin-left:25%}.offset-xl-4{margin-left:33.33333333%}.offset-xl-5{margin-left:41.66666667%}.offset-xl-6{margin-left:50%}.offset-xl-7{margin-left:58.33333333%}.offset-xl-8{margin-left:66.66666667%}.offset-xl-9{margin-left:75%}.offset-xl-10{margin-left:83.33333333%}.offset-xl-11{margin-left:91.66666667%}.g-xl-0,.gx-xl-0{--bs-gutter-x:0}.g-xl-0,.gy-xl-0{--bs-gutter-y:0}.g-xl-1,.gx-xl-1{--bs-gutter-x:0.25rem}.g-xl-1,.gy-xl-1{--bs-gutter-y:0.25rem}.g-xl-2,.gx-xl-2{--bs-gutter-x:0.5rem}.g-xl-2,.gy-xl-2{--bs-gutter-y:0.5rem}.g-xl-3,.gx-xl-3{--bs-gutter-x:1rem}.g-xl-3,.gy-xl-3{--bs-gutter-y:1rem}.g-xl-4,.gx-xl-4{--bs-gutter-x:1.5rem}.g-xl-4,.gy-xl-4{--bs-gutter-y:1.5rem}.g-xl-5,.gx-xl-5{--bs-gutter-x:3rem}.g-xl-5,.gy-xl-5{--bs-gutter-y:3rem}}@media (min-width:1400px){.col-xxl{flex:1 0 0%}.row-cols-xxl-auto>*{flex:0 0 auto;width:auto}.row-cols-xxl-1>*{flex:0 0 auto;width:100%}.row-cols-xxl-2>*{flex:0 0 auto;width:50%}.row-cols-xxl-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-xxl-4>*{flex:0 0 auto;width:25%}.row-cols-xxl-5>*{flex:0 0 auto;width:20%}.row-cols-xxl-6>*{flex:0 0 auto;width:16.6666666667%}.col-xxl-auto{flex:0 0 auto;width:auto}.col-xxl-1{flex:0 0 auto;width:8.33333333%}.col-xxl-2{flex:0 0 auto;width:16.66666667%}.col-xxl-3{flex:0 0 auto;width:25%}.col-xxl-4{flex:0 0 auto;width:33.33333333%}.col-xxl-5{flex:0 0 auto;width:41.66666667%}.col-xxl-6{flex:0 0 auto;width:50%}.col-xxl-7{flex:0 0 auto;width:58.33333333%}.col-xxl-8{flex:0 0 auto;width:66.66666667%}.col-xxl-9{flex:0 0 auto;width:75%}.col-xxl-10{flex:0 0 auto;width:83.33333333%}.col-xxl-11{flex:0 0 auto;width:91.66666667%}.col-xxl-12{flex:0 0 auto;width:100%}.offset-xxl-0{margin-left:0}.offset-xxl-1{margin-left:8.33333333%}.offset-xxl-2{margin-left:16.66666667%}.offset-xxl-3{margin-left:25%}.offset-xxl-4{margin-left:33.33333333%}.offset-xxl-5{margin-left:41.66666667%}.offset-xxl-6{margin-left:50%}.offset-xxl-7{margin-left:58.33333333%}.offset-xxl-8{margin-left:66.66666667%}.offset-xxl-9{margin-left:75%}.offset-xxl-10{margin-left:83.33333333%}.offset-xxl-11{margin-left:91.66666667%}.g-xxl-0,.gx-xxl-0{--bs-gutter-x:0}.g-xxl-0,.gy-xxl-0{--bs-gutter-y:0}.g-xxl-1,.gx-xxl-1{--bs-gutter-x:0.25rem}.g-xxl-1,.gy-xxl-1{--bs-gutter-y:0.25rem}.g-xxl-2,.gx-xxl-2{--bs-gutter-x:0.5rem}.g-xxl-2,.gy-xxl-2{--bs-gutter-y:0.5rem}.g-xxl-3,.gx-xxl-3{--bs-gutter-x:1rem}.g-xxl-3,.gy-xxl-3{--bs-gutter-y:1rem}.g-xxl-4,.gx-xxl-4{--bs-gutter-x:1.5rem}.g-xxl-4,.gy-xxl-4{--bs-gutter-y:1.5rem}.g-xxl-5,.gx-xxl-5{--bs-gutter-x:3rem}.g-xxl-5,.gy-xxl-5{--bs-gutter-y:3rem}}.table{--bs-table-bg:transparent;--bs-table-accent-bg:transparent;--bs-table-striped-color:#212529;--bs-table-striped-bg:rgba(0, 0, 0, 0.05);--bs-table-active-color:#212529;--bs-table-active-bg:rgba(0, 0, 0, 0.1);--bs-table-hover-color:#212529;--bs-table-hover-bg:rgba(0, 0, 0, 0.075);width:100%;margin-bottom:1rem;color:#212529;vertical-align:top;border-color:#dee2e6}.table>:not(caption)>*>*{padding:.5rem .5rem;background-color:var(--bs-table-bg);border-bottom-width:1px;box-shadow:inset 0 0 0 9999px var(--bs-table-accent-bg)}.table>tbody{vertical-align:inherit}.table>thead{vertical-align:bottom}.table>:not(:first-child){border-top:2px solid currentColor}.caption-top{caption-side:top}.table-sm>:not(caption)>*>*{padding:.25rem .25rem}.table-bordered>:not(caption)>*{border-width:1px 0}.table-bordered>:not(caption)>*>*{border-width:0 1px}.table-borderless>:not(caption)>*>*{border-bottom-width:0}.table-borderless>:not(:first-child){border-top-width:0}.table-striped>tbody>tr:nth-of-type(odd)>*{--bs-table-accent-bg:var(--bs-table-striped-bg);color:var(--bs-table-striped-color)}.table-active{--bs-table-accent-bg:var(--bs-table-active-bg);color:var(--bs-table-active-color)}.table-hover>tbody>tr:hover>*{--bs-table-accent-bg:var(--bs-table-hover-bg);color:var(--bs-table-hover-color)}.table-primary{--bs-table-bg:#cfe2ff;--bs-table-striped-bg:#c5d7f2;--bs-table-striped-color:#000;--bs-table-active-bg:#bacbe6;--bs-table-active-color:#000;--bs-table-hover-bg:#bfd1ec;--bs-table-hover-color:#000;color:#000;border-color:#bacbe6}.table-secondary{--bs-table-bg:#e2e3e5;--bs-table-striped-bg:#d7d8da;--bs-table-striped-color:#000;--bs-table-active-bg:#cbccce;--bs-table-active-color:#000;--bs-table-hover-bg:#d1d2d4;--bs-table-hover-color:#000;color:#000;border-color:#cbccce}.table-success{--bs-table-bg:#d1e7dd;--bs-table-striped-bg:#c7dbd2;--bs-table-striped-color:#000;--bs-table-active-bg:#bcd0c7;--bs-table-active-color:#000;--bs-table-hover-bg:#c1d6cc;--bs-table-hover-color:#000;color:#000;border-color:#bcd0c7}.table-info{--bs-table-bg:#cff4fc;--bs-table-striped-bg:#c5e8ef;--bs-table-striped-color:#000;--bs-table-active-bg:#badce3;--bs-table-active-color:#000;--bs-table-hover-bg:#bfe2e9;--bs-table-hover-color:#000;color:#000;border-color:#badce3}.table-warning{--bs-table-bg:#fff3cd;--bs-table-striped-bg:#f2e7c3;--bs-table-striped-color:#000;--bs-table-active-bg:#e6dbb9;--bs-table-active-color:#000;--bs-table-hover-bg:#ece1be;--bs-table-hover-color:#000;color:#000;border-color:#e6dbb9}.table-danger{--bs-table-bg:#f8d7da;--bs-table-striped-bg:#eccccf;--bs-table-striped-color:#000;--bs-table-active-bg:#dfc2c4;--bs-table-active-color:#000;--bs-table-hover-bg:#e5c7ca;--bs-table-hover-color:#000;color:#000;border-color:#dfc2c4}.table-light{--bs-table-bg:#f8f9fa;--bs-table-striped-bg:#ecedee;--bs-table-striped-color:#000;--bs-table-active-bg:#dfe0e1;--bs-table-active-color:#000;--bs-table-hover-bg:#e5e6e7;--bs-table-hover-color:#000;color:#000;border-color:#dfe0e1}.table-dark{--bs-table-bg:#212529;--bs-table-striped-bg:#2c3034;--bs-table-striped-color:#fff;--bs-table-active-bg:#373b3e;--bs-table-active-color:#fff;--bs-table-hover-bg:#323539;--bs-table-hover-color:#fff;color:#fff;border-color:#373b3e}.table-responsive{overflow-x:auto;-webkit-overflow-scrolling:touch}@media (max-width:575.98px){.table-responsive-sm{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:767.98px){.table-responsive-md{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:991.98px){.table-responsive-lg{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:1199.98px){.table-responsive-xl{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:1399.98px){.table-responsive-xxl{overflow-x:auto;-webkit-overflow-scrolling:touch}}.form-label{margin-bottom:.5rem}.col-form-label{padding-top:calc(.375rem + 1px);padding-bottom:calc(.375rem + 1px);margin-bottom:0;font-size:inherit;line-height:1.5}.col-form-label-lg{padding-top:calc(.5rem + 1px);padding-bottom:calc(.5rem + 1px);font-size:1.25rem}.col-form-label-sm{padding-top:calc(.25rem + 1px);padding-bottom:calc(.25rem + 1px);font-size:.875rem}.form-text{margin-top:.25rem;font-size:.875em;color:#6c757d}.form-control{display:block;width:100%;padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#212529;background-color:#fff;background-clip:padding-box;border:1px solid #ced4da;-webkit-appearance:none;-moz-appearance:none;appearance:none;border-radius:.25rem;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control{transition:none}}.form-control[type=file]{overflow:hidden}.form-control[type=file]:not(:disabled):not([readonly]){cursor:pointer}.form-control:focus{color:#212529;background-color:#fff;border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.form-control::-webkit-date-and-time-value{height:1.5em}.form-control::-moz-placeholder{color:#6c757d;opacity:1}.form-control::placeholder{color:#6c757d;opacity:1}.form-control:disabled,.form-control[readonly]{background-color:#e9ecef;opacity:1}.form-control::-webkit-file-upload-button{padding:.375rem .75rem;margin:-.375rem -.75rem;-webkit-margin-end:.75rem;margin-inline-end:.75rem;color:#212529;background-color:#e9ecef;pointer-events:none;border-color:inherit;border-style:solid;border-width:0;border-inline-end-width:1px;border-radius:0;-webkit-transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}.form-control::file-selector-button{padding:.375rem .75rem;margin:-.375rem -.75rem;-webkit-margin-end:.75rem;margin-inline-end:.75rem;color:#212529;background-color:#e9ecef;pointer-events:none;border-color:inherit;border-style:solid;border-width:0;border-inline-end-width:1px;border-radius:0;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control::-webkit-file-upload-button{-webkit-transition:none;transition:none}.form-control::file-selector-button{transition:none}}.form-control:hover:not(:disabled):not([readonly])::-webkit-file-upload-button{background-color:#dde0e3}.form-control:hover:not(:disabled):not([readonly])::file-selector-button{background-color:#dde0e3}.form-control::-webkit-file-upload-button{padding:.375rem .75rem;margin:-.375rem -.75rem;-webkit-margin-end:.75rem;margin-inline-end:.75rem;color:#212529;background-color:#e9ecef;pointer-events:none;border-color:inherit;border-style:solid;border-width:0;border-inline-end-width:1px;border-radius:0;-webkit-transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control::-webkit-file-upload-button{-webkit-transition:none;transition:none}}.form-control:hover:not(:disabled):not([readonly])::-webkit-file-upload-button{background-color:#dde0e3}.form-control-plaintext{display:block;width:100%;padding:.375rem 0;margin-bottom:0;line-height:1.5;color:#212529;background-color:transparent;border:solid transparent;border-width:1px 0}.form-control-plaintext.form-control-lg,.form-control-plaintext.form-control-sm{padding-right:0;padding-left:0}.form-control-sm{min-height:calc(1.5em + .5rem + 2px);padding:.25rem .5rem;font-size:.875rem;border-radius:.2rem}.form-control-sm::-webkit-file-upload-button{padding:.25rem .5rem;margin:-.25rem -.5rem;-webkit-margin-end:.5rem;margin-inline-end:.5rem}.form-control-sm::file-selector-button{padding:.25rem .5rem;margin:-.25rem -.5rem;-webkit-margin-end:.5rem;margin-inline-end:.5rem}.form-control-sm::-webkit-file-upload-button{padding:.25rem .5rem;margin:-.25rem -.5rem;-webkit-margin-end:.5rem;margin-inline-end:.5rem}.form-control-lg{min-height:calc(1.5em + 1rem + 2px);padding:.5rem 1rem;font-size:1.25rem;border-radius:.3rem}.form-control-lg::-webkit-file-upload-button{padding:.5rem 1rem;margin:-.5rem -1rem;-webkit-margin-end:1rem;margin-inline-end:1rem}.form-control-lg::file-selector-button{padding:.5rem 1rem;margin:-.5rem -1rem;-webkit-margin-end:1rem;margin-inline-end:1rem}.form-control-lg::-webkit-file-upload-button{padding:.5rem 1rem;margin:-.5rem -1rem;-webkit-margin-end:1rem;margin-inline-end:1rem}textarea.form-control{min-height:calc(1.5em + .75rem + 2px)}textarea.form-control-sm{min-height:calc(1.5em + .5rem + 2px)}textarea.form-control-lg{min-height:calc(1.5em + 1rem + 2px)}.form-control-color{width:3rem;height:auto;padding:.375rem}.form-control-color:not(:disabled):not([readonly]){cursor:pointer}.form-control-color::-moz-color-swatch{height:1.5em;border-radius:.25rem}.form-control-color::-webkit-color-swatch{height:1.5em;border-radius:.25rem}.form-select{display:block;width:100%;padding:.375rem 2.25rem .375rem .75rem;-moz-padding-start:calc(0.75rem - 3px);font-size:1rem;font-weight:400;line-height:1.5;color:#212529;background-color:#fff;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right .75rem center;background-size:16px 12px;border:1px solid #ced4da;border-radius:.25rem;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out;-webkit-appearance:none;-moz-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.form-select{transition:none}}.form-select:focus{border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.form-select[multiple],.form-select[size]:not([size="1"]){padding-right:.75rem;background-image:none}.form-select:disabled{background-color:#e9ecef}.form-select:-moz-focusring{color:transparent;text-shadow:0 0 0 #212529}.form-select-sm{padding-top:.25rem;padding-bottom:.25rem;padding-left:.5rem;font-size:.875rem;border-radius:.2rem}.form-select-lg{padding-top:.5rem;padding-bottom:.5rem;padding-left:1rem;font-size:1.25rem;border-radius:.3rem}.form-check{display:block;min-height:1.5rem;padding-left:1.5em;margin-bottom:.125rem}.form-check .form-check-input{float:left;margin-left:-1.5em}.form-check-input{width:1em;height:1em;margin-top:.25em;vertical-align:top;background-color:#fff;background-repeat:no-repeat;background-position:center;background-size:contain;border:1px solid rgba(0,0,0,.25);-webkit-appearance:none;-moz-appearance:none;appearance:none;-webkit-print-color-adjust:exact;color-adjust:exact}.form-check-input[type=checkbox]{border-radius:.25em}.form-check-input[type=radio]{border-radius:50%}.form-check-input:active{filter:brightness(90%)}.form-check-input:focus{border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.form-check-input:checked{background-color:#0d6efd;border-color:#0d6efd}.form-check-input:checked[type=checkbox]{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10l3 3l6-6'/%3e%3c/svg%3e")}.form-check-input:checked[type=radio]{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='2' fill='%23fff'/%3e%3c/svg%3e")}.form-check-input[type=checkbox]:indeterminate{background-color:#0d6efd;border-color:#0d6efd;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10h8'/%3e%3c/svg%3e")}.form-check-input:disabled{pointer-events:none;filter:none;opacity:.5}.form-check-input:disabled~.form-check-label,.form-check-input[disabled]~.form-check-label{opacity:.5}.form-switch{padding-left:2.5em}.form-switch .form-check-input{width:2em;margin-left:-2.5em;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%280, 0, 0, 0.25%29'/%3e%3c/svg%3e");background-position:left center;border-radius:2em;transition:background-position .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-switch .form-check-input{transition:none}}.form-switch .form-check-input:focus{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%2386b7fe'/%3e%3c/svg%3e")}.form-switch .form-check-input:checked{background-position:right center;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e")}.form-check-inline{display:inline-block;margin-right:1rem}.btn-check{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.btn-check:disabled+.btn,.btn-check[disabled]+.btn{pointer-events:none;filter:none;opacity:.65}.form-range{width:100%;height:1.5rem;padding:0;background-color:transparent;-webkit-appearance:none;-moz-appearance:none;appearance:none}.form-range:focus{outline:0}.form-range:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .25rem rgba(13,110,253,.25)}.form-range:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .25rem rgba(13,110,253,.25)}.form-range::-moz-focus-outer{border:0}.form-range::-webkit-slider-thumb{width:1rem;height:1rem;margin-top:-.25rem;background-color:#0d6efd;border:0;border-radius:1rem;-webkit-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;-webkit-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.form-range::-webkit-slider-thumb{-webkit-transition:none;transition:none}}.form-range::-webkit-slider-thumb:active{background-color:#b6d4fe}.form-range::-webkit-slider-runnable-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.form-range::-moz-range-thumb{width:1rem;height:1rem;background-color:#0d6efd;border:0;border-radius:1rem;-moz-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;-moz-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.form-range::-moz-range-thumb{-moz-transition:none;transition:none}}.form-range::-moz-range-thumb:active{background-color:#b6d4fe}.form-range::-moz-range-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.form-range:disabled{pointer-events:none}.form-range:disabled::-webkit-slider-thumb{background-color:#adb5bd}.form-range:disabled::-moz-range-thumb{background-color:#adb5bd}.form-floating{position:relative}.form-floating>.form-control,.form-floating>.form-select{height:calc(3.5rem + 2px);line-height:1.25}.form-floating>label{position:absolute;top:0;left:0;height:100%;padding:1rem .75rem;pointer-events:none;border:1px solid transparent;transform-origin:0 0;transition:opacity .1s ease-in-out,transform .1s ease-in-out}@media (prefers-reduced-motion:reduce){.form-floating>label{transition:none}}.form-floating>.form-control{padding:1rem .75rem}.form-floating>.form-control::-moz-placeholder{color:transparent}.form-floating>.form-control::placeholder{color:transparent}.form-floating>.form-control:not(:-moz-placeholder-shown){padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control:focus,.form-floating>.form-control:not(:placeholder-shown){padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control:-webkit-autofill{padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-select{padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control:not(:-moz-placeholder-shown)~label{opacity:.65;transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.form-floating>.form-control:focus~label,.form-floating>.form-control:not(:placeholder-shown)~label,.form-floating>.form-select~label{opacity:.65;transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.form-floating>.form-control:-webkit-autofill~label{opacity:.65;transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.input-group{position:relative;display:flex;flex-wrap:wrap;align-items:stretch;width:100%}.input-group>.form-control,.input-group>.form-select{position:relative;flex:1 1 auto;width:1%;min-width:0}.input-group>.form-control:focus,.input-group>.form-select:focus{z-index:3}.input-group .btn{position:relative;z-index:2}.input-group .btn:focus{z-index:3}.input-group-text{display:flex;align-items:center;padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:center;white-space:nowrap;background-color:#e9ecef;border:1px solid #ced4da;border-radius:.25rem}.input-group-lg>.btn,.input-group-lg>.form-control,.input-group-lg>.form-select,.input-group-lg>.input-group-text{padding:.5rem 1rem;font-size:1.25rem;border-radius:.3rem}.input-group-sm>.btn,.input-group-sm>.form-control,.input-group-sm>.form-select,.input-group-sm>.input-group-text{padding:.25rem .5rem;font-size:.875rem;border-radius:.2rem}.input-group-lg>.form-select,.input-group-sm>.form-select{padding-right:3rem}.input-group:not(.has-validation)>.dropdown-toggle:nth-last-child(n+3),.input-group:not(.has-validation)>:not(:last-child):not(.dropdown-toggle):not(.dropdown-menu){border-top-right-radius:0;border-bottom-right-radius:0}.input-group.has-validation>.dropdown-toggle:nth-last-child(n+4),.input-group.has-validation>:nth-last-child(n+3):not(.dropdown-toggle):not(.dropdown-menu){border-top-right-radius:0;border-bottom-right-radius:0}.input-group>:not(:first-child):not(.dropdown-menu):not(.valid-tooltip):not(.valid-feedback):not(.invalid-tooltip):not(.invalid-feedback){margin-left:-1px;border-top-left-radius:0;border-bottom-left-radius:0}.valid-feedback{display:none;width:100%;margin-top:.25rem;font-size:.875em;color:#198754}.valid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;color:#fff;background-color:rgba(25,135,84,.9);border-radius:.25rem}.is-valid~.valid-feedback,.is-valid~.valid-tooltip,.was-validated :valid~.valid-feedback,.was-validated :valid~.valid-tooltip{display:block}.form-control.is-valid,.was-validated .form-control:valid{border-color:#198754;padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-valid:focus,.was-validated .form-control:valid:focus{border-color:#198754;box-shadow:0 0 0 .25rem rgba(25,135,84,.25)}.was-validated textarea.form-control:valid,textarea.form-control.is-valid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.form-select.is-valid,.was-validated .form-select:valid{border-color:#198754}.form-select.is-valid:not([multiple]):not([size]),.form-select.is-valid:not([multiple])[size="1"],.was-validated .form-select:valid:not([multiple]):not([size]),.was-validated .form-select:valid:not([multiple])[size="1"]{padding-right:4.125rem;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e"),url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");background-position:right .75rem center,center right 2.25rem;background-size:16px 12px,calc(.75em + .375rem) calc(.75em + .375rem)}.form-select.is-valid:focus,.was-validated .form-select:valid:focus{border-color:#198754;box-shadow:0 0 0 .25rem rgba(25,135,84,.25)}.form-check-input.is-valid,.was-validated .form-check-input:valid{border-color:#198754}.form-check-input.is-valid:checked,.was-validated .form-check-input:valid:checked{background-color:#198754}.form-check-input.is-valid:focus,.was-validated .form-check-input:valid:focus{box-shadow:0 0 0 .25rem rgba(25,135,84,.25)}.form-check-input.is-valid~.form-check-label,.was-validated .form-check-input:valid~.form-check-label{color:#198754}.form-check-inline .form-check-input~.valid-feedback{margin-left:.5em}.input-group .form-control.is-valid,.input-group .form-select.is-valid,.was-validated .input-group .form-control:valid,.was-validated .input-group .form-select:valid{z-index:1}.input-group .form-control.is-valid:focus,.input-group .form-select.is-valid:focus,.was-validated .input-group .form-control:valid:focus,.was-validated .input-group .form-select:valid:focus{z-index:3}.invalid-feedback{display:none;width:100%;margin-top:.25rem;font-size:.875em;color:#dc3545}.invalid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;color:#fff;background-color:rgba(220,53,69,.9);border-radius:.25rem}.is-invalid~.invalid-feedback,.is-invalid~.invalid-tooltip,.was-validated :invalid~.invalid-feedback,.was-validated :invalid~.invalid-tooltip{display:block}.form-control.is-invalid,.was-validated .form-control:invalid{border-color:#dc3545;padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-invalid:focus,.was-validated .form-control:invalid:focus{border-color:#dc3545;box-shadow:0 0 0 .25rem rgba(220,53,69,.25)}.was-validated textarea.form-control:invalid,textarea.form-control.is-invalid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.form-select.is-invalid,.was-validated .form-select:invalid{border-color:#dc3545}.form-select.is-invalid:not([multiple]):not([size]),.form-select.is-invalid:not([multiple])[size="1"],.was-validated .form-select:invalid:not([multiple]):not([size]),.was-validated .form-select:invalid:not([multiple])[size="1"]{padding-right:4.125rem;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e"),url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e");background-position:right .75rem center,center right 2.25rem;background-size:16px 12px,calc(.75em + .375rem) calc(.75em + .375rem)}.form-select.is-invalid:focus,.was-validated .form-select:invalid:focus{border-color:#dc3545;box-shadow:0 0 0 .25rem rgba(220,53,69,.25)}.form-check-input.is-invalid,.was-validated .form-check-input:invalid{border-color:#dc3545}.form-check-input.is-invalid:checked,.was-validated .form-check-input:invalid:checked{background-color:#dc3545}.form-check-input.is-invalid:focus,.was-validated .form-check-input:invalid:focus{box-shadow:0 0 0 .25rem rgba(220,53,69,.25)}.form-check-input.is-invalid~.form-check-label,.was-validated .form-check-input:invalid~.form-check-label{color:#dc3545}.form-check-inline .form-check-input~.invalid-feedback{margin-left:.5em}.input-group .form-control.is-invalid,.input-group .form-select.is-invalid,.was-validated .input-group .form-control:invalid,.was-validated .input-group .form-select:invalid{z-index:2}.input-group .form-control.is-invalid:focus,.input-group .form-select.is-invalid:focus,.was-validated .input-group .form-control:invalid:focus,.was-validated .input-group .form-select:invalid:focus{z-index:3}.btn{display:inline-block;font-weight:400;line-height:1.5;color:#212529;text-align:center;text-decoration:none;vertical-align:middle;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;user-select:none;background-color:transparent;border:1px solid transparent;padding:.375rem .75rem;font-size:1rem;border-radius:.25rem;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.btn{transition:none}}.btn:hover{color:#212529}.btn-check:focus+.btn,.btn:focus{outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.btn.disabled,.btn:disabled,fieldset:disabled .btn{pointer-events:none;opacity:.65}.btn-primary{color:#fff;background-color:#0d6efd;border-color:#0d6efd}.btn-primary:hover{color:#fff;background-color:#0b5ed7;border-color:#0a58ca}.btn-check:focus+.btn-primary,.btn-primary:focus{color:#fff;background-color:#0b5ed7;border-color:#0a58ca;box-shadow:0 0 0 .25rem rgba(49,132,253,.5)}.btn-check:active+.btn-primary,.btn-check:checked+.btn-primary,.btn-primary.active,.btn-primary:active,.show>.btn-primary.dropdown-toggle{color:#fff;background-color:#0a58ca;border-color:#0a53be}.btn-check:active+.btn-primary:focus,.btn-check:checked+.btn-primary:focus,.btn-primary.active:focus,.btn-primary:active:focus,.show>.btn-primary.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(49,132,253,.5)}.btn-primary.disabled,.btn-primary:disabled{color:#fff;background-color:#0d6efd;border-color:#0d6efd}.btn-secondary{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-secondary:hover{color:#fff;background-color:#5c636a;border-color:#565e64}.btn-check:focus+.btn-secondary,.btn-secondary:focus{color:#fff;background-color:#5c636a;border-color:#565e64;box-shadow:0 0 0 .25rem rgba(130,138,145,.5)}.btn-check:active+.btn-secondary,.btn-check:checked+.btn-secondary,.btn-secondary.active,.btn-secondary:active,.show>.btn-secondary.dropdown-toggle{color:#fff;background-color:#565e64;border-color:#51585e}.btn-check:active+.btn-secondary:focus,.btn-check:checked+.btn-secondary:focus,.btn-secondary.active:focus,.btn-secondary:active:focus,.show>.btn-secondary.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(130,138,145,.5)}.btn-secondary.disabled,.btn-secondary:disabled{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-success{color:#fff;background-color:#198754;border-color:#198754}.btn-success:hover{color:#fff;background-color:#157347;border-color:#146c43}.btn-check:focus+.btn-success,.btn-success:focus{color:#fff;background-color:#157347;border-color:#146c43;box-shadow:0 0 0 .25rem rgba(60,153,110,.5)}.btn-check:active+.btn-success,.btn-check:checked+.btn-success,.btn-success.active,.btn-success:active,.show>.btn-success.dropdown-toggle{color:#fff;background-color:#146c43;border-color:#13653f}.btn-check:active+.btn-success:focus,.btn-check:checked+.btn-success:focus,.btn-success.active:focus,.btn-success:active:focus,.show>.btn-success.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(60,153,110,.5)}.btn-success.disabled,.btn-success:disabled{color:#fff;background-color:#198754;border-color:#198754}.btn-info{color:#000;background-color:#0dcaf0;border-color:#0dcaf0}.btn-info:hover{color:#000;background-color:#31d2f2;border-color:#25cff2}.btn-check:focus+.btn-info,.btn-info:focus{color:#000;background-color:#31d2f2;border-color:#25cff2;box-shadow:0 0 0 .25rem rgba(11,172,204,.5)}.btn-check:active+.btn-info,.btn-check:checked+.btn-info,.btn-info.active,.btn-info:active,.show>.btn-info.dropdown-toggle{color:#000;background-color:#3dd5f3;border-color:#25cff2}.btn-check:active+.btn-info:focus,.btn-check:checked+.btn-info:focus,.btn-info.active:focus,.btn-info:active:focus,.show>.btn-info.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(11,172,204,.5)}.btn-info.disabled,.btn-info:disabled{color:#000;background-color:#0dcaf0;border-color:#0dcaf0}.btn-warning{color:#000;background-color:#ffc107;border-color:#ffc107}.btn-warning:hover{color:#000;background-color:#ffca2c;border-color:#ffc720}.btn-check:focus+.btn-warning,.btn-warning:focus{color:#000;background-color:#ffca2c;border-color:#ffc720;box-shadow:0 0 0 .25rem rgba(217,164,6,.5)}.btn-check:active+.btn-warning,.btn-check:checked+.btn-warning,.btn-warning.active,.btn-warning:active,.show>.btn-warning.dropdown-toggle{color:#000;background-color:#ffcd39;border-color:#ffc720}.btn-check:active+.btn-warning:focus,.btn-check:checked+.btn-warning:focus,.btn-warning.active:focus,.btn-warning:active:focus,.show>.btn-warning.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(217,164,6,.5)}.btn-warning.disabled,.btn-warning:disabled{color:#000;background-color:#ffc107;border-color:#ffc107}.btn-danger{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-danger:hover{color:#fff;background-color:#bb2d3b;border-color:#b02a37}.btn-check:focus+.btn-danger,.btn-danger:focus{color:#fff;background-color:#bb2d3b;border-color:#b02a37;box-shadow:0 0 0 .25rem rgba(225,83,97,.5)}.btn-check:active+.btn-danger,.btn-check:checked+.btn-danger,.btn-danger.active,.btn-danger:active,.show>.btn-danger.dropdown-toggle{color:#fff;background-color:#b02a37;border-color:#a52834}.btn-check:active+.btn-danger:focus,.btn-check:checked+.btn-danger:focus,.btn-danger.active:focus,.btn-danger:active:focus,.show>.btn-danger.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(225,83,97,.5)}.btn-danger.disabled,.btn-danger:disabled{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-light{color:#000;background-color:#f8f9fa;border-color:#f8f9fa}.btn-light:hover{color:#000;background-color:#f9fafb;border-color:#f9fafb}.btn-check:focus+.btn-light,.btn-light:focus{color:#000;background-color:#f9fafb;border-color:#f9fafb;box-shadow:0 0 0 .25rem rgba(211,212,213,.5)}.btn-check:active+.btn-light,.btn-check:checked+.btn-light,.btn-light.active,.btn-light:active,.show>.btn-light.dropdown-toggle{color:#000;background-color:#f9fafb;border-color:#f9fafb}.btn-check:active+.btn-light:focus,.btn-check:checked+.btn-light:focus,.btn-light.active:focus,.btn-light:active:focus,.show>.btn-light.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(211,212,213,.5)}.btn-light.disabled,.btn-light:disabled{color:#000;background-color:#f8f9fa;border-color:#f8f9fa}.btn-dark{color:#fff;background-color:#212529;border-color:#212529}.btn-dark:hover{color:#fff;background-color:#1c1f23;border-color:#1a1e21}.btn-check:focus+.btn-dark,.btn-dark:focus{color:#fff;background-color:#1c1f23;border-color:#1a1e21;box-shadow:0 0 0 .25rem rgba(66,70,73,.5)}.btn-check:active+.btn-dark,.btn-check:checked+.btn-dark,.btn-dark.active,.btn-dark:active,.show>.btn-dark.dropdown-toggle{color:#fff;background-color:#1a1e21;border-color:#191c1f}.btn-check:active+.btn-dark:focus,.btn-check:checked+.btn-dark:focus,.btn-dark.active:focus,.btn-dark:active:focus,.show>.btn-dark.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(66,70,73,.5)}.btn-dark.disabled,.btn-dark:disabled{color:#fff;background-color:#212529;border-color:#212529}.btn-outline-primary{color:#0d6efd;border-color:#0d6efd}.btn-outline-primary:hover{color:#fff;background-color:#0d6efd;border-color:#0d6efd}.btn-check:focus+.btn-outline-primary,.btn-outline-primary:focus{box-shadow:0 0 0 .25rem rgba(13,110,253,.5)}.btn-check:active+.btn-outline-primary,.btn-check:checked+.btn-outline-primary,.btn-outline-primary.active,.btn-outline-primary.dropdown-toggle.show,.btn-outline-primary:active{color:#fff;background-color:#0d6efd;border-color:#0d6efd}.btn-check:active+.btn-outline-primary:focus,.btn-check:checked+.btn-outline-primary:focus,.btn-outline-primary.active:focus,.btn-outline-primary.dropdown-toggle.show:focus,.btn-outline-primary:active:focus{box-shadow:0 0 0 .25rem rgba(13,110,253,.5)}.btn-outline-primary.disabled,.btn-outline-primary:disabled{color:#0d6efd;background-color:transparent}.btn-outline-secondary{color:#6c757d;border-color:#6c757d}.btn-outline-secondary:hover{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-check:focus+.btn-outline-secondary,.btn-outline-secondary:focus{box-shadow:0 0 0 .25rem rgba(108,117,125,.5)}.btn-check:active+.btn-outline-secondary,.btn-check:checked+.btn-outline-secondary,.btn-outline-secondary.active,.btn-outline-secondary.dropdown-toggle.show,.btn-outline-secondary:active{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-check:active+.btn-outline-secondary:focus,.btn-check:checked+.btn-outline-secondary:focus,.btn-outline-secondary.active:focus,.btn-outline-secondary.dropdown-toggle.show:focus,.btn-outline-secondary:active:focus{box-shadow:0 0 0 .25rem rgba(108,117,125,.5)}.btn-outline-secondary.disabled,.btn-outline-secondary:disabled{color:#6c757d;background-color:transparent}.btn-outline-success{color:#198754;border-color:#198754}.btn-outline-success:hover{color:#fff;background-color:#198754;border-color:#198754}.btn-check:focus+.btn-outline-success,.btn-outline-success:focus{box-shadow:0 0 0 .25rem rgba(25,135,84,.5)}.btn-check:active+.btn-outline-success,.btn-check:checked+.btn-outline-success,.btn-outline-success.active,.btn-outline-success.dropdown-toggle.show,.btn-outline-success:active{color:#fff;background-color:#198754;border-color:#198754}.btn-check:active+.btn-outline-success:focus,.btn-check:checked+.btn-outline-success:focus,.btn-outline-success.active:focus,.btn-outline-success.dropdown-toggle.show:focus,.btn-outline-success:active:focus{box-shadow:0 0 0 .25rem rgba(25,135,84,.5)}.btn-outline-success.disabled,.btn-outline-success:disabled{color:#198754;background-color:transparent}.btn-outline-info{color:#0dcaf0;border-color:#0dcaf0}.btn-outline-info:hover{color:#000;background-color:#0dcaf0;border-color:#0dcaf0}.btn-check:focus+.btn-outline-info,.btn-outline-info:focus{box-shadow:0 0 0 .25rem rgba(13,202,240,.5)}.btn-check:active+.btn-outline-info,.btn-check:checked+.btn-outline-info,.btn-outline-info.active,.btn-outline-info.dropdown-toggle.show,.btn-outline-info:active{color:#000;background-color:#0dcaf0;border-color:#0dcaf0}.btn-check:active+.btn-outline-info:focus,.btn-check:checked+.btn-outline-info:focus,.btn-outline-info.active:focus,.btn-outline-info.dropdown-toggle.show:focus,.btn-outline-info:active:focus{box-shadow:0 0 0 .25rem rgba(13,202,240,.5)}.btn-outline-info.disabled,.btn-outline-info:disabled{color:#0dcaf0;background-color:transparent}.btn-outline-warning{color:#ffc107;border-color:#ffc107}.btn-outline-warning:hover{color:#000;background-color:#ffc107;border-color:#ffc107}.btn-check:focus+.btn-outline-warning,.btn-outline-warning:focus{box-shadow:0 0 0 .25rem rgba(255,193,7,.5)}.btn-check:active+.btn-outline-warning,.btn-check:checked+.btn-outline-warning,.btn-outline-warning.active,.btn-outline-warning.dropdown-toggle.show,.btn-outline-warning:active{color:#000;background-color:#ffc107;border-color:#ffc107}.btn-check:active+.btn-outline-warning:focus,.btn-check:checked+.btn-outline-warning:focus,.btn-outline-warning.active:focus,.btn-outline-warning.dropdown-toggle.show:focus,.btn-outline-warning:active:focus{box-shadow:0 0 0 .25rem rgba(255,193,7,.5)}.btn-outline-warning.disabled,.btn-outline-warning:disabled{color:#ffc107;background-color:transparent}.btn-outline-danger{color:#dc3545;border-color:#dc3545}.btn-outline-danger:hover{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-check:focus+.btn-outline-danger,.btn-outline-danger:focus{box-shadow:0 0 0 .25rem rgba(220,53,69,.5)}.btn-check:active+.btn-outline-danger,.btn-check:checked+.btn-outline-danger,.btn-outline-danger.active,.btn-outline-danger.dropdown-toggle.show,.btn-outline-danger:active{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-check:active+.btn-outline-danger:focus,.btn-check:checked+.btn-outline-danger:focus,.btn-outline-danger.active:focus,.btn-outline-danger.dropdown-toggle.show:focus,.btn-outline-danger:active:focus{box-shadow:0 0 0 .25rem rgba(220,53,69,.5)}.btn-outline-danger.disabled,.btn-outline-danger:disabled{color:#dc3545;background-color:transparent}.btn-outline-light{color:#f8f9fa;border-color:#f8f9fa}.btn-outline-light:hover{color:#000;background-color:#f8f9fa;border-color:#f8f9fa}.btn-check:focus+.btn-outline-light,.btn-outline-light:focus{box-shadow:0 0 0 .25rem rgba(248,249,250,.5)}.btn-check:active+.btn-outline-light,.btn-check:checked+.btn-outline-light,.btn-outline-light.active,.btn-outline-light.dropdown-toggle.show,.btn-outline-light:active{color:#000;background-color:#f8f9fa;border-color:#f8f9fa}.btn-check:active+.btn-outline-light:focus,.btn-check:checked+.btn-outline-light:focus,.btn-outline-light.active:focus,.btn-outline-light.dropdown-toggle.show:focus,.btn-outline-light:active:focus{box-shadow:0 0 0 .25rem rgba(248,249,250,.5)}.btn-outline-light.disabled,.btn-outline-light:disabled{color:#f8f9fa;background-color:transparent}.btn-outline-dark{color:#212529;border-color:#212529}.btn-outline-dark:hover{color:#fff;background-color:#212529;border-color:#212529}.btn-check:focus+.btn-outline-dark,.btn-outline-dark:focus{box-shadow:0 0 0 .25rem rgba(33,37,41,.5)}.btn-check:active+.btn-outline-dark,.btn-check:checked+.btn-outline-dark,.btn-outline-dark.active,.btn-outline-dark.dropdown-toggle.show,.btn-outline-dark:active{color:#fff;background-color:#212529;border-color:#212529}.btn-check:active+.btn-outline-dark:focus,.btn-check:checked+.btn-outline-dark:focus,.btn-outline-dark.active:focus,.btn-outline-dark.dropdown-toggle.show:focus,.btn-outline-dark:active:focus{box-shadow:0 0 0 .25rem rgba(33,37,41,.5)}.btn-outline-dark.disabled,.btn-outline-dark:disabled{color:#212529;background-color:transparent}.btn-link{font-weight:400;color:#0d6efd;text-decoration:underline}.btn-link:hover{color:#0a58ca}.btn-link.disabled,.btn-link:disabled{color:#6c757d}.btn-group-lg>.btn,.btn-lg{padding:.5rem 1rem;font-size:1.25rem;border-radius:.3rem}.btn-group-sm>.btn,.btn-sm{padding:.25rem .5rem;font-size:.875rem;border-radius:.2rem}.fade{transition:opacity .15s linear}@media (prefers-reduced-motion:reduce){.fade{transition:none}}.fade:not(.show){opacity:0}.collapse:not(.show){display:none}.collapsing{height:0;overflow:hidden;transition:height .35s ease}@media (prefers-reduced-motion:reduce){.collapsing{transition:none}}.collapsing.collapse-horizontal{width:0;height:auto;transition:width .35s ease}@media (prefers-reduced-motion:reduce){.collapsing.collapse-horizontal{transition:none}}.dropdown,.dropend,.dropstart,.dropup{position:relative}.dropdown-toggle{white-space:nowrap}.dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid;border-right:.3em solid transparent;border-bottom:0;border-left:.3em solid transparent}.dropdown-toggle:empty::after{margin-left:0}.dropdown-menu{position:absolute;z-index:1000;display:none;min-width:10rem;padding:.5rem 0;margin:0;font-size:1rem;color:#212529;text-align:left;list-style:none;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.15);border-radius:.25rem}.dropdown-menu[data-bs-popper]{top:100%;left:0;margin-top:.125rem}.dropdown-menu-start{--bs-position:start}.dropdown-menu-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-end{--bs-position:end}.dropdown-menu-end[data-bs-popper]{right:0;left:auto}@media (min-width:576px){.dropdown-menu-sm-start{--bs-position:start}.dropdown-menu-sm-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-sm-end{--bs-position:end}.dropdown-menu-sm-end[data-bs-popper]{right:0;left:auto}}@media (min-width:768px){.dropdown-menu-md-start{--bs-position:start}.dropdown-menu-md-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-md-end{--bs-position:end}.dropdown-menu-md-end[data-bs-popper]{right:0;left:auto}}@media (min-width:992px){.dropdown-menu-lg-start{--bs-position:start}.dropdown-menu-lg-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-lg-end{--bs-position:end}.dropdown-menu-lg-end[data-bs-popper]{right:0;left:auto}}@media (min-width:1200px){.dropdown-menu-xl-start{--bs-position:start}.dropdown-menu-xl-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-xl-end{--bs-position:end}.dropdown-menu-xl-end[data-bs-popper]{right:0;left:auto}}@media (min-width:1400px){.dropdown-menu-xxl-start{--bs-position:start}.dropdown-menu-xxl-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-xxl-end{--bs-position:end}.dropdown-menu-xxl-end[data-bs-popper]{right:0;left:auto}}.dropup .dropdown-menu[data-bs-popper]{top:auto;bottom:100%;margin-top:0;margin-bottom:.125rem}.dropup .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:0;border-right:.3em solid transparent;border-bottom:.3em solid;border-left:.3em solid transparent}.dropup .dropdown-toggle:empty::after{margin-left:0}.dropend .dropdown-menu[data-bs-popper]{top:0;right:auto;left:100%;margin-top:0;margin-left:.125rem}.dropend .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:0;border-bottom:.3em solid transparent;border-left:.3em solid}.dropend .dropdown-toggle:empty::after{margin-left:0}.dropend .dropdown-toggle::after{vertical-align:0}.dropstart .dropdown-menu[data-bs-popper]{top:0;right:100%;left:auto;margin-top:0;margin-right:.125rem}.dropstart .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:""}.dropstart .dropdown-toggle::after{display:none}.dropstart .dropdown-toggle::before{display:inline-block;margin-right:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:.3em solid;border-bottom:.3em solid transparent}.dropstart .dropdown-toggle:empty::after{margin-left:0}.dropstart .dropdown-toggle::before{vertical-align:0}.dropdown-divider{height:0;margin:.5rem 0;overflow:hidden;border-top:1px solid rgba(0,0,0,.15)}.dropdown-item{display:block;width:100%;padding:.25rem 1rem;clear:both;font-weight:400;color:#212529;text-align:inherit;text-decoration:none;white-space:nowrap;background-color:transparent;border:0}.dropdown-item:focus,.dropdown-item:hover{color:#1e2125;background-color:#e9ecef}.dropdown-item.active,.dropdown-item:active{color:#fff;text-decoration:none;background-color:#0d6efd}.dropdown-item.disabled,.dropdown-item:disabled{color:#adb5bd;pointer-events:none;background-color:transparent}.dropdown-menu.show{display:block}.dropdown-header{display:block;padding:.5rem 1rem;margin-bottom:0;font-size:.875rem;color:#6c757d;white-space:nowrap}.dropdown-item-text{display:block;padding:.25rem 1rem;color:#212529}.dropdown-menu-dark{color:#dee2e6;background-color:#343a40;border-color:rgba(0,0,0,.15)}.dropdown-menu-dark .dropdown-item{color:#dee2e6}.dropdown-menu-dark .dropdown-item:focus,.dropdown-menu-dark .dropdown-item:hover{color:#fff;background-color:rgba(255,255,255,.15)}.dropdown-menu-dark .dropdown-item.active,.dropdown-menu-dark .dropdown-item:active{color:#fff;background-color:#0d6efd}.dropdown-menu-dark .dropdown-item.disabled,.dropdown-menu-dark .dropdown-item:disabled{color:#adb5bd}.dropdown-menu-dark .dropdown-divider{border-color:rgba(0,0,0,.15)}.dropdown-menu-dark .dropdown-item-text{color:#dee2e6}.dropdown-menu-dark .dropdown-header{color:#adb5bd}.btn-group,.btn-group-vertical{position:relative;display:inline-flex;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;flex:1 1 auto}.btn-group-vertical>.btn-check:checked+.btn,.btn-group-vertical>.btn-check:focus+.btn,.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:hover,.btn-group>.btn-check:checked+.btn,.btn-group>.btn-check:focus+.btn,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus,.btn-group>.btn:hover{z-index:1}.btn-toolbar{display:flex;flex-wrap:wrap;justify-content:flex-start}.btn-toolbar .input-group{width:auto}.btn-group>.btn-group:not(:first-child),.btn-group>.btn:not(:first-child){margin-left:-1px}.btn-group>.btn-group:not(:last-child)>.btn,.btn-group>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:not(:first-child)>.btn,.btn-group>.btn:nth-child(n+3),.btn-group>:not(.btn-check)+.btn{border-top-left-radius:0;border-bottom-left-radius:0}.dropdown-toggle-split{padding-right:.5625rem;padding-left:.5625rem}.dropdown-toggle-split::after,.dropend .dropdown-toggle-split::after,.dropup .dropdown-toggle-split::after{margin-left:0}.dropstart .dropdown-toggle-split::before{margin-right:0}.btn-group-sm>.btn+.dropdown-toggle-split,.btn-sm+.dropdown-toggle-split{padding-right:.375rem;padding-left:.375rem}.btn-group-lg>.btn+.dropdown-toggle-split,.btn-lg+.dropdown-toggle-split{padding-right:.75rem;padding-left:.75rem}.btn-group-vertical{flex-direction:column;align-items:flex-start;justify-content:center}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group{width:100%}.btn-group-vertical>.btn-group:not(:first-child),.btn-group-vertical>.btn:not(:first-child){margin-top:-1px}.btn-group-vertical>.btn-group:not(:last-child)>.btn,.btn-group-vertical>.btn:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:not(:first-child)>.btn,.btn-group-vertical>.btn~.btn{border-top-left-radius:0;border-top-right-radius:0}.nav{display:flex;flex-wrap:wrap;padding-left:0;margin-bottom:0;list-style:none}.nav-link{display:block;padding:.5rem 1rem;color:#0d6efd;text-decoration:none;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out}@media (prefers-reduced-motion:reduce){.nav-link{transition:none}}.nav-link:focus,.nav-link:hover{color:#0a58ca}.nav-link.disabled{color:#6c757d;pointer-events:none;cursor:default}.nav-tabs{border-bottom:1px solid #dee2e6}.nav-tabs .nav-link{margin-bottom:-1px;background:0 0;border:1px solid transparent;border-top-left-radius:.25rem;border-top-right-radius:.25rem}.nav-tabs .nav-link:focus,.nav-tabs .nav-link:hover{border-color:#e9ecef #e9ecef #dee2e6;isolation:isolate}.nav-tabs .nav-link.disabled{color:#6c757d;background-color:transparent;border-color:transparent}.nav-tabs .nav-item.show .nav-link,.nav-tabs .nav-link.active{color:#495057;background-color:#fff;border-color:#dee2e6 #dee2e6 #fff}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-left-radius:0;border-top-right-radius:0}.nav-pills .nav-link{background:0 0;border:0;border-radius:.25rem}.nav-pills .nav-link.active,.nav-pills .show>.nav-link{color:#fff;background-color:#0d6efd}.nav-fill .nav-item,.nav-fill>.nav-link{flex:1 1 auto;text-align:center}.nav-justified .nav-item,.nav-justified>.nav-link{flex-basis:0;flex-grow:1;text-align:center}.nav-fill .nav-item .nav-link,.nav-justified .nav-item .nav-link{width:100%}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.navbar{position:relative;display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;padding-top:.5rem;padding-bottom:.5rem}.navbar>.container,.navbar>.container-fluid,.navbar>.container-lg,.navbar>.container-md,.navbar>.container-sm,.navbar>.container-xl,.navbar>.container-xxl{display:flex;flex-wrap:inherit;align-items:center;justify-content:space-between}.navbar-brand{padding-top:.3125rem;padding-bottom:.3125rem;margin-right:1rem;font-size:1.25rem;text-decoration:none;white-space:nowrap}.navbar-nav{display:flex;flex-direction:column;padding-left:0;margin-bottom:0;list-style:none}.navbar-nav .nav-link{padding-right:0;padding-left:0}.navbar-nav .dropdown-menu{position:static}.navbar-text{padding-top:.5rem;padding-bottom:.5rem}.navbar-collapse{flex-basis:100%;flex-grow:1;align-items:center}.navbar-toggler{padding:.25rem .75rem;font-size:1.25rem;line-height:1;background-color:transparent;border:1px solid transparent;border-radius:.25rem;transition:box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.navbar-toggler{transition:none}}.navbar-toggler:hover{text-decoration:none}.navbar-toggler:focus{text-decoration:none;outline:0;box-shadow:0 0 0 .25rem}.navbar-toggler-icon{display:inline-block;width:1.5em;height:1.5em;vertical-align:middle;background-repeat:no-repeat;background-position:center;background-size:100%}.navbar-nav-scroll{max-height:var(--bs-scroll-height,75vh);overflow-y:auto}@media (min-width:576px){.navbar-expand-sm{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-sm .navbar-nav{flex-direction:row}.navbar-expand-sm .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-sm .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-sm .navbar-nav-scroll{overflow:visible}.navbar-expand-sm .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-sm .navbar-toggler{display:none}.navbar-expand-sm .offcanvas-header{display:none}.navbar-expand-sm .offcanvas{position:inherit;bottom:0;z-index:1000;flex-grow:1;visibility:visible!important;background-color:transparent;border-right:0;border-left:0;transition:none;transform:none}.navbar-expand-sm .offcanvas-bottom,.navbar-expand-sm .offcanvas-top{height:auto;border-top:0;border-bottom:0}.navbar-expand-sm .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:768px){.navbar-expand-md{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-md .navbar-nav{flex-direction:row}.navbar-expand-md .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-md .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-md .navbar-nav-scroll{overflow:visible}.navbar-expand-md .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-md .navbar-toggler{display:none}.navbar-expand-md .offcanvas-header{display:none}.navbar-expand-md .offcanvas{position:inherit;bottom:0;z-index:1000;flex-grow:1;visibility:visible!important;background-color:transparent;border-right:0;border-left:0;transition:none;transform:none}.navbar-expand-md .offcanvas-bottom,.navbar-expand-md .offcanvas-top{height:auto;border-top:0;border-bottom:0}.navbar-expand-md .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:992px){.navbar-expand-lg{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-lg .navbar-nav{flex-direction:row}.navbar-expand-lg .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-lg .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-lg .navbar-nav-scroll{overflow:visible}.navbar-expand-lg .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-lg .navbar-toggler{display:none}.navbar-expand-lg .offcanvas-header{display:none}.navbar-expand-lg .offcanvas{position:inherit;bottom:0;z-index:1000;flex-grow:1;visibility:visible!important;background-color:transparent;border-right:0;border-left:0;transition:none;transform:none}.navbar-expand-lg .offcanvas-bottom,.navbar-expand-lg .offcanvas-top{height:auto;border-top:0;border-bottom:0}.navbar-expand-lg .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:1200px){.navbar-expand-xl{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-xl .navbar-nav{flex-direction:row}.navbar-expand-xl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xl .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-xl .navbar-nav-scroll{overflow:visible}.navbar-expand-xl .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-xl .navbar-toggler{display:none}.navbar-expand-xl .offcanvas-header{display:none}.navbar-expand-xl .offcanvas{position:inherit;bottom:0;z-index:1000;flex-grow:1;visibility:visible!important;background-color:transparent;border-right:0;border-left:0;transition:none;transform:none}.navbar-expand-xl .offcanvas-bottom,.navbar-expand-xl .offcanvas-top{height:auto;border-top:0;border-bottom:0}.navbar-expand-xl .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:1400px){.navbar-expand-xxl{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-xxl .navbar-nav{flex-direction:row}.navbar-expand-xxl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xxl .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-xxl .navbar-nav-scroll{overflow:visible}.navbar-expand-xxl .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-xxl .navbar-toggler{display:none}.navbar-expand-xxl .offcanvas-header{display:none}.navbar-expand-xxl .offcanvas{position:inherit;bottom:0;z-index:1000;flex-grow:1;visibility:visible!important;background-color:transparent;border-right:0;border-left:0;transition:none;transform:none}.navbar-expand-xxl .offcanvas-bottom,.navbar-expand-xxl .offcanvas-top{height:auto;border-top:0;border-bottom:0}.navbar-expand-xxl .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}.navbar-expand{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand .navbar-nav{flex-direction:row}.navbar-expand .navbar-nav .dropdown-menu{position:absolute}.navbar-expand .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand .navbar-nav-scroll{overflow:visible}.navbar-expand .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand .navbar-toggler{display:none}.navbar-expand .offcanvas-header{display:none}.navbar-expand .offcanvas{position:inherit;bottom:0;z-index:1000;flex-grow:1;visibility:visible!important;background-color:transparent;border-right:0;border-left:0;transition:none;transform:none}.navbar-expand .offcanvas-bottom,.navbar-expand .offcanvas-top{height:auto;border-top:0;border-bottom:0}.navbar-expand .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}.navbar-light .navbar-brand{color:rgba(0,0,0,.9)}.navbar-light .navbar-brand:focus,.navbar-light .navbar-brand:hover{color:rgba(0,0,0,.9)}.navbar-light .navbar-nav .nav-link{color:rgba(0,0,0,.55)}.navbar-light .navbar-nav .nav-link:focus,.navbar-light .navbar-nav .nav-link:hover{color:rgba(0,0,0,.7)}.navbar-light .navbar-nav .nav-link.disabled{color:rgba(0,0,0,.3)}.navbar-light .navbar-nav .nav-link.active,.navbar-light .navbar-nav .show>.nav-link{color:rgba(0,0,0,.9)}.navbar-light .navbar-toggler{color:rgba(0,0,0,.55);border-color:rgba(0,0,0,.1)}.navbar-light .navbar-toggler-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%280, 0, 0, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.navbar-light .navbar-text{color:rgba(0,0,0,.55)}.navbar-light .navbar-text a,.navbar-light .navbar-text a:focus,.navbar-light .navbar-text a:hover{color:rgba(0,0,0,.9)}.navbar-dark .navbar-brand{color:#fff}.navbar-dark .navbar-brand:focus,.navbar-dark .navbar-brand:hover{color:#fff}.navbar-dark .navbar-nav .nav-link{color:rgba(255,255,255,.55)}.navbar-dark .navbar-nav .nav-link:focus,.navbar-dark .navbar-nav .nav-link:hover{color:rgba(255,255,255,.75)}.navbar-dark .navbar-nav .nav-link.disabled{color:rgba(255,255,255,.25)}.navbar-dark .navbar-nav .nav-link.active,.navbar-dark .navbar-nav .show>.nav-link{color:#fff}.navbar-dark .navbar-toggler{color:rgba(255,255,255,.55);border-color:rgba(255,255,255,.1)}.navbar-dark .navbar-toggler-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.navbar-dark .navbar-text{color:rgba(255,255,255,.55)}.navbar-dark .navbar-text a,.navbar-dark .navbar-text a:focus,.navbar-dark .navbar-text a:hover{color:#fff}.card{position:relative;display:flex;flex-direction:column;min-width:0;word-wrap:break-word;background-color:#fff;background-clip:border-box;border:1px solid rgba(0,0,0,.125);border-radius:.25rem}.card>hr{margin-right:0;margin-left:0}.card>.list-group{border-top:inherit;border-bottom:inherit}.card>.list-group:first-child{border-top-width:0;border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.card>.list-group:last-child{border-bottom-width:0;border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.card>.card-header+.list-group,.card>.list-group+.card-footer{border-top:0}.card-body{flex:1 1 auto;padding:1rem 1rem}.card-title{margin-bottom:.5rem}.card-subtitle{margin-top:-.25rem;margin-bottom:0}.card-text:last-child{margin-bottom:0}.card-link+.card-link{margin-left:1rem}.card-header{padding:.5rem 1rem;margin-bottom:0;background-color:rgba(0,0,0,.03);border-bottom:1px solid rgba(0,0,0,.125)}.card-header:first-child{border-radius:calc(.25rem - 1px) calc(.25rem - 1px) 0 0}.card-footer{padding:.5rem 1rem;background-color:rgba(0,0,0,.03);border-top:1px solid rgba(0,0,0,.125)}.card-footer:last-child{border-radius:0 0 calc(.25rem - 1px) calc(.25rem - 1px)}.card-header-tabs{margin-right:-.5rem;margin-bottom:-.5rem;margin-left:-.5rem;border-bottom:0}.card-header-pills{margin-right:-.5rem;margin-left:-.5rem}.card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:1rem;border-radius:calc(.25rem - 1px)}.card-img,.card-img-bottom,.card-img-top{width:100%}.card-img,.card-img-top{border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.card-img,.card-img-bottom{border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.card-group>.card{margin-bottom:.75rem}@media (min-width:576px){.card-group{display:flex;flex-flow:row wrap}.card-group>.card{flex:1 0 0%;margin-bottom:0}.card-group>.card+.card{margin-left:0;border-left:0}.card-group>.card:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.card-group>.card:not(:last-child) .card-header,.card-group>.card:not(:last-child) .card-img-top{border-top-right-radius:0}.card-group>.card:not(:last-child) .card-footer,.card-group>.card:not(:last-child) .card-img-bottom{border-bottom-right-radius:0}.card-group>.card:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.card-group>.card:not(:first-child) .card-header,.card-group>.card:not(:first-child) .card-img-top{border-top-left-radius:0}.card-group>.card:not(:first-child) .card-footer,.card-group>.card:not(:first-child) .card-img-bottom{border-bottom-left-radius:0}}.accordion-button{position:relative;display:flex;align-items:center;width:100%;padding:1rem 1.25rem;font-size:1rem;color:#212529;text-align:left;background-color:#fff;border:0;border-radius:0;overflow-anchor:none;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out,border-radius .15s ease}@media (prefers-reduced-motion:reduce){.accordion-button{transition:none}}.accordion-button:not(.collapsed){color:#0c63e4;background-color:#e7f1ff;box-shadow:inset 0 -1px 0 rgba(0,0,0,.125)}.accordion-button:not(.collapsed)::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%230c63e4'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");transform:rotate(-180deg)}.accordion-button::after{flex-shrink:0;width:1.25rem;height:1.25rem;margin-left:auto;content:"";background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23212529'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");background-repeat:no-repeat;background-size:1.25rem;transition:transform .2s ease-in-out}@media (prefers-reduced-motion:reduce){.accordion-button::after{transition:none}}.accordion-button:hover{z-index:2}.accordion-button:focus{z-index:3;border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.accordion-header{margin-bottom:0}.accordion-item{background-color:#fff;border:1px solid rgba(0,0,0,.125)}.accordion-item:first-of-type{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.accordion-item:first-of-type .accordion-button{border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.accordion-item:not(:first-of-type){border-top:0}.accordion-item:last-of-type{border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.accordion-item:last-of-type .accordion-button.collapsed{border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.accordion-item:last-of-type .accordion-collapse{border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.accordion-body{padding:1rem 1.25rem}.accordion-flush .accordion-collapse{border-width:0}.accordion-flush .accordion-item{border-right:0;border-left:0;border-radius:0}.accordion-flush .accordion-item:first-child{border-top:0}.accordion-flush .accordion-item:last-child{border-bottom:0}.accordion-flush .accordion-item .accordion-button{border-radius:0}.breadcrumb{display:flex;flex-wrap:wrap;padding:0 0;margin-bottom:1rem;list-style:none}.breadcrumb-item+.breadcrumb-item{padding-left:.5rem}.breadcrumb-item+.breadcrumb-item::before{float:left;padding-right:.5rem;color:#6c757d;content:var(--bs-breadcrumb-divider, "/")}.breadcrumb-item.active{color:#6c757d}.pagination{display:flex;padding-left:0;list-style:none}.page-link{position:relative;display:block;color:#0d6efd;text-decoration:none;background-color:#fff;border:1px solid #dee2e6;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.page-link{transition:none}}.page-link:hover{z-index:2;color:#0a58ca;background-color:#e9ecef;border-color:#dee2e6}.page-link:focus{z-index:3;color:#0a58ca;background-color:#e9ecef;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.page-item:not(:first-child) .page-link{margin-left:-1px}.page-item.active .page-link{z-index:3;color:#fff;background-color:#0d6efd;border-color:#0d6efd}.page-item.disabled .page-link{color:#6c757d;pointer-events:none;background-color:#fff;border-color:#dee2e6}.page-link{padding:.375rem .75rem}.page-item:first-child .page-link{border-top-left-radius:.25rem;border-bottom-left-radius:.25rem}.page-item:last-child .page-link{border-top-right-radius:.25rem;border-bottom-right-radius:.25rem}.pagination-lg .page-link{padding:.75rem 1.5rem;font-size:1.25rem}.pagination-lg .page-item:first-child .page-link{border-top-left-radius:.3rem;border-bottom-left-radius:.3rem}.pagination-lg .page-item:last-child .page-link{border-top-right-radius:.3rem;border-bottom-right-radius:.3rem}.pagination-sm .page-link{padding:.25rem .5rem;font-size:.875rem}.pagination-sm .page-item:first-child .page-link{border-top-left-radius:.2rem;border-bottom-left-radius:.2rem}.pagination-sm .page-item:last-child .page-link{border-top-right-radius:.2rem;border-bottom-right-radius:.2rem}.badge{display:inline-block;padding:.35em .65em;font-size:.75em;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25rem}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.alert{position:relative;padding:1rem 1rem;margin-bottom:1rem;border:1px solid transparent;border-radius:.25rem}.alert-heading{color:inherit}.alert-link{font-weight:700}.alert-dismissible{padding-right:3rem}.alert-dismissible .btn-close{position:absolute;top:0;right:0;z-index:2;padding:1.25rem 1rem}.alert-primary{color:#084298;background-color:#cfe2ff;border-color:#b6d4fe}.alert-primary .alert-link{color:#06357a}.alert-secondary{color:#41464b;background-color:#e2e3e5;border-color:#d3d6d8}.alert-secondary .alert-link{color:#34383c}.alert-success{color:#0f5132;background-color:#d1e7dd;border-color:#badbcc}.alert-success .alert-link{color:#0c4128}.alert-info{color:#055160;background-color:#cff4fc;border-color:#b6effb}.alert-info .alert-link{color:#04414d}.alert-warning{color:#664d03;background-color:#fff3cd;border-color:#ffecb5}.alert-warning .alert-link{color:#523e02}.alert-danger{color:#842029;background-color:#f8d7da;border-color:#f5c2c7}.alert-danger .alert-link{color:#6a1a21}.alert-light{color:#636464;background-color:#fefefe;border-color:#fdfdfe}.alert-light .alert-link{color:#4f5050}.alert-dark{color:#141619;background-color:#d3d3d4;border-color:#bcbebf}.alert-dark .alert-link{color:#101214}@-webkit-keyframes progress-bar-stripes{0%{background-position-x:1rem}}@keyframes progress-bar-stripes{0%{background-position-x:1rem}}.progress{display:flex;height:1rem;overflow:hidden;font-size:.75rem;background-color:#e9ecef;border-radius:.25rem}.progress-bar{display:flex;flex-direction:column;justify-content:center;overflow:hidden;color:#fff;text-align:center;white-space:nowrap;background-color:#0d6efd;transition:width .6s ease}@media (prefers-reduced-motion:reduce){.progress-bar{transition:none}}.progress-bar-striped{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-size:1rem 1rem}.progress-bar-animated{-webkit-animation:1s linear infinite progress-bar-stripes;animation:1s linear infinite progress-bar-stripes}@media (prefers-reduced-motion:reduce){.progress-bar-animated{-webkit-animation:none;animation:none}}.list-group{display:flex;flex-direction:column;padding-left:0;margin-bottom:0;border-radius:.25rem}.list-group-numbered{list-style-type:none;counter-reset:section}.list-group-numbered>li::before{content:counters(section, ".") ". ";counter-increment:section}.list-group-item-action{width:100%;color:#495057;text-align:inherit}.list-group-item-action:focus,.list-group-item-action:hover{z-index:1;color:#495057;text-decoration:none;background-color:#f8f9fa}.list-group-item-action:active{color:#212529;background-color:#e9ecef}.list-group-item{position:relative;display:block;padding:.5rem 1rem;color:#212529;text-decoration:none;background-color:#fff;border:1px solid rgba(0,0,0,.125)}.list-group-item:first-child{border-top-left-radius:inherit;border-top-right-radius:inherit}.list-group-item:last-child{border-bottom-right-radius:inherit;border-bottom-left-radius:inherit}.list-group-item.disabled,.list-group-item:disabled{color:#6c757d;pointer-events:none;background-color:#fff}.list-group-item.active{z-index:2;color:#fff;background-color:#0d6efd;border-color:#0d6efd}.list-group-item+.list-group-item{border-top-width:0}.list-group-item+.list-group-item.active{margin-top:-1px;border-top-width:1px}.list-group-horizontal{flex-direction:row}.list-group-horizontal>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal>.list-group-item.active{margin-top:0}.list-group-horizontal>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}@media (min-width:576px){.list-group-horizontal-sm{flex-direction:row}.list-group-horizontal-sm>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-sm>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-sm>.list-group-item.active{margin-top:0}.list-group-horizontal-sm>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-sm>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:768px){.list-group-horizontal-md{flex-direction:row}.list-group-horizontal-md>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-md>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-md>.list-group-item.active{margin-top:0}.list-group-horizontal-md>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-md>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:992px){.list-group-horizontal-lg{flex-direction:row}.list-group-horizontal-lg>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-lg>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-lg>.list-group-item.active{margin-top:0}.list-group-horizontal-lg>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-lg>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:1200px){.list-group-horizontal-xl{flex-direction:row}.list-group-horizontal-xl>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-xl>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-xl>.list-group-item.active{margin-top:0}.list-group-horizontal-xl>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-xl>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:1400px){.list-group-horizontal-xxl{flex-direction:row}.list-group-horizontal-xxl>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-xxl>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-xxl>.list-group-item.active{margin-top:0}.list-group-horizontal-xxl>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-xxl>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}.list-group-flush{border-radius:0}.list-group-flush>.list-group-item{border-width:0 0 1px}.list-group-flush>.list-group-item:last-child{border-bottom-width:0}.list-group-item-primary{color:#084298;background-color:#cfe2ff}.list-group-item-primary.list-group-item-action:focus,.list-group-item-primary.list-group-item-action:hover{color:#084298;background-color:#bacbe6}.list-group-item-primary.list-group-item-action.active{color:#fff;background-color:#084298;border-color:#084298}.list-group-item-secondary{color:#41464b;background-color:#e2e3e5}.list-group-item-secondary.list-group-item-action:focus,.list-group-item-secondary.list-group-item-action:hover{color:#41464b;background-color:#cbccce}.list-group-item-secondary.list-group-item-action.active{color:#fff;background-color:#41464b;border-color:#41464b}.list-group-item-success{color:#0f5132;background-color:#d1e7dd}.list-group-item-success.list-group-item-action:focus,.list-group-item-success.list-group-item-action:hover{color:#0f5132;background-color:#bcd0c7}.list-group-item-success.list-group-item-action.active{color:#fff;background-color:#0f5132;border-color:#0f5132}.list-group-item-info{color:#055160;background-color:#cff4fc}.list-group-item-info.list-group-item-action:focus,.list-group-item-info.list-group-item-action:hover{color:#055160;background-color:#badce3}.list-group-item-info.list-group-item-action.active{color:#fff;background-color:#055160;border-color:#055160}.list-group-item-warning{color:#664d03;background-color:#fff3cd}.list-group-item-warning.list-group-item-action:focus,.list-group-item-warning.list-group-item-action:hover{color:#664d03;background-color:#e6dbb9}.list-group-item-warning.list-group-item-action.active{color:#fff;background-color:#664d03;border-color:#664d03}.list-group-item-danger{color:#842029;background-color:#f8d7da}.list-group-item-danger.list-group-item-action:focus,.list-group-item-danger.list-group-item-action:hover{color:#842029;background-color:#dfc2c4}.list-group-item-danger.list-group-item-action.active{color:#fff;background-color:#842029;border-color:#842029}.list-group-item-light{color:#636464;background-color:#fefefe}.list-group-item-light.list-group-item-action:focus,.list-group-item-light.list-group-item-action:hover{color:#636464;background-color:#e5e5e5}.list-group-item-light.list-group-item-action.active{color:#fff;background-color:#636464;border-color:#636464}.list-group-item-dark{color:#141619;background-color:#d3d3d4}.list-group-item-dark.list-group-item-action:focus,.list-group-item-dark.list-group-item-action:hover{color:#141619;background-color:#bebebf}.list-group-item-dark.list-group-item-action.active{color:#fff;background-color:#141619;border-color:#141619}.btn-close{box-sizing:content-box;width:1em;height:1em;padding:.25em .25em;color:#000;background:transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'%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") center/1em auto no-repeat;border:0;border-radius:.25rem;opacity:.5}.btn-close:hover{color:#000;text-decoration:none;opacity:.75}.btn-close:focus{outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25);opacity:1}.btn-close.disabled,.btn-close:disabled{pointer-events:none;-webkit-user-select:none;-moz-user-select:none;user-select:none;opacity:.25}.btn-close-white{filter:invert(1) grayscale(100%) brightness(200%)}.toast{width:350px;max-width:100%;font-size:.875rem;pointer-events:auto;background-color:rgba(255,255,255,.85);background-clip:padding-box;border:1px solid rgba(0,0,0,.1);box-shadow:0 .5rem 1rem rgba(0,0,0,.15);border-radius:.25rem}.toast.showing{opacity:0}.toast:not(.show){display:none}.toast-container{width:-webkit-max-content;width:-moz-max-content;width:max-content;max-width:100%;pointer-events:none}.toast-container>:not(:last-child){margin-bottom:.75rem}.toast-header{display:flex;align-items:center;padding:.5rem .75rem;color:#6c757d;background-color:rgba(255,255,255,.85);background-clip:padding-box;border-bottom:1px solid rgba(0,0,0,.05);border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.toast-header .btn-close{margin-right:-.375rem;margin-left:.75rem}.toast-body{padding:.75rem;word-wrap:break-word}.modal{position:fixed;top:0;left:0;z-index:1055;display:none;width:100%;height:100%;overflow-x:hidden;overflow-y:auto;outline:0}.modal-dialog{position:relative;width:auto;margin:.5rem;pointer-events:none}.modal.fade .modal-dialog{transition:transform .3s ease-out;transform:translate(0,-50px)}@media (prefers-reduced-motion:reduce){.modal.fade .modal-dialog{transition:none}}.modal.show .modal-dialog{transform:none}.modal.modal-static .modal-dialog{transform:scale(1.02)}.modal-dialog-scrollable{height:calc(100% - 1rem)}.modal-dialog-scrollable .modal-content{max-height:100%;overflow:hidden}.modal-dialog-scrollable .modal-body{overflow-y:auto}.modal-dialog-centered{display:flex;align-items:center;min-height:calc(100% - 1rem)}.modal-content{position:relative;display:flex;flex-direction:column;width:100%;pointer-events:auto;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem;outline:0}.modal-backdrop{position:fixed;top:0;left:0;z-index:1050;width:100vw;height:100vh;background-color:#000}.modal-backdrop.fade{opacity:0}.modal-backdrop.show{opacity:.5}.modal-header{display:flex;flex-shrink:0;align-items:center;justify-content:space-between;padding:1rem 1rem;border-bottom:1px solid #dee2e6;border-top-left-radius:calc(.3rem - 1px);border-top-right-radius:calc(.3rem - 1px)}.modal-header .btn-close{padding:.5rem .5rem;margin:-.5rem -.5rem -.5rem auto}.modal-title{margin-bottom:0;line-height:1.5}.modal-body{position:relative;flex:1 1 auto;padding:1rem}.modal-footer{display:flex;flex-wrap:wrap;flex-shrink:0;align-items:center;justify-content:flex-end;padding:.75rem;border-top:1px solid #dee2e6;border-bottom-right-radius:calc(.3rem - 1px);border-bottom-left-radius:calc(.3rem - 1px)}.modal-footer>*{margin:.25rem}@media (min-width:576px){.modal-dialog{max-width:500px;margin:1.75rem auto}.modal-dialog-scrollable{height:calc(100% - 3.5rem)}.modal-dialog-centered{min-height:calc(100% - 3.5rem)}.modal-sm{max-width:300px}}@media (min-width:992px){.modal-lg,.modal-xl{max-width:800px}}@media (min-width:1200px){.modal-xl{max-width:1140px}}.modal-fullscreen{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen .modal-header{border-radius:0}.modal-fullscreen .modal-body{overflow-y:auto}.modal-fullscreen .modal-footer{border-radius:0}@media (max-width:575.98px){.modal-fullscreen-sm-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-sm-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-sm-down .modal-header{border-radius:0}.modal-fullscreen-sm-down .modal-body{overflow-y:auto}.modal-fullscreen-sm-down .modal-footer{border-radius:0}}@media (max-width:767.98px){.modal-fullscreen-md-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-md-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-md-down .modal-header{border-radius:0}.modal-fullscreen-md-down .modal-body{overflow-y:auto}.modal-fullscreen-md-down .modal-footer{border-radius:0}}@media (max-width:991.98px){.modal-fullscreen-lg-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-lg-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-lg-down .modal-header{border-radius:0}.modal-fullscreen-lg-down .modal-body{overflow-y:auto}.modal-fullscreen-lg-down .modal-footer{border-radius:0}}@media (max-width:1199.98px){.modal-fullscreen-xl-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-xl-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-xl-down .modal-header{border-radius:0}.modal-fullscreen-xl-down .modal-body{overflow-y:auto}.modal-fullscreen-xl-down .modal-footer{border-radius:0}}@media (max-width:1399.98px){.modal-fullscreen-xxl-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-xxl-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-xxl-down .modal-header{border-radius:0}.modal-fullscreen-xxl-down .modal-body{overflow-y:auto}.modal-fullscreen-xxl-down .modal-footer{border-radius:0}}.tooltip{position:absolute;z-index:1080;display:block;margin:0;font-family:var(--bs-font-sans-serif);font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;opacity:0}.tooltip.show{opacity:.9}.tooltip .tooltip-arrow{position:absolute;display:block;width:.8rem;height:.4rem}.tooltip .tooltip-arrow::before{position:absolute;content:"";border-color:transparent;border-style:solid}.bs-tooltip-auto[data-popper-placement^=top],.bs-tooltip-top{padding:.4rem 0}.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow,.bs-tooltip-top .tooltip-arrow{bottom:0}.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow::before,.bs-tooltip-top .tooltip-arrow::before{top:-1px;border-width:.4rem .4rem 0;border-top-color:#000}.bs-tooltip-auto[data-popper-placement^=right],.bs-tooltip-end{padding:0 .4rem}.bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow,.bs-tooltip-end .tooltip-arrow{left:0;width:.4rem;height:.8rem}.bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow::before,.bs-tooltip-end .tooltip-arrow::before{right:-1px;border-width:.4rem .4rem .4rem 0;border-right-color:#000}.bs-tooltip-auto[data-popper-placement^=bottom],.bs-tooltip-bottom{padding:.4rem 0}.bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow,.bs-tooltip-bottom .tooltip-arrow{top:0}.bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow::before,.bs-tooltip-bottom .tooltip-arrow::before{bottom:-1px;border-width:0 .4rem .4rem;border-bottom-color:#000}.bs-tooltip-auto[data-popper-placement^=left],.bs-tooltip-start{padding:0 .4rem}.bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow,.bs-tooltip-start .tooltip-arrow{right:0;width:.4rem;height:.8rem}.bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow::before,.bs-tooltip-start .tooltip-arrow::before{left:-1px;border-width:.4rem 0 .4rem .4rem;border-left-color:#000}.tooltip-inner{max-width:200px;padding:.25rem .5rem;color:#fff;text-align:center;background-color:#000;border-radius:.25rem}.popover{position:absolute;top:0;left:0;z-index:1070;display:block;max-width:276px;font-family:var(--bs-font-sans-serif);font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem}.popover .popover-arrow{position:absolute;display:block;width:1rem;height:.5rem}.popover .popover-arrow::after,.popover .popover-arrow::before{position:absolute;display:block;content:"";border-color:transparent;border-style:solid}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow,.bs-popover-top>.popover-arrow{bottom:calc(-.5rem - 1px)}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::before,.bs-popover-top>.popover-arrow::before{bottom:0;border-width:.5rem .5rem 0;border-top-color:rgba(0,0,0,.25)}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::after,.bs-popover-top>.popover-arrow::after{bottom:1px;border-width:.5rem .5rem 0;border-top-color:#fff}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow,.bs-popover-end>.popover-arrow{left:calc(-.5rem - 1px);width:.5rem;height:1rem}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::before,.bs-popover-end>.popover-arrow::before{left:0;border-width:.5rem .5rem .5rem 0;border-right-color:rgba(0,0,0,.25)}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::after,.bs-popover-end>.popover-arrow::after{left:1px;border-width:.5rem .5rem .5rem 0;border-right-color:#fff}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow,.bs-popover-bottom>.popover-arrow{top:calc(-.5rem - 1px)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::before,.bs-popover-bottom>.popover-arrow::before{top:0;border-width:0 .5rem .5rem .5rem;border-bottom-color:rgba(0,0,0,.25)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::after,.bs-popover-bottom>.popover-arrow::after{top:1px;border-width:0 .5rem .5rem .5rem;border-bottom-color:#fff}.bs-popover-auto[data-popper-placement^=bottom] .popover-header::before,.bs-popover-bottom .popover-header::before{position:absolute;top:0;left:50%;display:block;width:1rem;margin-left:-.5rem;content:"";border-bottom:1px solid #f0f0f0}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow,.bs-popover-start>.popover-arrow{right:calc(-.5rem - 1px);width:.5rem;height:1rem}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::before,.bs-popover-start>.popover-arrow::before{right:0;border-width:.5rem 0 .5rem .5rem;border-left-color:rgba(0,0,0,.25)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::after,.bs-popover-start>.popover-arrow::after{right:1px;border-width:.5rem 0 .5rem .5rem;border-left-color:#fff}.popover-header{padding:.5rem 1rem;margin-bottom:0;font-size:1rem;background-color:#f0f0f0;border-bottom:1px solid rgba(0,0,0,.2);border-top-left-radius:calc(.3rem - 1px);border-top-right-radius:calc(.3rem - 1px)}.popover-header:empty{display:none}.popover-body{padding:1rem 1rem;color:#212529}.carousel{position:relative}.carousel.pointer-event{touch-action:pan-y}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner::after{display:block;clear:both;content:""}.carousel-item{position:relative;display:none;float:left;width:100%;margin-right:-100%;-webkit-backface-visibility:hidden;backface-visibility:hidden;transition:transform .6s ease-in-out}@media (prefers-reduced-motion:reduce){.carousel-item{transition:none}}.carousel-item-next,.carousel-item-prev,.carousel-item.active{display:block}.active.carousel-item-end,.carousel-item-next:not(.carousel-item-start){transform:translateX(100%)}.active.carousel-item-start,.carousel-item-prev:not(.carousel-item-end){transform:translateX(-100%)}.carousel-fade .carousel-item{opacity:0;transition-property:opacity;transform:none}.carousel-fade .carousel-item-next.carousel-item-start,.carousel-fade .carousel-item-prev.carousel-item-end,.carousel-fade .carousel-item.active{z-index:1;opacity:1}.carousel-fade .active.carousel-item-end,.carousel-fade .active.carousel-item-start{z-index:0;opacity:0;transition:opacity 0s .6s}@media (prefers-reduced-motion:reduce){.carousel-fade .active.carousel-item-end,.carousel-fade .active.carousel-item-start{transition:none}}.carousel-control-next,.carousel-control-prev{position:absolute;top:0;bottom:0;z-index:1;display:flex;align-items:center;justify-content:center;width:15%;padding:0;color:#fff;text-align:center;background:0 0;border:0;opacity:.5;transition:opacity .15s ease}@media (prefers-reduced-motion:reduce){.carousel-control-next,.carousel-control-prev{transition:none}}.carousel-control-next:focus,.carousel-control-next:hover,.carousel-control-prev:focus,.carousel-control-prev:hover{color:#fff;text-decoration:none;outline:0;opacity:.9}.carousel-control-prev{left:0}.carousel-control-next{right:0}.carousel-control-next-icon,.carousel-control-prev-icon{display:inline-block;width:2rem;height:2rem;background-repeat:no-repeat;background-position:50%;background-size:100% 100%}.carousel-control-prev-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z'/%3e%3c/svg%3e")}.carousel-control-next-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e")}.carousel-indicators{position:absolute;right:0;bottom:0;left:0;z-index:2;display:flex;justify-content:center;padding:0;margin-right:15%;margin-bottom:1rem;margin-left:15%;list-style:none}.carousel-indicators [data-bs-target]{box-sizing:content-box;flex:0 1 auto;width:30px;height:3px;padding:0;margin-right:3px;margin-left:3px;text-indent:-999px;cursor:pointer;background-color:#fff;background-clip:padding-box;border:0;border-top:10px solid transparent;border-bottom:10px solid transparent;opacity:.5;transition:opacity .6s ease}@media (prefers-reduced-motion:reduce){.carousel-indicators [data-bs-target]{transition:none}}.carousel-indicators .active{opacity:1}.carousel-caption{position:absolute;right:15%;bottom:1.25rem;left:15%;padding-top:1.25rem;padding-bottom:1.25rem;color:#fff;text-align:center}.carousel-dark .carousel-control-next-icon,.carousel-dark .carousel-control-prev-icon{filter:invert(1) grayscale(100)}.carousel-dark .carousel-indicators [data-bs-target]{background-color:#000}.carousel-dark .carousel-caption{color:#000}@-webkit-keyframes spinner-border{to{transform:rotate(360deg)}}@keyframes spinner-border{to{transform:rotate(360deg)}}.spinner-border{display:inline-block;width:2rem;height:2rem;vertical-align:-.125em;border:.25em solid currentColor;border-right-color:transparent;border-radius:50%;-webkit-animation:.75s linear infinite spinner-border;animation:.75s linear infinite spinner-border}.spinner-border-sm{width:1rem;height:1rem;border-width:.2em}@-webkit-keyframes spinner-grow{0%{transform:scale(0)}50%{opacity:1;transform:none}}@keyframes spinner-grow{0%{transform:scale(0)}50%{opacity:1;transform:none}}.spinner-grow{display:inline-block;width:2rem;height:2rem;vertical-align:-.125em;background-color:currentColor;border-radius:50%;opacity:0;-webkit-animation:.75s linear infinite spinner-grow;animation:.75s linear infinite spinner-grow}.spinner-grow-sm{width:1rem;height:1rem}@media (prefers-reduced-motion:reduce){.spinner-border,.spinner-grow{-webkit-animation-duration:1.5s;animation-duration:1.5s}}.offcanvas{position:fixed;bottom:0;z-index:1045;display:flex;flex-direction:column;max-width:100%;visibility:hidden;background-color:#fff;background-clip:padding-box;outline:0;transition:transform .3s ease-in-out}@media (prefers-reduced-motion:reduce){.offcanvas{transition:none}}.offcanvas-backdrop{position:fixed;top:0;left:0;z-index:1040;width:100vw;height:100vh;background-color:#000}.offcanvas-backdrop.fade{opacity:0}.offcanvas-backdrop.show{opacity:.5}.offcanvas-header{display:flex;align-items:center;justify-content:space-between;padding:1rem 1rem}.offcanvas-header .btn-close{padding:.5rem .5rem;margin-top:-.5rem;margin-right:-.5rem;margin-bottom:-.5rem}.offcanvas-title{margin-bottom:0;line-height:1.5}.offcanvas-body{flex-grow:1;padding:1rem 1rem;overflow-y:auto}.offcanvas-start{top:0;left:0;width:400px;border-right:1px solid rgba(0,0,0,.2);transform:translateX(-100%)}.offcanvas-end{top:0;right:0;width:400px;border-left:1px solid rgba(0,0,0,.2);transform:translateX(100%)}.offcanvas-top{top:0;right:0;left:0;height:30vh;max-height:100%;border-bottom:1px solid rgba(0,0,0,.2);transform:translateY(-100%)}.offcanvas-bottom{right:0;left:0;height:30vh;max-height:100%;border-top:1px solid rgba(0,0,0,.2);transform:translateY(100%)}.offcanvas.show{transform:none}.placeholder{display:inline-block;min-height:1em;vertical-align:middle;cursor:wait;background-color:currentColor;opacity:.5}.placeholder.btn::before{display:inline-block;content:""}.placeholder-xs{min-height:.6em}.placeholder-sm{min-height:.8em}.placeholder-lg{min-height:1.2em}.placeholder-glow .placeholder{-webkit-animation:placeholder-glow 2s ease-in-out infinite;animation:placeholder-glow 2s ease-in-out infinite}@-webkit-keyframes placeholder-glow{50%{opacity:.2}}@keyframes placeholder-glow{50%{opacity:.2}}.placeholder-wave{-webkit-mask-image:linear-gradient(130deg,#000 55%,rgba(0,0,0,0.8) 75%,#000 95%);mask-image:linear-gradient(130deg,#000 55%,rgba(0,0,0,0.8) 75%,#000 95%);-webkit-mask-size:200% 100%;mask-size:200% 100%;-webkit-animation:placeholder-wave 2s linear infinite;animation:placeholder-wave 2s linear infinite}@-webkit-keyframes placeholder-wave{100%{-webkit-mask-position:-200% 0%;mask-position:-200% 0%}}@keyframes placeholder-wave{100%{-webkit-mask-position:-200% 0%;mask-position:-200% 0%}}.clearfix::after{display:block;clear:both;content:""}.link-primary{color:#0d6efd}.link-primary:focus,.link-primary:hover{color:#0a58ca}.link-secondary{color:#6c757d}.link-secondary:focus,.link-secondary:hover{color:#565e64}.link-success{color:#198754}.link-success:focus,.link-success:hover{color:#146c43}.link-info{color:#0dcaf0}.link-info:focus,.link-info:hover{color:#3dd5f3}.link-warning{color:#ffc107}.link-warning:focus,.link-warning:hover{color:#ffcd39}.link-danger{color:#dc3545}.link-danger:focus,.link-danger:hover{color:#b02a37}.link-light{color:#f8f9fa}.link-light:focus,.link-light:hover{color:#f9fafb}.link-dark{color:#212529}.link-dark:focus,.link-dark:hover{color:#1a1e21}.ratio{position:relative;width:100%}.ratio::before{display:block;padding-top:var(--bs-aspect-ratio);content:""}.ratio>*{position:absolute;top:0;left:0;width:100%;height:100%}.ratio-1x1{--bs-aspect-ratio:100%}.ratio-4x3{--bs-aspect-ratio:75%}.ratio-16x9{--bs-aspect-ratio:56.25%}.ratio-21x9{--bs-aspect-ratio:42.8571428571%}.fixed-top{position:fixed;top:0;right:0;left:0;z-index:1030}.fixed-bottom{position:fixed;right:0;bottom:0;left:0;z-index:1030}.sticky-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}@media (min-width:576px){.sticky-sm-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}@media (min-width:768px){.sticky-md-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}@media (min-width:992px){.sticky-lg-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}@media (min-width:1200px){.sticky-xl-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}@media (min-width:1400px){.sticky-xxl-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}.hstack{display:flex;flex-direction:row;align-items:center;align-self:stretch}.vstack{display:flex;flex:1 1 auto;flex-direction:column;align-self:stretch}.visually-hidden,.visually-hidden-focusable:not(:focus):not(:focus-within){position:absolute!important;width:1px!important;height:1px!important;padding:0!important;margin:-1px!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;white-space:nowrap!important;border:0!important}.stretched-link::after{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;content:""}.text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.vr{display:inline-block;align-self:stretch;width:1px;min-height:1em;background-color:currentColor;opacity:.25}.align-baseline{vertical-align:baseline!important}.align-top{vertical-align:top!important}.align-middle{vertical-align:middle!important}.align-bottom{vertical-align:bottom!important}.align-text-bottom{vertical-align:text-bottom!important}.align-text-top{vertical-align:text-top!important}.float-start{float:left!important}.float-end{float:right!important}.float-none{float:none!important}.opacity-0{opacity:0!important}.opacity-25{opacity:.25!important}.opacity-50{opacity:.5!important}.opacity-75{opacity:.75!important}.opacity-100{opacity:1!important}.overflow-auto{overflow:auto!important}.overflow-hidden{overflow:hidden!important}.overflow-visible{overflow:visible!important}.overflow-scroll{overflow:scroll!important}.d-inline{display:inline!important}.d-inline-block{display:inline-block!important}.d-block{display:block!important}.d-grid{display:grid!important}.d-table{display:table!important}.d-table-row{display:table-row!important}.d-table-cell{display:table-cell!important}.d-flex{display:flex!important}.d-inline-flex{display:inline-flex!important}.d-none{display:none!important}.shadow{box-shadow:0 .5rem 1rem rgba(0,0,0,.15)!important}.shadow-sm{box-shadow:0 .125rem .25rem rgba(0,0,0,.075)!important}.shadow-lg{box-shadow:0 1rem 3rem rgba(0,0,0,.175)!important}.shadow-none{box-shadow:none!important}.position-static{position:static!important}.position-relative{position:relative!important}.position-absolute{position:absolute!important}.position-fixed{position:fixed!important}.position-sticky{position:-webkit-sticky!important;position:sticky!important}.top-0{top:0!important}.top-50{top:50%!important}.top-100{top:100%!important}.bottom-0{bottom:0!important}.bottom-50{bottom:50%!important}.bottom-100{bottom:100%!important}.start-0{left:0!important}.start-50{left:50%!important}.start-100{left:100%!important}.end-0{right:0!important}.end-50{right:50%!important}.end-100{right:100%!important}.translate-middle{transform:translate(-50%,-50%)!important}.translate-middle-x{transform:translateX(-50%)!important}.translate-middle-y{transform:translateY(-50%)!important}.border{border:1px solid #dee2e6!important}.border-0{border:0!important}.border-top{border-top:1px solid #dee2e6!important}.border-top-0{border-top:0!important}.border-end{border-right:1px solid #dee2e6!important}.border-end-0{border-right:0!important}.border-bottom{border-bottom:1px solid #dee2e6!important}.border-bottom-0{border-bottom:0!important}.border-start{border-left:1px solid #dee2e6!important}.border-start-0{border-left:0!important}.border-primary{border-color:#0d6efd!important}.border-secondary{border-color:#6c757d!important}.border-success{border-color:#198754!important}.border-info{border-color:#0dcaf0!important}.border-warning{border-color:#ffc107!important}.border-danger{border-color:#dc3545!important}.border-light{border-color:#f8f9fa!important}.border-dark{border-color:#212529!important}.border-white{border-color:#fff!important}.border-1{border-width:1px!important}.border-2{border-width:2px!important}.border-3{border-width:3px!important}.border-4{border-width:4px!important}.border-5{border-width:5px!important}.w-25{width:25%!important}.w-50{width:50%!important}.w-75{width:75%!important}.w-100{width:100%!important}.w-auto{width:auto!important}.mw-100{max-width:100%!important}.vw-100{width:100vw!important}.min-vw-100{min-width:100vw!important}.h-25{height:25%!important}.h-50{height:50%!important}.h-75{height:75%!important}.h-100{height:100%!important}.h-auto{height:auto!important}.mh-100{max-height:100%!important}.vh-100{height:100vh!important}.min-vh-100{min-height:100vh!important}.flex-fill{flex:1 1 auto!important}.flex-row{flex-direction:row!important}.flex-column{flex-direction:column!important}.flex-row-reverse{flex-direction:row-reverse!important}.flex-column-reverse{flex-direction:column-reverse!important}.flex-grow-0{flex-grow:0!important}.flex-grow-1{flex-grow:1!important}.flex-shrink-0{flex-shrink:0!important}.flex-shrink-1{flex-shrink:1!important}.flex-wrap{flex-wrap:wrap!important}.flex-nowrap{flex-wrap:nowrap!important}.flex-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-0{gap:0!important}.gap-1{gap:.25rem!important}.gap-2{gap:.5rem!important}.gap-3{gap:1rem!important}.gap-4{gap:1.5rem!important}.gap-5{gap:3rem!important}.justify-content-start{justify-content:flex-start!important}.justify-content-end{justify-content:flex-end!important}.justify-content-center{justify-content:center!important}.justify-content-between{justify-content:space-between!important}.justify-content-around{justify-content:space-around!important}.justify-content-evenly{justify-content:space-evenly!important}.align-items-start{align-items:flex-start!important}.align-items-end{align-items:flex-end!important}.align-items-center{align-items:center!important}.align-items-baseline{align-items:baseline!important}.align-items-stretch{align-items:stretch!important}.align-content-start{align-content:flex-start!important}.align-content-end{align-content:flex-end!important}.align-content-center{align-content:center!important}.align-content-between{align-content:space-between!important}.align-content-around{align-content:space-around!important}.align-content-stretch{align-content:stretch!important}.align-self-auto{align-self:auto!important}.align-self-start{align-self:flex-start!important}.align-self-end{align-self:flex-end!important}.align-self-center{align-self:center!important}.align-self-baseline{align-self:baseline!important}.align-self-stretch{align-self:stretch!important}.order-first{order:-1!important}.order-0{order:0!important}.order-1{order:1!important}.order-2{order:2!important}.order-3{order:3!important}.order-4{order:4!important}.order-5{order:5!important}.order-last{order:6!important}.m-0{margin:0!important}.m-1{margin:.25rem!important}.m-2{margin:.5rem!important}.m-3{margin:1rem!important}.m-4{margin:1.5rem!important}.m-5{margin:3rem!important}.m-auto{margin:auto!important}.mx-0{margin-right:0!important;margin-left:0!important}.mx-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-3{margin-right:1rem!important;margin-left:1rem!important}.mx-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-5{margin-right:3rem!important;margin-left:3rem!important}.mx-auto{margin-right:auto!important;margin-left:auto!important}.my-0{margin-top:0!important;margin-bottom:0!important}.my-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-0{margin-top:0!important}.mt-1{margin-top:.25rem!important}.mt-2{margin-top:.5rem!important}.mt-3{margin-top:1rem!important}.mt-4{margin-top:1.5rem!important}.mt-5{margin-top:3rem!important}.mt-auto{margin-top:auto!important}.me-0{margin-right:0!important}.me-1{margin-right:.25rem!important}.me-2{margin-right:.5rem!important}.me-3{margin-right:1rem!important}.me-4{margin-right:1.5rem!important}.me-5{margin-right:3rem!important}.me-auto{margin-right:auto!important}.mb-0{margin-bottom:0!important}.mb-1{margin-bottom:.25rem!important}.mb-2{margin-bottom:.5rem!important}.mb-3{margin-bottom:1rem!important}.mb-4{margin-bottom:1.5rem!important}.mb-5{margin-bottom:3rem!important}.mb-auto{margin-bottom:auto!important}.ms-0{margin-left:0!important}.ms-1{margin-left:.25rem!important}.ms-2{margin-left:.5rem!important}.ms-3{margin-left:1rem!important}.ms-4{margin-left:1.5rem!important}.ms-5{margin-left:3rem!important}.ms-auto{margin-left:auto!important}.p-0{padding:0!important}.p-1{padding:.25rem!important}.p-2{padding:.5rem!important}.p-3{padding:1rem!important}.p-4{padding:1.5rem!important}.p-5{padding:3rem!important}.px-0{padding-right:0!important;padding-left:0!important}.px-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-3{padding-right:1rem!important;padding-left:1rem!important}.px-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-5{padding-right:3rem!important;padding-left:3rem!important}.py-0{padding-top:0!important;padding-bottom:0!important}.py-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-0{padding-top:0!important}.pt-1{padding-top:.25rem!important}.pt-2{padding-top:.5rem!important}.pt-3{padding-top:1rem!important}.pt-4{padding-top:1.5rem!important}.pt-5{padding-top:3rem!important}.pe-0{padding-right:0!important}.pe-1{padding-right:.25rem!important}.pe-2{padding-right:.5rem!important}.pe-3{padding-right:1rem!important}.pe-4{padding-right:1.5rem!important}.pe-5{padding-right:3rem!important}.pb-0{padding-bottom:0!important}.pb-1{padding-bottom:.25rem!important}.pb-2{padding-bottom:.5rem!important}.pb-3{padding-bottom:1rem!important}.pb-4{padding-bottom:1.5rem!important}.pb-5{padding-bottom:3rem!important}.ps-0{padding-left:0!important}.ps-1{padding-left:.25rem!important}.ps-2{padding-left:.5rem!important}.ps-3{padding-left:1rem!important}.ps-4{padding-left:1.5rem!important}.ps-5{padding-left:3rem!important}.font-monospace{font-family:var(--bs-font-monospace)!important}.fs-1{font-size:calc(1.375rem + 1.5vw)!important}.fs-2{font-size:calc(1.325rem + .9vw)!important}.fs-3{font-size:calc(1.3rem + .6vw)!important}.fs-4{font-size:calc(1.275rem + .3vw)!important}.fs-5{font-size:1.25rem!important}.fs-6{font-size:1rem!important}.fst-italic{font-style:italic!important}.fst-normal{font-style:normal!important}.fw-light{font-weight:300!important}.fw-lighter{font-weight:lighter!important}.fw-normal{font-weight:400!important}.fw-bold{font-weight:700!important}.fw-bolder{font-weight:bolder!important}.lh-1{line-height:1!important}.lh-sm{line-height:1.25!important}.lh-base{line-height:1.5!important}.lh-lg{line-height:2!important}.text-start{text-align:left!important}.text-end{text-align:right!important}.text-center{text-align:center!important}.text-decoration-none{text-decoration:none!important}.text-decoration-underline{text-decoration:underline!important}.text-decoration-line-through{text-decoration:line-through!important}.text-lowercase{text-transform:lowercase!important}.text-uppercase{text-transform:uppercase!important}.text-capitalize{text-transform:capitalize!important}.text-wrap{white-space:normal!important}.text-nowrap{white-space:nowrap!important}.text-break{word-wrap:break-word!important;word-break:break-word!important}.text-primary{--bs-text-opacity:1;color:rgba(var(--bs-primary-rgb),var(--bs-text-opacity))!important}.text-secondary{--bs-text-opacity:1;color:rgba(var(--bs-secondary-rgb),var(--bs-text-opacity))!important}.text-success{--bs-text-opacity:1;color:rgba(var(--bs-success-rgb),var(--bs-text-opacity))!important}.text-info{--bs-text-opacity:1;color:rgba(var(--bs-info-rgb),var(--bs-text-opacity))!important}.text-warning{--bs-text-opacity:1;color:rgba(var(--bs-warning-rgb),var(--bs-text-opacity))!important}.text-danger{--bs-text-opacity:1;color:rgba(var(--bs-danger-rgb),var(--bs-text-opacity))!important}.text-light{--bs-text-opacity:1;color:rgba(var(--bs-light-rgb),var(--bs-text-opacity))!important}.text-dark{--bs-text-opacity:1;color:rgba(var(--bs-dark-rgb),var(--bs-text-opacity))!important}.text-black{--bs-text-opacity:1;color:rgba(var(--bs-black-rgb),var(--bs-text-opacity))!important}.text-white{--bs-text-opacity:1;color:rgba(var(--bs-white-rgb),var(--bs-text-opacity))!important}.text-body{--bs-text-opacity:1;color:rgba(var(--bs-body-color-rgb),var(--bs-text-opacity))!important}.text-muted{--bs-text-opacity:1;color:#6c757d!important}.text-black-50{--bs-text-opacity:1;color:rgba(0,0,0,.5)!important}.text-white-50{--bs-text-opacity:1;color:rgba(255,255,255,.5)!important}.text-reset{--bs-text-opacity:1;color:inherit!important}.text-opacity-25{--bs-text-opacity:0.25}.text-opacity-50{--bs-text-opacity:0.5}.text-opacity-75{--bs-text-opacity:0.75}.text-opacity-100{--bs-text-opacity:1}.bg-primary{--bs-bg-opacity:1;background-color:rgba(var(--bs-primary-rgb),var(--bs-bg-opacity))!important}.bg-secondary{--bs-bg-opacity:1;background-color:rgba(var(--bs-secondary-rgb),var(--bs-bg-opacity))!important}.bg-success{--bs-bg-opacity:1;background-color:rgba(var(--bs-success-rgb),var(--bs-bg-opacity))!important}.bg-info{--bs-bg-opacity:1;background-color:rgba(var(--bs-info-rgb),var(--bs-bg-opacity))!important}.bg-warning{--bs-bg-opacity:1;background-color:rgba(var(--bs-warning-rgb),var(--bs-bg-opacity))!important}.bg-danger{--bs-bg-opacity:1;background-color:rgba(var(--bs-danger-rgb),var(--bs-bg-opacity))!important}.bg-light{--bs-bg-opacity:1;background-color:rgba(var(--bs-light-rgb),var(--bs-bg-opacity))!important}.bg-dark{--bs-bg-opacity:1;background-color:rgba(var(--bs-dark-rgb),var(--bs-bg-opacity))!important}.bg-black{--bs-bg-opacity:1;background-color:rgba(var(--bs-black-rgb),var(--bs-bg-opacity))!important}.bg-white{--bs-bg-opacity:1;background-color:rgba(var(--bs-white-rgb),var(--bs-bg-opacity))!important}.bg-body{--bs-bg-opacity:1;background-color:rgba(var(--bs-body-bg-rgb),var(--bs-bg-opacity))!important}.bg-transparent{--bs-bg-opacity:1;background-color:transparent!important}.bg-opacity-10{--bs-bg-opacity:0.1}.bg-opacity-25{--bs-bg-opacity:0.25}.bg-opacity-50{--bs-bg-opacity:0.5}.bg-opacity-75{--bs-bg-opacity:0.75}.bg-opacity-100{--bs-bg-opacity:1}.bg-gradient{background-image:var(--bs-gradient)!important}.user-select-all{-webkit-user-select:all!important;-moz-user-select:all!important;user-select:all!important}.user-select-auto{-webkit-user-select:auto!important;-moz-user-select:auto!important;user-select:auto!important}.user-select-none{-webkit-user-select:none!important;-moz-user-select:none!important;user-select:none!important}.pe-none{pointer-events:none!important}.pe-auto{pointer-events:auto!important}.rounded{border-radius:.25rem!important}.rounded-0{border-radius:0!important}.rounded-1{border-radius:.2rem!important}.rounded-2{border-radius:.25rem!important}.rounded-3{border-radius:.3rem!important}.rounded-circle{border-radius:50%!important}.rounded-pill{border-radius:50rem!important}.rounded-top{border-top-left-radius:.25rem!important;border-top-right-radius:.25rem!important}.rounded-end{border-top-right-radius:.25rem!important;border-bottom-right-radius:.25rem!important}.rounded-bottom{border-bottom-right-radius:.25rem!important;border-bottom-left-radius:.25rem!important}.rounded-start{border-bottom-left-radius:.25rem!important;border-top-left-radius:.25rem!important}.visible{visibility:visible!important}.invisible{visibility:hidden!important}@media (min-width:576px){.float-sm-start{float:left!important}.float-sm-end{float:right!important}.float-sm-none{float:none!important}.d-sm-inline{display:inline!important}.d-sm-inline-block{display:inline-block!important}.d-sm-block{display:block!important}.d-sm-grid{display:grid!important}.d-sm-table{display:table!important}.d-sm-table-row{display:table-row!important}.d-sm-table-cell{display:table-cell!important}.d-sm-flex{display:flex!important}.d-sm-inline-flex{display:inline-flex!important}.d-sm-none{display:none!important}.flex-sm-fill{flex:1 1 auto!important}.flex-sm-row{flex-direction:row!important}.flex-sm-column{flex-direction:column!important}.flex-sm-row-reverse{flex-direction:row-reverse!important}.flex-sm-column-reverse{flex-direction:column-reverse!important}.flex-sm-grow-0{flex-grow:0!important}.flex-sm-grow-1{flex-grow:1!important}.flex-sm-shrink-0{flex-shrink:0!important}.flex-sm-shrink-1{flex-shrink:1!important}.flex-sm-wrap{flex-wrap:wrap!important}.flex-sm-nowrap{flex-wrap:nowrap!important}.flex-sm-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-sm-0{gap:0!important}.gap-sm-1{gap:.25rem!important}.gap-sm-2{gap:.5rem!important}.gap-sm-3{gap:1rem!important}.gap-sm-4{gap:1.5rem!important}.gap-sm-5{gap:3rem!important}.justify-content-sm-start{justify-content:flex-start!important}.justify-content-sm-end{justify-content:flex-end!important}.justify-content-sm-center{justify-content:center!important}.justify-content-sm-between{justify-content:space-between!important}.justify-content-sm-around{justify-content:space-around!important}.justify-content-sm-evenly{justify-content:space-evenly!important}.align-items-sm-start{align-items:flex-start!important}.align-items-sm-end{align-items:flex-end!important}.align-items-sm-center{align-items:center!important}.align-items-sm-baseline{align-items:baseline!important}.align-items-sm-stretch{align-items:stretch!important}.align-content-sm-start{align-content:flex-start!important}.align-content-sm-end{align-content:flex-end!important}.align-content-sm-center{align-content:center!important}.align-content-sm-between{align-content:space-between!important}.align-content-sm-around{align-content:space-around!important}.align-content-sm-stretch{align-content:stretch!important}.align-self-sm-auto{align-self:auto!important}.align-self-sm-start{align-self:flex-start!important}.align-self-sm-end{align-self:flex-end!important}.align-self-sm-center{align-self:center!important}.align-self-sm-baseline{align-self:baseline!important}.align-self-sm-stretch{align-self:stretch!important}.order-sm-first{order:-1!important}.order-sm-0{order:0!important}.order-sm-1{order:1!important}.order-sm-2{order:2!important}.order-sm-3{order:3!important}.order-sm-4{order:4!important}.order-sm-5{order:5!important}.order-sm-last{order:6!important}.m-sm-0{margin:0!important}.m-sm-1{margin:.25rem!important}.m-sm-2{margin:.5rem!important}.m-sm-3{margin:1rem!important}.m-sm-4{margin:1.5rem!important}.m-sm-5{margin:3rem!important}.m-sm-auto{margin:auto!important}.mx-sm-0{margin-right:0!important;margin-left:0!important}.mx-sm-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-sm-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-sm-3{margin-right:1rem!important;margin-left:1rem!important}.mx-sm-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-sm-5{margin-right:3rem!important;margin-left:3rem!important}.mx-sm-auto{margin-right:auto!important;margin-left:auto!important}.my-sm-0{margin-top:0!important;margin-bottom:0!important}.my-sm-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-sm-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-sm-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-sm-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-sm-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-sm-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-sm-0{margin-top:0!important}.mt-sm-1{margin-top:.25rem!important}.mt-sm-2{margin-top:.5rem!important}.mt-sm-3{margin-top:1rem!important}.mt-sm-4{margin-top:1.5rem!important}.mt-sm-5{margin-top:3rem!important}.mt-sm-auto{margin-top:auto!important}.me-sm-0{margin-right:0!important}.me-sm-1{margin-right:.25rem!important}.me-sm-2{margin-right:.5rem!important}.me-sm-3{margin-right:1rem!important}.me-sm-4{margin-right:1.5rem!important}.me-sm-5{margin-right:3rem!important}.me-sm-auto{margin-right:auto!important}.mb-sm-0{margin-bottom:0!important}.mb-sm-1{margin-bottom:.25rem!important}.mb-sm-2{margin-bottom:.5rem!important}.mb-sm-3{margin-bottom:1rem!important}.mb-sm-4{margin-bottom:1.5rem!important}.mb-sm-5{margin-bottom:3rem!important}.mb-sm-auto{margin-bottom:auto!important}.ms-sm-0{margin-left:0!important}.ms-sm-1{margin-left:.25rem!important}.ms-sm-2{margin-left:.5rem!important}.ms-sm-3{margin-left:1rem!important}.ms-sm-4{margin-left:1.5rem!important}.ms-sm-5{margin-left:3rem!important}.ms-sm-auto{margin-left:auto!important}.p-sm-0{padding:0!important}.p-sm-1{padding:.25rem!important}.p-sm-2{padding:.5rem!important}.p-sm-3{padding:1rem!important}.p-sm-4{padding:1.5rem!important}.p-sm-5{padding:3rem!important}.px-sm-0{padding-right:0!important;padding-left:0!important}.px-sm-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-sm-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-sm-3{padding-right:1rem!important;padding-left:1rem!important}.px-sm-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-sm-5{padding-right:3rem!important;padding-left:3rem!important}.py-sm-0{padding-top:0!important;padding-bottom:0!important}.py-sm-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-sm-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-sm-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-sm-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-sm-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-sm-0{padding-top:0!important}.pt-sm-1{padding-top:.25rem!important}.pt-sm-2{padding-top:.5rem!important}.pt-sm-3{padding-top:1rem!important}.pt-sm-4{padding-top:1.5rem!important}.pt-sm-5{padding-top:3rem!important}.pe-sm-0{padding-right:0!important}.pe-sm-1{padding-right:.25rem!important}.pe-sm-2{padding-right:.5rem!important}.pe-sm-3{padding-right:1rem!important}.pe-sm-4{padding-right:1.5rem!important}.pe-sm-5{padding-right:3rem!important}.pb-sm-0{padding-bottom:0!important}.pb-sm-1{padding-bottom:.25rem!important}.pb-sm-2{padding-bottom:.5rem!important}.pb-sm-3{padding-bottom:1rem!important}.pb-sm-4{padding-bottom:1.5rem!important}.pb-sm-5{padding-bottom:3rem!important}.ps-sm-0{padding-left:0!important}.ps-sm-1{padding-left:.25rem!important}.ps-sm-2{padding-left:.5rem!important}.ps-sm-3{padding-left:1rem!important}.ps-sm-4{padding-left:1.5rem!important}.ps-sm-5{padding-left:3rem!important}.text-sm-start{text-align:left!important}.text-sm-end{text-align:right!important}.text-sm-center{text-align:center!important}}@media (min-width:768px){.float-md-start{float:left!important}.float-md-end{float:right!important}.float-md-none{float:none!important}.d-md-inline{display:inline!important}.d-md-inline-block{display:inline-block!important}.d-md-block{display:block!important}.d-md-grid{display:grid!important}.d-md-table{display:table!important}.d-md-table-row{display:table-row!important}.d-md-table-cell{display:table-cell!important}.d-md-flex{display:flex!important}.d-md-inline-flex{display:inline-flex!important}.d-md-none{display:none!important}.flex-md-fill{flex:1 1 auto!important}.flex-md-row{flex-direction:row!important}.flex-md-column{flex-direction:column!important}.flex-md-row-reverse{flex-direction:row-reverse!important}.flex-md-column-reverse{flex-direction:column-reverse!important}.flex-md-grow-0{flex-grow:0!important}.flex-md-grow-1{flex-grow:1!important}.flex-md-shrink-0{flex-shrink:0!important}.flex-md-shrink-1{flex-shrink:1!important}.flex-md-wrap{flex-wrap:wrap!important}.flex-md-nowrap{flex-wrap:nowrap!important}.flex-md-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-md-0{gap:0!important}.gap-md-1{gap:.25rem!important}.gap-md-2{gap:.5rem!important}.gap-md-3{gap:1rem!important}.gap-md-4{gap:1.5rem!important}.gap-md-5{gap:3rem!important}.justify-content-md-start{justify-content:flex-start!important}.justify-content-md-end{justify-content:flex-end!important}.justify-content-md-center{justify-content:center!important}.justify-content-md-between{justify-content:space-between!important}.justify-content-md-around{justify-content:space-around!important}.justify-content-md-evenly{justify-content:space-evenly!important}.align-items-md-start{align-items:flex-start!important}.align-items-md-end{align-items:flex-end!important}.align-items-md-center{align-items:center!important}.align-items-md-baseline{align-items:baseline!important}.align-items-md-stretch{align-items:stretch!important}.align-content-md-start{align-content:flex-start!important}.align-content-md-end{align-content:flex-end!important}.align-content-md-center{align-content:center!important}.align-content-md-between{align-content:space-between!important}.align-content-md-around{align-content:space-around!important}.align-content-md-stretch{align-content:stretch!important}.align-self-md-auto{align-self:auto!important}.align-self-md-start{align-self:flex-start!important}.align-self-md-end{align-self:flex-end!important}.align-self-md-center{align-self:center!important}.align-self-md-baseline{align-self:baseline!important}.align-self-md-stretch{align-self:stretch!important}.order-md-first{order:-1!important}.order-md-0{order:0!important}.order-md-1{order:1!important}.order-md-2{order:2!important}.order-md-3{order:3!important}.order-md-4{order:4!important}.order-md-5{order:5!important}.order-md-last{order:6!important}.m-md-0{margin:0!important}.m-md-1{margin:.25rem!important}.m-md-2{margin:.5rem!important}.m-md-3{margin:1rem!important}.m-md-4{margin:1.5rem!important}.m-md-5{margin:3rem!important}.m-md-auto{margin:auto!important}.mx-md-0{margin-right:0!important;margin-left:0!important}.mx-md-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-md-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-md-3{margin-right:1rem!important;margin-left:1rem!important}.mx-md-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-md-5{margin-right:3rem!important;margin-left:3rem!important}.mx-md-auto{margin-right:auto!important;margin-left:auto!important}.my-md-0{margin-top:0!important;margin-bottom:0!important}.my-md-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-md-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-md-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-md-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-md-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-md-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-md-0{margin-top:0!important}.mt-md-1{margin-top:.25rem!important}.mt-md-2{margin-top:.5rem!important}.mt-md-3{margin-top:1rem!important}.mt-md-4{margin-top:1.5rem!important}.mt-md-5{margin-top:3rem!important}.mt-md-auto{margin-top:auto!important}.me-md-0{margin-right:0!important}.me-md-1{margin-right:.25rem!important}.me-md-2{margin-right:.5rem!important}.me-md-3{margin-right:1rem!important}.me-md-4{margin-right:1.5rem!important}.me-md-5{margin-right:3rem!important}.me-md-auto{margin-right:auto!important}.mb-md-0{margin-bottom:0!important}.mb-md-1{margin-bottom:.25rem!important}.mb-md-2{margin-bottom:.5rem!important}.mb-md-3{margin-bottom:1rem!important}.mb-md-4{margin-bottom:1.5rem!important}.mb-md-5{margin-bottom:3rem!important}.mb-md-auto{margin-bottom:auto!important}.ms-md-0{margin-left:0!important}.ms-md-1{margin-left:.25rem!important}.ms-md-2{margin-left:.5rem!important}.ms-md-3{margin-left:1rem!important}.ms-md-4{margin-left:1.5rem!important}.ms-md-5{margin-left:3rem!important}.ms-md-auto{margin-left:auto!important}.p-md-0{padding:0!important}.p-md-1{padding:.25rem!important}.p-md-2{padding:.5rem!important}.p-md-3{padding:1rem!important}.p-md-4{padding:1.5rem!important}.p-md-5{padding:3rem!important}.px-md-0{padding-right:0!important;padding-left:0!important}.px-md-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-md-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-md-3{padding-right:1rem!important;padding-left:1rem!important}.px-md-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-md-5{padding-right:3rem!important;padding-left:3rem!important}.py-md-0{padding-top:0!important;padding-bottom:0!important}.py-md-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-md-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-md-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-md-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-md-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-md-0{padding-top:0!important}.pt-md-1{padding-top:.25rem!important}.pt-md-2{padding-top:.5rem!important}.pt-md-3{padding-top:1rem!important}.pt-md-4{padding-top:1.5rem!important}.pt-md-5{padding-top:3rem!important}.pe-md-0{padding-right:0!important}.pe-md-1{padding-right:.25rem!important}.pe-md-2{padding-right:.5rem!important}.pe-md-3{padding-right:1rem!important}.pe-md-4{padding-right:1.5rem!important}.pe-md-5{padding-right:3rem!important}.pb-md-0{padding-bottom:0!important}.pb-md-1{padding-bottom:.25rem!important}.pb-md-2{padding-bottom:.5rem!important}.pb-md-3{padding-bottom:1rem!important}.pb-md-4{padding-bottom:1.5rem!important}.pb-md-5{padding-bottom:3rem!important}.ps-md-0{padding-left:0!important}.ps-md-1{padding-left:.25rem!important}.ps-md-2{padding-left:.5rem!important}.ps-md-3{padding-left:1rem!important}.ps-md-4{padding-left:1.5rem!important}.ps-md-5{padding-left:3rem!important}.text-md-start{text-align:left!important}.text-md-end{text-align:right!important}.text-md-center{text-align:center!important}}@media (min-width:992px){.float-lg-start{float:left!important}.float-lg-end{float:right!important}.float-lg-none{float:none!important}.d-lg-inline{display:inline!important}.d-lg-inline-block{display:inline-block!important}.d-lg-block{display:block!important}.d-lg-grid{display:grid!important}.d-lg-table{display:table!important}.d-lg-table-row{display:table-row!important}.d-lg-table-cell{display:table-cell!important}.d-lg-flex{display:flex!important}.d-lg-inline-flex{display:inline-flex!important}.d-lg-none{display:none!important}.flex-lg-fill{flex:1 1 auto!important}.flex-lg-row{flex-direction:row!important}.flex-lg-column{flex-direction:column!important}.flex-lg-row-reverse{flex-direction:row-reverse!important}.flex-lg-column-reverse{flex-direction:column-reverse!important}.flex-lg-grow-0{flex-grow:0!important}.flex-lg-grow-1{flex-grow:1!important}.flex-lg-shrink-0{flex-shrink:0!important}.flex-lg-shrink-1{flex-shrink:1!important}.flex-lg-wrap{flex-wrap:wrap!important}.flex-lg-nowrap{flex-wrap:nowrap!important}.flex-lg-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-lg-0{gap:0!important}.gap-lg-1{gap:.25rem!important}.gap-lg-2{gap:.5rem!important}.gap-lg-3{gap:1rem!important}.gap-lg-4{gap:1.5rem!important}.gap-lg-5{gap:3rem!important}.justify-content-lg-start{justify-content:flex-start!important}.justify-content-lg-end{justify-content:flex-end!important}.justify-content-lg-center{justify-content:center!important}.justify-content-lg-between{justify-content:space-between!important}.justify-content-lg-around{justify-content:space-around!important}.justify-content-lg-evenly{justify-content:space-evenly!important}.align-items-lg-start{align-items:flex-start!important}.align-items-lg-end{align-items:flex-end!important}.align-items-lg-center{align-items:center!important}.align-items-lg-baseline{align-items:baseline!important}.align-items-lg-stretch{align-items:stretch!important}.align-content-lg-start{align-content:flex-start!important}.align-content-lg-end{align-content:flex-end!important}.align-content-lg-center{align-content:center!important}.align-content-lg-between{align-content:space-between!important}.align-content-lg-around{align-content:space-around!important}.align-content-lg-stretch{align-content:stretch!important}.align-self-lg-auto{align-self:auto!important}.align-self-lg-start{align-self:flex-start!important}.align-self-lg-end{align-self:flex-end!important}.align-self-lg-center{align-self:center!important}.align-self-lg-baseline{align-self:baseline!important}.align-self-lg-stretch{align-self:stretch!important}.order-lg-first{order:-1!important}.order-lg-0{order:0!important}.order-lg-1{order:1!important}.order-lg-2{order:2!important}.order-lg-3{order:3!important}.order-lg-4{order:4!important}.order-lg-5{order:5!important}.order-lg-last{order:6!important}.m-lg-0{margin:0!important}.m-lg-1{margin:.25rem!important}.m-lg-2{margin:.5rem!important}.m-lg-3{margin:1rem!important}.m-lg-4{margin:1.5rem!important}.m-lg-5{margin:3rem!important}.m-lg-auto{margin:auto!important}.mx-lg-0{margin-right:0!important;margin-left:0!important}.mx-lg-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-lg-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-lg-3{margin-right:1rem!important;margin-left:1rem!important}.mx-lg-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-lg-5{margin-right:3rem!important;margin-left:3rem!important}.mx-lg-auto{margin-right:auto!important;margin-left:auto!important}.my-lg-0{margin-top:0!important;margin-bottom:0!important}.my-lg-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-lg-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-lg-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-lg-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-lg-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-lg-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-lg-0{margin-top:0!important}.mt-lg-1{margin-top:.25rem!important}.mt-lg-2{margin-top:.5rem!important}.mt-lg-3{margin-top:1rem!important}.mt-lg-4{margin-top:1.5rem!important}.mt-lg-5{margin-top:3rem!important}.mt-lg-auto{margin-top:auto!important}.me-lg-0{margin-right:0!important}.me-lg-1{margin-right:.25rem!important}.me-lg-2{margin-right:.5rem!important}.me-lg-3{margin-right:1rem!important}.me-lg-4{margin-right:1.5rem!important}.me-lg-5{margin-right:3rem!important}.me-lg-auto{margin-right:auto!important}.mb-lg-0{margin-bottom:0!important}.mb-lg-1{margin-bottom:.25rem!important}.mb-lg-2{margin-bottom:.5rem!important}.mb-lg-3{margin-bottom:1rem!important}.mb-lg-4{margin-bottom:1.5rem!important}.mb-lg-5{margin-bottom:3rem!important}.mb-lg-auto{margin-bottom:auto!important}.ms-lg-0{margin-left:0!important}.ms-lg-1{margin-left:.25rem!important}.ms-lg-2{margin-left:.5rem!important}.ms-lg-3{margin-left:1rem!important}.ms-lg-4{margin-left:1.5rem!important}.ms-lg-5{margin-left:3rem!important}.ms-lg-auto{margin-left:auto!important}.p-lg-0{padding:0!important}.p-lg-1{padding:.25rem!important}.p-lg-2{padding:.5rem!important}.p-lg-3{padding:1rem!important}.p-lg-4{padding:1.5rem!important}.p-lg-5{padding:3rem!important}.px-lg-0{padding-right:0!important;padding-left:0!important}.px-lg-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-lg-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-lg-3{padding-right:1rem!important;padding-left:1rem!important}.px-lg-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-lg-5{padding-right:3rem!important;padding-left:3rem!important}.py-lg-0{padding-top:0!important;padding-bottom:0!important}.py-lg-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-lg-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-lg-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-lg-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-lg-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-lg-0{padding-top:0!important}.pt-lg-1{padding-top:.25rem!important}.pt-lg-2{padding-top:.5rem!important}.pt-lg-3{padding-top:1rem!important}.pt-lg-4{padding-top:1.5rem!important}.pt-lg-5{padding-top:3rem!important}.pe-lg-0{padding-right:0!important}.pe-lg-1{padding-right:.25rem!important}.pe-lg-2{padding-right:.5rem!important}.pe-lg-3{padding-right:1rem!important}.pe-lg-4{padding-right:1.5rem!important}.pe-lg-5{padding-right:3rem!important}.pb-lg-0{padding-bottom:0!important}.pb-lg-1{padding-bottom:.25rem!important}.pb-lg-2{padding-bottom:.5rem!important}.pb-lg-3{padding-bottom:1rem!important}.pb-lg-4{padding-bottom:1.5rem!important}.pb-lg-5{padding-bottom:3rem!important}.ps-lg-0{padding-left:0!important}.ps-lg-1{padding-left:.25rem!important}.ps-lg-2{padding-left:.5rem!important}.ps-lg-3{padding-left:1rem!important}.ps-lg-4{padding-left:1.5rem!important}.ps-lg-5{padding-left:3rem!important}.text-lg-start{text-align:left!important}.text-lg-end{text-align:right!important}.text-lg-center{text-align:center!important}}@media (min-width:1200px){.float-xl-start{float:left!important}.float-xl-end{float:right!important}.float-xl-none{float:none!important}.d-xl-inline{display:inline!important}.d-xl-inline-block{display:inline-block!important}.d-xl-block{display:block!important}.d-xl-grid{display:grid!important}.d-xl-table{display:table!important}.d-xl-table-row{display:table-row!important}.d-xl-table-cell{display:table-cell!important}.d-xl-flex{display:flex!important}.d-xl-inline-flex{display:inline-flex!important}.d-xl-none{display:none!important}.flex-xl-fill{flex:1 1 auto!important}.flex-xl-row{flex-direction:row!important}.flex-xl-column{flex-direction:column!important}.flex-xl-row-reverse{flex-direction:row-reverse!important}.flex-xl-column-reverse{flex-direction:column-reverse!important}.flex-xl-grow-0{flex-grow:0!important}.flex-xl-grow-1{flex-grow:1!important}.flex-xl-shrink-0{flex-shrink:0!important}.flex-xl-shrink-1{flex-shrink:1!important}.flex-xl-wrap{flex-wrap:wrap!important}.flex-xl-nowrap{flex-wrap:nowrap!important}.flex-xl-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-xl-0{gap:0!important}.gap-xl-1{gap:.25rem!important}.gap-xl-2{gap:.5rem!important}.gap-xl-3{gap:1rem!important}.gap-xl-4{gap:1.5rem!important}.gap-xl-5{gap:3rem!important}.justify-content-xl-start{justify-content:flex-start!important}.justify-content-xl-end{justify-content:flex-end!important}.justify-content-xl-center{justify-content:center!important}.justify-content-xl-between{justify-content:space-between!important}.justify-content-xl-around{justify-content:space-around!important}.justify-content-xl-evenly{justify-content:space-evenly!important}.align-items-xl-start{align-items:flex-start!important}.align-items-xl-end{align-items:flex-end!important}.align-items-xl-center{align-items:center!important}.align-items-xl-baseline{align-items:baseline!important}.align-items-xl-stretch{align-items:stretch!important}.align-content-xl-start{align-content:flex-start!important}.align-content-xl-end{align-content:flex-end!important}.align-content-xl-center{align-content:center!important}.align-content-xl-between{align-content:space-between!important}.align-content-xl-around{align-content:space-around!important}.align-content-xl-stretch{align-content:stretch!important}.align-self-xl-auto{align-self:auto!important}.align-self-xl-start{align-self:flex-start!important}.align-self-xl-end{align-self:flex-end!important}.align-self-xl-center{align-self:center!important}.align-self-xl-baseline{align-self:baseline!important}.align-self-xl-stretch{align-self:stretch!important}.order-xl-first{order:-1!important}.order-xl-0{order:0!important}.order-xl-1{order:1!important}.order-xl-2{order:2!important}.order-xl-3{order:3!important}.order-xl-4{order:4!important}.order-xl-5{order:5!important}.order-xl-last{order:6!important}.m-xl-0{margin:0!important}.m-xl-1{margin:.25rem!important}.m-xl-2{margin:.5rem!important}.m-xl-3{margin:1rem!important}.m-xl-4{margin:1.5rem!important}.m-xl-5{margin:3rem!important}.m-xl-auto{margin:auto!important}.mx-xl-0{margin-right:0!important;margin-left:0!important}.mx-xl-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-xl-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-xl-3{margin-right:1rem!important;margin-left:1rem!important}.mx-xl-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-xl-5{margin-right:3rem!important;margin-left:3rem!important}.mx-xl-auto{margin-right:auto!important;margin-left:auto!important}.my-xl-0{margin-top:0!important;margin-bottom:0!important}.my-xl-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-xl-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-xl-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-xl-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-xl-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-xl-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-xl-0{margin-top:0!important}.mt-xl-1{margin-top:.25rem!important}.mt-xl-2{margin-top:.5rem!important}.mt-xl-3{margin-top:1rem!important}.mt-xl-4{margin-top:1.5rem!important}.mt-xl-5{margin-top:3rem!important}.mt-xl-auto{margin-top:auto!important}.me-xl-0{margin-right:0!important}.me-xl-1{margin-right:.25rem!important}.me-xl-2{margin-right:.5rem!important}.me-xl-3{margin-right:1rem!important}.me-xl-4{margin-right:1.5rem!important}.me-xl-5{margin-right:3rem!important}.me-xl-auto{margin-right:auto!important}.mb-xl-0{margin-bottom:0!important}.mb-xl-1{margin-bottom:.25rem!important}.mb-xl-2{margin-bottom:.5rem!important}.mb-xl-3{margin-bottom:1rem!important}.mb-xl-4{margin-bottom:1.5rem!important}.mb-xl-5{margin-bottom:3rem!important}.mb-xl-auto{margin-bottom:auto!important}.ms-xl-0{margin-left:0!important}.ms-xl-1{margin-left:.25rem!important}.ms-xl-2{margin-left:.5rem!important}.ms-xl-3{margin-left:1rem!important}.ms-xl-4{margin-left:1.5rem!important}.ms-xl-5{margin-left:3rem!important}.ms-xl-auto{margin-left:auto!important}.p-xl-0{padding:0!important}.p-xl-1{padding:.25rem!important}.p-xl-2{padding:.5rem!important}.p-xl-3{padding:1rem!important}.p-xl-4{padding:1.5rem!important}.p-xl-5{padding:3rem!important}.px-xl-0{padding-right:0!important;padding-left:0!important}.px-xl-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-xl-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-xl-3{padding-right:1rem!important;padding-left:1rem!important}.px-xl-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-xl-5{padding-right:3rem!important;padding-left:3rem!important}.py-xl-0{padding-top:0!important;padding-bottom:0!important}.py-xl-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-xl-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-xl-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-xl-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-xl-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-xl-0{padding-top:0!important}.pt-xl-1{padding-top:.25rem!important}.pt-xl-2{padding-top:.5rem!important}.pt-xl-3{padding-top:1rem!important}.pt-xl-4{padding-top:1.5rem!important}.pt-xl-5{padding-top:3rem!important}.pe-xl-0{padding-right:0!important}.pe-xl-1{padding-right:.25rem!important}.pe-xl-2{padding-right:.5rem!important}.pe-xl-3{padding-right:1rem!important}.pe-xl-4{padding-right:1.5rem!important}.pe-xl-5{padding-right:3rem!important}.pb-xl-0{padding-bottom:0!important}.pb-xl-1{padding-bottom:.25rem!important}.pb-xl-2{padding-bottom:.5rem!important}.pb-xl-3{padding-bottom:1rem!important}.pb-xl-4{padding-bottom:1.5rem!important}.pb-xl-5{padding-bottom:3rem!important}.ps-xl-0{padding-left:0!important}.ps-xl-1{padding-left:.25rem!important}.ps-xl-2{padding-left:.5rem!important}.ps-xl-3{padding-left:1rem!important}.ps-xl-4{padding-left:1.5rem!important}.ps-xl-5{padding-left:3rem!important}.text-xl-start{text-align:left!important}.text-xl-end{text-align:right!important}.text-xl-center{text-align:center!important}}@media (min-width:1400px){.float-xxl-start{float:left!important}.float-xxl-end{float:right!important}.float-xxl-none{float:none!important}.d-xxl-inline{display:inline!important}.d-xxl-inline-block{display:inline-block!important}.d-xxl-block{display:block!important}.d-xxl-grid{display:grid!important}.d-xxl-table{display:table!important}.d-xxl-table-row{display:table-row!important}.d-xxl-table-cell{display:table-cell!important}.d-xxl-flex{display:flex!important}.d-xxl-inline-flex{display:inline-flex!important}.d-xxl-none{display:none!important}.flex-xxl-fill{flex:1 1 auto!important}.flex-xxl-row{flex-direction:row!important}.flex-xxl-column{flex-direction:column!important}.flex-xxl-row-reverse{flex-direction:row-reverse!important}.flex-xxl-column-reverse{flex-direction:column-reverse!important}.flex-xxl-grow-0{flex-grow:0!important}.flex-xxl-grow-1{flex-grow:1!important}.flex-xxl-shrink-0{flex-shrink:0!important}.flex-xxl-shrink-1{flex-shrink:1!important}.flex-xxl-wrap{flex-wrap:wrap!important}.flex-xxl-nowrap{flex-wrap:nowrap!important}.flex-xxl-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-xxl-0{gap:0!important}.gap-xxl-1{gap:.25rem!important}.gap-xxl-2{gap:.5rem!important}.gap-xxl-3{gap:1rem!important}.gap-xxl-4{gap:1.5rem!important}.gap-xxl-5{gap:3rem!important}.justify-content-xxl-start{justify-content:flex-start!important}.justify-content-xxl-end{justify-content:flex-end!important}.justify-content-xxl-center{justify-content:center!important}.justify-content-xxl-between{justify-content:space-between!important}.justify-content-xxl-around{justify-content:space-around!important}.justify-content-xxl-evenly{justify-content:space-evenly!important}.align-items-xxl-start{align-items:flex-start!important}.align-items-xxl-end{align-items:flex-end!important}.align-items-xxl-center{align-items:center!important}.align-items-xxl-baseline{align-items:baseline!important}.align-items-xxl-stretch{align-items:stretch!important}.align-content-xxl-start{align-content:flex-start!important}.align-content-xxl-end{align-content:flex-end!important}.align-content-xxl-center{align-content:center!important}.align-content-xxl-between{align-content:space-between!important}.align-content-xxl-around{align-content:space-around!important}.align-content-xxl-stretch{align-content:stretch!important}.align-self-xxl-auto{align-self:auto!important}.align-self-xxl-start{align-self:flex-start!important}.align-self-xxl-end{align-self:flex-end!important}.align-self-xxl-center{align-self:center!important}.align-self-xxl-baseline{align-self:baseline!important}.align-self-xxl-stretch{align-self:stretch!important}.order-xxl-first{order:-1!important}.order-xxl-0{order:0!important}.order-xxl-1{order:1!important}.order-xxl-2{order:2!important}.order-xxl-3{order:3!important}.order-xxl-4{order:4!important}.order-xxl-5{order:5!important}.order-xxl-last{order:6!important}.m-xxl-0{margin:0!important}.m-xxl-1{margin:.25rem!important}.m-xxl-2{margin:.5rem!important}.m-xxl-3{margin:1rem!important}.m-xxl-4{margin:1.5rem!important}.m-xxl-5{margin:3rem!important}.m-xxl-auto{margin:auto!important}.mx-xxl-0{margin-right:0!important;margin-left:0!important}.mx-xxl-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-xxl-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-xxl-3{margin-right:1rem!important;margin-left:1rem!important}.mx-xxl-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-xxl-5{margin-right:3rem!important;margin-left:3rem!important}.mx-xxl-auto{margin-right:auto!important;margin-left:auto!important}.my-xxl-0{margin-top:0!important;margin-bottom:0!important}.my-xxl-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-xxl-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-xxl-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-xxl-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-xxl-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-xxl-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-xxl-0{margin-top:0!important}.mt-xxl-1{margin-top:.25rem!important}.mt-xxl-2{margin-top:.5rem!important}.mt-xxl-3{margin-top:1rem!important}.mt-xxl-4{margin-top:1.5rem!important}.mt-xxl-5{margin-top:3rem!important}.mt-xxl-auto{margin-top:auto!important}.me-xxl-0{margin-right:0!important}.me-xxl-1{margin-right:.25rem!important}.me-xxl-2{margin-right:.5rem!important}.me-xxl-3{margin-right:1rem!important}.me-xxl-4{margin-right:1.5rem!important}.me-xxl-5{margin-right:3rem!important}.me-xxl-auto{margin-right:auto!important}.mb-xxl-0{margin-bottom:0!important}.mb-xxl-1{margin-bottom:.25rem!important}.mb-xxl-2{margin-bottom:.5rem!important}.mb-xxl-3{margin-bottom:1rem!important}.mb-xxl-4{margin-bottom:1.5rem!important}.mb-xxl-5{margin-bottom:3rem!important}.mb-xxl-auto{margin-bottom:auto!important}.ms-xxl-0{margin-left:0!important}.ms-xxl-1{margin-left:.25rem!important}.ms-xxl-2{margin-left:.5rem!important}.ms-xxl-3{margin-left:1rem!important}.ms-xxl-4{margin-left:1.5rem!important}.ms-xxl-5{margin-left:3rem!important}.ms-xxl-auto{margin-left:auto!important}.p-xxl-0{padding:0!important}.p-xxl-1{padding:.25rem!important}.p-xxl-2{padding:.5rem!important}.p-xxl-3{padding:1rem!important}.p-xxl-4{padding:1.5rem!important}.p-xxl-5{padding:3rem!important}.px-xxl-0{padding-right:0!important;padding-left:0!important}.px-xxl-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-xxl-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-xxl-3{padding-right:1rem!important;padding-left:1rem!important}.px-xxl-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-xxl-5{padding-right:3rem!important;padding-left:3rem!important}.py-xxl-0{padding-top:0!important;padding-bottom:0!important}.py-xxl-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-xxl-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-xxl-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-xxl-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-xxl-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-xxl-0{padding-top:0!important}.pt-xxl-1{padding-top:.25rem!important}.pt-xxl-2{padding-top:.5rem!important}.pt-xxl-3{padding-top:1rem!important}.pt-xxl-4{padding-top:1.5rem!important}.pt-xxl-5{padding-top:3rem!important}.pe-xxl-0{padding-right:0!important}.pe-xxl-1{padding-right:.25rem!important}.pe-xxl-2{padding-right:.5rem!important}.pe-xxl-3{padding-right:1rem!important}.pe-xxl-4{padding-right:1.5rem!important}.pe-xxl-5{padding-right:3rem!important}.pb-xxl-0{padding-bottom:0!important}.pb-xxl-1{padding-bottom:.25rem!important}.pb-xxl-2{padding-bottom:.5rem!important}.pb-xxl-3{padding-bottom:1rem!important}.pb-xxl-4{padding-bottom:1.5rem!important}.pb-xxl-5{padding-bottom:3rem!important}.ps-xxl-0{padding-left:0!important}.ps-xxl-1{padding-left:.25rem!important}.ps-xxl-2{padding-left:.5rem!important}.ps-xxl-3{padding-left:1rem!important}.ps-xxl-4{padding-left:1.5rem!important}.ps-xxl-5{padding-left:3rem!important}.text-xxl-start{text-align:left!important}.text-xxl-end{text-align:right!important}.text-xxl-center{text-align:center!important}}@media (min-width:1200px){.fs-1{font-size:2.5rem!important}.fs-2{font-size:2rem!important}.fs-3{font-size:1.75rem!important}.fs-4{font-size:1.5rem!important}}@media print{.d-print-inline{display:inline!important}.d-print-inline-block{display:inline-block!important}.d-print-block{display:block!important}.d-print-grid{display:grid!important}.d-print-table{display:table!important}.d-print-table-row{display:table-row!important}.d-print-table-cell{display:table-cell!important}.d-print-flex{display:flex!important}.d-print-inline-flex{display:inline-flex!important}.d-print-none{display:none!important}} +/*# sourceMappingURL=bootstrap.min.css.map */ \ No newline at end of file diff --git a/Streamdeck/com.mirabox.streamingle.sdPlugin/dist/com.mirabox.streamingle.sdPlugin/propertyInspector/utils/common.js b/Streamdeck/com.mirabox.streamingle.sdPlugin/dist/com.mirabox.streamingle.sdPlugin/propertyInspector/utils/common.js new file mode 100644 index 00000000..8dda3c8f --- /dev/null +++ b/Streamdeck/com.mirabox.streamingle.sdPlugin/dist/com.mirabox.streamingle.sdPlugin/propertyInspector/utils/common.js @@ -0,0 +1,81 @@ +// 自定义事件类 +class EventPlus { + constructor() { + this.event = new EventTarget(); + } + on(name, callback) { + this.event.addEventListener(name, e => callback(e.detail)); + } + send(name, data) { + this.event.dispatchEvent(new CustomEvent(name, { + detail: data, + bubbles: false, + cancelable: false + })); + } +} + +// 补零 +String.prototype.fill = function () { + return this >= 10 ? this : '0' + this; +}; + +// unicode编码转换字符串 +String.prototype.uTs = function () { + return eval('"' + Array.from(this).join('') + '"'); +}; + +// 字符串转换unicode编码 +String.prototype.sTu = function (str = '') { + Array.from(this).forEach(item => str += `\\u${item.charCodeAt(0).toString(16)}`); + return str; +}; + +// 全局变量/方法 +const $emit = new EventPlus(), $ = (selector, isAll = false) => { + const element = document.querySelector(selector), methods = { + on: function (event, callback) { + this.addEventListener(event, callback); + }, + attr: function (name, value = '') { + value && this.setAttribute(name, value); + return this; + } + }; + if (!isAll && element) { + return Object.assign(element, methods); + } else if (!isAll && !element) { + throw `HTML没有 ${selector} 元素! 请检查是否拼写错误`; + } + return Array.from(document.querySelectorAll(selector)).map(item => Object.assign(item, methods)); +}; + +// 节流函数 +$.throttle = (fn, delay) => { + let Timer = null; + return function () { + if (Timer) return; + Timer = setTimeout(() => { + fn.apply(this, arguments); + Timer = null; + }, delay); + }; +}; + +// 防抖函数 +$.debounce = (fn, delay) => { + let Timer = null; + return function () { + clearTimeout(Timer); + Timer = setTimeout(() => fn.apply(this, arguments), delay); + }; +}; + +// 绑定限制数字方法 +Array.from($('input[type="num"]', true)).forEach(item => { + item.addEventListener('input', function limitNum() { + if (!item.value || /^\d+$/.test(item.value)) return; + item.value = item.value.slice(0, -1); + limitNum(item); + }); +}); \ No newline at end of file diff --git a/Streamdeck/com.mirabox.streamingle.sdPlugin/dist/com.mirabox.streamingle.sdPlugin/propertyInspector/utils/fonts/bootstrap-icons.woff b/Streamdeck/com.mirabox.streamingle.sdPlugin/dist/com.mirabox.streamingle.sdPlugin/propertyInspector/utils/fonts/bootstrap-icons.woff new file mode 100644 index 00000000..1f5d5430 Binary files /dev/null and b/Streamdeck/com.mirabox.streamingle.sdPlugin/dist/com.mirabox.streamingle.sdPlugin/propertyInspector/utils/fonts/bootstrap-icons.woff differ diff --git a/Streamdeck/com.mirabox.streamingle.sdPlugin/dist/com.mirabox.streamingle.sdPlugin/propertyInspector/utils/fonts/bootstrap-icons.woff2 b/Streamdeck/com.mirabox.streamingle.sdPlugin/dist/com.mirabox.streamingle.sdPlugin/propertyInspector/utils/fonts/bootstrap-icons.woff2 new file mode 100644 index 00000000..b3897eff Binary files /dev/null and b/Streamdeck/com.mirabox.streamingle.sdPlugin/dist/com.mirabox.streamingle.sdPlugin/propertyInspector/utils/fonts/bootstrap-icons.woff2 differ diff --git a/Streamdeck/com.mirabox.streamingle.sdPlugin/dist/com.mirabox.streamingle.sdPlugin/streamingle_plugin b/Streamdeck/com.mirabox.streamingle.sdPlugin/dist/com.mirabox.streamingle.sdPlugin/streamingle_plugin new file mode 100644 index 00000000..04440e92 --- /dev/null +++ b/Streamdeck/com.mirabox.streamingle.sdPlugin/dist/com.mirabox.streamingle.sdPlugin/streamingle_plugin @@ -0,0 +1,9 @@ +[2025-07-03T00:52:25.811] [INFO] default - 🚀 Streamingle Camera Controller 플러그인 시작됨 +[2025-07-03T00:58:26.107] [INFO] default - 🚀 Streamingle Camera Controller 플러그인 시작됨 +[2025-07-03T00:58:37.479] [INFO] default - 🚀 Streamingle Camera Controller 플러그인 시작됨 +[2025-07-03T00:58:50.582] [INFO] default - 🚀 Streamingle Camera Controller 플러그인 시작됨 +[2025-07-03T00:59:04.912] [INFO] default - 🚀 Streamingle Camera Controller 플러그인 시작됨 +[2025-07-03T01:00:19.924] [INFO] default - 🚀 Streamingle Camera Controller 플러그인 시작됨 +[2025-07-03T01:00:32.402] [INFO] default - 🚀 Streamingle Camera Controller 플러그인 시작됨 +[2025-07-03T01:01:32.400] [INFO] default - 🚀 Streamingle Camera Controller 플러그인 시작됨 +[2025-07-03T01:02:32.414] [INFO] default - 🚀 Streamingle Camera Controller 플러그인 시작됨 diff --git a/Streamdeck/com.mirabox.streamingle.sdPlugin/dist/com.mirabox.streamingle.sdPlugin/test.js b/Streamdeck/com.mirabox.streamingle.sdPlugin/dist/com.mirabox.streamingle.sdPlugin/test.js new file mode 100644 index 00000000..0f6e2a5f --- /dev/null +++ b/Streamdeck/com.mirabox.streamingle.sdPlugin/dist/com.mirabox.streamingle.sdPlugin/test.js @@ -0,0 +1,65 @@ +const StreaminglePlugin = require('./plugin.js'); + +console.log('=== Streamingle 플러그인 테스트 시작 ==='); + +const plugin = new StreaminglePlugin(); + +// 연결 상태 모니터링 +let connectionCheckInterval = setInterval(() => { + const status = plugin.getStatus(); + console.log(`📊 연결 상태: ${status.isConnected ? '✅ 연결됨' : '❌ 연결 안됨'}`); + + if (status.isConnected) { + console.log(`📷 카메라 개수: ${status.cameraCount}개`); + console.log(`🎯 현재 카메라: ${status.currentCamera >= 0 ? status.currentCamera : '없음'}`); + + if (status.cameraList && status.cameraList.length > 0) { + console.log('📋 카메라 목록:'); + status.cameraList.forEach((camera, index) => { + console.log(` ${index}: ${camera.name} ${camera.isActive ? '[활성]' : '[비활성]'}`); + }); + } + } + + console.log('---'); +}, 5000); + +// 3초 후 카메라 목록 요청 +setTimeout(() => { + console.log('🔍 카메라 목록 요청...'); + plugin.requestCameraList(); +}, 3000); + +// 8초 후 첫 번째 카메라로 전환 +setTimeout(() => { + console.log('🎬 첫 번째 카메라로 전환...'); + plugin.switchCamera(0); +}, 8000); + +// 13초 후 두 번째 카메라로 전환 +setTimeout(() => { + console.log('🎬 두 번째 카메라로 전환...'); + plugin.switchCamera(1); +}, 13000); + +// 18초 후 세 번째 카메라로 전환 (있다면) +setTimeout(() => { + console.log('🎬 세 번째 카메라로 전환...'); + plugin.switchCamera(2); +}, 18000); + +// 25초 후 종료 +setTimeout(() => { + console.log('🛑 테스트 종료...'); + clearInterval(connectionCheckInterval); + plugin.disconnect(); + process.exit(0); +}, 25000); + +// 프로세스 종료 시 정리 +process.on('SIGINT', () => { + console.log('🛑 테스트 중단...'); + clearInterval(connectionCheckInterval); + plugin.disconnect(); + process.exit(0); +}); \ No newline at end of file diff --git a/Streamdeck/com.mirabox.streamingle.sdPlugin/images/camera_icon.png b/Streamdeck/com.mirabox.streamingle.sdPlugin/images/camera_icon.png new file mode 100644 index 00000000..a3c6082a --- /dev/null +++ b/Streamdeck/com.mirabox.streamingle.sdPlugin/images/camera_icon.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4fbe3414cbcfb70433c24f89e5258a57056de963b36e2f82042668c080d40cd7 +size 34986 diff --git a/Streamdeck/com.mirabox.streamingle.sdPlugin/images/item_icon.png b/Streamdeck/com.mirabox.streamingle.sdPlugin/images/item_icon.png new file mode 100644 index 00000000..6349c11a --- /dev/null +++ b/Streamdeck/com.mirabox.streamingle.sdPlugin/images/item_icon.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:86a617ef92c62db653e476d2519e00541effeec56b5373546c1d5d6d35708cb5 +size 45040 diff --git a/Streamdeck/com.mirabox.streamingle.sdPlugin/images/item_icon_inactive.png b/Streamdeck/com.mirabox.streamingle.sdPlugin/images/item_icon_inactive.png new file mode 100644 index 00000000..107e4b56 --- /dev/null +++ b/Streamdeck/com.mirabox.streamingle.sdPlugin/images/item_icon_inactive.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b09e01fc84d62dabc743b94607cbb601b83adcd05f1d9b839dbfa879b2e30dc6 +size 38654 diff --git a/Streamdeck/com.mirabox.streamingle.sdPlugin/images/plugin_logo.png b/Streamdeck/com.mirabox.streamingle.sdPlugin/images/plugin_logo.png new file mode 100644 index 00000000..45b93e4e --- /dev/null +++ b/Streamdeck/com.mirabox.streamingle.sdPlugin/images/plugin_logo.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:62da15b1333bcf15bb1ef33c05fb246406b76a8bf9710a4df6e7bd0a16ef62b5 +size 42151 diff --git a/Streamdeck/com.mirabox.streamingle.sdPlugin/manifest.json b/Streamdeck/com.mirabox.streamingle.sdPlugin/manifest.json new file mode 100644 index 00000000..02c1f35a --- /dev/null +++ b/Streamdeck/com.mirabox.streamingle.sdPlugin/manifest.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:26c553668887187f9f7c5272a323c8ea371635cb62e5bf72b5fc4767a14501f7 +size 2191 diff --git a/Streamdeck/com.mirabox.streamingle.sdPlugin/node_modules/.package-lock.json b/Streamdeck/com.mirabox.streamingle.sdPlugin/node_modules/.package-lock.json new file mode 100644 index 00000000..0568689f --- /dev/null +++ b/Streamdeck/com.mirabox.streamingle.sdPlugin/node_modules/.package-lock.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6979b0a8a904c35ec359bc9b6dbe1a252fa0eb961d2e85b1c3bb5e7bc6a4bc9a +size 752 diff --git a/Streamdeck/com.mirabox.streamingle.sdPlugin/node_modules/ws/LICENSE b/Streamdeck/com.mirabox.streamingle.sdPlugin/node_modules/ws/LICENSE new file mode 100644 index 00000000..1da5b96a --- /dev/null +++ b/Streamdeck/com.mirabox.streamingle.sdPlugin/node_modules/ws/LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2011 Einar Otto Stangvik +Copyright (c) 2013 Arnout Kazemier and contributors +Copyright (c) 2016 Luigi Pinca and contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/Streamdeck/com.mirabox.streamingle.sdPlugin/node_modules/ws/README.md b/Streamdeck/com.mirabox.streamingle.sdPlugin/node_modules/ws/README.md new file mode 100644 index 00000000..21282681 --- /dev/null +++ b/Streamdeck/com.mirabox.streamingle.sdPlugin/node_modules/ws/README.md @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bb979132f3cbff08ce47f36d041e18071f8f534d01f591c0b129ba7abf1e480e +size 15306 diff --git a/Streamdeck/com.mirabox.streamingle.sdPlugin/node_modules/ws/browser.js b/Streamdeck/com.mirabox.streamingle.sdPlugin/node_modules/ws/browser.js new file mode 100644 index 00000000..ca4f628a --- /dev/null +++ b/Streamdeck/com.mirabox.streamingle.sdPlugin/node_modules/ws/browser.js @@ -0,0 +1,8 @@ +'use strict'; + +module.exports = function () { + throw new Error( + 'ws does not work in the browser. Browser clients must use the native ' + + 'WebSocket object' + ); +}; diff --git a/Streamdeck/com.mirabox.streamingle.sdPlugin/node_modules/ws/index.js b/Streamdeck/com.mirabox.streamingle.sdPlugin/node_modules/ws/index.js new file mode 100644 index 00000000..41edb3b8 --- /dev/null +++ b/Streamdeck/com.mirabox.streamingle.sdPlugin/node_modules/ws/index.js @@ -0,0 +1,13 @@ +'use strict'; + +const WebSocket = require('./lib/websocket'); + +WebSocket.createWebSocketStream = require('./lib/stream'); +WebSocket.Server = require('./lib/websocket-server'); +WebSocket.Receiver = require('./lib/receiver'); +WebSocket.Sender = require('./lib/sender'); + +WebSocket.WebSocket = WebSocket; +WebSocket.WebSocketServer = WebSocket.Server; + +module.exports = WebSocket; diff --git a/Streamdeck/com.mirabox.streamingle.sdPlugin/node_modules/ws/lib/buffer-util.js b/Streamdeck/com.mirabox.streamingle.sdPlugin/node_modules/ws/lib/buffer-util.js new file mode 100644 index 00000000..f7536e28 --- /dev/null +++ b/Streamdeck/com.mirabox.streamingle.sdPlugin/node_modules/ws/lib/buffer-util.js @@ -0,0 +1,131 @@ +'use strict'; + +const { EMPTY_BUFFER } = require('./constants'); + +const FastBuffer = Buffer[Symbol.species]; + +/** + * Merges an array of buffers into a new buffer. + * + * @param {Buffer[]} list The array of buffers to concat + * @param {Number} totalLength The total length of buffers in the list + * @return {Buffer} The resulting buffer + * @public + */ +function concat(list, totalLength) { + if (list.length === 0) return EMPTY_BUFFER; + if (list.length === 1) return list[0]; + + const target = Buffer.allocUnsafe(totalLength); + let offset = 0; + + for (let i = 0; i < list.length; i++) { + const buf = list[i]; + target.set(buf, offset); + offset += buf.length; + } + + if (offset < totalLength) { + return new FastBuffer(target.buffer, target.byteOffset, offset); + } + + return target; +} + +/** + * Masks a buffer using the given mask. + * + * @param {Buffer} source The buffer to mask + * @param {Buffer} mask The mask to use + * @param {Buffer} output The buffer where to store the result + * @param {Number} offset The offset at which to start writing + * @param {Number} length The number of bytes to mask. + * @public + */ +function _mask(source, mask, output, offset, length) { + for (let i = 0; i < length; i++) { + output[offset + i] = source[i] ^ mask[i & 3]; + } +} + +/** + * Unmasks a buffer using the given mask. + * + * @param {Buffer} buffer The buffer to unmask + * @param {Buffer} mask The mask to use + * @public + */ +function _unmask(buffer, mask) { + for (let i = 0; i < buffer.length; i++) { + buffer[i] ^= mask[i & 3]; + } +} + +/** + * Converts a buffer to an `ArrayBuffer`. + * + * @param {Buffer} buf The buffer to convert + * @return {ArrayBuffer} Converted buffer + * @public + */ +function toArrayBuffer(buf) { + if (buf.length === buf.buffer.byteLength) { + return buf.buffer; + } + + return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.length); +} + +/** + * Converts `data` to a `Buffer`. + * + * @param {*} data The data to convert + * @return {Buffer} The buffer + * @throws {TypeError} + * @public + */ +function toBuffer(data) { + toBuffer.readOnly = true; + + if (Buffer.isBuffer(data)) return data; + + let buf; + + if (data instanceof ArrayBuffer) { + buf = new FastBuffer(data); + } else if (ArrayBuffer.isView(data)) { + buf = new FastBuffer(data.buffer, data.byteOffset, data.byteLength); + } else { + buf = Buffer.from(data); + toBuffer.readOnly = false; + } + + return buf; +} + +module.exports = { + concat, + mask: _mask, + toArrayBuffer, + toBuffer, + unmask: _unmask +}; + +/* istanbul ignore else */ +if (!process.env.WS_NO_BUFFER_UTIL) { + try { + const bufferUtil = require('bufferutil'); + + module.exports.mask = function (source, mask, output, offset, length) { + if (length < 48) _mask(source, mask, output, offset, length); + else bufferUtil.mask(source, mask, output, offset, length); + }; + + module.exports.unmask = function (buffer, mask) { + if (buffer.length < 32) _unmask(buffer, mask); + else bufferUtil.unmask(buffer, mask); + }; + } catch (e) { + // Continue regardless of the error. + } +} diff --git a/Streamdeck/com.mirabox.streamingle.sdPlugin/node_modules/ws/lib/constants.js b/Streamdeck/com.mirabox.streamingle.sdPlugin/node_modules/ws/lib/constants.js new file mode 100644 index 00000000..74214d46 --- /dev/null +++ b/Streamdeck/com.mirabox.streamingle.sdPlugin/node_modules/ws/lib/constants.js @@ -0,0 +1,18 @@ +'use strict'; + +const BINARY_TYPES = ['nodebuffer', 'arraybuffer', 'fragments']; +const hasBlob = typeof Blob !== 'undefined'; + +if (hasBlob) BINARY_TYPES.push('blob'); + +module.exports = { + BINARY_TYPES, + EMPTY_BUFFER: Buffer.alloc(0), + GUID: '258EAFA5-E914-47DA-95CA-C5AB0DC85B11', + hasBlob, + kForOnEventAttribute: Symbol('kIsForOnEventAttribute'), + kListener: Symbol('kListener'), + kStatusCode: Symbol('status-code'), + kWebSocket: Symbol('websocket'), + NOOP: () => {} +}; diff --git a/Streamdeck/com.mirabox.streamingle.sdPlugin/node_modules/ws/lib/event-target.js b/Streamdeck/com.mirabox.streamingle.sdPlugin/node_modules/ws/lib/event-target.js new file mode 100644 index 00000000..fea4cbc5 --- /dev/null +++ b/Streamdeck/com.mirabox.streamingle.sdPlugin/node_modules/ws/lib/event-target.js @@ -0,0 +1,292 @@ +'use strict'; + +const { kForOnEventAttribute, kListener } = require('./constants'); + +const kCode = Symbol('kCode'); +const kData = Symbol('kData'); +const kError = Symbol('kError'); +const kMessage = Symbol('kMessage'); +const kReason = Symbol('kReason'); +const kTarget = Symbol('kTarget'); +const kType = Symbol('kType'); +const kWasClean = Symbol('kWasClean'); + +/** + * Class representing an event. + */ +class Event { + /** + * Create a new `Event`. + * + * @param {String} type The name of the event + * @throws {TypeError} If the `type` argument is not specified + */ + constructor(type) { + this[kTarget] = null; + this[kType] = type; + } + + /** + * @type {*} + */ + get target() { + return this[kTarget]; + } + + /** + * @type {String} + */ + get type() { + return this[kType]; + } +} + +Object.defineProperty(Event.prototype, 'target', { enumerable: true }); +Object.defineProperty(Event.prototype, 'type', { enumerable: true }); + +/** + * Class representing a close event. + * + * @extends Event + */ +class CloseEvent extends Event { + /** + * Create a new `CloseEvent`. + * + * @param {String} type The name of the event + * @param {Object} [options] A dictionary object that allows for setting + * attributes via object members of the same name + * @param {Number} [options.code=0] The status code explaining why the + * connection was closed + * @param {String} [options.reason=''] A human-readable string explaining why + * the connection was closed + * @param {Boolean} [options.wasClean=false] Indicates whether or not the + * connection was cleanly closed + */ + constructor(type, options = {}) { + super(type); + + this[kCode] = options.code === undefined ? 0 : options.code; + this[kReason] = options.reason === undefined ? '' : options.reason; + this[kWasClean] = options.wasClean === undefined ? false : options.wasClean; + } + + /** + * @type {Number} + */ + get code() { + return this[kCode]; + } + + /** + * @type {String} + */ + get reason() { + return this[kReason]; + } + + /** + * @type {Boolean} + */ + get wasClean() { + return this[kWasClean]; + } +} + +Object.defineProperty(CloseEvent.prototype, 'code', { enumerable: true }); +Object.defineProperty(CloseEvent.prototype, 'reason', { enumerable: true }); +Object.defineProperty(CloseEvent.prototype, 'wasClean', { enumerable: true }); + +/** + * Class representing an error event. + * + * @extends Event + */ +class ErrorEvent extends Event { + /** + * Create a new `ErrorEvent`. + * + * @param {String} type The name of the event + * @param {Object} [options] A dictionary object that allows for setting + * attributes via object members of the same name + * @param {*} [options.error=null] The error that generated this event + * @param {String} [options.message=''] The error message + */ + constructor(type, options = {}) { + super(type); + + this[kError] = options.error === undefined ? null : options.error; + this[kMessage] = options.message === undefined ? '' : options.message; + } + + /** + * @type {*} + */ + get error() { + return this[kError]; + } + + /** + * @type {String} + */ + get message() { + return this[kMessage]; + } +} + +Object.defineProperty(ErrorEvent.prototype, 'error', { enumerable: true }); +Object.defineProperty(ErrorEvent.prototype, 'message', { enumerable: true }); + +/** + * Class representing a message event. + * + * @extends Event + */ +class MessageEvent extends Event { + /** + * Create a new `MessageEvent`. + * + * @param {String} type The name of the event + * @param {Object} [options] A dictionary object that allows for setting + * attributes via object members of the same name + * @param {*} [options.data=null] The message content + */ + constructor(type, options = {}) { + super(type); + + this[kData] = options.data === undefined ? null : options.data; + } + + /** + * @type {*} + */ + get data() { + return this[kData]; + } +} + +Object.defineProperty(MessageEvent.prototype, 'data', { enumerable: true }); + +/** + * This provides methods for emulating the `EventTarget` interface. It's not + * meant to be used directly. + * + * @mixin + */ +const EventTarget = { + /** + * Register an event listener. + * + * @param {String} type A string representing the event type to listen for + * @param {(Function|Object)} handler The listener to add + * @param {Object} [options] An options object specifies characteristics about + * the event listener + * @param {Boolean} [options.once=false] A `Boolean` indicating that the + * listener should be invoked at most once after being added. If `true`, + * the listener would be automatically removed when invoked. + * @public + */ + addEventListener(type, handler, options = {}) { + for (const listener of this.listeners(type)) { + if ( + !options[kForOnEventAttribute] && + listener[kListener] === handler && + !listener[kForOnEventAttribute] + ) { + return; + } + } + + let wrapper; + + if (type === 'message') { + wrapper = function onMessage(data, isBinary) { + const event = new MessageEvent('message', { + data: isBinary ? data : data.toString() + }); + + event[kTarget] = this; + callListener(handler, this, event); + }; + } else if (type === 'close') { + wrapper = function onClose(code, message) { + const event = new CloseEvent('close', { + code, + reason: message.toString(), + wasClean: this._closeFrameReceived && this._closeFrameSent + }); + + event[kTarget] = this; + callListener(handler, this, event); + }; + } else if (type === 'error') { + wrapper = function onError(error) { + const event = new ErrorEvent('error', { + error, + message: error.message + }); + + event[kTarget] = this; + callListener(handler, this, event); + }; + } else if (type === 'open') { + wrapper = function onOpen() { + const event = new Event('open'); + + event[kTarget] = this; + callListener(handler, this, event); + }; + } else { + return; + } + + wrapper[kForOnEventAttribute] = !!options[kForOnEventAttribute]; + wrapper[kListener] = handler; + + if (options.once) { + this.once(type, wrapper); + } else { + this.on(type, wrapper); + } + }, + + /** + * Remove an event listener. + * + * @param {String} type A string representing the event type to remove + * @param {(Function|Object)} handler The listener to remove + * @public + */ + removeEventListener(type, handler) { + for (const listener of this.listeners(type)) { + if (listener[kListener] === handler && !listener[kForOnEventAttribute]) { + this.removeListener(type, listener); + break; + } + } + } +}; + +module.exports = { + CloseEvent, + ErrorEvent, + Event, + EventTarget, + MessageEvent +}; + +/** + * Call an event listener + * + * @param {(Function|Object)} listener The listener to call + * @param {*} thisArg The value to use as `this`` when calling the listener + * @param {Event} event The event to pass to the listener + * @private + */ +function callListener(listener, thisArg, event) { + if (typeof listener === 'object' && listener.handleEvent) { + listener.handleEvent.call(listener, event); + } else { + listener.call(thisArg, event); + } +} diff --git a/Streamdeck/com.mirabox.streamingle.sdPlugin/node_modules/ws/lib/extension.js b/Streamdeck/com.mirabox.streamingle.sdPlugin/node_modules/ws/lib/extension.js new file mode 100644 index 00000000..3d7895c1 --- /dev/null +++ b/Streamdeck/com.mirabox.streamingle.sdPlugin/node_modules/ws/lib/extension.js @@ -0,0 +1,203 @@ +'use strict'; + +const { tokenChars } = require('./validation'); + +/** + * Adds an offer to the map of extension offers or a parameter to the map of + * parameters. + * + * @param {Object} dest The map of extension offers or parameters + * @param {String} name The extension or parameter name + * @param {(Object|Boolean|String)} elem The extension parameters or the + * parameter value + * @private + */ +function push(dest, name, elem) { + if (dest[name] === undefined) dest[name] = [elem]; + else dest[name].push(elem); +} + +/** + * Parses the `Sec-WebSocket-Extensions` header into an object. + * + * @param {String} header The field value of the header + * @return {Object} The parsed object + * @public + */ +function parse(header) { + const offers = Object.create(null); + let params = Object.create(null); + let mustUnescape = false; + let isEscaping = false; + let inQuotes = false; + let extensionName; + let paramName; + let start = -1; + let code = -1; + let end = -1; + let i = 0; + + for (; i < header.length; i++) { + code = header.charCodeAt(i); + + if (extensionName === undefined) { + if (end === -1 && tokenChars[code] === 1) { + if (start === -1) start = i; + } else if ( + i !== 0 && + (code === 0x20 /* ' ' */ || code === 0x09) /* '\t' */ + ) { + if (end === -1 && start !== -1) end = i; + } else if (code === 0x3b /* ';' */ || code === 0x2c /* ',' */) { + if (start === -1) { + throw new SyntaxError(`Unexpected character at index ${i}`); + } + + if (end === -1) end = i; + const name = header.slice(start, end); + if (code === 0x2c) { + push(offers, name, params); + params = Object.create(null); + } else { + extensionName = name; + } + + start = end = -1; + } else { + throw new SyntaxError(`Unexpected character at index ${i}`); + } + } else if (paramName === undefined) { + if (end === -1 && tokenChars[code] === 1) { + if (start === -1) start = i; + } else if (code === 0x20 || code === 0x09) { + if (end === -1 && start !== -1) end = i; + } else if (code === 0x3b || code === 0x2c) { + if (start === -1) { + throw new SyntaxError(`Unexpected character at index ${i}`); + } + + if (end === -1) end = i; + push(params, header.slice(start, end), true); + if (code === 0x2c) { + push(offers, extensionName, params); + params = Object.create(null); + extensionName = undefined; + } + + start = end = -1; + } else if (code === 0x3d /* '=' */ && start !== -1 && end === -1) { + paramName = header.slice(start, i); + start = end = -1; + } else { + throw new SyntaxError(`Unexpected character at index ${i}`); + } + } else { + // + // The value of a quoted-string after unescaping must conform to the + // token ABNF, so only token characters are valid. + // Ref: https://tools.ietf.org/html/rfc6455#section-9.1 + // + if (isEscaping) { + if (tokenChars[code] !== 1) { + throw new SyntaxError(`Unexpected character at index ${i}`); + } + if (start === -1) start = i; + else if (!mustUnescape) mustUnescape = true; + isEscaping = false; + } else if (inQuotes) { + if (tokenChars[code] === 1) { + if (start === -1) start = i; + } else if (code === 0x22 /* '"' */ && start !== -1) { + inQuotes = false; + end = i; + } else if (code === 0x5c /* '\' */) { + isEscaping = true; + } else { + throw new SyntaxError(`Unexpected character at index ${i}`); + } + } else if (code === 0x22 && header.charCodeAt(i - 1) === 0x3d) { + inQuotes = true; + } else if (end === -1 && tokenChars[code] === 1) { + if (start === -1) start = i; + } else if (start !== -1 && (code === 0x20 || code === 0x09)) { + if (end === -1) end = i; + } else if (code === 0x3b || code === 0x2c) { + if (start === -1) { + throw new SyntaxError(`Unexpected character at index ${i}`); + } + + if (end === -1) end = i; + let value = header.slice(start, end); + if (mustUnescape) { + value = value.replace(/\\/g, ''); + mustUnescape = false; + } + push(params, paramName, value); + if (code === 0x2c) { + push(offers, extensionName, params); + params = Object.create(null); + extensionName = undefined; + } + + paramName = undefined; + start = end = -1; + } else { + throw new SyntaxError(`Unexpected character at index ${i}`); + } + } + } + + if (start === -1 || inQuotes || code === 0x20 || code === 0x09) { + throw new SyntaxError('Unexpected end of input'); + } + + if (end === -1) end = i; + const token = header.slice(start, end); + if (extensionName === undefined) { + push(offers, token, params); + } else { + if (paramName === undefined) { + push(params, token, true); + } else if (mustUnescape) { + push(params, paramName, token.replace(/\\/g, '')); + } else { + push(params, paramName, token); + } + push(offers, extensionName, params); + } + + return offers; +} + +/** + * Builds the `Sec-WebSocket-Extensions` header field value. + * + * @param {Object} extensions The map of extensions and parameters to format + * @return {String} A string representing the given object + * @public + */ +function format(extensions) { + return Object.keys(extensions) + .map((extension) => { + let configurations = extensions[extension]; + if (!Array.isArray(configurations)) configurations = [configurations]; + return configurations + .map((params) => { + return [extension] + .concat( + Object.keys(params).map((k) => { + let values = params[k]; + if (!Array.isArray(values)) values = [values]; + return values + .map((v) => (v === true ? k : `${k}=${v}`)) + .join('; '); + }) + ) + .join('; '); + }) + .join(', '); + }) + .join(', '); +} + +module.exports = { format, parse }; diff --git a/Streamdeck/com.mirabox.streamingle.sdPlugin/node_modules/ws/lib/limiter.js b/Streamdeck/com.mirabox.streamingle.sdPlugin/node_modules/ws/lib/limiter.js new file mode 100644 index 00000000..3fd35784 --- /dev/null +++ b/Streamdeck/com.mirabox.streamingle.sdPlugin/node_modules/ws/lib/limiter.js @@ -0,0 +1,55 @@ +'use strict'; + +const kDone = Symbol('kDone'); +const kRun = Symbol('kRun'); + +/** + * A very simple job queue with adjustable concurrency. Adapted from + * https://github.com/STRML/async-limiter + */ +class Limiter { + /** + * Creates a new `Limiter`. + * + * @param {Number} [concurrency=Infinity] The maximum number of jobs allowed + * to run concurrently + */ + constructor(concurrency) { + this[kDone] = () => { + this.pending--; + this[kRun](); + }; + this.concurrency = concurrency || Infinity; + this.jobs = []; + this.pending = 0; + } + + /** + * Adds a job to the queue. + * + * @param {Function} job The job to run + * @public + */ + add(job) { + this.jobs.push(job); + this[kRun](); + } + + /** + * Removes a job from the queue and runs it if possible. + * + * @private + */ + [kRun]() { + if (this.pending === this.concurrency) return; + + if (this.jobs.length) { + const job = this.jobs.shift(); + + this.pending++; + job(this[kDone]); + } + } +} + +module.exports = Limiter; diff --git a/Streamdeck/com.mirabox.streamingle.sdPlugin/node_modules/ws/lib/permessage-deflate.js b/Streamdeck/com.mirabox.streamingle.sdPlugin/node_modules/ws/lib/permessage-deflate.js new file mode 100644 index 00000000..41ff70e2 --- /dev/null +++ b/Streamdeck/com.mirabox.streamingle.sdPlugin/node_modules/ws/lib/permessage-deflate.js @@ -0,0 +1,528 @@ +'use strict'; + +const zlib = require('zlib'); + +const bufferUtil = require('./buffer-util'); +const Limiter = require('./limiter'); +const { kStatusCode } = require('./constants'); + +const FastBuffer = Buffer[Symbol.species]; +const TRAILER = Buffer.from([0x00, 0x00, 0xff, 0xff]); +const kPerMessageDeflate = Symbol('permessage-deflate'); +const kTotalLength = Symbol('total-length'); +const kCallback = Symbol('callback'); +const kBuffers = Symbol('buffers'); +const kError = Symbol('error'); + +// +// We limit zlib concurrency, which prevents severe memory fragmentation +// as documented in https://github.com/nodejs/node/issues/8871#issuecomment-250915913 +// and https://github.com/websockets/ws/issues/1202 +// +// Intentionally global; it's the global thread pool that's an issue. +// +let zlibLimiter; + +/** + * permessage-deflate implementation. + */ +class PerMessageDeflate { + /** + * Creates a PerMessageDeflate instance. + * + * @param {Object} [options] Configuration options + * @param {(Boolean|Number)} [options.clientMaxWindowBits] Advertise support + * for, or request, a custom client window size + * @param {Boolean} [options.clientNoContextTakeover=false] Advertise/ + * acknowledge disabling of client context takeover + * @param {Number} [options.concurrencyLimit=10] The number of concurrent + * calls to zlib + * @param {(Boolean|Number)} [options.serverMaxWindowBits] Request/confirm the + * use of a custom server window size + * @param {Boolean} [options.serverNoContextTakeover=false] Request/accept + * disabling of server context takeover + * @param {Number} [options.threshold=1024] Size (in bytes) below which + * messages should not be compressed if context takeover is disabled + * @param {Object} [options.zlibDeflateOptions] Options to pass to zlib on + * deflate + * @param {Object} [options.zlibInflateOptions] Options to pass to zlib on + * inflate + * @param {Boolean} [isServer=false] Create the instance in either server or + * client mode + * @param {Number} [maxPayload=0] The maximum allowed message length + */ + constructor(options, isServer, maxPayload) { + this._maxPayload = maxPayload | 0; + this._options = options || {}; + this._threshold = + this._options.threshold !== undefined ? this._options.threshold : 1024; + this._isServer = !!isServer; + this._deflate = null; + this._inflate = null; + + this.params = null; + + if (!zlibLimiter) { + const concurrency = + this._options.concurrencyLimit !== undefined + ? this._options.concurrencyLimit + : 10; + zlibLimiter = new Limiter(concurrency); + } + } + + /** + * @type {String} + */ + static get extensionName() { + return 'permessage-deflate'; + } + + /** + * Create an extension negotiation offer. + * + * @return {Object} Extension parameters + * @public + */ + offer() { + const params = {}; + + if (this._options.serverNoContextTakeover) { + params.server_no_context_takeover = true; + } + if (this._options.clientNoContextTakeover) { + params.client_no_context_takeover = true; + } + if (this._options.serverMaxWindowBits) { + params.server_max_window_bits = this._options.serverMaxWindowBits; + } + if (this._options.clientMaxWindowBits) { + params.client_max_window_bits = this._options.clientMaxWindowBits; + } else if (this._options.clientMaxWindowBits == null) { + params.client_max_window_bits = true; + } + + return params; + } + + /** + * Accept an extension negotiation offer/response. + * + * @param {Array} configurations The extension negotiation offers/reponse + * @return {Object} Accepted configuration + * @public + */ + accept(configurations) { + configurations = this.normalizeParams(configurations); + + this.params = this._isServer + ? this.acceptAsServer(configurations) + : this.acceptAsClient(configurations); + + return this.params; + } + + /** + * Releases all resources used by the extension. + * + * @public + */ + cleanup() { + if (this._inflate) { + this._inflate.close(); + this._inflate = null; + } + + if (this._deflate) { + const callback = this._deflate[kCallback]; + + this._deflate.close(); + this._deflate = null; + + if (callback) { + callback( + new Error( + 'The deflate stream was closed while data was being processed' + ) + ); + } + } + } + + /** + * Accept an extension negotiation offer. + * + * @param {Array} offers The extension negotiation offers + * @return {Object} Accepted configuration + * @private + */ + acceptAsServer(offers) { + const opts = this._options; + const accepted = offers.find((params) => { + if ( + (opts.serverNoContextTakeover === false && + params.server_no_context_takeover) || + (params.server_max_window_bits && + (opts.serverMaxWindowBits === false || + (typeof opts.serverMaxWindowBits === 'number' && + opts.serverMaxWindowBits > params.server_max_window_bits))) || + (typeof opts.clientMaxWindowBits === 'number' && + !params.client_max_window_bits) + ) { + return false; + } + + return true; + }); + + if (!accepted) { + throw new Error('None of the extension offers can be accepted'); + } + + if (opts.serverNoContextTakeover) { + accepted.server_no_context_takeover = true; + } + if (opts.clientNoContextTakeover) { + accepted.client_no_context_takeover = true; + } + if (typeof opts.serverMaxWindowBits === 'number') { + accepted.server_max_window_bits = opts.serverMaxWindowBits; + } + if (typeof opts.clientMaxWindowBits === 'number') { + accepted.client_max_window_bits = opts.clientMaxWindowBits; + } else if ( + accepted.client_max_window_bits === true || + opts.clientMaxWindowBits === false + ) { + delete accepted.client_max_window_bits; + } + + return accepted; + } + + /** + * Accept the extension negotiation response. + * + * @param {Array} response The extension negotiation response + * @return {Object} Accepted configuration + * @private + */ + acceptAsClient(response) { + const params = response[0]; + + if ( + this._options.clientNoContextTakeover === false && + params.client_no_context_takeover + ) { + throw new Error('Unexpected parameter "client_no_context_takeover"'); + } + + if (!params.client_max_window_bits) { + if (typeof this._options.clientMaxWindowBits === 'number') { + params.client_max_window_bits = this._options.clientMaxWindowBits; + } + } else if ( + this._options.clientMaxWindowBits === false || + (typeof this._options.clientMaxWindowBits === 'number' && + params.client_max_window_bits > this._options.clientMaxWindowBits) + ) { + throw new Error( + 'Unexpected or invalid parameter "client_max_window_bits"' + ); + } + + return params; + } + + /** + * Normalize parameters. + * + * @param {Array} configurations The extension negotiation offers/reponse + * @return {Array} The offers/response with normalized parameters + * @private + */ + normalizeParams(configurations) { + configurations.forEach((params) => { + Object.keys(params).forEach((key) => { + let value = params[key]; + + if (value.length > 1) { + throw new Error(`Parameter "${key}" must have only a single value`); + } + + value = value[0]; + + if (key === 'client_max_window_bits') { + if (value !== true) { + const num = +value; + if (!Number.isInteger(num) || num < 8 || num > 15) { + throw new TypeError( + `Invalid value for parameter "${key}": ${value}` + ); + } + value = num; + } else if (!this._isServer) { + throw new TypeError( + `Invalid value for parameter "${key}": ${value}` + ); + } + } else if (key === 'server_max_window_bits') { + const num = +value; + if (!Number.isInteger(num) || num < 8 || num > 15) { + throw new TypeError( + `Invalid value for parameter "${key}": ${value}` + ); + } + value = num; + } else if ( + key === 'client_no_context_takeover' || + key === 'server_no_context_takeover' + ) { + if (value !== true) { + throw new TypeError( + `Invalid value for parameter "${key}": ${value}` + ); + } + } else { + throw new Error(`Unknown parameter "${key}"`); + } + + params[key] = value; + }); + }); + + return configurations; + } + + /** + * Decompress data. Concurrency limited. + * + * @param {Buffer} data Compressed data + * @param {Boolean} fin Specifies whether or not this is the last fragment + * @param {Function} callback Callback + * @public + */ + decompress(data, fin, callback) { + zlibLimiter.add((done) => { + this._decompress(data, fin, (err, result) => { + done(); + callback(err, result); + }); + }); + } + + /** + * Compress data. Concurrency limited. + * + * @param {(Buffer|String)} data Data to compress + * @param {Boolean} fin Specifies whether or not this is the last fragment + * @param {Function} callback Callback + * @public + */ + compress(data, fin, callback) { + zlibLimiter.add((done) => { + this._compress(data, fin, (err, result) => { + done(); + callback(err, result); + }); + }); + } + + /** + * Decompress data. + * + * @param {Buffer} data Compressed data + * @param {Boolean} fin Specifies whether or not this is the last fragment + * @param {Function} callback Callback + * @private + */ + _decompress(data, fin, callback) { + const endpoint = this._isServer ? 'client' : 'server'; + + if (!this._inflate) { + const key = `${endpoint}_max_window_bits`; + const windowBits = + typeof this.params[key] !== 'number' + ? zlib.Z_DEFAULT_WINDOWBITS + : this.params[key]; + + this._inflate = zlib.createInflateRaw({ + ...this._options.zlibInflateOptions, + windowBits + }); + this._inflate[kPerMessageDeflate] = this; + this._inflate[kTotalLength] = 0; + this._inflate[kBuffers] = []; + this._inflate.on('error', inflateOnError); + this._inflate.on('data', inflateOnData); + } + + this._inflate[kCallback] = callback; + + this._inflate.write(data); + if (fin) this._inflate.write(TRAILER); + + this._inflate.flush(() => { + const err = this._inflate[kError]; + + if (err) { + this._inflate.close(); + this._inflate = null; + callback(err); + return; + } + + const data = bufferUtil.concat( + this._inflate[kBuffers], + this._inflate[kTotalLength] + ); + + if (this._inflate._readableState.endEmitted) { + this._inflate.close(); + this._inflate = null; + } else { + this._inflate[kTotalLength] = 0; + this._inflate[kBuffers] = []; + + if (fin && this.params[`${endpoint}_no_context_takeover`]) { + this._inflate.reset(); + } + } + + callback(null, data); + }); + } + + /** + * Compress data. + * + * @param {(Buffer|String)} data Data to compress + * @param {Boolean} fin Specifies whether or not this is the last fragment + * @param {Function} callback Callback + * @private + */ + _compress(data, fin, callback) { + const endpoint = this._isServer ? 'server' : 'client'; + + if (!this._deflate) { + const key = `${endpoint}_max_window_bits`; + const windowBits = + typeof this.params[key] !== 'number' + ? zlib.Z_DEFAULT_WINDOWBITS + : this.params[key]; + + this._deflate = zlib.createDeflateRaw({ + ...this._options.zlibDeflateOptions, + windowBits + }); + + this._deflate[kTotalLength] = 0; + this._deflate[kBuffers] = []; + + this._deflate.on('data', deflateOnData); + } + + this._deflate[kCallback] = callback; + + this._deflate.write(data); + this._deflate.flush(zlib.Z_SYNC_FLUSH, () => { + if (!this._deflate) { + // + // The deflate stream was closed while data was being processed. + // + return; + } + + let data = bufferUtil.concat( + this._deflate[kBuffers], + this._deflate[kTotalLength] + ); + + if (fin) { + data = new FastBuffer(data.buffer, data.byteOffset, data.length - 4); + } + + // + // Ensure that the callback will not be called again in + // `PerMessageDeflate#cleanup()`. + // + this._deflate[kCallback] = null; + + this._deflate[kTotalLength] = 0; + this._deflate[kBuffers] = []; + + if (fin && this.params[`${endpoint}_no_context_takeover`]) { + this._deflate.reset(); + } + + callback(null, data); + }); + } +} + +module.exports = PerMessageDeflate; + +/** + * The listener of the `zlib.DeflateRaw` stream `'data'` event. + * + * @param {Buffer} chunk A chunk of data + * @private + */ +function deflateOnData(chunk) { + this[kBuffers].push(chunk); + this[kTotalLength] += chunk.length; +} + +/** + * The listener of the `zlib.InflateRaw` stream `'data'` event. + * + * @param {Buffer} chunk A chunk of data + * @private + */ +function inflateOnData(chunk) { + this[kTotalLength] += chunk.length; + + if ( + this[kPerMessageDeflate]._maxPayload < 1 || + this[kTotalLength] <= this[kPerMessageDeflate]._maxPayload + ) { + this[kBuffers].push(chunk); + return; + } + + this[kError] = new RangeError('Max payload size exceeded'); + this[kError].code = 'WS_ERR_UNSUPPORTED_MESSAGE_LENGTH'; + this[kError][kStatusCode] = 1009; + this.removeListener('data', inflateOnData); + + // + // The choice to employ `zlib.reset()` over `zlib.close()` is dictated by the + // fact that in Node.js versions prior to 13.10.0, the callback for + // `zlib.flush()` is not called if `zlib.close()` is used. Utilizing + // `zlib.reset()` ensures that either the callback is invoked or an error is + // emitted. + // + this.reset(); +} + +/** + * The listener of the `zlib.InflateRaw` stream `'error'` event. + * + * @param {Error} err The emitted error + * @private + */ +function inflateOnError(err) { + // + // There is no need to call `Zlib#close()` as the handle is automatically + // closed when an error is emitted. + // + this[kPerMessageDeflate]._inflate = null; + + if (this[kError]) { + this[kCallback](this[kError]); + return; + } + + err[kStatusCode] = 1007; + this[kCallback](err); +} diff --git a/Streamdeck/com.mirabox.streamingle.sdPlugin/node_modules/ws/lib/receiver.js b/Streamdeck/com.mirabox.streamingle.sdPlugin/node_modules/ws/lib/receiver.js new file mode 100644 index 00000000..54d9b4fa --- /dev/null +++ b/Streamdeck/com.mirabox.streamingle.sdPlugin/node_modules/ws/lib/receiver.js @@ -0,0 +1,706 @@ +'use strict'; + +const { Writable } = require('stream'); + +const PerMessageDeflate = require('./permessage-deflate'); +const { + BINARY_TYPES, + EMPTY_BUFFER, + kStatusCode, + kWebSocket +} = require('./constants'); +const { concat, toArrayBuffer, unmask } = require('./buffer-util'); +const { isValidStatusCode, isValidUTF8 } = require('./validation'); + +const FastBuffer = Buffer[Symbol.species]; + +const GET_INFO = 0; +const GET_PAYLOAD_LENGTH_16 = 1; +const GET_PAYLOAD_LENGTH_64 = 2; +const GET_MASK = 3; +const GET_DATA = 4; +const INFLATING = 5; +const DEFER_EVENT = 6; + +/** + * HyBi Receiver implementation. + * + * @extends Writable + */ +class Receiver extends Writable { + /** + * Creates a Receiver instance. + * + * @param {Object} [options] Options object + * @param {Boolean} [options.allowSynchronousEvents=true] Specifies whether + * any of the `'message'`, `'ping'`, and `'pong'` events can be emitted + * multiple times in the same tick + * @param {String} [options.binaryType=nodebuffer] The type for binary data + * @param {Object} [options.extensions] An object containing the negotiated + * extensions + * @param {Boolean} [options.isServer=false] Specifies whether to operate in + * client or server mode + * @param {Number} [options.maxPayload=0] The maximum allowed message length + * @param {Boolean} [options.skipUTF8Validation=false] Specifies whether or + * not to skip UTF-8 validation for text and close messages + */ + constructor(options = {}) { + super(); + + this._allowSynchronousEvents = + options.allowSynchronousEvents !== undefined + ? options.allowSynchronousEvents + : true; + this._binaryType = options.binaryType || BINARY_TYPES[0]; + this._extensions = options.extensions || {}; + this._isServer = !!options.isServer; + this._maxPayload = options.maxPayload | 0; + this._skipUTF8Validation = !!options.skipUTF8Validation; + this[kWebSocket] = undefined; + + this._bufferedBytes = 0; + this._buffers = []; + + this._compressed = false; + this._payloadLength = 0; + this._mask = undefined; + this._fragmented = 0; + this._masked = false; + this._fin = false; + this._opcode = 0; + + this._totalPayloadLength = 0; + this._messageLength = 0; + this._fragments = []; + + this._errored = false; + this._loop = false; + this._state = GET_INFO; + } + + /** + * Implements `Writable.prototype._write()`. + * + * @param {Buffer} chunk The chunk of data to write + * @param {String} encoding The character encoding of `chunk` + * @param {Function} cb Callback + * @private + */ + _write(chunk, encoding, cb) { + if (this._opcode === 0x08 && this._state == GET_INFO) return cb(); + + this._bufferedBytes += chunk.length; + this._buffers.push(chunk); + this.startLoop(cb); + } + + /** + * Consumes `n` bytes from the buffered data. + * + * @param {Number} n The number of bytes to consume + * @return {Buffer} The consumed bytes + * @private + */ + consume(n) { + this._bufferedBytes -= n; + + if (n === this._buffers[0].length) return this._buffers.shift(); + + if (n < this._buffers[0].length) { + const buf = this._buffers[0]; + this._buffers[0] = new FastBuffer( + buf.buffer, + buf.byteOffset + n, + buf.length - n + ); + + return new FastBuffer(buf.buffer, buf.byteOffset, n); + } + + const dst = Buffer.allocUnsafe(n); + + do { + const buf = this._buffers[0]; + const offset = dst.length - n; + + if (n >= buf.length) { + dst.set(this._buffers.shift(), offset); + } else { + dst.set(new Uint8Array(buf.buffer, buf.byteOffset, n), offset); + this._buffers[0] = new FastBuffer( + buf.buffer, + buf.byteOffset + n, + buf.length - n + ); + } + + n -= buf.length; + } while (n > 0); + + return dst; + } + + /** + * Starts the parsing loop. + * + * @param {Function} cb Callback + * @private + */ + startLoop(cb) { + this._loop = true; + + do { + switch (this._state) { + case GET_INFO: + this.getInfo(cb); + break; + case GET_PAYLOAD_LENGTH_16: + this.getPayloadLength16(cb); + break; + case GET_PAYLOAD_LENGTH_64: + this.getPayloadLength64(cb); + break; + case GET_MASK: + this.getMask(); + break; + case GET_DATA: + this.getData(cb); + break; + case INFLATING: + case DEFER_EVENT: + this._loop = false; + return; + } + } while (this._loop); + + if (!this._errored) cb(); + } + + /** + * Reads the first two bytes of a frame. + * + * @param {Function} cb Callback + * @private + */ + getInfo(cb) { + if (this._bufferedBytes < 2) { + this._loop = false; + return; + } + + const buf = this.consume(2); + + if ((buf[0] & 0x30) !== 0x00) { + const error = this.createError( + RangeError, + 'RSV2 and RSV3 must be clear', + true, + 1002, + 'WS_ERR_UNEXPECTED_RSV_2_3' + ); + + cb(error); + return; + } + + const compressed = (buf[0] & 0x40) === 0x40; + + if (compressed && !this._extensions[PerMessageDeflate.extensionName]) { + const error = this.createError( + RangeError, + 'RSV1 must be clear', + true, + 1002, + 'WS_ERR_UNEXPECTED_RSV_1' + ); + + cb(error); + return; + } + + this._fin = (buf[0] & 0x80) === 0x80; + this._opcode = buf[0] & 0x0f; + this._payloadLength = buf[1] & 0x7f; + + if (this._opcode === 0x00) { + if (compressed) { + const error = this.createError( + RangeError, + 'RSV1 must be clear', + true, + 1002, + 'WS_ERR_UNEXPECTED_RSV_1' + ); + + cb(error); + return; + } + + if (!this._fragmented) { + const error = this.createError( + RangeError, + 'invalid opcode 0', + true, + 1002, + 'WS_ERR_INVALID_OPCODE' + ); + + cb(error); + return; + } + + this._opcode = this._fragmented; + } else if (this._opcode === 0x01 || this._opcode === 0x02) { + if (this._fragmented) { + const error = this.createError( + RangeError, + `invalid opcode ${this._opcode}`, + true, + 1002, + 'WS_ERR_INVALID_OPCODE' + ); + + cb(error); + return; + } + + this._compressed = compressed; + } else if (this._opcode > 0x07 && this._opcode < 0x0b) { + if (!this._fin) { + const error = this.createError( + RangeError, + 'FIN must be set', + true, + 1002, + 'WS_ERR_EXPECTED_FIN' + ); + + cb(error); + return; + } + + if (compressed) { + const error = this.createError( + RangeError, + 'RSV1 must be clear', + true, + 1002, + 'WS_ERR_UNEXPECTED_RSV_1' + ); + + cb(error); + return; + } + + if ( + this._payloadLength > 0x7d || + (this._opcode === 0x08 && this._payloadLength === 1) + ) { + const error = this.createError( + RangeError, + `invalid payload length ${this._payloadLength}`, + true, + 1002, + 'WS_ERR_INVALID_CONTROL_PAYLOAD_LENGTH' + ); + + cb(error); + return; + } + } else { + const error = this.createError( + RangeError, + `invalid opcode ${this._opcode}`, + true, + 1002, + 'WS_ERR_INVALID_OPCODE' + ); + + cb(error); + return; + } + + if (!this._fin && !this._fragmented) this._fragmented = this._opcode; + this._masked = (buf[1] & 0x80) === 0x80; + + if (this._isServer) { + if (!this._masked) { + const error = this.createError( + RangeError, + 'MASK must be set', + true, + 1002, + 'WS_ERR_EXPECTED_MASK' + ); + + cb(error); + return; + } + } else if (this._masked) { + const error = this.createError( + RangeError, + 'MASK must be clear', + true, + 1002, + 'WS_ERR_UNEXPECTED_MASK' + ); + + cb(error); + return; + } + + if (this._payloadLength === 126) this._state = GET_PAYLOAD_LENGTH_16; + else if (this._payloadLength === 127) this._state = GET_PAYLOAD_LENGTH_64; + else this.haveLength(cb); + } + + /** + * Gets extended payload length (7+16). + * + * @param {Function} cb Callback + * @private + */ + getPayloadLength16(cb) { + if (this._bufferedBytes < 2) { + this._loop = false; + return; + } + + this._payloadLength = this.consume(2).readUInt16BE(0); + this.haveLength(cb); + } + + /** + * Gets extended payload length (7+64). + * + * @param {Function} cb Callback + * @private + */ + getPayloadLength64(cb) { + if (this._bufferedBytes < 8) { + this._loop = false; + return; + } + + const buf = this.consume(8); + const num = buf.readUInt32BE(0); + + // + // The maximum safe integer in JavaScript is 2^53 - 1. An error is returned + // if payload length is greater than this number. + // + if (num > Math.pow(2, 53 - 32) - 1) { + const error = this.createError( + RangeError, + 'Unsupported WebSocket frame: payload length > 2^53 - 1', + false, + 1009, + 'WS_ERR_UNSUPPORTED_DATA_PAYLOAD_LENGTH' + ); + + cb(error); + return; + } + + this._payloadLength = num * Math.pow(2, 32) + buf.readUInt32BE(4); + this.haveLength(cb); + } + + /** + * Payload length has been read. + * + * @param {Function} cb Callback + * @private + */ + haveLength(cb) { + if (this._payloadLength && this._opcode < 0x08) { + this._totalPayloadLength += this._payloadLength; + if (this._totalPayloadLength > this._maxPayload && this._maxPayload > 0) { + const error = this.createError( + RangeError, + 'Max payload size exceeded', + false, + 1009, + 'WS_ERR_UNSUPPORTED_MESSAGE_LENGTH' + ); + + cb(error); + return; + } + } + + if (this._masked) this._state = GET_MASK; + else this._state = GET_DATA; + } + + /** + * Reads mask bytes. + * + * @private + */ + getMask() { + if (this._bufferedBytes < 4) { + this._loop = false; + return; + } + + this._mask = this.consume(4); + this._state = GET_DATA; + } + + /** + * Reads data bytes. + * + * @param {Function} cb Callback + * @private + */ + getData(cb) { + let data = EMPTY_BUFFER; + + if (this._payloadLength) { + if (this._bufferedBytes < this._payloadLength) { + this._loop = false; + return; + } + + data = this.consume(this._payloadLength); + + if ( + this._masked && + (this._mask[0] | this._mask[1] | this._mask[2] | this._mask[3]) !== 0 + ) { + unmask(data, this._mask); + } + } + + if (this._opcode > 0x07) { + this.controlMessage(data, cb); + return; + } + + if (this._compressed) { + this._state = INFLATING; + this.decompress(data, cb); + return; + } + + if (data.length) { + // + // This message is not compressed so its length is the sum of the payload + // length of all fragments. + // + this._messageLength = this._totalPayloadLength; + this._fragments.push(data); + } + + this.dataMessage(cb); + } + + /** + * Decompresses data. + * + * @param {Buffer} data Compressed data + * @param {Function} cb Callback + * @private + */ + decompress(data, cb) { + const perMessageDeflate = this._extensions[PerMessageDeflate.extensionName]; + + perMessageDeflate.decompress(data, this._fin, (err, buf) => { + if (err) return cb(err); + + if (buf.length) { + this._messageLength += buf.length; + if (this._messageLength > this._maxPayload && this._maxPayload > 0) { + const error = this.createError( + RangeError, + 'Max payload size exceeded', + false, + 1009, + 'WS_ERR_UNSUPPORTED_MESSAGE_LENGTH' + ); + + cb(error); + return; + } + + this._fragments.push(buf); + } + + this.dataMessage(cb); + if (this._state === GET_INFO) this.startLoop(cb); + }); + } + + /** + * Handles a data message. + * + * @param {Function} cb Callback + * @private + */ + dataMessage(cb) { + if (!this._fin) { + this._state = GET_INFO; + return; + } + + const messageLength = this._messageLength; + const fragments = this._fragments; + + this._totalPayloadLength = 0; + this._messageLength = 0; + this._fragmented = 0; + this._fragments = []; + + if (this._opcode === 2) { + let data; + + if (this._binaryType === 'nodebuffer') { + data = concat(fragments, messageLength); + } else if (this._binaryType === 'arraybuffer') { + data = toArrayBuffer(concat(fragments, messageLength)); + } else if (this._binaryType === 'blob') { + data = new Blob(fragments); + } else { + data = fragments; + } + + if (this._allowSynchronousEvents) { + this.emit('message', data, true); + this._state = GET_INFO; + } else { + this._state = DEFER_EVENT; + setImmediate(() => { + this.emit('message', data, true); + this._state = GET_INFO; + this.startLoop(cb); + }); + } + } else { + const buf = concat(fragments, messageLength); + + if (!this._skipUTF8Validation && !isValidUTF8(buf)) { + const error = this.createError( + Error, + 'invalid UTF-8 sequence', + true, + 1007, + 'WS_ERR_INVALID_UTF8' + ); + + cb(error); + return; + } + + if (this._state === INFLATING || this._allowSynchronousEvents) { + this.emit('message', buf, false); + this._state = GET_INFO; + } else { + this._state = DEFER_EVENT; + setImmediate(() => { + this.emit('message', buf, false); + this._state = GET_INFO; + this.startLoop(cb); + }); + } + } + } + + /** + * Handles a control message. + * + * @param {Buffer} data Data to handle + * @return {(Error|RangeError|undefined)} A possible error + * @private + */ + controlMessage(data, cb) { + if (this._opcode === 0x08) { + if (data.length === 0) { + this._loop = false; + this.emit('conclude', 1005, EMPTY_BUFFER); + this.end(); + } else { + const code = data.readUInt16BE(0); + + if (!isValidStatusCode(code)) { + const error = this.createError( + RangeError, + `invalid status code ${code}`, + true, + 1002, + 'WS_ERR_INVALID_CLOSE_CODE' + ); + + cb(error); + return; + } + + const buf = new FastBuffer( + data.buffer, + data.byteOffset + 2, + data.length - 2 + ); + + if (!this._skipUTF8Validation && !isValidUTF8(buf)) { + const error = this.createError( + Error, + 'invalid UTF-8 sequence', + true, + 1007, + 'WS_ERR_INVALID_UTF8' + ); + + cb(error); + return; + } + + this._loop = false; + this.emit('conclude', code, buf); + this.end(); + } + + this._state = GET_INFO; + return; + } + + if (this._allowSynchronousEvents) { + this.emit(this._opcode === 0x09 ? 'ping' : 'pong', data); + this._state = GET_INFO; + } else { + this._state = DEFER_EVENT; + setImmediate(() => { + this.emit(this._opcode === 0x09 ? 'ping' : 'pong', data); + this._state = GET_INFO; + this.startLoop(cb); + }); + } + } + + /** + * Builds an error object. + * + * @param {function(new:Error|RangeError)} ErrorCtor The error constructor + * @param {String} message The error message + * @param {Boolean} prefix Specifies whether or not to add a default prefix to + * `message` + * @param {Number} statusCode The status code + * @param {String} errorCode The exposed error code + * @return {(Error|RangeError)} The error + * @private + */ + createError(ErrorCtor, message, prefix, statusCode, errorCode) { + this._loop = false; + this._errored = true; + + const err = new ErrorCtor( + prefix ? `Invalid WebSocket frame: ${message}` : message + ); + + Error.captureStackTrace(err, this.createError); + err.code = errorCode; + err[kStatusCode] = statusCode; + return err; + } +} + +module.exports = Receiver; diff --git a/Streamdeck/com.mirabox.streamingle.sdPlugin/node_modules/ws/lib/sender.js b/Streamdeck/com.mirabox.streamingle.sdPlugin/node_modules/ws/lib/sender.js new file mode 100644 index 00000000..a8b1da3a --- /dev/null +++ b/Streamdeck/com.mirabox.streamingle.sdPlugin/node_modules/ws/lib/sender.js @@ -0,0 +1,602 @@ +/* eslint no-unused-vars: ["error", { "varsIgnorePattern": "^Duplex" }] */ + +'use strict'; + +const { Duplex } = require('stream'); +const { randomFillSync } = require('crypto'); + +const PerMessageDeflate = require('./permessage-deflate'); +const { EMPTY_BUFFER, kWebSocket, NOOP } = require('./constants'); +const { isBlob, isValidStatusCode } = require('./validation'); +const { mask: applyMask, toBuffer } = require('./buffer-util'); + +const kByteLength = Symbol('kByteLength'); +const maskBuffer = Buffer.alloc(4); +const RANDOM_POOL_SIZE = 8 * 1024; +let randomPool; +let randomPoolPointer = RANDOM_POOL_SIZE; + +const DEFAULT = 0; +const DEFLATING = 1; +const GET_BLOB_DATA = 2; + +/** + * HyBi Sender implementation. + */ +class Sender { + /** + * Creates a Sender instance. + * + * @param {Duplex} socket The connection socket + * @param {Object} [extensions] An object containing the negotiated extensions + * @param {Function} [generateMask] The function used to generate the masking + * key + */ + constructor(socket, extensions, generateMask) { + this._extensions = extensions || {}; + + if (generateMask) { + this._generateMask = generateMask; + this._maskBuffer = Buffer.alloc(4); + } + + this._socket = socket; + + this._firstFragment = true; + this._compress = false; + + this._bufferedBytes = 0; + this._queue = []; + this._state = DEFAULT; + this.onerror = NOOP; + this[kWebSocket] = undefined; + } + + /** + * Frames a piece of data according to the HyBi WebSocket protocol. + * + * @param {(Buffer|String)} data The data to frame + * @param {Object} options Options object + * @param {Boolean} [options.fin=false] Specifies whether or not to set the + * FIN bit + * @param {Function} [options.generateMask] The function used to generate the + * masking key + * @param {Boolean} [options.mask=false] Specifies whether or not to mask + * `data` + * @param {Buffer} [options.maskBuffer] The buffer used to store the masking + * key + * @param {Number} options.opcode The opcode + * @param {Boolean} [options.readOnly=false] Specifies whether `data` can be + * modified + * @param {Boolean} [options.rsv1=false] Specifies whether or not to set the + * RSV1 bit + * @return {(Buffer|String)[]} The framed data + * @public + */ + static frame(data, options) { + let mask; + let merge = false; + let offset = 2; + let skipMasking = false; + + if (options.mask) { + mask = options.maskBuffer || maskBuffer; + + if (options.generateMask) { + options.generateMask(mask); + } else { + if (randomPoolPointer === RANDOM_POOL_SIZE) { + /* istanbul ignore else */ + if (randomPool === undefined) { + // + // This is lazily initialized because server-sent frames must not + // be masked so it may never be used. + // + randomPool = Buffer.alloc(RANDOM_POOL_SIZE); + } + + randomFillSync(randomPool, 0, RANDOM_POOL_SIZE); + randomPoolPointer = 0; + } + + mask[0] = randomPool[randomPoolPointer++]; + mask[1] = randomPool[randomPoolPointer++]; + mask[2] = randomPool[randomPoolPointer++]; + mask[3] = randomPool[randomPoolPointer++]; + } + + skipMasking = (mask[0] | mask[1] | mask[2] | mask[3]) === 0; + offset = 6; + } + + let dataLength; + + if (typeof data === 'string') { + if ( + (!options.mask || skipMasking) && + options[kByteLength] !== undefined + ) { + dataLength = options[kByteLength]; + } else { + data = Buffer.from(data); + dataLength = data.length; + } + } else { + dataLength = data.length; + merge = options.mask && options.readOnly && !skipMasking; + } + + let payloadLength = dataLength; + + if (dataLength >= 65536) { + offset += 8; + payloadLength = 127; + } else if (dataLength > 125) { + offset += 2; + payloadLength = 126; + } + + const target = Buffer.allocUnsafe(merge ? dataLength + offset : offset); + + target[0] = options.fin ? options.opcode | 0x80 : options.opcode; + if (options.rsv1) target[0] |= 0x40; + + target[1] = payloadLength; + + if (payloadLength === 126) { + target.writeUInt16BE(dataLength, 2); + } else if (payloadLength === 127) { + target[2] = target[3] = 0; + target.writeUIntBE(dataLength, 4, 6); + } + + if (!options.mask) return [target, data]; + + target[1] |= 0x80; + target[offset - 4] = mask[0]; + target[offset - 3] = mask[1]; + target[offset - 2] = mask[2]; + target[offset - 1] = mask[3]; + + if (skipMasking) return [target, data]; + + if (merge) { + applyMask(data, mask, target, offset, dataLength); + return [target]; + } + + applyMask(data, mask, data, 0, dataLength); + return [target, data]; + } + + /** + * Sends a close message to the other peer. + * + * @param {Number} [code] The status code component of the body + * @param {(String|Buffer)} [data] The message component of the body + * @param {Boolean} [mask=false] Specifies whether or not to mask the message + * @param {Function} [cb] Callback + * @public + */ + close(code, data, mask, cb) { + let buf; + + if (code === undefined) { + buf = EMPTY_BUFFER; + } else if (typeof code !== 'number' || !isValidStatusCode(code)) { + throw new TypeError('First argument must be a valid error code number'); + } else if (data === undefined || !data.length) { + buf = Buffer.allocUnsafe(2); + buf.writeUInt16BE(code, 0); + } else { + const length = Buffer.byteLength(data); + + if (length > 123) { + throw new RangeError('The message must not be greater than 123 bytes'); + } + + buf = Buffer.allocUnsafe(2 + length); + buf.writeUInt16BE(code, 0); + + if (typeof data === 'string') { + buf.write(data, 2); + } else { + buf.set(data, 2); + } + } + + const options = { + [kByteLength]: buf.length, + fin: true, + generateMask: this._generateMask, + mask, + maskBuffer: this._maskBuffer, + opcode: 0x08, + readOnly: false, + rsv1: false + }; + + if (this._state !== DEFAULT) { + this.enqueue([this.dispatch, buf, false, options, cb]); + } else { + this.sendFrame(Sender.frame(buf, options), cb); + } + } + + /** + * Sends a ping message to the other peer. + * + * @param {*} data The message to send + * @param {Boolean} [mask=false] Specifies whether or not to mask `data` + * @param {Function} [cb] Callback + * @public + */ + ping(data, mask, cb) { + let byteLength; + let readOnly; + + if (typeof data === 'string') { + byteLength = Buffer.byteLength(data); + readOnly = false; + } else if (isBlob(data)) { + byteLength = data.size; + readOnly = false; + } else { + data = toBuffer(data); + byteLength = data.length; + readOnly = toBuffer.readOnly; + } + + if (byteLength > 125) { + throw new RangeError('The data size must not be greater than 125 bytes'); + } + + const options = { + [kByteLength]: byteLength, + fin: true, + generateMask: this._generateMask, + mask, + maskBuffer: this._maskBuffer, + opcode: 0x09, + readOnly, + rsv1: false + }; + + if (isBlob(data)) { + if (this._state !== DEFAULT) { + this.enqueue([this.getBlobData, data, false, options, cb]); + } else { + this.getBlobData(data, false, options, cb); + } + } else if (this._state !== DEFAULT) { + this.enqueue([this.dispatch, data, false, options, cb]); + } else { + this.sendFrame(Sender.frame(data, options), cb); + } + } + + /** + * Sends a pong message to the other peer. + * + * @param {*} data The message to send + * @param {Boolean} [mask=false] Specifies whether or not to mask `data` + * @param {Function} [cb] Callback + * @public + */ + pong(data, mask, cb) { + let byteLength; + let readOnly; + + if (typeof data === 'string') { + byteLength = Buffer.byteLength(data); + readOnly = false; + } else if (isBlob(data)) { + byteLength = data.size; + readOnly = false; + } else { + data = toBuffer(data); + byteLength = data.length; + readOnly = toBuffer.readOnly; + } + + if (byteLength > 125) { + throw new RangeError('The data size must not be greater than 125 bytes'); + } + + const options = { + [kByteLength]: byteLength, + fin: true, + generateMask: this._generateMask, + mask, + maskBuffer: this._maskBuffer, + opcode: 0x0a, + readOnly, + rsv1: false + }; + + if (isBlob(data)) { + if (this._state !== DEFAULT) { + this.enqueue([this.getBlobData, data, false, options, cb]); + } else { + this.getBlobData(data, false, options, cb); + } + } else if (this._state !== DEFAULT) { + this.enqueue([this.dispatch, data, false, options, cb]); + } else { + this.sendFrame(Sender.frame(data, options), cb); + } + } + + /** + * Sends a data message to the other peer. + * + * @param {*} data The message to send + * @param {Object} options Options object + * @param {Boolean} [options.binary=false] Specifies whether `data` is binary + * or text + * @param {Boolean} [options.compress=false] Specifies whether or not to + * compress `data` + * @param {Boolean} [options.fin=false] Specifies whether the fragment is the + * last one + * @param {Boolean} [options.mask=false] Specifies whether or not to mask + * `data` + * @param {Function} [cb] Callback + * @public + */ + send(data, options, cb) { + const perMessageDeflate = this._extensions[PerMessageDeflate.extensionName]; + let opcode = options.binary ? 2 : 1; + let rsv1 = options.compress; + + let byteLength; + let readOnly; + + if (typeof data === 'string') { + byteLength = Buffer.byteLength(data); + readOnly = false; + } else if (isBlob(data)) { + byteLength = data.size; + readOnly = false; + } else { + data = toBuffer(data); + byteLength = data.length; + readOnly = toBuffer.readOnly; + } + + if (this._firstFragment) { + this._firstFragment = false; + if ( + rsv1 && + perMessageDeflate && + perMessageDeflate.params[ + perMessageDeflate._isServer + ? 'server_no_context_takeover' + : 'client_no_context_takeover' + ] + ) { + rsv1 = byteLength >= perMessageDeflate._threshold; + } + this._compress = rsv1; + } else { + rsv1 = false; + opcode = 0; + } + + if (options.fin) this._firstFragment = true; + + const opts = { + [kByteLength]: byteLength, + fin: options.fin, + generateMask: this._generateMask, + mask: options.mask, + maskBuffer: this._maskBuffer, + opcode, + readOnly, + rsv1 + }; + + if (isBlob(data)) { + if (this._state !== DEFAULT) { + this.enqueue([this.getBlobData, data, this._compress, opts, cb]); + } else { + this.getBlobData(data, this._compress, opts, cb); + } + } else if (this._state !== DEFAULT) { + this.enqueue([this.dispatch, data, this._compress, opts, cb]); + } else { + this.dispatch(data, this._compress, opts, cb); + } + } + + /** + * Gets the contents of a blob as binary data. + * + * @param {Blob} blob The blob + * @param {Boolean} [compress=false] Specifies whether or not to compress + * the data + * @param {Object} options Options object + * @param {Boolean} [options.fin=false] Specifies whether or not to set the + * FIN bit + * @param {Function} [options.generateMask] The function used to generate the + * masking key + * @param {Boolean} [options.mask=false] Specifies whether or not to mask + * `data` + * @param {Buffer} [options.maskBuffer] The buffer used to store the masking + * key + * @param {Number} options.opcode The opcode + * @param {Boolean} [options.readOnly=false] Specifies whether `data` can be + * modified + * @param {Boolean} [options.rsv1=false] Specifies whether or not to set the + * RSV1 bit + * @param {Function} [cb] Callback + * @private + */ + getBlobData(blob, compress, options, cb) { + this._bufferedBytes += options[kByteLength]; + this._state = GET_BLOB_DATA; + + blob + .arrayBuffer() + .then((arrayBuffer) => { + if (this._socket.destroyed) { + const err = new Error( + 'The socket was closed while the blob was being read' + ); + + // + // `callCallbacks` is called in the next tick to ensure that errors + // that might be thrown in the callbacks behave like errors thrown + // outside the promise chain. + // + process.nextTick(callCallbacks, this, err, cb); + return; + } + + this._bufferedBytes -= options[kByteLength]; + const data = toBuffer(arrayBuffer); + + if (!compress) { + this._state = DEFAULT; + this.sendFrame(Sender.frame(data, options), cb); + this.dequeue(); + } else { + this.dispatch(data, compress, options, cb); + } + }) + .catch((err) => { + // + // `onError` is called in the next tick for the same reason that + // `callCallbacks` above is. + // + process.nextTick(onError, this, err, cb); + }); + } + + /** + * Dispatches a message. + * + * @param {(Buffer|String)} data The message to send + * @param {Boolean} [compress=false] Specifies whether or not to compress + * `data` + * @param {Object} options Options object + * @param {Boolean} [options.fin=false] Specifies whether or not to set the + * FIN bit + * @param {Function} [options.generateMask] The function used to generate the + * masking key + * @param {Boolean} [options.mask=false] Specifies whether or not to mask + * `data` + * @param {Buffer} [options.maskBuffer] The buffer used to store the masking + * key + * @param {Number} options.opcode The opcode + * @param {Boolean} [options.readOnly=false] Specifies whether `data` can be + * modified + * @param {Boolean} [options.rsv1=false] Specifies whether or not to set the + * RSV1 bit + * @param {Function} [cb] Callback + * @private + */ + dispatch(data, compress, options, cb) { + if (!compress) { + this.sendFrame(Sender.frame(data, options), cb); + return; + } + + const perMessageDeflate = this._extensions[PerMessageDeflate.extensionName]; + + this._bufferedBytes += options[kByteLength]; + this._state = DEFLATING; + perMessageDeflate.compress(data, options.fin, (_, buf) => { + if (this._socket.destroyed) { + const err = new Error( + 'The socket was closed while data was being compressed' + ); + + callCallbacks(this, err, cb); + return; + } + + this._bufferedBytes -= options[kByteLength]; + this._state = DEFAULT; + options.readOnly = false; + this.sendFrame(Sender.frame(buf, options), cb); + this.dequeue(); + }); + } + + /** + * Executes queued send operations. + * + * @private + */ + dequeue() { + while (this._state === DEFAULT && this._queue.length) { + const params = this._queue.shift(); + + this._bufferedBytes -= params[3][kByteLength]; + Reflect.apply(params[0], this, params.slice(1)); + } + } + + /** + * Enqueues a send operation. + * + * @param {Array} params Send operation parameters. + * @private + */ + enqueue(params) { + this._bufferedBytes += params[3][kByteLength]; + this._queue.push(params); + } + + /** + * Sends a frame. + * + * @param {(Buffer | String)[]} list The frame to send + * @param {Function} [cb] Callback + * @private + */ + sendFrame(list, cb) { + if (list.length === 2) { + this._socket.cork(); + this._socket.write(list[0]); + this._socket.write(list[1], cb); + this._socket.uncork(); + } else { + this._socket.write(list[0], cb); + } + } +} + +module.exports = Sender; + +/** + * Calls queued callbacks with an error. + * + * @param {Sender} sender The `Sender` instance + * @param {Error} err The error to call the callbacks with + * @param {Function} [cb] The first callback + * @private + */ +function callCallbacks(sender, err, cb) { + if (typeof cb === 'function') cb(err); + + for (let i = 0; i < sender._queue.length; i++) { + const params = sender._queue[i]; + const callback = params[params.length - 1]; + + if (typeof callback === 'function') callback(err); + } +} + +/** + * Handles a `Sender` error. + * + * @param {Sender} sender The `Sender` instance + * @param {Error} err The error + * @param {Function} [cb] The first pending callback + * @private + */ +function onError(sender, err, cb) { + callCallbacks(sender, err, cb); + sender.onerror(err); +} diff --git a/Streamdeck/com.mirabox.streamingle.sdPlugin/node_modules/ws/lib/stream.js b/Streamdeck/com.mirabox.streamingle.sdPlugin/node_modules/ws/lib/stream.js new file mode 100644 index 00000000..4c58c911 --- /dev/null +++ b/Streamdeck/com.mirabox.streamingle.sdPlugin/node_modules/ws/lib/stream.js @@ -0,0 +1,161 @@ +/* eslint no-unused-vars: ["error", { "varsIgnorePattern": "^WebSocket$" }] */ +'use strict'; + +const WebSocket = require('./websocket'); +const { Duplex } = require('stream'); + +/** + * Emits the `'close'` event on a stream. + * + * @param {Duplex} stream The stream. + * @private + */ +function emitClose(stream) { + stream.emit('close'); +} + +/** + * The listener of the `'end'` event. + * + * @private + */ +function duplexOnEnd() { + if (!this.destroyed && this._writableState.finished) { + this.destroy(); + } +} + +/** + * The listener of the `'error'` event. + * + * @param {Error} err The error + * @private + */ +function duplexOnError(err) { + this.removeListener('error', duplexOnError); + this.destroy(); + if (this.listenerCount('error') === 0) { + // Do not suppress the throwing behavior. + this.emit('error', err); + } +} + +/** + * Wraps a `WebSocket` in a duplex stream. + * + * @param {WebSocket} ws The `WebSocket` to wrap + * @param {Object} [options] The options for the `Duplex` constructor + * @return {Duplex} The duplex stream + * @public + */ +function createWebSocketStream(ws, options) { + let terminateOnDestroy = true; + + const duplex = new Duplex({ + ...options, + autoDestroy: false, + emitClose: false, + objectMode: false, + writableObjectMode: false + }); + + ws.on('message', function message(msg, isBinary) { + const data = + !isBinary && duplex._readableState.objectMode ? msg.toString() : msg; + + if (!duplex.push(data)) ws.pause(); + }); + + ws.once('error', function error(err) { + if (duplex.destroyed) return; + + // Prevent `ws.terminate()` from being called by `duplex._destroy()`. + // + // - If the `'error'` event is emitted before the `'open'` event, then + // `ws.terminate()` is a noop as no socket is assigned. + // - Otherwise, the error is re-emitted by the listener of the `'error'` + // event of the `Receiver` object. The listener already closes the + // connection by calling `ws.close()`. This allows a close frame to be + // sent to the other peer. If `ws.terminate()` is called right after this, + // then the close frame might not be sent. + terminateOnDestroy = false; + duplex.destroy(err); + }); + + ws.once('close', function close() { + if (duplex.destroyed) return; + + duplex.push(null); + }); + + duplex._destroy = function (err, callback) { + if (ws.readyState === ws.CLOSED) { + callback(err); + process.nextTick(emitClose, duplex); + return; + } + + let called = false; + + ws.once('error', function error(err) { + called = true; + callback(err); + }); + + ws.once('close', function close() { + if (!called) callback(err); + process.nextTick(emitClose, duplex); + }); + + if (terminateOnDestroy) ws.terminate(); + }; + + duplex._final = function (callback) { + if (ws.readyState === ws.CONNECTING) { + ws.once('open', function open() { + duplex._final(callback); + }); + return; + } + + // If the value of the `_socket` property is `null` it means that `ws` is a + // client websocket and the handshake failed. In fact, when this happens, a + // socket is never assigned to the websocket. Wait for the `'error'` event + // that will be emitted by the websocket. + if (ws._socket === null) return; + + if (ws._socket._writableState.finished) { + callback(); + if (duplex._readableState.endEmitted) duplex.destroy(); + } else { + ws._socket.once('finish', function finish() { + // `duplex` is not destroyed here because the `'end'` event will be + // emitted on `duplex` after this `'finish'` event. The EOF signaling + // `null` chunk is, in fact, pushed when the websocket emits `'close'`. + callback(); + }); + ws.close(); + } + }; + + duplex._read = function () { + if (ws.isPaused) ws.resume(); + }; + + duplex._write = function (chunk, encoding, callback) { + if (ws.readyState === ws.CONNECTING) { + ws.once('open', function open() { + duplex._write(chunk, encoding, callback); + }); + return; + } + + ws.send(chunk, callback); + }; + + duplex.on('end', duplexOnEnd); + duplex.on('error', duplexOnError); + return duplex; +} + +module.exports = createWebSocketStream; diff --git a/Streamdeck/com.mirabox.streamingle.sdPlugin/node_modules/ws/lib/subprotocol.js b/Streamdeck/com.mirabox.streamingle.sdPlugin/node_modules/ws/lib/subprotocol.js new file mode 100644 index 00000000..d4381e88 --- /dev/null +++ b/Streamdeck/com.mirabox.streamingle.sdPlugin/node_modules/ws/lib/subprotocol.js @@ -0,0 +1,62 @@ +'use strict'; + +const { tokenChars } = require('./validation'); + +/** + * Parses the `Sec-WebSocket-Protocol` header into a set of subprotocol names. + * + * @param {String} header The field value of the header + * @return {Set} The subprotocol names + * @public + */ +function parse(header) { + const protocols = new Set(); + let start = -1; + let end = -1; + let i = 0; + + for (i; i < header.length; i++) { + const code = header.charCodeAt(i); + + if (end === -1 && tokenChars[code] === 1) { + if (start === -1) start = i; + } else if ( + i !== 0 && + (code === 0x20 /* ' ' */ || code === 0x09) /* '\t' */ + ) { + if (end === -1 && start !== -1) end = i; + } else if (code === 0x2c /* ',' */) { + if (start === -1) { + throw new SyntaxError(`Unexpected character at index ${i}`); + } + + if (end === -1) end = i; + + const protocol = header.slice(start, end); + + if (protocols.has(protocol)) { + throw new SyntaxError(`The "${protocol}" subprotocol is duplicated`); + } + + protocols.add(protocol); + start = end = -1; + } else { + throw new SyntaxError(`Unexpected character at index ${i}`); + } + } + + if (start === -1 || end !== -1) { + throw new SyntaxError('Unexpected end of input'); + } + + const protocol = header.slice(start, i); + + if (protocols.has(protocol)) { + throw new SyntaxError(`The "${protocol}" subprotocol is duplicated`); + } + + protocols.add(protocol); + return protocols; +} + +module.exports = { parse }; diff --git a/Streamdeck/com.mirabox.streamingle.sdPlugin/node_modules/ws/lib/validation.js b/Streamdeck/com.mirabox.streamingle.sdPlugin/node_modules/ws/lib/validation.js new file mode 100644 index 00000000..4a2e68d5 --- /dev/null +++ b/Streamdeck/com.mirabox.streamingle.sdPlugin/node_modules/ws/lib/validation.js @@ -0,0 +1,152 @@ +'use strict'; + +const { isUtf8 } = require('buffer'); + +const { hasBlob } = require('./constants'); + +// +// Allowed token characters: +// +// '!', '#', '$', '%', '&', ''', '*', '+', '-', +// '.', 0-9, A-Z, '^', '_', '`', a-z, '|', '~' +// +// tokenChars[32] === 0 // ' ' +// tokenChars[33] === 1 // '!' +// tokenChars[34] === 0 // '"' +// ... +// +// prettier-ignore +const tokenChars = [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0 - 15 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 16 - 31 + 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 0, // 32 - 47 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, // 48 - 63 + 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 64 - 79 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, // 80 - 95 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 96 - 111 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0 // 112 - 127 +]; + +/** + * Checks if a status code is allowed in a close frame. + * + * @param {Number} code The status code + * @return {Boolean} `true` if the status code is valid, else `false` + * @public + */ +function isValidStatusCode(code) { + return ( + (code >= 1000 && + code <= 1014 && + code !== 1004 && + code !== 1005 && + code !== 1006) || + (code >= 3000 && code <= 4999) + ); +} + +/** + * Checks if a given buffer contains only correct UTF-8. + * Ported from https://www.cl.cam.ac.uk/%7Emgk25/ucs/utf8_check.c by + * Markus Kuhn. + * + * @param {Buffer} buf The buffer to check + * @return {Boolean} `true` if `buf` contains only correct UTF-8, else `false` + * @public + */ +function _isValidUTF8(buf) { + const len = buf.length; + let i = 0; + + while (i < len) { + if ((buf[i] & 0x80) === 0) { + // 0xxxxxxx + i++; + } else if ((buf[i] & 0xe0) === 0xc0) { + // 110xxxxx 10xxxxxx + if ( + i + 1 === len || + (buf[i + 1] & 0xc0) !== 0x80 || + (buf[i] & 0xfe) === 0xc0 // Overlong + ) { + return false; + } + + i += 2; + } else if ((buf[i] & 0xf0) === 0xe0) { + // 1110xxxx 10xxxxxx 10xxxxxx + if ( + i + 2 >= len || + (buf[i + 1] & 0xc0) !== 0x80 || + (buf[i + 2] & 0xc0) !== 0x80 || + (buf[i] === 0xe0 && (buf[i + 1] & 0xe0) === 0x80) || // Overlong + (buf[i] === 0xed && (buf[i + 1] & 0xe0) === 0xa0) // Surrogate (U+D800 - U+DFFF) + ) { + return false; + } + + i += 3; + } else if ((buf[i] & 0xf8) === 0xf0) { + // 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx + if ( + i + 3 >= len || + (buf[i + 1] & 0xc0) !== 0x80 || + (buf[i + 2] & 0xc0) !== 0x80 || + (buf[i + 3] & 0xc0) !== 0x80 || + (buf[i] === 0xf0 && (buf[i + 1] & 0xf0) === 0x80) || // Overlong + (buf[i] === 0xf4 && buf[i + 1] > 0x8f) || + buf[i] > 0xf4 // > U+10FFFF + ) { + return false; + } + + i += 4; + } else { + return false; + } + } + + return true; +} + +/** + * Determines whether a value is a `Blob`. + * + * @param {*} value The value to be tested + * @return {Boolean} `true` if `value` is a `Blob`, else `false` + * @private + */ +function isBlob(value) { + return ( + hasBlob && + typeof value === 'object' && + typeof value.arrayBuffer === 'function' && + typeof value.type === 'string' && + typeof value.stream === 'function' && + (value[Symbol.toStringTag] === 'Blob' || + value[Symbol.toStringTag] === 'File') + ); +} + +module.exports = { + isBlob, + isValidStatusCode, + isValidUTF8: _isValidUTF8, + tokenChars +}; + +if (isUtf8) { + module.exports.isValidUTF8 = function (buf) { + return buf.length < 24 ? _isValidUTF8(buf) : isUtf8(buf); + }; +} /* istanbul ignore else */ else if (!process.env.WS_NO_UTF_8_VALIDATE) { + try { + const isValidUTF8 = require('utf-8-validate'); + + module.exports.isValidUTF8 = function (buf) { + return buf.length < 32 ? _isValidUTF8(buf) : isValidUTF8(buf); + }; + } catch (e) { + // Continue regardless of the error. + } +} diff --git a/Streamdeck/com.mirabox.streamingle.sdPlugin/node_modules/ws/lib/websocket-server.js b/Streamdeck/com.mirabox.streamingle.sdPlugin/node_modules/ws/lib/websocket-server.js new file mode 100644 index 00000000..33e09858 --- /dev/null +++ b/Streamdeck/com.mirabox.streamingle.sdPlugin/node_modules/ws/lib/websocket-server.js @@ -0,0 +1,550 @@ +/* eslint no-unused-vars: ["error", { "varsIgnorePattern": "^Duplex$", "caughtErrors": "none" }] */ + +'use strict'; + +const EventEmitter = require('events'); +const http = require('http'); +const { Duplex } = require('stream'); +const { createHash } = require('crypto'); + +const extension = require('./extension'); +const PerMessageDeflate = require('./permessage-deflate'); +const subprotocol = require('./subprotocol'); +const WebSocket = require('./websocket'); +const { GUID, kWebSocket } = require('./constants'); + +const keyRegex = /^[+/0-9A-Za-z]{22}==$/; + +const RUNNING = 0; +const CLOSING = 1; +const CLOSED = 2; + +/** + * Class representing a WebSocket server. + * + * @extends EventEmitter + */ +class WebSocketServer extends EventEmitter { + /** + * Create a `WebSocketServer` instance. + * + * @param {Object} options Configuration options + * @param {Boolean} [options.allowSynchronousEvents=true] Specifies whether + * any of the `'message'`, `'ping'`, and `'pong'` events can be emitted + * multiple times in the same tick + * @param {Boolean} [options.autoPong=true] Specifies whether or not to + * automatically send a pong in response to a ping + * @param {Number} [options.backlog=511] The maximum length of the queue of + * pending connections + * @param {Boolean} [options.clientTracking=true] Specifies whether or not to + * track clients + * @param {Function} [options.handleProtocols] A hook to handle protocols + * @param {String} [options.host] The hostname where to bind the server + * @param {Number} [options.maxPayload=104857600] The maximum allowed message + * size + * @param {Boolean} [options.noServer=false] Enable no server mode + * @param {String} [options.path] Accept only connections matching this path + * @param {(Boolean|Object)} [options.perMessageDeflate=false] Enable/disable + * permessage-deflate + * @param {Number} [options.port] The port where to bind the server + * @param {(http.Server|https.Server)} [options.server] A pre-created HTTP/S + * server to use + * @param {Boolean} [options.skipUTF8Validation=false] Specifies whether or + * not to skip UTF-8 validation for text and close messages + * @param {Function} [options.verifyClient] A hook to reject connections + * @param {Function} [options.WebSocket=WebSocket] Specifies the `WebSocket` + * class to use. It must be the `WebSocket` class or class that extends it + * @param {Function} [callback] A listener for the `listening` event + */ + constructor(options, callback) { + super(); + + options = { + allowSynchronousEvents: true, + autoPong: true, + maxPayload: 100 * 1024 * 1024, + skipUTF8Validation: false, + perMessageDeflate: false, + handleProtocols: null, + clientTracking: true, + verifyClient: null, + noServer: false, + backlog: null, // use default (511 as implemented in net.js) + server: null, + host: null, + path: null, + port: null, + WebSocket, + ...options + }; + + if ( + (options.port == null && !options.server && !options.noServer) || + (options.port != null && (options.server || options.noServer)) || + (options.server && options.noServer) + ) { + throw new TypeError( + 'One and only one of the "port", "server", or "noServer" options ' + + 'must be specified' + ); + } + + if (options.port != null) { + this._server = http.createServer((req, res) => { + const body = http.STATUS_CODES[426]; + + res.writeHead(426, { + 'Content-Length': body.length, + 'Content-Type': 'text/plain' + }); + res.end(body); + }); + this._server.listen( + options.port, + options.host, + options.backlog, + callback + ); + } else if (options.server) { + this._server = options.server; + } + + if (this._server) { + const emitConnection = this.emit.bind(this, 'connection'); + + this._removeListeners = addListeners(this._server, { + listening: this.emit.bind(this, 'listening'), + error: this.emit.bind(this, 'error'), + upgrade: (req, socket, head) => { + this.handleUpgrade(req, socket, head, emitConnection); + } + }); + } + + if (options.perMessageDeflate === true) options.perMessageDeflate = {}; + if (options.clientTracking) { + this.clients = new Set(); + this._shouldEmitClose = false; + } + + this.options = options; + this._state = RUNNING; + } + + /** + * Returns the bound address, the address family name, and port of the server + * as reported by the operating system if listening on an IP socket. + * If the server is listening on a pipe or UNIX domain socket, the name is + * returned as a string. + * + * @return {(Object|String|null)} The address of the server + * @public + */ + address() { + if (this.options.noServer) { + throw new Error('The server is operating in "noServer" mode'); + } + + if (!this._server) return null; + return this._server.address(); + } + + /** + * Stop the server from accepting new connections and emit the `'close'` event + * when all existing connections are closed. + * + * @param {Function} [cb] A one-time listener for the `'close'` event + * @public + */ + close(cb) { + if (this._state === CLOSED) { + if (cb) { + this.once('close', () => { + cb(new Error('The server is not running')); + }); + } + + process.nextTick(emitClose, this); + return; + } + + if (cb) this.once('close', cb); + + if (this._state === CLOSING) return; + this._state = CLOSING; + + if (this.options.noServer || this.options.server) { + if (this._server) { + this._removeListeners(); + this._removeListeners = this._server = null; + } + + if (this.clients) { + if (!this.clients.size) { + process.nextTick(emitClose, this); + } else { + this._shouldEmitClose = true; + } + } else { + process.nextTick(emitClose, this); + } + } else { + const server = this._server; + + this._removeListeners(); + this._removeListeners = this._server = null; + + // + // The HTTP/S server was created internally. Close it, and rely on its + // `'close'` event. + // + server.close(() => { + emitClose(this); + }); + } + } + + /** + * See if a given request should be handled by this server instance. + * + * @param {http.IncomingMessage} req Request object to inspect + * @return {Boolean} `true` if the request is valid, else `false` + * @public + */ + shouldHandle(req) { + if (this.options.path) { + const index = req.url.indexOf('?'); + const pathname = index !== -1 ? req.url.slice(0, index) : req.url; + + if (pathname !== this.options.path) return false; + } + + return true; + } + + /** + * Handle a HTTP Upgrade request. + * + * @param {http.IncomingMessage} req The request object + * @param {Duplex} socket The network socket between the server and client + * @param {Buffer} head The first packet of the upgraded stream + * @param {Function} cb Callback + * @public + */ + handleUpgrade(req, socket, head, cb) { + socket.on('error', socketOnError); + + const key = req.headers['sec-websocket-key']; + const upgrade = req.headers.upgrade; + const version = +req.headers['sec-websocket-version']; + + if (req.method !== 'GET') { + const message = 'Invalid HTTP method'; + abortHandshakeOrEmitwsClientError(this, req, socket, 405, message); + return; + } + + if (upgrade === undefined || upgrade.toLowerCase() !== 'websocket') { + const message = 'Invalid Upgrade header'; + abortHandshakeOrEmitwsClientError(this, req, socket, 400, message); + return; + } + + if (key === undefined || !keyRegex.test(key)) { + const message = 'Missing or invalid Sec-WebSocket-Key header'; + abortHandshakeOrEmitwsClientError(this, req, socket, 400, message); + return; + } + + if (version !== 13 && version !== 8) { + const message = 'Missing or invalid Sec-WebSocket-Version header'; + abortHandshakeOrEmitwsClientError(this, req, socket, 400, message, { + 'Sec-WebSocket-Version': '13, 8' + }); + return; + } + + if (!this.shouldHandle(req)) { + abortHandshake(socket, 400); + return; + } + + const secWebSocketProtocol = req.headers['sec-websocket-protocol']; + let protocols = new Set(); + + if (secWebSocketProtocol !== undefined) { + try { + protocols = subprotocol.parse(secWebSocketProtocol); + } catch (err) { + const message = 'Invalid Sec-WebSocket-Protocol header'; + abortHandshakeOrEmitwsClientError(this, req, socket, 400, message); + return; + } + } + + const secWebSocketExtensions = req.headers['sec-websocket-extensions']; + const extensions = {}; + + if ( + this.options.perMessageDeflate && + secWebSocketExtensions !== undefined + ) { + const perMessageDeflate = new PerMessageDeflate( + this.options.perMessageDeflate, + true, + this.options.maxPayload + ); + + try { + const offers = extension.parse(secWebSocketExtensions); + + if (offers[PerMessageDeflate.extensionName]) { + perMessageDeflate.accept(offers[PerMessageDeflate.extensionName]); + extensions[PerMessageDeflate.extensionName] = perMessageDeflate; + } + } catch (err) { + const message = + 'Invalid or unacceptable Sec-WebSocket-Extensions header'; + abortHandshakeOrEmitwsClientError(this, req, socket, 400, message); + return; + } + } + + // + // Optionally call external client verification handler. + // + if (this.options.verifyClient) { + const info = { + origin: + req.headers[`${version === 8 ? 'sec-websocket-origin' : 'origin'}`], + secure: !!(req.socket.authorized || req.socket.encrypted), + req + }; + + if (this.options.verifyClient.length === 2) { + this.options.verifyClient(info, (verified, code, message, headers) => { + if (!verified) { + return abortHandshake(socket, code || 401, message, headers); + } + + this.completeUpgrade( + extensions, + key, + protocols, + req, + socket, + head, + cb + ); + }); + return; + } + + if (!this.options.verifyClient(info)) return abortHandshake(socket, 401); + } + + this.completeUpgrade(extensions, key, protocols, req, socket, head, cb); + } + + /** + * Upgrade the connection to WebSocket. + * + * @param {Object} extensions The accepted extensions + * @param {String} key The value of the `Sec-WebSocket-Key` header + * @param {Set} protocols The subprotocols + * @param {http.IncomingMessage} req The request object + * @param {Duplex} socket The network socket between the server and client + * @param {Buffer} head The first packet of the upgraded stream + * @param {Function} cb Callback + * @throws {Error} If called more than once with the same socket + * @private + */ + completeUpgrade(extensions, key, protocols, req, socket, head, cb) { + // + // Destroy the socket if the client has already sent a FIN packet. + // + if (!socket.readable || !socket.writable) return socket.destroy(); + + if (socket[kWebSocket]) { + throw new Error( + 'server.handleUpgrade() was called more than once with the same ' + + 'socket, possibly due to a misconfiguration' + ); + } + + if (this._state > RUNNING) return abortHandshake(socket, 503); + + const digest = createHash('sha1') + .update(key + GUID) + .digest('base64'); + + const headers = [ + 'HTTP/1.1 101 Switching Protocols', + 'Upgrade: websocket', + 'Connection: Upgrade', + `Sec-WebSocket-Accept: ${digest}` + ]; + + const ws = new this.options.WebSocket(null, undefined, this.options); + + if (protocols.size) { + // + // Optionally call external protocol selection handler. + // + const protocol = this.options.handleProtocols + ? this.options.handleProtocols(protocols, req) + : protocols.values().next().value; + + if (protocol) { + headers.push(`Sec-WebSocket-Protocol: ${protocol}`); + ws._protocol = protocol; + } + } + + if (extensions[PerMessageDeflate.extensionName]) { + const params = extensions[PerMessageDeflate.extensionName].params; + const value = extension.format({ + [PerMessageDeflate.extensionName]: [params] + }); + headers.push(`Sec-WebSocket-Extensions: ${value}`); + ws._extensions = extensions; + } + + // + // Allow external modification/inspection of handshake headers. + // + this.emit('headers', headers, req); + + socket.write(headers.concat('\r\n').join('\r\n')); + socket.removeListener('error', socketOnError); + + ws.setSocket(socket, head, { + allowSynchronousEvents: this.options.allowSynchronousEvents, + maxPayload: this.options.maxPayload, + skipUTF8Validation: this.options.skipUTF8Validation + }); + + if (this.clients) { + this.clients.add(ws); + ws.on('close', () => { + this.clients.delete(ws); + + if (this._shouldEmitClose && !this.clients.size) { + process.nextTick(emitClose, this); + } + }); + } + + cb(ws, req); + } +} + +module.exports = WebSocketServer; + +/** + * Add event listeners on an `EventEmitter` using a map of + * pairs. + * + * @param {EventEmitter} server The event emitter + * @param {Object.} map The listeners to add + * @return {Function} A function that will remove the added listeners when + * called + * @private + */ +function addListeners(server, map) { + for (const event of Object.keys(map)) server.on(event, map[event]); + + return function removeListeners() { + for (const event of Object.keys(map)) { + server.removeListener(event, map[event]); + } + }; +} + +/** + * Emit a `'close'` event on an `EventEmitter`. + * + * @param {EventEmitter} server The event emitter + * @private + */ +function emitClose(server) { + server._state = CLOSED; + server.emit('close'); +} + +/** + * Handle socket errors. + * + * @private + */ +function socketOnError() { + this.destroy(); +} + +/** + * Close the connection when preconditions are not fulfilled. + * + * @param {Duplex} socket The socket of the upgrade request + * @param {Number} code The HTTP response status code + * @param {String} [message] The HTTP response body + * @param {Object} [headers] Additional HTTP response headers + * @private + */ +function abortHandshake(socket, code, message, headers) { + // + // The socket is writable unless the user destroyed or ended it before calling + // `server.handleUpgrade()` or in the `verifyClient` function, which is a user + // error. Handling this does not make much sense as the worst that can happen + // is that some of the data written by the user might be discarded due to the + // call to `socket.end()` below, which triggers an `'error'` event that in + // turn causes the socket to be destroyed. + // + message = message || http.STATUS_CODES[code]; + headers = { + Connection: 'close', + 'Content-Type': 'text/html', + 'Content-Length': Buffer.byteLength(message), + ...headers + }; + + socket.once('finish', socket.destroy); + + socket.end( + `HTTP/1.1 ${code} ${http.STATUS_CODES[code]}\r\n` + + Object.keys(headers) + .map((h) => `${h}: ${headers[h]}`) + .join('\r\n') + + '\r\n\r\n' + + message + ); +} + +/** + * Emit a `'wsClientError'` event on a `WebSocketServer` if there is at least + * one listener for it, otherwise call `abortHandshake()`. + * + * @param {WebSocketServer} server The WebSocket server + * @param {http.IncomingMessage} req The request object + * @param {Duplex} socket The socket of the upgrade request + * @param {Number} code The HTTP response status code + * @param {String} message The HTTP response body + * @param {Object} [headers] The HTTP response headers + * @private + */ +function abortHandshakeOrEmitwsClientError( + server, + req, + socket, + code, + message, + headers +) { + if (server.listenerCount('wsClientError')) { + const err = new Error(message); + Error.captureStackTrace(err, abortHandshakeOrEmitwsClientError); + + server.emit('wsClientError', err, socket, req); + } else { + abortHandshake(socket, code, message, headers); + } +} diff --git a/Streamdeck/com.mirabox.streamingle.sdPlugin/node_modules/ws/lib/websocket.js b/Streamdeck/com.mirabox.streamingle.sdPlugin/node_modules/ws/lib/websocket.js new file mode 100644 index 00000000..ad8764a0 --- /dev/null +++ b/Streamdeck/com.mirabox.streamingle.sdPlugin/node_modules/ws/lib/websocket.js @@ -0,0 +1,1388 @@ +/* eslint no-unused-vars: ["error", { "varsIgnorePattern": "^Duplex|Readable$", "caughtErrors": "none" }] */ + +'use strict'; + +const EventEmitter = require('events'); +const https = require('https'); +const http = require('http'); +const net = require('net'); +const tls = require('tls'); +const { randomBytes, createHash } = require('crypto'); +const { Duplex, Readable } = require('stream'); +const { URL } = require('url'); + +const PerMessageDeflate = require('./permessage-deflate'); +const Receiver = require('./receiver'); +const Sender = require('./sender'); +const { isBlob } = require('./validation'); + +const { + BINARY_TYPES, + EMPTY_BUFFER, + GUID, + kForOnEventAttribute, + kListener, + kStatusCode, + kWebSocket, + NOOP +} = require('./constants'); +const { + EventTarget: { addEventListener, removeEventListener } +} = require('./event-target'); +const { format, parse } = require('./extension'); +const { toBuffer } = require('./buffer-util'); + +const closeTimeout = 30 * 1000; +const kAborted = Symbol('kAborted'); +const protocolVersions = [8, 13]; +const readyStates = ['CONNECTING', 'OPEN', 'CLOSING', 'CLOSED']; +const subprotocolRegex = /^[!#$%&'*+\-.0-9A-Z^_`|a-z~]+$/; + +/** + * Class representing a WebSocket. + * + * @extends EventEmitter + */ +class WebSocket extends EventEmitter { + /** + * Create a new `WebSocket`. + * + * @param {(String|URL)} address The URL to which to connect + * @param {(String|String[])} [protocols] The subprotocols + * @param {Object} [options] Connection options + */ + constructor(address, protocols, options) { + super(); + + this._binaryType = BINARY_TYPES[0]; + this._closeCode = 1006; + this._closeFrameReceived = false; + this._closeFrameSent = false; + this._closeMessage = EMPTY_BUFFER; + this._closeTimer = null; + this._errorEmitted = false; + this._extensions = {}; + this._paused = false; + this._protocol = ''; + this._readyState = WebSocket.CONNECTING; + this._receiver = null; + this._sender = null; + this._socket = null; + + if (address !== null) { + this._bufferedAmount = 0; + this._isServer = false; + this._redirects = 0; + + if (protocols === undefined) { + protocols = []; + } else if (!Array.isArray(protocols)) { + if (typeof protocols === 'object' && protocols !== null) { + options = protocols; + protocols = []; + } else { + protocols = [protocols]; + } + } + + initAsClient(this, address, protocols, options); + } else { + this._autoPong = options.autoPong; + this._isServer = true; + } + } + + /** + * For historical reasons, the custom "nodebuffer" type is used by the default + * instead of "blob". + * + * @type {String} + */ + get binaryType() { + return this._binaryType; + } + + set binaryType(type) { + if (!BINARY_TYPES.includes(type)) return; + + this._binaryType = type; + + // + // Allow to change `binaryType` on the fly. + // + if (this._receiver) this._receiver._binaryType = type; + } + + /** + * @type {Number} + */ + get bufferedAmount() { + if (!this._socket) return this._bufferedAmount; + + return this._socket._writableState.length + this._sender._bufferedBytes; + } + + /** + * @type {String} + */ + get extensions() { + return Object.keys(this._extensions).join(); + } + + /** + * @type {Boolean} + */ + get isPaused() { + return this._paused; + } + + /** + * @type {Function} + */ + /* istanbul ignore next */ + get onclose() { + return null; + } + + /** + * @type {Function} + */ + /* istanbul ignore next */ + get onerror() { + return null; + } + + /** + * @type {Function} + */ + /* istanbul ignore next */ + get onopen() { + return null; + } + + /** + * @type {Function} + */ + /* istanbul ignore next */ + get onmessage() { + return null; + } + + /** + * @type {String} + */ + get protocol() { + return this._protocol; + } + + /** + * @type {Number} + */ + get readyState() { + return this._readyState; + } + + /** + * @type {String} + */ + get url() { + return this._url; + } + + /** + * Set up the socket and the internal resources. + * + * @param {Duplex} socket The network socket between the server and client + * @param {Buffer} head The first packet of the upgraded stream + * @param {Object} options Options object + * @param {Boolean} [options.allowSynchronousEvents=false] Specifies whether + * any of the `'message'`, `'ping'`, and `'pong'` events can be emitted + * multiple times in the same tick + * @param {Function} [options.generateMask] The function used to generate the + * masking key + * @param {Number} [options.maxPayload=0] The maximum allowed message size + * @param {Boolean} [options.skipUTF8Validation=false] Specifies whether or + * not to skip UTF-8 validation for text and close messages + * @private + */ + setSocket(socket, head, options) { + const receiver = new Receiver({ + allowSynchronousEvents: options.allowSynchronousEvents, + binaryType: this.binaryType, + extensions: this._extensions, + isServer: this._isServer, + maxPayload: options.maxPayload, + skipUTF8Validation: options.skipUTF8Validation + }); + + const sender = new Sender(socket, this._extensions, options.generateMask); + + this._receiver = receiver; + this._sender = sender; + this._socket = socket; + + receiver[kWebSocket] = this; + sender[kWebSocket] = this; + socket[kWebSocket] = this; + + receiver.on('conclude', receiverOnConclude); + receiver.on('drain', receiverOnDrain); + receiver.on('error', receiverOnError); + receiver.on('message', receiverOnMessage); + receiver.on('ping', receiverOnPing); + receiver.on('pong', receiverOnPong); + + sender.onerror = senderOnError; + + // + // These methods may not be available if `socket` is just a `Duplex`. + // + if (socket.setTimeout) socket.setTimeout(0); + if (socket.setNoDelay) socket.setNoDelay(); + + if (head.length > 0) socket.unshift(head); + + socket.on('close', socketOnClose); + socket.on('data', socketOnData); + socket.on('end', socketOnEnd); + socket.on('error', socketOnError); + + this._readyState = WebSocket.OPEN; + this.emit('open'); + } + + /** + * Emit the `'close'` event. + * + * @private + */ + emitClose() { + if (!this._socket) { + this._readyState = WebSocket.CLOSED; + this.emit('close', this._closeCode, this._closeMessage); + return; + } + + if (this._extensions[PerMessageDeflate.extensionName]) { + this._extensions[PerMessageDeflate.extensionName].cleanup(); + } + + this._receiver.removeAllListeners(); + this._readyState = WebSocket.CLOSED; + this.emit('close', this._closeCode, this._closeMessage); + } + + /** + * Start a closing handshake. + * + * +----------+ +-----------+ +----------+ + * - - -|ws.close()|-->|close frame|-->|ws.close()|- - - + * | +----------+ +-----------+ +----------+ | + * +----------+ +-----------+ | + * CLOSING |ws.close()|<--|close frame|<--+-----+ CLOSING + * +----------+ +-----------+ | + * | | | +---+ | + * +------------------------+-->|fin| - - - - + * | +---+ | +---+ + * - - - - -|fin|<---------------------+ + * +---+ + * + * @param {Number} [code] Status code explaining why the connection is closing + * @param {(String|Buffer)} [data] The reason why the connection is + * closing + * @public + */ + close(code, data) { + if (this.readyState === WebSocket.CLOSED) return; + if (this.readyState === WebSocket.CONNECTING) { + const msg = 'WebSocket was closed before the connection was established'; + abortHandshake(this, this._req, msg); + return; + } + + if (this.readyState === WebSocket.CLOSING) { + if ( + this._closeFrameSent && + (this._closeFrameReceived || this._receiver._writableState.errorEmitted) + ) { + this._socket.end(); + } + + return; + } + + this._readyState = WebSocket.CLOSING; + this._sender.close(code, data, !this._isServer, (err) => { + // + // This error is handled by the `'error'` listener on the socket. We only + // want to know if the close frame has been sent here. + // + if (err) return; + + this._closeFrameSent = true; + + if ( + this._closeFrameReceived || + this._receiver._writableState.errorEmitted + ) { + this._socket.end(); + } + }); + + setCloseTimer(this); + } + + /** + * Pause the socket. + * + * @public + */ + pause() { + if ( + this.readyState === WebSocket.CONNECTING || + this.readyState === WebSocket.CLOSED + ) { + return; + } + + this._paused = true; + this._socket.pause(); + } + + /** + * Send a ping. + * + * @param {*} [data] The data to send + * @param {Boolean} [mask] Indicates whether or not to mask `data` + * @param {Function} [cb] Callback which is executed when the ping is sent + * @public + */ + ping(data, mask, cb) { + if (this.readyState === WebSocket.CONNECTING) { + throw new Error('WebSocket is not open: readyState 0 (CONNECTING)'); + } + + if (typeof data === 'function') { + cb = data; + data = mask = undefined; + } else if (typeof mask === 'function') { + cb = mask; + mask = undefined; + } + + if (typeof data === 'number') data = data.toString(); + + if (this.readyState !== WebSocket.OPEN) { + sendAfterClose(this, data, cb); + return; + } + + if (mask === undefined) mask = !this._isServer; + this._sender.ping(data || EMPTY_BUFFER, mask, cb); + } + + /** + * Send a pong. + * + * @param {*} [data] The data to send + * @param {Boolean} [mask] Indicates whether or not to mask `data` + * @param {Function} [cb] Callback which is executed when the pong is sent + * @public + */ + pong(data, mask, cb) { + if (this.readyState === WebSocket.CONNECTING) { + throw new Error('WebSocket is not open: readyState 0 (CONNECTING)'); + } + + if (typeof data === 'function') { + cb = data; + data = mask = undefined; + } else if (typeof mask === 'function') { + cb = mask; + mask = undefined; + } + + if (typeof data === 'number') data = data.toString(); + + if (this.readyState !== WebSocket.OPEN) { + sendAfterClose(this, data, cb); + return; + } + + if (mask === undefined) mask = !this._isServer; + this._sender.pong(data || EMPTY_BUFFER, mask, cb); + } + + /** + * Resume the socket. + * + * @public + */ + resume() { + if ( + this.readyState === WebSocket.CONNECTING || + this.readyState === WebSocket.CLOSED + ) { + return; + } + + this._paused = false; + if (!this._receiver._writableState.needDrain) this._socket.resume(); + } + + /** + * Send a data message. + * + * @param {*} data The message to send + * @param {Object} [options] Options object + * @param {Boolean} [options.binary] Specifies whether `data` is binary or + * text + * @param {Boolean} [options.compress] Specifies whether or not to compress + * `data` + * @param {Boolean} [options.fin=true] Specifies whether the fragment is the + * last one + * @param {Boolean} [options.mask] Specifies whether or not to mask `data` + * @param {Function} [cb] Callback which is executed when data is written out + * @public + */ + send(data, options, cb) { + if (this.readyState === WebSocket.CONNECTING) { + throw new Error('WebSocket is not open: readyState 0 (CONNECTING)'); + } + + if (typeof options === 'function') { + cb = options; + options = {}; + } + + if (typeof data === 'number') data = data.toString(); + + if (this.readyState !== WebSocket.OPEN) { + sendAfterClose(this, data, cb); + return; + } + + const opts = { + binary: typeof data !== 'string', + mask: !this._isServer, + compress: true, + fin: true, + ...options + }; + + if (!this._extensions[PerMessageDeflate.extensionName]) { + opts.compress = false; + } + + this._sender.send(data || EMPTY_BUFFER, opts, cb); + } + + /** + * Forcibly close the connection. + * + * @public + */ + terminate() { + if (this.readyState === WebSocket.CLOSED) return; + if (this.readyState === WebSocket.CONNECTING) { + const msg = 'WebSocket was closed before the connection was established'; + abortHandshake(this, this._req, msg); + return; + } + + if (this._socket) { + this._readyState = WebSocket.CLOSING; + this._socket.destroy(); + } + } +} + +/** + * @constant {Number} CONNECTING + * @memberof WebSocket + */ +Object.defineProperty(WebSocket, 'CONNECTING', { + enumerable: true, + value: readyStates.indexOf('CONNECTING') +}); + +/** + * @constant {Number} CONNECTING + * @memberof WebSocket.prototype + */ +Object.defineProperty(WebSocket.prototype, 'CONNECTING', { + enumerable: true, + value: readyStates.indexOf('CONNECTING') +}); + +/** + * @constant {Number} OPEN + * @memberof WebSocket + */ +Object.defineProperty(WebSocket, 'OPEN', { + enumerable: true, + value: readyStates.indexOf('OPEN') +}); + +/** + * @constant {Number} OPEN + * @memberof WebSocket.prototype + */ +Object.defineProperty(WebSocket.prototype, 'OPEN', { + enumerable: true, + value: readyStates.indexOf('OPEN') +}); + +/** + * @constant {Number} CLOSING + * @memberof WebSocket + */ +Object.defineProperty(WebSocket, 'CLOSING', { + enumerable: true, + value: readyStates.indexOf('CLOSING') +}); + +/** + * @constant {Number} CLOSING + * @memberof WebSocket.prototype + */ +Object.defineProperty(WebSocket.prototype, 'CLOSING', { + enumerable: true, + value: readyStates.indexOf('CLOSING') +}); + +/** + * @constant {Number} CLOSED + * @memberof WebSocket + */ +Object.defineProperty(WebSocket, 'CLOSED', { + enumerable: true, + value: readyStates.indexOf('CLOSED') +}); + +/** + * @constant {Number} CLOSED + * @memberof WebSocket.prototype + */ +Object.defineProperty(WebSocket.prototype, 'CLOSED', { + enumerable: true, + value: readyStates.indexOf('CLOSED') +}); + +[ + 'binaryType', + 'bufferedAmount', + 'extensions', + 'isPaused', + 'protocol', + 'readyState', + 'url' +].forEach((property) => { + Object.defineProperty(WebSocket.prototype, property, { enumerable: true }); +}); + +// +// Add the `onopen`, `onerror`, `onclose`, and `onmessage` attributes. +// See https://html.spec.whatwg.org/multipage/comms.html#the-websocket-interface +// +['open', 'error', 'close', 'message'].forEach((method) => { + Object.defineProperty(WebSocket.prototype, `on${method}`, { + enumerable: true, + get() { + for (const listener of this.listeners(method)) { + if (listener[kForOnEventAttribute]) return listener[kListener]; + } + + return null; + }, + set(handler) { + for (const listener of this.listeners(method)) { + if (listener[kForOnEventAttribute]) { + this.removeListener(method, listener); + break; + } + } + + if (typeof handler !== 'function') return; + + this.addEventListener(method, handler, { + [kForOnEventAttribute]: true + }); + } + }); +}); + +WebSocket.prototype.addEventListener = addEventListener; +WebSocket.prototype.removeEventListener = removeEventListener; + +module.exports = WebSocket; + +/** + * Initialize a WebSocket client. + * + * @param {WebSocket} websocket The client to initialize + * @param {(String|URL)} address The URL to which to connect + * @param {Array} protocols The subprotocols + * @param {Object} [options] Connection options + * @param {Boolean} [options.allowSynchronousEvents=true] Specifies whether any + * of the `'message'`, `'ping'`, and `'pong'` events can be emitted multiple + * times in the same tick + * @param {Boolean} [options.autoPong=true] Specifies whether or not to + * automatically send a pong in response to a ping + * @param {Function} [options.finishRequest] A function which can be used to + * customize the headers of each http request before it is sent + * @param {Boolean} [options.followRedirects=false] Whether or not to follow + * redirects + * @param {Function} [options.generateMask] The function used to generate the + * masking key + * @param {Number} [options.handshakeTimeout] Timeout in milliseconds for the + * handshake request + * @param {Number} [options.maxPayload=104857600] The maximum allowed message + * size + * @param {Number} [options.maxRedirects=10] The maximum number of redirects + * allowed + * @param {String} [options.origin] Value of the `Origin` or + * `Sec-WebSocket-Origin` header + * @param {(Boolean|Object)} [options.perMessageDeflate=true] Enable/disable + * permessage-deflate + * @param {Number} [options.protocolVersion=13] Value of the + * `Sec-WebSocket-Version` header + * @param {Boolean} [options.skipUTF8Validation=false] Specifies whether or + * not to skip UTF-8 validation for text and close messages + * @private + */ +function initAsClient(websocket, address, protocols, options) { + const opts = { + allowSynchronousEvents: true, + autoPong: true, + protocolVersion: protocolVersions[1], + maxPayload: 100 * 1024 * 1024, + skipUTF8Validation: false, + perMessageDeflate: true, + followRedirects: false, + maxRedirects: 10, + ...options, + socketPath: undefined, + hostname: undefined, + protocol: undefined, + timeout: undefined, + method: 'GET', + host: undefined, + path: undefined, + port: undefined + }; + + websocket._autoPong = opts.autoPong; + + if (!protocolVersions.includes(opts.protocolVersion)) { + throw new RangeError( + `Unsupported protocol version: ${opts.protocolVersion} ` + + `(supported versions: ${protocolVersions.join(', ')})` + ); + } + + let parsedUrl; + + if (address instanceof URL) { + parsedUrl = address; + } else { + try { + parsedUrl = new URL(address); + } catch (e) { + throw new SyntaxError(`Invalid URL: ${address}`); + } + } + + if (parsedUrl.protocol === 'http:') { + parsedUrl.protocol = 'ws:'; + } else if (parsedUrl.protocol === 'https:') { + parsedUrl.protocol = 'wss:'; + } + + websocket._url = parsedUrl.href; + + const isSecure = parsedUrl.protocol === 'wss:'; + const isIpcUrl = parsedUrl.protocol === 'ws+unix:'; + let invalidUrlMessage; + + if (parsedUrl.protocol !== 'ws:' && !isSecure && !isIpcUrl) { + invalidUrlMessage = + 'The URL\'s protocol must be one of "ws:", "wss:", ' + + '"http:", "https:", or "ws+unix:"'; + } else if (isIpcUrl && !parsedUrl.pathname) { + invalidUrlMessage = "The URL's pathname is empty"; + } else if (parsedUrl.hash) { + invalidUrlMessage = 'The URL contains a fragment identifier'; + } + + if (invalidUrlMessage) { + const err = new SyntaxError(invalidUrlMessage); + + if (websocket._redirects === 0) { + throw err; + } else { + emitErrorAndClose(websocket, err); + return; + } + } + + const defaultPort = isSecure ? 443 : 80; + const key = randomBytes(16).toString('base64'); + const request = isSecure ? https.request : http.request; + const protocolSet = new Set(); + let perMessageDeflate; + + opts.createConnection = + opts.createConnection || (isSecure ? tlsConnect : netConnect); + opts.defaultPort = opts.defaultPort || defaultPort; + opts.port = parsedUrl.port || defaultPort; + opts.host = parsedUrl.hostname.startsWith('[') + ? parsedUrl.hostname.slice(1, -1) + : parsedUrl.hostname; + opts.headers = { + ...opts.headers, + 'Sec-WebSocket-Version': opts.protocolVersion, + 'Sec-WebSocket-Key': key, + Connection: 'Upgrade', + Upgrade: 'websocket' + }; + opts.path = parsedUrl.pathname + parsedUrl.search; + opts.timeout = opts.handshakeTimeout; + + if (opts.perMessageDeflate) { + perMessageDeflate = new PerMessageDeflate( + opts.perMessageDeflate !== true ? opts.perMessageDeflate : {}, + false, + opts.maxPayload + ); + opts.headers['Sec-WebSocket-Extensions'] = format({ + [PerMessageDeflate.extensionName]: perMessageDeflate.offer() + }); + } + if (protocols.length) { + for (const protocol of protocols) { + if ( + typeof protocol !== 'string' || + !subprotocolRegex.test(protocol) || + protocolSet.has(protocol) + ) { + throw new SyntaxError( + 'An invalid or duplicated subprotocol was specified' + ); + } + + protocolSet.add(protocol); + } + + opts.headers['Sec-WebSocket-Protocol'] = protocols.join(','); + } + if (opts.origin) { + if (opts.protocolVersion < 13) { + opts.headers['Sec-WebSocket-Origin'] = opts.origin; + } else { + opts.headers.Origin = opts.origin; + } + } + if (parsedUrl.username || parsedUrl.password) { + opts.auth = `${parsedUrl.username}:${parsedUrl.password}`; + } + + if (isIpcUrl) { + const parts = opts.path.split(':'); + + opts.socketPath = parts[0]; + opts.path = parts[1]; + } + + let req; + + if (opts.followRedirects) { + if (websocket._redirects === 0) { + websocket._originalIpc = isIpcUrl; + websocket._originalSecure = isSecure; + websocket._originalHostOrSocketPath = isIpcUrl + ? opts.socketPath + : parsedUrl.host; + + const headers = options && options.headers; + + // + // Shallow copy the user provided options so that headers can be changed + // without mutating the original object. + // + options = { ...options, headers: {} }; + + if (headers) { + for (const [key, value] of Object.entries(headers)) { + options.headers[key.toLowerCase()] = value; + } + } + } else if (websocket.listenerCount('redirect') === 0) { + const isSameHost = isIpcUrl + ? websocket._originalIpc + ? opts.socketPath === websocket._originalHostOrSocketPath + : false + : websocket._originalIpc + ? false + : parsedUrl.host === websocket._originalHostOrSocketPath; + + if (!isSameHost || (websocket._originalSecure && !isSecure)) { + // + // Match curl 7.77.0 behavior and drop the following headers. These + // headers are also dropped when following a redirect to a subdomain. + // + delete opts.headers.authorization; + delete opts.headers.cookie; + + if (!isSameHost) delete opts.headers.host; + + opts.auth = undefined; + } + } + + // + // Match curl 7.77.0 behavior and make the first `Authorization` header win. + // If the `Authorization` header is set, then there is nothing to do as it + // will take precedence. + // + if (opts.auth && !options.headers.authorization) { + options.headers.authorization = + 'Basic ' + Buffer.from(opts.auth).toString('base64'); + } + + req = websocket._req = request(opts); + + if (websocket._redirects) { + // + // Unlike what is done for the `'upgrade'` event, no early exit is + // triggered here if the user calls `websocket.close()` or + // `websocket.terminate()` from a listener of the `'redirect'` event. This + // is because the user can also call `request.destroy()` with an error + // before calling `websocket.close()` or `websocket.terminate()` and this + // would result in an error being emitted on the `request` object with no + // `'error'` event listeners attached. + // + websocket.emit('redirect', websocket.url, req); + } + } else { + req = websocket._req = request(opts); + } + + if (opts.timeout) { + req.on('timeout', () => { + abortHandshake(websocket, req, 'Opening handshake has timed out'); + }); + } + + req.on('error', (err) => { + if (req === null || req[kAborted]) return; + + req = websocket._req = null; + emitErrorAndClose(websocket, err); + }); + + req.on('response', (res) => { + const location = res.headers.location; + const statusCode = res.statusCode; + + if ( + location && + opts.followRedirects && + statusCode >= 300 && + statusCode < 400 + ) { + if (++websocket._redirects > opts.maxRedirects) { + abortHandshake(websocket, req, 'Maximum redirects exceeded'); + return; + } + + req.abort(); + + let addr; + + try { + addr = new URL(location, address); + } catch (e) { + const err = new SyntaxError(`Invalid URL: ${location}`); + emitErrorAndClose(websocket, err); + return; + } + + initAsClient(websocket, addr, protocols, options); + } else if (!websocket.emit('unexpected-response', req, res)) { + abortHandshake( + websocket, + req, + `Unexpected server response: ${res.statusCode}` + ); + } + }); + + req.on('upgrade', (res, socket, head) => { + websocket.emit('upgrade', res); + + // + // The user may have closed the connection from a listener of the + // `'upgrade'` event. + // + if (websocket.readyState !== WebSocket.CONNECTING) return; + + req = websocket._req = null; + + const upgrade = res.headers.upgrade; + + if (upgrade === undefined || upgrade.toLowerCase() !== 'websocket') { + abortHandshake(websocket, socket, 'Invalid Upgrade header'); + return; + } + + const digest = createHash('sha1') + .update(key + GUID) + .digest('base64'); + + if (res.headers['sec-websocket-accept'] !== digest) { + abortHandshake(websocket, socket, 'Invalid Sec-WebSocket-Accept header'); + return; + } + + const serverProt = res.headers['sec-websocket-protocol']; + let protError; + + if (serverProt !== undefined) { + if (!protocolSet.size) { + protError = 'Server sent a subprotocol but none was requested'; + } else if (!protocolSet.has(serverProt)) { + protError = 'Server sent an invalid subprotocol'; + } + } else if (protocolSet.size) { + protError = 'Server sent no subprotocol'; + } + + if (protError) { + abortHandshake(websocket, socket, protError); + return; + } + + if (serverProt) websocket._protocol = serverProt; + + const secWebSocketExtensions = res.headers['sec-websocket-extensions']; + + if (secWebSocketExtensions !== undefined) { + if (!perMessageDeflate) { + const message = + 'Server sent a Sec-WebSocket-Extensions header but no extension ' + + 'was requested'; + abortHandshake(websocket, socket, message); + return; + } + + let extensions; + + try { + extensions = parse(secWebSocketExtensions); + } catch (err) { + const message = 'Invalid Sec-WebSocket-Extensions header'; + abortHandshake(websocket, socket, message); + return; + } + + const extensionNames = Object.keys(extensions); + + if ( + extensionNames.length !== 1 || + extensionNames[0] !== PerMessageDeflate.extensionName + ) { + const message = 'Server indicated an extension that was not requested'; + abortHandshake(websocket, socket, message); + return; + } + + try { + perMessageDeflate.accept(extensions[PerMessageDeflate.extensionName]); + } catch (err) { + const message = 'Invalid Sec-WebSocket-Extensions header'; + abortHandshake(websocket, socket, message); + return; + } + + websocket._extensions[PerMessageDeflate.extensionName] = + perMessageDeflate; + } + + websocket.setSocket(socket, head, { + allowSynchronousEvents: opts.allowSynchronousEvents, + generateMask: opts.generateMask, + maxPayload: opts.maxPayload, + skipUTF8Validation: opts.skipUTF8Validation + }); + }); + + if (opts.finishRequest) { + opts.finishRequest(req, websocket); + } else { + req.end(); + } +} + +/** + * Emit the `'error'` and `'close'` events. + * + * @param {WebSocket} websocket The WebSocket instance + * @param {Error} The error to emit + * @private + */ +function emitErrorAndClose(websocket, err) { + websocket._readyState = WebSocket.CLOSING; + // + // The following assignment is practically useless and is done only for + // consistency. + // + websocket._errorEmitted = true; + websocket.emit('error', err); + websocket.emitClose(); +} + +/** + * Create a `net.Socket` and initiate a connection. + * + * @param {Object} options Connection options + * @return {net.Socket} The newly created socket used to start the connection + * @private + */ +function netConnect(options) { + options.path = options.socketPath; + return net.connect(options); +} + +/** + * Create a `tls.TLSSocket` and initiate a connection. + * + * @param {Object} options Connection options + * @return {tls.TLSSocket} The newly created socket used to start the connection + * @private + */ +function tlsConnect(options) { + options.path = undefined; + + if (!options.servername && options.servername !== '') { + options.servername = net.isIP(options.host) ? '' : options.host; + } + + return tls.connect(options); +} + +/** + * Abort the handshake and emit an error. + * + * @param {WebSocket} websocket The WebSocket instance + * @param {(http.ClientRequest|net.Socket|tls.Socket)} stream The request to + * abort or the socket to destroy + * @param {String} message The error message + * @private + */ +function abortHandshake(websocket, stream, message) { + websocket._readyState = WebSocket.CLOSING; + + const err = new Error(message); + Error.captureStackTrace(err, abortHandshake); + + if (stream.setHeader) { + stream[kAborted] = true; + stream.abort(); + + if (stream.socket && !stream.socket.destroyed) { + // + // On Node.js >= 14.3.0 `request.abort()` does not destroy the socket if + // called after the request completed. See + // https://github.com/websockets/ws/issues/1869. + // + stream.socket.destroy(); + } + + process.nextTick(emitErrorAndClose, websocket, err); + } else { + stream.destroy(err); + stream.once('error', websocket.emit.bind(websocket, 'error')); + stream.once('close', websocket.emitClose.bind(websocket)); + } +} + +/** + * Handle cases where the `ping()`, `pong()`, or `send()` methods are called + * when the `readyState` attribute is `CLOSING` or `CLOSED`. + * + * @param {WebSocket} websocket The WebSocket instance + * @param {*} [data] The data to send + * @param {Function} [cb] Callback + * @private + */ +function sendAfterClose(websocket, data, cb) { + if (data) { + const length = isBlob(data) ? data.size : toBuffer(data).length; + + // + // The `_bufferedAmount` property is used only when the peer is a client and + // the opening handshake fails. Under these circumstances, in fact, the + // `setSocket()` method is not called, so the `_socket` and `_sender` + // properties are set to `null`. + // + if (websocket._socket) websocket._sender._bufferedBytes += length; + else websocket._bufferedAmount += length; + } + + if (cb) { + const err = new Error( + `WebSocket is not open: readyState ${websocket.readyState} ` + + `(${readyStates[websocket.readyState]})` + ); + process.nextTick(cb, err); + } +} + +/** + * The listener of the `Receiver` `'conclude'` event. + * + * @param {Number} code The status code + * @param {Buffer} reason The reason for closing + * @private + */ +function receiverOnConclude(code, reason) { + const websocket = this[kWebSocket]; + + websocket._closeFrameReceived = true; + websocket._closeMessage = reason; + websocket._closeCode = code; + + if (websocket._socket[kWebSocket] === undefined) return; + + websocket._socket.removeListener('data', socketOnData); + process.nextTick(resume, websocket._socket); + + if (code === 1005) websocket.close(); + else websocket.close(code, reason); +} + +/** + * The listener of the `Receiver` `'drain'` event. + * + * @private + */ +function receiverOnDrain() { + const websocket = this[kWebSocket]; + + if (!websocket.isPaused) websocket._socket.resume(); +} + +/** + * The listener of the `Receiver` `'error'` event. + * + * @param {(RangeError|Error)} err The emitted error + * @private + */ +function receiverOnError(err) { + const websocket = this[kWebSocket]; + + if (websocket._socket[kWebSocket] !== undefined) { + websocket._socket.removeListener('data', socketOnData); + + // + // On Node.js < 14.0.0 the `'error'` event is emitted synchronously. See + // https://github.com/websockets/ws/issues/1940. + // + process.nextTick(resume, websocket._socket); + + websocket.close(err[kStatusCode]); + } + + if (!websocket._errorEmitted) { + websocket._errorEmitted = true; + websocket.emit('error', err); + } +} + +/** + * The listener of the `Receiver` `'finish'` event. + * + * @private + */ +function receiverOnFinish() { + this[kWebSocket].emitClose(); +} + +/** + * The listener of the `Receiver` `'message'` event. + * + * @param {Buffer|ArrayBuffer|Buffer[])} data The message + * @param {Boolean} isBinary Specifies whether the message is binary or not + * @private + */ +function receiverOnMessage(data, isBinary) { + this[kWebSocket].emit('message', data, isBinary); +} + +/** + * The listener of the `Receiver` `'ping'` event. + * + * @param {Buffer} data The data included in the ping frame + * @private + */ +function receiverOnPing(data) { + const websocket = this[kWebSocket]; + + if (websocket._autoPong) websocket.pong(data, !this._isServer, NOOP); + websocket.emit('ping', data); +} + +/** + * The listener of the `Receiver` `'pong'` event. + * + * @param {Buffer} data The data included in the pong frame + * @private + */ +function receiverOnPong(data) { + this[kWebSocket].emit('pong', data); +} + +/** + * Resume a readable stream + * + * @param {Readable} stream The readable stream + * @private + */ +function resume(stream) { + stream.resume(); +} + +/** + * The `Sender` error event handler. + * + * @param {Error} The error + * @private + */ +function senderOnError(err) { + const websocket = this[kWebSocket]; + + if (websocket.readyState === WebSocket.CLOSED) return; + if (websocket.readyState === WebSocket.OPEN) { + websocket._readyState = WebSocket.CLOSING; + setCloseTimer(websocket); + } + + // + // `socket.end()` is used instead of `socket.destroy()` to allow the other + // peer to finish sending queued data. There is no need to set a timer here + // because `CLOSING` means that it is already set or not needed. + // + this._socket.end(); + + if (!websocket._errorEmitted) { + websocket._errorEmitted = true; + websocket.emit('error', err); + } +} + +/** + * Set a timer to destroy the underlying raw socket of a WebSocket. + * + * @param {WebSocket} websocket The WebSocket instance + * @private + */ +function setCloseTimer(websocket) { + websocket._closeTimer = setTimeout( + websocket._socket.destroy.bind(websocket._socket), + closeTimeout + ); +} + +/** + * The listener of the socket `'close'` event. + * + * @private + */ +function socketOnClose() { + const websocket = this[kWebSocket]; + + this.removeListener('close', socketOnClose); + this.removeListener('data', socketOnData); + this.removeListener('end', socketOnEnd); + + websocket._readyState = WebSocket.CLOSING; + + let chunk; + + // + // The close frame might not have been received or the `'end'` event emitted, + // for example, if the socket was destroyed due to an error. Ensure that the + // `receiver` stream is closed after writing any remaining buffered data to + // it. If the readable side of the socket is in flowing mode then there is no + // buffered data as everything has been already written and `readable.read()` + // will return `null`. If instead, the socket is paused, any possible buffered + // data will be read as a single chunk. + // + if ( + !this._readableState.endEmitted && + !websocket._closeFrameReceived && + !websocket._receiver._writableState.errorEmitted && + (chunk = websocket._socket.read()) !== null + ) { + websocket._receiver.write(chunk); + } + + websocket._receiver.end(); + + this[kWebSocket] = undefined; + + clearTimeout(websocket._closeTimer); + + if ( + websocket._receiver._writableState.finished || + websocket._receiver._writableState.errorEmitted + ) { + websocket.emitClose(); + } else { + websocket._receiver.on('error', receiverOnFinish); + websocket._receiver.on('finish', receiverOnFinish); + } +} + +/** + * The listener of the socket `'data'` event. + * + * @param {Buffer} chunk A chunk of data + * @private + */ +function socketOnData(chunk) { + if (!this[kWebSocket]._receiver.write(chunk)) { + this.pause(); + } +} + +/** + * The listener of the socket `'end'` event. + * + * @private + */ +function socketOnEnd() { + const websocket = this[kWebSocket]; + + websocket._readyState = WebSocket.CLOSING; + websocket._receiver.end(); + this.end(); +} + +/** + * The listener of the socket `'error'` event. + * + * @private + */ +function socketOnError() { + const websocket = this[kWebSocket]; + + this.removeListener('error', socketOnError); + this.on('error', NOOP); + + if (websocket) { + websocket._readyState = WebSocket.CLOSING; + this.destroy(); + } +} diff --git a/Streamdeck/com.mirabox.streamingle.sdPlugin/node_modules/ws/package.json b/Streamdeck/com.mirabox.streamingle.sdPlugin/node_modules/ws/package.json new file mode 100644 index 00000000..9f14239e --- /dev/null +++ b/Streamdeck/com.mirabox.streamingle.sdPlugin/node_modules/ws/package.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:aaedef2a72b60db8fb36d9b46c48d44986051785a2b6450c62994603c85dd959 +size 1723 diff --git a/Streamdeck/com.mirabox.streamingle.sdPlugin/node_modules/ws/wrapper.mjs b/Streamdeck/com.mirabox.streamingle.sdPlugin/node_modules/ws/wrapper.mjs new file mode 100644 index 00000000..7245ad15 --- /dev/null +++ b/Streamdeck/com.mirabox.streamingle.sdPlugin/node_modules/ws/wrapper.mjs @@ -0,0 +1,8 @@ +import createWebSocketStream from './lib/stream.js'; +import Receiver from './lib/receiver.js'; +import Sender from './lib/sender.js'; +import WebSocket from './lib/websocket.js'; +import WebSocketServer from './lib/websocket-server.js'; + +export { createWebSocketStream, Receiver, Sender, WebSocket, WebSocketServer }; +export default WebSocket; diff --git a/Streamdeck/com.mirabox.streamingle.sdPlugin/package-lock.json b/Streamdeck/com.mirabox.streamingle.sdPlugin/package-lock.json new file mode 100644 index 00000000..1bcaffc7 --- /dev/null +++ b/Streamdeck/com.mirabox.streamingle.sdPlugin/package-lock.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6366bf57fe6a33384ba7adf3be5c88202b46a8b245ed0daf29c901d207dfe23a +size 926 diff --git a/Streamdeck/com.mirabox.streamingle.sdPlugin/package.json b/Streamdeck/com.mirabox.streamingle.sdPlugin/package.json new file mode 100644 index 00000000..a00b1619 --- /dev/null +++ b/Streamdeck/com.mirabox.streamingle.sdPlugin/package.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9049f13bb5d188b24222abd6b76d1e7d3e66821f80988a9c5e69431f7a21a8b3 +size 672 diff --git a/Streamdeck/com.mirabox.streamingle.sdPlugin/plugin/index.html b/Streamdeck/com.mirabox.streamingle.sdPlugin/plugin/index.html new file mode 100644 index 00000000..b7cefb2e --- /dev/null +++ b/Streamdeck/com.mirabox.streamingle.sdPlugin/plugin/index.html @@ -0,0 +1,10 @@ + + + + + Streamingle Plugin + + + + + \ No newline at end of file diff --git a/Streamdeck/com.mirabox.streamingle.sdPlugin/plugin/index.js b/Streamdeck/com.mirabox.streamingle.sdPlugin/plugin/index.js new file mode 100644 index 00000000..5b201e28 --- /dev/null +++ b/Streamdeck/com.mirabox.streamingle.sdPlugin/plugin/index.js @@ -0,0 +1,825 @@ +// MiraBox StreamDock 플러그인 - 브라우저 기반 +console.log('🚀 Streamingle 플러그인 시작 (브라우저 기반)'); + +// Global variables +let websocket = null; +let buttonContexts = new Map(); // 각 버튼의 컨텍스트별 설정 저장 +let unitySocket = null; +let isUnityConnected = false; +let cameraList = []; // 카메라 목록 저장 +let itemList = []; // 아이템 목록 저장 + +// StreamDock 연결 함수 (브라우저 기반) +function connectElgatoStreamDeckSocket(inPort, inUUID, inEvent, inInfo, inActionInfo) { + console.log('🔌 StreamDock 연결 시작 (브라우저)'); + console.log('📡 포트:', inPort, 'UUID:', inUUID, '이벤트:', inEvent); + + // Parse action info to get initial settings + try { + if (inActionInfo) { + const actionInfo = JSON.parse(inActionInfo); + const context = actionInfo.context; + const settings = actionInfo.payload?.settings || {}; + + // 각 컨텍스트별로 설정 저장 + buttonContexts.set(context, settings); + console.log('⚙️ 초기 설정 저장:', settings); + } + } catch (error) { + console.error('❌ ActionInfo 파싱 오류:', error); + } + + // WebSocket 연결 + if (!websocket) { + websocket = new WebSocket('ws://localhost:' + inPort); + + websocket.onopen = function() { + console.log('✅ StreamDock 연결됨 (브라우저)'); + + // StreamDock에 등록 + websocket.send(JSON.stringify({ + event: inEvent, + uuid: inUUID + })); + + // Unity 연결 시도 + setTimeout(() => { + connectToUnity(); + }, 1000); + + // Unity 연결 재시도 (5초마다) + setInterval(() => { + if (!isUnityConnected) { + console.log('🔄 Unity 재연결 시도...'); + connectToUnity(); + } + }, 5000); + }; + + websocket.onmessage = function(evt) { + try { + const jsonObj = JSON.parse(evt.data); + console.log('📨 StreamDock 메시지 수신:', jsonObj.event); + console.log('📋 전체 메시지 데이터:', JSON.stringify(jsonObj, null, 2)); + + switch(jsonObj.event) { + case 'didReceiveSettings': + if (jsonObj.payload && jsonObj.context) { + const newSettings = jsonObj.payload.settings || {}; + buttonContexts.set(jsonObj.context, newSettings); + console.log('⚙️ 설정 업데이트:', newSettings); + updateButtonTitle(jsonObj.context); + } + break; + case 'willAppear': + console.log('👀 버튼 나타남:', jsonObj.context); + + let settings = jsonObj.payload?.settings || {}; + + // action UUID로 actionType 결정 + if (jsonObj.action === 'com.mirabox.streamingle.item') { + settings.actionType = 'item'; + console.log('🎯 아이템 컨트롤러 등록:', jsonObj.context); + } else { + settings.actionType = 'camera'; + console.log('📹 카메라 컨트롤러 등록:', jsonObj.context); + } + + buttonContexts.set(jsonObj.context, settings); + updateButtonTitle(jsonObj.context); + + // Unity가 이미 연결되어 있다면 Property Inspector에 상태 전송 + if (isUnityConnected) { + const settings = getCurrentSettings(jsonObj.context); + const actionType = settings.actionType || 'camera'; + let actionUUID = 'com.mirabox.streamingle.camera'; + if (actionType === 'item') { + actionUUID = 'com.mirabox.streamingle.item'; + } + + sendToPropertyInspector(jsonObj.context, 'unity_connected', { connected: true }, actionUUID); + } + break; + + case 'keyUp': + console.log('🔘 버튼 클릭됨!'); + handleButtonClick(jsonObj.context); + break; + + case 'sendToPlugin': + handlePropertyInspectorMessage(jsonObj.payload, jsonObj.context, jsonObj.action); + break; + + default: + console.log('❓ 알 수 없는 이벤트:', jsonObj.event); + break; + } + } catch (error) { + console.error('❌ 메시지 파싱 오류:', error); + } + }; + + websocket.onclose = function() { + console.log('❌ StreamDock 연결 끊어짐'); + websocket = null; + }; + + websocket.onerror = function(error) { + console.error('❌ StreamDock 연결 오류:', error); + }; + } +} + +// Unity 연결 함수 (브라우저 네이티브 WebSocket 사용) +function connectToUnity() { + console.log('🔌 Unity 연결 시도 (브라우저)...'); + + if (unitySocket) { + unitySocket.close(); + } + + try { + unitySocket = new WebSocket('ws://localhost:10701'); + + unitySocket.onopen = function() { + isUnityConnected = true; + console.log('✅ Unity 연결 성공 (브라우저)!'); + + // 카메라 목록 요청 + setTimeout(() => { + const message = JSON.stringify({ type: 'get_camera_list' }); + unitySocket.send(message); + console.log('📋 카메라 목록 요청:', message); + }, 100); + + // 아이템 목록 요청 + setTimeout(() => { + const message = JSON.stringify({ type: 'get_item_list' }); + unitySocket.send(message); + console.log('📋 아이템 목록 요청:', message); + }, 200); + }; + + unitySocket.onmessage = function(event) { + try { + const data = JSON.parse(event.data); + console.log('📨 Unity 메시지 수신:', data.type); + console.log('📋 Unity 메시지 전체 데이터:', JSON.stringify(data, null, 2)); + handleUnityMessage(data); + } catch (error) { + console.error('❌ Unity 메시지 파싱 오류:', error); + } + }; + + unitySocket.onclose = function() { + isUnityConnected = false; + console.log('❌ Unity 연결 끊어짐'); + + // Property Inspector들에게 Unity 연결 해제 알림 + for (const context of buttonContexts.keys()) { + // context별로 적절한 action UUID 결정 + const settings = getCurrentSettings(context); + const actionType = settings.actionType || 'camera'; + let actionUUID = 'com.mirabox.streamingle.camera'; + if (actionType === 'item') { + actionUUID = 'com.mirabox.streamingle.item'; + } + sendToPropertyInspector(context, 'unity_disconnected', { connected: false }, actionUUID); + } + }; + + unitySocket.onerror = function(error) { + console.error('❌ Unity 연결 오류:', error); + console.log('🔍 Unity가 실행 중인지 확인하세요 (포트 10701)'); + }; +} catch (error) { + console.error('❌ Unity 연결 설정 오류:', error); + } +} + +// 버튼 클릭 처리 +function handleButtonClick(context) { + console.log('🎯 버튼 클릭 처리 시작'); + console.log('📍 컨텍스트:', context); + console.log('🔌 Unity 연결 상태:', isUnityConnected); + + if (!isUnityConnected || !unitySocket) { + console.error('❌ Unity 연결되지 않음'); + return; + } + + // context별 settings 사용 + const settings = getCurrentSettings(context); + const actionType = settings.actionType || 'camera'; // 기본값은 camera + + console.log('🎯 액션 타입:', actionType); + + switch (actionType) { + case 'camera': + handleCameraAction(settings); + break; + case 'item': + handleItemAction(settings); + break; + default: + console.log('⚠️ 알 수 없는 액션 타입:', actionType); + // 기본적으로 카메라 액션으로 처리 + handleCameraAction(settings); + } +} + +// 카메라 액션 처리 +function handleCameraAction(settings) { + let cameraIndex = settings.cameraIndex; + + // 카메라 인덱스가 설정되지 않았으면 0 사용 + if (typeof cameraIndex !== 'number') { + cameraIndex = 0; + console.log('⚠️ 카메라 인덱스가 설정되지 않음, 기본값 0 사용'); + } + + console.log('📹 전환할 카메라 인덱스:', cameraIndex); + + // Unity에 카메라 전환 요청 + const message = JSON.stringify({ + type: 'switch_camera', + data: { + camera_index: cameraIndex + } + }); + + console.log('📤 Unity에 카메라 전환 요청 전송:', message); + console.log('🔍 Unity 연결 상태:', isUnityConnected); + console.log('🔍 Unity 소켓 상태:', !!unitySocket); + + if (unitySocket && unitySocket.readyState === WebSocket.OPEN) { + unitySocket.send(message); + console.log('✅ 메시지 전송 완료'); + } else { + console.error('❌ Unity 소켓이 연결되지 않음'); + } +} + +// 아이템 액션 처리 +function handleItemAction(settings) { + let itemIndex = settings.itemIndex; + const itemAction = settings.itemAction || 'toggle'; // 기본값은 toggle + + // 아이템 인덱스가 설정되지 않았으면 0 사용 + if (typeof itemIndex !== 'number') { + itemIndex = 0; + console.log('⚠️ 아이템 인덱스가 설정되지 않음, 기본값 0 사용'); + } + + console.log('🎯 아이템 액션:', itemAction, '인덱스:', itemIndex); + + let messageType = 'toggle_item'; + if (itemAction === 'set') { + messageType = 'set_item'; + } + + // Unity에 아이템 액션 요청 + const message = JSON.stringify({ + type: messageType, + data: { + item_index: itemIndex + } + }); + + console.log('📤 Unity에 아이템 액션 요청 전송:', message); + console.log('🔍 Unity 연결 상태:', isUnityConnected); + console.log('🔍 Unity 소켓 상태:', !!unitySocket); + + if (unitySocket && unitySocket.readyState === WebSocket.OPEN) { + unitySocket.send(message); + console.log('✅ 메시지 전송 완료'); + } else { + console.error('❌ Unity 소켓이 연결되지 않음'); + } +} + +// Property Inspector 메시지 처리 +function handlePropertyInspectorMessage(payload, context, actionUUID) { + const command = payload.command; + console.log('📤 Property Inspector 명령 처리:', command, 'action:', actionUUID); + switch (command) { + case 'get_unity_status': + console.log('📡 Unity 상태 요청 - context:', context, 'action:', actionUUID); + console.log('📡 Unity 연결 상태:', isUnityConnected); + console.log('📡 카메라 목록 길이:', cameraList.length); + console.log('📡 아이템 목록 길이:', itemList.length); + + sendToPropertyInspector(context, 'unity_connected', { connected: isUnityConnected }, actionUUID); + if (isUnityConnected) { + if (cameraList.length > 0) { + // 현재 활성 카메라 인덱스 찾기 + const currentCameraIndex = cameraList.findIndex(cam => cam.isActive) || 0; + console.log('📡 현재 활성 카메라 인덱스:', currentCameraIndex); + sendToPropertyInspector(context, 'camera_list', { + cameras: cameraList, + currentIndex: currentCameraIndex + }, actionUUID); + } + if (itemList.length > 0) { + // 현재 활성 아이템 인덱스 찾기 + const currentItemIndex = itemList.findIndex(item => item.isActive) || 0; + console.log('📡 현재 활성 아이템 인덱스:', currentItemIndex); + sendToPropertyInspector(context, 'item_list', { + items: itemList, + currentIndex: currentItemIndex + }, actionUUID); + } + } else { + // Unity가 연결되지 않은 경우에도 빈 목록 전송 + console.log('📡 Unity 연결 안됨 - 빈 목록 전송'); + sendToPropertyInspector(context, 'camera_list', { + cameras: [], + currentIndex: 0 + }, actionUUID); + sendToPropertyInspector(context, 'item_list', { + items: [], + currentIndex: 0 + }, actionUUID); + } + break; + case 'get_camera_list': + console.log('📹 카메라 목록 요청'); + if (isUnityConnected && unitySocket) { + const message = JSON.stringify({ type: 'get_camera_list' }); + unitySocket.send(message); + console.log('📤 Unity에 카메라 목록 요청 전송'); + } + break; + case 'get_item_list': + console.log('🎯 아이템 목록 요청'); + if (isUnityConnected && unitySocket) { + const message = JSON.stringify({ type: 'get_item_list' }); + unitySocket.send(message); + console.log('📤 Unity에 아이템 목록 요청 전송'); + } + break; + case 'switch_camera': + console.log('📹 카메라 전환 요청:', payload.cameraIndex); + if (isUnityConnected && unitySocket) { + const message = JSON.stringify({ + type: 'switch_camera', + data: { camera_index: payload.cameraIndex } + }); + unitySocket.send(message); + console.log('📤 Unity에 카메라 전환 요청 전송'); + } + break; + case 'toggle_item': + console.log('🎯 아이템 토글 요청:', payload.itemIndex); + if (isUnityConnected && unitySocket) { + const message = JSON.stringify({ + type: 'toggle_item', + data: { item_index: payload.itemIndex } + }); + unitySocket.send(message); + console.log('📤 Unity에 아이템 토글 요청 전송'); + } + break; + case 'set_item': + console.log('🎯 아이템 설정 요청:', payload.itemIndex); + if (isUnityConnected && unitySocket) { + const message = JSON.stringify({ + type: 'set_item', + data: { item_index: payload.itemIndex } + }); + unitySocket.send(message); + console.log('📤 Unity에 아이템 설정 요청 전송'); + } + break; + case 'refresh_camera_list': + console.log('🔄 카메라 목록 새로고침 요청'); + if (isUnityConnected && unitySocket) { + const message = JSON.stringify({ type: 'get_camera_list' }); + unitySocket.send(message); + console.log('📤 Unity에 카메라 목록 요청 전송'); + } + break; + case 'refresh_item_list': + console.log('🔄 아이템 목록 새로고침 요청'); + if (isUnityConnected && unitySocket) { + const message = JSON.stringify({ type: 'get_item_list' }); + unitySocket.send(message); + console.log('📤 Unity에 아이템 목록 요청 전송'); + } + break; + case 'update_title': + console.log('🏷️ 버튼 제목 업데이트 요청'); + updateButtonTitle(context); + break; + default: + console.log('❓ 알 수 없는 Property Inspector 명령:', command); + } +} + +// Property Inspector로 메시지 전송 (action UUID 지정 가능) +function sendToPropertyInspector(context, event, payload, actionUUID = null) { + console.log('📤 Property Inspector로 메시지 전송 시작 - context:', context, 'event:', event); + + if (!websocket || !context) { + console.log('🚫 WebSocket 또는 context 없음 - Property Inspector 메시지 전송 건너뜀'); + console.log('🔍 websocket 상태:', !!websocket); + console.log('🔍 context:', context); + return; + } + + // action UUID 결정 + let action = actionUUID; + if (!action) { + // Property Inspector에서 보내는 메시지의 action UUID를 그대로 사용 + // 카메라 컨트롤러 Property Inspector는 'com.mirabox.streamingle.camera'를 사용 + // 아이템 컨트롤러 Property Inspector는 'com.mirabox.streamingle.item'을 사용 + action = 'com.mirabox.streamingle.camera'; // 기본값 + + // context별로 적절한 action 결정 + const settings = getCurrentSettings(context); + console.log('📤 context 설정:', settings); + const actionType = settings.actionType || 'camera'; + + if (actionType === 'item') { + action = 'com.mirabox.streamingle.item'; + } + } + + console.log('📤 결정된 action:', action); + + const message = { + action: action, + event: 'sendToPropertyInspector', + context: context, + payload: { + event: event, + ...payload + } + }; + console.log('📤 Property Inspector로 전송할 메시지:', JSON.stringify(message, null, 2)); + websocket.send(JSON.stringify(message)); + console.log('✅ Property Inspector로 메시지 전송 완료:', event, payload); +} + +// Unity 메시지 처리 +function handleUnityMessage(data) { + console.log('📨 Unity 메시지 수신:', data.type); + console.log('📋 전체 메시지 데이터:', JSON.stringify(data, null, 2)); + + switch (data.type) { + case 'connection_established': + console.log('🎉 Unity 연결 확인됨'); + console.log('📋 connection_established 데이터:', JSON.stringify(data, null, 2)); + + // 연결 시 카메라 및 아이템 데이터 저장 + if (data.data) { + // 카메라 데이터 처리 + if (data.data.camera_data) { + let cameras = data.data.camera_data.presets || data.data.camera_data; + if (Array.isArray(cameras)) { + cameraList = cameras; + console.log('📹 카메라 목록 저장됨:', cameraList.length, '개'); + } else { + cameraList = []; + console.log('⚠️ 카메라 데이터가 배열이 아님:', cameras); + } + } + + // 아이템 데이터 처리 + if (data.data.item_data) { + let items = data.data.item_data.items || data.data.item_data; + if (Array.isArray(items)) { + itemList = items; + console.log('🎯 아이템 목록 저장됨:', itemList.length, '개'); + } else { + itemList = []; + console.log('⚠️ 아이템 데이터가 배열이 아님:', items); + } + } + + updateAllButtonTitles(); + + // Property Inspector들에게 Unity 연결 상태 알림 + console.log('📤 Property Inspector들에게 Unity 연결 알림 전송 시작'); + console.log('📤 현재 등록된 컨텍스트들:', Array.from(buttonContexts.keys())); + + if (buttonContexts.size === 0) { + console.log('⚠️ 등록된 컨텍스트가 없음 - Property Inspector가 아직 연결되지 않았을 수 있음'); + } else { + console.log('📤 등록된 컨텍스트들:', Array.from(buttonContexts.keys())); + for (const context of buttonContexts.keys()) { + console.log('📤 Property Inspector로 전송할 컨텍스트:', context); + + // context별로 적절한 action UUID 결정 + const settings = getCurrentSettings(context); + const actionType = settings.actionType || 'camera'; + let actionUUID = 'com.mirabox.streamingle.camera'; + if (actionType === 'item') { + actionUUID = 'com.mirabox.streamingle.item'; + } + + console.log('🔍 컨텍스트 분석:', context, 'Action Type:', actionType, 'Action UUID:', actionUUID); + sendToPropertyInspector(context, 'unity_connected', { connected: true }, actionUUID); + + // action UUID에 따라 적절한 데이터만 전송 + if (actionUUID === 'com.mirabox.streamingle.camera') { + // 카메라 컨트롤러에는 카메라 데이터만 전송 + let currentCameraIndex = 0; + if (typeof data.data.camera_data?.current_index === 'number' && data.data.camera_data.current_index >= 0) { + currentCameraIndex = data.data.camera_data.current_index; + } + console.log('📹 카메라 컨트롤러에 카메라 데이터 전송:', context, '카메라 수:', cameraList.length); + sendToPropertyInspector(context, 'camera_list', { + cameras: cameraList, + currentIndex: currentCameraIndex + }, actionUUID); + } else if (actionUUID === 'com.mirabox.streamingle.item') { + // 아이템 컨트롤러에는 아이템 데이터만 전송 + let currentItemIndex = 0; + if (typeof data.data.item_data?.current_index === 'number' && data.data.item_data.current_index >= 0) { + currentItemIndex = data.data.item_data.current_index; + } + console.log('🎯 아이템 컨트롤러에 아이템 데이터 전송:', context, '아이템 수:', itemList.length); + sendToPropertyInspector(context, 'item_list', { + items: itemList, + currentIndex: currentItemIndex + }, actionUUID); + } + } + console.log('✅ Property Inspector들에게 Unity 연결 알림 전송 완료'); + } + } else { + console.log('⚠️ Unity 연결 시 데이터가 없음'); + console.log('📋 data.data:', data.data); + } + break; + + case 'camera_list_response': + console.log('📹 카메라 목록 수신'); + + if (data.data && data.data.camera_data) { + let cameras = data.data.camera_data.presets || data.data.camera_data; + + if (Array.isArray(cameras)) { + cameraList = cameras; + const currentIndex = data.data.current_camera ?? data.data.camera_data?.current_index ?? 0; + console.log('📹 카메라 목록 업데이트됨:', cameraList.length, '개'); + updateAllButtonTitles(); + + // Property Inspector들에게 카메라 목록 전송 (카메라 컨트롤러만) + for (const context of buttonContexts.keys()) { + const settings = getCurrentSettings(context); + const actionType = settings.actionType || 'camera'; + + if (actionType === 'camera') { + sendToPropertyInspector(context, 'camera_list', { + cameras: cameraList, + currentIndex: currentIndex + }, 'com.mirabox.streamingle.camera'); + } + } + } else { + console.log('⚠️ 카메라 목록 응답에서 카메라 데이터가 배열이 아님'); + console.log('📋 cameras:', cameras); + } + } + break; + + case 'camera_changed': + console.log('🎯 카메라 변경 알림'); + break; + + case 'item_changed': + console.log('🎯 아이템 변경 알림'); + if (data.data && data.data.item_data) { + let items = data.data.item_data.items || data.data.item_data; + + if (Array.isArray(items)) { + itemList = items; + console.log('🎯 아이템 목록 업데이트됨:', itemList.length, '개'); + updateAllButtonTitles(); + + // Property Inspector들에게 아이템 목록 전송 (아이템 컨트롤러만) + for (const context of buttonContexts.keys()) { + const settings = getCurrentSettings(context); + const actionType = settings.actionType || 'camera'; + + if (actionType === 'item') { + sendToPropertyInspector(context, 'item_list', { + items: itemList, + currentIndex: data.data.item_data?.current_index || 0 + }, 'com.mirabox.streamingle.item'); + + // 아이템 상태에 따라 버튼 상태도 업데이트 + const itemIndex = settings.itemIndex || 0; + if (itemList[itemIndex]) { + const isActive = itemList[itemIndex].isActive !== false; // 기본값은 true + setButtonState(context, isActive); + console.log('🎨 아이템 변경으로 상태 업데이트:', context, '아이템 인덱스:', itemIndex, '활성:', isActive); + } + } + } + } else { + console.log('⚠️ 아이템 목록 응답에서 아이템 데이터가 배열이 아님'); + console.log('📋 items:', items); + } + } + break; + + case 'item_list_response': + console.log('🎯 아이템 목록 수신'); + + if (data.data && data.data.item_data) { + let items = data.data.item_data.items || data.data.item_data; + + if (Array.isArray(items)) { + itemList = items; + const currentIndex = data.data.current_item ?? data.data.item_data?.current_index ?? 0; + console.log('🎯 아이템 목록 업데이트됨:', itemList.length, '개'); + updateAllButtonTitles(); + + // Property Inspector들에게 아이템 목록 전송 (아이템 컨트롤러만) + for (const context of buttonContexts.keys()) { + const settings = getCurrentSettings(context); + const actionType = settings.actionType || 'camera'; + + if (actionType === 'item') { + sendToPropertyInspector(context, 'item_list', { + items: itemList, + currentIndex: currentIndex + }, 'com.mirabox.streamingle.item'); + + // 아이템 상태에 따라 버튼 상태도 업데이트 + const itemIndex = settings.itemIndex || 0; + if (itemList[itemIndex]) { + const isActive = itemList[itemIndex].isActive !== false; // 기본값은 true + setButtonState(context, isActive); + } + } + } + } else { + console.log('⚠️ 아이템 목록 응답에서 아이템 데이터가 배열이 아님'); + console.log('📋 items:', items); + } + } + break; + + default: + console.log('❓ 알 수 없는 Unity 메시지 타입:', data.type); + } +} + +// 모든 버튼의 제목 업데이트 +function updateAllButtonTitles() { + for (const context of buttonContexts.keys()) { + updateButtonTitle(context); + } +} + +// StreamDock 버튼 제목 업데이트 +function updateButtonTitle(context) { + if (!websocket || !context) { + console.log('🚫 WebSocket 또는 context 없음 - 제목 업데이트 건너뜀'); + return; + } + + const settings = getCurrentSettings(context); + const actionType = settings.actionType || 'camera'; + let title = 'Camera'; + let isActive = true; // 아이템 활성화 상태 (기본값: 활성) + + if (actionType === 'camera') { + const cameraIndex = typeof settings.cameraIndex === 'number' ? settings.cameraIndex : 0; + title = `카메라 ${cameraIndex + 1}`; + + // 카메라 목록에서 이름 찾기 + if (cameraList && cameraList.length > cameraIndex) { + const camera = cameraList[cameraIndex]; + if (camera && camera.name) { + // 카메라 이름에서 불필요한 부분 제거하고 짧게 만들기 + let shortName = camera.name + .replace('Cam0', '') + .replace('Cam', '') + .replace('_', ' ') + .substring(0, 10); // 최대 10글자 + + title = shortName || `카메라 ${cameraIndex + 1}`; + } + } + } else if (actionType === 'item') { + const itemIndex = typeof settings.itemIndex === 'number' ? settings.itemIndex : 0; + title = `아이템 ${itemIndex + 1}`; + + // 아이템 목록에서 이름 찾기 + if (itemList && itemList.length > itemIndex) { + const item = itemList[itemIndex]; + if (item && (item.name || item.groupName)) { + // 아이템 이름에서 불필요한 부분 제거하고 짧게 만들기 + let itemName = item.name || item.groupName; + let shortName = itemName + .replace('Item', '') + .replace('Group', '') + .replace('_', ' ') + .substring(0, 10); // 최대 10글자 + + title = shortName || `아이템 ${itemIndex + 1}`; + + // 아이템 활성화 상태 확인 + if (typeof item.isActive === 'boolean') { + isActive = item.isActive; + } + } + } + } + + // StreamDock에 제목 업데이트 요청 + const message = { + event: 'setTitle', + context: context, + payload: { + title: title, + target: 0, // hardware and software + titleParameters: { + fontSize: 36, // 기존 24 → 36으로 키움 + showTitle: true, + titleAlignment: "bottom" // 중앙(middle) → 아래(bottom)으로 변경 + } + } + }; + + websocket.send(JSON.stringify(message)); + console.log('🏷️ 버튼 제목 업데이트:', title, '(액션 타입:', actionType, ', 활성:', isActive, ')'); + + // 아이템이 비활성화되어 있으면 아이콘을 어둡게 표시 + if (actionType === 'item' && !isActive) { + setButtonState(context, false); // 비활성 상태로 설정 + } else { + setButtonState(context, true); // 활성 상태로 설정 + } +} + +// 버튼 상태 설정 (활성/비활성) +function setButtonState(context, isActive) { + if (!websocket || !context) { + console.log('🚫 WebSocket 또는 context 없음 - 버튼 상태 업데이트 건너뜀'); + return; + } + + const settings = getCurrentSettings(context); + const actionType = settings.actionType || 'camera'; + + // 아이템 컨트롤러만 상태 변경 적용 + if (actionType === 'item') { + // 방법 1: setState 이벤트 사용 + const stateMessage = { + event: 'setState', + context: context, + payload: { + state: isActive ? 0 : 1, // 0: 활성 상태, 1: 비활성 상태 + target: 0 // hardware and software + } + }; + + websocket.send(JSON.stringify(stateMessage)); + console.log('🎨 버튼 상태 업데이트 (setState):', context, '(활성:', isActive, ', 상태:', isActive ? 0 : 1, ')'); + + // 방법 2: setImage 이벤트로 아이콘 직접 변경 + const imageName = isActive ? 'item_icon.png' : 'item_icon_inactive.png'; + const imageMessage = { + event: 'setImage', + context: context, + payload: { + image: imageName, + target: 0 // hardware and software + } + }; + + websocket.send(JSON.stringify(imageMessage)); + console.log('🖼️ 버튼 아이콘 업데이트 (setImage):', context, '(활성:', isActive, ', 이미지:', imageName, ')'); + + // 추가 디버깅을 위한 로그 + setTimeout(() => { + console.log('🔍 상태 변경 후 확인 - Context:', context, 'Action Type:', actionType, '활성:', isActive); + }, 100); + } +} + +// 설정 관리 헬퍼 함수들 +function getCurrentSettings(context) { + if (!context) return {}; + return buttonContexts.get(context) || {}; +} + +function setCurrentSettings(context, newSettings) { + if (!context) return; + buttonContexts.set(context, newSettings); +} + +function updateCurrentSettings(context, partialSettings) { + if (!context) return; + const currentSettings = getCurrentSettings(context); + setCurrentSettings(context, { ...currentSettings, ...partialSettings }); +} + +// 브라우저 환경에서 자동 시작 +console.log('🌐 브라우저 기반 플러그인 준비 완료'); \ No newline at end of file diff --git a/Streamdeck/com.mirabox.streamingle.sdPlugin/propertyInspector/camera/index.html b/Streamdeck/com.mirabox.streamingle.sdPlugin/propertyInspector/camera/index.html new file mode 100644 index 00000000..0e2f2161 --- /dev/null +++ b/Streamdeck/com.mirabox.streamingle.sdPlugin/propertyInspector/camera/index.html @@ -0,0 +1,187 @@ + + + + + Streamingle Camera Inspector + + + + +
+
+ Unity 연결 안됨 +
+ + +
+ +
+ +
+
현재 카메라: -
+
+ + +
+ + +
+ + +
+ +
+ + +
+ +
+
+ + + + + \ No newline at end of file diff --git a/Streamdeck/com.mirabox.streamingle.sdPlugin/propertyInspector/camera/index.js b/Streamdeck/com.mirabox.streamingle.sdPlugin/propertyInspector/camera/index.js new file mode 100644 index 00000000..10a010c4 --- /dev/null +++ b/Streamdeck/com.mirabox.streamingle.sdPlugin/propertyInspector/camera/index.js @@ -0,0 +1,407 @@ +/** + * Streamingle Camera Controller - Property Inspector (단순화 버전) + * Plugin Main을 통해 Unity와 통신 + */ + +// Global variables +let websocket = null; +// 전역 uuid와 actionContext를 명확히 구분 +let uuid = null; // StreamDeck에서 받은 context(uuid) +let actionContext = null; // actionInfo.context +let settings = {}; + +// Context별 설정 관리 +const contextSettings = new Map(); +let currentActionContext = null; + +// Unity 연결 상태 (Plugin Main에서 받아옴) +let isUnityConnected = false; +let cameraData = []; +let currentCamera = 0; + +// DOM elements +let statusDot = null; +let connectionStatus = null; +let cameraSelect = null; +let currentCameraDisplay = null; +let refreshButton = null; + +// 화면에 로그를 표시하는 함수 +function logToScreen(msg, color = "#fff") { + let logDiv = document.getElementById('logArea'); + if (!logDiv) { + console.log('로그 영역을 찾을 수 없음'); + return; + } + const line = document.createElement('div'); + line.style.color = color; + line.textContent = `[${new Date().toLocaleTimeString()}] ${msg}`; + logDiv.appendChild(line); + logDiv.scrollTop = logDiv.scrollHeight; +} + +// 기존 console.log/console.error를 화면에도 출력 +const origLog = console.log; +console.log = function(...args) { + origLog.apply(console, args); + logToScreen(args.map(a => (typeof a === 'object' ? JSON.stringify(a) : a)).join(' '), '#0f0'); +}; +const origErr = console.error; +console.error = function(...args) { + origErr.apply(console, args); + logToScreen(args.map(a => (typeof a === 'object' ? JSON.stringify(a) : a)).join(' '), '#f55'); +}; + +console.log('🔧 Property Inspector script loaded'); + +// Initialize when DOM is loaded +document.addEventListener('DOMContentLoaded', function() { + console.log('📋 Property Inspector 초기화'); + initializePropertyInspector(); +}); + +// Initialize Property Inspector +function initializePropertyInspector() { + // Get DOM elements + statusDot = document.getElementById('statusDot'); + connectionStatus = document.getElementById('connection-status'); + cameraSelect = document.getElementById('camera-select'); + currentCameraDisplay = document.getElementById('current-camera'); + refreshButton = document.getElementById('refresh-button'); + + // Setup event listeners + if (cameraSelect) { + cameraSelect.addEventListener('change', onCameraSelectionChanged); + } + + if (refreshButton) { + refreshButton.addEventListener('click', onRefreshClicked); + } + + console.log('✅ Property Inspector 준비 완료'); +} + +// Send message to plugin +function sendToPlugin(command, data = {}) { + if (!websocket) { + console.error('❌ WebSocket not available'); + return; + } + try { + const message = { + action: 'com.mirabox.streamingle.camera', // manifest.json의 Action UUID + event: 'sendToPlugin', + context: uuid, // connectElgatoStreamDeckSocket에서 받은 uuid + payload: { + command, + ...data + } + }; + websocket.send(JSON.stringify(message)); + console.log('📤 Message sent to plugin:', command, data, 'context:', uuid); + } catch (error) { + console.error('❌ Failed to send message to plugin:', error); + } +} + +// Update connection status display +function updateConnectionStatus(isConnected) { + console.log('🔄 Connection status update:', isConnected); + + // 전역 변수도 업데이트 + isUnityConnected = isConnected; + + if (statusDot) { + statusDot.className = `dot ${isConnected ? 'green' : 'red'}`; + } + + if (connectionStatus) { + connectionStatus.textContent = isConnected ? 'Unity 연결됨' : 'Unity 연결 안됨'; + connectionStatus.className = isConnected ? 'connected' : 'disconnected'; + } + + if (cameraSelect) { + cameraSelect.disabled = !isConnected; + } + + if (refreshButton) { + refreshButton.disabled = !isConnected; + } +} + +// Update camera data display +function updateCameraData(cameraDataParam, currentCamera) { + console.log('📹 Camera data update:', cameraDataParam, currentCamera); + if (cameraSelect && cameraDataParam) { + cameraSelect.innerHTML = ''; + let cameras = cameraDataParam; + if (cameraDataParam.cameras) { + cameras = cameraDataParam.cameras; + } else if (Array.isArray(cameraDataParam)) { + cameras = cameraDataParam; + } + console.log('📹 처리할 카메라 배열:', cameras); + if (cameras && cameras.length > 0) { + cameraData = cameras; + console.log('💾 전역 cameraData 저장됨:', cameraData.length + '개'); + cameras.forEach((camera, index) => { + const option = document.createElement('option'); + option.value = index; + option.textContent = `카메라 ${index + 1}`; + if (camera.name) { + option.textContent += ` (${camera.name})`; + } + cameraSelect.appendChild(option); + }); + // settings.cameraIndex를 우선 적용 + let selectedIndex = currentCamera; + if (typeof settings.cameraIndex === 'number') { + selectedIndex = settings.cameraIndex; + } + if (typeof selectedIndex === 'number') { + cameraSelect.value = selectedIndex; + } + cameraSelect.disabled = false; + console.log('✅ 카메라 목록 업데이트 완료:', cameras.length + '개'); + } else { + console.log('⚠️ 카메라 데이터가 없거나 빈 배열'); + cameraSelect.disabled = true; + } + } + updateCurrentCameraDisplay(currentCamera); +} + +// Update current camera display +function updateCurrentCameraDisplay(currentCamera) { + if (currentCameraDisplay) { + if (typeof currentCamera === 'number') { + currentCameraDisplay.textContent = `현재 카메라: ${currentCamera + 1}`; + } else { + currentCameraDisplay.textContent = '현재 카메라: -'; + } + } +} + +// Camera selection changed +function onCameraSelectionChanged() { + if (!cameraSelect) return; + + const selectedIndex = parseInt(cameraSelect.value); + if (isNaN(selectedIndex)) return; + + console.log('📹 카메라 선택 변경:', selectedIndex); + + // Plugin Main에 카메라 변경 요청 + sendToPlugin('switch_camera', { cameraIndex: selectedIndex }); + + // 설정 저장 + const currentSettings = getContextSettings(uuid); + const newSettings = { + ...currentSettings, + cameraIndex: selectedIndex + }; + saveContextSettings(newSettings); + + // 버튼 제목 업데이트 요청 + sendToPlugin('update_title', { cameraIndex: selectedIndex }); +} + +// Refresh button clicked +function onRefreshClicked() { + console.log('🔄 카메라 목록 새로고침 요청'); + sendToPlugin('get_camera_list'); +} + +// Handle messages from plugin +function handleMessage(jsonObj) { + console.log('📨 Property Inspector 메시지 수신:', jsonObj.event); + + // 플러그인에서 오는 메시지는 event가 sendToPropertyInspector이고, 실제 타입은 payload.event에 있음 + if (jsonObj.event === 'sendToPropertyInspector' && jsonObj.payload && jsonObj.payload.event) { + const innerEvent = jsonObj.payload.event; + console.log('📨 내부 이벤트:', innerEvent, 'payload:', jsonObj.payload); + + switch (innerEvent) { + case 'unity_connected': + console.log('✅ Unity 연결 상태 업데이트'); + updateConnectionStatus(true); + break; + case 'unity_disconnected': + console.log('❌ Unity 연결 해제 상태 업데이트'); + updateConnectionStatus(false); + break; + case 'camera_list': + console.log('📹 카메라 목록 수신'); + updateCameraData(jsonObj.payload.cameras, jsonObj.payload.currentIndex); + break; + case 'camera_changed': + console.log('📹 카메라 변경 알림'); + updateCurrentCameraDisplay(jsonObj.payload.cameraIndex); + break; + case 'item_list': + console.log('🎯 아이템 목록 수신 (카메라 Property Inspector에서는 무시)'); + console.log('⚠️ 카메라 컨트롤러에서 아이템 데이터를 받았습니다. 이는 잘못된 데이터 전송입니다.'); + break; + default: + console.log('❓ 알 수 없는 내부 이벤트:', innerEvent); + } + return; + } + + // 기존 방식도 유지 + switch (jsonObj.event) { + case 'unity_connected': + console.log('✅ Unity 연결 상태 업데이트'); + updateConnectionStatus(true); + break; + case 'unity_disconnected': + console.log('❌ Unity 연결 해제 상태 업데이트'); + updateConnectionStatus(false); + break; + case 'camera_list': + console.log('📹 카메라 목록 수신'); + if (jsonObj.payload && jsonObj.payload.cameras) { + updateCameraData(jsonObj.payload.cameras, jsonObj.payload.currentIndex); + } + break; + case 'camera_changed': + console.log('📹 카메라 변경 알림'); + if (jsonObj.payload && typeof jsonObj.payload.cameraIndex === 'number') { + updateCurrentCameraDisplay(jsonObj.payload.cameraIndex); + } + break; + case 'didReceiveSettings': + console.log('⚙️ 설정 수신'); + if (jsonObj.payload && jsonObj.context) { + const newSettings = jsonObj.payload.settings || {}; + contextSettings.set(jsonObj.context, newSettings); + // 카메라 인덱스가 있으면 선택 + if (typeof newSettings.cameraIndex === 'number' && cameraSelect) { + cameraSelect.value = newSettings.cameraIndex; + updateCurrentCameraDisplay(newSettings.cameraIndex); + } + } + break; + default: + console.log('❓ 알 수 없는 메시지 타입:', jsonObj.event); + } +} + +// StreamDeck SDK 연결 +function connectElgatoStreamDeckSocket(inPort, inPropertyInspectorUUID, inRegisterEvent, inInfo, inActionInfo) { + console.log('🔌 Property Inspector StreamDeck 연결'); + console.log('📡 포트:', inPort, 'UUID:', inPropertyInspectorUUID); + + uuid = inPropertyInspectorUUID; + + // Parse action info + try { + if (inActionInfo) { + const actionInfo = JSON.parse(inActionInfo); + actionContext = actionInfo.context; + settings = actionInfo.payload?.settings || {}; + + console.log('⚙️ 초기 설정:', settings); + + // 카메라 인덱스가 있으면 선택 + if (typeof settings.cameraIndex === 'number' && cameraSelect) { + cameraSelect.value = settings.cameraIndex; + updateCurrentCameraDisplay(settings.cameraIndex); + } + } + } catch (error) { + console.error('❌ ActionInfo 파싱 오류:', error); + } + + // WebSocket 연결 + if (!websocket) { + websocket = new WebSocket('ws://localhost:' + inPort); + + websocket.onopen = function() { + console.log('✅ Property Inspector StreamDeck 연결됨'); + + // StreamDeck에 등록 + websocket.send(JSON.stringify({ + event: inRegisterEvent, + uuid: inPropertyInspectorUUID + })); + + // Unity 상태 요청 + setTimeout(() => { + sendToPlugin('get_unity_status'); + }, 500); + }; + + websocket.onmessage = function(evt) { + try { + const jsonObj = JSON.parse(evt.data); + handleMessage(jsonObj); + } catch (error) { + console.error('❌ 메시지 파싱 오류:', error); + } + }; + + websocket.onclose = function() { + console.log('❌ Property Inspector StreamDeck 연결 끊어짐'); + websocket = null; + }; + + websocket.onerror = function(error) { + console.error('❌ Property Inspector StreamDeck 연결 오류:', error); + }; + } +} + +// 설정 관리 헬퍼 함수들 +function getContextSettings(context) { + if (!context) return {}; + return contextSettings.get(context) || {}; +} + +// context별 설정 저장 (StreamDeck 표준 setSettings 사용) +function saveContextSettings(newSettings) { + if (!uuid || !websocket) return; + websocket.send(JSON.stringify({ + action: 'com.mirabox.streamingle.camera', + event: 'setSettings', + context: uuid, + payload: newSettings + })); + contextSettings.set(uuid, newSettings); // 로컬에도 저장 +} + +// context별 설정 불러오기 (StreamDeck 표준 getSettings 사용) +function requestSettings() { + if (!uuid || !websocket) return; + websocket.send(JSON.stringify({ + action: 'com.mirabox.streamingle.camera', + event: 'getSettings', + context: uuid + })); +} + +// Property Inspector 초기화 시 context별 설정 요청 +function initializePropertyInspector() { + // Get DOM elements + statusDot = document.getElementById('statusDot'); + connectionStatus = document.getElementById('connection-status'); + cameraSelect = document.getElementById('camera-select'); + currentCameraDisplay = document.getElementById('current-camera'); + refreshButton = document.getElementById('refresh-button'); + + // Setup event listeners + if (cameraSelect) { + cameraSelect.addEventListener('change', onCameraSelectionChanged); + } + + if (refreshButton) { + refreshButton.addEventListener('click', onRefreshClicked); + } + + // 현재 액션의 컨텍스트가 있으면 설정 요청 + if (typeof uuid === 'string') { + requestSettings(); + } + + console.log('✅ Property Inspector 준비 완료'); +} \ No newline at end of file diff --git a/Streamdeck/com.mirabox.streamingle.sdPlugin/propertyInspector/item/index.html b/Streamdeck/com.mirabox.streamingle.sdPlugin/propertyInspector/item/index.html new file mode 100644 index 00000000..0524e259 --- /dev/null +++ b/Streamdeck/com.mirabox.streamingle.sdPlugin/propertyInspector/item/index.html @@ -0,0 +1,187 @@ + + + + + Streamingle Item Inspector + + + + +
+
+ Unity 연결 안됨 +
+ + +
+ +
+ +
+
현재 아이템 그룹: -
+
+ + +
+ + +
+ + +
+ +
+ + +
+ +
+
+ + + + + \ No newline at end of file diff --git a/Streamdeck/com.mirabox.streamingle.sdPlugin/propertyInspector/item/index.js b/Streamdeck/com.mirabox.streamingle.sdPlugin/propertyInspector/item/index.js new file mode 100644 index 00000000..fbd582ab --- /dev/null +++ b/Streamdeck/com.mirabox.streamingle.sdPlugin/propertyInspector/item/index.js @@ -0,0 +1,294 @@ +// Item Controller Property Inspector +let websocket = null; +let uuid = null; +let settings = {}; +let itemList = []; +let isUnityConnected = false; + +// DOM 요소들 +let itemSelect = null; +let statusDot = null; +let connectionStatus = null; +let currentItem = null; +let refreshButton = null; +let autoSwitch = null; +let logArea = null; + +function logToScreen(msg, color = "#fff") { + let logDiv = document.getElementById('logArea'); + if (!logDiv) return; + const line = document.createElement('div'); + line.style.color = color; + line.textContent = `[${new Date().toLocaleTimeString()}] ${msg}`; + logDiv.appendChild(line); + logDiv.scrollTop = logDiv.scrollHeight; +} + +console.log = function(...args) { + logToScreen(args.map(a => (typeof a === 'object' ? JSON.stringify(a) : a)).join(' '), '#0f0'); +}; +console.error = function(...args) { + logToScreen(args.map(a => (typeof a === 'object' ? JSON.stringify(a) : a)).join(' '), '#f55'); +}; + +// DOM 초기화 +window.addEventListener('DOMContentLoaded', function() { + itemSelect = document.getElementById('item-select'); + statusDot = document.getElementById('statusDot'); + connectionStatus = document.getElementById('connection-status'); + currentItem = document.getElementById('current-item'); + refreshButton = document.getElementById('refresh-button'); + autoSwitch = document.getElementById('autoSwitch'); + logArea = document.getElementById('logArea'); + + if (refreshButton) refreshButton.addEventListener('click', onRefreshClicked); + if (itemSelect) itemSelect.addEventListener('change', onItemSelectionChanged); + if (autoSwitch) autoSwitch.addEventListener('change', onAutoSwitchChanged); + + console.log('✅ Item Property Inspector 준비 완료'); +}); + +// StreamDeck 연결 +window.connectElgatoStreamDeckSocket = function(inPort, inUUID, inEvent, inInfo, inActionInfo) { + uuid = inUUID; + console.log('🔌 StreamDeck 연결 시작:', inPort, inUUID); + + try { + if (inActionInfo) { + const actionInfo = JSON.parse(inActionInfo); + settings = actionInfo.payload?.settings || {}; + console.log('⚙️ 초기 설정:', settings); + } + } catch (e) { + console.error('ActionInfo 파싱 오류:', e); + } + + if (!websocket) { + websocket = new WebSocket('ws://localhost:' + inPort); + + websocket.onopen = function() { + console.log('✅ StreamDeck 연결됨'); + websocket.send(JSON.stringify({ event: inEvent, uuid: inUUID })); + + // Unity 상태 요청 + setTimeout(() => { + sendToPlugin('get_unity_status'); + }, 500); + }; + + websocket.onmessage = function(evt) { + try { + const jsonObj = JSON.parse(evt.data); + handleMessage(jsonObj); + } catch (e) { + console.error('메시지 파싱 오류:', e); + } + }; + + websocket.onclose = function() { + console.log('❌ StreamDeck 연결 끊어짐'); + websocket = null; + }; + + websocket.onerror = function(e) { + console.error('WebSocket 오류:', e); + }; + } +}; + +function sendToPlugin(command, data = {}) { + if (!websocket) return; + const message = { + action: 'com.mirabox.streamingle.item', + event: 'sendToPlugin', + context: uuid, + payload: { command, ...data } + }; + websocket.send(JSON.stringify(message)); + console.log('📤 Plugin으로 메시지 전송:', command, data); +} + +function handleMessage(jsonObj) { + console.log('📨 메시지 수신:', jsonObj.event); + + if (jsonObj.event === 'sendToPropertyInspector' && jsonObj.payload && jsonObj.payload.event) { + const innerEvent = jsonObj.payload.event; + console.log('📨 Property Inspector 이벤트:', innerEvent); + + switch (innerEvent) { + case 'unity_connected': + updateUnityConnection(true); + break; + case 'unity_disconnected': + updateUnityConnection(false); + break; + case 'item_list': + updateItemList(jsonObj.payload.items, jsonObj.payload.currentIndex); + break; + case 'item_changed': + updateItemState(jsonObj.payload.currentIndex); + break; + case 'camera_list': + console.log('📹 카메라 목록 수신 (아이템 Property Inspector에서는 무시)'); + console.log('⚠️ 아이템 컨트롤러에서 카메라 데이터를 받았습니다. 이는 잘못된 데이터 전송입니다.'); + break; + } + } + + if (jsonObj.event === 'didReceiveSettings' && jsonObj.payload && jsonObj.context) { + settings = jsonObj.payload.settings || {}; + console.log('⚙️ 설정 수신:', settings); + + if (settings.itemIndex !== undefined && itemSelect) { + itemSelect.value = settings.itemIndex; + } + + if (settings.autoSwitch !== undefined && autoSwitch) { + autoSwitch.checked = settings.autoSwitch; + } + } +} + +function updateUnityConnection(connected) { + isUnityConnected = connected; + + if (statusDot) { + statusDot.className = 'dot ' + (connected ? 'green' : 'red'); + } + + if (connectionStatus) { + connectionStatus.textContent = connected ? 'Unity 연결됨' : 'Unity 연결 안됨'; + connectionStatus.className = connected ? 'connected' : 'disconnected'; + } + + if (itemSelect) { + itemSelect.disabled = !connected; + } + + if (refreshButton) { + refreshButton.disabled = !connected; + } + + console.log('🔗 Unity 연결 상태 변경:', connected); +} + +function updateItemList(items, currentIndex) { + itemList = items || []; + console.log('🎯 아이템 목록 업데이트:', itemList.length, '개'); + + if (!itemSelect) return; + + itemSelect.innerHTML = ''; + + if (itemList.length === 0) { + const option = document.createElement('option'); + option.value = ''; + option.textContent = '아이템 그룹이 없습니다'; + itemSelect.appendChild(option); + } else { + itemList.forEach((item, index) => { + const option = document.createElement('option'); + option.value = index; + option.textContent = item.name || item.groupName || `아이템 그룹 ${index + 1}`; + itemSelect.appendChild(option); + }); + } + + // 현재 선택된 아이템 설정 + if (settings.itemIndex !== undefined) { + itemSelect.value = settings.itemIndex; + } else if (currentIndex !== undefined) { + itemSelect.value = currentIndex; + } + + updateCurrentItem(); +} + +function updateCurrentItem() { + if (!currentItem || !itemSelect) return; + + const selectedIndex = parseInt(itemSelect.value); + if (isNaN(selectedIndex) || selectedIndex < 0 || selectedIndex >= itemList.length) { + currentItem.textContent = '현재 아이템 그룹: -'; + return; + } + + const selectedItem = itemList[selectedIndex]; + if (selectedItem) { + const status = selectedItem.isActive ? '활성' : '비활성'; + currentItem.textContent = `현재 아이템 그룹: ${selectedItem.name || selectedItem.groupName || `그룹 ${selectedIndex + 1}`} (${status})`; + } +} + +function updateItemState(currentIndex) { + if (currentIndex !== undefined && itemSelect) { + itemSelect.value = currentIndex; + } + updateCurrentItem(); +} + +function onItemSelectionChanged() { + const selectedIndex = parseInt(itemSelect.value); + if (isNaN(selectedIndex)) return; + + // 항상 actionType: 'item'을 포함 + settings.itemIndex = selectedIndex; + settings.actionType = 'item'; + updateCurrentItem(); + + // 설정 저장 (actionType 포함) + if (websocket && uuid) { + websocket.send(JSON.stringify({ + action: 'com.mirabox.streamingle.item', + event: 'setSettings', + context: uuid, + payload: settings + })); + } + + // 버튼 제목 업데이트 요청 (actionType 포함) + sendToPlugin('update_title', { itemIndex: selectedIndex, actionType: 'item' }); + + // 즉시 버튼 제목 업데이트 요청 + if (websocket && uuid) { + websocket.send(JSON.stringify({ + action: 'com.mirabox.streamingle.item', + event: 'setTitle', + context: uuid, + payload: { + title: `아이템 ${selectedIndex + 1}`, + target: 0 + } + })); + } + + console.log('🎯 아이템 그룹 선택 변경:', selectedIndex, settings); +} + +function onAutoSwitchChanged() { + if (!autoSwitch) return; + + settings.autoSwitch = autoSwitch.checked; + + // 설정 저장 + if (websocket && uuid) { + websocket.send(JSON.stringify({ + action: 'com.mirabox.streamingle.item', + event: 'setSettings', + context: uuid, + payload: settings + })); + } + + console.log('⚙️ 자동 전환 설정 변경:', autoSwitch.checked); +} + +function onRefreshClicked() { + if (!isUnityConnected) { + console.log('⚠️ Unity가 연결되지 않음'); + return; + } + + console.log('🔄 아이템 그룹 목록 새로고침 요청'); + sendToPlugin('refresh_item_list'); +} \ No newline at end of file diff --git a/Streamdeck/com.mirabox.streamingle.sdPlugin/propertyInspector/utils/action.js b/Streamdeck/com.mirabox.streamingle.sdPlugin/propertyInspector/utils/action.js new file mode 100644 index 00000000..7cc9620a --- /dev/null +++ b/Streamdeck/com.mirabox.streamingle.sdPlugin/propertyInspector/utils/action.js @@ -0,0 +1,157 @@ +let $websocket, $uuid, $action, $context, $settings, $lang, $FileID = ''; + +WebSocket.prototype.setGlobalSettings = function(payload) { + this.send(JSON.stringify({ + event: "setGlobalSettings", + context: $uuid, payload + })); +} + +WebSocket.prototype.getGlobalSettings = function() { + this.send(JSON.stringify({ + event: "getGlobalSettings", + context: $uuid, + })); +} + +// 与插件通信 +WebSocket.prototype.sendToPlugin = function (payload) { + this.send(JSON.stringify({ + event: "sendToPlugin", + action: $action, + context: $uuid, + payload + })); +}; + +//设置标题 +WebSocket.prototype.setTitle = function (str, row = 0, num = 6) { + console.log(str); + let newStr = ''; + if (row) { + let nowRow = 1, strArr = str.split(''); + strArr.forEach((item, index) => { + if (nowRow < row && index >= nowRow * num) { nowRow++; newStr += '\n'; } + if (nowRow <= row && index < nowRow * num) { newStr += item; } + }); + if (strArr.length > row * num) { newStr = newStr.substring(0, newStr.length - 1); newStr += '..'; } + } + this.send(JSON.stringify({ + event: "setTitle", + context: $context, + payload: { + target: 0, + title: newStr || str + } + })); +} + +// 设置状态 +WebSocket.prototype.setState = function (state) { + this.send(JSON.stringify({ + event: "setState", + context: $context, + payload: { state } + })); +}; + +// 设置背景 +WebSocket.prototype.setImage = function (url) { + let image = new Image(); + image.src = url; + image.onload = () => { + let canvas = document.createElement("canvas"); + canvas.width = image.naturalWidth; + canvas.height = image.naturalHeight; + let ctx = canvas.getContext("2d"); + ctx.drawImage(image, 0, 0); + this.send(JSON.stringify({ + event: "setImage", + context: $context, + payload: { + target: 0, + image: canvas.toDataURL("image/png") + } + })); + }; +}; + +// 打开网页 +WebSocket.prototype.openUrl = function (url) { + this.send(JSON.stringify({ + event: "openUrl", + payload: { url } + })); +}; + +// 保存持久化数据 +WebSocket.prototype.saveData = $.debounce(function (payload) { + this.send(JSON.stringify({ + event: "setSettings", + context: $uuid, + payload + })); +}); + +// StreamDock 软件入口函数 +const connectSocket = connectElgatoStreamDeckSocket; +async function connectElgatoStreamDeckSocket(port, uuid, event, app, info) { + info = JSON.parse(info); + $uuid = uuid; $action = info.action; + $context = info.context; + $websocket = new WebSocket('ws://127.0.0.1:' + port); + $websocket.onopen = () => $websocket.send(JSON.stringify({ event, uuid })); + + // 持久数据代理 + $websocket.onmessage = e => { + let data = JSON.parse(e.data); + if (data.event === 'didReceiveSettings') { + $settings = new Proxy(data.payload.settings, { + get(target, property) { + return target[property]; + }, + set(target, property, value) { + target[property] = value; + $websocket.saveData(data.payload.settings); + } + }); + if (!$back) $dom.main.style.display = 'block'; + } + $propEvent[data.event]?.(data.payload); + }; + + // 自动翻译页面 + if (!$local) return; + $lang = await new Promise(resolve => { + const req = new XMLHttpRequest(); + req.open('GET', `../../${JSON.parse(app).application.language}.json`); + req.send(); + req.onreadystatechange = () => { + if (req.readyState === 4) { + resolve(JSON.parse(req.responseText).Localization); + } + }; + }); + + // 遍历文本节点并翻译所有文本节点 + const walker = document.createTreeWalker($dom.main, NodeFilter.SHOW_TEXT, (e) => { + return e.data.trim() && NodeFilter.FILTER_ACCEPT; + }); + while (walker.nextNode()) { + console.log(walker.currentNode.data); + walker.currentNode.data = $lang[walker.currentNode.data]; + } + // placeholder 特殊处理 + const translate = item => { + if (item.placeholder?.trim()) { + console.log(item.placeholder); + item.placeholder = $lang[item.placeholder]; + } + }; + $('input', true).forEach(translate); + $('textarea', true).forEach(translate); +} + +// StreamDock 文件路径回调 +Array.from($('input[type="file"]', true)).forEach(item => item.addEventListener('click', () => $FileID = item.id)); +const onFilePickerReturn = (url) => $emit.send(`File-${$FileID}`, JSON.parse(url)); \ No newline at end of file diff --git a/Streamdeck/com.mirabox.streamingle.sdPlugin/propertyInspector/utils/axios.js b/Streamdeck/com.mirabox.streamingle.sdPlugin/propertyInspector/utils/axios.js new file mode 100644 index 00000000..78aa7b89 --- /dev/null +++ b/Streamdeck/com.mirabox.streamingle.sdPlugin/propertyInspector/utils/axios.js @@ -0,0 +1 @@ +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).axios=t()}(this,(function(){"use strict";function e(t){return e="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},e(t)}function t(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function n(e,t){for(var n=0;ne.length)&&(t=e.length);for(var n=0,r=new Array(t);n2&&void 0!==arguments[2]?arguments[2]:{},a=i.allOwnKeys,s=void 0!==a&&a;if(null!=t)if("object"!==e(t)&&(t=[t]),p(t))for(r=0,o=t.length;r0;)if(t===(n=r[o]).toLowerCase())return n;return null}var C="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:"undefined"!=typeof window?window:global,N=function(e){return!h(e)&&e!==C};var x,P=(x="undefined"!=typeof Uint8Array&&c(Uint8Array),function(e){return x&&e instanceof x}),k=l("HTMLFormElement"),U=function(e){var t=Object.prototype.hasOwnProperty;return function(e,n){return t.call(e,n)}}(),_=l("RegExp"),F=function(e,t){var n=Object.getOwnPropertyDescriptors(e),r={};T(n,(function(n,o){var i;!1!==(i=t(n,o,e))&&(r[o]=i||n)})),Object.defineProperties(e,r)},B="abcdefghijklmnopqrstuvwxyz",L="0123456789",D={DIGIT:L,ALPHA:B,ALPHA_DIGIT:B+B.toUpperCase()+L};var I=l("AsyncFunction"),q={isArray:p,isArrayBuffer:m,isBuffer:function(e){return null!==e&&!h(e)&&null!==e.constructor&&!h(e.constructor)&&y(e.constructor.isBuffer)&&e.constructor.isBuffer(e)},isFormData:function(e){var t;return e&&("function"==typeof FormData&&e instanceof FormData||y(e.append)&&("formdata"===(t=f(e))||"object"===t&&y(e.toString)&&"[object FormData]"===e.toString()))},isArrayBufferView:function(e){return"undefined"!=typeof ArrayBuffer&&ArrayBuffer.isView?ArrayBuffer.isView(e):e&&e.buffer&&m(e.buffer)},isString:v,isNumber:b,isBoolean:function(e){return!0===e||!1===e},isObject:g,isPlainObject:w,isUndefined:h,isDate:E,isFile:O,isBlob:S,isRegExp:_,isFunction:y,isStream:function(e){return g(e)&&y(e.pipe)},isURLSearchParams:A,isTypedArray:P,isFileList:R,forEach:T,merge:function e(){for(var t=N(this)&&this||{},n=t.caseless,r={},o=function(t,o){var i=n&&j(r,o)||o;w(r[i])&&w(t)?r[i]=e(r[i],t):w(t)?r[i]=e({},t):p(t)?r[i]=t.slice():r[i]=t},i=0,a=arguments.length;i3&&void 0!==arguments[3]?arguments[3]:{},o=r.allOwnKeys;return T(t,(function(t,r){n&&y(t)?e[r]=a(t,n):e[r]=t}),{allOwnKeys:o}),e},trim:function(e){return e.trim?e.trim():e.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,"")},stripBOM:function(e){return 65279===e.charCodeAt(0)&&(e=e.slice(1)),e},inherits:function(e,t,n,r){e.prototype=Object.create(t.prototype,r),e.prototype.constructor=e,Object.defineProperty(e,"super",{value:t.prototype}),n&&Object.assign(e.prototype,n)},toFlatObject:function(e,t,n,r){var o,i,a,s={};if(t=t||{},null==e)return t;do{for(i=(o=Object.getOwnPropertyNames(e)).length;i-- >0;)a=o[i],r&&!r(a,e,t)||s[a]||(t[a]=e[a],s[a]=!0);e=!1!==n&&c(e)}while(e&&(!n||n(e,t))&&e!==Object.prototype);return t},kindOf:f,kindOfTest:l,endsWith:function(e,t,n){e=String(e),(void 0===n||n>e.length)&&(n=e.length),n-=t.length;var r=e.indexOf(t,n);return-1!==r&&r===n},toArray:function(e){if(!e)return null;if(p(e))return e;var t=e.length;if(!b(t))return null;for(var n=new Array(t);t-- >0;)n[t]=e[t];return n},forEachEntry:function(e,t){for(var n,r=(e&&e[Symbol.iterator]).call(e);(n=r.next())&&!n.done;){var o=n.value;t.call(e,o[0],o[1])}},matchAll:function(e,t){for(var n,r=[];null!==(n=e.exec(t));)r.push(n);return r},isHTMLForm:k,hasOwnProperty:U,hasOwnProp:U,reduceDescriptors:F,freezeMethods:function(e){F(e,(function(t,n){if(y(e)&&-1!==["arguments","caller","callee"].indexOf(n))return!1;var r=e[n];y(r)&&(t.enumerable=!1,"writable"in t?t.writable=!1:t.set||(t.set=function(){throw Error("Can not rewrite read-only method '"+n+"'")}))}))},toObjectSet:function(e,t){var n={},r=function(e){e.forEach((function(e){n[e]=!0}))};return p(e)?r(e):r(String(e).split(t)),n},toCamelCase:function(e){return e.toLowerCase().replace(/[-_\s]([a-z\d])(\w*)/g,(function(e,t,n){return t.toUpperCase()+n}))},noop:function(){},toFiniteNumber:function(e,t){return e=+e,Number.isFinite(e)?e:t},findKey:j,global:C,isContextDefined:N,ALPHABET:D,generateString:function(){for(var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:16,t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:D.ALPHA_DIGIT,n="",r=t.length;e--;)n+=t[Math.random()*r|0];return n},isSpecCompliantForm:function(e){return!!(e&&y(e.append)&&"FormData"===e[Symbol.toStringTag]&&e[Symbol.iterator])},toJSONObject:function(e){var t=new Array(10);return function e(n,r){if(g(n)){if(t.indexOf(n)>=0)return;if(!("toJSON"in n)){t[r]=n;var o=p(n)?[]:{};return T(n,(function(t,n){var i=e(t,r+1);!h(i)&&(o[n]=i)})),t[r]=void 0,o}}return n}(e,0)},isAsyncFn:I,isThenable:function(e){return e&&(g(e)||y(e))&&y(e.then)&&y(e.catch)}};function M(e,t,n,r,o){Error.call(this),Error.captureStackTrace?Error.captureStackTrace(this,this.constructor):this.stack=(new Error).stack,this.message=e,this.name="AxiosError",t&&(this.code=t),n&&(this.config=n),r&&(this.request=r),o&&(this.response=o)}q.inherits(M,Error,{toJSON:function(){return{message:this.message,name:this.name,description:this.description,number:this.number,fileName:this.fileName,lineNumber:this.lineNumber,columnNumber:this.columnNumber,stack:this.stack,config:q.toJSONObject(this.config),code:this.code,status:this.response&&this.response.status?this.response.status:null}}});var z=M.prototype,H={};["ERR_BAD_OPTION_VALUE","ERR_BAD_OPTION","ECONNABORTED","ETIMEDOUT","ERR_NETWORK","ERR_FR_TOO_MANY_REDIRECTS","ERR_DEPRECATED","ERR_BAD_RESPONSE","ERR_BAD_REQUEST","ERR_CANCELED","ERR_NOT_SUPPORT","ERR_INVALID_URL"].forEach((function(e){H[e]={value:e}})),Object.defineProperties(M,H),Object.defineProperty(z,"isAxiosError",{value:!0}),M.from=function(e,t,n,r,o,i){var a=Object.create(z);return q.toFlatObject(e,a,(function(e){return e!==Error.prototype}),(function(e){return"isAxiosError"!==e})),M.call(a,e.message,t,n,r,o),a.cause=e,a.name=e.name,i&&Object.assign(a,i),a};function J(e){return q.isPlainObject(e)||q.isArray(e)}function W(e){return q.endsWith(e,"[]")?e.slice(0,-2):e}function K(e,t,n){return e?e.concat(t).map((function(e,t){return e=W(e),!n&&t?"["+e+"]":e})).join(n?".":""):t}var V=q.toFlatObject(q,{},null,(function(e){return/^is[A-Z]/.test(e)}));function G(t,n,r){if(!q.isObject(t))throw new TypeError("target must be an object");n=n||new FormData;var o=(r=q.toFlatObject(r,{metaTokens:!0,dots:!1,indexes:!1},!1,(function(e,t){return!q.isUndefined(t[e])}))).metaTokens,i=r.visitor||f,a=r.dots,s=r.indexes,u=(r.Blob||"undefined"!=typeof Blob&&Blob)&&q.isSpecCompliantForm(n);if(!q.isFunction(i))throw new TypeError("visitor must be a function");function c(e){if(null===e)return"";if(q.isDate(e))return e.toISOString();if(!u&&q.isBlob(e))throw new M("Blob is not supported. Use a Buffer instead.");return q.isArrayBuffer(e)||q.isTypedArray(e)?u&&"function"==typeof Blob?new Blob([e]):Buffer.from(e):e}function f(t,r,i){var u=t;if(t&&!i&&"object"===e(t))if(q.endsWith(r,"{}"))r=o?r:r.slice(0,-2),t=JSON.stringify(t);else if(q.isArray(t)&&function(e){return q.isArray(e)&&!e.some(J)}(t)||(q.isFileList(t)||q.endsWith(r,"[]"))&&(u=q.toArray(t)))return r=W(r),u.forEach((function(e,t){!q.isUndefined(e)&&null!==e&&n.append(!0===s?K([r],t,a):null===s?r:r+"[]",c(e))})),!1;return!!J(t)||(n.append(K(i,r,a),c(t)),!1)}var l=[],d=Object.assign(V,{defaultVisitor:f,convertValue:c,isVisitable:J});if(!q.isObject(t))throw new TypeError("data must be an object");return function e(t,r){if(!q.isUndefined(t)){if(-1!==l.indexOf(t))throw Error("Circular reference detected in "+r.join("."));l.push(t),q.forEach(t,(function(t,o){!0===(!(q.isUndefined(t)||null===t)&&i.call(n,t,q.isString(o)?o.trim():o,r,d))&&e(t,r?r.concat(o):[o])})),l.pop()}}(t),n}function $(e){var t={"!":"%21","'":"%27","(":"%28",")":"%29","~":"%7E","%20":"+","%00":"\0"};return encodeURIComponent(e).replace(/[!'()~]|%20|%00/g,(function(e){return t[e]}))}function X(e,t){this._pairs=[],e&&G(e,this,t)}var Q=X.prototype;function Z(e){return encodeURIComponent(e).replace(/%3A/gi,":").replace(/%24/g,"$").replace(/%2C/gi,",").replace(/%20/g,"+").replace(/%5B/gi,"[").replace(/%5D/gi,"]")}function Y(e,t,n){if(!t)return e;var r,o=n&&n.encode||Z,i=n&&n.serialize;if(r=i?i(t,n):q.isURLSearchParams(t)?t.toString():new X(t,n).toString(o)){var a=e.indexOf("#");-1!==a&&(e=e.slice(0,a)),e+=(-1===e.indexOf("?")?"?":"&")+r}return e}Q.append=function(e,t){this._pairs.push([e,t])},Q.toString=function(e){var t=e?function(t){return e.call(this,t,$)}:$;return this._pairs.map((function(e){return t(e[0])+"="+t(e[1])}),"").join("&")};var ee,te=function(){function e(){t(this,e),this.handlers=[]}return r(e,[{key:"use",value:function(e,t,n){return this.handlers.push({fulfilled:e,rejected:t,synchronous:!!n&&n.synchronous,runWhen:n?n.runWhen:null}),this.handlers.length-1}},{key:"eject",value:function(e){this.handlers[e]&&(this.handlers[e]=null)}},{key:"clear",value:function(){this.handlers&&(this.handlers=[])}},{key:"forEach",value:function(e){q.forEach(this.handlers,(function(t){null!==t&&e(t)}))}}]),e}(),ne={silentJSONParsing:!0,forcedJSONParsing:!0,clarifyTimeoutError:!1},re={isBrowser:!0,classes:{URLSearchParams:"undefined"!=typeof URLSearchParams?URLSearchParams:X,FormData:"undefined"!=typeof FormData?FormData:null,Blob:"undefined"!=typeof Blob?Blob:null},isStandardBrowserEnv:("undefined"==typeof navigator||"ReactNative"!==(ee=navigator.product)&&"NativeScript"!==ee&&"NS"!==ee)&&"undefined"!=typeof window&&"undefined"!=typeof document,isStandardBrowserWebWorkerEnv:"undefined"!=typeof WorkerGlobalScope&&self instanceof WorkerGlobalScope&&"function"==typeof self.importScripts,protocols:["http","https","file","blob","url","data"]};function oe(e){function t(e,n,r,o){var i=e[o++],a=Number.isFinite(+i),s=o>=e.length;return i=!i&&q.isArray(r)?r.length:i,s?(q.hasOwnProp(r,i)?r[i]=[r[i],n]:r[i]=n,!a):(r[i]&&q.isObject(r[i])||(r[i]=[]),t(e,n,r[i],o)&&q.isArray(r[i])&&(r[i]=function(e){var t,n,r={},o=Object.keys(e),i=o.length;for(t=0;t-1,i=q.isObject(e);if(i&&q.isHTMLForm(e)&&(e=new FormData(e)),q.isFormData(e))return o&&o?JSON.stringify(oe(e)):e;if(q.isArrayBuffer(e)||q.isBuffer(e)||q.isStream(e)||q.isFile(e)||q.isBlob(e))return e;if(q.isArrayBufferView(e))return e.buffer;if(q.isURLSearchParams(e))return t.setContentType("application/x-www-form-urlencoded;charset=utf-8",!1),e.toString();if(i){if(r.indexOf("application/x-www-form-urlencoded")>-1)return function(e,t){return G(e,new re.classes.URLSearchParams,Object.assign({visitor:function(e,t,n,r){return re.isNode&&q.isBuffer(e)?(this.append(t,e.toString("base64")),!1):r.defaultVisitor.apply(this,arguments)}},t))}(e,this.formSerializer).toString();if((n=q.isFileList(e))||r.indexOf("multipart/form-data")>-1){var a=this.env&&this.env.FormData;return G(n?{"files[]":e}:e,a&&new a,this.formSerializer)}}return i||o?(t.setContentType("application/json",!1),function(e,t,n){if(q.isString(e))try{return(t||JSON.parse)(e),q.trim(e)}catch(e){if("SyntaxError"!==e.name)throw e}return(n||JSON.stringify)(e)}(e)):e}],transformResponse:[function(e){var t=this.transitional||ie.transitional,n=t&&t.forcedJSONParsing,r="json"===this.responseType;if(e&&q.isString(e)&&(n&&!this.responseType||r)){var o=!(t&&t.silentJSONParsing)&&r;try{return JSON.parse(e)}catch(e){if(o){if("SyntaxError"===e.name)throw M.from(e,M.ERR_BAD_RESPONSE,this,null,this.response);throw e}}}return e}],timeout:0,xsrfCookieName:"XSRF-TOKEN",xsrfHeaderName:"X-XSRF-TOKEN",maxContentLength:-1,maxBodyLength:-1,env:{FormData:re.classes.FormData,Blob:re.classes.Blob},validateStatus:function(e){return e>=200&&e<300},headers:{common:{Accept:"application/json, text/plain, */*","Content-Type":void 0}}};q.forEach(["delete","get","head","post","put","patch"],(function(e){ie.headers[e]={}}));var ae=ie,se=q.toObjectSet(["age","authorization","content-length","content-type","etag","expires","from","host","if-modified-since","if-unmodified-since","last-modified","location","max-forwards","proxy-authorization","referer","retry-after","user-agent"]),ue=Symbol("internals");function ce(e){return e&&String(e).trim().toLowerCase()}function fe(e){return!1===e||null==e?e:q.isArray(e)?e.map(fe):String(e)}function le(e,t,n,r,o){return q.isFunction(r)?r.call(this,t,n):(o&&(t=n),q.isString(t)?q.isString(r)?-1!==t.indexOf(r):q.isRegExp(r)?r.test(t):void 0:void 0)}var de=function(e,n){function i(e){t(this,i),e&&this.set(e)}return r(i,[{key:"set",value:function(e,t,n){var r=this;function o(e,t,n){var o=ce(t);if(!o)throw new Error("header name must be a non-empty string");var i=q.findKey(r,o);(!i||void 0===r[i]||!0===n||void 0===n&&!1!==r[i])&&(r[i||t]=fe(e))}var i,a,s,u,c,f=function(e,t){return q.forEach(e,(function(e,n){return o(e,n,t)}))};return q.isPlainObject(e)||e instanceof this.constructor?f(e,t):q.isString(e)&&(e=e.trim())&&!/^[-_a-zA-Z0-9^`|~,!#$%&'*+.]+$/.test(e.trim())?f((c={},(i=e)&&i.split("\n").forEach((function(e){u=e.indexOf(":"),a=e.substring(0,u).trim().toLowerCase(),s=e.substring(u+1).trim(),!a||c[a]&&se[a]||("set-cookie"===a?c[a]?c[a].push(s):c[a]=[s]:c[a]=c[a]?c[a]+", "+s:s)})),c),t):null!=e&&o(t,e,n),this}},{key:"get",value:function(e,t){if(e=ce(e)){var n=q.findKey(this,e);if(n){var r=this[n];if(!t)return r;if(!0===t)return function(e){for(var t,n=Object.create(null),r=/([^\s,;=]+)\s*(?:=\s*([^,;]+))?/g;t=r.exec(e);)n[t[1]]=t[2];return n}(r);if(q.isFunction(t))return t.call(this,r,n);if(q.isRegExp(t))return t.exec(r);throw new TypeError("parser must be boolean|regexp|function")}}}},{key:"has",value:function(e,t){if(e=ce(e)){var n=q.findKey(this,e);return!(!n||void 0===this[n]||t&&!le(0,this[n],n,t))}return!1}},{key:"delete",value:function(e,t){var n=this,r=!1;function o(e){if(e=ce(e)){var o=q.findKey(n,e);!o||t&&!le(0,n[o],o,t)||(delete n[o],r=!0)}}return q.isArray(e)?e.forEach(o):o(e),r}},{key:"clear",value:function(e){for(var t=Object.keys(this),n=t.length,r=!1;n--;){var o=t[n];e&&!le(0,this[o],o,e,!0)||(delete this[o],r=!0)}return r}},{key:"normalize",value:function(e){var t=this,n={};return q.forEach(this,(function(r,o){var i=q.findKey(n,o);if(i)return t[i]=fe(r),void delete t[o];var a=e?function(e){return e.trim().toLowerCase().replace(/([a-z\d])(\w*)/g,(function(e,t,n){return t.toUpperCase()+n}))}(o):String(o).trim();a!==o&&delete t[o],t[a]=fe(r),n[a]=!0})),this}},{key:"concat",value:function(){for(var e,t=arguments.length,n=new Array(t),r=0;r1?n-1:0),o=1;o1?"since :\n"+u.map(Oe).join("\n"):" "+Oe(u[0]):"as no adapter specified"),"ERR_NOT_SUPPORT")}return n};function Ae(e){if(e.cancelToken&&e.cancelToken.throwIfRequested(),e.signal&&e.signal.aborted)throw new ve(null,e)}function Te(e){return Ae(e),e.headers=pe.from(e.headers),e.data=he.call(e,e.transformRequest),-1!==["post","put","patch"].indexOf(e.method)&&e.headers.setContentType("application/x-www-form-urlencoded",!1),Re(e.adapter||ae.adapter)(e).then((function(t){return Ae(e),t.data=he.call(e,e.transformResponse,t),t.headers=pe.from(t.headers),t}),(function(t){return me(t)||(Ae(e),t&&t.response&&(t.response.data=he.call(e,e.transformResponse,t.response),t.response.headers=pe.from(t.response.headers))),Promise.reject(t)}))}var je=function(e){return e instanceof pe?e.toJSON():e};function Ce(e,t){t=t||{};var n={};function r(e,t,n){return q.isPlainObject(e)&&q.isPlainObject(t)?q.merge.call({caseless:n},e,t):q.isPlainObject(t)?q.merge({},t):q.isArray(t)?t.slice():t}function o(e,t,n){return q.isUndefined(t)?q.isUndefined(e)?void 0:r(void 0,e,n):r(e,t,n)}function i(e,t){if(!q.isUndefined(t))return r(void 0,t)}function a(e,t){return q.isUndefined(t)?q.isUndefined(e)?void 0:r(void 0,e):r(void 0,t)}function s(n,o,i){return i in t?r(n,o):i in e?r(void 0,n):void 0}var u={url:i,method:i,data:i,baseURL:a,transformRequest:a,transformResponse:a,paramsSerializer:a,timeout:a,timeoutMessage:a,withCredentials:a,adapter:a,responseType:a,xsrfCookieName:a,xsrfHeaderName:a,onUploadProgress:a,onDownloadProgress:a,decompress:a,maxContentLength:a,maxBodyLength:a,beforeRedirect:a,transport:a,httpAgent:a,httpsAgent:a,cancelToken:a,socketPath:a,responseEncoding:a,validateStatus:s,headers:function(e,t){return o(je(e),je(t),!0)}};return q.forEach(Object.keys(Object.assign({},e,t)),(function(r){var i=u[r]||o,a=i(e[r],t[r],r);q.isUndefined(a)&&i!==s||(n[r]=a)})),n}var Ne="1.5.1",xe={};["object","boolean","number","function","string","symbol"].forEach((function(t,n){xe[t]=function(r){return e(r)===t||"a"+(n<1?"n ":" ")+t}}));var Pe={};xe.transitional=function(e,t,n){function r(e,t){return"[Axios v1.5.1] Transitional option '"+e+"'"+t+(n?". "+n:"")}return function(n,o,i){if(!1===e)throw new M(r(o," has been removed"+(t?" in "+t:"")),M.ERR_DEPRECATED);return t&&!Pe[o]&&(Pe[o]=!0,console.warn(r(o," has been deprecated since v"+t+" and will be removed in the near future"))),!e||e(n,o,i)}};var ke={assertOptions:function(t,n,r){if("object"!==e(t))throw new M("options must be an object",M.ERR_BAD_OPTION_VALUE);for(var o=Object.keys(t),i=o.length;i-- >0;){var a=o[i],s=n[a];if(s){var u=t[a],c=void 0===u||s(u,a,t);if(!0!==c)throw new M("option "+a+" must be "+c,M.ERR_BAD_OPTION_VALUE)}else if(!0!==r)throw new M("Unknown option "+a,M.ERR_BAD_OPTION)}},validators:xe},Ue=ke.validators,_e=function(){function e(n){t(this,e),this.defaults=n,this.interceptors={request:new te,response:new te}}return r(e,[{key:"request",value:function(e,t){"string"==typeof e?(t=t||{}).url=e:t=e||{};var n=t=Ce(this.defaults,t),r=n.transitional,o=n.paramsSerializer,i=n.headers;void 0!==r&&ke.assertOptions(r,{silentJSONParsing:Ue.transitional(Ue.boolean),forcedJSONParsing:Ue.transitional(Ue.boolean),clarifyTimeoutError:Ue.transitional(Ue.boolean)},!1),null!=o&&(q.isFunction(o)?t.paramsSerializer={serialize:o}:ke.assertOptions(o,{encode:Ue.function,serialize:Ue.function},!0)),t.method=(t.method||this.defaults.method||"get").toLowerCase();var a=i&&q.merge(i.common,i[t.method]);i&&q.forEach(["delete","get","head","post","put","patch","common"],(function(e){delete i[e]})),t.headers=pe.concat(a,i);var s=[],u=!0;this.interceptors.request.forEach((function(e){"function"==typeof e.runWhen&&!1===e.runWhen(t)||(u=u&&e.synchronous,s.unshift(e.fulfilled,e.rejected))}));var c,f=[];this.interceptors.response.forEach((function(e){f.push(e.fulfilled,e.rejected)}));var l,d=0;if(!u){var p=[Te.bind(this),void 0];for(p.unshift.apply(p,s),p.push.apply(p,f),l=p.length,c=Promise.resolve(t);d0;)o._listeners[t](e);o._listeners=null}})),this.promise.then=function(e){var t,n=new Promise((function(e){o.subscribe(e),t=e})).then(e);return n.cancel=function(){o.unsubscribe(t)},n},n((function(e,t,n){o.reason||(o.reason=new ve(e,t,n),r(o.reason))}))}return r(e,[{key:"throwIfRequested",value:function(){if(this.reason)throw this.reason}},{key:"subscribe",value:function(e){this.reason?e(this.reason):this._listeners?this._listeners.push(e):this._listeners=[e]}},{key:"unsubscribe",value:function(e){if(this._listeners){var t=this._listeners.indexOf(e);-1!==t&&this._listeners.splice(t,1)}}}],[{key:"source",value:function(){var t;return{token:new e((function(e){t=e})),cancel:t}}}]),e}();var Le={Continue:100,SwitchingProtocols:101,Processing:102,EarlyHints:103,Ok:200,Created:201,Accepted:202,NonAuthoritativeInformation:203,NoContent:204,ResetContent:205,PartialContent:206,MultiStatus:207,AlreadyReported:208,ImUsed:226,MultipleChoices:300,MovedPermanently:301,Found:302,SeeOther:303,NotModified:304,UseProxy:305,Unused:306,TemporaryRedirect:307,PermanentRedirect:308,BadRequest:400,Unauthorized:401,PaymentRequired:402,Forbidden:403,NotFound:404,MethodNotAllowed:405,NotAcceptable:406,ProxyAuthenticationRequired:407,RequestTimeout:408,Conflict:409,Gone:410,LengthRequired:411,PreconditionFailed:412,PayloadTooLarge:413,UriTooLong:414,UnsupportedMediaType:415,RangeNotSatisfiable:416,ExpectationFailed:417,ImATeapot:418,MisdirectedRequest:421,UnprocessableEntity:422,Locked:423,FailedDependency:424,TooEarly:425,UpgradeRequired:426,PreconditionRequired:428,TooManyRequests:429,RequestHeaderFieldsTooLarge:431,UnavailableForLegalReasons:451,InternalServerError:500,NotImplemented:501,BadGateway:502,ServiceUnavailable:503,GatewayTimeout:504,HttpVersionNotSupported:505,VariantAlsoNegotiates:506,InsufficientStorage:507,LoopDetected:508,NotExtended:510,NetworkAuthenticationRequired:511};Object.entries(Le).forEach((function(e){var t=o(e,2),n=t[0],r=t[1];Le[r]=n}));var De=Le;var Ie=function e(t){var n=new Fe(t),r=a(Fe.prototype.request,n);return q.extend(r,Fe.prototype,n,{allOwnKeys:!0}),q.extend(r,n,null,{allOwnKeys:!0}),r.create=function(n){return e(Ce(t,n))},r}(ae);return Ie.Axios=Fe,Ie.CanceledError=ve,Ie.CancelToken=Be,Ie.isCancel=me,Ie.VERSION=Ne,Ie.toFormData=G,Ie.AxiosError=M,Ie.Cancel=Ie.CanceledError,Ie.all=function(e){return Promise.all(e)},Ie.spread=function(e){return function(t){return e.apply(null,t)}},Ie.isAxiosError=function(e){return q.isObject(e)&&!0===e.isAxiosError},Ie.mergeConfig=Ce,Ie.AxiosHeaders=pe,Ie.formToJSON=function(e){return oe(q.isHTMLForm(e)?new FormData(e):e)},Ie.getAdapter=Re,Ie.HttpStatusCode=De,Ie.default=Ie,Ie})); \ No newline at end of file diff --git a/Streamdeck/com.mirabox.streamingle.sdPlugin/propertyInspector/utils/bootstrap-icons.css b/Streamdeck/com.mirabox.streamingle.sdPlugin/propertyInspector/utils/bootstrap-icons.css new file mode 100644 index 00000000..3c1537f1 --- /dev/null +++ b/Streamdeck/com.mirabox.streamingle.sdPlugin/propertyInspector/utils/bootstrap-icons.css @@ -0,0 +1,1556 @@ +@font-face { + font-family: "bootstrap-icons"; + src: url("./fonts/bootstrap-icons.woff2?30af91bf14e37666a085fb8a161ff36d") format("woff2"), +url("./fonts/bootstrap-icons.woff?30af91bf14e37666a085fb8a161ff36d") format("woff"); +} + +.bi::before, +[class^="bi-"]::before, +[class*=" bi-"]::before { + display: inline-block; + font-family: bootstrap-icons !important; + font-style: normal; + font-weight: normal !important; + font-variant: normal; + text-transform: none; + line-height: 1; + vertical-align: -.125em; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.bi-123::before { content: "\f67f"; } +.bi-alarm-fill::before { content: "\f101"; } +.bi-alarm::before { content: "\f102"; } +.bi-align-bottom::before { content: "\f103"; } +.bi-align-center::before { content: "\f104"; } +.bi-align-end::before { content: "\f105"; } +.bi-align-middle::before { content: "\f106"; } +.bi-align-start::before { content: "\f107"; } +.bi-align-top::before { content: "\f108"; } +.bi-alt::before { content: "\f109"; } +.bi-app-indicator::before { content: "\f10a"; } +.bi-app::before { content: "\f10b"; } +.bi-archive-fill::before { content: "\f10c"; } +.bi-archive::before { content: "\f10d"; } +.bi-arrow-90deg-down::before { content: "\f10e"; } +.bi-arrow-90deg-left::before { content: "\f10f"; } +.bi-arrow-90deg-right::before { content: "\f110"; } +.bi-arrow-90deg-up::before { content: "\f111"; } +.bi-arrow-bar-down::before { content: "\f112"; } +.bi-arrow-bar-left::before { content: "\f113"; } +.bi-arrow-bar-right::before { content: "\f114"; } +.bi-arrow-bar-up::before { content: "\f115"; } +.bi-arrow-clockwise::before { content: "\f116"; } +.bi-arrow-counterclockwise::before { content: "\f117"; } +.bi-arrow-down-circle-fill::before { content: "\f118"; } +.bi-arrow-down-circle::before { content: "\f119"; } +.bi-arrow-down-left-circle-fill::before { content: "\f11a"; } +.bi-arrow-down-left-circle::before { content: "\f11b"; } +.bi-arrow-down-left-square-fill::before { content: "\f11c"; } +.bi-arrow-down-left-square::before { content: "\f11d"; } +.bi-arrow-down-left::before { content: "\f11e"; } +.bi-arrow-down-right-circle-fill::before { content: "\f11f"; } +.bi-arrow-down-right-circle::before { content: "\f120"; } +.bi-arrow-down-right-square-fill::before { content: "\f121"; } +.bi-arrow-down-right-square::before { content: "\f122"; } +.bi-arrow-down-right::before { content: "\f123"; } +.bi-arrow-down-short::before { content: "\f124"; } +.bi-arrow-down-square-fill::before { content: "\f125"; } +.bi-arrow-down-square::before { content: "\f126"; } +.bi-arrow-down-up::before { content: "\f127"; } +.bi-arrow-down::before { content: "\f128"; } +.bi-arrow-left-circle-fill::before { content: "\f129"; } +.bi-arrow-left-circle::before { content: "\f12a"; } +.bi-arrow-left-right::before { content: "\f12b"; } +.bi-arrow-left-short::before { content: "\f12c"; } +.bi-arrow-left-square-fill::before { content: "\f12d"; } +.bi-arrow-left-square::before { content: "\f12e"; } +.bi-arrow-left::before { content: "\f12f"; } +.bi-arrow-repeat::before { content: "\f130"; } +.bi-arrow-return-left::before { content: "\f131"; } +.bi-arrow-return-right::before { content: "\f132"; } +.bi-arrow-right-circle-fill::before { content: "\f133"; } +.bi-arrow-right-circle::before { content: "\f134"; } +.bi-arrow-right-short::before { content: "\f135"; } +.bi-arrow-right-square-fill::before { content: "\f136"; } +.bi-arrow-right-square::before { content: "\f137"; } +.bi-arrow-right::before { content: "\f138"; } +.bi-arrow-up-circle-fill::before { content: "\f139"; } +.bi-arrow-up-circle::before { content: "\f13a"; } +.bi-arrow-up-left-circle-fill::before { content: "\f13b"; } +.bi-arrow-up-left-circle::before { content: "\f13c"; } +.bi-arrow-up-left-square-fill::before { content: "\f13d"; } +.bi-arrow-up-left-square::before { content: "\f13e"; } +.bi-arrow-up-left::before { content: "\f13f"; } +.bi-arrow-up-right-circle-fill::before { content: "\f140"; } +.bi-arrow-up-right-circle::before { content: "\f141"; } +.bi-arrow-up-right-square-fill::before { content: "\f142"; } +.bi-arrow-up-right-square::before { content: "\f143"; } +.bi-arrow-up-right::before { content: "\f144"; } +.bi-arrow-up-short::before { content: "\f145"; } +.bi-arrow-up-square-fill::before { content: "\f146"; } +.bi-arrow-up-square::before { content: "\f147"; } +.bi-arrow-up::before { content: "\f148"; } +.bi-arrows-angle-contract::before { content: "\f149"; } +.bi-arrows-angle-expand::before { content: "\f14a"; } +.bi-arrows-collapse::before { content: "\f14b"; } +.bi-arrows-expand::before { content: "\f14c"; } +.bi-arrows-fullscreen::before { content: "\f14d"; } +.bi-arrows-move::before { content: "\f14e"; } +.bi-aspect-ratio-fill::before { content: "\f14f"; } +.bi-aspect-ratio::before { content: "\f150"; } +.bi-asterisk::before { content: "\f151"; } +.bi-at::before { content: "\f152"; } +.bi-award-fill::before { content: "\f153"; } +.bi-award::before { content: "\f154"; } +.bi-back::before { content: "\f155"; } +.bi-backspace-fill::before { content: "\f156"; } +.bi-backspace-reverse-fill::before { content: "\f157"; } +.bi-backspace-reverse::before { content: "\f158"; } +.bi-backspace::before { content: "\f159"; } +.bi-badge-3d-fill::before { content: "\f15a"; } +.bi-badge-3d::before { content: "\f15b"; } +.bi-badge-4k-fill::before { content: "\f15c"; } +.bi-badge-4k::before { content: "\f15d"; } +.bi-badge-8k-fill::before { content: "\f15e"; } +.bi-badge-8k::before { content: "\f15f"; } +.bi-badge-ad-fill::before { content: "\f160"; } +.bi-badge-ad::before { content: "\f161"; } +.bi-badge-ar-fill::before { content: "\f162"; } +.bi-badge-ar::before { content: "\f163"; } +.bi-badge-cc-fill::before { content: "\f164"; } +.bi-badge-cc::before { content: "\f165"; } +.bi-badge-hd-fill::before { content: "\f166"; } +.bi-badge-hd::before { content: "\f167"; } +.bi-badge-tm-fill::before { content: "\f168"; } +.bi-badge-tm::before { content: "\f169"; } +.bi-badge-vo-fill::before { content: "\f16a"; } +.bi-badge-vo::before { content: "\f16b"; } +.bi-badge-vr-fill::before { content: "\f16c"; } +.bi-badge-vr::before { content: "\f16d"; } +.bi-badge-wc-fill::before { content: "\f16e"; } +.bi-badge-wc::before { content: "\f16f"; } +.bi-bag-check-fill::before { content: "\f170"; } +.bi-bag-check::before { content: "\f171"; } +.bi-bag-dash-fill::before { content: "\f172"; } +.bi-bag-dash::before { content: "\f173"; } +.bi-bag-fill::before { content: "\f174"; } +.bi-bag-plus-fill::before { content: "\f175"; } +.bi-bag-plus::before { content: "\f176"; } +.bi-bag-x-fill::before { content: "\f177"; } +.bi-bag-x::before { content: "\f178"; } +.bi-bag::before { content: "\f179"; } +.bi-bar-chart-fill::before { content: "\f17a"; } +.bi-bar-chart-line-fill::before { content: "\f17b"; } +.bi-bar-chart-line::before { content: "\f17c"; } +.bi-bar-chart-steps::before { content: "\f17d"; } +.bi-bar-chart::before { content: "\f17e"; } +.bi-basket-fill::before { content: "\f17f"; } +.bi-basket::before { content: "\f180"; } +.bi-basket2-fill::before { content: "\f181"; } +.bi-basket2::before { content: "\f182"; } +.bi-basket3-fill::before { content: "\f183"; } +.bi-basket3::before { content: "\f184"; } +.bi-battery-charging::before { content: "\f185"; } +.bi-battery-full::before { content: "\f186"; } +.bi-battery-half::before { content: "\f187"; } +.bi-battery::before { content: "\f188"; } +.bi-bell-fill::before { content: "\f189"; } +.bi-bell::before { content: "\f18a"; } +.bi-bezier::before { content: "\f18b"; } +.bi-bezier2::before { content: "\f18c"; } +.bi-bicycle::before { content: "\f18d"; } +.bi-binoculars-fill::before { content: "\f18e"; } +.bi-binoculars::before { content: "\f18f"; } +.bi-blockquote-left::before { content: "\f190"; } +.bi-blockquote-right::before { content: "\f191"; } +.bi-book-fill::before { content: "\f192"; } +.bi-book-half::before { content: "\f193"; } +.bi-book::before { content: "\f194"; } +.bi-bookmark-check-fill::before { content: "\f195"; } +.bi-bookmark-check::before { content: "\f196"; } +.bi-bookmark-dash-fill::before { content: "\f197"; } +.bi-bookmark-dash::before { content: "\f198"; } +.bi-bookmark-fill::before { content: "\f199"; } +.bi-bookmark-heart-fill::before { content: "\f19a"; } +.bi-bookmark-heart::before { content: "\f19b"; } +.bi-bookmark-plus-fill::before { content: "\f19c"; } +.bi-bookmark-plus::before { content: "\f19d"; } +.bi-bookmark-star-fill::before { content: "\f19e"; } +.bi-bookmark-star::before { content: "\f19f"; } +.bi-bookmark-x-fill::before { content: "\f1a0"; } +.bi-bookmark-x::before { content: "\f1a1"; } +.bi-bookmark::before { content: "\f1a2"; } +.bi-bookmarks-fill::before { content: "\f1a3"; } +.bi-bookmarks::before { content: "\f1a4"; } +.bi-bookshelf::before { content: "\f1a5"; } +.bi-bootstrap-fill::before { content: "\f1a6"; } +.bi-bootstrap-reboot::before { content: "\f1a7"; } +.bi-bootstrap::before { content: "\f1a8"; } +.bi-border-all::before { content: "\f1a9"; } +.bi-border-bottom::before { content: "\f1aa"; } +.bi-border-center::before { content: "\f1ab"; } +.bi-border-inner::before { content: "\f1ac"; } +.bi-border-left::before { content: "\f1ad"; } +.bi-border-middle::before { content: "\f1ae"; } +.bi-border-outer::before { content: "\f1af"; } +.bi-border-right::before { content: "\f1b0"; } +.bi-border-style::before { content: "\f1b1"; } +.bi-border-top::before { content: "\f1b2"; } +.bi-border-width::before { content: "\f1b3"; } +.bi-border::before { content: "\f1b4"; } +.bi-bounding-box-circles::before { content: "\f1b5"; } +.bi-bounding-box::before { content: "\f1b6"; } +.bi-box-arrow-down-left::before { content: "\f1b7"; } +.bi-box-arrow-down-right::before { content: "\f1b8"; } +.bi-box-arrow-down::before { content: "\f1b9"; } +.bi-box-arrow-in-down-left::before { content: "\f1ba"; } +.bi-box-arrow-in-down-right::before { content: "\f1bb"; } +.bi-box-arrow-in-down::before { content: "\f1bc"; } +.bi-box-arrow-in-left::before { content: "\f1bd"; } +.bi-box-arrow-in-right::before { content: "\f1be"; } +.bi-box-arrow-in-up-left::before { content: "\f1bf"; } +.bi-box-arrow-in-up-right::before { content: "\f1c0"; } +.bi-box-arrow-in-up::before { content: "\f1c1"; } +.bi-box-arrow-left::before { content: "\f1c2"; } +.bi-box-arrow-right::before { content: "\f1c3"; } +.bi-box-arrow-up-left::before { content: "\f1c4"; } +.bi-box-arrow-up-right::before { content: "\f1c5"; } +.bi-box-arrow-up::before { content: "\f1c6"; } +.bi-box-seam::before { content: "\f1c7"; } +.bi-box::before { content: "\f1c8"; } +.bi-braces::before { content: "\f1c9"; } +.bi-bricks::before { content: "\f1ca"; } +.bi-briefcase-fill::before { content: "\f1cb"; } +.bi-briefcase::before { content: "\f1cc"; } +.bi-brightness-alt-high-fill::before { content: "\f1cd"; } +.bi-brightness-alt-high::before { content: "\f1ce"; } +.bi-brightness-alt-low-fill::before { content: "\f1cf"; } +.bi-brightness-alt-low::before { content: "\f1d0"; } +.bi-brightness-high-fill::before { content: "\f1d1"; } +.bi-brightness-high::before { content: "\f1d2"; } +.bi-brightness-low-fill::before { content: "\f1d3"; } +.bi-brightness-low::before { content: "\f1d4"; } +.bi-broadcast-pin::before { content: "\f1d5"; } +.bi-broadcast::before { content: "\f1d6"; } +.bi-brush-fill::before { content: "\f1d7"; } +.bi-brush::before { content: "\f1d8"; } +.bi-bucket-fill::before { content: "\f1d9"; } +.bi-bucket::before { content: "\f1da"; } +.bi-bug-fill::before { content: "\f1db"; } +.bi-bug::before { content: "\f1dc"; } +.bi-building::before { content: "\f1dd"; } +.bi-bullseye::before { content: "\f1de"; } +.bi-calculator-fill::before { content: "\f1df"; } +.bi-calculator::before { content: "\f1e0"; } +.bi-calendar-check-fill::before { content: "\f1e1"; } +.bi-calendar-check::before { content: "\f1e2"; } +.bi-calendar-date-fill::before { content: "\f1e3"; } +.bi-calendar-date::before { content: "\f1e4"; } +.bi-calendar-day-fill::before { content: "\f1e5"; } +.bi-calendar-day::before { content: "\f1e6"; } +.bi-calendar-event-fill::before { content: "\f1e7"; } +.bi-calendar-event::before { content: "\f1e8"; } +.bi-calendar-fill::before { content: "\f1e9"; } +.bi-calendar-minus-fill::before { content: "\f1ea"; } +.bi-calendar-minus::before { content: "\f1eb"; } +.bi-calendar-month-fill::before { content: "\f1ec"; } +.bi-calendar-month::before { content: "\f1ed"; } +.bi-calendar-plus-fill::before { content: "\f1ee"; } +.bi-calendar-plus::before { content: "\f1ef"; } +.bi-calendar-range-fill::before { content: "\f1f0"; } +.bi-calendar-range::before { content: "\f1f1"; } +.bi-calendar-week-fill::before { content: "\f1f2"; } +.bi-calendar-week::before { content: "\f1f3"; } +.bi-calendar-x-fill::before { content: "\f1f4"; } +.bi-calendar-x::before { content: "\f1f5"; } +.bi-calendar::before { content: "\f1f6"; } +.bi-calendar2-check-fill::before { content: "\f1f7"; } +.bi-calendar2-check::before { content: "\f1f8"; } +.bi-calendar2-date-fill::before { content: "\f1f9"; } +.bi-calendar2-date::before { content: "\f1fa"; } +.bi-calendar2-day-fill::before { content: "\f1fb"; } +.bi-calendar2-day::before { content: "\f1fc"; } +.bi-calendar2-event-fill::before { content: "\f1fd"; } +.bi-calendar2-event::before { content: "\f1fe"; } +.bi-calendar2-fill::before { content: "\f1ff"; } +.bi-calendar2-minus-fill::before { content: "\f200"; } +.bi-calendar2-minus::before { content: "\f201"; } +.bi-calendar2-month-fill::before { content: "\f202"; } +.bi-calendar2-month::before { content: "\f203"; } +.bi-calendar2-plus-fill::before { content: "\f204"; } +.bi-calendar2-plus::before { content: "\f205"; } +.bi-calendar2-range-fill::before { content: "\f206"; } +.bi-calendar2-range::before { content: "\f207"; } +.bi-calendar2-week-fill::before { content: "\f208"; } +.bi-calendar2-week::before { content: "\f209"; } +.bi-calendar2-x-fill::before { content: "\f20a"; } +.bi-calendar2-x::before { content: "\f20b"; } +.bi-calendar2::before { content: "\f20c"; } +.bi-calendar3-event-fill::before { content: "\f20d"; } +.bi-calendar3-event::before { content: "\f20e"; } +.bi-calendar3-fill::before { content: "\f20f"; } +.bi-calendar3-range-fill::before { content: "\f210"; } +.bi-calendar3-range::before { content: "\f211"; } +.bi-calendar3-week-fill::before { content: "\f212"; } +.bi-calendar3-week::before { content: "\f213"; } +.bi-calendar3::before { content: "\f214"; } +.bi-calendar4-event::before { content: "\f215"; } +.bi-calendar4-range::before { content: "\f216"; } +.bi-calendar4-week::before { content: "\f217"; } +.bi-calendar4::before { content: "\f218"; } +.bi-camera-fill::before { content: "\f219"; } +.bi-camera-reels-fill::before { content: "\f21a"; } +.bi-camera-reels::before { content: "\f21b"; } +.bi-camera-video-fill::before { content: "\f21c"; } +.bi-camera-video-off-fill::before { content: "\f21d"; } +.bi-camera-video-off::before { content: "\f21e"; } +.bi-camera-video::before { content: "\f21f"; } +.bi-camera::before { content: "\f220"; } +.bi-camera2::before { content: "\f221"; } +.bi-capslock-fill::before { content: "\f222"; } +.bi-capslock::before { content: "\f223"; } +.bi-card-checklist::before { content: "\f224"; } +.bi-card-heading::before { content: "\f225"; } +.bi-card-image::before { content: "\f226"; } +.bi-card-list::before { content: "\f227"; } +.bi-card-text::before { content: "\f228"; } +.bi-caret-down-fill::before { content: "\f229"; } +.bi-caret-down-square-fill::before { content: "\f22a"; } +.bi-caret-down-square::before { content: "\f22b"; } +.bi-caret-down::before { content: "\f22c"; } +.bi-caret-left-fill::before { content: "\f22d"; } +.bi-caret-left-square-fill::before { content: "\f22e"; } +.bi-caret-left-square::before { content: "\f22f"; } +.bi-caret-left::before { content: "\f230"; } +.bi-caret-right-fill::before { content: "\f231"; } +.bi-caret-right-square-fill::before { content: "\f232"; } +.bi-caret-right-square::before { content: "\f233"; } +.bi-caret-right::before { content: "\f234"; } +.bi-caret-up-fill::before { content: "\f235"; } +.bi-caret-up-square-fill::before { content: "\f236"; } +.bi-caret-up-square::before { content: "\f237"; } +.bi-caret-up::before { content: "\f238"; } +.bi-cart-check-fill::before { content: "\f239"; } +.bi-cart-check::before { content: "\f23a"; } +.bi-cart-dash-fill::before { content: "\f23b"; } +.bi-cart-dash::before { content: "\f23c"; } +.bi-cart-fill::before { content: "\f23d"; } +.bi-cart-plus-fill::before { content: "\f23e"; } +.bi-cart-plus::before { content: "\f23f"; } +.bi-cart-x-fill::before { content: "\f240"; } +.bi-cart-x::before { content: "\f241"; } +.bi-cart::before { content: "\f242"; } +.bi-cart2::before { content: "\f243"; } +.bi-cart3::before { content: "\f244"; } +.bi-cart4::before { content: "\f245"; } +.bi-cash-stack::before { content: "\f246"; } +.bi-cash::before { content: "\f247"; } +.bi-cast::before { content: "\f248"; } +.bi-chat-dots-fill::before { content: "\f249"; } +.bi-chat-dots::before { content: "\f24a"; } +.bi-chat-fill::before { content: "\f24b"; } +.bi-chat-left-dots-fill::before { content: "\f24c"; } +.bi-chat-left-dots::before { content: "\f24d"; } +.bi-chat-left-fill::before { content: "\f24e"; } +.bi-chat-left-quote-fill::before { content: "\f24f"; } +.bi-chat-left-quote::before { content: "\f250"; } +.bi-chat-left-text-fill::before { content: "\f251"; } +.bi-chat-left-text::before { content: "\f252"; } +.bi-chat-left::before { content: "\f253"; } +.bi-chat-quote-fill::before { content: "\f254"; } +.bi-chat-quote::before { content: "\f255"; } +.bi-chat-right-dots-fill::before { content: "\f256"; } +.bi-chat-right-dots::before { content: "\f257"; } +.bi-chat-right-fill::before { content: "\f258"; } +.bi-chat-right-quote-fill::before { content: "\f259"; } +.bi-chat-right-quote::before { content: "\f25a"; } +.bi-chat-right-text-fill::before { content: "\f25b"; } +.bi-chat-right-text::before { content: "\f25c"; } +.bi-chat-right::before { content: "\f25d"; } +.bi-chat-square-dots-fill::before { content: "\f25e"; } +.bi-chat-square-dots::before { content: "\f25f"; } +.bi-chat-square-fill::before { content: "\f260"; } +.bi-chat-square-quote-fill::before { content: "\f261"; } +.bi-chat-square-quote::before { content: "\f262"; } +.bi-chat-square-text-fill::before { content: "\f263"; } +.bi-chat-square-text::before { content: "\f264"; } +.bi-chat-square::before { content: "\f265"; } +.bi-chat-text-fill::before { content: "\f266"; } +.bi-chat-text::before { content: "\f267"; } +.bi-chat::before { content: "\f268"; } +.bi-check-all::before { content: "\f269"; } +.bi-check-circle-fill::before { content: "\f26a"; } +.bi-check-circle::before { content: "\f26b"; } +.bi-check-square-fill::before { content: "\f26c"; } +.bi-check-square::before { content: "\f26d"; } +.bi-check::before { content: "\f26e"; } +.bi-check2-all::before { content: "\f26f"; } +.bi-check2-circle::before { content: "\f270"; } +.bi-check2-square::before { content: "\f271"; } +.bi-check2::before { content: "\f272"; } +.bi-chevron-bar-contract::before { content: "\f273"; } +.bi-chevron-bar-down::before { content: "\f274"; } +.bi-chevron-bar-expand::before { content: "\f275"; } +.bi-chevron-bar-left::before { content: "\f276"; } +.bi-chevron-bar-right::before { content: "\f277"; } +.bi-chevron-bar-up::before { content: "\f278"; } +.bi-chevron-compact-down::before { content: "\f279"; } +.bi-chevron-compact-left::before { content: "\f27a"; } +.bi-chevron-compact-right::before { content: "\f27b"; } +.bi-chevron-compact-up::before { content: "\f27c"; } +.bi-chevron-contract::before { content: "\f27d"; } +.bi-chevron-double-down::before { content: "\f27e"; } +.bi-chevron-double-left::before { content: "\f27f"; } +.bi-chevron-double-right::before { content: "\f280"; } +.bi-chevron-double-up::before { content: "\f281"; } +.bi-chevron-down::before { content: "\f282"; } +.bi-chevron-expand::before { content: "\f283"; } +.bi-chevron-left::before { content: "\f284"; } +.bi-chevron-right::before { content: "\f285"; } +.bi-chevron-up::before { content: "\f286"; } +.bi-circle-fill::before { content: "\f287"; } +.bi-circle-half::before { content: "\f288"; } +.bi-circle-square::before { content: "\f289"; } +.bi-circle::before { content: "\f28a"; } +.bi-clipboard-check::before { content: "\f28b"; } +.bi-clipboard-data::before { content: "\f28c"; } +.bi-clipboard-minus::before { content: "\f28d"; } +.bi-clipboard-plus::before { content: "\f28e"; } +.bi-clipboard-x::before { content: "\f28f"; } +.bi-clipboard::before { content: "\f290"; } +.bi-clock-fill::before { content: "\f291"; } +.bi-clock-history::before { content: "\f292"; } +.bi-clock::before { content: "\f293"; } +.bi-cloud-arrow-down-fill::before { content: "\f294"; } +.bi-cloud-arrow-down::before { content: "\f295"; } +.bi-cloud-arrow-up-fill::before { content: "\f296"; } +.bi-cloud-arrow-up::before { content: "\f297"; } +.bi-cloud-check-fill::before { content: "\f298"; } +.bi-cloud-check::before { content: "\f299"; } +.bi-cloud-download-fill::before { content: "\f29a"; } +.bi-cloud-download::before { content: "\f29b"; } +.bi-cloud-drizzle-fill::before { content: "\f29c"; } +.bi-cloud-drizzle::before { content: "\f29d"; } +.bi-cloud-fill::before { content: "\f29e"; } +.bi-cloud-fog-fill::before { content: "\f29f"; } +.bi-cloud-fog::before { content: "\f2a0"; } +.bi-cloud-fog2-fill::before { content: "\f2a1"; } +.bi-cloud-fog2::before { content: "\f2a2"; } +.bi-cloud-hail-fill::before { content: "\f2a3"; } +.bi-cloud-hail::before { content: "\f2a4"; } +.bi-cloud-haze-1::before { content: "\f2a5"; } +.bi-cloud-haze-fill::before { content: "\f2a6"; } +.bi-cloud-haze::before { content: "\f2a7"; } +.bi-cloud-haze2-fill::before { content: "\f2a8"; } +.bi-cloud-lightning-fill::before { content: "\f2a9"; } +.bi-cloud-lightning-rain-fill::before { content: "\f2aa"; } +.bi-cloud-lightning-rain::before { content: "\f2ab"; } +.bi-cloud-lightning::before { content: "\f2ac"; } +.bi-cloud-minus-fill::before { content: "\f2ad"; } +.bi-cloud-minus::before { content: "\f2ae"; } +.bi-cloud-moon-fill::before { content: "\f2af"; } +.bi-cloud-moon::before { content: "\f2b0"; } +.bi-cloud-plus-fill::before { content: "\f2b1"; } +.bi-cloud-plus::before { content: "\f2b2"; } +.bi-cloud-rain-fill::before { content: "\f2b3"; } +.bi-cloud-rain-heavy-fill::before { content: "\f2b4"; } +.bi-cloud-rain-heavy::before { content: "\f2b5"; } +.bi-cloud-rain::before { content: "\f2b6"; } +.bi-cloud-slash-fill::before { content: "\f2b7"; } +.bi-cloud-slash::before { content: "\f2b8"; } +.bi-cloud-sleet-fill::before { content: "\f2b9"; } +.bi-cloud-sleet::before { content: "\f2ba"; } +.bi-cloud-snow-fill::before { content: "\f2bb"; } +.bi-cloud-snow::before { content: "\f2bc"; } +.bi-cloud-sun-fill::before { content: "\f2bd"; } +.bi-cloud-sun::before { content: "\f2be"; } +.bi-cloud-upload-fill::before { content: "\f2bf"; } +.bi-cloud-upload::before { content: "\f2c0"; } +.bi-cloud::before { content: "\f2c1"; } +.bi-clouds-fill::before { content: "\f2c2"; } +.bi-clouds::before { content: "\f2c3"; } +.bi-cloudy-fill::before { content: "\f2c4"; } +.bi-cloudy::before { content: "\f2c5"; } +.bi-code-slash::before { content: "\f2c6"; } +.bi-code-square::before { content: "\f2c7"; } +.bi-code::before { content: "\f2c8"; } +.bi-collection-fill::before { content: "\f2c9"; } +.bi-collection-play-fill::before { content: "\f2ca"; } +.bi-collection-play::before { content: "\f2cb"; } +.bi-collection::before { content: "\f2cc"; } +.bi-columns-gap::before { content: "\f2cd"; } +.bi-columns::before { content: "\f2ce"; } +.bi-command::before { content: "\f2cf"; } +.bi-compass-fill::before { content: "\f2d0"; } +.bi-compass::before { content: "\f2d1"; } +.bi-cone-striped::before { content: "\f2d2"; } +.bi-cone::before { content: "\f2d3"; } +.bi-controller::before { content: "\f2d4"; } +.bi-cpu-fill::before { content: "\f2d5"; } +.bi-cpu::before { content: "\f2d6"; } +.bi-credit-card-2-back-fill::before { content: "\f2d7"; } +.bi-credit-card-2-back::before { content: "\f2d8"; } +.bi-credit-card-2-front-fill::before { content: "\f2d9"; } +.bi-credit-card-2-front::before { content: "\f2da"; } +.bi-credit-card-fill::before { content: "\f2db"; } +.bi-credit-card::before { content: "\f2dc"; } +.bi-crop::before { content: "\f2dd"; } +.bi-cup-fill::before { content: "\f2de"; } +.bi-cup-straw::before { content: "\f2df"; } +.bi-cup::before { content: "\f2e0"; } +.bi-cursor-fill::before { content: "\f2e1"; } +.bi-cursor-text::before { content: "\f2e2"; } +.bi-cursor::before { content: "\f2e3"; } +.bi-dash-circle-dotted::before { content: "\f2e4"; } +.bi-dash-circle-fill::before { content: "\f2e5"; } +.bi-dash-circle::before { content: "\f2e6"; } +.bi-dash-square-dotted::before { content: "\f2e7"; } +.bi-dash-square-fill::before { content: "\f2e8"; } +.bi-dash-square::before { content: "\f2e9"; } +.bi-dash::before { content: "\f2ea"; } +.bi-diagram-2-fill::before { content: "\f2eb"; } +.bi-diagram-2::before { content: "\f2ec"; } +.bi-diagram-3-fill::before { content: "\f2ed"; } +.bi-diagram-3::before { content: "\f2ee"; } +.bi-diamond-fill::before { content: "\f2ef"; } +.bi-diamond-half::before { content: "\f2f0"; } +.bi-diamond::before { content: "\f2f1"; } +.bi-dice-1-fill::before { content: "\f2f2"; } +.bi-dice-1::before { content: "\f2f3"; } +.bi-dice-2-fill::before { content: "\f2f4"; } +.bi-dice-2::before { content: "\f2f5"; } +.bi-dice-3-fill::before { content: "\f2f6"; } +.bi-dice-3::before { content: "\f2f7"; } +.bi-dice-4-fill::before { content: "\f2f8"; } +.bi-dice-4::before { content: "\f2f9"; } +.bi-dice-5-fill::before { content: "\f2fa"; } +.bi-dice-5::before { content: "\f2fb"; } +.bi-dice-6-fill::before { content: "\f2fc"; } +.bi-dice-6::before { content: "\f2fd"; } +.bi-disc-fill::before { content: "\f2fe"; } +.bi-disc::before { content: "\f2ff"; } +.bi-discord::before { content: "\f300"; } +.bi-display-fill::before { content: "\f301"; } +.bi-display::before { content: "\f302"; } +.bi-distribute-horizontal::before { content: "\f303"; } +.bi-distribute-vertical::before { content: "\f304"; } +.bi-door-closed-fill::before { content: "\f305"; } +.bi-door-closed::before { content: "\f306"; } +.bi-door-open-fill::before { content: "\f307"; } +.bi-door-open::before { content: "\f308"; } +.bi-dot::before { content: "\f309"; } +.bi-download::before { content: "\f30a"; } +.bi-droplet-fill::before { content: "\f30b"; } +.bi-droplet-half::before { content: "\f30c"; } +.bi-droplet::before { content: "\f30d"; } +.bi-earbuds::before { content: "\f30e"; } +.bi-easel-fill::before { content: "\f30f"; } +.bi-easel::before { content: "\f310"; } +.bi-egg-fill::before { content: "\f311"; } +.bi-egg-fried::before { content: "\f312"; } +.bi-egg::before { content: "\f313"; } +.bi-eject-fill::before { content: "\f314"; } +.bi-eject::before { content: "\f315"; } +.bi-emoji-angry-fill::before { content: "\f316"; } +.bi-emoji-angry::before { content: "\f317"; } +.bi-emoji-dizzy-fill::before { content: "\f318"; } +.bi-emoji-dizzy::before { content: "\f319"; } +.bi-emoji-expressionless-fill::before { content: "\f31a"; } +.bi-emoji-expressionless::before { content: "\f31b"; } +.bi-emoji-frown-fill::before { content: "\f31c"; } +.bi-emoji-frown::before { content: "\f31d"; } +.bi-emoji-heart-eyes-fill::before { content: "\f31e"; } +.bi-emoji-heart-eyes::before { content: "\f31f"; } +.bi-emoji-laughing-fill::before { content: "\f320"; } +.bi-emoji-laughing::before { content: "\f321"; } +.bi-emoji-neutral-fill::before { content: "\f322"; } +.bi-emoji-neutral::before { content: "\f323"; } +.bi-emoji-smile-fill::before { content: "\f324"; } +.bi-emoji-smile-upside-down-fill::before { content: "\f325"; } +.bi-emoji-smile-upside-down::before { content: "\f326"; } +.bi-emoji-smile::before { content: "\f327"; } +.bi-emoji-sunglasses-fill::before { content: "\f328"; } +.bi-emoji-sunglasses::before { content: "\f329"; } +.bi-emoji-wink-fill::before { content: "\f32a"; } +.bi-emoji-wink::before { content: "\f32b"; } +.bi-envelope-fill::before { content: "\f32c"; } +.bi-envelope-open-fill::before { content: "\f32d"; } +.bi-envelope-open::before { content: "\f32e"; } +.bi-envelope::before { content: "\f32f"; } +.bi-eraser-fill::before { content: "\f330"; } +.bi-eraser::before { content: "\f331"; } +.bi-exclamation-circle-fill::before { content: "\f332"; } +.bi-exclamation-circle::before { content: "\f333"; } +.bi-exclamation-diamond-fill::before { content: "\f334"; } +.bi-exclamation-diamond::before { content: "\f335"; } +.bi-exclamation-octagon-fill::before { content: "\f336"; } +.bi-exclamation-octagon::before { content: "\f337"; } +.bi-exclamation-square-fill::before { content: "\f338"; } +.bi-exclamation-square::before { content: "\f339"; } +.bi-exclamation-triangle-fill::before { content: "\f33a"; } +.bi-exclamation-triangle::before { content: "\f33b"; } +.bi-exclamation::before { content: "\f33c"; } +.bi-exclude::before { content: "\f33d"; } +.bi-eye-fill::before { content: "\f33e"; } +.bi-eye-slash-fill::before { content: "\f33f"; } +.bi-eye-slash::before { content: "\f340"; } +.bi-eye::before { content: "\f341"; } +.bi-eyedropper::before { content: "\f342"; } +.bi-eyeglasses::before { content: "\f343"; } +.bi-facebook::before { content: "\f344"; } +.bi-file-arrow-down-fill::before { content: "\f345"; } +.bi-file-arrow-down::before { content: "\f346"; } +.bi-file-arrow-up-fill::before { content: "\f347"; } +.bi-file-arrow-up::before { content: "\f348"; } +.bi-file-bar-graph-fill::before { content: "\f349"; } +.bi-file-bar-graph::before { content: "\f34a"; } +.bi-file-binary-fill::before { content: "\f34b"; } +.bi-file-binary::before { content: "\f34c"; } +.bi-file-break-fill::before { content: "\f34d"; } +.bi-file-break::before { content: "\f34e"; } +.bi-file-check-fill::before { content: "\f34f"; } +.bi-file-check::before { content: "\f350"; } +.bi-file-code-fill::before { content: "\f351"; } +.bi-file-code::before { content: "\f352"; } +.bi-file-diff-fill::before { content: "\f353"; } +.bi-file-diff::before { content: "\f354"; } +.bi-file-earmark-arrow-down-fill::before { content: "\f355"; } +.bi-file-earmark-arrow-down::before { content: "\f356"; } +.bi-file-earmark-arrow-up-fill::before { content: "\f357"; } +.bi-file-earmark-arrow-up::before { content: "\f358"; } +.bi-file-earmark-bar-graph-fill::before { content: "\f359"; } +.bi-file-earmark-bar-graph::before { content: "\f35a"; } +.bi-file-earmark-binary-fill::before { content: "\f35b"; } +.bi-file-earmark-binary::before { content: "\f35c"; } +.bi-file-earmark-break-fill::before { content: "\f35d"; } +.bi-file-earmark-break::before { content: "\f35e"; } +.bi-file-earmark-check-fill::before { content: "\f35f"; } +.bi-file-earmark-check::before { content: "\f360"; } +.bi-file-earmark-code-fill::before { content: "\f361"; } +.bi-file-earmark-code::before { content: "\f362"; } +.bi-file-earmark-diff-fill::before { content: "\f363"; } +.bi-file-earmark-diff::before { content: "\f364"; } +.bi-file-earmark-easel-fill::before { content: "\f365"; } +.bi-file-earmark-easel::before { content: "\f366"; } +.bi-file-earmark-excel-fill::before { content: "\f367"; } +.bi-file-earmark-excel::before { content: "\f368"; } +.bi-file-earmark-fill::before { content: "\f369"; } +.bi-file-earmark-font-fill::before { content: "\f36a"; } +.bi-file-earmark-font::before { content: "\f36b"; } +.bi-file-earmark-image-fill::before { content: "\f36c"; } +.bi-file-earmark-image::before { content: "\f36d"; } +.bi-file-earmark-lock-fill::before { content: "\f36e"; } +.bi-file-earmark-lock::before { content: "\f36f"; } +.bi-file-earmark-lock2-fill::before { content: "\f370"; } +.bi-file-earmark-lock2::before { content: "\f371"; } +.bi-file-earmark-medical-fill::before { content: "\f372"; } +.bi-file-earmark-medical::before { content: "\f373"; } +.bi-file-earmark-minus-fill::before { content: "\f374"; } +.bi-file-earmark-minus::before { content: "\f375"; } +.bi-file-earmark-music-fill::before { content: "\f376"; } +.bi-file-earmark-music::before { content: "\f377"; } +.bi-file-earmark-person-fill::before { content: "\f378"; } +.bi-file-earmark-person::before { content: "\f379"; } +.bi-file-earmark-play-fill::before { content: "\f37a"; } +.bi-file-earmark-play::before { content: "\f37b"; } +.bi-file-earmark-plus-fill::before { content: "\f37c"; } +.bi-file-earmark-plus::before { content: "\f37d"; } +.bi-file-earmark-post-fill::before { content: "\f37e"; } +.bi-file-earmark-post::before { content: "\f37f"; } +.bi-file-earmark-ppt-fill::before { content: "\f380"; } +.bi-file-earmark-ppt::before { content: "\f381"; } +.bi-file-earmark-richtext-fill::before { content: "\f382"; } +.bi-file-earmark-richtext::before { content: "\f383"; } +.bi-file-earmark-ruled-fill::before { content: "\f384"; } +.bi-file-earmark-ruled::before { content: "\f385"; } +.bi-file-earmark-slides-fill::before { content: "\f386"; } +.bi-file-earmark-slides::before { content: "\f387"; } +.bi-file-earmark-spreadsheet-fill::before { content: "\f388"; } +.bi-file-earmark-spreadsheet::before { content: "\f389"; } +.bi-file-earmark-text-fill::before { content: "\f38a"; } +.bi-file-earmark-text::before { content: "\f38b"; } +.bi-file-earmark-word-fill::before { content: "\f38c"; } +.bi-file-earmark-word::before { content: "\f38d"; } +.bi-file-earmark-x-fill::before { content: "\f38e"; } +.bi-file-earmark-x::before { content: "\f38f"; } +.bi-file-earmark-zip-fill::before { content: "\f390"; } +.bi-file-earmark-zip::before { content: "\f391"; } +.bi-file-earmark::before { content: "\f392"; } +.bi-file-easel-fill::before { content: "\f393"; } +.bi-file-easel::before { content: "\f394"; } +.bi-file-excel-fill::before { content: "\f395"; } +.bi-file-excel::before { content: "\f396"; } +.bi-file-fill::before { content: "\f397"; } +.bi-file-font-fill::before { content: "\f398"; } +.bi-file-font::before { content: "\f399"; } +.bi-file-image-fill::before { content: "\f39a"; } +.bi-file-image::before { content: "\f39b"; } +.bi-file-lock-fill::before { content: "\f39c"; } +.bi-file-lock::before { content: "\f39d"; } +.bi-file-lock2-fill::before { content: "\f39e"; } +.bi-file-lock2::before { content: "\f39f"; } +.bi-file-medical-fill::before { content: "\f3a0"; } +.bi-file-medical::before { content: "\f3a1"; } +.bi-file-minus-fill::before { content: "\f3a2"; } +.bi-file-minus::before { content: "\f3a3"; } +.bi-file-music-fill::before { content: "\f3a4"; } +.bi-file-music::before { content: "\f3a5"; } +.bi-file-person-fill::before { content: "\f3a6"; } +.bi-file-person::before { content: "\f3a7"; } +.bi-file-play-fill::before { content: "\f3a8"; } +.bi-file-play::before { content: "\f3a9"; } +.bi-file-plus-fill::before { content: "\f3aa"; } +.bi-file-plus::before { content: "\f3ab"; } +.bi-file-post-fill::before { content: "\f3ac"; } +.bi-file-post::before { content: "\f3ad"; } +.bi-file-ppt-fill::before { content: "\f3ae"; } +.bi-file-ppt::before { content: "\f3af"; } +.bi-file-richtext-fill::before { content: "\f3b0"; } +.bi-file-richtext::before { content: "\f3b1"; } +.bi-file-ruled-fill::before { content: "\f3b2"; } +.bi-file-ruled::before { content: "\f3b3"; } +.bi-file-slides-fill::before { content: "\f3b4"; } +.bi-file-slides::before { content: "\f3b5"; } +.bi-file-spreadsheet-fill::before { content: "\f3b6"; } +.bi-file-spreadsheet::before { content: "\f3b7"; } +.bi-file-text-fill::before { content: "\f3b8"; } +.bi-file-text::before { content: "\f3b9"; } +.bi-file-word-fill::before { content: "\f3ba"; } +.bi-file-word::before { content: "\f3bb"; } +.bi-file-x-fill::before { content: "\f3bc"; } +.bi-file-x::before { content: "\f3bd"; } +.bi-file-zip-fill::before { content: "\f3be"; } +.bi-file-zip::before { content: "\f3bf"; } +.bi-file::before { content: "\f3c0"; } +.bi-files-alt::before { content: "\f3c1"; } +.bi-files::before { content: "\f3c2"; } +.bi-film::before { content: "\f3c3"; } +.bi-filter-circle-fill::before { content: "\f3c4"; } +.bi-filter-circle::before { content: "\f3c5"; } +.bi-filter-left::before { content: "\f3c6"; } +.bi-filter-right::before { content: "\f3c7"; } +.bi-filter-square-fill::before { content: "\f3c8"; } +.bi-filter-square::before { content: "\f3c9"; } +.bi-filter::before { content: "\f3ca"; } +.bi-flag-fill::before { content: "\f3cb"; } +.bi-flag::before { content: "\f3cc"; } +.bi-flower1::before { content: "\f3cd"; } +.bi-flower2::before { content: "\f3ce"; } +.bi-flower3::before { content: "\f3cf"; } +.bi-folder-check::before { content: "\f3d0"; } +.bi-folder-fill::before { content: "\f3d1"; } +.bi-folder-minus::before { content: "\f3d2"; } +.bi-folder-plus::before { content: "\f3d3"; } +.bi-folder-symlink-fill::before { content: "\f3d4"; } +.bi-folder-symlink::before { content: "\f3d5"; } +.bi-folder-x::before { content: "\f3d6"; } +.bi-folder::before { content: "\f3d7"; } +.bi-folder2-open::before { content: "\f3d8"; } +.bi-folder2::before { content: "\f3d9"; } +.bi-fonts::before { content: "\f3da"; } +.bi-forward-fill::before { content: "\f3db"; } +.bi-forward::before { content: "\f3dc"; } +.bi-front::before { content: "\f3dd"; } +.bi-fullscreen-exit::before { content: "\f3de"; } +.bi-fullscreen::before { content: "\f3df"; } +.bi-funnel-fill::before { content: "\f3e0"; } +.bi-funnel::before { content: "\f3e1"; } +.bi-gear-fill::before { content: "\f3e2"; } +.bi-gear-wide-connected::before { content: "\f3e3"; } +.bi-gear-wide::before { content: "\f3e4"; } +.bi-gear::before { content: "\f3e5"; } +.bi-gem::before { content: "\f3e6"; } +.bi-geo-alt-fill::before { content: "\f3e7"; } +.bi-geo-alt::before { content: "\f3e8"; } +.bi-geo-fill::before { content: "\f3e9"; } +.bi-geo::before { content: "\f3ea"; } +.bi-gift-fill::before { content: "\f3eb"; } +.bi-gift::before { content: "\f3ec"; } +.bi-github::before { content: "\f3ed"; } +.bi-globe::before { content: "\f3ee"; } +.bi-globe2::before { content: "\f3ef"; } +.bi-google::before { content: "\f3f0"; } +.bi-graph-down::before { content: "\f3f1"; } +.bi-graph-up::before { content: "\f3f2"; } +.bi-grid-1x2-fill::before { content: "\f3f3"; } +.bi-grid-1x2::before { content: "\f3f4"; } +.bi-grid-3x2-gap-fill::before { content: "\f3f5"; } +.bi-grid-3x2-gap::before { content: "\f3f6"; } +.bi-grid-3x2::before { content: "\f3f7"; } +.bi-grid-3x3-gap-fill::before { content: "\f3f8"; } +.bi-grid-3x3-gap::before { content: "\f3f9"; } +.bi-grid-3x3::before { content: "\f3fa"; } +.bi-grid-fill::before { content: "\f3fb"; } +.bi-grid::before { content: "\f3fc"; } +.bi-grip-horizontal::before { content: "\f3fd"; } +.bi-grip-vertical::before { content: "\f3fe"; } +.bi-hammer::before { content: "\f3ff"; } +.bi-hand-index-fill::before { content: "\f400"; } +.bi-hand-index-thumb-fill::before { content: "\f401"; } +.bi-hand-index-thumb::before { content: "\f402"; } +.bi-hand-index::before { content: "\f403"; } +.bi-hand-thumbs-down-fill::before { content: "\f404"; } +.bi-hand-thumbs-down::before { content: "\f405"; } +.bi-hand-thumbs-up-fill::before { content: "\f406"; } +.bi-hand-thumbs-up::before { content: "\f407"; } +.bi-handbag-fill::before { content: "\f408"; } +.bi-handbag::before { content: "\f409"; } +.bi-hash::before { content: "\f40a"; } +.bi-hdd-fill::before { content: "\f40b"; } +.bi-hdd-network-fill::before { content: "\f40c"; } +.bi-hdd-network::before { content: "\f40d"; } +.bi-hdd-rack-fill::before { content: "\f40e"; } +.bi-hdd-rack::before { content: "\f40f"; } +.bi-hdd-stack-fill::before { content: "\f410"; } +.bi-hdd-stack::before { content: "\f411"; } +.bi-hdd::before { content: "\f412"; } +.bi-headphones::before { content: "\f413"; } +.bi-headset::before { content: "\f414"; } +.bi-heart-fill::before { content: "\f415"; } +.bi-heart-half::before { content: "\f416"; } +.bi-heart::before { content: "\f417"; } +.bi-heptagon-fill::before { content: "\f418"; } +.bi-heptagon-half::before { content: "\f419"; } +.bi-heptagon::before { content: "\f41a"; } +.bi-hexagon-fill::before { content: "\f41b"; } +.bi-hexagon-half::before { content: "\f41c"; } +.bi-hexagon::before { content: "\f41d"; } +.bi-hourglass-bottom::before { content: "\f41e"; } +.bi-hourglass-split::before { content: "\f41f"; } +.bi-hourglass-top::before { content: "\f420"; } +.bi-hourglass::before { content: "\f421"; } +.bi-house-door-fill::before { content: "\f422"; } +.bi-house-door::before { content: "\f423"; } +.bi-house-fill::before { content: "\f424"; } +.bi-house::before { content: "\f425"; } +.bi-hr::before { content: "\f426"; } +.bi-hurricane::before { content: "\f427"; } +.bi-image-alt::before { content: "\f428"; } +.bi-image-fill::before { content: "\f429"; } +.bi-image::before { content: "\f42a"; } +.bi-images::before { content: "\f42b"; } +.bi-inbox-fill::before { content: "\f42c"; } +.bi-inbox::before { content: "\f42d"; } +.bi-inboxes-fill::before { content: "\f42e"; } +.bi-inboxes::before { content: "\f42f"; } +.bi-info-circle-fill::before { content: "\f430"; } +.bi-info-circle::before { content: "\f431"; } +.bi-info-square-fill::before { content: "\f432"; } +.bi-info-square::before { content: "\f433"; } +.bi-info::before { content: "\f434"; } +.bi-input-cursor-text::before { content: "\f435"; } +.bi-input-cursor::before { content: "\f436"; } +.bi-instagram::before { content: "\f437"; } +.bi-intersect::before { content: "\f438"; } +.bi-journal-album::before { content: "\f439"; } +.bi-journal-arrow-down::before { content: "\f43a"; } +.bi-journal-arrow-up::before { content: "\f43b"; } +.bi-journal-bookmark-fill::before { content: "\f43c"; } +.bi-journal-bookmark::before { content: "\f43d"; } +.bi-journal-check::before { content: "\f43e"; } +.bi-journal-code::before { content: "\f43f"; } +.bi-journal-medical::before { content: "\f440"; } +.bi-journal-minus::before { content: "\f441"; } +.bi-journal-plus::before { content: "\f442"; } +.bi-journal-richtext::before { content: "\f443"; } +.bi-journal-text::before { content: "\f444"; } +.bi-journal-x::before { content: "\f445"; } +.bi-journal::before { content: "\f446"; } +.bi-journals::before { content: "\f447"; } +.bi-joystick::before { content: "\f448"; } +.bi-justify-left::before { content: "\f449"; } +.bi-justify-right::before { content: "\f44a"; } +.bi-justify::before { content: "\f44b"; } +.bi-kanban-fill::before { content: "\f44c"; } +.bi-kanban::before { content: "\f44d"; } +.bi-key-fill::before { content: "\f44e"; } +.bi-key::before { content: "\f44f"; } +.bi-keyboard-fill::before { content: "\f450"; } +.bi-keyboard::before { content: "\f451"; } +.bi-ladder::before { content: "\f452"; } +.bi-lamp-fill::before { content: "\f453"; } +.bi-lamp::before { content: "\f454"; } +.bi-laptop-fill::before { content: "\f455"; } +.bi-laptop::before { content: "\f456"; } +.bi-layer-backward::before { content: "\f457"; } +.bi-layer-forward::before { content: "\f458"; } +.bi-layers-fill::before { content: "\f459"; } +.bi-layers-half::before { content: "\f45a"; } +.bi-layers::before { content: "\f45b"; } +.bi-layout-sidebar-inset-reverse::before { content: "\f45c"; } +.bi-layout-sidebar-inset::before { content: "\f45d"; } +.bi-layout-sidebar-reverse::before { content: "\f45e"; } +.bi-layout-sidebar::before { content: "\f45f"; } +.bi-layout-split::before { content: "\f460"; } +.bi-layout-text-sidebar-reverse::before { content: "\f461"; } +.bi-layout-text-sidebar::before { content: "\f462"; } +.bi-layout-text-window-reverse::before { content: "\f463"; } +.bi-layout-text-window::before { content: "\f464"; } +.bi-layout-three-columns::before { content: "\f465"; } +.bi-layout-wtf::before { content: "\f466"; } +.bi-life-preserver::before { content: "\f467"; } +.bi-lightbulb-fill::before { content: "\f468"; } +.bi-lightbulb-off-fill::before { content: "\f469"; } +.bi-lightbulb-off::before { content: "\f46a"; } +.bi-lightbulb::before { content: "\f46b"; } +.bi-lightning-charge-fill::before { content: "\f46c"; } +.bi-lightning-charge::before { content: "\f46d"; } +.bi-lightning-fill::before { content: "\f46e"; } +.bi-lightning::before { content: "\f46f"; } +.bi-link-45deg::before { content: "\f470"; } +.bi-link::before { content: "\f471"; } +.bi-linkedin::before { content: "\f472"; } +.bi-list-check::before { content: "\f473"; } +.bi-list-nested::before { content: "\f474"; } +.bi-list-ol::before { content: "\f475"; } +.bi-list-stars::before { content: "\f476"; } +.bi-list-task::before { content: "\f477"; } +.bi-list-ul::before { content: "\f478"; } +.bi-list::before { content: "\f479"; } +.bi-lock-fill::before { content: "\f47a"; } +.bi-lock::before { content: "\f47b"; } +.bi-mailbox::before { content: "\f47c"; } +.bi-mailbox2::before { content: "\f47d"; } +.bi-map-fill::before { content: "\f47e"; } +.bi-map::before { content: "\f47f"; } +.bi-markdown-fill::before { content: "\f480"; } +.bi-markdown::before { content: "\f481"; } +.bi-mask::before { content: "\f482"; } +.bi-megaphone-fill::before { content: "\f483"; } +.bi-megaphone::before { content: "\f484"; } +.bi-menu-app-fill::before { content: "\f485"; } +.bi-menu-app::before { content: "\f486"; } +.bi-menu-button-fill::before { content: "\f487"; } +.bi-menu-button-wide-fill::before { content: "\f488"; } +.bi-menu-button-wide::before { content: "\f489"; } +.bi-menu-button::before { content: "\f48a"; } +.bi-menu-down::before { content: "\f48b"; } +.bi-menu-up::before { content: "\f48c"; } +.bi-mic-fill::before { content: "\f48d"; } +.bi-mic-mute-fill::before { content: "\f48e"; } +.bi-mic-mute::before { content: "\f48f"; } +.bi-mic::before { content: "\f490"; } +.bi-minecart-loaded::before { content: "\f491"; } +.bi-minecart::before { content: "\f492"; } +.bi-moisture::before { content: "\f493"; } +.bi-moon-fill::before { content: "\f494"; } +.bi-moon-stars-fill::before { content: "\f495"; } +.bi-moon-stars::before { content: "\f496"; } +.bi-moon::before { content: "\f497"; } +.bi-mouse-fill::before { content: "\f498"; } +.bi-mouse::before { content: "\f499"; } +.bi-mouse2-fill::before { content: "\f49a"; } +.bi-mouse2::before { content: "\f49b"; } +.bi-mouse3-fill::before { content: "\f49c"; } +.bi-mouse3::before { content: "\f49d"; } +.bi-music-note-beamed::before { content: "\f49e"; } +.bi-music-note-list::before { content: "\f49f"; } +.bi-music-note::before { content: "\f4a0"; } +.bi-music-player-fill::before { content: "\f4a1"; } +.bi-music-player::before { content: "\f4a2"; } +.bi-newspaper::before { content: "\f4a3"; } +.bi-node-minus-fill::before { content: "\f4a4"; } +.bi-node-minus::before { content: "\f4a5"; } +.bi-node-plus-fill::before { content: "\f4a6"; } +.bi-node-plus::before { content: "\f4a7"; } +.bi-nut-fill::before { content: "\f4a8"; } +.bi-nut::before { content: "\f4a9"; } +.bi-octagon-fill::before { content: "\f4aa"; } +.bi-octagon-half::before { content: "\f4ab"; } +.bi-octagon::before { content: "\f4ac"; } +.bi-option::before { content: "\f4ad"; } +.bi-outlet::before { content: "\f4ae"; } +.bi-paint-bucket::before { content: "\f4af"; } +.bi-palette-fill::before { content: "\f4b0"; } +.bi-palette::before { content: "\f4b1"; } +.bi-palette2::before { content: "\f4b2"; } +.bi-paperclip::before { content: "\f4b3"; } +.bi-paragraph::before { content: "\f4b4"; } +.bi-patch-check-fill::before { content: "\f4b5"; } +.bi-patch-check::before { content: "\f4b6"; } +.bi-patch-exclamation-fill::before { content: "\f4b7"; } +.bi-patch-exclamation::before { content: "\f4b8"; } +.bi-patch-minus-fill::before { content: "\f4b9"; } +.bi-patch-minus::before { content: "\f4ba"; } +.bi-patch-plus-fill::before { content: "\f4bb"; } +.bi-patch-plus::before { content: "\f4bc"; } +.bi-patch-question-fill::before { content: "\f4bd"; } +.bi-patch-question::before { content: "\f4be"; } +.bi-pause-btn-fill::before { content: "\f4bf"; } +.bi-pause-btn::before { content: "\f4c0"; } +.bi-pause-circle-fill::before { content: "\f4c1"; } +.bi-pause-circle::before { content: "\f4c2"; } +.bi-pause-fill::before { content: "\f4c3"; } +.bi-pause::before { content: "\f4c4"; } +.bi-peace-fill::before { content: "\f4c5"; } +.bi-peace::before { content: "\f4c6"; } +.bi-pen-fill::before { content: "\f4c7"; } +.bi-pen::before { content: "\f4c8"; } +.bi-pencil-fill::before { content: "\f4c9"; } +.bi-pencil-square::before { content: "\f4ca"; } +.bi-pencil::before { content: "\f4cb"; } +.bi-pentagon-fill::before { content: "\f4cc"; } +.bi-pentagon-half::before { content: "\f4cd"; } +.bi-pentagon::before { content: "\f4ce"; } +.bi-people-fill::before { content: "\f4cf"; } +.bi-people::before { content: "\f4d0"; } +.bi-percent::before { content: "\f4d1"; } +.bi-person-badge-fill::before { content: "\f4d2"; } +.bi-person-badge::before { content: "\f4d3"; } +.bi-person-bounding-box::before { content: "\f4d4"; } +.bi-person-check-fill::before { content: "\f4d5"; } +.bi-person-check::before { content: "\f4d6"; } +.bi-person-circle::before { content: "\f4d7"; } +.bi-person-dash-fill::before { content: "\f4d8"; } +.bi-person-dash::before { content: "\f4d9"; } +.bi-person-fill::before { content: "\f4da"; } +.bi-person-lines-fill::before { content: "\f4db"; } +.bi-person-plus-fill::before { content: "\f4dc"; } +.bi-person-plus::before { content: "\f4dd"; } +.bi-person-square::before { content: "\f4de"; } +.bi-person-x-fill::before { content: "\f4df"; } +.bi-person-x::before { content: "\f4e0"; } +.bi-person::before { content: "\f4e1"; } +.bi-phone-fill::before { content: "\f4e2"; } +.bi-phone-landscape-fill::before { content: "\f4e3"; } +.bi-phone-landscape::before { content: "\f4e4"; } +.bi-phone-vibrate-fill::before { content: "\f4e5"; } +.bi-phone-vibrate::before { content: "\f4e6"; } +.bi-phone::before { content: "\f4e7"; } +.bi-pie-chart-fill::before { content: "\f4e8"; } +.bi-pie-chart::before { content: "\f4e9"; } +.bi-pin-angle-fill::before { content: "\f4ea"; } +.bi-pin-angle::before { content: "\f4eb"; } +.bi-pin-fill::before { content: "\f4ec"; } +.bi-pin::before { content: "\f4ed"; } +.bi-pip-fill::before { content: "\f4ee"; } +.bi-pip::before { content: "\f4ef"; } +.bi-play-btn-fill::before { content: "\f4f0"; } +.bi-play-btn::before { content: "\f4f1"; } +.bi-play-circle-fill::before { content: "\f4f2"; } +.bi-play-circle::before { content: "\f4f3"; } +.bi-play-fill::before { content: "\f4f4"; } +.bi-play::before { content: "\f4f5"; } +.bi-plug-fill::before { content: "\f4f6"; } +.bi-plug::before { content: "\f4f7"; } +.bi-plus-circle-dotted::before { content: "\f4f8"; } +.bi-plus-circle-fill::before { content: "\f4f9"; } +.bi-plus-circle::before { content: "\f4fa"; } +.bi-plus-square-dotted::before { content: "\f4fb"; } +.bi-plus-square-fill::before { content: "\f4fc"; } +.bi-plus-square::before { content: "\f4fd"; } +.bi-plus::before { content: "\f4fe"; } +.bi-power::before { content: "\f4ff"; } +.bi-printer-fill::before { content: "\f500"; } +.bi-printer::before { content: "\f501"; } +.bi-puzzle-fill::before { content: "\f502"; } +.bi-puzzle::before { content: "\f503"; } +.bi-question-circle-fill::before { content: "\f504"; } +.bi-question-circle::before { content: "\f505"; } +.bi-question-diamond-fill::before { content: "\f506"; } +.bi-question-diamond::before { content: "\f507"; } +.bi-question-octagon-fill::before { content: "\f508"; } +.bi-question-octagon::before { content: "\f509"; } +.bi-question-square-fill::before { content: "\f50a"; } +.bi-question-square::before { content: "\f50b"; } +.bi-question::before { content: "\f50c"; } +.bi-rainbow::before { content: "\f50d"; } +.bi-receipt-cutoff::before { content: "\f50e"; } +.bi-receipt::before { content: "\f50f"; } +.bi-reception-0::before { content: "\f510"; } +.bi-reception-1::before { content: "\f511"; } +.bi-reception-2::before { content: "\f512"; } +.bi-reception-3::before { content: "\f513"; } +.bi-reception-4::before { content: "\f514"; } +.bi-record-btn-fill::before { content: "\f515"; } +.bi-record-btn::before { content: "\f516"; } +.bi-record-circle-fill::before { content: "\f517"; } +.bi-record-circle::before { content: "\f518"; } +.bi-record-fill::before { content: "\f519"; } +.bi-record::before { content: "\f51a"; } +.bi-record2-fill::before { content: "\f51b"; } +.bi-record2::before { content: "\f51c"; } +.bi-reply-all-fill::before { content: "\f51d"; } +.bi-reply-all::before { content: "\f51e"; } +.bi-reply-fill::before { content: "\f51f"; } +.bi-reply::before { content: "\f520"; } +.bi-rss-fill::before { content: "\f521"; } +.bi-rss::before { content: "\f522"; } +.bi-rulers::before { content: "\f523"; } +.bi-save-fill::before { content: "\f524"; } +.bi-save::before { content: "\f525"; } +.bi-save2-fill::before { content: "\f526"; } +.bi-save2::before { content: "\f527"; } +.bi-scissors::before { content: "\f528"; } +.bi-screwdriver::before { content: "\f529"; } +.bi-search::before { content: "\f52a"; } +.bi-segmented-nav::before { content: "\f52b"; } +.bi-server::before { content: "\f52c"; } +.bi-share-fill::before { content: "\f52d"; } +.bi-share::before { content: "\f52e"; } +.bi-shield-check::before { content: "\f52f"; } +.bi-shield-exclamation::before { content: "\f530"; } +.bi-shield-fill-check::before { content: "\f531"; } +.bi-shield-fill-exclamation::before { content: "\f532"; } +.bi-shield-fill-minus::before { content: "\f533"; } +.bi-shield-fill-plus::before { content: "\f534"; } +.bi-shield-fill-x::before { content: "\f535"; } +.bi-shield-fill::before { content: "\f536"; } +.bi-shield-lock-fill::before { content: "\f537"; } +.bi-shield-lock::before { content: "\f538"; } +.bi-shield-minus::before { content: "\f539"; } +.bi-shield-plus::before { content: "\f53a"; } +.bi-shield-shaded::before { content: "\f53b"; } +.bi-shield-slash-fill::before { content: "\f53c"; } +.bi-shield-slash::before { content: "\f53d"; } +.bi-shield-x::before { content: "\f53e"; } +.bi-shield::before { content: "\f53f"; } +.bi-shift-fill::before { content: "\f540"; } +.bi-shift::before { content: "\f541"; } +.bi-shop-window::before { content: "\f542"; } +.bi-shop::before { content: "\f543"; } +.bi-shuffle::before { content: "\f544"; } +.bi-signpost-2-fill::before { content: "\f545"; } +.bi-signpost-2::before { content: "\f546"; } +.bi-signpost-fill::before { content: "\f547"; } +.bi-signpost-split-fill::before { content: "\f548"; } +.bi-signpost-split::before { content: "\f549"; } +.bi-signpost::before { content: "\f54a"; } +.bi-sim-fill::before { content: "\f54b"; } +.bi-sim::before { content: "\f54c"; } +.bi-skip-backward-btn-fill::before { content: "\f54d"; } +.bi-skip-backward-btn::before { content: "\f54e"; } +.bi-skip-backward-circle-fill::before { content: "\f54f"; } +.bi-skip-backward-circle::before { content: "\f550"; } +.bi-skip-backward-fill::before { content: "\f551"; } +.bi-skip-backward::before { content: "\f552"; } +.bi-skip-end-btn-fill::before { content: "\f553"; } +.bi-skip-end-btn::before { content: "\f554"; } +.bi-skip-end-circle-fill::before { content: "\f555"; } +.bi-skip-end-circle::before { content: "\f556"; } +.bi-skip-end-fill::before { content: "\f557"; } +.bi-skip-end::before { content: "\f558"; } +.bi-skip-forward-btn-fill::before { content: "\f559"; } +.bi-skip-forward-btn::before { content: "\f55a"; } +.bi-skip-forward-circle-fill::before { content: "\f55b"; } +.bi-skip-forward-circle::before { content: "\f55c"; } +.bi-skip-forward-fill::before { content: "\f55d"; } +.bi-skip-forward::before { content: "\f55e"; } +.bi-skip-start-btn-fill::before { content: "\f55f"; } +.bi-skip-start-btn::before { content: "\f560"; } +.bi-skip-start-circle-fill::before { content: "\f561"; } +.bi-skip-start-circle::before { content: "\f562"; } +.bi-skip-start-fill::before { content: "\f563"; } +.bi-skip-start::before { content: "\f564"; } +.bi-slack::before { content: "\f565"; } +.bi-slash-circle-fill::before { content: "\f566"; } +.bi-slash-circle::before { content: "\f567"; } +.bi-slash-square-fill::before { content: "\f568"; } +.bi-slash-square::before { content: "\f569"; } +.bi-slash::before { content: "\f56a"; } +.bi-sliders::before { content: "\f56b"; } +.bi-smartwatch::before { content: "\f56c"; } +.bi-snow::before { content: "\f56d"; } +.bi-snow2::before { content: "\f56e"; } +.bi-snow3::before { content: "\f56f"; } +.bi-sort-alpha-down-alt::before { content: "\f570"; } +.bi-sort-alpha-down::before { content: "\f571"; } +.bi-sort-alpha-up-alt::before { content: "\f572"; } +.bi-sort-alpha-up::before { content: "\f573"; } +.bi-sort-down-alt::before { content: "\f574"; } +.bi-sort-down::before { content: "\f575"; } +.bi-sort-numeric-down-alt::before { content: "\f576"; } +.bi-sort-numeric-down::before { content: "\f577"; } +.bi-sort-numeric-up-alt::before { content: "\f578"; } +.bi-sort-numeric-up::before { content: "\f579"; } +.bi-sort-up-alt::before { content: "\f57a"; } +.bi-sort-up::before { content: "\f57b"; } +.bi-soundwave::before { content: "\f57c"; } +.bi-speaker-fill::before { content: "\f57d"; } +.bi-speaker::before { content: "\f57e"; } +.bi-speedometer::before { content: "\f57f"; } +.bi-speedometer2::before { content: "\f580"; } +.bi-spellcheck::before { content: "\f581"; } +.bi-square-fill::before { content: "\f582"; } +.bi-square-half::before { content: "\f583"; } +.bi-square::before { content: "\f584"; } +.bi-stack::before { content: "\f585"; } +.bi-star-fill::before { content: "\f586"; } +.bi-star-half::before { content: "\f587"; } +.bi-star::before { content: "\f588"; } +.bi-stars::before { content: "\f589"; } +.bi-stickies-fill::before { content: "\f58a"; } +.bi-stickies::before { content: "\f58b"; } +.bi-sticky-fill::before { content: "\f58c"; } +.bi-sticky::before { content: "\f58d"; } +.bi-stop-btn-fill::before { content: "\f58e"; } +.bi-stop-btn::before { content: "\f58f"; } +.bi-stop-circle-fill::before { content: "\f590"; } +.bi-stop-circle::before { content: "\f591"; } +.bi-stop-fill::before { content: "\f592"; } +.bi-stop::before { content: "\f593"; } +.bi-stoplights-fill::before { content: "\f594"; } +.bi-stoplights::before { content: "\f595"; } +.bi-stopwatch-fill::before { content: "\f596"; } +.bi-stopwatch::before { content: "\f597"; } +.bi-subtract::before { content: "\f598"; } +.bi-suit-club-fill::before { content: "\f599"; } +.bi-suit-club::before { content: "\f59a"; } +.bi-suit-diamond-fill::before { content: "\f59b"; } +.bi-suit-diamond::before { content: "\f59c"; } +.bi-suit-heart-fill::before { content: "\f59d"; } +.bi-suit-heart::before { content: "\f59e"; } +.bi-suit-spade-fill::before { content: "\f59f"; } +.bi-suit-spade::before { content: "\f5a0"; } +.bi-sun-fill::before { content: "\f5a1"; } +.bi-sun::before { content: "\f5a2"; } +.bi-sunglasses::before { content: "\f5a3"; } +.bi-sunrise-fill::before { content: "\f5a4"; } +.bi-sunrise::before { content: "\f5a5"; } +.bi-sunset-fill::before { content: "\f5a6"; } +.bi-sunset::before { content: "\f5a7"; } +.bi-symmetry-horizontal::before { content: "\f5a8"; } +.bi-symmetry-vertical::before { content: "\f5a9"; } +.bi-table::before { content: "\f5aa"; } +.bi-tablet-fill::before { content: "\f5ab"; } +.bi-tablet-landscape-fill::before { content: "\f5ac"; } +.bi-tablet-landscape::before { content: "\f5ad"; } +.bi-tablet::before { content: "\f5ae"; } +.bi-tag-fill::before { content: "\f5af"; } +.bi-tag::before { content: "\f5b0"; } +.bi-tags-fill::before { content: "\f5b1"; } +.bi-tags::before { content: "\f5b2"; } +.bi-telegram::before { content: "\f5b3"; } +.bi-telephone-fill::before { content: "\f5b4"; } +.bi-telephone-forward-fill::before { content: "\f5b5"; } +.bi-telephone-forward::before { content: "\f5b6"; } +.bi-telephone-inbound-fill::before { content: "\f5b7"; } +.bi-telephone-inbound::before { content: "\f5b8"; } +.bi-telephone-minus-fill::before { content: "\f5b9"; } +.bi-telephone-minus::before { content: "\f5ba"; } +.bi-telephone-outbound-fill::before { content: "\f5bb"; } +.bi-telephone-outbound::before { content: "\f5bc"; } +.bi-telephone-plus-fill::before { content: "\f5bd"; } +.bi-telephone-plus::before { content: "\f5be"; } +.bi-telephone-x-fill::before { content: "\f5bf"; } +.bi-telephone-x::before { content: "\f5c0"; } +.bi-telephone::before { content: "\f5c1"; } +.bi-terminal-fill::before { content: "\f5c2"; } +.bi-terminal::before { content: "\f5c3"; } +.bi-text-center::before { content: "\f5c4"; } +.bi-text-indent-left::before { content: "\f5c5"; } +.bi-text-indent-right::before { content: "\f5c6"; } +.bi-text-left::before { content: "\f5c7"; } +.bi-text-paragraph::before { content: "\f5c8"; } +.bi-text-right::before { content: "\f5c9"; } +.bi-textarea-resize::before { content: "\f5ca"; } +.bi-textarea-t::before { content: "\f5cb"; } +.bi-textarea::before { content: "\f5cc"; } +.bi-thermometer-half::before { content: "\f5cd"; } +.bi-thermometer-high::before { content: "\f5ce"; } +.bi-thermometer-low::before { content: "\f5cf"; } +.bi-thermometer-snow::before { content: "\f5d0"; } +.bi-thermometer-sun::before { content: "\f5d1"; } +.bi-thermometer::before { content: "\f5d2"; } +.bi-three-dots-vertical::before { content: "\f5d3"; } +.bi-three-dots::before { content: "\f5d4"; } +.bi-toggle-off::before { content: "\f5d5"; } +.bi-toggle-on::before { content: "\f5d6"; } +.bi-toggle2-off::before { content: "\f5d7"; } +.bi-toggle2-on::before { content: "\f5d8"; } +.bi-toggles::before { content: "\f5d9"; } +.bi-toggles2::before { content: "\f5da"; } +.bi-tools::before { content: "\f5db"; } +.bi-tornado::before { content: "\f5dc"; } +.bi-trash-fill::before { content: "\f5dd"; } +.bi-trash::before { content: "\f5de"; } +.bi-trash2-fill::before { content: "\f5df"; } +.bi-trash2::before { content: "\f5e0"; } +.bi-tree-fill::before { content: "\f5e1"; } +.bi-tree::before { content: "\f5e2"; } +.bi-triangle-fill::before { content: "\f5e3"; } +.bi-triangle-half::before { content: "\f5e4"; } +.bi-triangle::before { content: "\f5e5"; } +.bi-trophy-fill::before { content: "\f5e6"; } +.bi-trophy::before { content: "\f5e7"; } +.bi-tropical-storm::before { content: "\f5e8"; } +.bi-truck-flatbed::before { content: "\f5e9"; } +.bi-truck::before { content: "\f5ea"; } +.bi-tsunami::before { content: "\f5eb"; } +.bi-tv-fill::before { content: "\f5ec"; } +.bi-tv::before { content: "\f5ed"; } +.bi-twitch::before { content: "\f5ee"; } +.bi-twitter::before { content: "\f5ef"; } +.bi-type-bold::before { content: "\f5f0"; } +.bi-type-h1::before { content: "\f5f1"; } +.bi-type-h2::before { content: "\f5f2"; } +.bi-type-h3::before { content: "\f5f3"; } +.bi-type-italic::before { content: "\f5f4"; } +.bi-type-strikethrough::before { content: "\f5f5"; } +.bi-type-underline::before { content: "\f5f6"; } +.bi-type::before { content: "\f5f7"; } +.bi-ui-checks-grid::before { content: "\f5f8"; } +.bi-ui-checks::before { content: "\f5f9"; } +.bi-ui-radios-grid::before { content: "\f5fa"; } +.bi-ui-radios::before { content: "\f5fb"; } +.bi-umbrella-fill::before { content: "\f5fc"; } +.bi-umbrella::before { content: "\f5fd"; } +.bi-union::before { content: "\f5fe"; } +.bi-unlock-fill::before { content: "\f5ff"; } +.bi-unlock::before { content: "\f600"; } +.bi-upc-scan::before { content: "\f601"; } +.bi-upc::before { content: "\f602"; } +.bi-upload::before { content: "\f603"; } +.bi-vector-pen::before { content: "\f604"; } +.bi-view-list::before { content: "\f605"; } +.bi-view-stacked::before { content: "\f606"; } +.bi-vinyl-fill::before { content: "\f607"; } +.bi-vinyl::before { content: "\f608"; } +.bi-voicemail::before { content: "\f609"; } +.bi-volume-down-fill::before { content: "\f60a"; } +.bi-volume-down::before { content: "\f60b"; } +.bi-volume-mute-fill::before { content: "\f60c"; } +.bi-volume-mute::before { content: "\f60d"; } +.bi-volume-off-fill::before { content: "\f60e"; } +.bi-volume-off::before { content: "\f60f"; } +.bi-volume-up-fill::before { content: "\f610"; } +.bi-volume-up::before { content: "\f611"; } +.bi-vr::before { content: "\f612"; } +.bi-wallet-fill::before { content: "\f613"; } +.bi-wallet::before { content: "\f614"; } +.bi-wallet2::before { content: "\f615"; } +.bi-watch::before { content: "\f616"; } +.bi-water::before { content: "\f617"; } +.bi-whatsapp::before { content: "\f618"; } +.bi-wifi-1::before { content: "\f619"; } +.bi-wifi-2::before { content: "\f61a"; } +.bi-wifi-off::before { content: "\f61b"; } +.bi-wifi::before { content: "\f61c"; } +.bi-wind::before { content: "\f61d"; } +.bi-window-dock::before { content: "\f61e"; } +.bi-window-sidebar::before { content: "\f61f"; } +.bi-window::before { content: "\f620"; } +.bi-wrench::before { content: "\f621"; } +.bi-x-circle-fill::before { content: "\f622"; } +.bi-x-circle::before { content: "\f623"; } +.bi-x-diamond-fill::before { content: "\f624"; } +.bi-x-diamond::before { content: "\f625"; } +.bi-x-octagon-fill::before { content: "\f626"; } +.bi-x-octagon::before { content: "\f627"; } +.bi-x-square-fill::before { content: "\f628"; } +.bi-x-square::before { content: "\f629"; } +.bi-x::before { content: "\f62a"; } +.bi-youtube::before { content: "\f62b"; } +.bi-zoom-in::before { content: "\f62c"; } +.bi-zoom-out::before { content: "\f62d"; } +.bi-bank::before { content: "\f62e"; } +.bi-bank2::before { content: "\f62f"; } +.bi-bell-slash-fill::before { content: "\f630"; } +.bi-bell-slash::before { content: "\f631"; } +.bi-cash-coin::before { content: "\f632"; } +.bi-check-lg::before { content: "\f633"; } +.bi-coin::before { content: "\f634"; } +.bi-currency-bitcoin::before { content: "\f635"; } +.bi-currency-dollar::before { content: "\f636"; } +.bi-currency-euro::before { content: "\f637"; } +.bi-currency-exchange::before { content: "\f638"; } +.bi-currency-pound::before { content: "\f639"; } +.bi-currency-yen::before { content: "\f63a"; } +.bi-dash-lg::before { content: "\f63b"; } +.bi-exclamation-lg::before { content: "\f63c"; } +.bi-file-earmark-pdf-fill::before { content: "\f63d"; } +.bi-file-earmark-pdf::before { content: "\f63e"; } +.bi-file-pdf-fill::before { content: "\f63f"; } +.bi-file-pdf::before { content: "\f640"; } +.bi-gender-ambiguous::before { content: "\f641"; } +.bi-gender-female::before { content: "\f642"; } +.bi-gender-male::before { content: "\f643"; } +.bi-gender-trans::before { content: "\f644"; } +.bi-headset-vr::before { content: "\f645"; } +.bi-info-lg::before { content: "\f646"; } +.bi-mastodon::before { content: "\f647"; } +.bi-messenger::before { content: "\f648"; } +.bi-piggy-bank-fill::before { content: "\f649"; } +.bi-piggy-bank::before { content: "\f64a"; } +.bi-pin-map-fill::before { content: "\f64b"; } +.bi-pin-map::before { content: "\f64c"; } +.bi-plus-lg::before { content: "\f64d"; } +.bi-question-lg::before { content: "\f64e"; } +.bi-recycle::before { content: "\f64f"; } +.bi-reddit::before { content: "\f650"; } +.bi-safe-fill::before { content: "\f651"; } +.bi-safe2-fill::before { content: "\f652"; } +.bi-safe2::before { content: "\f653"; } +.bi-sd-card-fill::before { content: "\f654"; } +.bi-sd-card::before { content: "\f655"; } +.bi-skype::before { content: "\f656"; } +.bi-slash-lg::before { content: "\f657"; } +.bi-translate::before { content: "\f658"; } +.bi-x-lg::before { content: "\f659"; } +.bi-safe::before { content: "\f65a"; } +.bi-apple::before { content: "\f65b"; } +.bi-microsoft::before { content: "\f65d"; } +.bi-windows::before { content: "\f65e"; } +.bi-behance::before { content: "\f65c"; } +.bi-dribbble::before { content: "\f65f"; } +.bi-line::before { content: "\f660"; } +.bi-medium::before { content: "\f661"; } +.bi-paypal::before { content: "\f662"; } +.bi-pinterest::before { content: "\f663"; } +.bi-signal::before { content: "\f664"; } +.bi-snapchat::before { content: "\f665"; } +.bi-spotify::before { content: "\f666"; } +.bi-stack-overflow::before { content: "\f667"; } +.bi-strava::before { content: "\f668"; } +.bi-wordpress::before { content: "\f669"; } +.bi-vimeo::before { content: "\f66a"; } +.bi-activity::before { content: "\f66b"; } +.bi-easel2-fill::before { content: "\f66c"; } +.bi-easel2::before { content: "\f66d"; } +.bi-easel3-fill::before { content: "\f66e"; } +.bi-easel3::before { content: "\f66f"; } +.bi-fan::before { content: "\f670"; } +.bi-fingerprint::before { content: "\f671"; } +.bi-graph-down-arrow::before { content: "\f672"; } +.bi-graph-up-arrow::before { content: "\f673"; } +.bi-hypnotize::before { content: "\f674"; } +.bi-magic::before { content: "\f675"; } +.bi-person-rolodex::before { content: "\f676"; } +.bi-person-video::before { content: "\f677"; } +.bi-person-video2::before { content: "\f678"; } +.bi-person-video3::before { content: "\f679"; } +.bi-person-workspace::before { content: "\f67a"; } +.bi-radioactive::before { content: "\f67b"; } +.bi-webcam-fill::before { content: "\f67c"; } +.bi-webcam::before { content: "\f67d"; } +.bi-yin-yang::before { content: "\f67e"; } +.bi-bandaid-fill::before { content: "\f680"; } +.bi-bandaid::before { content: "\f681"; } +.bi-bluetooth::before { content: "\f682"; } +.bi-body-text::before { content: "\f683"; } +.bi-boombox::before { content: "\f684"; } +.bi-boxes::before { content: "\f685"; } +.bi-dpad-fill::before { content: "\f686"; } +.bi-dpad::before { content: "\f687"; } +.bi-ear-fill::before { content: "\f688"; } +.bi-ear::before { content: "\f689"; } +.bi-envelope-check-1::before { content: "\f68a"; } +.bi-envelope-check-fill::before { content: "\f68b"; } +.bi-envelope-check::before { content: "\f68c"; } +.bi-envelope-dash-1::before { content: "\f68d"; } +.bi-envelope-dash-fill::before { content: "\f68e"; } +.bi-envelope-dash::before { content: "\f68f"; } +.bi-envelope-exclamation-1::before { content: "\f690"; } +.bi-envelope-exclamation-fill::before { content: "\f691"; } +.bi-envelope-exclamation::before { content: "\f692"; } +.bi-envelope-plus-fill::before { content: "\f693"; } +.bi-envelope-plus::before { content: "\f694"; } +.bi-envelope-slash-1::before { content: "\f695"; } +.bi-envelope-slash-fill::before { content: "\f696"; } +.bi-envelope-slash::before { content: "\f697"; } +.bi-envelope-x-1::before { content: "\f698"; } +.bi-envelope-x-fill::before { content: "\f699"; } +.bi-envelope-x::before { content: "\f69a"; } +.bi-explicit-fill::before { content: "\f69b"; } +.bi-explicit::before { content: "\f69c"; } +.bi-git::before { content: "\f69d"; } +.bi-infinity::before { content: "\f69e"; } +.bi-list-columns-reverse::before { content: "\f69f"; } +.bi-list-columns::before { content: "\f6a0"; } +.bi-meta::before { content: "\f6a1"; } +.bi-mortorboard-fill::before { content: "\f6a2"; } +.bi-mortorboard::before { content: "\f6a3"; } +.bi-nintendo-switch::before { content: "\f6a4"; } +.bi-pc-display-horizontal::before { content: "\f6a5"; } +.bi-pc-display::before { content: "\f6a6"; } +.bi-pc-horizontal::before { content: "\f6a7"; } +.bi-pc::before { content: "\f6a8"; } +.bi-playstation::before { content: "\f6a9"; } +.bi-plus-slash-minus::before { content: "\f6aa"; } +.bi-projector-fill::before { content: "\f6ab"; } +.bi-projector::before { content: "\f6ac"; } +.bi-qr-code-scan::before { content: "\f6ad"; } +.bi-qr-code::before { content: "\f6ae"; } +.bi-quora::before { content: "\f6af"; } +.bi-quote::before { content: "\f6b0"; } +.bi-robot::before { content: "\f6b1"; } +.bi-send-check-fill::before { content: "\f6b2"; } +.bi-send-check::before { content: "\f6b3"; } +.bi-send-dash-fill::before { content: "\f6b4"; } +.bi-send-dash::before { content: "\f6b5"; } +.bi-send-exclamation-1::before { content: "\f6b6"; } +.bi-send-exclamation-fill::before { content: "\f6b7"; } +.bi-send-exclamation::before { content: "\f6b8"; } +.bi-send-fill::before { content: "\f6b9"; } +.bi-send-plus-fill::before { content: "\f6ba"; } +.bi-send-plus::before { content: "\f6bb"; } +.bi-send-slash-fill::before { content: "\f6bc"; } +.bi-send-slash::before { content: "\f6bd"; } +.bi-send-x-fill::before { content: "\f6be"; } +.bi-send-x::before { content: "\f6bf"; } +.bi-send::before { content: "\f6c0"; } +.bi-steam::before { content: "\f6c1"; } +.bi-terminal-dash-1::before { content: "\f6c2"; } +.bi-terminal-dash::before { content: "\f6c3"; } +.bi-terminal-plus::before { content: "\f6c4"; } +.bi-terminal-split::before { content: "\f6c5"; } +.bi-ticket-detailed-fill::before { content: "\f6c6"; } +.bi-ticket-detailed::before { content: "\f6c7"; } +.bi-ticket-fill::before { content: "\f6c8"; } +.bi-ticket-perforated-fill::before { content: "\f6c9"; } +.bi-ticket-perforated::before { content: "\f6ca"; } +.bi-ticket::before { content: "\f6cb"; } +.bi-tiktok::before { content: "\f6cc"; } +.bi-window-dash::before { content: "\f6cd"; } +.bi-window-desktop::before { content: "\f6ce"; } +.bi-window-fullscreen::before { content: "\f6cf"; } +.bi-window-plus::before { content: "\f6d0"; } +.bi-window-split::before { content: "\f6d1"; } +.bi-window-stack::before { content: "\f6d2"; } +.bi-window-x::before { content: "\f6d3"; } +.bi-xbox::before { content: "\f6d4"; } +.bi-ethernet::before { content: "\f6d5"; } +.bi-hdmi-fill::before { content: "\f6d6"; } +.bi-hdmi::before { content: "\f6d7"; } +.bi-usb-c-fill::before { content: "\f6d8"; } +.bi-usb-c::before { content: "\f6d9"; } +.bi-usb-fill::before { content: "\f6da"; } +.bi-usb-plug-fill::before { content: "\f6db"; } +.bi-usb-plug::before { content: "\f6dc"; } +.bi-usb-symbol::before { content: "\f6dd"; } +.bi-usb::before { content: "\f6de"; } +.bi-boombox-fill::before { content: "\f6df"; } +.bi-displayport-1::before { content: "\f6e0"; } +.bi-displayport::before { content: "\f6e1"; } +.bi-gpu-card::before { content: "\f6e2"; } +.bi-memory::before { content: "\f6e3"; } +.bi-modem-fill::before { content: "\f6e4"; } +.bi-modem::before { content: "\f6e5"; } +.bi-motherboard-fill::before { content: "\f6e6"; } +.bi-motherboard::before { content: "\f6e7"; } +.bi-optical-audio-fill::before { content: "\f6e8"; } +.bi-optical-audio::before { content: "\f6e9"; } +.bi-pci-card::before { content: "\f6ea"; } +.bi-router-fill::before { content: "\f6eb"; } +.bi-router::before { content: "\f6ec"; } +.bi-ssd-fill::before { content: "\f6ed"; } +.bi-ssd::before { content: "\f6ee"; } +.bi-thunderbolt-fill::before { content: "\f6ef"; } +.bi-thunderbolt::before { content: "\f6f0"; } +.bi-usb-drive-fill::before { content: "\f6f1"; } +.bi-usb-drive::before { content: "\f6f2"; } +.bi-usb-micro-fill::before { content: "\f6f3"; } +.bi-usb-micro::before { content: "\f6f4"; } +.bi-usb-mini-fill::before { content: "\f6f5"; } +.bi-usb-mini::before { content: "\f6f6"; } +.bi-cloud-haze2::before { content: "\f6f7"; } +.bi-device-hdd-fill::before { content: "\f6f8"; } +.bi-device-hdd::before { content: "\f6f9"; } +.bi-device-ssd-fill::before { content: "\f6fa"; } +.bi-device-ssd::before { content: "\f6fb"; } +.bi-displayport-fill::before { content: "\f6fc"; } +.bi-mortarboard-fill::before { content: "\f6fd"; } +.bi-mortarboard::before { content: "\f6fe"; } +.bi-terminal-x::before { content: "\f6ff"; } diff --git a/Streamdeck/com.mirabox.streamingle.sdPlugin/propertyInspector/utils/bootstrap.min.css b/Streamdeck/com.mirabox.streamingle.sdPlugin/propertyInspector/utils/bootstrap.min.css new file mode 100644 index 00000000..1472dec0 --- /dev/null +++ b/Streamdeck/com.mirabox.streamingle.sdPlugin/propertyInspector/utils/bootstrap.min.css @@ -0,0 +1,7 @@ +@charset "UTF-8";/*! + * Bootstrap v5.1.3 (https://getbootstrap.com/) + * Copyright 2011-2021 The Bootstrap Authors + * Copyright 2011-2021 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + */:root{--bs-blue:#0d6efd;--bs-indigo:#6610f2;--bs-purple:#6f42c1;--bs-pink:#d63384;--bs-red:#dc3545;--bs-orange:#fd7e14;--bs-yellow:#ffc107;--bs-green:#198754;--bs-teal:#20c997;--bs-cyan:#0dcaf0;--bs-white:#fff;--bs-gray:#6c757d;--bs-gray-dark:#343a40;--bs-gray-100:#f8f9fa;--bs-gray-200:#e9ecef;--bs-gray-300:#dee2e6;--bs-gray-400:#ced4da;--bs-gray-500:#adb5bd;--bs-gray-600:#6c757d;--bs-gray-700:#495057;--bs-gray-800:#343a40;--bs-gray-900:#212529;--bs-primary:#0d6efd;--bs-secondary:#6c757d;--bs-success:#198754;--bs-info:#0dcaf0;--bs-warning:#ffc107;--bs-danger:#dc3545;--bs-light:#f8f9fa;--bs-dark:#212529;--bs-primary-rgb:13,110,253;--bs-secondary-rgb:108,117,125;--bs-success-rgb:25,135,84;--bs-info-rgb:13,202,240;--bs-warning-rgb:255,193,7;--bs-danger-rgb:220,53,69;--bs-light-rgb:248,249,250;--bs-dark-rgb:33,37,41;--bs-white-rgb:255,255,255;--bs-black-rgb:0,0,0;--bs-body-color-rgb:33,37,41;--bs-body-bg-rgb:255,255,255;--bs-font-sans-serif:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans","Liberation Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--bs-font-monospace:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--bs-gradient:linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));--bs-body-font-family:var(--bs-font-sans-serif);--bs-body-font-size:1rem;--bs-body-font-weight:400;--bs-body-line-height:1.5;--bs-body-color:#212529;--bs-body-bg:#fff}*,::after,::before{box-sizing:border-box}@media (prefers-reduced-motion:no-preference){:root{scroll-behavior:smooth}}body{margin:0;font-family:var(--bs-body-font-family);font-size:var(--bs-body-font-size);font-weight:var(--bs-body-font-weight);line-height:var(--bs-body-line-height);color:var(--bs-body-color);text-align:var(--bs-body-text-align);background-color:var(--bs-body-bg);-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}hr{margin:1rem 0;color:inherit;background-color:currentColor;border:0;opacity:.25}hr:not([size]){height:1px}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem;font-weight:500;line-height:1.2}.h1,h1{font-size:calc(1.375rem + 1.5vw)}@media (min-width:1200px){.h1,h1{font-size:2.5rem}}.h2,h2{font-size:calc(1.325rem + .9vw)}@media (min-width:1200px){.h2,h2{font-size:2rem}}.h3,h3{font-size:calc(1.3rem + .6vw)}@media (min-width:1200px){.h3,h3{font-size:1.75rem}}.h4,h4{font-size:calc(1.275rem + .3vw)}@media (min-width:1200px){.h4,h4{font-size:1.5rem}}.h5,h5{font-size:1.25rem}.h6,h6{font-size:1rem}p{margin-top:0;margin-bottom:1rem}abbr[data-bs-original-title],abbr[title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}ol,ul{padding-left:2rem}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}.small,small{font-size:.875em}.mark,mark{padding:.2em;background-color:#fcf8e3}sub,sup{position:relative;font-size:.75em;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#0d6efd;text-decoration:underline}a:hover{color:#0a58ca}a:not([href]):not([class]),a:not([href]):not([class]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:var(--bs-font-monospace);font-size:1em;direction:ltr;unicode-bidi:bidi-override}pre{display:block;margin-top:0;margin-bottom:1rem;overflow:auto;font-size:.875em}pre code{font-size:inherit;color:inherit;word-break:normal}code{font-size:.875em;color:#d63384;word-wrap:break-word}a>code{color:inherit}kbd{padding:.2rem .4rem;font-size:.875em;color:#fff;background-color:#212529;border-radius:.2rem}kbd kbd{padding:0;font-size:1em;font-weight:700}figure{margin:0 0 1rem}img,svg{vertical-align:middle}table{caption-side:bottom;border-collapse:collapse}caption{padding-top:.5rem;padding-bottom:.5rem;color:#6c757d;text-align:left}th{text-align:inherit;text-align:-webkit-match-parent}tbody,td,tfoot,th,thead,tr{border-color:inherit;border-style:solid;border-width:0}label{display:inline-block}button{border-radius:0}button:focus:not(:focus-visible){outline:0}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}select:disabled{opacity:1}[list]::-webkit-calendar-picker-indicator{display:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}::-moz-focus-inner{padding:0;border-style:none}textarea{resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{float:left;width:100%;padding:0;margin-bottom:.5rem;font-size:calc(1.275rem + .3vw);line-height:inherit}@media (min-width:1200px){legend{font-size:1.5rem}}legend+*{clear:left}::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-fields-wrapper,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-text,::-webkit-datetime-edit-year-field{padding:0}::-webkit-inner-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:textfield}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-color-swatch-wrapper{padding:0}::-webkit-file-upload-button{font:inherit}::file-selector-button{font:inherit}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}iframe{border:0}summary{display:list-item;cursor:pointer}progress{vertical-align:baseline}[hidden]{display:none!important}.lead{font-size:1.25rem;font-weight:300}.display-1{font-size:calc(1.625rem + 4.5vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-1{font-size:5rem}}.display-2{font-size:calc(1.575rem + 3.9vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-2{font-size:4.5rem}}.display-3{font-size:calc(1.525rem + 3.3vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-3{font-size:4rem}}.display-4{font-size:calc(1.475rem + 2.7vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-4{font-size:3.5rem}}.display-5{font-size:calc(1.425rem + 2.1vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-5{font-size:3rem}}.display-6{font-size:calc(1.375rem + 1.5vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-6{font-size:2.5rem}}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none}.list-inline-item{display:inline-block}.list-inline-item:not(:last-child){margin-right:.5rem}.initialism{font-size:.875em;text-transform:uppercase}.blockquote{margin-bottom:1rem;font-size:1.25rem}.blockquote>:last-child{margin-bottom:0}.blockquote-footer{margin-top:-1rem;margin-bottom:1rem;font-size:.875em;color:#6c757d}.blockquote-footer::before{content:"— "}.img-fluid{max-width:100%;height:auto}.img-thumbnail{padding:.25rem;background-color:#fff;border:1px solid #dee2e6;border-radius:.25rem;max-width:100%;height:auto}.figure{display:inline-block}.figure-img{margin-bottom:.5rem;line-height:1}.figure-caption{font-size:.875em;color:#6c757d}.container,.container-fluid,.container-lg,.container-md,.container-sm,.container-xl,.container-xxl{width:100%;padding-right:var(--bs-gutter-x,.75rem);padding-left:var(--bs-gutter-x,.75rem);margin-right:auto;margin-left:auto}@media (min-width:576px){.container,.container-sm{max-width:540px}}@media (min-width:768px){.container,.container-md,.container-sm{max-width:720px}}@media (min-width:992px){.container,.container-lg,.container-md,.container-sm{max-width:960px}}@media (min-width:1200px){.container,.container-lg,.container-md,.container-sm,.container-xl{max-width:1140px}}@media (min-width:1400px){.container,.container-lg,.container-md,.container-sm,.container-xl,.container-xxl{max-width:1320px}}.row{--bs-gutter-x:1.5rem;--bs-gutter-y:0;display:flex;flex-wrap:wrap;margin-top:calc(-1 * var(--bs-gutter-y));margin-right:calc(-.5 * var(--bs-gutter-x));margin-left:calc(-.5 * var(--bs-gutter-x))}.row>*{flex-shrink:0;width:100%;max-width:100%;padding-right:calc(var(--bs-gutter-x) * .5);padding-left:calc(var(--bs-gutter-x) * .5);margin-top:var(--bs-gutter-y)}.col{flex:1 0 0%}.row-cols-auto>*{flex:0 0 auto;width:auto}.row-cols-1>*{flex:0 0 auto;width:100%}.row-cols-2>*{flex:0 0 auto;width:50%}.row-cols-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-4>*{flex:0 0 auto;width:25%}.row-cols-5>*{flex:0 0 auto;width:20%}.row-cols-6>*{flex:0 0 auto;width:16.6666666667%}.col-auto{flex:0 0 auto;width:auto}.col-1{flex:0 0 auto;width:8.33333333%}.col-2{flex:0 0 auto;width:16.66666667%}.col-3{flex:0 0 auto;width:25%}.col-4{flex:0 0 auto;width:33.33333333%}.col-5{flex:0 0 auto;width:41.66666667%}.col-6{flex:0 0 auto;width:50%}.col-7{flex:0 0 auto;width:58.33333333%}.col-8{flex:0 0 auto;width:66.66666667%}.col-9{flex:0 0 auto;width:75%}.col-10{flex:0 0 auto;width:83.33333333%}.col-11{flex:0 0 auto;width:91.66666667%}.col-12{flex:0 0 auto;width:100%}.offset-1{margin-left:8.33333333%}.offset-2{margin-left:16.66666667%}.offset-3{margin-left:25%}.offset-4{margin-left:33.33333333%}.offset-5{margin-left:41.66666667%}.offset-6{margin-left:50%}.offset-7{margin-left:58.33333333%}.offset-8{margin-left:66.66666667%}.offset-9{margin-left:75%}.offset-10{margin-left:83.33333333%}.offset-11{margin-left:91.66666667%}.g-0,.gx-0{--bs-gutter-x:0}.g-0,.gy-0{--bs-gutter-y:0}.g-1,.gx-1{--bs-gutter-x:0.25rem}.g-1,.gy-1{--bs-gutter-y:0.25rem}.g-2,.gx-2{--bs-gutter-x:0.5rem}.g-2,.gy-2{--bs-gutter-y:0.5rem}.g-3,.gx-3{--bs-gutter-x:1rem}.g-3,.gy-3{--bs-gutter-y:1rem}.g-4,.gx-4{--bs-gutter-x:1.5rem}.g-4,.gy-4{--bs-gutter-y:1.5rem}.g-5,.gx-5{--bs-gutter-x:3rem}.g-5,.gy-5{--bs-gutter-y:3rem}@media (min-width:576px){.col-sm{flex:1 0 0%}.row-cols-sm-auto>*{flex:0 0 auto;width:auto}.row-cols-sm-1>*{flex:0 0 auto;width:100%}.row-cols-sm-2>*{flex:0 0 auto;width:50%}.row-cols-sm-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-sm-4>*{flex:0 0 auto;width:25%}.row-cols-sm-5>*{flex:0 0 auto;width:20%}.row-cols-sm-6>*{flex:0 0 auto;width:16.6666666667%}.col-sm-auto{flex:0 0 auto;width:auto}.col-sm-1{flex:0 0 auto;width:8.33333333%}.col-sm-2{flex:0 0 auto;width:16.66666667%}.col-sm-3{flex:0 0 auto;width:25%}.col-sm-4{flex:0 0 auto;width:33.33333333%}.col-sm-5{flex:0 0 auto;width:41.66666667%}.col-sm-6{flex:0 0 auto;width:50%}.col-sm-7{flex:0 0 auto;width:58.33333333%}.col-sm-8{flex:0 0 auto;width:66.66666667%}.col-sm-9{flex:0 0 auto;width:75%}.col-sm-10{flex:0 0 auto;width:83.33333333%}.col-sm-11{flex:0 0 auto;width:91.66666667%}.col-sm-12{flex:0 0 auto;width:100%}.offset-sm-0{margin-left:0}.offset-sm-1{margin-left:8.33333333%}.offset-sm-2{margin-left:16.66666667%}.offset-sm-3{margin-left:25%}.offset-sm-4{margin-left:33.33333333%}.offset-sm-5{margin-left:41.66666667%}.offset-sm-6{margin-left:50%}.offset-sm-7{margin-left:58.33333333%}.offset-sm-8{margin-left:66.66666667%}.offset-sm-9{margin-left:75%}.offset-sm-10{margin-left:83.33333333%}.offset-sm-11{margin-left:91.66666667%}.g-sm-0,.gx-sm-0{--bs-gutter-x:0}.g-sm-0,.gy-sm-0{--bs-gutter-y:0}.g-sm-1,.gx-sm-1{--bs-gutter-x:0.25rem}.g-sm-1,.gy-sm-1{--bs-gutter-y:0.25rem}.g-sm-2,.gx-sm-2{--bs-gutter-x:0.5rem}.g-sm-2,.gy-sm-2{--bs-gutter-y:0.5rem}.g-sm-3,.gx-sm-3{--bs-gutter-x:1rem}.g-sm-3,.gy-sm-3{--bs-gutter-y:1rem}.g-sm-4,.gx-sm-4{--bs-gutter-x:1.5rem}.g-sm-4,.gy-sm-4{--bs-gutter-y:1.5rem}.g-sm-5,.gx-sm-5{--bs-gutter-x:3rem}.g-sm-5,.gy-sm-5{--bs-gutter-y:3rem}}@media (min-width:768px){.col-md{flex:1 0 0%}.row-cols-md-auto>*{flex:0 0 auto;width:auto}.row-cols-md-1>*{flex:0 0 auto;width:100%}.row-cols-md-2>*{flex:0 0 auto;width:50%}.row-cols-md-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-md-4>*{flex:0 0 auto;width:25%}.row-cols-md-5>*{flex:0 0 auto;width:20%}.row-cols-md-6>*{flex:0 0 auto;width:16.6666666667%}.col-md-auto{flex:0 0 auto;width:auto}.col-md-1{flex:0 0 auto;width:8.33333333%}.col-md-2{flex:0 0 auto;width:16.66666667%}.col-md-3{flex:0 0 auto;width:25%}.col-md-4{flex:0 0 auto;width:33.33333333%}.col-md-5{flex:0 0 auto;width:41.66666667%}.col-md-6{flex:0 0 auto;width:50%}.col-md-7{flex:0 0 auto;width:58.33333333%}.col-md-8{flex:0 0 auto;width:66.66666667%}.col-md-9{flex:0 0 auto;width:75%}.col-md-10{flex:0 0 auto;width:83.33333333%}.col-md-11{flex:0 0 auto;width:91.66666667%}.col-md-12{flex:0 0 auto;width:100%}.offset-md-0{margin-left:0}.offset-md-1{margin-left:8.33333333%}.offset-md-2{margin-left:16.66666667%}.offset-md-3{margin-left:25%}.offset-md-4{margin-left:33.33333333%}.offset-md-5{margin-left:41.66666667%}.offset-md-6{margin-left:50%}.offset-md-7{margin-left:58.33333333%}.offset-md-8{margin-left:66.66666667%}.offset-md-9{margin-left:75%}.offset-md-10{margin-left:83.33333333%}.offset-md-11{margin-left:91.66666667%}.g-md-0,.gx-md-0{--bs-gutter-x:0}.g-md-0,.gy-md-0{--bs-gutter-y:0}.g-md-1,.gx-md-1{--bs-gutter-x:0.25rem}.g-md-1,.gy-md-1{--bs-gutter-y:0.25rem}.g-md-2,.gx-md-2{--bs-gutter-x:0.5rem}.g-md-2,.gy-md-2{--bs-gutter-y:0.5rem}.g-md-3,.gx-md-3{--bs-gutter-x:1rem}.g-md-3,.gy-md-3{--bs-gutter-y:1rem}.g-md-4,.gx-md-4{--bs-gutter-x:1.5rem}.g-md-4,.gy-md-4{--bs-gutter-y:1.5rem}.g-md-5,.gx-md-5{--bs-gutter-x:3rem}.g-md-5,.gy-md-5{--bs-gutter-y:3rem}}@media (min-width:992px){.col-lg{flex:1 0 0%}.row-cols-lg-auto>*{flex:0 0 auto;width:auto}.row-cols-lg-1>*{flex:0 0 auto;width:100%}.row-cols-lg-2>*{flex:0 0 auto;width:50%}.row-cols-lg-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-lg-4>*{flex:0 0 auto;width:25%}.row-cols-lg-5>*{flex:0 0 auto;width:20%}.row-cols-lg-6>*{flex:0 0 auto;width:16.6666666667%}.col-lg-auto{flex:0 0 auto;width:auto}.col-lg-1{flex:0 0 auto;width:8.33333333%}.col-lg-2{flex:0 0 auto;width:16.66666667%}.col-lg-3{flex:0 0 auto;width:25%}.col-lg-4{flex:0 0 auto;width:33.33333333%}.col-lg-5{flex:0 0 auto;width:41.66666667%}.col-lg-6{flex:0 0 auto;width:50%}.col-lg-7{flex:0 0 auto;width:58.33333333%}.col-lg-8{flex:0 0 auto;width:66.66666667%}.col-lg-9{flex:0 0 auto;width:75%}.col-lg-10{flex:0 0 auto;width:83.33333333%}.col-lg-11{flex:0 0 auto;width:91.66666667%}.col-lg-12{flex:0 0 auto;width:100%}.offset-lg-0{margin-left:0}.offset-lg-1{margin-left:8.33333333%}.offset-lg-2{margin-left:16.66666667%}.offset-lg-3{margin-left:25%}.offset-lg-4{margin-left:33.33333333%}.offset-lg-5{margin-left:41.66666667%}.offset-lg-6{margin-left:50%}.offset-lg-7{margin-left:58.33333333%}.offset-lg-8{margin-left:66.66666667%}.offset-lg-9{margin-left:75%}.offset-lg-10{margin-left:83.33333333%}.offset-lg-11{margin-left:91.66666667%}.g-lg-0,.gx-lg-0{--bs-gutter-x:0}.g-lg-0,.gy-lg-0{--bs-gutter-y:0}.g-lg-1,.gx-lg-1{--bs-gutter-x:0.25rem}.g-lg-1,.gy-lg-1{--bs-gutter-y:0.25rem}.g-lg-2,.gx-lg-2{--bs-gutter-x:0.5rem}.g-lg-2,.gy-lg-2{--bs-gutter-y:0.5rem}.g-lg-3,.gx-lg-3{--bs-gutter-x:1rem}.g-lg-3,.gy-lg-3{--bs-gutter-y:1rem}.g-lg-4,.gx-lg-4{--bs-gutter-x:1.5rem}.g-lg-4,.gy-lg-4{--bs-gutter-y:1.5rem}.g-lg-5,.gx-lg-5{--bs-gutter-x:3rem}.g-lg-5,.gy-lg-5{--bs-gutter-y:3rem}}@media (min-width:1200px){.col-xl{flex:1 0 0%}.row-cols-xl-auto>*{flex:0 0 auto;width:auto}.row-cols-xl-1>*{flex:0 0 auto;width:100%}.row-cols-xl-2>*{flex:0 0 auto;width:50%}.row-cols-xl-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-xl-4>*{flex:0 0 auto;width:25%}.row-cols-xl-5>*{flex:0 0 auto;width:20%}.row-cols-xl-6>*{flex:0 0 auto;width:16.6666666667%}.col-xl-auto{flex:0 0 auto;width:auto}.col-xl-1{flex:0 0 auto;width:8.33333333%}.col-xl-2{flex:0 0 auto;width:16.66666667%}.col-xl-3{flex:0 0 auto;width:25%}.col-xl-4{flex:0 0 auto;width:33.33333333%}.col-xl-5{flex:0 0 auto;width:41.66666667%}.col-xl-6{flex:0 0 auto;width:50%}.col-xl-7{flex:0 0 auto;width:58.33333333%}.col-xl-8{flex:0 0 auto;width:66.66666667%}.col-xl-9{flex:0 0 auto;width:75%}.col-xl-10{flex:0 0 auto;width:83.33333333%}.col-xl-11{flex:0 0 auto;width:91.66666667%}.col-xl-12{flex:0 0 auto;width:100%}.offset-xl-0{margin-left:0}.offset-xl-1{margin-left:8.33333333%}.offset-xl-2{margin-left:16.66666667%}.offset-xl-3{margin-left:25%}.offset-xl-4{margin-left:33.33333333%}.offset-xl-5{margin-left:41.66666667%}.offset-xl-6{margin-left:50%}.offset-xl-7{margin-left:58.33333333%}.offset-xl-8{margin-left:66.66666667%}.offset-xl-9{margin-left:75%}.offset-xl-10{margin-left:83.33333333%}.offset-xl-11{margin-left:91.66666667%}.g-xl-0,.gx-xl-0{--bs-gutter-x:0}.g-xl-0,.gy-xl-0{--bs-gutter-y:0}.g-xl-1,.gx-xl-1{--bs-gutter-x:0.25rem}.g-xl-1,.gy-xl-1{--bs-gutter-y:0.25rem}.g-xl-2,.gx-xl-2{--bs-gutter-x:0.5rem}.g-xl-2,.gy-xl-2{--bs-gutter-y:0.5rem}.g-xl-3,.gx-xl-3{--bs-gutter-x:1rem}.g-xl-3,.gy-xl-3{--bs-gutter-y:1rem}.g-xl-4,.gx-xl-4{--bs-gutter-x:1.5rem}.g-xl-4,.gy-xl-4{--bs-gutter-y:1.5rem}.g-xl-5,.gx-xl-5{--bs-gutter-x:3rem}.g-xl-5,.gy-xl-5{--bs-gutter-y:3rem}}@media (min-width:1400px){.col-xxl{flex:1 0 0%}.row-cols-xxl-auto>*{flex:0 0 auto;width:auto}.row-cols-xxl-1>*{flex:0 0 auto;width:100%}.row-cols-xxl-2>*{flex:0 0 auto;width:50%}.row-cols-xxl-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-xxl-4>*{flex:0 0 auto;width:25%}.row-cols-xxl-5>*{flex:0 0 auto;width:20%}.row-cols-xxl-6>*{flex:0 0 auto;width:16.6666666667%}.col-xxl-auto{flex:0 0 auto;width:auto}.col-xxl-1{flex:0 0 auto;width:8.33333333%}.col-xxl-2{flex:0 0 auto;width:16.66666667%}.col-xxl-3{flex:0 0 auto;width:25%}.col-xxl-4{flex:0 0 auto;width:33.33333333%}.col-xxl-5{flex:0 0 auto;width:41.66666667%}.col-xxl-6{flex:0 0 auto;width:50%}.col-xxl-7{flex:0 0 auto;width:58.33333333%}.col-xxl-8{flex:0 0 auto;width:66.66666667%}.col-xxl-9{flex:0 0 auto;width:75%}.col-xxl-10{flex:0 0 auto;width:83.33333333%}.col-xxl-11{flex:0 0 auto;width:91.66666667%}.col-xxl-12{flex:0 0 auto;width:100%}.offset-xxl-0{margin-left:0}.offset-xxl-1{margin-left:8.33333333%}.offset-xxl-2{margin-left:16.66666667%}.offset-xxl-3{margin-left:25%}.offset-xxl-4{margin-left:33.33333333%}.offset-xxl-5{margin-left:41.66666667%}.offset-xxl-6{margin-left:50%}.offset-xxl-7{margin-left:58.33333333%}.offset-xxl-8{margin-left:66.66666667%}.offset-xxl-9{margin-left:75%}.offset-xxl-10{margin-left:83.33333333%}.offset-xxl-11{margin-left:91.66666667%}.g-xxl-0,.gx-xxl-0{--bs-gutter-x:0}.g-xxl-0,.gy-xxl-0{--bs-gutter-y:0}.g-xxl-1,.gx-xxl-1{--bs-gutter-x:0.25rem}.g-xxl-1,.gy-xxl-1{--bs-gutter-y:0.25rem}.g-xxl-2,.gx-xxl-2{--bs-gutter-x:0.5rem}.g-xxl-2,.gy-xxl-2{--bs-gutter-y:0.5rem}.g-xxl-3,.gx-xxl-3{--bs-gutter-x:1rem}.g-xxl-3,.gy-xxl-3{--bs-gutter-y:1rem}.g-xxl-4,.gx-xxl-4{--bs-gutter-x:1.5rem}.g-xxl-4,.gy-xxl-4{--bs-gutter-y:1.5rem}.g-xxl-5,.gx-xxl-5{--bs-gutter-x:3rem}.g-xxl-5,.gy-xxl-5{--bs-gutter-y:3rem}}.table{--bs-table-bg:transparent;--bs-table-accent-bg:transparent;--bs-table-striped-color:#212529;--bs-table-striped-bg:rgba(0, 0, 0, 0.05);--bs-table-active-color:#212529;--bs-table-active-bg:rgba(0, 0, 0, 0.1);--bs-table-hover-color:#212529;--bs-table-hover-bg:rgba(0, 0, 0, 0.075);width:100%;margin-bottom:1rem;color:#212529;vertical-align:top;border-color:#dee2e6}.table>:not(caption)>*>*{padding:.5rem .5rem;background-color:var(--bs-table-bg);border-bottom-width:1px;box-shadow:inset 0 0 0 9999px var(--bs-table-accent-bg)}.table>tbody{vertical-align:inherit}.table>thead{vertical-align:bottom}.table>:not(:first-child){border-top:2px solid currentColor}.caption-top{caption-side:top}.table-sm>:not(caption)>*>*{padding:.25rem .25rem}.table-bordered>:not(caption)>*{border-width:1px 0}.table-bordered>:not(caption)>*>*{border-width:0 1px}.table-borderless>:not(caption)>*>*{border-bottom-width:0}.table-borderless>:not(:first-child){border-top-width:0}.table-striped>tbody>tr:nth-of-type(odd)>*{--bs-table-accent-bg:var(--bs-table-striped-bg);color:var(--bs-table-striped-color)}.table-active{--bs-table-accent-bg:var(--bs-table-active-bg);color:var(--bs-table-active-color)}.table-hover>tbody>tr:hover>*{--bs-table-accent-bg:var(--bs-table-hover-bg);color:var(--bs-table-hover-color)}.table-primary{--bs-table-bg:#cfe2ff;--bs-table-striped-bg:#c5d7f2;--bs-table-striped-color:#000;--bs-table-active-bg:#bacbe6;--bs-table-active-color:#000;--bs-table-hover-bg:#bfd1ec;--bs-table-hover-color:#000;color:#000;border-color:#bacbe6}.table-secondary{--bs-table-bg:#e2e3e5;--bs-table-striped-bg:#d7d8da;--bs-table-striped-color:#000;--bs-table-active-bg:#cbccce;--bs-table-active-color:#000;--bs-table-hover-bg:#d1d2d4;--bs-table-hover-color:#000;color:#000;border-color:#cbccce}.table-success{--bs-table-bg:#d1e7dd;--bs-table-striped-bg:#c7dbd2;--bs-table-striped-color:#000;--bs-table-active-bg:#bcd0c7;--bs-table-active-color:#000;--bs-table-hover-bg:#c1d6cc;--bs-table-hover-color:#000;color:#000;border-color:#bcd0c7}.table-info{--bs-table-bg:#cff4fc;--bs-table-striped-bg:#c5e8ef;--bs-table-striped-color:#000;--bs-table-active-bg:#badce3;--bs-table-active-color:#000;--bs-table-hover-bg:#bfe2e9;--bs-table-hover-color:#000;color:#000;border-color:#badce3}.table-warning{--bs-table-bg:#fff3cd;--bs-table-striped-bg:#f2e7c3;--bs-table-striped-color:#000;--bs-table-active-bg:#e6dbb9;--bs-table-active-color:#000;--bs-table-hover-bg:#ece1be;--bs-table-hover-color:#000;color:#000;border-color:#e6dbb9}.table-danger{--bs-table-bg:#f8d7da;--bs-table-striped-bg:#eccccf;--bs-table-striped-color:#000;--bs-table-active-bg:#dfc2c4;--bs-table-active-color:#000;--bs-table-hover-bg:#e5c7ca;--bs-table-hover-color:#000;color:#000;border-color:#dfc2c4}.table-light{--bs-table-bg:#f8f9fa;--bs-table-striped-bg:#ecedee;--bs-table-striped-color:#000;--bs-table-active-bg:#dfe0e1;--bs-table-active-color:#000;--bs-table-hover-bg:#e5e6e7;--bs-table-hover-color:#000;color:#000;border-color:#dfe0e1}.table-dark{--bs-table-bg:#212529;--bs-table-striped-bg:#2c3034;--bs-table-striped-color:#fff;--bs-table-active-bg:#373b3e;--bs-table-active-color:#fff;--bs-table-hover-bg:#323539;--bs-table-hover-color:#fff;color:#fff;border-color:#373b3e}.table-responsive{overflow-x:auto;-webkit-overflow-scrolling:touch}@media (max-width:575.98px){.table-responsive-sm{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:767.98px){.table-responsive-md{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:991.98px){.table-responsive-lg{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:1199.98px){.table-responsive-xl{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:1399.98px){.table-responsive-xxl{overflow-x:auto;-webkit-overflow-scrolling:touch}}.form-label{margin-bottom:.5rem}.col-form-label{padding-top:calc(.375rem + 1px);padding-bottom:calc(.375rem + 1px);margin-bottom:0;font-size:inherit;line-height:1.5}.col-form-label-lg{padding-top:calc(.5rem + 1px);padding-bottom:calc(.5rem + 1px);font-size:1.25rem}.col-form-label-sm{padding-top:calc(.25rem + 1px);padding-bottom:calc(.25rem + 1px);font-size:.875rem}.form-text{margin-top:.25rem;font-size:.875em;color:#6c757d}.form-control{display:block;width:100%;padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#212529;background-color:#fff;background-clip:padding-box;border:1px solid #ced4da;-webkit-appearance:none;-moz-appearance:none;appearance:none;border-radius:.25rem;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control{transition:none}}.form-control[type=file]{overflow:hidden}.form-control[type=file]:not(:disabled):not([readonly]){cursor:pointer}.form-control:focus{color:#212529;background-color:#fff;border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.form-control::-webkit-date-and-time-value{height:1.5em}.form-control::-moz-placeholder{color:#6c757d;opacity:1}.form-control::placeholder{color:#6c757d;opacity:1}.form-control:disabled,.form-control[readonly]{background-color:#e9ecef;opacity:1}.form-control::-webkit-file-upload-button{padding:.375rem .75rem;margin:-.375rem -.75rem;-webkit-margin-end:.75rem;margin-inline-end:.75rem;color:#212529;background-color:#e9ecef;pointer-events:none;border-color:inherit;border-style:solid;border-width:0;border-inline-end-width:1px;border-radius:0;-webkit-transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}.form-control::file-selector-button{padding:.375rem .75rem;margin:-.375rem -.75rem;-webkit-margin-end:.75rem;margin-inline-end:.75rem;color:#212529;background-color:#e9ecef;pointer-events:none;border-color:inherit;border-style:solid;border-width:0;border-inline-end-width:1px;border-radius:0;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control::-webkit-file-upload-button{-webkit-transition:none;transition:none}.form-control::file-selector-button{transition:none}}.form-control:hover:not(:disabled):not([readonly])::-webkit-file-upload-button{background-color:#dde0e3}.form-control:hover:not(:disabled):not([readonly])::file-selector-button{background-color:#dde0e3}.form-control::-webkit-file-upload-button{padding:.375rem .75rem;margin:-.375rem -.75rem;-webkit-margin-end:.75rem;margin-inline-end:.75rem;color:#212529;background-color:#e9ecef;pointer-events:none;border-color:inherit;border-style:solid;border-width:0;border-inline-end-width:1px;border-radius:0;-webkit-transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control::-webkit-file-upload-button{-webkit-transition:none;transition:none}}.form-control:hover:not(:disabled):not([readonly])::-webkit-file-upload-button{background-color:#dde0e3}.form-control-plaintext{display:block;width:100%;padding:.375rem 0;margin-bottom:0;line-height:1.5;color:#212529;background-color:transparent;border:solid transparent;border-width:1px 0}.form-control-plaintext.form-control-lg,.form-control-plaintext.form-control-sm{padding-right:0;padding-left:0}.form-control-sm{min-height:calc(1.5em + .5rem + 2px);padding:.25rem .5rem;font-size:.875rem;border-radius:.2rem}.form-control-sm::-webkit-file-upload-button{padding:.25rem .5rem;margin:-.25rem -.5rem;-webkit-margin-end:.5rem;margin-inline-end:.5rem}.form-control-sm::file-selector-button{padding:.25rem .5rem;margin:-.25rem -.5rem;-webkit-margin-end:.5rem;margin-inline-end:.5rem}.form-control-sm::-webkit-file-upload-button{padding:.25rem .5rem;margin:-.25rem -.5rem;-webkit-margin-end:.5rem;margin-inline-end:.5rem}.form-control-lg{min-height:calc(1.5em + 1rem + 2px);padding:.5rem 1rem;font-size:1.25rem;border-radius:.3rem}.form-control-lg::-webkit-file-upload-button{padding:.5rem 1rem;margin:-.5rem -1rem;-webkit-margin-end:1rem;margin-inline-end:1rem}.form-control-lg::file-selector-button{padding:.5rem 1rem;margin:-.5rem -1rem;-webkit-margin-end:1rem;margin-inline-end:1rem}.form-control-lg::-webkit-file-upload-button{padding:.5rem 1rem;margin:-.5rem -1rem;-webkit-margin-end:1rem;margin-inline-end:1rem}textarea.form-control{min-height:calc(1.5em + .75rem + 2px)}textarea.form-control-sm{min-height:calc(1.5em + .5rem + 2px)}textarea.form-control-lg{min-height:calc(1.5em + 1rem + 2px)}.form-control-color{width:3rem;height:auto;padding:.375rem}.form-control-color:not(:disabled):not([readonly]){cursor:pointer}.form-control-color::-moz-color-swatch{height:1.5em;border-radius:.25rem}.form-control-color::-webkit-color-swatch{height:1.5em;border-radius:.25rem}.form-select{display:block;width:100%;padding:.375rem 2.25rem .375rem .75rem;-moz-padding-start:calc(0.75rem - 3px);font-size:1rem;font-weight:400;line-height:1.5;color:#212529;background-color:#fff;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right .75rem center;background-size:16px 12px;border:1px solid #ced4da;border-radius:.25rem;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out;-webkit-appearance:none;-moz-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.form-select{transition:none}}.form-select:focus{border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.form-select[multiple],.form-select[size]:not([size="1"]){padding-right:.75rem;background-image:none}.form-select:disabled{background-color:#e9ecef}.form-select:-moz-focusring{color:transparent;text-shadow:0 0 0 #212529}.form-select-sm{padding-top:.25rem;padding-bottom:.25rem;padding-left:.5rem;font-size:.875rem;border-radius:.2rem}.form-select-lg{padding-top:.5rem;padding-bottom:.5rem;padding-left:1rem;font-size:1.25rem;border-radius:.3rem}.form-check{display:block;min-height:1.5rem;padding-left:1.5em;margin-bottom:.125rem}.form-check .form-check-input{float:left;margin-left:-1.5em}.form-check-input{width:1em;height:1em;margin-top:.25em;vertical-align:top;background-color:#fff;background-repeat:no-repeat;background-position:center;background-size:contain;border:1px solid rgba(0,0,0,.25);-webkit-appearance:none;-moz-appearance:none;appearance:none;-webkit-print-color-adjust:exact;color-adjust:exact}.form-check-input[type=checkbox]{border-radius:.25em}.form-check-input[type=radio]{border-radius:50%}.form-check-input:active{filter:brightness(90%)}.form-check-input:focus{border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.form-check-input:checked{background-color:#0d6efd;border-color:#0d6efd}.form-check-input:checked[type=checkbox]{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10l3 3l6-6'/%3e%3c/svg%3e")}.form-check-input:checked[type=radio]{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='2' fill='%23fff'/%3e%3c/svg%3e")}.form-check-input[type=checkbox]:indeterminate{background-color:#0d6efd;border-color:#0d6efd;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10h8'/%3e%3c/svg%3e")}.form-check-input:disabled{pointer-events:none;filter:none;opacity:.5}.form-check-input:disabled~.form-check-label,.form-check-input[disabled]~.form-check-label{opacity:.5}.form-switch{padding-left:2.5em}.form-switch .form-check-input{width:2em;margin-left:-2.5em;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%280, 0, 0, 0.25%29'/%3e%3c/svg%3e");background-position:left center;border-radius:2em;transition:background-position .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-switch .form-check-input{transition:none}}.form-switch .form-check-input:focus{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%2386b7fe'/%3e%3c/svg%3e")}.form-switch .form-check-input:checked{background-position:right center;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e")}.form-check-inline{display:inline-block;margin-right:1rem}.btn-check{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.btn-check:disabled+.btn,.btn-check[disabled]+.btn{pointer-events:none;filter:none;opacity:.65}.form-range{width:100%;height:1.5rem;padding:0;background-color:transparent;-webkit-appearance:none;-moz-appearance:none;appearance:none}.form-range:focus{outline:0}.form-range:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .25rem rgba(13,110,253,.25)}.form-range:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .25rem rgba(13,110,253,.25)}.form-range::-moz-focus-outer{border:0}.form-range::-webkit-slider-thumb{width:1rem;height:1rem;margin-top:-.25rem;background-color:#0d6efd;border:0;border-radius:1rem;-webkit-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;-webkit-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.form-range::-webkit-slider-thumb{-webkit-transition:none;transition:none}}.form-range::-webkit-slider-thumb:active{background-color:#b6d4fe}.form-range::-webkit-slider-runnable-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.form-range::-moz-range-thumb{width:1rem;height:1rem;background-color:#0d6efd;border:0;border-radius:1rem;-moz-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;-moz-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.form-range::-moz-range-thumb{-moz-transition:none;transition:none}}.form-range::-moz-range-thumb:active{background-color:#b6d4fe}.form-range::-moz-range-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.form-range:disabled{pointer-events:none}.form-range:disabled::-webkit-slider-thumb{background-color:#adb5bd}.form-range:disabled::-moz-range-thumb{background-color:#adb5bd}.form-floating{position:relative}.form-floating>.form-control,.form-floating>.form-select{height:calc(3.5rem + 2px);line-height:1.25}.form-floating>label{position:absolute;top:0;left:0;height:100%;padding:1rem .75rem;pointer-events:none;border:1px solid transparent;transform-origin:0 0;transition:opacity .1s ease-in-out,transform .1s ease-in-out}@media (prefers-reduced-motion:reduce){.form-floating>label{transition:none}}.form-floating>.form-control{padding:1rem .75rem}.form-floating>.form-control::-moz-placeholder{color:transparent}.form-floating>.form-control::placeholder{color:transparent}.form-floating>.form-control:not(:-moz-placeholder-shown){padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control:focus,.form-floating>.form-control:not(:placeholder-shown){padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control:-webkit-autofill{padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-select{padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control:not(:-moz-placeholder-shown)~label{opacity:.65;transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.form-floating>.form-control:focus~label,.form-floating>.form-control:not(:placeholder-shown)~label,.form-floating>.form-select~label{opacity:.65;transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.form-floating>.form-control:-webkit-autofill~label{opacity:.65;transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.input-group{position:relative;display:flex;flex-wrap:wrap;align-items:stretch;width:100%}.input-group>.form-control,.input-group>.form-select{position:relative;flex:1 1 auto;width:1%;min-width:0}.input-group>.form-control:focus,.input-group>.form-select:focus{z-index:3}.input-group .btn{position:relative;z-index:2}.input-group .btn:focus{z-index:3}.input-group-text{display:flex;align-items:center;padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:center;white-space:nowrap;background-color:#e9ecef;border:1px solid #ced4da;border-radius:.25rem}.input-group-lg>.btn,.input-group-lg>.form-control,.input-group-lg>.form-select,.input-group-lg>.input-group-text{padding:.5rem 1rem;font-size:1.25rem;border-radius:.3rem}.input-group-sm>.btn,.input-group-sm>.form-control,.input-group-sm>.form-select,.input-group-sm>.input-group-text{padding:.25rem .5rem;font-size:.875rem;border-radius:.2rem}.input-group-lg>.form-select,.input-group-sm>.form-select{padding-right:3rem}.input-group:not(.has-validation)>.dropdown-toggle:nth-last-child(n+3),.input-group:not(.has-validation)>:not(:last-child):not(.dropdown-toggle):not(.dropdown-menu){border-top-right-radius:0;border-bottom-right-radius:0}.input-group.has-validation>.dropdown-toggle:nth-last-child(n+4),.input-group.has-validation>:nth-last-child(n+3):not(.dropdown-toggle):not(.dropdown-menu){border-top-right-radius:0;border-bottom-right-radius:0}.input-group>:not(:first-child):not(.dropdown-menu):not(.valid-tooltip):not(.valid-feedback):not(.invalid-tooltip):not(.invalid-feedback){margin-left:-1px;border-top-left-radius:0;border-bottom-left-radius:0}.valid-feedback{display:none;width:100%;margin-top:.25rem;font-size:.875em;color:#198754}.valid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;color:#fff;background-color:rgba(25,135,84,.9);border-radius:.25rem}.is-valid~.valid-feedback,.is-valid~.valid-tooltip,.was-validated :valid~.valid-feedback,.was-validated :valid~.valid-tooltip{display:block}.form-control.is-valid,.was-validated .form-control:valid{border-color:#198754;padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-valid:focus,.was-validated .form-control:valid:focus{border-color:#198754;box-shadow:0 0 0 .25rem rgba(25,135,84,.25)}.was-validated textarea.form-control:valid,textarea.form-control.is-valid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.form-select.is-valid,.was-validated .form-select:valid{border-color:#198754}.form-select.is-valid:not([multiple]):not([size]),.form-select.is-valid:not([multiple])[size="1"],.was-validated .form-select:valid:not([multiple]):not([size]),.was-validated .form-select:valid:not([multiple])[size="1"]{padding-right:4.125rem;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e"),url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");background-position:right .75rem center,center right 2.25rem;background-size:16px 12px,calc(.75em + .375rem) calc(.75em + .375rem)}.form-select.is-valid:focus,.was-validated .form-select:valid:focus{border-color:#198754;box-shadow:0 0 0 .25rem rgba(25,135,84,.25)}.form-check-input.is-valid,.was-validated .form-check-input:valid{border-color:#198754}.form-check-input.is-valid:checked,.was-validated .form-check-input:valid:checked{background-color:#198754}.form-check-input.is-valid:focus,.was-validated .form-check-input:valid:focus{box-shadow:0 0 0 .25rem rgba(25,135,84,.25)}.form-check-input.is-valid~.form-check-label,.was-validated .form-check-input:valid~.form-check-label{color:#198754}.form-check-inline .form-check-input~.valid-feedback{margin-left:.5em}.input-group .form-control.is-valid,.input-group .form-select.is-valid,.was-validated .input-group .form-control:valid,.was-validated .input-group .form-select:valid{z-index:1}.input-group .form-control.is-valid:focus,.input-group .form-select.is-valid:focus,.was-validated .input-group .form-control:valid:focus,.was-validated .input-group .form-select:valid:focus{z-index:3}.invalid-feedback{display:none;width:100%;margin-top:.25rem;font-size:.875em;color:#dc3545}.invalid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;color:#fff;background-color:rgba(220,53,69,.9);border-radius:.25rem}.is-invalid~.invalid-feedback,.is-invalid~.invalid-tooltip,.was-validated :invalid~.invalid-feedback,.was-validated :invalid~.invalid-tooltip{display:block}.form-control.is-invalid,.was-validated .form-control:invalid{border-color:#dc3545;padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-invalid:focus,.was-validated .form-control:invalid:focus{border-color:#dc3545;box-shadow:0 0 0 .25rem rgba(220,53,69,.25)}.was-validated textarea.form-control:invalid,textarea.form-control.is-invalid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.form-select.is-invalid,.was-validated .form-select:invalid{border-color:#dc3545}.form-select.is-invalid:not([multiple]):not([size]),.form-select.is-invalid:not([multiple])[size="1"],.was-validated .form-select:invalid:not([multiple]):not([size]),.was-validated .form-select:invalid:not([multiple])[size="1"]{padding-right:4.125rem;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e"),url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e");background-position:right .75rem center,center right 2.25rem;background-size:16px 12px,calc(.75em + .375rem) calc(.75em + .375rem)}.form-select.is-invalid:focus,.was-validated .form-select:invalid:focus{border-color:#dc3545;box-shadow:0 0 0 .25rem rgba(220,53,69,.25)}.form-check-input.is-invalid,.was-validated .form-check-input:invalid{border-color:#dc3545}.form-check-input.is-invalid:checked,.was-validated .form-check-input:invalid:checked{background-color:#dc3545}.form-check-input.is-invalid:focus,.was-validated .form-check-input:invalid:focus{box-shadow:0 0 0 .25rem rgba(220,53,69,.25)}.form-check-input.is-invalid~.form-check-label,.was-validated .form-check-input:invalid~.form-check-label{color:#dc3545}.form-check-inline .form-check-input~.invalid-feedback{margin-left:.5em}.input-group .form-control.is-invalid,.input-group .form-select.is-invalid,.was-validated .input-group .form-control:invalid,.was-validated .input-group .form-select:invalid{z-index:2}.input-group .form-control.is-invalid:focus,.input-group .form-select.is-invalid:focus,.was-validated .input-group .form-control:invalid:focus,.was-validated .input-group .form-select:invalid:focus{z-index:3}.btn{display:inline-block;font-weight:400;line-height:1.5;color:#212529;text-align:center;text-decoration:none;vertical-align:middle;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;user-select:none;background-color:transparent;border:1px solid transparent;padding:.375rem .75rem;font-size:1rem;border-radius:.25rem;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.btn{transition:none}}.btn:hover{color:#212529}.btn-check:focus+.btn,.btn:focus{outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.btn.disabled,.btn:disabled,fieldset:disabled .btn{pointer-events:none;opacity:.65}.btn-primary{color:#fff;background-color:#0d6efd;border-color:#0d6efd}.btn-primary:hover{color:#fff;background-color:#0b5ed7;border-color:#0a58ca}.btn-check:focus+.btn-primary,.btn-primary:focus{color:#fff;background-color:#0b5ed7;border-color:#0a58ca;box-shadow:0 0 0 .25rem rgba(49,132,253,.5)}.btn-check:active+.btn-primary,.btn-check:checked+.btn-primary,.btn-primary.active,.btn-primary:active,.show>.btn-primary.dropdown-toggle{color:#fff;background-color:#0a58ca;border-color:#0a53be}.btn-check:active+.btn-primary:focus,.btn-check:checked+.btn-primary:focus,.btn-primary.active:focus,.btn-primary:active:focus,.show>.btn-primary.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(49,132,253,.5)}.btn-primary.disabled,.btn-primary:disabled{color:#fff;background-color:#0d6efd;border-color:#0d6efd}.btn-secondary{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-secondary:hover{color:#fff;background-color:#5c636a;border-color:#565e64}.btn-check:focus+.btn-secondary,.btn-secondary:focus{color:#fff;background-color:#5c636a;border-color:#565e64;box-shadow:0 0 0 .25rem rgba(130,138,145,.5)}.btn-check:active+.btn-secondary,.btn-check:checked+.btn-secondary,.btn-secondary.active,.btn-secondary:active,.show>.btn-secondary.dropdown-toggle{color:#fff;background-color:#565e64;border-color:#51585e}.btn-check:active+.btn-secondary:focus,.btn-check:checked+.btn-secondary:focus,.btn-secondary.active:focus,.btn-secondary:active:focus,.show>.btn-secondary.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(130,138,145,.5)}.btn-secondary.disabled,.btn-secondary:disabled{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-success{color:#fff;background-color:#198754;border-color:#198754}.btn-success:hover{color:#fff;background-color:#157347;border-color:#146c43}.btn-check:focus+.btn-success,.btn-success:focus{color:#fff;background-color:#157347;border-color:#146c43;box-shadow:0 0 0 .25rem rgba(60,153,110,.5)}.btn-check:active+.btn-success,.btn-check:checked+.btn-success,.btn-success.active,.btn-success:active,.show>.btn-success.dropdown-toggle{color:#fff;background-color:#146c43;border-color:#13653f}.btn-check:active+.btn-success:focus,.btn-check:checked+.btn-success:focus,.btn-success.active:focus,.btn-success:active:focus,.show>.btn-success.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(60,153,110,.5)}.btn-success.disabled,.btn-success:disabled{color:#fff;background-color:#198754;border-color:#198754}.btn-info{color:#000;background-color:#0dcaf0;border-color:#0dcaf0}.btn-info:hover{color:#000;background-color:#31d2f2;border-color:#25cff2}.btn-check:focus+.btn-info,.btn-info:focus{color:#000;background-color:#31d2f2;border-color:#25cff2;box-shadow:0 0 0 .25rem rgba(11,172,204,.5)}.btn-check:active+.btn-info,.btn-check:checked+.btn-info,.btn-info.active,.btn-info:active,.show>.btn-info.dropdown-toggle{color:#000;background-color:#3dd5f3;border-color:#25cff2}.btn-check:active+.btn-info:focus,.btn-check:checked+.btn-info:focus,.btn-info.active:focus,.btn-info:active:focus,.show>.btn-info.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(11,172,204,.5)}.btn-info.disabled,.btn-info:disabled{color:#000;background-color:#0dcaf0;border-color:#0dcaf0}.btn-warning{color:#000;background-color:#ffc107;border-color:#ffc107}.btn-warning:hover{color:#000;background-color:#ffca2c;border-color:#ffc720}.btn-check:focus+.btn-warning,.btn-warning:focus{color:#000;background-color:#ffca2c;border-color:#ffc720;box-shadow:0 0 0 .25rem rgba(217,164,6,.5)}.btn-check:active+.btn-warning,.btn-check:checked+.btn-warning,.btn-warning.active,.btn-warning:active,.show>.btn-warning.dropdown-toggle{color:#000;background-color:#ffcd39;border-color:#ffc720}.btn-check:active+.btn-warning:focus,.btn-check:checked+.btn-warning:focus,.btn-warning.active:focus,.btn-warning:active:focus,.show>.btn-warning.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(217,164,6,.5)}.btn-warning.disabled,.btn-warning:disabled{color:#000;background-color:#ffc107;border-color:#ffc107}.btn-danger{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-danger:hover{color:#fff;background-color:#bb2d3b;border-color:#b02a37}.btn-check:focus+.btn-danger,.btn-danger:focus{color:#fff;background-color:#bb2d3b;border-color:#b02a37;box-shadow:0 0 0 .25rem rgba(225,83,97,.5)}.btn-check:active+.btn-danger,.btn-check:checked+.btn-danger,.btn-danger.active,.btn-danger:active,.show>.btn-danger.dropdown-toggle{color:#fff;background-color:#b02a37;border-color:#a52834}.btn-check:active+.btn-danger:focus,.btn-check:checked+.btn-danger:focus,.btn-danger.active:focus,.btn-danger:active:focus,.show>.btn-danger.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(225,83,97,.5)}.btn-danger.disabled,.btn-danger:disabled{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-light{color:#000;background-color:#f8f9fa;border-color:#f8f9fa}.btn-light:hover{color:#000;background-color:#f9fafb;border-color:#f9fafb}.btn-check:focus+.btn-light,.btn-light:focus{color:#000;background-color:#f9fafb;border-color:#f9fafb;box-shadow:0 0 0 .25rem rgba(211,212,213,.5)}.btn-check:active+.btn-light,.btn-check:checked+.btn-light,.btn-light.active,.btn-light:active,.show>.btn-light.dropdown-toggle{color:#000;background-color:#f9fafb;border-color:#f9fafb}.btn-check:active+.btn-light:focus,.btn-check:checked+.btn-light:focus,.btn-light.active:focus,.btn-light:active:focus,.show>.btn-light.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(211,212,213,.5)}.btn-light.disabled,.btn-light:disabled{color:#000;background-color:#f8f9fa;border-color:#f8f9fa}.btn-dark{color:#fff;background-color:#212529;border-color:#212529}.btn-dark:hover{color:#fff;background-color:#1c1f23;border-color:#1a1e21}.btn-check:focus+.btn-dark,.btn-dark:focus{color:#fff;background-color:#1c1f23;border-color:#1a1e21;box-shadow:0 0 0 .25rem rgba(66,70,73,.5)}.btn-check:active+.btn-dark,.btn-check:checked+.btn-dark,.btn-dark.active,.btn-dark:active,.show>.btn-dark.dropdown-toggle{color:#fff;background-color:#1a1e21;border-color:#191c1f}.btn-check:active+.btn-dark:focus,.btn-check:checked+.btn-dark:focus,.btn-dark.active:focus,.btn-dark:active:focus,.show>.btn-dark.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(66,70,73,.5)}.btn-dark.disabled,.btn-dark:disabled{color:#fff;background-color:#212529;border-color:#212529}.btn-outline-primary{color:#0d6efd;border-color:#0d6efd}.btn-outline-primary:hover{color:#fff;background-color:#0d6efd;border-color:#0d6efd}.btn-check:focus+.btn-outline-primary,.btn-outline-primary:focus{box-shadow:0 0 0 .25rem rgba(13,110,253,.5)}.btn-check:active+.btn-outline-primary,.btn-check:checked+.btn-outline-primary,.btn-outline-primary.active,.btn-outline-primary.dropdown-toggle.show,.btn-outline-primary:active{color:#fff;background-color:#0d6efd;border-color:#0d6efd}.btn-check:active+.btn-outline-primary:focus,.btn-check:checked+.btn-outline-primary:focus,.btn-outline-primary.active:focus,.btn-outline-primary.dropdown-toggle.show:focus,.btn-outline-primary:active:focus{box-shadow:0 0 0 .25rem rgba(13,110,253,.5)}.btn-outline-primary.disabled,.btn-outline-primary:disabled{color:#0d6efd;background-color:transparent}.btn-outline-secondary{color:#6c757d;border-color:#6c757d}.btn-outline-secondary:hover{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-check:focus+.btn-outline-secondary,.btn-outline-secondary:focus{box-shadow:0 0 0 .25rem rgba(108,117,125,.5)}.btn-check:active+.btn-outline-secondary,.btn-check:checked+.btn-outline-secondary,.btn-outline-secondary.active,.btn-outline-secondary.dropdown-toggle.show,.btn-outline-secondary:active{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-check:active+.btn-outline-secondary:focus,.btn-check:checked+.btn-outline-secondary:focus,.btn-outline-secondary.active:focus,.btn-outline-secondary.dropdown-toggle.show:focus,.btn-outline-secondary:active:focus{box-shadow:0 0 0 .25rem rgba(108,117,125,.5)}.btn-outline-secondary.disabled,.btn-outline-secondary:disabled{color:#6c757d;background-color:transparent}.btn-outline-success{color:#198754;border-color:#198754}.btn-outline-success:hover{color:#fff;background-color:#198754;border-color:#198754}.btn-check:focus+.btn-outline-success,.btn-outline-success:focus{box-shadow:0 0 0 .25rem rgba(25,135,84,.5)}.btn-check:active+.btn-outline-success,.btn-check:checked+.btn-outline-success,.btn-outline-success.active,.btn-outline-success.dropdown-toggle.show,.btn-outline-success:active{color:#fff;background-color:#198754;border-color:#198754}.btn-check:active+.btn-outline-success:focus,.btn-check:checked+.btn-outline-success:focus,.btn-outline-success.active:focus,.btn-outline-success.dropdown-toggle.show:focus,.btn-outline-success:active:focus{box-shadow:0 0 0 .25rem rgba(25,135,84,.5)}.btn-outline-success.disabled,.btn-outline-success:disabled{color:#198754;background-color:transparent}.btn-outline-info{color:#0dcaf0;border-color:#0dcaf0}.btn-outline-info:hover{color:#000;background-color:#0dcaf0;border-color:#0dcaf0}.btn-check:focus+.btn-outline-info,.btn-outline-info:focus{box-shadow:0 0 0 .25rem rgba(13,202,240,.5)}.btn-check:active+.btn-outline-info,.btn-check:checked+.btn-outline-info,.btn-outline-info.active,.btn-outline-info.dropdown-toggle.show,.btn-outline-info:active{color:#000;background-color:#0dcaf0;border-color:#0dcaf0}.btn-check:active+.btn-outline-info:focus,.btn-check:checked+.btn-outline-info:focus,.btn-outline-info.active:focus,.btn-outline-info.dropdown-toggle.show:focus,.btn-outline-info:active:focus{box-shadow:0 0 0 .25rem rgba(13,202,240,.5)}.btn-outline-info.disabled,.btn-outline-info:disabled{color:#0dcaf0;background-color:transparent}.btn-outline-warning{color:#ffc107;border-color:#ffc107}.btn-outline-warning:hover{color:#000;background-color:#ffc107;border-color:#ffc107}.btn-check:focus+.btn-outline-warning,.btn-outline-warning:focus{box-shadow:0 0 0 .25rem rgba(255,193,7,.5)}.btn-check:active+.btn-outline-warning,.btn-check:checked+.btn-outline-warning,.btn-outline-warning.active,.btn-outline-warning.dropdown-toggle.show,.btn-outline-warning:active{color:#000;background-color:#ffc107;border-color:#ffc107}.btn-check:active+.btn-outline-warning:focus,.btn-check:checked+.btn-outline-warning:focus,.btn-outline-warning.active:focus,.btn-outline-warning.dropdown-toggle.show:focus,.btn-outline-warning:active:focus{box-shadow:0 0 0 .25rem rgba(255,193,7,.5)}.btn-outline-warning.disabled,.btn-outline-warning:disabled{color:#ffc107;background-color:transparent}.btn-outline-danger{color:#dc3545;border-color:#dc3545}.btn-outline-danger:hover{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-check:focus+.btn-outline-danger,.btn-outline-danger:focus{box-shadow:0 0 0 .25rem rgba(220,53,69,.5)}.btn-check:active+.btn-outline-danger,.btn-check:checked+.btn-outline-danger,.btn-outline-danger.active,.btn-outline-danger.dropdown-toggle.show,.btn-outline-danger:active{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-check:active+.btn-outline-danger:focus,.btn-check:checked+.btn-outline-danger:focus,.btn-outline-danger.active:focus,.btn-outline-danger.dropdown-toggle.show:focus,.btn-outline-danger:active:focus{box-shadow:0 0 0 .25rem rgba(220,53,69,.5)}.btn-outline-danger.disabled,.btn-outline-danger:disabled{color:#dc3545;background-color:transparent}.btn-outline-light{color:#f8f9fa;border-color:#f8f9fa}.btn-outline-light:hover{color:#000;background-color:#f8f9fa;border-color:#f8f9fa}.btn-check:focus+.btn-outline-light,.btn-outline-light:focus{box-shadow:0 0 0 .25rem rgba(248,249,250,.5)}.btn-check:active+.btn-outline-light,.btn-check:checked+.btn-outline-light,.btn-outline-light.active,.btn-outline-light.dropdown-toggle.show,.btn-outline-light:active{color:#000;background-color:#f8f9fa;border-color:#f8f9fa}.btn-check:active+.btn-outline-light:focus,.btn-check:checked+.btn-outline-light:focus,.btn-outline-light.active:focus,.btn-outline-light.dropdown-toggle.show:focus,.btn-outline-light:active:focus{box-shadow:0 0 0 .25rem rgba(248,249,250,.5)}.btn-outline-light.disabled,.btn-outline-light:disabled{color:#f8f9fa;background-color:transparent}.btn-outline-dark{color:#212529;border-color:#212529}.btn-outline-dark:hover{color:#fff;background-color:#212529;border-color:#212529}.btn-check:focus+.btn-outline-dark,.btn-outline-dark:focus{box-shadow:0 0 0 .25rem rgba(33,37,41,.5)}.btn-check:active+.btn-outline-dark,.btn-check:checked+.btn-outline-dark,.btn-outline-dark.active,.btn-outline-dark.dropdown-toggle.show,.btn-outline-dark:active{color:#fff;background-color:#212529;border-color:#212529}.btn-check:active+.btn-outline-dark:focus,.btn-check:checked+.btn-outline-dark:focus,.btn-outline-dark.active:focus,.btn-outline-dark.dropdown-toggle.show:focus,.btn-outline-dark:active:focus{box-shadow:0 0 0 .25rem rgba(33,37,41,.5)}.btn-outline-dark.disabled,.btn-outline-dark:disabled{color:#212529;background-color:transparent}.btn-link{font-weight:400;color:#0d6efd;text-decoration:underline}.btn-link:hover{color:#0a58ca}.btn-link.disabled,.btn-link:disabled{color:#6c757d}.btn-group-lg>.btn,.btn-lg{padding:.5rem 1rem;font-size:1.25rem;border-radius:.3rem}.btn-group-sm>.btn,.btn-sm{padding:.25rem .5rem;font-size:.875rem;border-radius:.2rem}.fade{transition:opacity .15s linear}@media (prefers-reduced-motion:reduce){.fade{transition:none}}.fade:not(.show){opacity:0}.collapse:not(.show){display:none}.collapsing{height:0;overflow:hidden;transition:height .35s ease}@media (prefers-reduced-motion:reduce){.collapsing{transition:none}}.collapsing.collapse-horizontal{width:0;height:auto;transition:width .35s ease}@media (prefers-reduced-motion:reduce){.collapsing.collapse-horizontal{transition:none}}.dropdown,.dropend,.dropstart,.dropup{position:relative}.dropdown-toggle{white-space:nowrap}.dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid;border-right:.3em solid transparent;border-bottom:0;border-left:.3em solid transparent}.dropdown-toggle:empty::after{margin-left:0}.dropdown-menu{position:absolute;z-index:1000;display:none;min-width:10rem;padding:.5rem 0;margin:0;font-size:1rem;color:#212529;text-align:left;list-style:none;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.15);border-radius:.25rem}.dropdown-menu[data-bs-popper]{top:100%;left:0;margin-top:.125rem}.dropdown-menu-start{--bs-position:start}.dropdown-menu-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-end{--bs-position:end}.dropdown-menu-end[data-bs-popper]{right:0;left:auto}@media (min-width:576px){.dropdown-menu-sm-start{--bs-position:start}.dropdown-menu-sm-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-sm-end{--bs-position:end}.dropdown-menu-sm-end[data-bs-popper]{right:0;left:auto}}@media (min-width:768px){.dropdown-menu-md-start{--bs-position:start}.dropdown-menu-md-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-md-end{--bs-position:end}.dropdown-menu-md-end[data-bs-popper]{right:0;left:auto}}@media (min-width:992px){.dropdown-menu-lg-start{--bs-position:start}.dropdown-menu-lg-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-lg-end{--bs-position:end}.dropdown-menu-lg-end[data-bs-popper]{right:0;left:auto}}@media (min-width:1200px){.dropdown-menu-xl-start{--bs-position:start}.dropdown-menu-xl-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-xl-end{--bs-position:end}.dropdown-menu-xl-end[data-bs-popper]{right:0;left:auto}}@media (min-width:1400px){.dropdown-menu-xxl-start{--bs-position:start}.dropdown-menu-xxl-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-xxl-end{--bs-position:end}.dropdown-menu-xxl-end[data-bs-popper]{right:0;left:auto}}.dropup .dropdown-menu[data-bs-popper]{top:auto;bottom:100%;margin-top:0;margin-bottom:.125rem}.dropup .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:0;border-right:.3em solid transparent;border-bottom:.3em solid;border-left:.3em solid transparent}.dropup .dropdown-toggle:empty::after{margin-left:0}.dropend .dropdown-menu[data-bs-popper]{top:0;right:auto;left:100%;margin-top:0;margin-left:.125rem}.dropend .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:0;border-bottom:.3em solid transparent;border-left:.3em solid}.dropend .dropdown-toggle:empty::after{margin-left:0}.dropend .dropdown-toggle::after{vertical-align:0}.dropstart .dropdown-menu[data-bs-popper]{top:0;right:100%;left:auto;margin-top:0;margin-right:.125rem}.dropstart .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:""}.dropstart .dropdown-toggle::after{display:none}.dropstart .dropdown-toggle::before{display:inline-block;margin-right:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:.3em solid;border-bottom:.3em solid transparent}.dropstart .dropdown-toggle:empty::after{margin-left:0}.dropstart .dropdown-toggle::before{vertical-align:0}.dropdown-divider{height:0;margin:.5rem 0;overflow:hidden;border-top:1px solid rgba(0,0,0,.15)}.dropdown-item{display:block;width:100%;padding:.25rem 1rem;clear:both;font-weight:400;color:#212529;text-align:inherit;text-decoration:none;white-space:nowrap;background-color:transparent;border:0}.dropdown-item:focus,.dropdown-item:hover{color:#1e2125;background-color:#e9ecef}.dropdown-item.active,.dropdown-item:active{color:#fff;text-decoration:none;background-color:#0d6efd}.dropdown-item.disabled,.dropdown-item:disabled{color:#adb5bd;pointer-events:none;background-color:transparent}.dropdown-menu.show{display:block}.dropdown-header{display:block;padding:.5rem 1rem;margin-bottom:0;font-size:.875rem;color:#6c757d;white-space:nowrap}.dropdown-item-text{display:block;padding:.25rem 1rem;color:#212529}.dropdown-menu-dark{color:#dee2e6;background-color:#343a40;border-color:rgba(0,0,0,.15)}.dropdown-menu-dark .dropdown-item{color:#dee2e6}.dropdown-menu-dark .dropdown-item:focus,.dropdown-menu-dark .dropdown-item:hover{color:#fff;background-color:rgba(255,255,255,.15)}.dropdown-menu-dark .dropdown-item.active,.dropdown-menu-dark .dropdown-item:active{color:#fff;background-color:#0d6efd}.dropdown-menu-dark .dropdown-item.disabled,.dropdown-menu-dark .dropdown-item:disabled{color:#adb5bd}.dropdown-menu-dark .dropdown-divider{border-color:rgba(0,0,0,.15)}.dropdown-menu-dark .dropdown-item-text{color:#dee2e6}.dropdown-menu-dark .dropdown-header{color:#adb5bd}.btn-group,.btn-group-vertical{position:relative;display:inline-flex;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;flex:1 1 auto}.btn-group-vertical>.btn-check:checked+.btn,.btn-group-vertical>.btn-check:focus+.btn,.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:hover,.btn-group>.btn-check:checked+.btn,.btn-group>.btn-check:focus+.btn,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus,.btn-group>.btn:hover{z-index:1}.btn-toolbar{display:flex;flex-wrap:wrap;justify-content:flex-start}.btn-toolbar .input-group{width:auto}.btn-group>.btn-group:not(:first-child),.btn-group>.btn:not(:first-child){margin-left:-1px}.btn-group>.btn-group:not(:last-child)>.btn,.btn-group>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:not(:first-child)>.btn,.btn-group>.btn:nth-child(n+3),.btn-group>:not(.btn-check)+.btn{border-top-left-radius:0;border-bottom-left-radius:0}.dropdown-toggle-split{padding-right:.5625rem;padding-left:.5625rem}.dropdown-toggle-split::after,.dropend .dropdown-toggle-split::after,.dropup .dropdown-toggle-split::after{margin-left:0}.dropstart .dropdown-toggle-split::before{margin-right:0}.btn-group-sm>.btn+.dropdown-toggle-split,.btn-sm+.dropdown-toggle-split{padding-right:.375rem;padding-left:.375rem}.btn-group-lg>.btn+.dropdown-toggle-split,.btn-lg+.dropdown-toggle-split{padding-right:.75rem;padding-left:.75rem}.btn-group-vertical{flex-direction:column;align-items:flex-start;justify-content:center}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group{width:100%}.btn-group-vertical>.btn-group:not(:first-child),.btn-group-vertical>.btn:not(:first-child){margin-top:-1px}.btn-group-vertical>.btn-group:not(:last-child)>.btn,.btn-group-vertical>.btn:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:not(:first-child)>.btn,.btn-group-vertical>.btn~.btn{border-top-left-radius:0;border-top-right-radius:0}.nav{display:flex;flex-wrap:wrap;padding-left:0;margin-bottom:0;list-style:none}.nav-link{display:block;padding:.5rem 1rem;color:#0d6efd;text-decoration:none;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out}@media (prefers-reduced-motion:reduce){.nav-link{transition:none}}.nav-link:focus,.nav-link:hover{color:#0a58ca}.nav-link.disabled{color:#6c757d;pointer-events:none;cursor:default}.nav-tabs{border-bottom:1px solid #dee2e6}.nav-tabs .nav-link{margin-bottom:-1px;background:0 0;border:1px solid transparent;border-top-left-radius:.25rem;border-top-right-radius:.25rem}.nav-tabs .nav-link:focus,.nav-tabs .nav-link:hover{border-color:#e9ecef #e9ecef #dee2e6;isolation:isolate}.nav-tabs .nav-link.disabled{color:#6c757d;background-color:transparent;border-color:transparent}.nav-tabs .nav-item.show .nav-link,.nav-tabs .nav-link.active{color:#495057;background-color:#fff;border-color:#dee2e6 #dee2e6 #fff}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-left-radius:0;border-top-right-radius:0}.nav-pills .nav-link{background:0 0;border:0;border-radius:.25rem}.nav-pills .nav-link.active,.nav-pills .show>.nav-link{color:#fff;background-color:#0d6efd}.nav-fill .nav-item,.nav-fill>.nav-link{flex:1 1 auto;text-align:center}.nav-justified .nav-item,.nav-justified>.nav-link{flex-basis:0;flex-grow:1;text-align:center}.nav-fill .nav-item .nav-link,.nav-justified .nav-item .nav-link{width:100%}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.navbar{position:relative;display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;padding-top:.5rem;padding-bottom:.5rem}.navbar>.container,.navbar>.container-fluid,.navbar>.container-lg,.navbar>.container-md,.navbar>.container-sm,.navbar>.container-xl,.navbar>.container-xxl{display:flex;flex-wrap:inherit;align-items:center;justify-content:space-between}.navbar-brand{padding-top:.3125rem;padding-bottom:.3125rem;margin-right:1rem;font-size:1.25rem;text-decoration:none;white-space:nowrap}.navbar-nav{display:flex;flex-direction:column;padding-left:0;margin-bottom:0;list-style:none}.navbar-nav .nav-link{padding-right:0;padding-left:0}.navbar-nav .dropdown-menu{position:static}.navbar-text{padding-top:.5rem;padding-bottom:.5rem}.navbar-collapse{flex-basis:100%;flex-grow:1;align-items:center}.navbar-toggler{padding:.25rem .75rem;font-size:1.25rem;line-height:1;background-color:transparent;border:1px solid transparent;border-radius:.25rem;transition:box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.navbar-toggler{transition:none}}.navbar-toggler:hover{text-decoration:none}.navbar-toggler:focus{text-decoration:none;outline:0;box-shadow:0 0 0 .25rem}.navbar-toggler-icon{display:inline-block;width:1.5em;height:1.5em;vertical-align:middle;background-repeat:no-repeat;background-position:center;background-size:100%}.navbar-nav-scroll{max-height:var(--bs-scroll-height,75vh);overflow-y:auto}@media (min-width:576px){.navbar-expand-sm{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-sm .navbar-nav{flex-direction:row}.navbar-expand-sm .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-sm .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-sm .navbar-nav-scroll{overflow:visible}.navbar-expand-sm .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-sm .navbar-toggler{display:none}.navbar-expand-sm .offcanvas-header{display:none}.navbar-expand-sm .offcanvas{position:inherit;bottom:0;z-index:1000;flex-grow:1;visibility:visible!important;background-color:transparent;border-right:0;border-left:0;transition:none;transform:none}.navbar-expand-sm .offcanvas-bottom,.navbar-expand-sm .offcanvas-top{height:auto;border-top:0;border-bottom:0}.navbar-expand-sm .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:768px){.navbar-expand-md{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-md .navbar-nav{flex-direction:row}.navbar-expand-md .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-md .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-md .navbar-nav-scroll{overflow:visible}.navbar-expand-md .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-md .navbar-toggler{display:none}.navbar-expand-md .offcanvas-header{display:none}.navbar-expand-md .offcanvas{position:inherit;bottom:0;z-index:1000;flex-grow:1;visibility:visible!important;background-color:transparent;border-right:0;border-left:0;transition:none;transform:none}.navbar-expand-md .offcanvas-bottom,.navbar-expand-md .offcanvas-top{height:auto;border-top:0;border-bottom:0}.navbar-expand-md .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:992px){.navbar-expand-lg{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-lg .navbar-nav{flex-direction:row}.navbar-expand-lg .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-lg .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-lg .navbar-nav-scroll{overflow:visible}.navbar-expand-lg .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-lg .navbar-toggler{display:none}.navbar-expand-lg .offcanvas-header{display:none}.navbar-expand-lg .offcanvas{position:inherit;bottom:0;z-index:1000;flex-grow:1;visibility:visible!important;background-color:transparent;border-right:0;border-left:0;transition:none;transform:none}.navbar-expand-lg .offcanvas-bottom,.navbar-expand-lg .offcanvas-top{height:auto;border-top:0;border-bottom:0}.navbar-expand-lg .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:1200px){.navbar-expand-xl{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-xl .navbar-nav{flex-direction:row}.navbar-expand-xl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xl .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-xl .navbar-nav-scroll{overflow:visible}.navbar-expand-xl .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-xl .navbar-toggler{display:none}.navbar-expand-xl .offcanvas-header{display:none}.navbar-expand-xl .offcanvas{position:inherit;bottom:0;z-index:1000;flex-grow:1;visibility:visible!important;background-color:transparent;border-right:0;border-left:0;transition:none;transform:none}.navbar-expand-xl .offcanvas-bottom,.navbar-expand-xl .offcanvas-top{height:auto;border-top:0;border-bottom:0}.navbar-expand-xl .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:1400px){.navbar-expand-xxl{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-xxl .navbar-nav{flex-direction:row}.navbar-expand-xxl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xxl .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-xxl .navbar-nav-scroll{overflow:visible}.navbar-expand-xxl .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-xxl .navbar-toggler{display:none}.navbar-expand-xxl .offcanvas-header{display:none}.navbar-expand-xxl .offcanvas{position:inherit;bottom:0;z-index:1000;flex-grow:1;visibility:visible!important;background-color:transparent;border-right:0;border-left:0;transition:none;transform:none}.navbar-expand-xxl .offcanvas-bottom,.navbar-expand-xxl .offcanvas-top{height:auto;border-top:0;border-bottom:0}.navbar-expand-xxl .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}.navbar-expand{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand .navbar-nav{flex-direction:row}.navbar-expand .navbar-nav .dropdown-menu{position:absolute}.navbar-expand .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand .navbar-nav-scroll{overflow:visible}.navbar-expand .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand .navbar-toggler{display:none}.navbar-expand .offcanvas-header{display:none}.navbar-expand .offcanvas{position:inherit;bottom:0;z-index:1000;flex-grow:1;visibility:visible!important;background-color:transparent;border-right:0;border-left:0;transition:none;transform:none}.navbar-expand .offcanvas-bottom,.navbar-expand .offcanvas-top{height:auto;border-top:0;border-bottom:0}.navbar-expand .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}.navbar-light .navbar-brand{color:rgba(0,0,0,.9)}.navbar-light .navbar-brand:focus,.navbar-light .navbar-brand:hover{color:rgba(0,0,0,.9)}.navbar-light .navbar-nav .nav-link{color:rgba(0,0,0,.55)}.navbar-light .navbar-nav .nav-link:focus,.navbar-light .navbar-nav .nav-link:hover{color:rgba(0,0,0,.7)}.navbar-light .navbar-nav .nav-link.disabled{color:rgba(0,0,0,.3)}.navbar-light .navbar-nav .nav-link.active,.navbar-light .navbar-nav .show>.nav-link{color:rgba(0,0,0,.9)}.navbar-light .navbar-toggler{color:rgba(0,0,0,.55);border-color:rgba(0,0,0,.1)}.navbar-light .navbar-toggler-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%280, 0, 0, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.navbar-light .navbar-text{color:rgba(0,0,0,.55)}.navbar-light .navbar-text a,.navbar-light .navbar-text a:focus,.navbar-light .navbar-text a:hover{color:rgba(0,0,0,.9)}.navbar-dark .navbar-brand{color:#fff}.navbar-dark .navbar-brand:focus,.navbar-dark .navbar-brand:hover{color:#fff}.navbar-dark .navbar-nav .nav-link{color:rgba(255,255,255,.55)}.navbar-dark .navbar-nav .nav-link:focus,.navbar-dark .navbar-nav .nav-link:hover{color:rgba(255,255,255,.75)}.navbar-dark .navbar-nav .nav-link.disabled{color:rgba(255,255,255,.25)}.navbar-dark .navbar-nav .nav-link.active,.navbar-dark .navbar-nav .show>.nav-link{color:#fff}.navbar-dark .navbar-toggler{color:rgba(255,255,255,.55);border-color:rgba(255,255,255,.1)}.navbar-dark .navbar-toggler-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.navbar-dark .navbar-text{color:rgba(255,255,255,.55)}.navbar-dark .navbar-text a,.navbar-dark .navbar-text a:focus,.navbar-dark .navbar-text a:hover{color:#fff}.card{position:relative;display:flex;flex-direction:column;min-width:0;word-wrap:break-word;background-color:#fff;background-clip:border-box;border:1px solid rgba(0,0,0,.125);border-radius:.25rem}.card>hr{margin-right:0;margin-left:0}.card>.list-group{border-top:inherit;border-bottom:inherit}.card>.list-group:first-child{border-top-width:0;border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.card>.list-group:last-child{border-bottom-width:0;border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.card>.card-header+.list-group,.card>.list-group+.card-footer{border-top:0}.card-body{flex:1 1 auto;padding:1rem 1rem}.card-title{margin-bottom:.5rem}.card-subtitle{margin-top:-.25rem;margin-bottom:0}.card-text:last-child{margin-bottom:0}.card-link+.card-link{margin-left:1rem}.card-header{padding:.5rem 1rem;margin-bottom:0;background-color:rgba(0,0,0,.03);border-bottom:1px solid rgba(0,0,0,.125)}.card-header:first-child{border-radius:calc(.25rem - 1px) calc(.25rem - 1px) 0 0}.card-footer{padding:.5rem 1rem;background-color:rgba(0,0,0,.03);border-top:1px solid rgba(0,0,0,.125)}.card-footer:last-child{border-radius:0 0 calc(.25rem - 1px) calc(.25rem - 1px)}.card-header-tabs{margin-right:-.5rem;margin-bottom:-.5rem;margin-left:-.5rem;border-bottom:0}.card-header-pills{margin-right:-.5rem;margin-left:-.5rem}.card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:1rem;border-radius:calc(.25rem - 1px)}.card-img,.card-img-bottom,.card-img-top{width:100%}.card-img,.card-img-top{border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.card-img,.card-img-bottom{border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.card-group>.card{margin-bottom:.75rem}@media (min-width:576px){.card-group{display:flex;flex-flow:row wrap}.card-group>.card{flex:1 0 0%;margin-bottom:0}.card-group>.card+.card{margin-left:0;border-left:0}.card-group>.card:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.card-group>.card:not(:last-child) .card-header,.card-group>.card:not(:last-child) .card-img-top{border-top-right-radius:0}.card-group>.card:not(:last-child) .card-footer,.card-group>.card:not(:last-child) .card-img-bottom{border-bottom-right-radius:0}.card-group>.card:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.card-group>.card:not(:first-child) .card-header,.card-group>.card:not(:first-child) .card-img-top{border-top-left-radius:0}.card-group>.card:not(:first-child) .card-footer,.card-group>.card:not(:first-child) .card-img-bottom{border-bottom-left-radius:0}}.accordion-button{position:relative;display:flex;align-items:center;width:100%;padding:1rem 1.25rem;font-size:1rem;color:#212529;text-align:left;background-color:#fff;border:0;border-radius:0;overflow-anchor:none;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out,border-radius .15s ease}@media (prefers-reduced-motion:reduce){.accordion-button{transition:none}}.accordion-button:not(.collapsed){color:#0c63e4;background-color:#e7f1ff;box-shadow:inset 0 -1px 0 rgba(0,0,0,.125)}.accordion-button:not(.collapsed)::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%230c63e4'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");transform:rotate(-180deg)}.accordion-button::after{flex-shrink:0;width:1.25rem;height:1.25rem;margin-left:auto;content:"";background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23212529'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");background-repeat:no-repeat;background-size:1.25rem;transition:transform .2s ease-in-out}@media (prefers-reduced-motion:reduce){.accordion-button::after{transition:none}}.accordion-button:hover{z-index:2}.accordion-button:focus{z-index:3;border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.accordion-header{margin-bottom:0}.accordion-item{background-color:#fff;border:1px solid rgba(0,0,0,.125)}.accordion-item:first-of-type{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.accordion-item:first-of-type .accordion-button{border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.accordion-item:not(:first-of-type){border-top:0}.accordion-item:last-of-type{border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.accordion-item:last-of-type .accordion-button.collapsed{border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.accordion-item:last-of-type .accordion-collapse{border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.accordion-body{padding:1rem 1.25rem}.accordion-flush .accordion-collapse{border-width:0}.accordion-flush .accordion-item{border-right:0;border-left:0;border-radius:0}.accordion-flush .accordion-item:first-child{border-top:0}.accordion-flush .accordion-item:last-child{border-bottom:0}.accordion-flush .accordion-item .accordion-button{border-radius:0}.breadcrumb{display:flex;flex-wrap:wrap;padding:0 0;margin-bottom:1rem;list-style:none}.breadcrumb-item+.breadcrumb-item{padding-left:.5rem}.breadcrumb-item+.breadcrumb-item::before{float:left;padding-right:.5rem;color:#6c757d;content:var(--bs-breadcrumb-divider, "/")}.breadcrumb-item.active{color:#6c757d}.pagination{display:flex;padding-left:0;list-style:none}.page-link{position:relative;display:block;color:#0d6efd;text-decoration:none;background-color:#fff;border:1px solid #dee2e6;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.page-link{transition:none}}.page-link:hover{z-index:2;color:#0a58ca;background-color:#e9ecef;border-color:#dee2e6}.page-link:focus{z-index:3;color:#0a58ca;background-color:#e9ecef;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.page-item:not(:first-child) .page-link{margin-left:-1px}.page-item.active .page-link{z-index:3;color:#fff;background-color:#0d6efd;border-color:#0d6efd}.page-item.disabled .page-link{color:#6c757d;pointer-events:none;background-color:#fff;border-color:#dee2e6}.page-link{padding:.375rem .75rem}.page-item:first-child .page-link{border-top-left-radius:.25rem;border-bottom-left-radius:.25rem}.page-item:last-child .page-link{border-top-right-radius:.25rem;border-bottom-right-radius:.25rem}.pagination-lg .page-link{padding:.75rem 1.5rem;font-size:1.25rem}.pagination-lg .page-item:first-child .page-link{border-top-left-radius:.3rem;border-bottom-left-radius:.3rem}.pagination-lg .page-item:last-child .page-link{border-top-right-radius:.3rem;border-bottom-right-radius:.3rem}.pagination-sm .page-link{padding:.25rem .5rem;font-size:.875rem}.pagination-sm .page-item:first-child .page-link{border-top-left-radius:.2rem;border-bottom-left-radius:.2rem}.pagination-sm .page-item:last-child .page-link{border-top-right-radius:.2rem;border-bottom-right-radius:.2rem}.badge{display:inline-block;padding:.35em .65em;font-size:.75em;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25rem}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.alert{position:relative;padding:1rem 1rem;margin-bottom:1rem;border:1px solid transparent;border-radius:.25rem}.alert-heading{color:inherit}.alert-link{font-weight:700}.alert-dismissible{padding-right:3rem}.alert-dismissible .btn-close{position:absolute;top:0;right:0;z-index:2;padding:1.25rem 1rem}.alert-primary{color:#084298;background-color:#cfe2ff;border-color:#b6d4fe}.alert-primary .alert-link{color:#06357a}.alert-secondary{color:#41464b;background-color:#e2e3e5;border-color:#d3d6d8}.alert-secondary .alert-link{color:#34383c}.alert-success{color:#0f5132;background-color:#d1e7dd;border-color:#badbcc}.alert-success .alert-link{color:#0c4128}.alert-info{color:#055160;background-color:#cff4fc;border-color:#b6effb}.alert-info .alert-link{color:#04414d}.alert-warning{color:#664d03;background-color:#fff3cd;border-color:#ffecb5}.alert-warning .alert-link{color:#523e02}.alert-danger{color:#842029;background-color:#f8d7da;border-color:#f5c2c7}.alert-danger .alert-link{color:#6a1a21}.alert-light{color:#636464;background-color:#fefefe;border-color:#fdfdfe}.alert-light .alert-link{color:#4f5050}.alert-dark{color:#141619;background-color:#d3d3d4;border-color:#bcbebf}.alert-dark .alert-link{color:#101214}@-webkit-keyframes progress-bar-stripes{0%{background-position-x:1rem}}@keyframes progress-bar-stripes{0%{background-position-x:1rem}}.progress{display:flex;height:1rem;overflow:hidden;font-size:.75rem;background-color:#e9ecef;border-radius:.25rem}.progress-bar{display:flex;flex-direction:column;justify-content:center;overflow:hidden;color:#fff;text-align:center;white-space:nowrap;background-color:#0d6efd;transition:width .6s ease}@media (prefers-reduced-motion:reduce){.progress-bar{transition:none}}.progress-bar-striped{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-size:1rem 1rem}.progress-bar-animated{-webkit-animation:1s linear infinite progress-bar-stripes;animation:1s linear infinite progress-bar-stripes}@media (prefers-reduced-motion:reduce){.progress-bar-animated{-webkit-animation:none;animation:none}}.list-group{display:flex;flex-direction:column;padding-left:0;margin-bottom:0;border-radius:.25rem}.list-group-numbered{list-style-type:none;counter-reset:section}.list-group-numbered>li::before{content:counters(section, ".") ". ";counter-increment:section}.list-group-item-action{width:100%;color:#495057;text-align:inherit}.list-group-item-action:focus,.list-group-item-action:hover{z-index:1;color:#495057;text-decoration:none;background-color:#f8f9fa}.list-group-item-action:active{color:#212529;background-color:#e9ecef}.list-group-item{position:relative;display:block;padding:.5rem 1rem;color:#212529;text-decoration:none;background-color:#fff;border:1px solid rgba(0,0,0,.125)}.list-group-item:first-child{border-top-left-radius:inherit;border-top-right-radius:inherit}.list-group-item:last-child{border-bottom-right-radius:inherit;border-bottom-left-radius:inherit}.list-group-item.disabled,.list-group-item:disabled{color:#6c757d;pointer-events:none;background-color:#fff}.list-group-item.active{z-index:2;color:#fff;background-color:#0d6efd;border-color:#0d6efd}.list-group-item+.list-group-item{border-top-width:0}.list-group-item+.list-group-item.active{margin-top:-1px;border-top-width:1px}.list-group-horizontal{flex-direction:row}.list-group-horizontal>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal>.list-group-item.active{margin-top:0}.list-group-horizontal>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}@media (min-width:576px){.list-group-horizontal-sm{flex-direction:row}.list-group-horizontal-sm>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-sm>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-sm>.list-group-item.active{margin-top:0}.list-group-horizontal-sm>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-sm>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:768px){.list-group-horizontal-md{flex-direction:row}.list-group-horizontal-md>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-md>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-md>.list-group-item.active{margin-top:0}.list-group-horizontal-md>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-md>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:992px){.list-group-horizontal-lg{flex-direction:row}.list-group-horizontal-lg>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-lg>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-lg>.list-group-item.active{margin-top:0}.list-group-horizontal-lg>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-lg>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:1200px){.list-group-horizontal-xl{flex-direction:row}.list-group-horizontal-xl>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-xl>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-xl>.list-group-item.active{margin-top:0}.list-group-horizontal-xl>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-xl>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:1400px){.list-group-horizontal-xxl{flex-direction:row}.list-group-horizontal-xxl>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-xxl>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-xxl>.list-group-item.active{margin-top:0}.list-group-horizontal-xxl>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-xxl>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}.list-group-flush{border-radius:0}.list-group-flush>.list-group-item{border-width:0 0 1px}.list-group-flush>.list-group-item:last-child{border-bottom-width:0}.list-group-item-primary{color:#084298;background-color:#cfe2ff}.list-group-item-primary.list-group-item-action:focus,.list-group-item-primary.list-group-item-action:hover{color:#084298;background-color:#bacbe6}.list-group-item-primary.list-group-item-action.active{color:#fff;background-color:#084298;border-color:#084298}.list-group-item-secondary{color:#41464b;background-color:#e2e3e5}.list-group-item-secondary.list-group-item-action:focus,.list-group-item-secondary.list-group-item-action:hover{color:#41464b;background-color:#cbccce}.list-group-item-secondary.list-group-item-action.active{color:#fff;background-color:#41464b;border-color:#41464b}.list-group-item-success{color:#0f5132;background-color:#d1e7dd}.list-group-item-success.list-group-item-action:focus,.list-group-item-success.list-group-item-action:hover{color:#0f5132;background-color:#bcd0c7}.list-group-item-success.list-group-item-action.active{color:#fff;background-color:#0f5132;border-color:#0f5132}.list-group-item-info{color:#055160;background-color:#cff4fc}.list-group-item-info.list-group-item-action:focus,.list-group-item-info.list-group-item-action:hover{color:#055160;background-color:#badce3}.list-group-item-info.list-group-item-action.active{color:#fff;background-color:#055160;border-color:#055160}.list-group-item-warning{color:#664d03;background-color:#fff3cd}.list-group-item-warning.list-group-item-action:focus,.list-group-item-warning.list-group-item-action:hover{color:#664d03;background-color:#e6dbb9}.list-group-item-warning.list-group-item-action.active{color:#fff;background-color:#664d03;border-color:#664d03}.list-group-item-danger{color:#842029;background-color:#f8d7da}.list-group-item-danger.list-group-item-action:focus,.list-group-item-danger.list-group-item-action:hover{color:#842029;background-color:#dfc2c4}.list-group-item-danger.list-group-item-action.active{color:#fff;background-color:#842029;border-color:#842029}.list-group-item-light{color:#636464;background-color:#fefefe}.list-group-item-light.list-group-item-action:focus,.list-group-item-light.list-group-item-action:hover{color:#636464;background-color:#e5e5e5}.list-group-item-light.list-group-item-action.active{color:#fff;background-color:#636464;border-color:#636464}.list-group-item-dark{color:#141619;background-color:#d3d3d4}.list-group-item-dark.list-group-item-action:focus,.list-group-item-dark.list-group-item-action:hover{color:#141619;background-color:#bebebf}.list-group-item-dark.list-group-item-action.active{color:#fff;background-color:#141619;border-color:#141619}.btn-close{box-sizing:content-box;width:1em;height:1em;padding:.25em .25em;color:#000;background:transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'%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") center/1em auto no-repeat;border:0;border-radius:.25rem;opacity:.5}.btn-close:hover{color:#000;text-decoration:none;opacity:.75}.btn-close:focus{outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25);opacity:1}.btn-close.disabled,.btn-close:disabled{pointer-events:none;-webkit-user-select:none;-moz-user-select:none;user-select:none;opacity:.25}.btn-close-white{filter:invert(1) grayscale(100%) brightness(200%)}.toast{width:350px;max-width:100%;font-size:.875rem;pointer-events:auto;background-color:rgba(255,255,255,.85);background-clip:padding-box;border:1px solid rgba(0,0,0,.1);box-shadow:0 .5rem 1rem rgba(0,0,0,.15);border-radius:.25rem}.toast.showing{opacity:0}.toast:not(.show){display:none}.toast-container{width:-webkit-max-content;width:-moz-max-content;width:max-content;max-width:100%;pointer-events:none}.toast-container>:not(:last-child){margin-bottom:.75rem}.toast-header{display:flex;align-items:center;padding:.5rem .75rem;color:#6c757d;background-color:rgba(255,255,255,.85);background-clip:padding-box;border-bottom:1px solid rgba(0,0,0,.05);border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.toast-header .btn-close{margin-right:-.375rem;margin-left:.75rem}.toast-body{padding:.75rem;word-wrap:break-word}.modal{position:fixed;top:0;left:0;z-index:1055;display:none;width:100%;height:100%;overflow-x:hidden;overflow-y:auto;outline:0}.modal-dialog{position:relative;width:auto;margin:.5rem;pointer-events:none}.modal.fade .modal-dialog{transition:transform .3s ease-out;transform:translate(0,-50px)}@media (prefers-reduced-motion:reduce){.modal.fade .modal-dialog{transition:none}}.modal.show .modal-dialog{transform:none}.modal.modal-static .modal-dialog{transform:scale(1.02)}.modal-dialog-scrollable{height:calc(100% - 1rem)}.modal-dialog-scrollable .modal-content{max-height:100%;overflow:hidden}.modal-dialog-scrollable .modal-body{overflow-y:auto}.modal-dialog-centered{display:flex;align-items:center;min-height:calc(100% - 1rem)}.modal-content{position:relative;display:flex;flex-direction:column;width:100%;pointer-events:auto;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem;outline:0}.modal-backdrop{position:fixed;top:0;left:0;z-index:1050;width:100vw;height:100vh;background-color:#000}.modal-backdrop.fade{opacity:0}.modal-backdrop.show{opacity:.5}.modal-header{display:flex;flex-shrink:0;align-items:center;justify-content:space-between;padding:1rem 1rem;border-bottom:1px solid #dee2e6;border-top-left-radius:calc(.3rem - 1px);border-top-right-radius:calc(.3rem - 1px)}.modal-header .btn-close{padding:.5rem .5rem;margin:-.5rem -.5rem -.5rem auto}.modal-title{margin-bottom:0;line-height:1.5}.modal-body{position:relative;flex:1 1 auto;padding:1rem}.modal-footer{display:flex;flex-wrap:wrap;flex-shrink:0;align-items:center;justify-content:flex-end;padding:.75rem;border-top:1px solid #dee2e6;border-bottom-right-radius:calc(.3rem - 1px);border-bottom-left-radius:calc(.3rem - 1px)}.modal-footer>*{margin:.25rem}@media (min-width:576px){.modal-dialog{max-width:500px;margin:1.75rem auto}.modal-dialog-scrollable{height:calc(100% - 3.5rem)}.modal-dialog-centered{min-height:calc(100% - 3.5rem)}.modal-sm{max-width:300px}}@media (min-width:992px){.modal-lg,.modal-xl{max-width:800px}}@media (min-width:1200px){.modal-xl{max-width:1140px}}.modal-fullscreen{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen .modal-header{border-radius:0}.modal-fullscreen .modal-body{overflow-y:auto}.modal-fullscreen .modal-footer{border-radius:0}@media (max-width:575.98px){.modal-fullscreen-sm-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-sm-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-sm-down .modal-header{border-radius:0}.modal-fullscreen-sm-down .modal-body{overflow-y:auto}.modal-fullscreen-sm-down .modal-footer{border-radius:0}}@media (max-width:767.98px){.modal-fullscreen-md-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-md-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-md-down .modal-header{border-radius:0}.modal-fullscreen-md-down .modal-body{overflow-y:auto}.modal-fullscreen-md-down .modal-footer{border-radius:0}}@media (max-width:991.98px){.modal-fullscreen-lg-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-lg-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-lg-down .modal-header{border-radius:0}.modal-fullscreen-lg-down .modal-body{overflow-y:auto}.modal-fullscreen-lg-down .modal-footer{border-radius:0}}@media (max-width:1199.98px){.modal-fullscreen-xl-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-xl-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-xl-down .modal-header{border-radius:0}.modal-fullscreen-xl-down .modal-body{overflow-y:auto}.modal-fullscreen-xl-down .modal-footer{border-radius:0}}@media (max-width:1399.98px){.modal-fullscreen-xxl-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-xxl-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-xxl-down .modal-header{border-radius:0}.modal-fullscreen-xxl-down .modal-body{overflow-y:auto}.modal-fullscreen-xxl-down .modal-footer{border-radius:0}}.tooltip{position:absolute;z-index:1080;display:block;margin:0;font-family:var(--bs-font-sans-serif);font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;opacity:0}.tooltip.show{opacity:.9}.tooltip .tooltip-arrow{position:absolute;display:block;width:.8rem;height:.4rem}.tooltip .tooltip-arrow::before{position:absolute;content:"";border-color:transparent;border-style:solid}.bs-tooltip-auto[data-popper-placement^=top],.bs-tooltip-top{padding:.4rem 0}.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow,.bs-tooltip-top .tooltip-arrow{bottom:0}.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow::before,.bs-tooltip-top .tooltip-arrow::before{top:-1px;border-width:.4rem .4rem 0;border-top-color:#000}.bs-tooltip-auto[data-popper-placement^=right],.bs-tooltip-end{padding:0 .4rem}.bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow,.bs-tooltip-end .tooltip-arrow{left:0;width:.4rem;height:.8rem}.bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow::before,.bs-tooltip-end .tooltip-arrow::before{right:-1px;border-width:.4rem .4rem .4rem 0;border-right-color:#000}.bs-tooltip-auto[data-popper-placement^=bottom],.bs-tooltip-bottom{padding:.4rem 0}.bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow,.bs-tooltip-bottom .tooltip-arrow{top:0}.bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow::before,.bs-tooltip-bottom .tooltip-arrow::before{bottom:-1px;border-width:0 .4rem .4rem;border-bottom-color:#000}.bs-tooltip-auto[data-popper-placement^=left],.bs-tooltip-start{padding:0 .4rem}.bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow,.bs-tooltip-start .tooltip-arrow{right:0;width:.4rem;height:.8rem}.bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow::before,.bs-tooltip-start .tooltip-arrow::before{left:-1px;border-width:.4rem 0 .4rem .4rem;border-left-color:#000}.tooltip-inner{max-width:200px;padding:.25rem .5rem;color:#fff;text-align:center;background-color:#000;border-radius:.25rem}.popover{position:absolute;top:0;left:0;z-index:1070;display:block;max-width:276px;font-family:var(--bs-font-sans-serif);font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem}.popover .popover-arrow{position:absolute;display:block;width:1rem;height:.5rem}.popover .popover-arrow::after,.popover .popover-arrow::before{position:absolute;display:block;content:"";border-color:transparent;border-style:solid}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow,.bs-popover-top>.popover-arrow{bottom:calc(-.5rem - 1px)}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::before,.bs-popover-top>.popover-arrow::before{bottom:0;border-width:.5rem .5rem 0;border-top-color:rgba(0,0,0,.25)}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::after,.bs-popover-top>.popover-arrow::after{bottom:1px;border-width:.5rem .5rem 0;border-top-color:#fff}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow,.bs-popover-end>.popover-arrow{left:calc(-.5rem - 1px);width:.5rem;height:1rem}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::before,.bs-popover-end>.popover-arrow::before{left:0;border-width:.5rem .5rem .5rem 0;border-right-color:rgba(0,0,0,.25)}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::after,.bs-popover-end>.popover-arrow::after{left:1px;border-width:.5rem .5rem .5rem 0;border-right-color:#fff}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow,.bs-popover-bottom>.popover-arrow{top:calc(-.5rem - 1px)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::before,.bs-popover-bottom>.popover-arrow::before{top:0;border-width:0 .5rem .5rem .5rem;border-bottom-color:rgba(0,0,0,.25)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::after,.bs-popover-bottom>.popover-arrow::after{top:1px;border-width:0 .5rem .5rem .5rem;border-bottom-color:#fff}.bs-popover-auto[data-popper-placement^=bottom] .popover-header::before,.bs-popover-bottom .popover-header::before{position:absolute;top:0;left:50%;display:block;width:1rem;margin-left:-.5rem;content:"";border-bottom:1px solid #f0f0f0}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow,.bs-popover-start>.popover-arrow{right:calc(-.5rem - 1px);width:.5rem;height:1rem}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::before,.bs-popover-start>.popover-arrow::before{right:0;border-width:.5rem 0 .5rem .5rem;border-left-color:rgba(0,0,0,.25)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::after,.bs-popover-start>.popover-arrow::after{right:1px;border-width:.5rem 0 .5rem .5rem;border-left-color:#fff}.popover-header{padding:.5rem 1rem;margin-bottom:0;font-size:1rem;background-color:#f0f0f0;border-bottom:1px solid rgba(0,0,0,.2);border-top-left-radius:calc(.3rem - 1px);border-top-right-radius:calc(.3rem - 1px)}.popover-header:empty{display:none}.popover-body{padding:1rem 1rem;color:#212529}.carousel{position:relative}.carousel.pointer-event{touch-action:pan-y}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner::after{display:block;clear:both;content:""}.carousel-item{position:relative;display:none;float:left;width:100%;margin-right:-100%;-webkit-backface-visibility:hidden;backface-visibility:hidden;transition:transform .6s ease-in-out}@media (prefers-reduced-motion:reduce){.carousel-item{transition:none}}.carousel-item-next,.carousel-item-prev,.carousel-item.active{display:block}.active.carousel-item-end,.carousel-item-next:not(.carousel-item-start){transform:translateX(100%)}.active.carousel-item-start,.carousel-item-prev:not(.carousel-item-end){transform:translateX(-100%)}.carousel-fade .carousel-item{opacity:0;transition-property:opacity;transform:none}.carousel-fade .carousel-item-next.carousel-item-start,.carousel-fade .carousel-item-prev.carousel-item-end,.carousel-fade .carousel-item.active{z-index:1;opacity:1}.carousel-fade .active.carousel-item-end,.carousel-fade .active.carousel-item-start{z-index:0;opacity:0;transition:opacity 0s .6s}@media (prefers-reduced-motion:reduce){.carousel-fade .active.carousel-item-end,.carousel-fade .active.carousel-item-start{transition:none}}.carousel-control-next,.carousel-control-prev{position:absolute;top:0;bottom:0;z-index:1;display:flex;align-items:center;justify-content:center;width:15%;padding:0;color:#fff;text-align:center;background:0 0;border:0;opacity:.5;transition:opacity .15s ease}@media (prefers-reduced-motion:reduce){.carousel-control-next,.carousel-control-prev{transition:none}}.carousel-control-next:focus,.carousel-control-next:hover,.carousel-control-prev:focus,.carousel-control-prev:hover{color:#fff;text-decoration:none;outline:0;opacity:.9}.carousel-control-prev{left:0}.carousel-control-next{right:0}.carousel-control-next-icon,.carousel-control-prev-icon{display:inline-block;width:2rem;height:2rem;background-repeat:no-repeat;background-position:50%;background-size:100% 100%}.carousel-control-prev-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z'/%3e%3c/svg%3e")}.carousel-control-next-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e")}.carousel-indicators{position:absolute;right:0;bottom:0;left:0;z-index:2;display:flex;justify-content:center;padding:0;margin-right:15%;margin-bottom:1rem;margin-left:15%;list-style:none}.carousel-indicators [data-bs-target]{box-sizing:content-box;flex:0 1 auto;width:30px;height:3px;padding:0;margin-right:3px;margin-left:3px;text-indent:-999px;cursor:pointer;background-color:#fff;background-clip:padding-box;border:0;border-top:10px solid transparent;border-bottom:10px solid transparent;opacity:.5;transition:opacity .6s ease}@media (prefers-reduced-motion:reduce){.carousel-indicators [data-bs-target]{transition:none}}.carousel-indicators .active{opacity:1}.carousel-caption{position:absolute;right:15%;bottom:1.25rem;left:15%;padding-top:1.25rem;padding-bottom:1.25rem;color:#fff;text-align:center}.carousel-dark .carousel-control-next-icon,.carousel-dark .carousel-control-prev-icon{filter:invert(1) grayscale(100)}.carousel-dark .carousel-indicators [data-bs-target]{background-color:#000}.carousel-dark .carousel-caption{color:#000}@-webkit-keyframes spinner-border{to{transform:rotate(360deg)}}@keyframes spinner-border{to{transform:rotate(360deg)}}.spinner-border{display:inline-block;width:2rem;height:2rem;vertical-align:-.125em;border:.25em solid currentColor;border-right-color:transparent;border-radius:50%;-webkit-animation:.75s linear infinite spinner-border;animation:.75s linear infinite spinner-border}.spinner-border-sm{width:1rem;height:1rem;border-width:.2em}@-webkit-keyframes spinner-grow{0%{transform:scale(0)}50%{opacity:1;transform:none}}@keyframes spinner-grow{0%{transform:scale(0)}50%{opacity:1;transform:none}}.spinner-grow{display:inline-block;width:2rem;height:2rem;vertical-align:-.125em;background-color:currentColor;border-radius:50%;opacity:0;-webkit-animation:.75s linear infinite spinner-grow;animation:.75s linear infinite spinner-grow}.spinner-grow-sm{width:1rem;height:1rem}@media (prefers-reduced-motion:reduce){.spinner-border,.spinner-grow{-webkit-animation-duration:1.5s;animation-duration:1.5s}}.offcanvas{position:fixed;bottom:0;z-index:1045;display:flex;flex-direction:column;max-width:100%;visibility:hidden;background-color:#fff;background-clip:padding-box;outline:0;transition:transform .3s ease-in-out}@media (prefers-reduced-motion:reduce){.offcanvas{transition:none}}.offcanvas-backdrop{position:fixed;top:0;left:0;z-index:1040;width:100vw;height:100vh;background-color:#000}.offcanvas-backdrop.fade{opacity:0}.offcanvas-backdrop.show{opacity:.5}.offcanvas-header{display:flex;align-items:center;justify-content:space-between;padding:1rem 1rem}.offcanvas-header .btn-close{padding:.5rem .5rem;margin-top:-.5rem;margin-right:-.5rem;margin-bottom:-.5rem}.offcanvas-title{margin-bottom:0;line-height:1.5}.offcanvas-body{flex-grow:1;padding:1rem 1rem;overflow-y:auto}.offcanvas-start{top:0;left:0;width:400px;border-right:1px solid rgba(0,0,0,.2);transform:translateX(-100%)}.offcanvas-end{top:0;right:0;width:400px;border-left:1px solid rgba(0,0,0,.2);transform:translateX(100%)}.offcanvas-top{top:0;right:0;left:0;height:30vh;max-height:100%;border-bottom:1px solid rgba(0,0,0,.2);transform:translateY(-100%)}.offcanvas-bottom{right:0;left:0;height:30vh;max-height:100%;border-top:1px solid rgba(0,0,0,.2);transform:translateY(100%)}.offcanvas.show{transform:none}.placeholder{display:inline-block;min-height:1em;vertical-align:middle;cursor:wait;background-color:currentColor;opacity:.5}.placeholder.btn::before{display:inline-block;content:""}.placeholder-xs{min-height:.6em}.placeholder-sm{min-height:.8em}.placeholder-lg{min-height:1.2em}.placeholder-glow .placeholder{-webkit-animation:placeholder-glow 2s ease-in-out infinite;animation:placeholder-glow 2s ease-in-out infinite}@-webkit-keyframes placeholder-glow{50%{opacity:.2}}@keyframes placeholder-glow{50%{opacity:.2}}.placeholder-wave{-webkit-mask-image:linear-gradient(130deg,#000 55%,rgba(0,0,0,0.8) 75%,#000 95%);mask-image:linear-gradient(130deg,#000 55%,rgba(0,0,0,0.8) 75%,#000 95%);-webkit-mask-size:200% 100%;mask-size:200% 100%;-webkit-animation:placeholder-wave 2s linear infinite;animation:placeholder-wave 2s linear infinite}@-webkit-keyframes placeholder-wave{100%{-webkit-mask-position:-200% 0%;mask-position:-200% 0%}}@keyframes placeholder-wave{100%{-webkit-mask-position:-200% 0%;mask-position:-200% 0%}}.clearfix::after{display:block;clear:both;content:""}.link-primary{color:#0d6efd}.link-primary:focus,.link-primary:hover{color:#0a58ca}.link-secondary{color:#6c757d}.link-secondary:focus,.link-secondary:hover{color:#565e64}.link-success{color:#198754}.link-success:focus,.link-success:hover{color:#146c43}.link-info{color:#0dcaf0}.link-info:focus,.link-info:hover{color:#3dd5f3}.link-warning{color:#ffc107}.link-warning:focus,.link-warning:hover{color:#ffcd39}.link-danger{color:#dc3545}.link-danger:focus,.link-danger:hover{color:#b02a37}.link-light{color:#f8f9fa}.link-light:focus,.link-light:hover{color:#f9fafb}.link-dark{color:#212529}.link-dark:focus,.link-dark:hover{color:#1a1e21}.ratio{position:relative;width:100%}.ratio::before{display:block;padding-top:var(--bs-aspect-ratio);content:""}.ratio>*{position:absolute;top:0;left:0;width:100%;height:100%}.ratio-1x1{--bs-aspect-ratio:100%}.ratio-4x3{--bs-aspect-ratio:75%}.ratio-16x9{--bs-aspect-ratio:56.25%}.ratio-21x9{--bs-aspect-ratio:42.8571428571%}.fixed-top{position:fixed;top:0;right:0;left:0;z-index:1030}.fixed-bottom{position:fixed;right:0;bottom:0;left:0;z-index:1030}.sticky-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}@media (min-width:576px){.sticky-sm-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}@media (min-width:768px){.sticky-md-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}@media (min-width:992px){.sticky-lg-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}@media (min-width:1200px){.sticky-xl-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}@media (min-width:1400px){.sticky-xxl-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}.hstack{display:flex;flex-direction:row;align-items:center;align-self:stretch}.vstack{display:flex;flex:1 1 auto;flex-direction:column;align-self:stretch}.visually-hidden,.visually-hidden-focusable:not(:focus):not(:focus-within){position:absolute!important;width:1px!important;height:1px!important;padding:0!important;margin:-1px!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;white-space:nowrap!important;border:0!important}.stretched-link::after{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;content:""}.text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.vr{display:inline-block;align-self:stretch;width:1px;min-height:1em;background-color:currentColor;opacity:.25}.align-baseline{vertical-align:baseline!important}.align-top{vertical-align:top!important}.align-middle{vertical-align:middle!important}.align-bottom{vertical-align:bottom!important}.align-text-bottom{vertical-align:text-bottom!important}.align-text-top{vertical-align:text-top!important}.float-start{float:left!important}.float-end{float:right!important}.float-none{float:none!important}.opacity-0{opacity:0!important}.opacity-25{opacity:.25!important}.opacity-50{opacity:.5!important}.opacity-75{opacity:.75!important}.opacity-100{opacity:1!important}.overflow-auto{overflow:auto!important}.overflow-hidden{overflow:hidden!important}.overflow-visible{overflow:visible!important}.overflow-scroll{overflow:scroll!important}.d-inline{display:inline!important}.d-inline-block{display:inline-block!important}.d-block{display:block!important}.d-grid{display:grid!important}.d-table{display:table!important}.d-table-row{display:table-row!important}.d-table-cell{display:table-cell!important}.d-flex{display:flex!important}.d-inline-flex{display:inline-flex!important}.d-none{display:none!important}.shadow{box-shadow:0 .5rem 1rem rgba(0,0,0,.15)!important}.shadow-sm{box-shadow:0 .125rem .25rem rgba(0,0,0,.075)!important}.shadow-lg{box-shadow:0 1rem 3rem rgba(0,0,0,.175)!important}.shadow-none{box-shadow:none!important}.position-static{position:static!important}.position-relative{position:relative!important}.position-absolute{position:absolute!important}.position-fixed{position:fixed!important}.position-sticky{position:-webkit-sticky!important;position:sticky!important}.top-0{top:0!important}.top-50{top:50%!important}.top-100{top:100%!important}.bottom-0{bottom:0!important}.bottom-50{bottom:50%!important}.bottom-100{bottom:100%!important}.start-0{left:0!important}.start-50{left:50%!important}.start-100{left:100%!important}.end-0{right:0!important}.end-50{right:50%!important}.end-100{right:100%!important}.translate-middle{transform:translate(-50%,-50%)!important}.translate-middle-x{transform:translateX(-50%)!important}.translate-middle-y{transform:translateY(-50%)!important}.border{border:1px solid #dee2e6!important}.border-0{border:0!important}.border-top{border-top:1px solid #dee2e6!important}.border-top-0{border-top:0!important}.border-end{border-right:1px solid #dee2e6!important}.border-end-0{border-right:0!important}.border-bottom{border-bottom:1px solid #dee2e6!important}.border-bottom-0{border-bottom:0!important}.border-start{border-left:1px solid #dee2e6!important}.border-start-0{border-left:0!important}.border-primary{border-color:#0d6efd!important}.border-secondary{border-color:#6c757d!important}.border-success{border-color:#198754!important}.border-info{border-color:#0dcaf0!important}.border-warning{border-color:#ffc107!important}.border-danger{border-color:#dc3545!important}.border-light{border-color:#f8f9fa!important}.border-dark{border-color:#212529!important}.border-white{border-color:#fff!important}.border-1{border-width:1px!important}.border-2{border-width:2px!important}.border-3{border-width:3px!important}.border-4{border-width:4px!important}.border-5{border-width:5px!important}.w-25{width:25%!important}.w-50{width:50%!important}.w-75{width:75%!important}.w-100{width:100%!important}.w-auto{width:auto!important}.mw-100{max-width:100%!important}.vw-100{width:100vw!important}.min-vw-100{min-width:100vw!important}.h-25{height:25%!important}.h-50{height:50%!important}.h-75{height:75%!important}.h-100{height:100%!important}.h-auto{height:auto!important}.mh-100{max-height:100%!important}.vh-100{height:100vh!important}.min-vh-100{min-height:100vh!important}.flex-fill{flex:1 1 auto!important}.flex-row{flex-direction:row!important}.flex-column{flex-direction:column!important}.flex-row-reverse{flex-direction:row-reverse!important}.flex-column-reverse{flex-direction:column-reverse!important}.flex-grow-0{flex-grow:0!important}.flex-grow-1{flex-grow:1!important}.flex-shrink-0{flex-shrink:0!important}.flex-shrink-1{flex-shrink:1!important}.flex-wrap{flex-wrap:wrap!important}.flex-nowrap{flex-wrap:nowrap!important}.flex-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-0{gap:0!important}.gap-1{gap:.25rem!important}.gap-2{gap:.5rem!important}.gap-3{gap:1rem!important}.gap-4{gap:1.5rem!important}.gap-5{gap:3rem!important}.justify-content-start{justify-content:flex-start!important}.justify-content-end{justify-content:flex-end!important}.justify-content-center{justify-content:center!important}.justify-content-between{justify-content:space-between!important}.justify-content-around{justify-content:space-around!important}.justify-content-evenly{justify-content:space-evenly!important}.align-items-start{align-items:flex-start!important}.align-items-end{align-items:flex-end!important}.align-items-center{align-items:center!important}.align-items-baseline{align-items:baseline!important}.align-items-stretch{align-items:stretch!important}.align-content-start{align-content:flex-start!important}.align-content-end{align-content:flex-end!important}.align-content-center{align-content:center!important}.align-content-between{align-content:space-between!important}.align-content-around{align-content:space-around!important}.align-content-stretch{align-content:stretch!important}.align-self-auto{align-self:auto!important}.align-self-start{align-self:flex-start!important}.align-self-end{align-self:flex-end!important}.align-self-center{align-self:center!important}.align-self-baseline{align-self:baseline!important}.align-self-stretch{align-self:stretch!important}.order-first{order:-1!important}.order-0{order:0!important}.order-1{order:1!important}.order-2{order:2!important}.order-3{order:3!important}.order-4{order:4!important}.order-5{order:5!important}.order-last{order:6!important}.m-0{margin:0!important}.m-1{margin:.25rem!important}.m-2{margin:.5rem!important}.m-3{margin:1rem!important}.m-4{margin:1.5rem!important}.m-5{margin:3rem!important}.m-auto{margin:auto!important}.mx-0{margin-right:0!important;margin-left:0!important}.mx-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-3{margin-right:1rem!important;margin-left:1rem!important}.mx-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-5{margin-right:3rem!important;margin-left:3rem!important}.mx-auto{margin-right:auto!important;margin-left:auto!important}.my-0{margin-top:0!important;margin-bottom:0!important}.my-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-0{margin-top:0!important}.mt-1{margin-top:.25rem!important}.mt-2{margin-top:.5rem!important}.mt-3{margin-top:1rem!important}.mt-4{margin-top:1.5rem!important}.mt-5{margin-top:3rem!important}.mt-auto{margin-top:auto!important}.me-0{margin-right:0!important}.me-1{margin-right:.25rem!important}.me-2{margin-right:.5rem!important}.me-3{margin-right:1rem!important}.me-4{margin-right:1.5rem!important}.me-5{margin-right:3rem!important}.me-auto{margin-right:auto!important}.mb-0{margin-bottom:0!important}.mb-1{margin-bottom:.25rem!important}.mb-2{margin-bottom:.5rem!important}.mb-3{margin-bottom:1rem!important}.mb-4{margin-bottom:1.5rem!important}.mb-5{margin-bottom:3rem!important}.mb-auto{margin-bottom:auto!important}.ms-0{margin-left:0!important}.ms-1{margin-left:.25rem!important}.ms-2{margin-left:.5rem!important}.ms-3{margin-left:1rem!important}.ms-4{margin-left:1.5rem!important}.ms-5{margin-left:3rem!important}.ms-auto{margin-left:auto!important}.p-0{padding:0!important}.p-1{padding:.25rem!important}.p-2{padding:.5rem!important}.p-3{padding:1rem!important}.p-4{padding:1.5rem!important}.p-5{padding:3rem!important}.px-0{padding-right:0!important;padding-left:0!important}.px-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-3{padding-right:1rem!important;padding-left:1rem!important}.px-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-5{padding-right:3rem!important;padding-left:3rem!important}.py-0{padding-top:0!important;padding-bottom:0!important}.py-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-0{padding-top:0!important}.pt-1{padding-top:.25rem!important}.pt-2{padding-top:.5rem!important}.pt-3{padding-top:1rem!important}.pt-4{padding-top:1.5rem!important}.pt-5{padding-top:3rem!important}.pe-0{padding-right:0!important}.pe-1{padding-right:.25rem!important}.pe-2{padding-right:.5rem!important}.pe-3{padding-right:1rem!important}.pe-4{padding-right:1.5rem!important}.pe-5{padding-right:3rem!important}.pb-0{padding-bottom:0!important}.pb-1{padding-bottom:.25rem!important}.pb-2{padding-bottom:.5rem!important}.pb-3{padding-bottom:1rem!important}.pb-4{padding-bottom:1.5rem!important}.pb-5{padding-bottom:3rem!important}.ps-0{padding-left:0!important}.ps-1{padding-left:.25rem!important}.ps-2{padding-left:.5rem!important}.ps-3{padding-left:1rem!important}.ps-4{padding-left:1.5rem!important}.ps-5{padding-left:3rem!important}.font-monospace{font-family:var(--bs-font-monospace)!important}.fs-1{font-size:calc(1.375rem + 1.5vw)!important}.fs-2{font-size:calc(1.325rem + .9vw)!important}.fs-3{font-size:calc(1.3rem + .6vw)!important}.fs-4{font-size:calc(1.275rem + .3vw)!important}.fs-5{font-size:1.25rem!important}.fs-6{font-size:1rem!important}.fst-italic{font-style:italic!important}.fst-normal{font-style:normal!important}.fw-light{font-weight:300!important}.fw-lighter{font-weight:lighter!important}.fw-normal{font-weight:400!important}.fw-bold{font-weight:700!important}.fw-bolder{font-weight:bolder!important}.lh-1{line-height:1!important}.lh-sm{line-height:1.25!important}.lh-base{line-height:1.5!important}.lh-lg{line-height:2!important}.text-start{text-align:left!important}.text-end{text-align:right!important}.text-center{text-align:center!important}.text-decoration-none{text-decoration:none!important}.text-decoration-underline{text-decoration:underline!important}.text-decoration-line-through{text-decoration:line-through!important}.text-lowercase{text-transform:lowercase!important}.text-uppercase{text-transform:uppercase!important}.text-capitalize{text-transform:capitalize!important}.text-wrap{white-space:normal!important}.text-nowrap{white-space:nowrap!important}.text-break{word-wrap:break-word!important;word-break:break-word!important}.text-primary{--bs-text-opacity:1;color:rgba(var(--bs-primary-rgb),var(--bs-text-opacity))!important}.text-secondary{--bs-text-opacity:1;color:rgba(var(--bs-secondary-rgb),var(--bs-text-opacity))!important}.text-success{--bs-text-opacity:1;color:rgba(var(--bs-success-rgb),var(--bs-text-opacity))!important}.text-info{--bs-text-opacity:1;color:rgba(var(--bs-info-rgb),var(--bs-text-opacity))!important}.text-warning{--bs-text-opacity:1;color:rgba(var(--bs-warning-rgb),var(--bs-text-opacity))!important}.text-danger{--bs-text-opacity:1;color:rgba(var(--bs-danger-rgb),var(--bs-text-opacity))!important}.text-light{--bs-text-opacity:1;color:rgba(var(--bs-light-rgb),var(--bs-text-opacity))!important}.text-dark{--bs-text-opacity:1;color:rgba(var(--bs-dark-rgb),var(--bs-text-opacity))!important}.text-black{--bs-text-opacity:1;color:rgba(var(--bs-black-rgb),var(--bs-text-opacity))!important}.text-white{--bs-text-opacity:1;color:rgba(var(--bs-white-rgb),var(--bs-text-opacity))!important}.text-body{--bs-text-opacity:1;color:rgba(var(--bs-body-color-rgb),var(--bs-text-opacity))!important}.text-muted{--bs-text-opacity:1;color:#6c757d!important}.text-black-50{--bs-text-opacity:1;color:rgba(0,0,0,.5)!important}.text-white-50{--bs-text-opacity:1;color:rgba(255,255,255,.5)!important}.text-reset{--bs-text-opacity:1;color:inherit!important}.text-opacity-25{--bs-text-opacity:0.25}.text-opacity-50{--bs-text-opacity:0.5}.text-opacity-75{--bs-text-opacity:0.75}.text-opacity-100{--bs-text-opacity:1}.bg-primary{--bs-bg-opacity:1;background-color:rgba(var(--bs-primary-rgb),var(--bs-bg-opacity))!important}.bg-secondary{--bs-bg-opacity:1;background-color:rgba(var(--bs-secondary-rgb),var(--bs-bg-opacity))!important}.bg-success{--bs-bg-opacity:1;background-color:rgba(var(--bs-success-rgb),var(--bs-bg-opacity))!important}.bg-info{--bs-bg-opacity:1;background-color:rgba(var(--bs-info-rgb),var(--bs-bg-opacity))!important}.bg-warning{--bs-bg-opacity:1;background-color:rgba(var(--bs-warning-rgb),var(--bs-bg-opacity))!important}.bg-danger{--bs-bg-opacity:1;background-color:rgba(var(--bs-danger-rgb),var(--bs-bg-opacity))!important}.bg-light{--bs-bg-opacity:1;background-color:rgba(var(--bs-light-rgb),var(--bs-bg-opacity))!important}.bg-dark{--bs-bg-opacity:1;background-color:rgba(var(--bs-dark-rgb),var(--bs-bg-opacity))!important}.bg-black{--bs-bg-opacity:1;background-color:rgba(var(--bs-black-rgb),var(--bs-bg-opacity))!important}.bg-white{--bs-bg-opacity:1;background-color:rgba(var(--bs-white-rgb),var(--bs-bg-opacity))!important}.bg-body{--bs-bg-opacity:1;background-color:rgba(var(--bs-body-bg-rgb),var(--bs-bg-opacity))!important}.bg-transparent{--bs-bg-opacity:1;background-color:transparent!important}.bg-opacity-10{--bs-bg-opacity:0.1}.bg-opacity-25{--bs-bg-opacity:0.25}.bg-opacity-50{--bs-bg-opacity:0.5}.bg-opacity-75{--bs-bg-opacity:0.75}.bg-opacity-100{--bs-bg-opacity:1}.bg-gradient{background-image:var(--bs-gradient)!important}.user-select-all{-webkit-user-select:all!important;-moz-user-select:all!important;user-select:all!important}.user-select-auto{-webkit-user-select:auto!important;-moz-user-select:auto!important;user-select:auto!important}.user-select-none{-webkit-user-select:none!important;-moz-user-select:none!important;user-select:none!important}.pe-none{pointer-events:none!important}.pe-auto{pointer-events:auto!important}.rounded{border-radius:.25rem!important}.rounded-0{border-radius:0!important}.rounded-1{border-radius:.2rem!important}.rounded-2{border-radius:.25rem!important}.rounded-3{border-radius:.3rem!important}.rounded-circle{border-radius:50%!important}.rounded-pill{border-radius:50rem!important}.rounded-top{border-top-left-radius:.25rem!important;border-top-right-radius:.25rem!important}.rounded-end{border-top-right-radius:.25rem!important;border-bottom-right-radius:.25rem!important}.rounded-bottom{border-bottom-right-radius:.25rem!important;border-bottom-left-radius:.25rem!important}.rounded-start{border-bottom-left-radius:.25rem!important;border-top-left-radius:.25rem!important}.visible{visibility:visible!important}.invisible{visibility:hidden!important}@media (min-width:576px){.float-sm-start{float:left!important}.float-sm-end{float:right!important}.float-sm-none{float:none!important}.d-sm-inline{display:inline!important}.d-sm-inline-block{display:inline-block!important}.d-sm-block{display:block!important}.d-sm-grid{display:grid!important}.d-sm-table{display:table!important}.d-sm-table-row{display:table-row!important}.d-sm-table-cell{display:table-cell!important}.d-sm-flex{display:flex!important}.d-sm-inline-flex{display:inline-flex!important}.d-sm-none{display:none!important}.flex-sm-fill{flex:1 1 auto!important}.flex-sm-row{flex-direction:row!important}.flex-sm-column{flex-direction:column!important}.flex-sm-row-reverse{flex-direction:row-reverse!important}.flex-sm-column-reverse{flex-direction:column-reverse!important}.flex-sm-grow-0{flex-grow:0!important}.flex-sm-grow-1{flex-grow:1!important}.flex-sm-shrink-0{flex-shrink:0!important}.flex-sm-shrink-1{flex-shrink:1!important}.flex-sm-wrap{flex-wrap:wrap!important}.flex-sm-nowrap{flex-wrap:nowrap!important}.flex-sm-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-sm-0{gap:0!important}.gap-sm-1{gap:.25rem!important}.gap-sm-2{gap:.5rem!important}.gap-sm-3{gap:1rem!important}.gap-sm-4{gap:1.5rem!important}.gap-sm-5{gap:3rem!important}.justify-content-sm-start{justify-content:flex-start!important}.justify-content-sm-end{justify-content:flex-end!important}.justify-content-sm-center{justify-content:center!important}.justify-content-sm-between{justify-content:space-between!important}.justify-content-sm-around{justify-content:space-around!important}.justify-content-sm-evenly{justify-content:space-evenly!important}.align-items-sm-start{align-items:flex-start!important}.align-items-sm-end{align-items:flex-end!important}.align-items-sm-center{align-items:center!important}.align-items-sm-baseline{align-items:baseline!important}.align-items-sm-stretch{align-items:stretch!important}.align-content-sm-start{align-content:flex-start!important}.align-content-sm-end{align-content:flex-end!important}.align-content-sm-center{align-content:center!important}.align-content-sm-between{align-content:space-between!important}.align-content-sm-around{align-content:space-around!important}.align-content-sm-stretch{align-content:stretch!important}.align-self-sm-auto{align-self:auto!important}.align-self-sm-start{align-self:flex-start!important}.align-self-sm-end{align-self:flex-end!important}.align-self-sm-center{align-self:center!important}.align-self-sm-baseline{align-self:baseline!important}.align-self-sm-stretch{align-self:stretch!important}.order-sm-first{order:-1!important}.order-sm-0{order:0!important}.order-sm-1{order:1!important}.order-sm-2{order:2!important}.order-sm-3{order:3!important}.order-sm-4{order:4!important}.order-sm-5{order:5!important}.order-sm-last{order:6!important}.m-sm-0{margin:0!important}.m-sm-1{margin:.25rem!important}.m-sm-2{margin:.5rem!important}.m-sm-3{margin:1rem!important}.m-sm-4{margin:1.5rem!important}.m-sm-5{margin:3rem!important}.m-sm-auto{margin:auto!important}.mx-sm-0{margin-right:0!important;margin-left:0!important}.mx-sm-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-sm-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-sm-3{margin-right:1rem!important;margin-left:1rem!important}.mx-sm-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-sm-5{margin-right:3rem!important;margin-left:3rem!important}.mx-sm-auto{margin-right:auto!important;margin-left:auto!important}.my-sm-0{margin-top:0!important;margin-bottom:0!important}.my-sm-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-sm-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-sm-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-sm-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-sm-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-sm-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-sm-0{margin-top:0!important}.mt-sm-1{margin-top:.25rem!important}.mt-sm-2{margin-top:.5rem!important}.mt-sm-3{margin-top:1rem!important}.mt-sm-4{margin-top:1.5rem!important}.mt-sm-5{margin-top:3rem!important}.mt-sm-auto{margin-top:auto!important}.me-sm-0{margin-right:0!important}.me-sm-1{margin-right:.25rem!important}.me-sm-2{margin-right:.5rem!important}.me-sm-3{margin-right:1rem!important}.me-sm-4{margin-right:1.5rem!important}.me-sm-5{margin-right:3rem!important}.me-sm-auto{margin-right:auto!important}.mb-sm-0{margin-bottom:0!important}.mb-sm-1{margin-bottom:.25rem!important}.mb-sm-2{margin-bottom:.5rem!important}.mb-sm-3{margin-bottom:1rem!important}.mb-sm-4{margin-bottom:1.5rem!important}.mb-sm-5{margin-bottom:3rem!important}.mb-sm-auto{margin-bottom:auto!important}.ms-sm-0{margin-left:0!important}.ms-sm-1{margin-left:.25rem!important}.ms-sm-2{margin-left:.5rem!important}.ms-sm-3{margin-left:1rem!important}.ms-sm-4{margin-left:1.5rem!important}.ms-sm-5{margin-left:3rem!important}.ms-sm-auto{margin-left:auto!important}.p-sm-0{padding:0!important}.p-sm-1{padding:.25rem!important}.p-sm-2{padding:.5rem!important}.p-sm-3{padding:1rem!important}.p-sm-4{padding:1.5rem!important}.p-sm-5{padding:3rem!important}.px-sm-0{padding-right:0!important;padding-left:0!important}.px-sm-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-sm-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-sm-3{padding-right:1rem!important;padding-left:1rem!important}.px-sm-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-sm-5{padding-right:3rem!important;padding-left:3rem!important}.py-sm-0{padding-top:0!important;padding-bottom:0!important}.py-sm-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-sm-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-sm-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-sm-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-sm-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-sm-0{padding-top:0!important}.pt-sm-1{padding-top:.25rem!important}.pt-sm-2{padding-top:.5rem!important}.pt-sm-3{padding-top:1rem!important}.pt-sm-4{padding-top:1.5rem!important}.pt-sm-5{padding-top:3rem!important}.pe-sm-0{padding-right:0!important}.pe-sm-1{padding-right:.25rem!important}.pe-sm-2{padding-right:.5rem!important}.pe-sm-3{padding-right:1rem!important}.pe-sm-4{padding-right:1.5rem!important}.pe-sm-5{padding-right:3rem!important}.pb-sm-0{padding-bottom:0!important}.pb-sm-1{padding-bottom:.25rem!important}.pb-sm-2{padding-bottom:.5rem!important}.pb-sm-3{padding-bottom:1rem!important}.pb-sm-4{padding-bottom:1.5rem!important}.pb-sm-5{padding-bottom:3rem!important}.ps-sm-0{padding-left:0!important}.ps-sm-1{padding-left:.25rem!important}.ps-sm-2{padding-left:.5rem!important}.ps-sm-3{padding-left:1rem!important}.ps-sm-4{padding-left:1.5rem!important}.ps-sm-5{padding-left:3rem!important}.text-sm-start{text-align:left!important}.text-sm-end{text-align:right!important}.text-sm-center{text-align:center!important}}@media (min-width:768px){.float-md-start{float:left!important}.float-md-end{float:right!important}.float-md-none{float:none!important}.d-md-inline{display:inline!important}.d-md-inline-block{display:inline-block!important}.d-md-block{display:block!important}.d-md-grid{display:grid!important}.d-md-table{display:table!important}.d-md-table-row{display:table-row!important}.d-md-table-cell{display:table-cell!important}.d-md-flex{display:flex!important}.d-md-inline-flex{display:inline-flex!important}.d-md-none{display:none!important}.flex-md-fill{flex:1 1 auto!important}.flex-md-row{flex-direction:row!important}.flex-md-column{flex-direction:column!important}.flex-md-row-reverse{flex-direction:row-reverse!important}.flex-md-column-reverse{flex-direction:column-reverse!important}.flex-md-grow-0{flex-grow:0!important}.flex-md-grow-1{flex-grow:1!important}.flex-md-shrink-0{flex-shrink:0!important}.flex-md-shrink-1{flex-shrink:1!important}.flex-md-wrap{flex-wrap:wrap!important}.flex-md-nowrap{flex-wrap:nowrap!important}.flex-md-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-md-0{gap:0!important}.gap-md-1{gap:.25rem!important}.gap-md-2{gap:.5rem!important}.gap-md-3{gap:1rem!important}.gap-md-4{gap:1.5rem!important}.gap-md-5{gap:3rem!important}.justify-content-md-start{justify-content:flex-start!important}.justify-content-md-end{justify-content:flex-end!important}.justify-content-md-center{justify-content:center!important}.justify-content-md-between{justify-content:space-between!important}.justify-content-md-around{justify-content:space-around!important}.justify-content-md-evenly{justify-content:space-evenly!important}.align-items-md-start{align-items:flex-start!important}.align-items-md-end{align-items:flex-end!important}.align-items-md-center{align-items:center!important}.align-items-md-baseline{align-items:baseline!important}.align-items-md-stretch{align-items:stretch!important}.align-content-md-start{align-content:flex-start!important}.align-content-md-end{align-content:flex-end!important}.align-content-md-center{align-content:center!important}.align-content-md-between{align-content:space-between!important}.align-content-md-around{align-content:space-around!important}.align-content-md-stretch{align-content:stretch!important}.align-self-md-auto{align-self:auto!important}.align-self-md-start{align-self:flex-start!important}.align-self-md-end{align-self:flex-end!important}.align-self-md-center{align-self:center!important}.align-self-md-baseline{align-self:baseline!important}.align-self-md-stretch{align-self:stretch!important}.order-md-first{order:-1!important}.order-md-0{order:0!important}.order-md-1{order:1!important}.order-md-2{order:2!important}.order-md-3{order:3!important}.order-md-4{order:4!important}.order-md-5{order:5!important}.order-md-last{order:6!important}.m-md-0{margin:0!important}.m-md-1{margin:.25rem!important}.m-md-2{margin:.5rem!important}.m-md-3{margin:1rem!important}.m-md-4{margin:1.5rem!important}.m-md-5{margin:3rem!important}.m-md-auto{margin:auto!important}.mx-md-0{margin-right:0!important;margin-left:0!important}.mx-md-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-md-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-md-3{margin-right:1rem!important;margin-left:1rem!important}.mx-md-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-md-5{margin-right:3rem!important;margin-left:3rem!important}.mx-md-auto{margin-right:auto!important;margin-left:auto!important}.my-md-0{margin-top:0!important;margin-bottom:0!important}.my-md-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-md-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-md-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-md-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-md-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-md-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-md-0{margin-top:0!important}.mt-md-1{margin-top:.25rem!important}.mt-md-2{margin-top:.5rem!important}.mt-md-3{margin-top:1rem!important}.mt-md-4{margin-top:1.5rem!important}.mt-md-5{margin-top:3rem!important}.mt-md-auto{margin-top:auto!important}.me-md-0{margin-right:0!important}.me-md-1{margin-right:.25rem!important}.me-md-2{margin-right:.5rem!important}.me-md-3{margin-right:1rem!important}.me-md-4{margin-right:1.5rem!important}.me-md-5{margin-right:3rem!important}.me-md-auto{margin-right:auto!important}.mb-md-0{margin-bottom:0!important}.mb-md-1{margin-bottom:.25rem!important}.mb-md-2{margin-bottom:.5rem!important}.mb-md-3{margin-bottom:1rem!important}.mb-md-4{margin-bottom:1.5rem!important}.mb-md-5{margin-bottom:3rem!important}.mb-md-auto{margin-bottom:auto!important}.ms-md-0{margin-left:0!important}.ms-md-1{margin-left:.25rem!important}.ms-md-2{margin-left:.5rem!important}.ms-md-3{margin-left:1rem!important}.ms-md-4{margin-left:1.5rem!important}.ms-md-5{margin-left:3rem!important}.ms-md-auto{margin-left:auto!important}.p-md-0{padding:0!important}.p-md-1{padding:.25rem!important}.p-md-2{padding:.5rem!important}.p-md-3{padding:1rem!important}.p-md-4{padding:1.5rem!important}.p-md-5{padding:3rem!important}.px-md-0{padding-right:0!important;padding-left:0!important}.px-md-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-md-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-md-3{padding-right:1rem!important;padding-left:1rem!important}.px-md-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-md-5{padding-right:3rem!important;padding-left:3rem!important}.py-md-0{padding-top:0!important;padding-bottom:0!important}.py-md-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-md-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-md-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-md-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-md-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-md-0{padding-top:0!important}.pt-md-1{padding-top:.25rem!important}.pt-md-2{padding-top:.5rem!important}.pt-md-3{padding-top:1rem!important}.pt-md-4{padding-top:1.5rem!important}.pt-md-5{padding-top:3rem!important}.pe-md-0{padding-right:0!important}.pe-md-1{padding-right:.25rem!important}.pe-md-2{padding-right:.5rem!important}.pe-md-3{padding-right:1rem!important}.pe-md-4{padding-right:1.5rem!important}.pe-md-5{padding-right:3rem!important}.pb-md-0{padding-bottom:0!important}.pb-md-1{padding-bottom:.25rem!important}.pb-md-2{padding-bottom:.5rem!important}.pb-md-3{padding-bottom:1rem!important}.pb-md-4{padding-bottom:1.5rem!important}.pb-md-5{padding-bottom:3rem!important}.ps-md-0{padding-left:0!important}.ps-md-1{padding-left:.25rem!important}.ps-md-2{padding-left:.5rem!important}.ps-md-3{padding-left:1rem!important}.ps-md-4{padding-left:1.5rem!important}.ps-md-5{padding-left:3rem!important}.text-md-start{text-align:left!important}.text-md-end{text-align:right!important}.text-md-center{text-align:center!important}}@media (min-width:992px){.float-lg-start{float:left!important}.float-lg-end{float:right!important}.float-lg-none{float:none!important}.d-lg-inline{display:inline!important}.d-lg-inline-block{display:inline-block!important}.d-lg-block{display:block!important}.d-lg-grid{display:grid!important}.d-lg-table{display:table!important}.d-lg-table-row{display:table-row!important}.d-lg-table-cell{display:table-cell!important}.d-lg-flex{display:flex!important}.d-lg-inline-flex{display:inline-flex!important}.d-lg-none{display:none!important}.flex-lg-fill{flex:1 1 auto!important}.flex-lg-row{flex-direction:row!important}.flex-lg-column{flex-direction:column!important}.flex-lg-row-reverse{flex-direction:row-reverse!important}.flex-lg-column-reverse{flex-direction:column-reverse!important}.flex-lg-grow-0{flex-grow:0!important}.flex-lg-grow-1{flex-grow:1!important}.flex-lg-shrink-0{flex-shrink:0!important}.flex-lg-shrink-1{flex-shrink:1!important}.flex-lg-wrap{flex-wrap:wrap!important}.flex-lg-nowrap{flex-wrap:nowrap!important}.flex-lg-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-lg-0{gap:0!important}.gap-lg-1{gap:.25rem!important}.gap-lg-2{gap:.5rem!important}.gap-lg-3{gap:1rem!important}.gap-lg-4{gap:1.5rem!important}.gap-lg-5{gap:3rem!important}.justify-content-lg-start{justify-content:flex-start!important}.justify-content-lg-end{justify-content:flex-end!important}.justify-content-lg-center{justify-content:center!important}.justify-content-lg-between{justify-content:space-between!important}.justify-content-lg-around{justify-content:space-around!important}.justify-content-lg-evenly{justify-content:space-evenly!important}.align-items-lg-start{align-items:flex-start!important}.align-items-lg-end{align-items:flex-end!important}.align-items-lg-center{align-items:center!important}.align-items-lg-baseline{align-items:baseline!important}.align-items-lg-stretch{align-items:stretch!important}.align-content-lg-start{align-content:flex-start!important}.align-content-lg-end{align-content:flex-end!important}.align-content-lg-center{align-content:center!important}.align-content-lg-between{align-content:space-between!important}.align-content-lg-around{align-content:space-around!important}.align-content-lg-stretch{align-content:stretch!important}.align-self-lg-auto{align-self:auto!important}.align-self-lg-start{align-self:flex-start!important}.align-self-lg-end{align-self:flex-end!important}.align-self-lg-center{align-self:center!important}.align-self-lg-baseline{align-self:baseline!important}.align-self-lg-stretch{align-self:stretch!important}.order-lg-first{order:-1!important}.order-lg-0{order:0!important}.order-lg-1{order:1!important}.order-lg-2{order:2!important}.order-lg-3{order:3!important}.order-lg-4{order:4!important}.order-lg-5{order:5!important}.order-lg-last{order:6!important}.m-lg-0{margin:0!important}.m-lg-1{margin:.25rem!important}.m-lg-2{margin:.5rem!important}.m-lg-3{margin:1rem!important}.m-lg-4{margin:1.5rem!important}.m-lg-5{margin:3rem!important}.m-lg-auto{margin:auto!important}.mx-lg-0{margin-right:0!important;margin-left:0!important}.mx-lg-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-lg-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-lg-3{margin-right:1rem!important;margin-left:1rem!important}.mx-lg-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-lg-5{margin-right:3rem!important;margin-left:3rem!important}.mx-lg-auto{margin-right:auto!important;margin-left:auto!important}.my-lg-0{margin-top:0!important;margin-bottom:0!important}.my-lg-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-lg-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-lg-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-lg-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-lg-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-lg-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-lg-0{margin-top:0!important}.mt-lg-1{margin-top:.25rem!important}.mt-lg-2{margin-top:.5rem!important}.mt-lg-3{margin-top:1rem!important}.mt-lg-4{margin-top:1.5rem!important}.mt-lg-5{margin-top:3rem!important}.mt-lg-auto{margin-top:auto!important}.me-lg-0{margin-right:0!important}.me-lg-1{margin-right:.25rem!important}.me-lg-2{margin-right:.5rem!important}.me-lg-3{margin-right:1rem!important}.me-lg-4{margin-right:1.5rem!important}.me-lg-5{margin-right:3rem!important}.me-lg-auto{margin-right:auto!important}.mb-lg-0{margin-bottom:0!important}.mb-lg-1{margin-bottom:.25rem!important}.mb-lg-2{margin-bottom:.5rem!important}.mb-lg-3{margin-bottom:1rem!important}.mb-lg-4{margin-bottom:1.5rem!important}.mb-lg-5{margin-bottom:3rem!important}.mb-lg-auto{margin-bottom:auto!important}.ms-lg-0{margin-left:0!important}.ms-lg-1{margin-left:.25rem!important}.ms-lg-2{margin-left:.5rem!important}.ms-lg-3{margin-left:1rem!important}.ms-lg-4{margin-left:1.5rem!important}.ms-lg-5{margin-left:3rem!important}.ms-lg-auto{margin-left:auto!important}.p-lg-0{padding:0!important}.p-lg-1{padding:.25rem!important}.p-lg-2{padding:.5rem!important}.p-lg-3{padding:1rem!important}.p-lg-4{padding:1.5rem!important}.p-lg-5{padding:3rem!important}.px-lg-0{padding-right:0!important;padding-left:0!important}.px-lg-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-lg-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-lg-3{padding-right:1rem!important;padding-left:1rem!important}.px-lg-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-lg-5{padding-right:3rem!important;padding-left:3rem!important}.py-lg-0{padding-top:0!important;padding-bottom:0!important}.py-lg-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-lg-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-lg-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-lg-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-lg-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-lg-0{padding-top:0!important}.pt-lg-1{padding-top:.25rem!important}.pt-lg-2{padding-top:.5rem!important}.pt-lg-3{padding-top:1rem!important}.pt-lg-4{padding-top:1.5rem!important}.pt-lg-5{padding-top:3rem!important}.pe-lg-0{padding-right:0!important}.pe-lg-1{padding-right:.25rem!important}.pe-lg-2{padding-right:.5rem!important}.pe-lg-3{padding-right:1rem!important}.pe-lg-4{padding-right:1.5rem!important}.pe-lg-5{padding-right:3rem!important}.pb-lg-0{padding-bottom:0!important}.pb-lg-1{padding-bottom:.25rem!important}.pb-lg-2{padding-bottom:.5rem!important}.pb-lg-3{padding-bottom:1rem!important}.pb-lg-4{padding-bottom:1.5rem!important}.pb-lg-5{padding-bottom:3rem!important}.ps-lg-0{padding-left:0!important}.ps-lg-1{padding-left:.25rem!important}.ps-lg-2{padding-left:.5rem!important}.ps-lg-3{padding-left:1rem!important}.ps-lg-4{padding-left:1.5rem!important}.ps-lg-5{padding-left:3rem!important}.text-lg-start{text-align:left!important}.text-lg-end{text-align:right!important}.text-lg-center{text-align:center!important}}@media (min-width:1200px){.float-xl-start{float:left!important}.float-xl-end{float:right!important}.float-xl-none{float:none!important}.d-xl-inline{display:inline!important}.d-xl-inline-block{display:inline-block!important}.d-xl-block{display:block!important}.d-xl-grid{display:grid!important}.d-xl-table{display:table!important}.d-xl-table-row{display:table-row!important}.d-xl-table-cell{display:table-cell!important}.d-xl-flex{display:flex!important}.d-xl-inline-flex{display:inline-flex!important}.d-xl-none{display:none!important}.flex-xl-fill{flex:1 1 auto!important}.flex-xl-row{flex-direction:row!important}.flex-xl-column{flex-direction:column!important}.flex-xl-row-reverse{flex-direction:row-reverse!important}.flex-xl-column-reverse{flex-direction:column-reverse!important}.flex-xl-grow-0{flex-grow:0!important}.flex-xl-grow-1{flex-grow:1!important}.flex-xl-shrink-0{flex-shrink:0!important}.flex-xl-shrink-1{flex-shrink:1!important}.flex-xl-wrap{flex-wrap:wrap!important}.flex-xl-nowrap{flex-wrap:nowrap!important}.flex-xl-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-xl-0{gap:0!important}.gap-xl-1{gap:.25rem!important}.gap-xl-2{gap:.5rem!important}.gap-xl-3{gap:1rem!important}.gap-xl-4{gap:1.5rem!important}.gap-xl-5{gap:3rem!important}.justify-content-xl-start{justify-content:flex-start!important}.justify-content-xl-end{justify-content:flex-end!important}.justify-content-xl-center{justify-content:center!important}.justify-content-xl-between{justify-content:space-between!important}.justify-content-xl-around{justify-content:space-around!important}.justify-content-xl-evenly{justify-content:space-evenly!important}.align-items-xl-start{align-items:flex-start!important}.align-items-xl-end{align-items:flex-end!important}.align-items-xl-center{align-items:center!important}.align-items-xl-baseline{align-items:baseline!important}.align-items-xl-stretch{align-items:stretch!important}.align-content-xl-start{align-content:flex-start!important}.align-content-xl-end{align-content:flex-end!important}.align-content-xl-center{align-content:center!important}.align-content-xl-between{align-content:space-between!important}.align-content-xl-around{align-content:space-around!important}.align-content-xl-stretch{align-content:stretch!important}.align-self-xl-auto{align-self:auto!important}.align-self-xl-start{align-self:flex-start!important}.align-self-xl-end{align-self:flex-end!important}.align-self-xl-center{align-self:center!important}.align-self-xl-baseline{align-self:baseline!important}.align-self-xl-stretch{align-self:stretch!important}.order-xl-first{order:-1!important}.order-xl-0{order:0!important}.order-xl-1{order:1!important}.order-xl-2{order:2!important}.order-xl-3{order:3!important}.order-xl-4{order:4!important}.order-xl-5{order:5!important}.order-xl-last{order:6!important}.m-xl-0{margin:0!important}.m-xl-1{margin:.25rem!important}.m-xl-2{margin:.5rem!important}.m-xl-3{margin:1rem!important}.m-xl-4{margin:1.5rem!important}.m-xl-5{margin:3rem!important}.m-xl-auto{margin:auto!important}.mx-xl-0{margin-right:0!important;margin-left:0!important}.mx-xl-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-xl-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-xl-3{margin-right:1rem!important;margin-left:1rem!important}.mx-xl-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-xl-5{margin-right:3rem!important;margin-left:3rem!important}.mx-xl-auto{margin-right:auto!important;margin-left:auto!important}.my-xl-0{margin-top:0!important;margin-bottom:0!important}.my-xl-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-xl-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-xl-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-xl-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-xl-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-xl-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-xl-0{margin-top:0!important}.mt-xl-1{margin-top:.25rem!important}.mt-xl-2{margin-top:.5rem!important}.mt-xl-3{margin-top:1rem!important}.mt-xl-4{margin-top:1.5rem!important}.mt-xl-5{margin-top:3rem!important}.mt-xl-auto{margin-top:auto!important}.me-xl-0{margin-right:0!important}.me-xl-1{margin-right:.25rem!important}.me-xl-2{margin-right:.5rem!important}.me-xl-3{margin-right:1rem!important}.me-xl-4{margin-right:1.5rem!important}.me-xl-5{margin-right:3rem!important}.me-xl-auto{margin-right:auto!important}.mb-xl-0{margin-bottom:0!important}.mb-xl-1{margin-bottom:.25rem!important}.mb-xl-2{margin-bottom:.5rem!important}.mb-xl-3{margin-bottom:1rem!important}.mb-xl-4{margin-bottom:1.5rem!important}.mb-xl-5{margin-bottom:3rem!important}.mb-xl-auto{margin-bottom:auto!important}.ms-xl-0{margin-left:0!important}.ms-xl-1{margin-left:.25rem!important}.ms-xl-2{margin-left:.5rem!important}.ms-xl-3{margin-left:1rem!important}.ms-xl-4{margin-left:1.5rem!important}.ms-xl-5{margin-left:3rem!important}.ms-xl-auto{margin-left:auto!important}.p-xl-0{padding:0!important}.p-xl-1{padding:.25rem!important}.p-xl-2{padding:.5rem!important}.p-xl-3{padding:1rem!important}.p-xl-4{padding:1.5rem!important}.p-xl-5{padding:3rem!important}.px-xl-0{padding-right:0!important;padding-left:0!important}.px-xl-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-xl-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-xl-3{padding-right:1rem!important;padding-left:1rem!important}.px-xl-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-xl-5{padding-right:3rem!important;padding-left:3rem!important}.py-xl-0{padding-top:0!important;padding-bottom:0!important}.py-xl-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-xl-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-xl-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-xl-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-xl-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-xl-0{padding-top:0!important}.pt-xl-1{padding-top:.25rem!important}.pt-xl-2{padding-top:.5rem!important}.pt-xl-3{padding-top:1rem!important}.pt-xl-4{padding-top:1.5rem!important}.pt-xl-5{padding-top:3rem!important}.pe-xl-0{padding-right:0!important}.pe-xl-1{padding-right:.25rem!important}.pe-xl-2{padding-right:.5rem!important}.pe-xl-3{padding-right:1rem!important}.pe-xl-4{padding-right:1.5rem!important}.pe-xl-5{padding-right:3rem!important}.pb-xl-0{padding-bottom:0!important}.pb-xl-1{padding-bottom:.25rem!important}.pb-xl-2{padding-bottom:.5rem!important}.pb-xl-3{padding-bottom:1rem!important}.pb-xl-4{padding-bottom:1.5rem!important}.pb-xl-5{padding-bottom:3rem!important}.ps-xl-0{padding-left:0!important}.ps-xl-1{padding-left:.25rem!important}.ps-xl-2{padding-left:.5rem!important}.ps-xl-3{padding-left:1rem!important}.ps-xl-4{padding-left:1.5rem!important}.ps-xl-5{padding-left:3rem!important}.text-xl-start{text-align:left!important}.text-xl-end{text-align:right!important}.text-xl-center{text-align:center!important}}@media (min-width:1400px){.float-xxl-start{float:left!important}.float-xxl-end{float:right!important}.float-xxl-none{float:none!important}.d-xxl-inline{display:inline!important}.d-xxl-inline-block{display:inline-block!important}.d-xxl-block{display:block!important}.d-xxl-grid{display:grid!important}.d-xxl-table{display:table!important}.d-xxl-table-row{display:table-row!important}.d-xxl-table-cell{display:table-cell!important}.d-xxl-flex{display:flex!important}.d-xxl-inline-flex{display:inline-flex!important}.d-xxl-none{display:none!important}.flex-xxl-fill{flex:1 1 auto!important}.flex-xxl-row{flex-direction:row!important}.flex-xxl-column{flex-direction:column!important}.flex-xxl-row-reverse{flex-direction:row-reverse!important}.flex-xxl-column-reverse{flex-direction:column-reverse!important}.flex-xxl-grow-0{flex-grow:0!important}.flex-xxl-grow-1{flex-grow:1!important}.flex-xxl-shrink-0{flex-shrink:0!important}.flex-xxl-shrink-1{flex-shrink:1!important}.flex-xxl-wrap{flex-wrap:wrap!important}.flex-xxl-nowrap{flex-wrap:nowrap!important}.flex-xxl-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-xxl-0{gap:0!important}.gap-xxl-1{gap:.25rem!important}.gap-xxl-2{gap:.5rem!important}.gap-xxl-3{gap:1rem!important}.gap-xxl-4{gap:1.5rem!important}.gap-xxl-5{gap:3rem!important}.justify-content-xxl-start{justify-content:flex-start!important}.justify-content-xxl-end{justify-content:flex-end!important}.justify-content-xxl-center{justify-content:center!important}.justify-content-xxl-between{justify-content:space-between!important}.justify-content-xxl-around{justify-content:space-around!important}.justify-content-xxl-evenly{justify-content:space-evenly!important}.align-items-xxl-start{align-items:flex-start!important}.align-items-xxl-end{align-items:flex-end!important}.align-items-xxl-center{align-items:center!important}.align-items-xxl-baseline{align-items:baseline!important}.align-items-xxl-stretch{align-items:stretch!important}.align-content-xxl-start{align-content:flex-start!important}.align-content-xxl-end{align-content:flex-end!important}.align-content-xxl-center{align-content:center!important}.align-content-xxl-between{align-content:space-between!important}.align-content-xxl-around{align-content:space-around!important}.align-content-xxl-stretch{align-content:stretch!important}.align-self-xxl-auto{align-self:auto!important}.align-self-xxl-start{align-self:flex-start!important}.align-self-xxl-end{align-self:flex-end!important}.align-self-xxl-center{align-self:center!important}.align-self-xxl-baseline{align-self:baseline!important}.align-self-xxl-stretch{align-self:stretch!important}.order-xxl-first{order:-1!important}.order-xxl-0{order:0!important}.order-xxl-1{order:1!important}.order-xxl-2{order:2!important}.order-xxl-3{order:3!important}.order-xxl-4{order:4!important}.order-xxl-5{order:5!important}.order-xxl-last{order:6!important}.m-xxl-0{margin:0!important}.m-xxl-1{margin:.25rem!important}.m-xxl-2{margin:.5rem!important}.m-xxl-3{margin:1rem!important}.m-xxl-4{margin:1.5rem!important}.m-xxl-5{margin:3rem!important}.m-xxl-auto{margin:auto!important}.mx-xxl-0{margin-right:0!important;margin-left:0!important}.mx-xxl-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-xxl-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-xxl-3{margin-right:1rem!important;margin-left:1rem!important}.mx-xxl-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-xxl-5{margin-right:3rem!important;margin-left:3rem!important}.mx-xxl-auto{margin-right:auto!important;margin-left:auto!important}.my-xxl-0{margin-top:0!important;margin-bottom:0!important}.my-xxl-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-xxl-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-xxl-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-xxl-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-xxl-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-xxl-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-xxl-0{margin-top:0!important}.mt-xxl-1{margin-top:.25rem!important}.mt-xxl-2{margin-top:.5rem!important}.mt-xxl-3{margin-top:1rem!important}.mt-xxl-4{margin-top:1.5rem!important}.mt-xxl-5{margin-top:3rem!important}.mt-xxl-auto{margin-top:auto!important}.me-xxl-0{margin-right:0!important}.me-xxl-1{margin-right:.25rem!important}.me-xxl-2{margin-right:.5rem!important}.me-xxl-3{margin-right:1rem!important}.me-xxl-4{margin-right:1.5rem!important}.me-xxl-5{margin-right:3rem!important}.me-xxl-auto{margin-right:auto!important}.mb-xxl-0{margin-bottom:0!important}.mb-xxl-1{margin-bottom:.25rem!important}.mb-xxl-2{margin-bottom:.5rem!important}.mb-xxl-3{margin-bottom:1rem!important}.mb-xxl-4{margin-bottom:1.5rem!important}.mb-xxl-5{margin-bottom:3rem!important}.mb-xxl-auto{margin-bottom:auto!important}.ms-xxl-0{margin-left:0!important}.ms-xxl-1{margin-left:.25rem!important}.ms-xxl-2{margin-left:.5rem!important}.ms-xxl-3{margin-left:1rem!important}.ms-xxl-4{margin-left:1.5rem!important}.ms-xxl-5{margin-left:3rem!important}.ms-xxl-auto{margin-left:auto!important}.p-xxl-0{padding:0!important}.p-xxl-1{padding:.25rem!important}.p-xxl-2{padding:.5rem!important}.p-xxl-3{padding:1rem!important}.p-xxl-4{padding:1.5rem!important}.p-xxl-5{padding:3rem!important}.px-xxl-0{padding-right:0!important;padding-left:0!important}.px-xxl-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-xxl-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-xxl-3{padding-right:1rem!important;padding-left:1rem!important}.px-xxl-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-xxl-5{padding-right:3rem!important;padding-left:3rem!important}.py-xxl-0{padding-top:0!important;padding-bottom:0!important}.py-xxl-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-xxl-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-xxl-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-xxl-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-xxl-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-xxl-0{padding-top:0!important}.pt-xxl-1{padding-top:.25rem!important}.pt-xxl-2{padding-top:.5rem!important}.pt-xxl-3{padding-top:1rem!important}.pt-xxl-4{padding-top:1.5rem!important}.pt-xxl-5{padding-top:3rem!important}.pe-xxl-0{padding-right:0!important}.pe-xxl-1{padding-right:.25rem!important}.pe-xxl-2{padding-right:.5rem!important}.pe-xxl-3{padding-right:1rem!important}.pe-xxl-4{padding-right:1.5rem!important}.pe-xxl-5{padding-right:3rem!important}.pb-xxl-0{padding-bottom:0!important}.pb-xxl-1{padding-bottom:.25rem!important}.pb-xxl-2{padding-bottom:.5rem!important}.pb-xxl-3{padding-bottom:1rem!important}.pb-xxl-4{padding-bottom:1.5rem!important}.pb-xxl-5{padding-bottom:3rem!important}.ps-xxl-0{padding-left:0!important}.ps-xxl-1{padding-left:.25rem!important}.ps-xxl-2{padding-left:.5rem!important}.ps-xxl-3{padding-left:1rem!important}.ps-xxl-4{padding-left:1.5rem!important}.ps-xxl-5{padding-left:3rem!important}.text-xxl-start{text-align:left!important}.text-xxl-end{text-align:right!important}.text-xxl-center{text-align:center!important}}@media (min-width:1200px){.fs-1{font-size:2.5rem!important}.fs-2{font-size:2rem!important}.fs-3{font-size:1.75rem!important}.fs-4{font-size:1.5rem!important}}@media print{.d-print-inline{display:inline!important}.d-print-inline-block{display:inline-block!important}.d-print-block{display:block!important}.d-print-grid{display:grid!important}.d-print-table{display:table!important}.d-print-table-row{display:table-row!important}.d-print-table-cell{display:table-cell!important}.d-print-flex{display:flex!important}.d-print-inline-flex{display:inline-flex!important}.d-print-none{display:none!important}} +/*# sourceMappingURL=bootstrap.min.css.map */ \ No newline at end of file diff --git a/Streamdeck/com.mirabox.streamingle.sdPlugin/propertyInspector/utils/common.js b/Streamdeck/com.mirabox.streamingle.sdPlugin/propertyInspector/utils/common.js new file mode 100644 index 00000000..8dda3c8f --- /dev/null +++ b/Streamdeck/com.mirabox.streamingle.sdPlugin/propertyInspector/utils/common.js @@ -0,0 +1,81 @@ +// 自定义事件类 +class EventPlus { + constructor() { + this.event = new EventTarget(); + } + on(name, callback) { + this.event.addEventListener(name, e => callback(e.detail)); + } + send(name, data) { + this.event.dispatchEvent(new CustomEvent(name, { + detail: data, + bubbles: false, + cancelable: false + })); + } +} + +// 补零 +String.prototype.fill = function () { + return this >= 10 ? this : '0' + this; +}; + +// unicode编码转换字符串 +String.prototype.uTs = function () { + return eval('"' + Array.from(this).join('') + '"'); +}; + +// 字符串转换unicode编码 +String.prototype.sTu = function (str = '') { + Array.from(this).forEach(item => str += `\\u${item.charCodeAt(0).toString(16)}`); + return str; +}; + +// 全局变量/方法 +const $emit = new EventPlus(), $ = (selector, isAll = false) => { + const element = document.querySelector(selector), methods = { + on: function (event, callback) { + this.addEventListener(event, callback); + }, + attr: function (name, value = '') { + value && this.setAttribute(name, value); + return this; + } + }; + if (!isAll && element) { + return Object.assign(element, methods); + } else if (!isAll && !element) { + throw `HTML没有 ${selector} 元素! 请检查是否拼写错误`; + } + return Array.from(document.querySelectorAll(selector)).map(item => Object.assign(item, methods)); +}; + +// 节流函数 +$.throttle = (fn, delay) => { + let Timer = null; + return function () { + if (Timer) return; + Timer = setTimeout(() => { + fn.apply(this, arguments); + Timer = null; + }, delay); + }; +}; + +// 防抖函数 +$.debounce = (fn, delay) => { + let Timer = null; + return function () { + clearTimeout(Timer); + Timer = setTimeout(() => fn.apply(this, arguments), delay); + }; +}; + +// 绑定限制数字方法 +Array.from($('input[type="num"]', true)).forEach(item => { + item.addEventListener('input', function limitNum() { + if (!item.value || /^\d+$/.test(item.value)) return; + item.value = item.value.slice(0, -1); + limitNum(item); + }); +}); \ No newline at end of file diff --git a/Streamdeck/com.mirabox.streamingle.sdPlugin/propertyInspector/utils/fonts/bootstrap-icons.woff b/Streamdeck/com.mirabox.streamingle.sdPlugin/propertyInspector/utils/fonts/bootstrap-icons.woff new file mode 100644 index 00000000..1f5d5430 Binary files /dev/null and b/Streamdeck/com.mirabox.streamingle.sdPlugin/propertyInspector/utils/fonts/bootstrap-icons.woff differ diff --git a/Streamdeck/com.mirabox.streamingle.sdPlugin/propertyInspector/utils/fonts/bootstrap-icons.woff2 b/Streamdeck/com.mirabox.streamingle.sdPlugin/propertyInspector/utils/fonts/bootstrap-icons.woff2 new file mode 100644 index 00000000..b3897eff Binary files /dev/null and b/Streamdeck/com.mirabox.streamingle.sdPlugin/propertyInspector/utils/fonts/bootstrap-icons.woff2 differ diff --git a/Streamdeck/com.mirabox.streamingle.sdPlugin/streamingle_plugin b/Streamdeck/com.mirabox.streamingle.sdPlugin/streamingle_plugin new file mode 100644 index 00000000..04440e92 --- /dev/null +++ b/Streamdeck/com.mirabox.streamingle.sdPlugin/streamingle_plugin @@ -0,0 +1,9 @@ +[2025-07-03T00:52:25.811] [INFO] default - 🚀 Streamingle Camera Controller 플러그인 시작됨 +[2025-07-03T00:58:26.107] [INFO] default - 🚀 Streamingle Camera Controller 플러그인 시작됨 +[2025-07-03T00:58:37.479] [INFO] default - 🚀 Streamingle Camera Controller 플러그인 시작됨 +[2025-07-03T00:58:50.582] [INFO] default - 🚀 Streamingle Camera Controller 플러그인 시작됨 +[2025-07-03T00:59:04.912] [INFO] default - 🚀 Streamingle Camera Controller 플러그인 시작됨 +[2025-07-03T01:00:19.924] [INFO] default - 🚀 Streamingle Camera Controller 플러그인 시작됨 +[2025-07-03T01:00:32.402] [INFO] default - 🚀 Streamingle Camera Controller 플러그인 시작됨 +[2025-07-03T01:01:32.400] [INFO] default - 🚀 Streamingle Camera Controller 플러그인 시작됨 +[2025-07-03T01:02:32.414] [INFO] default - 🚀 Streamingle Camera Controller 플러그인 시작됨 diff --git a/Streamdeck/com.mirabox.streamingle.sdPlugin/test.js b/Streamdeck/com.mirabox.streamingle.sdPlugin/test.js new file mode 100644 index 00000000..0f6e2a5f --- /dev/null +++ b/Streamdeck/com.mirabox.streamingle.sdPlugin/test.js @@ -0,0 +1,65 @@ +const StreaminglePlugin = require('./plugin.js'); + +console.log('=== Streamingle 플러그인 테스트 시작 ==='); + +const plugin = new StreaminglePlugin(); + +// 연결 상태 모니터링 +let connectionCheckInterval = setInterval(() => { + const status = plugin.getStatus(); + console.log(`📊 연결 상태: ${status.isConnected ? '✅ 연결됨' : '❌ 연결 안됨'}`); + + if (status.isConnected) { + console.log(`📷 카메라 개수: ${status.cameraCount}개`); + console.log(`🎯 현재 카메라: ${status.currentCamera >= 0 ? status.currentCamera : '없음'}`); + + if (status.cameraList && status.cameraList.length > 0) { + console.log('📋 카메라 목록:'); + status.cameraList.forEach((camera, index) => { + console.log(` ${index}: ${camera.name} ${camera.isActive ? '[활성]' : '[비활성]'}`); + }); + } + } + + console.log('---'); +}, 5000); + +// 3초 후 카메라 목록 요청 +setTimeout(() => { + console.log('🔍 카메라 목록 요청...'); + plugin.requestCameraList(); +}, 3000); + +// 8초 후 첫 번째 카메라로 전환 +setTimeout(() => { + console.log('🎬 첫 번째 카메라로 전환...'); + plugin.switchCamera(0); +}, 8000); + +// 13초 후 두 번째 카메라로 전환 +setTimeout(() => { + console.log('🎬 두 번째 카메라로 전환...'); + plugin.switchCamera(1); +}, 13000); + +// 18초 후 세 번째 카메라로 전환 (있다면) +setTimeout(() => { + console.log('🎬 세 번째 카메라로 전환...'); + plugin.switchCamera(2); +}, 18000); + +// 25초 후 종료 +setTimeout(() => { + console.log('🛑 테스트 종료...'); + clearInterval(connectionCheckInterval); + plugin.disconnect(); + process.exit(0); +}, 25000); + +// 프로세스 종료 시 정리 +process.on('SIGINT', () => { + console.log('🛑 테스트 중단...'); + clearInterval(connectionCheckInterval); + plugin.disconnect(); + process.exit(0); +}); \ No newline at end of file