Release v0.1.2
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
// SbLauncher — GUI для sing-box.
|
||||
//
|
||||
// ПРИНЦИП (изменён по сравнению с прошлой версией):
|
||||
// Источник правды — profiles.json (серверы, общее) и local.json (ключ,
|
||||
// Источник правды — profiles.base.json (базовые серверы), profiles.json
|
||||
// (личные серверы) и local.json (ключ,
|
||||
// адрес, адаптер — своё на каждой машине). Конфиг sing-box НЕ хранится:
|
||||
// он собирается из шаблона templates\*.json при каждом запуске и кладётся
|
||||
// в run\config.json. Поэтому папок conf_* больше нет, и править один и
|
||||
@@ -71,6 +72,7 @@ namespace SbLauncher
|
||||
{
|
||||
public const string Dark = "dark";
|
||||
public const string Light = "light";
|
||||
static readonly TimeSpan TransitionDuration = TimeSpan.FromMilliseconds(460);
|
||||
|
||||
public static string DetectSystem()
|
||||
{
|
||||
@@ -103,12 +105,92 @@ namespace SbLauncher
|
||||
ResourceDictionary src = w.Resources[key] as ResourceDictionary;
|
||||
if (src == null) return;
|
||||
|
||||
// Первый кадр и служебные рендеры сразу получают конечную тему.
|
||||
bool animate = w.IsLoaded && SystemParameters.ClientAreaAnimation;
|
||||
foreach (object k in src.Keys)
|
||||
{
|
||||
object v = src[k];
|
||||
Brush target = v as Brush;
|
||||
if (target != null)
|
||||
v = TransitionBrush(w.Resources[k] as Brush, target, animate);
|
||||
w.Resources[k] = v;
|
||||
if (Application.Current != null) Application.Current.Resources[k] = v;
|
||||
}
|
||||
UpdateIcon(w, "ThemeSun", theme != Light, -100, animate);
|
||||
UpdateIcon(w, "ThemeMoon", theme == Light, 90, animate);
|
||||
}
|
||||
|
||||
static Brush TransitionBrush(Brush current, Brush target, bool animate)
|
||||
{
|
||||
// Исходные словари остаются неизменными. Новый переход начинается
|
||||
// с видимого цвета, даже если предыдущий ещё не успел завершиться.
|
||||
Brush next = target.Clone();
|
||||
if (!animate || current == null) return next;
|
||||
|
||||
SolidColorBrush fromSolid = current as SolidColorBrush;
|
||||
SolidColorBrush toSolid = next as SolidColorBrush;
|
||||
GradientBrush fromGradient = current as GradientBrush;
|
||||
GradientBrush toGradient = next as GradientBrush;
|
||||
if (fromSolid != null && toSolid != null)
|
||||
AnimateColor(toSolid, SolidColorBrush.ColorProperty, fromSolid.Color, toSolid.Color);
|
||||
else if (fromGradient != null && toGradient != null &&
|
||||
current.GetType() == target.GetType() &&
|
||||
fromGradient.GradientStops.Count == toGradient.GradientStops.Count)
|
||||
{
|
||||
for (int i = 0; i < toGradient.GradientStops.Count; i++)
|
||||
{
|
||||
GradientStop from = fromGradient.GradientStops[i];
|
||||
GradientStop to = toGradient.GradientStops[i];
|
||||
AnimateColor(to, GradientStop.ColorProperty, from.Color, to.Color);
|
||||
AnimateDouble(to, GradientStop.OffsetProperty, from.Offset, to.Offset, true);
|
||||
}
|
||||
}
|
||||
AnimateDouble(next, Brush.OpacityProperty, current.Opacity, target.Opacity, true);
|
||||
return next;
|
||||
}
|
||||
|
||||
static void AnimateColor(Animatable target, DependencyProperty property, Color from, Color to)
|
||||
{
|
||||
if (from == to) return;
|
||||
// WPF активирует clock на следующем кадре: до него тоже должен
|
||||
// оставаться исходный цвет, иначе при клике видна вспышка цели.
|
||||
target.SetValue(property, from);
|
||||
var animation = new ColorAnimation(from, to, TransitionDuration);
|
||||
animation.EasingFunction = new CubicEase { EasingMode = EasingMode.EaseInOut };
|
||||
animation.FillBehavior = FillBehavior.HoldEnd;
|
||||
target.BeginAnimation(property, animation, HandoffBehavior.SnapshotAndReplace);
|
||||
}
|
||||
|
||||
static void AnimateDouble(DependencyObject target, DependencyProperty property,
|
||||
double from, double to, bool animate)
|
||||
{
|
||||
var animated = (IAnimatable)target;
|
||||
animated.BeginAnimation(property, null);
|
||||
bool transition = animate && Math.Abs(from - to) >= 0.001;
|
||||
target.SetValue(property, transition ? from : to);
|
||||
if (!transition) return;
|
||||
var animation = new DoubleAnimation(from, to, TransitionDuration);
|
||||
animation.EasingFunction = new CubicEase { EasingMode = EasingMode.EaseInOut };
|
||||
// SnapshotAndReplace keeps one clock per property. Holding the
|
||||
// endpoint avoids callbacks from an interrupted transition.
|
||||
animation.FillBehavior = FillBehavior.HoldEnd;
|
||||
animated.BeginAnimation(property, animation, HandoffBehavior.SnapshotAndReplace);
|
||||
}
|
||||
|
||||
static void UpdateIcon(Window window, string name, bool visible, double hiddenAngle, bool animate)
|
||||
{
|
||||
FrameworkElement icon = window.FindName(name) as FrameworkElement;
|
||||
if (icon == null) return;
|
||||
TransformGroup transforms = icon.RenderTransform as TransformGroup;
|
||||
if (transforms == null || transforms.Children.Count != 2) return;
|
||||
if (transforms.IsFrozen) icon.RenderTransform = transforms = transforms.Clone();
|
||||
var scale = transforms.Children[0] as ScaleTransform;
|
||||
var rotation = transforms.Children[1] as RotateTransform;
|
||||
if (scale == null || rotation == null) return;
|
||||
AnimateDouble(icon, UIElement.OpacityProperty, icon.Opacity, visible ? 1 : 0, animate);
|
||||
AnimateDouble(scale, ScaleTransform.ScaleXProperty, scale.ScaleX, visible ? 1 : 0.4, animate);
|
||||
AnimateDouble(scale, ScaleTransform.ScaleYProperty, scale.ScaleY, visible ? 1 : 0.4, animate);
|
||||
AnimateDouble(rotation, RotateTransform.AngleProperty, rotation.Angle, visible ? 0 : hiddenAngle, animate);
|
||||
}
|
||||
|
||||
// Смысловые цвета (Ok/Warn/Bad/Idle/Na/Muted/Accent) тоже живут в
|
||||
@@ -534,6 +616,7 @@ namespace SbLauncher
|
||||
Border selection = w.FindName("ServerSelectionWindow") as Border;
|
||||
if (serverList != null && serverHost != null && selection != null)
|
||||
MoveServerSelection(serverList, serverHost, selection, false);
|
||||
UpdateTabs(w, vm, false);
|
||||
root2.UpdateLayout();
|
||||
|
||||
RenderTargetBitmap rtb = new RenderTargetBitmap(
|
||||
@@ -569,10 +652,12 @@ namespace SbLauncher
|
||||
if (argv[i] == "--root") return argv[i + 1];
|
||||
|
||||
string exeDir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
|
||||
if (File.Exists(Path.Combine(exeDir, "profiles.json"))) return exeDir;
|
||||
if (File.Exists(Path.Combine(exeDir, "profiles.json")) ||
|
||||
File.Exists(Path.Combine(exeDir, "profiles.base.json"))) return exeDir;
|
||||
|
||||
string parent = Path.GetDirectoryName(exeDir);
|
||||
if (parent != null && File.Exists(Path.Combine(parent, "profiles.json"))) return parent;
|
||||
if (parent != null && (File.Exists(Path.Combine(parent, "profiles.json")) ||
|
||||
File.Exists(Path.Combine(parent, "profiles.base.json")))) return parent;
|
||||
|
||||
return exeDir;
|
||||
}
|
||||
@@ -665,9 +750,83 @@ namespace SbLauncher
|
||||
|
||||
WireSwitch(w);
|
||||
WireServerSelection(w);
|
||||
WireTabs(w, vm);
|
||||
w.Loaded += delegate { vm.PingAll(); };
|
||||
}
|
||||
|
||||
static void WireTabs(Window w, MainVm vm)
|
||||
{
|
||||
FrameworkElement host = w.FindName("TabContentHost") as FrameworkElement;
|
||||
if (host == null) return;
|
||||
|
||||
w.Loaded += delegate { UpdateTabs(w, vm, false); };
|
||||
host.SizeChanged += delegate { UpdateTabs(w, vm, false); };
|
||||
PropertyChangedEventHandler changed = delegate(object sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
if (e.PropertyName == "ShowSettings") UpdateTabs(w, vm, w.IsLoaded);
|
||||
};
|
||||
vm.PropertyChanged += changed;
|
||||
w.Closed += delegate { vm.PropertyChanged -= changed; };
|
||||
}
|
||||
|
||||
static void UpdateTabs(Window w, MainVm vm, bool animate)
|
||||
{
|
||||
FrameworkElement host = w.FindName("TabContentHost") as FrameworkElement;
|
||||
FrameworkElement selector = w.FindName("TabSelectorHost") as FrameworkElement;
|
||||
Border highlight = w.FindName("TabSelectionWindow") as Border;
|
||||
Border log = w.FindName("LogPane") as Border;
|
||||
Border settings = w.FindName("SettingsPane") as Border;
|
||||
Button selected = w.FindName(vm.ShowSettings ? "BtnTabSettings" : "BtnTabLog") as Button;
|
||||
if (host == null || selector == null || highlight == null || log == null ||
|
||||
settings == null || selected == null || host.ActualWidth <= 0) return;
|
||||
|
||||
bool showSettings = vm.ShowSettings;
|
||||
Border inactive = showSettings ? log : settings;
|
||||
// Уходящая панель остаётся видимой до конца движения, но её поля
|
||||
// уже не должны получать клики или фокус с клавиатуры.
|
||||
if (inactive.IsKeyboardFocusWithin) selected.Focus();
|
||||
log.IsHitTestVisible = !showSettings;
|
||||
settings.IsHitTestVisible = showSettings;
|
||||
KeyboardNavigation.SetTabNavigation(log, showSettings ? KeyboardNavigationMode.None : KeyboardNavigationMode.Continue);
|
||||
KeyboardNavigation.SetTabNavigation(settings, showSettings ? KeyboardNavigationMode.Continue : KeyboardNavigationMode.None);
|
||||
log.Visibility = settings.Visibility = Visibility.Visible;
|
||||
|
||||
double travel = host.ActualWidth + 16;
|
||||
SlideTabElement(log, showSettings ? -travel : 0, animate,
|
||||
delegate { if (vm.ShowSettings) log.Visibility = Visibility.Hidden; });
|
||||
SlideTabElement(settings, showSettings ? 0 : travel, animate,
|
||||
delegate { if (!vm.ShowSettings) settings.Visibility = Visibility.Hidden; });
|
||||
double targetX = selected.TransformToAncestor(selector).Transform(new Point()).X;
|
||||
SlideTabElement(highlight, targetX, animate, null);
|
||||
}
|
||||
|
||||
static void SlideTabElement(FrameworkElement element, double targetX, bool animate, Action completed)
|
||||
{
|
||||
TranslateTransform shift = element.RenderTransform as TranslateTransform;
|
||||
if (shift == null) element.RenderTransform = shift = new TranslateTransform();
|
||||
else if (shift.IsFrozen) element.RenderTransform = shift = shift.Clone();
|
||||
|
||||
double fromX = shift.X;
|
||||
shift.BeginAnimation(TranslateTransform.XProperty, null);
|
||||
shift.X = fromX;
|
||||
if (!animate || Math.Abs(fromX - targetX) < 0.1)
|
||||
{
|
||||
shift.X = targetX;
|
||||
if (completed != null) completed();
|
||||
return;
|
||||
}
|
||||
|
||||
DoubleAnimation slide = new DoubleAnimation(fromX, targetX, TimeSpan.FromMilliseconds(520));
|
||||
slide.EasingFunction = new BackEase { Amplitude = 0.22, EasingMode = EasingMode.EaseOut };
|
||||
slide.Completed += delegate
|
||||
{
|
||||
shift.X = targetX;
|
||||
shift.BeginAnimation(TranslateTransform.XProperty, null);
|
||||
if (completed != null) completed();
|
||||
};
|
||||
shift.BeginAnimation(TranslateTransform.XProperty, slide);
|
||||
}
|
||||
|
||||
static void WireSwitch(Window w)
|
||||
{
|
||||
ToggleButton toggle = w.FindName("WireguardSwitch") as ToggleButton;
|
||||
|
||||
Reference in New Issue
Block a user