diff --git a/Updater/NewUpdater.cs b/Updater/NewUpdater.cs index 5e63215..12bf623 100644 --- a/Updater/NewUpdater.cs +++ b/Updater/NewUpdater.cs @@ -1,8 +1,10 @@ using System.ComponentModel; using System.Diagnostics; using System.Net; +using System.Reflection; using System.Text.Json; using GraphicsManager; +using GraphicsManager.Enums; using GraphicsManager.Objects; using GraphicsManager.Objects.Core; using OpenTK.Mathematics; @@ -15,32 +17,64 @@ public class NewUpdater : FPSWindow private readonly ConfigFile cf; private ProgressBar TotalProgress; private Argumets Argumets; - private List ProgressThreads = new(); + private List> ProgressThreads = new(); private readonly BackgroundWorker Download = new(); + private Label? Total_Downloaded, Transfer_Speed; + private readonly System.Timers.Timer Speed; + private List speeds = new(); + public NewUpdater(NativeWindowSettings nws, GameWindowSettings gws, Argumets args) :base(nws, gws) { + Speed = new() + { + Interval = 500 + }; + Speed.Elapsed += Speed_Elapsed; + CanControleUpdate = false; + //small = (uint)Math.Round(0.0656167979002625 * StartGUI.Height, 0); + //RenderObjects.Add(Total_Downloaded = new RenderText("Download Amount", "OpenSans-Regular", small, -0.9381188f, 0.149797574f, 1.0f, new Vector2(1f, 0f), new Vector4(cf.Colors.Text.R, cf.Colors.Text.G, cf.Colors.Text.B, cf.Colors.Text.A))); + //small = (uint)Math.Round(0.078740157480315 * StartGUI.Height, 0); + //RenderObjects.Add(Transfer_Speed = new RenderText("Download Speed", "OpenSans-Regular", small, -0.9381188f, -0.05263158f, 1.2f, new Vector2(1f, 0f), new Vector4(cf.Colors.Text.R, cf.Colors.Text.G, cf.Colors.Text.B, cf.Colors.Text.A))); + + FontFamily r = FontFamily.LoadFontFamily(Tools.GetResourceStream(Assembly.GetExecutingAssembly(), "Updater.Resource.OpenSans.zip"), "OpenSans").Result; + FontInteraction fi = FontInteraction.Load(r); cf = Config.GetConfig(); + + Size = new(cf.Scale(Size.X), cf.Scale(Size.Y)); Argumets = args; - var r = FontFamily.LoadFontFamily().Result; - FontInteraction fi = FontInteraction.Load(r); - BackgroundColor = cf.Colors.Background.ToColor4(); int WallDistance = cf.Scale(15); - //Texture ProgressbarTexture = TextureManager.AddTexture(File.OpenRead("/home/jacob/Pictures/Progress.png")); - Controls.Add(TotalProgress = new ProgressBar() + BackgroundColor = cf.Colors.Background.ToColor4(); + + Texture ProgressbarTexture = TextureManager.AddTexture(File.OpenRead("/home/jacob/Pictures/Progress.png")); + Controls.Add(TotalProgress = new ProgressBar(ProgressbarTexture) { Location = new(WallDistance, cf.Scale(70), 0), Size = new(Size.X - WallDistance - WallDistance, cf.Scale(20)), BackgroundColor = cf.Colors.Progress_bars.Backcolor.ToColor4(), ProgressColor = cf.Colors.Progress_bars.Fillcolor.ToColor4(), - ProgressValue = 25, + ProgressValue = 5, ProgressGap = (uint)cf.Scale(3), - //Shader = Rectangle.DefaultAlphaShader[Context], - //TextureDisplay = TextureDisplay.ProgressHorizontalCenter, - UpdateOnDraw = true + Shader = Rectangle.DefaultAlphaShader[Context], + TextureDisplay = TextureDisplay.ProgressHorizontalCenter, + UpdateOnDraw = true, + InnerShader = Rectangle.DefaultAlphaShader[Context] }); - //TotalProgress.InnerShader = TotalProgress.Shader; + + + fi.PixelHeight = (uint)WallDistance; + Controls.Add(Total_Downloaded = new(fi) + { + Text = "D", + Location = new((int)FloatToInt(-0.9381188f), (int)FloatToInt(0.149797574f + 0.14f, true), 0) + }); + Controls.Add(Transfer_Speed = new(fi) + { + Text = "abcdefghijklmnopqrstuvwxyz", + Location = new(Total_Downloaded.Location.X, (int)FloatToInt(-0.05263158f + 0.09f, true), 0) + }); + int gap = cf.Scale(10); for (int i = 0; i < cf.Format.DownloadThreads; i++) { @@ -49,10 +83,10 @@ public class NewUpdater : FPSWindow Label l = new(fi) { Color = Color4.Black, - Text = "Downloading bob" + Text = "Downloading ...." }; l.Location = new(cf.Scale(5), (int)(((TotalProgress.Size.Y - l.TrueHeight) / 2) - fi.PixelHeight + l.PostiveTrueHeight), 0); - Controls.Add(temp = new ProgressBar() + Controls.Add(temp = new ProgressBar(ProgressbarTexture) { Location = new(WallDistance, cf.Scale(70) + (TotalProgress.Size.Y * (i + 1)) + (gap * (i + 1)), 0), Size = TotalProgress.Size, @@ -63,18 +97,17 @@ public class NewUpdater : FPSWindow Shader = TotalProgress.Shader, TextureDisplay = TotalProgress.TextureDisplay, UpdateOnDraw = true, - InnerShader = TotalProgress.InnerShader, - Tag = l + InnerShader = TotalProgress.InnerShader }); temp.Controls.Add(l); - ProgressThreads.Add(temp); + ProgressThreads.Add(new(temp,l)); } - fi.PixelHeight = (uint)cf.Scale(15); + Download.DoWork += Download_DoWork; Download.RunWorkerCompleted += Download_RunWorkerCompleted; Download.WorkerReportsProgress = true; Invoke(new Action(() => { Download.RunWorkerAsync(); })); - + Speed.Start(); Controls.Add(new Label(fi) @@ -87,6 +120,23 @@ public class NewUpdater : FPSWindow + FloatToInt(-0.5748032f, true).ToString() }); } + + private ulong last = 0; + + private void Speed_Elapsed(object? sender, System.Timers.ElapsedEventArgs e) + { + ulong BytesPertenthSec; + if (last > TotalProgress.ProgressValue) BytesPertenthSec= 0; + else BytesPertenthSec= (TotalProgress.ProgressValue) - last; + last = TotalProgress.ProgressValue; + speeds.Add(BytesPertenthSec * 2); + if (speeds.Count > 25) speeds.RemoveAt(0); + Debug.WriteLine($"l: {last}"); + Debug.WriteLine($"bps: {BytesPertenthSec}"); + if (Transfer_Speed is not null && speeds.Average() != 0) Transfer_Speed.Text = Download_Speed_Neat(speeds.Average()); + } + + private bool ran = true; private void Download_DoWork(object? sender, DoWorkEventArgs e) { @@ -143,12 +193,10 @@ public class NewUpdater : FPSWindow ulong realmax = ulong.Parse(webPre.DownloadString($"https://{Program.Domain}/Updater/GetSize?directory={Argumets.RemoteDirectory}{Argumets.UriBranch}{Argumets.UriSeflContaind}{Argumets.UriPlatform}{Argumets.UriVersion}")); TotalProgress.MaxProgressValue = realmax; Console.WriteLine(TotalProgress.MaxProgressValue); - //queue.Enqueue(new Action(() => { if (Total_Downloaded is not null) Total_Downloaded.Text = TotalDownloadAmountNeat(realmax); })); - //Speed.Start(); string[] files = rem.Split('\n'); int c = -1; - ProgressBar getpb() + Tuple getpb() { c++; if (c == ProgressThreads.Count) c = 0; @@ -163,8 +211,6 @@ public class NewUpdater : FPSWindow if (string.IsNullOrEmpty(file)) return; string uri = $"https://{Program.Domain}/Updater/GetFileSize?directory={Argumets.RemoteDirectory}{Argumets.UriBranch}{Argumets.UriVersion}{Argumets.UriSeflContaind}{Argumets.UriPlatform}&file={file}"; Uri u = new($"https://{Program.Domain}/Updater/GetFile?directory={Argumets.RemoteDirectory}{Argumets.UriBranch}{Argumets.UriVersion}{Argumets.UriSeflContaind}{Argumets.UriPlatform}&file={file}"); - Console.WriteLine(file); - Console.WriteLine(uri); string[] temp = file.Split('/'); if (file.Contains('/')) { @@ -208,33 +254,48 @@ public class NewUpdater : FPSWindow } } - private Task DownloadFile(ProgressBar pb, WebClient web, string uri, Uri u, string[] temp, string file) + private Task DownloadFile(Tuple pb, WebClient web, string uri, Uri u, string[] temp, string file) { while (!web.IsBusy) { + string a = web.DownloadString(uri); //Invoke(new Action(() =>pb.MaxProgressValue = ulong.Parse(a))); - pb.MaxProgressValue = ulong.Parse(a); - Console.WriteLine(uri + ": " +pb.MaxProgressValue); + pb.Item1.MaxProgressValue = ulong.Parse(a); + pb.Item1.ProgressValue = 0; //ulong byres = 0; web.DownloadProgressChanged += (sender, args) => { - TotalProgress.ProgressValue += (ulong)args.BytesReceived - pb.ProgressValue; - pb.ProgressValue = (ulong)args.BytesReceived; + TotalProgress.ProgressValue += (ulong)args.BytesReceived - pb.Item1.ProgressValue; + pb.Item1.ProgressValue = (ulong)args.BytesReceived; + if (ran) + { + ran = false; + Invoke( () => + { + // Total_Downloaded!.Text = + // $"Downloaded {TotalDownloadAmountNeat(TotalProgress.ProgressValue)}"; + ran = true; + }); + } }; string s = $"Downloading {temp[^1]}"; ChangeProcessText(s); - if (pb.Tag is Label l && temp[^1] == "file2.txt") + try { - l.Text = s; + pb.Item2.Text = s; } + catch (Exception e) + { + Console.WriteLine(e); + } if (Argumets.Updater is null) { - web.DownloadFileTaskAsync(u, Argumets.LocalDirectory + file).Wait(); + web.DownloadFileTaskAsync(u, Path.Combine(Argumets.LocalDirectory, file)).Wait(); } else { - web.DownloadFileTaskAsync(u, Argumets.Updater + file).Wait(); + web.DownloadFileTaskAsync(u, Path.Combine(Argumets.Updater, file)).Wait(); } return Task.CompletedTask; @@ -246,86 +307,8 @@ public class NewUpdater : FPSWindow { try { - if (Argumets.Updater is not null) - { - Process p = new(); - p.StartInfo.FileName = Path.Combine(Argumets.Updater, AppDomain.CurrentDomain.FriendlyName); - if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS() || OperatingSystem.IsFreeBSD()) - { - Process m = Process.Start("/usr/bin/chmod", $"+x {Path.Combine(Argumets.Updater, AppDomain.CurrentDomain.FriendlyName)}"); - m.WaitForExit(); - } - - p.StartInfo.WorkingDirectory = Argumets.Updater; - p.StartInfo.CreateNoWindow = true; - Program.rawargs[1] = AppDomain.CurrentDomain.BaseDirectory; - p.StartInfo.Arguments = $"\"{string.Join("\" \"", Program.rawargs)}\""; - p.StartInfo.WindowStyle = ProcessWindowStyle.Normal; - - p.Start(); - Console.WriteLine("Program done"); - Close(); - } - else - { - if (Argumets.Loc is not null) - { - FileInfo fi = new(Argumets.Loc); - if (!fi.Directory!.Exists) Directory.CreateDirectory(fi.Directory.FullName); - UpdaterSettings temp; - if (!File.Exists(Argumets.Loc)) - { - File.WriteAllText(Argumets.Loc, JsonSerializer.Serialize(new UpdaterSettings(), UpdaterSettingsContext.Default.UpdaterSettings)); - temp = new(); - } - - try - { - UpdaterSettings? ss = JsonSerializer.Deserialize(File.ReadAllText(Argumets.Loc), UpdaterSettingsContext.Default.UpdaterSettings); - if (ss is null) - { - ss = new(); - } - - temp = ss; - } - catch - { - temp = new(); - } - - temp.Updater = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, AppDomain.CurrentDomain.FriendlyName); - if (Argumets.Platform is not null) temp.Platform = Argumets.Platform!.ToLower(); - temp.SelfContained = Argumets.SeflContaind; - temp.Branch = Argumets.Branch!.ToLower() switch - { - "dev" => Branch.Dev, - "beta" => Branch.Beta, - "master" or _ => Branch.Master - }; - File.WriteAllText(Argumets.Loc, JsonSerializer.Serialize(temp, UpdaterSettingsContext.Default.UpdaterSettings)); - } - if (string.IsNullOrEmpty(Argumets.DLL) || string.IsNullOrWhiteSpace(Argumets.DLL)) Close(); - Process p = new(); - p.StartInfo.FileName = Path.Combine(Argumets.LocalDirectory, Argumets.DLL); - if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS() || OperatingSystem.IsFreeBSD()) - { - Process m = Process.Start("/usr/bin/chmod", $"+x \"{Path.Combine(Argumets.LocalDirectory, Argumets.DLL)}\""); - m.WaitForExit(); - } - - p.StartInfo.WorkingDirectory = Argumets.LocalDirectory; - p.StartInfo.CreateNoWindow = true; - foreach (string arg in Argumets.args!) - { - p.StartInfo.ArgumentList.Add(arg); - } - - p.StartInfo.WindowStyle = ProcessWindowStyle.Normal; - - p.Start(); - Close(); - } + StartGUI.close = true; + Close(); } catch (Exception exception) { @@ -338,4 +321,125 @@ public class NewUpdater : FPSWindow Console.WriteLine(Process); //queue.Enqueue(new Action(() => { if (Current_Process is not null) Current_Process.Text = $"{Process}"; })); } + + #region Neat + private string Download_Speed_Neat(double Number) + { + Debug.WriteLine($"a: {Number}"); + ConfigFileSize cfs = cf.Format.DownloadSpeed; + string Size = SizeName(Number, cfs); + int bytes = 1024; + if (cfs == ConfigFileSize.MebiBit || cfs == ConfigFileSize.MebiByte) bytes = 1000; + int Exponet; + if (Number / bytes >= 1) + if (Number / Math.Pow(bytes, 2) >= 1) + if (Number / Math.Pow(bytes, 3) >= 1) Exponet = 3; + else Exponet = 2; + else Exponet = 1; + else Exponet = 0; + if (cfs == ConfigFileSize.MebiBit || cfs == ConfigFileSize.MegaBit) Number *= 8; + Number = Math.Round(Number / Math.Pow(bytes, Exponet), 2, MidpointRounding.ToEven); + if (!Number.ToString().Contains('.')) + { + return $"{Number}.00{Size}/s"; + } + else + { + if (Number.ToString().Remove(0, Number.ToString().IndexOf('.')).Length == 1) + { + return $"{Number}0{Size}/s"; + } + else + { + return $"{Number}{Size}/s"; + } + } + } + + private string Downloaded_Amount_Neat(double Number, string Old_Text) + { + int Exponet = 0; + ConfigFileSize cfs = cf.Format.DownloadAmount; + int bytes = 1024; + if (cfs == ConfigFileSize.MebiBit || cfs == ConfigFileSize.MebiByte) bytes = 1000; + string Size = SizeName(Number, cfs); + if (Size.ToLower().StartsWith('g')) Exponet = 3; + else if (Size.ToLower().StartsWith('m')) Exponet = 2; + else if (Size.ToLower().StartsWith('k')) Exponet = 1; + Number = Math.Round(Number / Math.Pow(bytes, Exponet), 2, MidpointRounding.ToEven); + if (cfs == ConfigFileSize.MebiBit || cfs == ConfigFileSize.MegaBit) Number *= 8; + string text = Old_Text.Remove(0, Old_Text.IndexOf(' ')); + if (!Number.ToString().Contains('.')) + { + text = $"{Number}.00{Size}{text}"; + } + else + { + text = Number.ToString().Remove(0, Number.ToString().IndexOf('.')).Length != 1 ? $"{Number}{Size}{text}" : $"{Number}0{Size}{text}"; + } + return text; + } + + private string TotalDownloadAmountNeat(double Number) + { + string Output; + int Exponet; + ConfigFileSize cfs = cf.Format.DownloadAmount; + int bytes = 1024; + if (cfs == ConfigFileSize.MebiBit || cfs == ConfigFileSize.MebiByte) bytes = 1000; + if (Number / bytes >= 1) + if (Number / Math.Pow(bytes, 2) >= 1) + if (Number / Math.Pow(bytes, 3) >= 1) Exponet = 3; + else Exponet = 2; + else Exponet = 1; + else Exponet = 0; + if (cfs == ConfigFileSize.MebiBit || cfs == ConfigFileSize.MegaBit) Number *= 8; + Output = $"{Math.Round(Number / Math.Pow(bytes, Exponet), 2, MidpointRounding.ToEven)}{SizeName(Number, cfs)}"; + return Output; + } + + private static string SizeName(double Number, ConfigFileSize format) + { + string Size; + switch (format) + { + default: + case ConfigFileSize.MegaByte: + if (Number / 1000 >= 1) + if (Number / Math.Pow(1000, 2) >= 1) + if (Number / Math.Pow(1000, 3) >= 1) Size = "GB"; + else Size = "MB"; + else Size = "KB"; + else Size = "B"; + break; + case ConfigFileSize.MebiByte: + if (Number / 1024 >= 1) + if (Number / Math.Pow(1024, 2) >= 1) + if (Number / Math.Pow(1024, 3) >= 1) Size = "GiB"; + else Size = "MiB"; + else Size = "KiB"; + else Size = "B"; + break; + case ConfigFileSize.MegaBit: + Number *= 8; + if (Number / 1000 >= 1) + if (Number / Math.Pow(1000, 2) >= 1) + if (Number / Math.Pow(1000, 3) >= 1) Size = "Gb"; + else Size = "Mb"; + else Size = "Kb"; + else Size = "b"; + break; + case ConfigFileSize.MebiBit: + Number *= 8; + if (Number / 1024 >= 1) + if (Number / Math.Pow(1024, 2) >= 1) + if (Number / Math.Pow(1024, 3) >= 1) Size = "Gib"; + else Size = "Mib"; + else Size = "Kib"; + else Size = "b"; + break; + } + return Size; + } + #endregion } \ No newline at end of file diff --git a/Updater/Program.cs b/Updater/Program.cs index c279131..eea0fcf 100755 --- a/Updater/Program.cs +++ b/Updater/Program.cs @@ -39,7 +39,7 @@ public class Program try { #if DEBUG - args = new string[] { "--process", "vyukgykgyh", "--remotedirectory", "Luski","--version", "1.1.0.1", "--localdirectory", $"/home/jacob/Documents/Updater/Updater/bin/Release/ttt", "--dll", "Luski", "--selfcontained", "true", "--branch", "Dev", "--platform", "linux-x64" }; + args = new string[] { "--process", "vyukgykgyh", "--remotedirectory", "Luski", "--localdirectory", $"/home/jacob/Documents/Updater/Updater/bin/Release/ttt", "--dll", "Luski", "--selfcontained", "false", "--branch", "Dev", "--platform", "linux-x64" }; string bbbbbb = $"\"{string.Join("\" \"", args)}\""; #endif rawargs = args; diff --git a/Updater/Resource/OpenSans.zip b/Updater/Resource/OpenSans.zip new file mode 100644 index 0000000..7159a53 Binary files /dev/null and b/Updater/Resource/OpenSans.zip differ diff --git a/Updater/StartGUI.cs b/Updater/StartGUI.cs index 80ba8c6..d950864 100755 --- a/Updater/StartGUI.cs +++ b/Updater/StartGUI.cs @@ -1,10 +1,12 @@ #if true +using System.Diagnostics; using OpenTK.Mathematics; using OpenTK.Windowing.Common; using OpenTK.Windowing.Common.Input; using OpenTK.Windowing.Desktop; using SixLabors.ImageSharp.PixelFormats; using System.Reflection; +using System.Text.Json; using GraphicsManager; using SixLabors.ImageSharp; using Image = OpenTK.Windowing.Common.Input.Image; @@ -27,13 +29,15 @@ public class StartGUI private static readonly GameWindowSettings GameWindowSettings = new() { - RenderFrequency = 30, + RenderFrequency = 24, UpdateFrequency = 30 }; internal static NewUpdater? Up; + public static bool close = false; + /// /// args[0] = Process /// args[1] = Remote Program dir @@ -60,13 +64,104 @@ public class StartGUI //Settings.Size = new Vector2i(Width, Height); */ Image Logo = SixLabors.ImageSharp.Image.Load(Tools.GetResourceStream(Assembly.GetExecutingAssembly(), "Updater.Resource.Logo.png")); - Logo.DangerousTryGetSinglePixelMemory(out Memory m); + Logo.DangerousTryGetSinglePixelMemory(out Memory mm); byte[] pixels = new byte[4 * Logo.Width * Logo.Height]; Logo.CopyPixelDataTo(pixels); Settings.Icon = new WindowIcon(new Image(Logo.Width, Logo.Height, pixels)); (Up = new NewUpdater(Settings, GameWindowSettings, args)).Run(); + Up.Close(); + Up.Dispose(); + if (close) + { + if (args.Updater is not null) + { + Process p = new(); + p.StartInfo.FileName = Path.Combine(args.Updater, AppDomain.CurrentDomain.FriendlyName); + if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS() || OperatingSystem.IsFreeBSD()) + { + Process m = Process.Start("/usr/bin/chmod", $"+x {Path.Combine(args.Updater, AppDomain.CurrentDomain.FriendlyName)}"); + m.WaitForExit(); + } + + p.StartInfo.WorkingDirectory = args.Updater; + p.StartInfo.CreateNoWindow = true; + Program.rawargs[1] = AppDomain.CurrentDomain.BaseDirectory; + p.StartInfo.Arguments = $"\"{string.Join("\" \"", Program.rawargs)}\""; + p.StartInfo.WindowStyle = ProcessWindowStyle.Normal; + + p.Start(); + Console.WriteLine("Program done"); + return 0; + } + else + { + if (args.Loc is not null) + { + FileInfo fi = new(args.Loc); + if (!fi.Directory!.Exists) Directory.CreateDirectory(fi.Directory.FullName); + UpdaterSettings temp; + if (!File.Exists(args.Loc)) + { + File.WriteAllText(args.Loc, JsonSerializer.Serialize(new UpdaterSettings(), UpdaterSettingsContext.Default.UpdaterSettings)); + temp = new(); + } + + try + { + UpdaterSettings? ss = JsonSerializer.Deserialize(File.ReadAllText(args.Loc), UpdaterSettingsContext.Default.UpdaterSettings); + if (ss is null) + { + ss = new(); + } + + temp = ss; + } + catch + { + temp = new(); + } + + temp.Updater = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, AppDomain.CurrentDomain.FriendlyName); + if (args.Platform is not null) temp.Platform = args.Platform!.ToLower(); + temp.SelfContained = args.SeflContaind; + temp.Branch = args.Branch!.ToLower() switch + { + "dev" => Branch.Dev, + "beta" => Branch.Beta, + "master" or _ => Branch.Master + }; + File.WriteAllText(args.Loc, JsonSerializer.Serialize(temp, UpdaterSettingsContext.Default.UpdaterSettings)); + } + + if (string.IsNullOrEmpty(args.DLL) || string.IsNullOrWhiteSpace(args.DLL)) + { + return 0; + } + if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS() || OperatingSystem.IsFreeBSD()) + { + Process m = Process.Start("/usr/bin/chmod", $"+x \"{Path.Combine(args.LocalDirectory, args.DLL)}\""); + m.WaitForExit(); + } + + string a = ""; + foreach (string arg in args.args!) + { + a += arg + " "; + } + + Process.Start(new ProcessStartInfo() + { + FileName = Path.Combine(args.LocalDirectory, args.DLL), + Arguments = a, + WorkingDirectory = args.LocalDirectory, + CreateNoWindow = true, + UseShellExecute = true, + + })!.WaitForExit(); + } + } return 0; } } diff --git a/Updater/Updater.csproj b/Updater/Updater.csproj index 5e30e1b..e01422b 100755 --- a/Updater/Updater.csproj +++ b/Updater/Updater.csproj @@ -1,7 +1,7 @@  WinExe - net7.0 + net8.0 enable enable true @@ -18,36 +18,21 @@ portable - - - - - - - Always - - + - - Never - - - - - - + NU1701 - + - +