Release v0.1.2
This commit is contained in:
@@ -0,0 +1,156 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Threading;
|
||||
using SbLauncher;
|
||||
|
||||
// Real first-run/import/save/restart/update paths using synthetic credentials.
|
||||
public static class ProfileUpdates
|
||||
{
|
||||
[STAThread]
|
||||
public static int Main()
|
||||
{
|
||||
string root = Path.Combine(Path.GetTempPath(), "SbLauncherUpdates-" + Guid.NewGuid().ToString("N"));
|
||||
Directory.CreateDirectory(root);
|
||||
var app = new Application { ShutdownMode = ShutdownMode.OnExplicitShutdown };
|
||||
MainVm vm = null;
|
||||
try
|
||||
{
|
||||
var a = Shared("a", "AA", "127.0.0.1", "00000000-0000-4000-8000-000000000001");
|
||||
var b = Shared("b", "BB", "127.0.0.1", "00000000-0000-4000-8000-000000000002");
|
||||
WriteBase(root, "198.51.100.10", a, b);
|
||||
Paths paths = new Paths(root);
|
||||
vm = new MainVm();
|
||||
vm.Load(paths);
|
||||
Require(vm.Prof.Servers.Count == 2 && !File.Exists(paths.Profiles), "Fresh install loads shared servers without personal profiles");
|
||||
vm.PrivateKey = Convert.ToBase64String(Enumerable.Range(1, 32).Select(delegate(int n) { return (byte)n; }).ToArray());
|
||||
vm.LocalAddress = "10.10.0.2/24";
|
||||
vm.Adapter = "Personal adapter";
|
||||
Require(Wait(vm.AddFromLinkAsync("trojan://personal-secret@127.0.0.2:443?sni=example.com#Personal")), "UI import adds a personal server");
|
||||
var own = vm.Prof.Servers.Single(delegate(ServerCfg server) { return server.BaseId.Length == 0; });
|
||||
string ownCode = own.Code;
|
||||
vm.Selected = vm.Servers.Single(delegate(ServerVm card) { return card.Cfg != null && card.Cfg.BaseId == "b"; });
|
||||
vm.FlushLocal();
|
||||
string localBefore = File.ReadAllText(paths.Local);
|
||||
string personalBefore = File.ReadAllText(paths.Profiles);
|
||||
Require(!personalBefore.Contains("00000000-0000-4000-8000-000000000001") &&
|
||||
!personalBefore.Contains("00000000-0000-4000-8000-000000000002"), "Saving personal servers excludes all shared credentials");
|
||||
vm.DisposeTimers();
|
||||
vm = null;
|
||||
|
||||
Profiles removed = Profiles.Load(paths.Profiles);
|
||||
removed.Remove(removed.Servers.Single(delegate(ServerCfg server) { return server.BaseId == "a"; }));
|
||||
removed.Save(paths.Profiles);
|
||||
string personalHidden = File.ReadAllText(paths.Profiles);
|
||||
// Simulate replacement of files shipped by the next release.
|
||||
var changedA = Shared("a", "AA", "127.0.0.3", "00000000-0000-4000-8000-000000000003");
|
||||
var changedB = Shared("b", "BB", "127.0.0.4", "00000000-0000-4000-8000-000000000004");
|
||||
var c = Shared("c", ownCode, "127.0.0.5", "00000000-0000-4000-8000-000000000005");
|
||||
WriteBase(root, "198.51.100.20", changedA, changedB, c);
|
||||
Require(File.ReadAllText(paths.Local) == localBefore && File.ReadAllText(paths.Profiles) == personalHidden,
|
||||
"Replacing the release catalog does not write either personal file");
|
||||
vm = new MainVm();
|
||||
vm.Load(paths);
|
||||
Require(vm.Prof.Servers.Count == 3 && vm.Prof.Wg.Server == "198.51.100.20", "Updated shared catalog and WireGuard endpoint load");
|
||||
Require(vm.Prof.Servers.All(delegate(ServerCfg server) { return server.BaseId != "a"; }), "Hidden shared server stays hidden after its credentials change");
|
||||
Require(vm.Prof.Servers.Single(delegate(ServerCfg server) { return server.BaseId == "b"; }).Server == "127.0.0.4", "Existing shared server receives updated configuration");
|
||||
Require(vm.Prof.Servers.Single(delegate(ServerCfg server) { return server.BaseId.Length == 0; }).Code == ownCode,
|
||||
"Personal server survives a shared code collision");
|
||||
Require(vm.Prof.Servers.Select(delegate(ServerCfg server) { return server.Code; }).Distinct(StringComparer.OrdinalIgnoreCase).Count() == 3,
|
||||
"Merged server codes are unique");
|
||||
Require(vm.Selected.Cfg.BaseId == "b", "Shared selection is restored by stable id");
|
||||
vm.FlushLocal();
|
||||
Require(File.ReadAllText(paths.Local) == localBefore, "WireGuard key, address, adapter and selection survive restart after update");
|
||||
vm.DisposeTimers();
|
||||
vm = null;
|
||||
|
||||
// Upgrade a v0.1.1 full list with one personal server and settings.
|
||||
string legacyRoot = Path.Combine(root, "legacy");
|
||||
Directory.CreateDirectory(legacyRoot);
|
||||
WriteBase(legacyRoot, "198.51.100.10", a, b);
|
||||
Profiles legacy = new Profiles();
|
||||
legacy.Wg.Server = "198.51.100.10";
|
||||
legacy.Wg.PeerPublicKey = "shared-public-key";
|
||||
legacy.Servers.Add(new ServerCfg { Code = "AA", Server = "127.0.0.1", Uuid = "00000000-0000-4000-8000-000000000001" });
|
||||
legacy.Servers.Add(new ServerCfg { Code = "BB", Server = "127.0.0.1", Uuid = "00000000-0000-4000-8000-000000000002" });
|
||||
legacy.Servers.Add(own);
|
||||
string legacyPath = Path.Combine(legacyRoot, "profiles.json");
|
||||
legacy.Save(legacyPath);
|
||||
File.WriteAllText(Path.Combine(legacyRoot, "local.json"), localBefore);
|
||||
string legacyBefore = File.ReadAllText(legacyPath);
|
||||
vm = new MainVm();
|
||||
vm.Load(new Paths(legacyRoot));
|
||||
Require(vm.Prof.Servers.Count == 3 && !vm.Prof.NeedsMigration, "Legacy list migrates without losing personal servers or duplicating shared servers");
|
||||
Require(File.ReadAllText(Path.Combine(legacyRoot, "profiles.legacy-backup.json")) == legacyBefore,
|
||||
"Migration preserves a complete legacy backup");
|
||||
Require(!File.ReadAllText(legacyPath).Contains("00000000-0000-4000-8000-000000000001"), "Migration stores only personal servers");
|
||||
vm.DisposeTimers();
|
||||
vm = null;
|
||||
WriteBase(legacyRoot, "198.51.100.20", changedB);
|
||||
Profiles upgraded = Profiles.Load(legacyPath);
|
||||
Require(upgraded.Servers.Count == 2 && upgraded.Wg.Server == "198.51.100.20", "Migrated installations receive later shared updates and removals");
|
||||
Require(File.ReadAllText(Path.Combine(legacyRoot, "local.json")) == localBefore, "Legacy upgrade preserves the personal WireGuard settings");
|
||||
string customRoot = Path.Combine(root, "custom");
|
||||
Directory.CreateDirectory(customRoot);
|
||||
WriteBase(customRoot, "198.51.100.10", a);
|
||||
Profiles custom = new Profiles();
|
||||
custom.Servers.Add(new ServerCfg { Code = "AA", Server = "127.0.0.1",
|
||||
Uuid = "00000000-0000-4000-8000-000000000001", Sni = "custom.example.com" });
|
||||
custom.Save(Path.Combine(customRoot, "profiles.json"));
|
||||
Require(Profiles.Load(Path.Combine(customRoot, "profiles.json")).Servers.Count == 2,
|
||||
"Legacy custom connection settings are preserved even with a matching endpoint and UUID");
|
||||
upgraded.Remove(upgraded.Servers.Single(delegate(ServerCfg server) { return server.BaseId.Length == 0; }));
|
||||
upgraded.Save(legacyPath);
|
||||
Require(Profiles.Load(legacyPath).Servers.Count == 1, "Deleted personal server does not return on restart");
|
||||
Console.WriteLine("Profile update checks passed.");
|
||||
return 0;
|
||||
}
|
||||
catch (Exception ex) { Console.Error.WriteLine(ex); return 1; }
|
||||
finally
|
||||
{
|
||||
if (vm != null) vm.DisposeTimers();
|
||||
app.Shutdown();
|
||||
string full = Path.GetFullPath(root);
|
||||
string temp = Path.GetFullPath(Path.GetTempPath()).TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar;
|
||||
if (full.StartsWith(temp, StringComparison.OrdinalIgnoreCase)) Directory.Delete(full, true);
|
||||
}
|
||||
}
|
||||
|
||||
static Dictionary<string, object> Shared(string id, string code, string host, string uuid)
|
||||
{
|
||||
return new Dictionary<string, object> {
|
||||
{ "id", id }, { "code", code }, { "name", code }, { "server", host },
|
||||
{ "port", 443 }, { "uuid", uuid }, { "protocol", "vless" }, { "flow", "" }
|
||||
};
|
||||
}
|
||||
|
||||
static void WriteBase(string root, string wgHost, params Dictionary<string, object>[] servers)
|
||||
{
|
||||
J.Write(Path.Combine(root, "profiles.base.json"), J.ToJson(new Dictionary<string, object> {
|
||||
{ "wireguard", new Dictionary<string, object> { { "server", wgHost }, { "peer_public_key", "shared-public-key" } } },
|
||||
{ "servers", servers }
|
||||
}));
|
||||
}
|
||||
|
||||
static bool Wait(Task<bool> task)
|
||||
{
|
||||
var frame = new DispatcherFrame();
|
||||
var started = DateTime.UtcNow;
|
||||
var timer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(20) };
|
||||
timer.Tick += delegate {
|
||||
if (task.IsCompleted || (DateTime.UtcNow - started).TotalSeconds > 10) { timer.Stop(); frame.Continue = false; }
|
||||
};
|
||||
timer.Start();
|
||||
Dispatcher.PushFrame(frame);
|
||||
return task.IsCompleted && task.Result;
|
||||
}
|
||||
|
||||
static void Require(bool condition, string message)
|
||||
{
|
||||
if (!condition) throw new InvalidOperationException(message);
|
||||
Console.WriteLine("PASS " + message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Markup;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Threading;
|
||||
using SbLauncher;
|
||||
|
||||
// Exercises real WPF animation clocks, including interruption and settling.
|
||||
// Synthetic view model only: no local settings, network, or sing-box process.
|
||||
public static class ThemeTransitions
|
||||
{
|
||||
[STAThread]
|
||||
public static int Main(string[] args)
|
||||
{
|
||||
var app = new Application { ShutdownMode = ShutdownMode.OnExplicitShutdown };
|
||||
Window window = null;
|
||||
var vm = new MainVm();
|
||||
try
|
||||
{
|
||||
string root = args.Length > 0 ? args[0] : AppDomain.CurrentDomain.BaseDirectory;
|
||||
window = (Window)XamlReader.Parse(File.ReadAllText(Path.Combine(root, "Ui.xaml")));
|
||||
window.DataContext = vm;
|
||||
vm.ThemeApplier = delegate(string theme) { Theme.Apply(window, theme); };
|
||||
vm.SetTheme(Theme.Dark);
|
||||
var dark = (ResourceDictionary)window.Resources["Theme.Dark"];
|
||||
var light = (ResourceDictionary)window.Resources["Theme.Light"];
|
||||
var sun = (TextBlock)window.FindName("ThemeSun");
|
||||
var moon = (TextBlock)window.FindName("ThemeMoon");
|
||||
Color darkText = ((SolidColorBrush)dark["Text"]).Color;
|
||||
Color lightText = ((SolidColorBrush)light["Text"]).Color;
|
||||
Require(SameColor(TextColor(window), darkText) && sun.Opacity == 1 && moon.Opacity == 0,
|
||||
"Startup is immediate and shows the correct icon");
|
||||
vm.SetTheme(Theme.Light);
|
||||
Require(SameColor(TextColor(window), lightText) && sun.Opacity == 0 && moon.Opacity == 1,
|
||||
"Unloaded screenshot rendering is immediate in both themes");
|
||||
vm.SetTheme(Theme.Dark);
|
||||
|
||||
window.WindowStartupLocation = WindowStartupLocation.Manual;
|
||||
window.Left = -20000;
|
||||
window.Top = -20000;
|
||||
window.ShowInTaskbar = false;
|
||||
window.ShowActivated = false;
|
||||
window.Show();
|
||||
PumpFor(100);
|
||||
vm.SetTheme(Theme.Light);
|
||||
Require(object.ReferenceEquals(window.Resources["Muted"], Application.Current.Resources["Muted"]),
|
||||
"Semantic colors and XAML share the transition brushes");
|
||||
if (SystemParameters.ClientAreaAnimation)
|
||||
{
|
||||
Require(SameColor(TextColor(window), darkText), "No initial palette flash");
|
||||
PumpFor(170);
|
||||
Color halfway = TextColor(window);
|
||||
Require(!SameColor(halfway, darkText) && !SameColor(halfway, lightText), "Text colors interpolate");
|
||||
Require(sun.Opacity > 0 && sun.Opacity < 1 && moon.Opacity > 0 && moon.Opacity < 1,
|
||||
"Both icons participate in the crossfade");
|
||||
var sunTransform = (TransformGroup)sun.RenderTransform;
|
||||
Require(((RotateTransform)sunTransform.Children[1]).Angle != 0 &&
|
||||
((ScaleTransform)sunTransform.Children[0]).ScaleX < 1,
|
||||
"Outgoing icon rotates and scales");
|
||||
var background = (GradientBrush)window.Resources["Bg"];
|
||||
Color middleStop = background.GradientStops[0].Color;
|
||||
double sunOpacity = sun.Opacity;
|
||||
vm.SetTheme(Theme.Dark);
|
||||
Require(SameColor(TextColor(window), halfway) &&
|
||||
SameColor(((GradientBrush)window.Resources["Bg"]).GradientStops[0].Color, middleStop) &&
|
||||
Math.Abs(sun.Opacity - sunOpacity) < 0.001,
|
||||
"Reversal continues from current colors and icon position");
|
||||
}
|
||||
else Require(SameColor(TextColor(window), lightText) && moon.Opacity == 1,
|
||||
"Windows reduced animation preference is respected");
|
||||
|
||||
for (int i = 0; i < 12; i++)
|
||||
{
|
||||
vm.SetTheme(i % 2 == 0 ? Theme.Light : Theme.Dark);
|
||||
PumpFor(35);
|
||||
}
|
||||
vm.SetTheme(Theme.Light);
|
||||
PumpFor(650);
|
||||
Require(SameColor(TextColor(window), lightText) && sun.Opacity == 0 && moon.Opacity == 1,
|
||||
"Latest request wins after rapid switching");
|
||||
Require(vm.ThemeAction == "Включить тёмную тему", "Accessible action matches the destination");
|
||||
CheckPalette(window, light);
|
||||
vm.SetTheme(Theme.Dark);
|
||||
PumpFor(650);
|
||||
Require(SameColor(TextColor(window), darkText) && sun.Opacity == 1 && moon.Opacity == 0,
|
||||
"Reverse transition reaches its exact final state");
|
||||
CheckPalette(window, dark);
|
||||
Require(((SolidColorBrush)dark["Text"]).Color == darkText &&
|
||||
((SolidColorBrush)light["Text"]).Color == lightText &&
|
||||
!((SolidColorBrush)dark["Text"]).HasAnimatedProperties &&
|
||||
!((SolidColorBrush)light["Text"]).HasAnimatedProperties,
|
||||
"Source palettes are unchanged");
|
||||
Console.WriteLine("Theme transition checks passed.");
|
||||
return 0;
|
||||
}
|
||||
catch (Exception ex) { Console.Error.WriteLine(ex); return 1; }
|
||||
finally
|
||||
{
|
||||
vm.DisposeTimers();
|
||||
if (window != null) window.Close();
|
||||
app.Shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
static Color TextColor(Window window) { return ((SolidColorBrush)window.Resources["Text"]).Color; }
|
||||
|
||||
// ColorAnimation interpolates in scRGB; compare the rendered 8-bit channels
|
||||
// rather than exact floating-point equality with XAML's sRGB representation.
|
||||
static bool SameColor(Color a, Color b) { return a.A == b.A && a.R == b.R && a.G == b.G && a.B == b.B; }
|
||||
|
||||
static void CheckPalette(Window window, ResourceDictionary expected)
|
||||
{
|
||||
foreach (object key in expected.Keys)
|
||||
{
|
||||
var solid = expected[key] as SolidColorBrush;
|
||||
if (solid != null)
|
||||
Require(SameColor(((SolidColorBrush)window.Resources[key]).Color, solid.Color), "Final color: " + key);
|
||||
var gradient = expected[key] as GradientBrush;
|
||||
if (gradient != null)
|
||||
{
|
||||
var actual = (GradientBrush)window.Resources[key];
|
||||
for (int i = 0; i < gradient.GradientStops.Count; i++)
|
||||
Require(SameColor(actual.GradientStops[i].Color, gradient.GradientStops[i].Color) &&
|
||||
actual.GradientStops[i].Offset == gradient.GradientStops[i].Offset,
|
||||
"Final gradient: " + key + " / " + i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void PumpFor(int milliseconds)
|
||||
{
|
||||
var frame = new DispatcherFrame();
|
||||
var timer = new DispatcherTimer(DispatcherPriority.Background) { Interval = TimeSpan.FromMilliseconds(milliseconds) };
|
||||
timer.Tick += delegate { timer.Stop(); frame.Continue = false; };
|
||||
timer.Start();
|
||||
Dispatcher.PushFrame(frame);
|
||||
}
|
||||
|
||||
static void Require(bool condition, string message)
|
||||
{
|
||||
if (!condition) throw new InvalidOperationException(message);
|
||||
Console.WriteLine("PASS " + message);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
@@ -26,6 +27,9 @@ public static class UiStress
|
||||
vm.ThemeApplier = delegate(string theme) { Theme.Apply(window, theme); };
|
||||
vm.Load(new Paths(dataRoot));
|
||||
vm.SetTheme(Theme.Dark);
|
||||
// Подключаем переходы вкладок без сетевых действий основного Wire.
|
||||
typeof(SbLauncher.App).GetMethod("WireTabs", BindingFlags.Static | BindingFlags.NonPublic)
|
||||
.Invoke(null, new object[] { window, vm });
|
||||
var stopwatch = Stopwatch.StartNew();
|
||||
long previous = 0, longest = 0;
|
||||
int ticks = 0;
|
||||
|
||||
Reference in New Issue
Block a user