Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b2ae91b5b3 |
@@ -3,6 +3,9 @@
|
|||||||
local.json
|
local.json
|
||||||
profiles.json
|
profiles.json
|
||||||
|
|
||||||
|
# локальные ярлыки Windows
|
||||||
|
*.lnk
|
||||||
|
|
||||||
# ── рантайм ──────────────────────────────────────────────────────────
|
# ── рантайм ──────────────────────────────────────────────────────────
|
||||||
# сюда лаунчер генерирует config.json перед запуском
|
# сюда лаунчер генерирует config.json перед запуском
|
||||||
run/
|
run/
|
||||||
|
|||||||
@@ -33,10 +33,12 @@ using System.Threading.Tasks;
|
|||||||
using System.Web.Script.Serialization;
|
using System.Web.Script.Serialization;
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
using System.Windows.Controls;
|
using System.Windows.Controls;
|
||||||
|
using System.Windows.Controls.Primitives;
|
||||||
using System.Windows.Input;
|
using System.Windows.Input;
|
||||||
using System.Windows.Interop;
|
using System.Windows.Interop;
|
||||||
using System.Windows.Markup;
|
using System.Windows.Markup;
|
||||||
using System.Windows.Media;
|
using System.Windows.Media;
|
||||||
|
using System.Windows.Media.Animation;
|
||||||
using System.Windows.Media.Imaging;
|
using System.Windows.Media.Imaging;
|
||||||
using System.Windows.Threading;
|
using System.Windows.Threading;
|
||||||
|
|
||||||
@@ -147,7 +149,7 @@ namespace SbLauncher
|
|||||||
|
|
||||||
_ni = new System.Windows.Forms.NotifyIcon();
|
_ni = new System.Windows.Forms.NotifyIcon();
|
||||||
_ni.Icon = LoadIcon();
|
_ni.Icon = LoadIcon();
|
||||||
_ni.Text = "sb launcher";
|
_ni.Text = "SbLauncher";
|
||||||
_ni.Visible = true;
|
_ni.Visible = true;
|
||||||
_ni.DoubleClick += delegate { Restore(); };
|
_ni.DoubleClick += delegate { Restore(); };
|
||||||
|
|
||||||
@@ -497,13 +499,12 @@ namespace SbLauncher
|
|||||||
vm.SetTheme(themeArg != null ? themeArg : Theme.Dark);
|
vm.SetTheme(themeArg != null ? themeArg : Theme.Dark);
|
||||||
vm.Load(paths);
|
vm.Load(paths);
|
||||||
vm.ShowSettings = settingsTab;
|
vm.ShowSettings = settingsTab;
|
||||||
vm.AppendLog("демо-рендер интерфейса — sing-box не запускался");
|
vm.ClearLog();
|
||||||
|
vm.AppendLog("демонстрация интерфейса — sing-box не запускался");
|
||||||
|
|
||||||
ImageSource logo = LoadLogo();
|
ImageSource logo = LoadLogo();
|
||||||
Image li = w.FindName("LogoImg") as Image;
|
Image li = w.FindName("LogoImg") as Image;
|
||||||
if (li != null && logo != null) li.Source = logo;
|
if (li != null && logo != null) li.Source = logo;
|
||||||
Image statusLogo = w.FindName("StatusLogoImg") as Image;
|
|
||||||
if (statusLogo != null && logo != null) statusLogo.Source = logo;
|
|
||||||
|
|
||||||
// немного «живых» данных, чтобы картинка была честной
|
// немного «живых» данных, чтобы картинка была честной
|
||||||
int i = 0;
|
int i = 0;
|
||||||
@@ -526,6 +527,14 @@ namespace SbLauncher
|
|||||||
root2.UpdateLayout();
|
root2.UpdateLayout();
|
||||||
Pump(); // дать ItemsControl создать карточки
|
Pump(); // дать ItemsControl создать карточки
|
||||||
root2.UpdateLayout();
|
root2.UpdateLayout();
|
||||||
|
ToggleButton modeSwitch = w.FindName("WireguardSwitch") as ToggleButton;
|
||||||
|
if (modeSwitch != null) UpdateSwitch(modeSwitch, false);
|
||||||
|
ListBox serverList = w.FindName("ServerList") as ListBox;
|
||||||
|
FrameworkElement serverHost = w.FindName("ServerListHost") as FrameworkElement;
|
||||||
|
Border selection = w.FindName("ServerSelectionWindow") as Border;
|
||||||
|
if (serverList != null && serverHost != null && selection != null)
|
||||||
|
MoveServerSelection(serverList, serverHost, selection, false);
|
||||||
|
root2.UpdateLayout();
|
||||||
|
|
||||||
RenderTargetBitmap rtb = new RenderTargetBitmap(
|
RenderTargetBitmap rtb = new RenderTargetBitmap(
|
||||||
(int)(ww * 1.5), (int)(hh * 1.5), 144, 144, PixelFormats.Pbgra32);
|
(int)(ww * 1.5), (int)(hh * 1.5), 144, 144, PixelFormats.Pbgra32);
|
||||||
@@ -608,8 +617,6 @@ namespace SbLauncher
|
|||||||
{
|
{
|
||||||
Image li = w.FindName("LogoImg") as Image;
|
Image li = w.FindName("LogoImg") as Image;
|
||||||
if (li != null) li.Source = logo;
|
if (li != null) li.Source = logo;
|
||||||
Image statusLogo = w.FindName("StatusLogoImg") as Image;
|
|
||||||
if (statusLogo != null) statusLogo.Source = logo;
|
|
||||||
try { w.Icon = logo; } catch { }
|
try { w.Icon = logo; } catch { }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -656,7 +663,127 @@ namespace SbLauncher
|
|||||||
if (log != null)
|
if (log != null)
|
||||||
log.TextChanged += delegate { log.ScrollToEnd(); };
|
log.TextChanged += delegate { log.ScrollToEnd(); };
|
||||||
|
|
||||||
|
WireSwitch(w);
|
||||||
|
WireServerSelection(w);
|
||||||
w.Loaded += delegate { vm.PingAll(); };
|
w.Loaded += delegate { vm.PingAll(); };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void WireSwitch(Window w)
|
||||||
|
{
|
||||||
|
ToggleButton toggle = w.FindName("WireguardSwitch") as ToggleButton;
|
||||||
|
if (toggle == null) return;
|
||||||
|
|
||||||
|
// При загрузке положение выставляется сразу; последующие изменения
|
||||||
|
// идут от текущего положения, поэтому быстрые клики не дают скачка.
|
||||||
|
toggle.Loaded += delegate { UpdateSwitch(toggle, false); };
|
||||||
|
toggle.Checked += delegate { UpdateSwitch(toggle, true); };
|
||||||
|
toggle.Unchecked += delegate { UpdateSwitch(toggle, true); };
|
||||||
|
}
|
||||||
|
|
||||||
|
static void UpdateSwitch(ToggleButton toggle, bool animate)
|
||||||
|
{
|
||||||
|
if (!toggle.IsLoaded && animate) return;
|
||||||
|
toggle.ApplyTemplate();
|
||||||
|
Border active = toggle.Template.FindName("ActiveTrack", toggle) as Border;
|
||||||
|
System.Windows.Shapes.Ellipse thumb =
|
||||||
|
toggle.Template.FindName("Thumb", toggle) as System.Windows.Shapes.Ellipse;
|
||||||
|
TranslateTransform shift = thumb != null ? thumb.RenderTransform as TranslateTransform : null;
|
||||||
|
if (active == null || shift == null) return;
|
||||||
|
if (shift.IsFrozen)
|
||||||
|
{
|
||||||
|
shift = shift.Clone();
|
||||||
|
thumb.RenderTransform = shift;
|
||||||
|
}
|
||||||
|
|
||||||
|
double targetX = toggle.IsChecked == true ? 22 : 0;
|
||||||
|
double targetOpacity = toggle.IsChecked == true ? 1 : 0;
|
||||||
|
double fromX = shift.X, fromOpacity = active.Opacity;
|
||||||
|
shift.BeginAnimation(TranslateTransform.XProperty, null);
|
||||||
|
active.BeginAnimation(UIElement.OpacityProperty, null);
|
||||||
|
shift.X = targetX;
|
||||||
|
active.Opacity = targetOpacity;
|
||||||
|
if (!animate) return;
|
||||||
|
|
||||||
|
DoubleAnimation slide = new DoubleAnimation(fromX, targetX,
|
||||||
|
TimeSpan.FromMilliseconds(430));
|
||||||
|
slide.EasingFunction = new BackEase { Amplitude = 0.28, EasingMode = EasingMode.EaseOut };
|
||||||
|
slide.FillBehavior = FillBehavior.Stop;
|
||||||
|
shift.BeginAnimation(TranslateTransform.XProperty, slide);
|
||||||
|
|
||||||
|
DoubleAnimation color = new DoubleAnimation(fromOpacity, targetOpacity,
|
||||||
|
TimeSpan.FromMilliseconds(300));
|
||||||
|
color.EasingFunction = new CubicEase { EasingMode = EasingMode.EaseOut };
|
||||||
|
color.FillBehavior = FillBehavior.Stop;
|
||||||
|
active.BeginAnimation(UIElement.OpacityProperty, color);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void WireServerSelection(Window w)
|
||||||
|
{
|
||||||
|
ListBox list = w.FindName("ServerList") as ListBox;
|
||||||
|
FrameworkElement host = w.FindName("ServerListHost") as FrameworkElement;
|
||||||
|
Border highlight = w.FindName("ServerSelectionWindow") as Border;
|
||||||
|
if (list == null || host == null || highlight == null) return;
|
||||||
|
|
||||||
|
bool selectionPending = false;
|
||||||
|
int selectionRevision = 0;
|
||||||
|
list.Loaded += delegate { MoveServerSelection(list, host, highlight, false); };
|
||||||
|
list.SelectionChanged += delegate
|
||||||
|
{
|
||||||
|
selectionPending = true;
|
||||||
|
int revision = ++selectionRevision;
|
||||||
|
if (list.SelectedItem != null) list.ScrollIntoView(list.SelectedItem);
|
||||||
|
list.Dispatcher.BeginInvoke(DispatcherPriority.Loaded, new Action(delegate
|
||||||
|
{
|
||||||
|
if (revision != selectionRevision) return;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
list.UpdateLayout();
|
||||||
|
MoveServerSelection(list, host, highlight, true);
|
||||||
|
}
|
||||||
|
finally { selectionPending = false; }
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
list.AddHandler(ScrollViewer.ScrollChangedEvent, new ScrollChangedEventHandler(
|
||||||
|
delegate { if (!selectionPending) MoveServerSelection(list, host, highlight, false); }));
|
||||||
|
list.SizeChanged += delegate { if (!selectionPending) MoveServerSelection(list, host, highlight, false); };
|
||||||
|
}
|
||||||
|
|
||||||
|
static void MoveServerSelection(ListBox list, FrameworkElement host,
|
||||||
|
Border highlight, bool animate)
|
||||||
|
{
|
||||||
|
ListBoxItem item = list.ItemContainerGenerator.ContainerFromItem(list.SelectedItem) as ListBoxItem;
|
||||||
|
if (item == null || item.ActualHeight <= 10)
|
||||||
|
{
|
||||||
|
highlight.Visibility = Visibility.Collapsed;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Point p;
|
||||||
|
try { p = item.TransformToAncestor(host).Transform(new Point(0, 0)); }
|
||||||
|
catch (InvalidOperationException) { return; }
|
||||||
|
double height = item.ActualHeight - 10; // промежуток между карточками
|
||||||
|
if (p.Y + height < 0 || p.Y > host.ActualHeight)
|
||||||
|
{
|
||||||
|
highlight.Visibility = Visibility.Collapsed;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool wasVisible = highlight.Visibility == Visibility.Visible;
|
||||||
|
double fromY = Canvas.GetTop(highlight);
|
||||||
|
if (double.IsNaN(fromY)) fromY = p.Y;
|
||||||
|
highlight.BeginAnimation(Canvas.TopProperty, null);
|
||||||
|
Canvas.SetLeft(highlight, p.X);
|
||||||
|
Canvas.SetTop(highlight, p.Y);
|
||||||
|
highlight.Width = item.ActualWidth;
|
||||||
|
highlight.Height = height;
|
||||||
|
highlight.Visibility = Visibility.Visible;
|
||||||
|
if (!animate || !wasVisible || Math.Abs(fromY - p.Y) < 0.5) return;
|
||||||
|
|
||||||
|
DoubleAnimation slide = new DoubleAnimation(fromY, p.Y,
|
||||||
|
TimeSpan.FromMilliseconds(520));
|
||||||
|
slide.EasingFunction = new BackEase { Amplitude = 0.32, EasingMode = EasingMode.EaseOut };
|
||||||
|
slide.FillBehavior = FillBehavior.Stop;
|
||||||
|
highlight.BeginAnimation(Canvas.TopProperty, slide);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,14 @@
|
|||||||
|
|
||||||
Пока нет записей.
|
Пока нет записей.
|
||||||
|
|
||||||
|
## 0.1.1 — 2026-09-27
|
||||||
|
|
||||||
|
- Название окна и значка в трее приведено к `SbLauncher`; в шапке показывается версия сборки.
|
||||||
|
- Значок в блоке подключения заменён кнопкой питания.
|
||||||
|
- Кнопка очистки лога убрана из интерфейса.
|
||||||
|
- Переключатель WireGuard получил плавное движение с лёгкой отдачей; подсветка выбранного сервера скользит между карточками.
|
||||||
|
- Анимация подключения переделана: мягкий пульс во время ожидания и спокойное свечение при успешном подключении.
|
||||||
|
|
||||||
## 0.1.0 — 2026-09-26
|
## 0.1.0 — 2026-09-26
|
||||||
|
|
||||||
- Первый рабочий прототип WPF-лаунчера с интерфейсом glass.
|
- Первый рабочий прототип WPF-лаунчера с интерфейсом glass.
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# sb launcher · glass
|
# SbLauncher
|
||||||
|
|
||||||
Лаунчер для sing-box с интерфейсом WPF. Работает в Windows 10/11 x64 с
|
Лаунчер для sing-box с интерфейсом WPF. Работает в Windows 10/11 x64 с
|
||||||
.NET Framework 4.x. Поддерживает импорт и выбор серверов, три режима
|
.NET Framework 4.x. Поддерживает импорт и выбор серверов, три режима
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:sys="clr-namespace:System;assembly=mscorlib"
|
xmlns:sys="clr-namespace:System;assembly=mscorlib"
|
||||||
Title="sb launcher · glass preview" Width="1120" Height="790"
|
Title="SbLauncher" Width="1120" Height="790"
|
||||||
WindowStyle="None" AllowsTransparency="False" Background="{DynamicResource Bg}"
|
WindowStyle="None" AllowsTransparency="False" Background="{DynamicResource Bg}"
|
||||||
ResizeMode="NoResize" WindowStartupLocation="CenterScreen"
|
ResizeMode="NoResize" WindowStartupLocation="CenterScreen"
|
||||||
TextOptions.TextFormattingMode="Display"
|
TextOptions.TextFormattingMode="Display"
|
||||||
@@ -338,19 +338,38 @@
|
|||||||
<Setter Property="Template">
|
<Setter Property="Template">
|
||||||
<Setter.Value>
|
<Setter.Value>
|
||||||
<ControlTemplate TargetType="ToggleButton">
|
<ControlTemplate TargetType="ToggleButton">
|
||||||
<Grid Width="46" Height="26">
|
<Grid Width="52" Height="30">
|
||||||
<Border x:Name="Track" CornerRadius="13" Background="{DynamicResource SwitchTrack}"
|
<Border CornerRadius="15" Background="{DynamicResource SwitchTrack}"
|
||||||
BorderBrush="{DynamicResource Stroke}" BorderThickness="1"/>
|
BorderBrush="{DynamicResource Stroke}" BorderThickness="1"/>
|
||||||
<Ellipse x:Name="Thumb" Width="18" Height="18" Fill="{DynamicResource SwitchThumb}"
|
<Border x:Name="ActiveTrack" CornerRadius="15" Background="{DynamicResource Accent}"
|
||||||
HorizontalAlignment="Left" Margin="4,0,0,0"/>
|
BorderBrush="{DynamicResource Accent}" BorderThickness="1" Opacity="0"/>
|
||||||
|
<Ellipse x:Name="Thumb" Width="22" Height="22" Fill="{DynamicResource SwitchThumb}"
|
||||||
|
HorizontalAlignment="Left" VerticalAlignment="Center" Margin="4,0,0,0">
|
||||||
|
<Ellipse.RenderTransform><TranslateTransform/></Ellipse.RenderTransform>
|
||||||
|
<Ellipse.Effect>
|
||||||
|
<DropShadowEffect Color="#50000000" BlurRadius="4" ShadowDepth="1" Opacity="0.35"/>
|
||||||
|
</Ellipse.Effect>
|
||||||
|
</Ellipse>
|
||||||
</Grid>
|
</Grid>
|
||||||
<ControlTemplate.Triggers>
|
<ControlTemplate.Triggers>
|
||||||
<Trigger Property="IsChecked" Value="True">
|
<Trigger Property="IsPressed" Value="True">
|
||||||
<Setter TargetName="Track" Property="Background" Value="{DynamicResource Accent}"/>
|
<Setter TargetName="Thumb" Property="Width" Value="26"/>
|
||||||
<Setter TargetName="Track" Property="BorderBrush" Value="{DynamicResource Accent}"/>
|
<Trigger.EnterActions>
|
||||||
<Setter TargetName="Thumb" Property="Fill" Value="{DynamicResource OnAccent}"/>
|
<BeginStoryboard>
|
||||||
<Setter TargetName="Thumb" Property="HorizontalAlignment" Value="Right"/>
|
<Storyboard FillBehavior="Stop">
|
||||||
<Setter TargetName="Thumb" Property="Margin" Value="0,0,4,0"/>
|
<DoubleAnimation Storyboard.TargetName="Thumb" Storyboard.TargetProperty="Width"
|
||||||
|
From="22" To="26" Duration="0:0:0.12"/>
|
||||||
|
</Storyboard>
|
||||||
|
</BeginStoryboard>
|
||||||
|
</Trigger.EnterActions>
|
||||||
|
<Trigger.ExitActions>
|
||||||
|
<BeginStoryboard>
|
||||||
|
<Storyboard FillBehavior="Stop">
|
||||||
|
<DoubleAnimation Storyboard.TargetName="Thumb" Storyboard.TargetProperty="Width"
|
||||||
|
From="26" To="22" Duration="0:0:0.18"/>
|
||||||
|
</Storyboard>
|
||||||
|
</BeginStoryboard>
|
||||||
|
</Trigger.ExitActions>
|
||||||
</Trigger>
|
</Trigger>
|
||||||
</ControlTemplate.Triggers>
|
</ControlTemplate.Triggers>
|
||||||
</ControlTemplate>
|
</ControlTemplate>
|
||||||
@@ -419,10 +438,6 @@
|
|||||||
<Trigger SourceName="Card" Property="IsMouseOver" Value="True">
|
<Trigger SourceName="Card" Property="IsMouseOver" Value="True">
|
||||||
<Setter TargetName="Card" Property="Background" Value="{DynamicResource CardHover}"/>
|
<Setter TargetName="Card" Property="Background" Value="{DynamicResource CardHover}"/>
|
||||||
</Trigger>
|
</Trigger>
|
||||||
<DataTrigger Binding="{Binding IsSelected}" Value="True">
|
|
||||||
<Setter TargetName="Card" Property="Background" Value="{DynamicResource CardSel}"/>
|
|
||||||
<Setter TargetName="Card" Property="BorderBrush" Value="{DynamicResource Accent}"/>
|
|
||||||
</DataTrigger>
|
|
||||||
</DataTemplate.Triggers>
|
</DataTemplate.Triggers>
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
|
|
||||||
@@ -618,12 +633,12 @@
|
|||||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center" Margin="23,0,0,0">
|
<StackPanel Orientation="Horizontal" VerticalAlignment="Center" Margin="23,0,0,0">
|
||||||
<Image x:Name="LogoImg" Width="31" Height="31" VerticalAlignment="Center"
|
<Image x:Name="LogoImg" Width="31" Height="31" VerticalAlignment="Center"
|
||||||
RenderOptions.BitmapScalingMode="HighQuality"/>
|
RenderOptions.BitmapScalingMode="HighQuality"/>
|
||||||
<TextBlock Text="sb launcher" Foreground="{DynamicResource Text}"
|
<TextBlock Text="SbLauncher" Foreground="{DynamicResource Text}"
|
||||||
FontSize="15" FontWeight="SemiBold" Margin="11,0,0,0"
|
FontSize="15" FontWeight="SemiBold" Margin="11,0,0,0"
|
||||||
VerticalAlignment="Center"/>
|
VerticalAlignment="Center"/>
|
||||||
<Border Background="{DynamicResource SoftBg}" BorderBrush="{DynamicResource Stroke}"
|
<Border Background="{DynamicResource SoftBg}" BorderBrush="{DynamicResource Stroke}"
|
||||||
BorderThickness="1" CornerRadius="10" Margin="12,0,0,0" Padding="8,3">
|
BorderThickness="1" CornerRadius="10" Margin="12,0,0,0" Padding="8,3">
|
||||||
<TextBlock Text="glass preview" Foreground="{DynamicResource Muted}"
|
<TextBlock Text="{Binding VersionText}" Foreground="{DynamicResource Muted}"
|
||||||
FontSize="10" VerticalAlignment="Center"/>
|
FontSize="10" VerticalAlignment="Center"/>
|
||||||
</Border>
|
</Border>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
@@ -662,31 +677,40 @@
|
|||||||
Padding="10,4"/>
|
Padding="10,4"/>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<ListBox Grid.Row="1" ItemsSource="{Binding Servers}"
|
<Grid x:Name="ServerListHost" Grid.Row="1" ClipToBounds="True">
|
||||||
SelectedItem="{Binding Selected, Mode=TwoWay}"
|
<ListBox x:Name="ServerList" ItemsSource="{Binding Servers}"
|
||||||
ItemTemplate="{StaticResource ServerCard}"
|
SelectedItem="{Binding Selected, Mode=TwoWay}"
|
||||||
Background="Transparent" BorderThickness="0"
|
ItemTemplate="{StaticResource ServerCard}"
|
||||||
Margin="12,0,12,14" Padding="0,0,4,0"
|
Background="Transparent" BorderThickness="0"
|
||||||
ScrollViewer.VerticalScrollBarVisibility="Auto"
|
Margin="12,0,12,14" Padding="0,0,4,0"
|
||||||
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
|
ScrollViewer.VerticalScrollBarVisibility="Auto"
|
||||||
ScrollViewer.CanContentScroll="True"
|
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
|
||||||
VirtualizingStackPanel.IsVirtualizing="True"
|
ScrollViewer.CanContentScroll="True"
|
||||||
VirtualizingStackPanel.VirtualizationMode="Recycling">
|
VirtualizingStackPanel.IsVirtualizing="True"
|
||||||
<ListBox.ItemContainerStyle>
|
VirtualizingStackPanel.VirtualizationMode="Recycling">
|
||||||
<Style TargetType="ListBoxItem">
|
<ListBox.ItemContainerStyle>
|
||||||
<Setter Property="Padding" Value="0"/>
|
<Style TargetType="ListBoxItem">
|
||||||
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
|
<Setter Property="Padding" Value="0"/>
|
||||||
<Setter Property="Template">
|
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
|
||||||
<Setter.Value>
|
<Setter Property="Template">
|
||||||
<ControlTemplate TargetType="ListBoxItem">
|
<Setter.Value>
|
||||||
<ContentPresenter ContentSource="Content"
|
<ControlTemplate TargetType="ListBoxItem">
|
||||||
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"/>
|
<ContentPresenter ContentSource="Content"
|
||||||
</ControlTemplate>
|
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"/>
|
||||||
</Setter.Value>
|
</ControlTemplate>
|
||||||
</Setter>
|
</Setter.Value>
|
||||||
</Style>
|
</Setter>
|
||||||
</ListBox.ItemContainerStyle>
|
</Style>
|
||||||
</ListBox>
|
</ListBox.ItemContainerStyle>
|
||||||
|
</ListBox>
|
||||||
|
<Canvas IsHitTestVisible="False" ClipToBounds="True">
|
||||||
|
<Border x:Name="ServerSelectionWindow" Visibility="Collapsed"
|
||||||
|
CornerRadius="16" BorderBrush="{DynamicResource Accent}"
|
||||||
|
BorderThickness="1.5">
|
||||||
|
<Border CornerRadius="15" Background="{DynamicResource Accent}" Opacity="0.16"/>
|
||||||
|
</Border>
|
||||||
|
</Canvas>
|
||||||
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
@@ -714,8 +738,8 @@
|
|||||||
StrokeThickness="1"/>
|
StrokeThickness="1"/>
|
||||||
<Ellipse Width="62" Height="62" Stroke="{DynamicResource HeroRingDim}"
|
<Ellipse Width="62" Height="62" Stroke="{DynamicResource HeroRingDim}"
|
||||||
StrokeThickness="2" Margin="2"/>
|
StrokeThickness="2" Margin="2"/>
|
||||||
<!-- После удачного подключения — один спокойный световой импульс. -->
|
<!-- Мягкое заполнение после подключения вместо резкой вспышки. -->
|
||||||
<Ellipse Width="56" Height="56" Fill="{DynamicResource Ok}"
|
<Ellipse x:Name="ConnectionGlow" Width="58" Height="58" Fill="{DynamicResource Ok}"
|
||||||
RenderTransformOrigin="0.5,0.5" IsHitTestVisible="False">
|
RenderTransformOrigin="0.5,0.5" IsHitTestVisible="False">
|
||||||
<Ellipse.RenderTransform><ScaleTransform/></Ellipse.RenderTransform>
|
<Ellipse.RenderTransform><ScaleTransform/></Ellipse.RenderTransform>
|
||||||
<Ellipse.Style>
|
<Ellipse.Style>
|
||||||
@@ -723,15 +747,24 @@
|
|||||||
<Setter Property="Opacity" Value="0"/>
|
<Setter Property="Opacity" Value="0"/>
|
||||||
<Style.Triggers>
|
<Style.Triggers>
|
||||||
<DataTrigger Binding="{Binding ConnectionPhase}" Value="connected">
|
<DataTrigger Binding="{Binding ConnectionPhase}" Value="connected">
|
||||||
|
<Setter Property="Opacity" Value="0.18"/>
|
||||||
<DataTrigger.EnterActions>
|
<DataTrigger.EnterActions>
|
||||||
<BeginStoryboard>
|
<BeginStoryboard>
|
||||||
<Storyboard>
|
<Storyboard FillBehavior="Stop">
|
||||||
<DoubleAnimation Storyboard.TargetProperty="Opacity"
|
<DoubleAnimation Storyboard.TargetProperty="Opacity"
|
||||||
From="0.36" To="0" Duration="0:0:0.65"/>
|
From="0" To="0.18" Duration="0:0:0.55"/>
|
||||||
<DoubleAnimation Storyboard.TargetProperty="(UIElement.RenderTransform).(ScaleTransform.ScaleX)"
|
<DoubleAnimation Storyboard.TargetProperty="(UIElement.RenderTransform).(ScaleTransform.ScaleX)"
|
||||||
From="0.8" To="1.2" Duration="0:0:0.65"/>
|
From="0.82" To="1" Duration="0:0:0.55">
|
||||||
|
<DoubleAnimation.EasingFunction>
|
||||||
|
<BackEase Amplitude="0.28" EasingMode="EaseOut"/>
|
||||||
|
</DoubleAnimation.EasingFunction>
|
||||||
|
</DoubleAnimation>
|
||||||
<DoubleAnimation Storyboard.TargetProperty="(UIElement.RenderTransform).(ScaleTransform.ScaleY)"
|
<DoubleAnimation Storyboard.TargetProperty="(UIElement.RenderTransform).(ScaleTransform.ScaleY)"
|
||||||
From="0.8" To="1.2" Duration="0:0:0.65"/>
|
From="0.82" To="1" Duration="0:0:0.55">
|
||||||
|
<DoubleAnimation.EasingFunction>
|
||||||
|
<BackEase Amplitude="0.28" EasingMode="EaseOut"/>
|
||||||
|
</DoubleAnimation.EasingFunction>
|
||||||
|
</DoubleAnimation>
|
||||||
</Storyboard>
|
</Storyboard>
|
||||||
</BeginStoryboard>
|
</BeginStoryboard>
|
||||||
</DataTrigger.EnterActions>
|
</DataTrigger.EnterActions>
|
||||||
@@ -740,44 +773,80 @@
|
|||||||
</Style>
|
</Style>
|
||||||
</Ellipse.Style>
|
</Ellipse.Style>
|
||||||
</Ellipse>
|
</Ellipse>
|
||||||
<Ellipse Width="62" Height="62" Stroke="{DynamicResource HeroRing}"
|
<!-- Пока соединение меняется, ободок спокойно «дышит». -->
|
||||||
StrokeThickness="3" StrokeDashArray="2 5" Margin="2"
|
<Ellipse x:Name="ConnectionBreathRing" Width="58" Height="58" Stroke="{DynamicResource Accent}"
|
||||||
|
StrokeThickness="2.5" IsHitTestVisible="False"
|
||||||
RenderTransformOrigin="0.5,0.5">
|
RenderTransformOrigin="0.5,0.5">
|
||||||
<Ellipse.RenderTransform><RotateTransform/></Ellipse.RenderTransform>
|
<Ellipse.RenderTransform><ScaleTransform/></Ellipse.RenderTransform>
|
||||||
<Ellipse.Style>
|
<Ellipse.Style>
|
||||||
<Style TargetType="Ellipse">
|
<Style TargetType="Ellipse">
|
||||||
<Setter Property="Opacity" Value="0"/>
|
<Setter Property="Opacity" Value="0"/>
|
||||||
<Style.Triggers>
|
<Style.Triggers>
|
||||||
<DataTrigger Binding="{Binding ConnectionPhase}" Value="connecting">
|
<DataTrigger Binding="{Binding ConnectionInProgress}" Value="True">
|
||||||
<Setter Property="Opacity" Value="1"/>
|
|
||||||
<DataTrigger.EnterActions>
|
<DataTrigger.EnterActions>
|
||||||
<BeginStoryboard x:Name="ConnectOrbit">
|
<BeginStoryboard x:Name="ConnectionBreath">
|
||||||
<Storyboard RepeatBehavior="Forever">
|
<Storyboard RepeatBehavior="Forever" AutoReverse="True">
|
||||||
<DoubleAnimation Storyboard.TargetProperty="(UIElement.RenderTransform).(RotateTransform.Angle)"
|
<DoubleAnimation Storyboard.TargetProperty="Opacity"
|
||||||
From="0" To="360" Duration="0:0:2"/>
|
From="0.25" To="0.72" Duration="0:0:0.9">
|
||||||
|
<DoubleAnimation.EasingFunction><SineEase EasingMode="EaseInOut"/></DoubleAnimation.EasingFunction>
|
||||||
|
</DoubleAnimation>
|
||||||
|
<DoubleAnimation Storyboard.TargetProperty="(UIElement.RenderTransform).(ScaleTransform.ScaleX)"
|
||||||
|
From="0.9" To="1.08" Duration="0:0:0.9">
|
||||||
|
<DoubleAnimation.EasingFunction><SineEase EasingMode="EaseInOut"/></DoubleAnimation.EasingFunction>
|
||||||
|
</DoubleAnimation>
|
||||||
|
<DoubleAnimation Storyboard.TargetProperty="(UIElement.RenderTransform).(ScaleTransform.ScaleY)"
|
||||||
|
From="0.9" To="1.08" Duration="0:0:0.9">
|
||||||
|
<DoubleAnimation.EasingFunction><SineEase EasingMode="EaseInOut"/></DoubleAnimation.EasingFunction>
|
||||||
|
</DoubleAnimation>
|
||||||
</Storyboard>
|
</Storyboard>
|
||||||
</BeginStoryboard>
|
</BeginStoryboard>
|
||||||
</DataTrigger.EnterActions>
|
</DataTrigger.EnterActions>
|
||||||
<DataTrigger.ExitActions><StopStoryboard BeginStoryboardName="ConnectOrbit"/></DataTrigger.ExitActions>
|
<DataTrigger.ExitActions><StopStoryboard BeginStoryboardName="ConnectionBreath"/></DataTrigger.ExitActions>
|
||||||
</DataTrigger>
|
|
||||||
<DataTrigger Binding="{Binding ConnectionPhase}" Value="disconnecting">
|
|
||||||
<Setter Property="Opacity" Value="1"/>
|
|
||||||
<DataTrigger.EnterActions>
|
|
||||||
<BeginStoryboard x:Name="DisconnectOrbit">
|
|
||||||
<Storyboard RepeatBehavior="Forever">
|
|
||||||
<DoubleAnimation Storyboard.TargetProperty="(UIElement.RenderTransform).(RotateTransform.Angle)"
|
|
||||||
From="360" To="0" Duration="0:0:1.4"/>
|
|
||||||
</Storyboard>
|
|
||||||
</BeginStoryboard>
|
|
||||||
</DataTrigger.EnterActions>
|
|
||||||
<DataTrigger.ExitActions><StopStoryboard BeginStoryboardName="DisconnectOrbit"/></DataTrigger.ExitActions>
|
|
||||||
</DataTrigger>
|
</DataTrigger>
|
||||||
</Style.Triggers>
|
</Style.Triggers>
|
||||||
</Style>
|
</Style>
|
||||||
</Ellipse.Style>
|
</Ellipse.Style>
|
||||||
</Ellipse>
|
</Ellipse>
|
||||||
<Image x:Name="StatusLogoImg" Width="38" Height="38"
|
<Button x:Name="BtnPower" Width="66" Height="66"
|
||||||
RenderOptions.BitmapScalingMode="HighQuality"/>
|
Command="{Binding ConnectCommand}" Cursor="Hand"
|
||||||
|
ToolTip="{Binding ConnectText}"
|
||||||
|
AutomationProperties.Name="{Binding ConnectText}">
|
||||||
|
<Button.Template>
|
||||||
|
<ControlTemplate TargetType="Button">
|
||||||
|
<Grid Background="Transparent">
|
||||||
|
<Viewbox Width="32" Height="32">
|
||||||
|
<Canvas Width="32" Height="32">
|
||||||
|
<Path x:Name="PowerRing"
|
||||||
|
Data="M 9,8 C 5,10 4,14 4,17 C 4,24 9,29 16,29 C 23,29 28,24 28,17 C 28,14 27,10 23,8"
|
||||||
|
Stroke="{DynamicResource Text}" StrokeThickness="3"
|
||||||
|
StrokeStartLineCap="Round" StrokeEndLineCap="Round"/>
|
||||||
|
<Path x:Name="PowerStem" Data="M 16,3 L 16,17"
|
||||||
|
Stroke="{DynamicResource Text}" StrokeThickness="3"
|
||||||
|
StrokeStartLineCap="Round" StrokeEndLineCap="Round"/>
|
||||||
|
</Canvas>
|
||||||
|
</Viewbox>
|
||||||
|
</Grid>
|
||||||
|
<ControlTemplate.Triggers>
|
||||||
|
<DataTrigger Binding="{Binding ConnectionPhase}" Value="connected">
|
||||||
|
<Setter TargetName="PowerRing" Property="Stroke" Value="{DynamicResource Ok}"/>
|
||||||
|
<Setter TargetName="PowerStem" Property="Stroke" Value="{DynamicResource Ok}"/>
|
||||||
|
</DataTrigger>
|
||||||
|
<DataTrigger Binding="{Binding ConnectionPhase}" Value="connecting">
|
||||||
|
<Setter TargetName="PowerRing" Property="Stroke" Value="{DynamicResource Accent}"/>
|
||||||
|
<Setter TargetName="PowerStem" Property="Stroke" Value="{DynamicResource Accent}"/>
|
||||||
|
</DataTrigger>
|
||||||
|
<Trigger Property="IsMouseOver" Value="True">
|
||||||
|
<Setter TargetName="PowerRing" Property="Stroke" Value="{DynamicResource Accent}"/>
|
||||||
|
<Setter TargetName="PowerStem" Property="Stroke" Value="{DynamicResource Accent}"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsPressed" Value="True">
|
||||||
|
<Setter TargetName="PowerRing" Property="Stroke" Value="{DynamicResource AccentDim}"/>
|
||||||
|
<Setter TargetName="PowerStem" Property="Stroke" Value="{DynamicResource AccentDim}"/>
|
||||||
|
</Trigger>
|
||||||
|
</ControlTemplate.Triggers>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Button.Template>
|
||||||
|
</Button>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<StackPanel Grid.Column="1" VerticalAlignment="Center" Margin="6,0,10,0">
|
<StackPanel Grid.Column="1" VerticalAlignment="Center" Margin="6,0,10,0">
|
||||||
@@ -808,7 +877,7 @@
|
|||||||
<TextBlock Foreground="{DynamicResource Muted}" FontSize="11" Margin="0,3,0,0"
|
<TextBlock Foreground="{DynamicResource Muted}" FontSize="11" Margin="0,3,0,0"
|
||||||
Text="вкл — VLESS поверх WireGuard · выкл — напрямую через сетевой адаптер"/>
|
Text="вкл — VLESS поверх WireGuard · выкл — напрямую через сетевой адаптер"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<ToggleButton Style="{StaticResource Switch}"
|
<ToggleButton x:Name="WireguardSwitch" Style="{StaticResource Switch}"
|
||||||
IsChecked="{Binding WireguardOn, Mode=TwoWay}"
|
IsChecked="{Binding WireguardOn, Mode=TwoWay}"
|
||||||
HorizontalAlignment="Right" VerticalAlignment="Center"/>
|
HorizontalAlignment="Right" VerticalAlignment="Center"/>
|
||||||
</Grid>
|
</Grid>
|
||||||
@@ -822,10 +891,6 @@
|
|||||||
<Button x:Name="BtnTabSettings" Style="{StaticResource Tab}" Content="НАСТРОЙКИ"
|
<Button x:Name="BtnTabSettings" Style="{StaticResource Tab}" Content="НАСТРОЙКИ"
|
||||||
Foreground="{Binding TabSettingsBrush}"/>
|
Foreground="{Binding TabSettingsBrush}"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<Button Style="{StaticResource Soft}" Content="Очистить лог"
|
|
||||||
Command="{Binding ClearLogCommand}" Visibility="{Binding LogVis}"
|
|
||||||
HorizontalAlignment="Right" VerticalAlignment="Center"
|
|
||||||
ToolTip="Удалить записи из окна лога"/>
|
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<!-- содержимое вкладки: два слоя, видим всегда ровно один -->
|
<!-- содержимое вкладки: два слоя, видим всегда ровно один -->
|
||||||
|
|||||||
+3
-3
@@ -1,6 +1,6 @@
|
|||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
|
||||||
// Обновляйте вместе с CHANGELOG.md перед выпуском новой версии.
|
// Обновляйте вместе с CHANGELOG.md перед выпуском новой версии.
|
||||||
[assembly: AssemblyVersion("0.1.0.0")]
|
[assembly: AssemblyVersion("0.1.1.0")]
|
||||||
[assembly: AssemblyFileVersion("0.1.0.0")]
|
[assembly: AssemblyFileVersion("0.1.1.0")]
|
||||||
[assembly: AssemblyInformationalVersion("0.1.0")]
|
[assembly: AssemblyInformationalVersion("0.1.1")]
|
||||||
|
|||||||
+21
-1
@@ -299,13 +299,33 @@ namespace SbLauncher
|
|||||||
public ICommand DeleteSelectedCommand { get; private set; }
|
public ICommand DeleteSelectedCommand { get; private set; }
|
||||||
|
|
||||||
// ── статус ───────────────────────────────────────────────────────
|
// ── статус ───────────────────────────────────────────────────────
|
||||||
|
public string VersionText
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
Assembly assembly = Assembly.GetExecutingAssembly();
|
||||||
|
AssemblyInformationalVersionAttribute version =
|
||||||
|
(AssemblyInformationalVersionAttribute)Attribute.GetCustomAttribute(
|
||||||
|
assembly, typeof(AssemblyInformationalVersionAttribute));
|
||||||
|
return "v" + (version != null ? version.InformationalVersion :
|
||||||
|
assembly.GetName().Version.ToString(3));
|
||||||
|
}
|
||||||
|
}
|
||||||
public string StatusTitle { get { return _statusTitle; } set { _statusTitle = value; Raise(); } }
|
public string StatusTitle { get { return _statusTitle; } set { _statusTitle = value; Raise(); } }
|
||||||
public string StatusSub { get { return _statusSub; } set { _statusSub = value; Raise(); } }
|
public string StatusSub { get { return _statusSub; } set { _statusSub = value; Raise(); } }
|
||||||
public string StatusKey { set { _statusKey = value; Raise("StatusBrush"); } }
|
public string StatusKey { set { _statusKey = value; Raise("StatusBrush"); } }
|
||||||
public Brush StatusBrush { get { return Theme.Res(_statusKey); } }
|
public Brush StatusBrush { get { return Theme.Res(_statusKey); } }
|
||||||
public string ConnectText { get { return _connectText; } set { _connectText = value; Raise(); } }
|
public string ConnectText { get { return _connectText; } set { _connectText = value; Raise(); } }
|
||||||
// Лёгкая анимация в XAML зависит только от фазы, без таймеров на UI-потоке.
|
// Лёгкая анимация в XAML зависит только от фазы, без таймеров на UI-потоке.
|
||||||
public string ConnectionPhase { get { return _connectionPhase; } set { _connectionPhase = value; Raise(); } }
|
public string ConnectionPhase
|
||||||
|
{
|
||||||
|
get { return _connectionPhase; }
|
||||||
|
set { _connectionPhase = value; Raise(); Raise("ConnectionInProgress"); }
|
||||||
|
}
|
||||||
|
public bool ConnectionInProgress
|
||||||
|
{
|
||||||
|
get { return _connectionPhase == "connecting" || _connectionPhase == "disconnecting"; }
|
||||||
|
}
|
||||||
public bool IsConnected { get { return _runner.IsRunning; } }
|
public bool IsConnected { get { return _runner.IsRunning; } }
|
||||||
public string LogText { get { return _log; } set { _log = value; Raise(); } }
|
public string LogText { get { return _log; } set { _log = value; Raise(); } }
|
||||||
public Brush LogBrush { get { return Theme.Res("LogText"); } }
|
public Brush LogBrush { get { return Theme.Res("LogText"); } }
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
|
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
<assemblyIdentity version="0.1.0.0" name="SbLauncher"/>
|
<assemblyIdentity version="0.1.1.0" name="SbLauncher"/>
|
||||||
|
|
||||||
<!-- TUN + auto_route требуют администратора.
|
<!-- TUN + auto_route требуют администратора.
|
||||||
Цена: UAC при каждом запуске и автозапуск только через Планировщик
|
Цена: UAC при каждом запуске и автозапуск только через Планировщик
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 397 KiB After Width: | Height: | Size: 350 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 396 KiB After Width: | Height: | Size: 352 KiB |
+3
-2
@@ -26,10 +26,11 @@ try {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$tag = 'v' + $Version
|
$tag = 'v' + $Version
|
||||||
$tagCommit = & git rev-list -n 1 $tag 2>$null
|
& git show-ref --verify --quiet ("refs/tags/" + $tag)
|
||||||
if ($LASTEXITCODE -eq 0) {
|
if ($LASTEXITCODE -eq 0) {
|
||||||
|
$tagCommit = (& git rev-list -n 1 $tag).Trim()
|
||||||
$headCommit = (& git rev-parse HEAD).Trim()
|
$headCommit = (& git rev-parse HEAD).Trim()
|
||||||
if ($tagCommit.Trim() -ne $headCommit) {
|
if ($tagCommit -ne $headCommit) {
|
||||||
throw "Тег $tag уже указывает на другой коммит. Выберите новую версию."
|
throw "Тег $tag уже указывает на другой коммит. Выберите новую версию."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user