Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b2ae91b5b3 |
@@ -3,6 +3,9 @@
|
||||
local.json
|
||||
profiles.json
|
||||
|
||||
# локальные ярлыки Windows
|
||||
*.lnk
|
||||
|
||||
# ── рантайм ──────────────────────────────────────────────────────────
|
||||
# сюда лаунчер генерирует config.json перед запуском
|
||||
run/
|
||||
|
||||
@@ -33,10 +33,12 @@ using System.Threading.Tasks;
|
||||
using System.Web.Script.Serialization;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Controls.Primitives;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Interop;
|
||||
using System.Windows.Markup;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Animation;
|
||||
using System.Windows.Media.Imaging;
|
||||
using System.Windows.Threading;
|
||||
|
||||
@@ -147,7 +149,7 @@ namespace SbLauncher
|
||||
|
||||
_ni = new System.Windows.Forms.NotifyIcon();
|
||||
_ni.Icon = LoadIcon();
|
||||
_ni.Text = "sb launcher";
|
||||
_ni.Text = "SbLauncher";
|
||||
_ni.Visible = true;
|
||||
_ni.DoubleClick += delegate { Restore(); };
|
||||
|
||||
@@ -497,13 +499,12 @@ namespace SbLauncher
|
||||
vm.SetTheme(themeArg != null ? themeArg : Theme.Dark);
|
||||
vm.Load(paths);
|
||||
vm.ShowSettings = settingsTab;
|
||||
vm.AppendLog("демо-рендер интерфейса — sing-box не запускался");
|
||||
vm.ClearLog();
|
||||
vm.AppendLog("демонстрация интерфейса — sing-box не запускался");
|
||||
|
||||
ImageSource logo = LoadLogo();
|
||||
Image li = w.FindName("LogoImg") as Image;
|
||||
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;
|
||||
@@ -526,6 +527,14 @@ namespace SbLauncher
|
||||
root2.UpdateLayout();
|
||||
Pump(); // дать ItemsControl создать карточки
|
||||
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(
|
||||
(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;
|
||||
if (li != null) li.Source = logo;
|
||||
Image statusLogo = w.FindName("StatusLogoImg") as Image;
|
||||
if (statusLogo != null) statusLogo.Source = logo;
|
||||
try { w.Icon = logo; } catch { }
|
||||
}
|
||||
|
||||
@@ -656,7 +663,127 @@ namespace SbLauncher
|
||||
if (log != null)
|
||||
log.TextChanged += delegate { log.ScrollToEnd(); };
|
||||
|
||||
WireSwitch(w);
|
||||
WireServerSelection(w);
|
||||
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
|
||||
|
||||
- Первый рабочий прототип WPF-лаунчера с интерфейсом glass.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# sb launcher · glass
|
||||
# SbLauncher
|
||||
|
||||
Лаунчер для sing-box с интерфейсом WPF. Работает в Windows 10/11 x64 с
|
||||
.NET Framework 4.x. Поддерживает импорт и выбор серверов, три режима
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
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}"
|
||||
ResizeMode="NoResize" WindowStartupLocation="CenterScreen"
|
||||
TextOptions.TextFormattingMode="Display"
|
||||
@@ -338,19 +338,38 @@
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="ToggleButton">
|
||||
<Grid Width="46" Height="26">
|
||||
<Border x:Name="Track" CornerRadius="13" Background="{DynamicResource SwitchTrack}"
|
||||
<Grid Width="52" Height="30">
|
||||
<Border CornerRadius="15" Background="{DynamicResource SwitchTrack}"
|
||||
BorderBrush="{DynamicResource Stroke}" BorderThickness="1"/>
|
||||
<Ellipse x:Name="Thumb" Width="18" Height="18" Fill="{DynamicResource SwitchThumb}"
|
||||
HorizontalAlignment="Left" Margin="4,0,0,0"/>
|
||||
<Border x:Name="ActiveTrack" CornerRadius="15" Background="{DynamicResource Accent}"
|
||||
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>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsChecked" Value="True">
|
||||
<Setter TargetName="Track" Property="Background" Value="{DynamicResource Accent}"/>
|
||||
<Setter TargetName="Track" Property="BorderBrush" Value="{DynamicResource Accent}"/>
|
||||
<Setter TargetName="Thumb" Property="Fill" Value="{DynamicResource OnAccent}"/>
|
||||
<Setter TargetName="Thumb" Property="HorizontalAlignment" Value="Right"/>
|
||||
<Setter TargetName="Thumb" Property="Margin" Value="0,0,4,0"/>
|
||||
<Trigger Property="IsPressed" Value="True">
|
||||
<Setter TargetName="Thumb" Property="Width" Value="26"/>
|
||||
<Trigger.EnterActions>
|
||||
<BeginStoryboard>
|
||||
<Storyboard FillBehavior="Stop">
|
||||
<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>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
@@ -419,10 +438,6 @@
|
||||
<Trigger SourceName="Card" Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="Card" Property="Background" Value="{DynamicResource CardHover}"/>
|
||||
</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>
|
||||
|
||||
@@ -618,12 +633,12 @@
|
||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center" Margin="23,0,0,0">
|
||||
<Image x:Name="LogoImg" Width="31" Height="31" VerticalAlignment="Center"
|
||||
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"
|
||||
VerticalAlignment="Center"/>
|
||||
<Border Background="{DynamicResource SoftBg}" BorderBrush="{DynamicResource Stroke}"
|
||||
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"/>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
@@ -662,31 +677,40 @@
|
||||
Padding="10,4"/>
|
||||
</Grid>
|
||||
|
||||
<ListBox Grid.Row="1" ItemsSource="{Binding Servers}"
|
||||
SelectedItem="{Binding Selected, Mode=TwoWay}"
|
||||
ItemTemplate="{StaticResource ServerCard}"
|
||||
Background="Transparent" BorderThickness="0"
|
||||
Margin="12,0,12,14" Padding="0,0,4,0"
|
||||
ScrollViewer.VerticalScrollBarVisibility="Auto"
|
||||
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
|
||||
ScrollViewer.CanContentScroll="True"
|
||||
VirtualizingStackPanel.IsVirtualizing="True"
|
||||
VirtualizingStackPanel.VirtualizationMode="Recycling">
|
||||
<ListBox.ItemContainerStyle>
|
||||
<Style TargetType="ListBoxItem">
|
||||
<Setter Property="Padding" Value="0"/>
|
||||
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="ListBoxItem">
|
||||
<ContentPresenter ContentSource="Content"
|
||||
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"/>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
</ListBox.ItemContainerStyle>
|
||||
</ListBox>
|
||||
<Grid x:Name="ServerListHost" Grid.Row="1" ClipToBounds="True">
|
||||
<ListBox x:Name="ServerList" ItemsSource="{Binding Servers}"
|
||||
SelectedItem="{Binding Selected, Mode=TwoWay}"
|
||||
ItemTemplate="{StaticResource ServerCard}"
|
||||
Background="Transparent" BorderThickness="0"
|
||||
Margin="12,0,12,14" Padding="0,0,4,0"
|
||||
ScrollViewer.VerticalScrollBarVisibility="Auto"
|
||||
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
|
||||
ScrollViewer.CanContentScroll="True"
|
||||
VirtualizingStackPanel.IsVirtualizing="True"
|
||||
VirtualizingStackPanel.VirtualizationMode="Recycling">
|
||||
<ListBox.ItemContainerStyle>
|
||||
<Style TargetType="ListBoxItem">
|
||||
<Setter Property="Padding" Value="0"/>
|
||||
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="ListBoxItem">
|
||||
<ContentPresenter ContentSource="Content"
|
||||
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"/>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
</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>
|
||||
</Border>
|
||||
|
||||
@@ -714,8 +738,8 @@
|
||||
StrokeThickness="1"/>
|
||||
<Ellipse Width="62" Height="62" Stroke="{DynamicResource HeroRingDim}"
|
||||
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">
|
||||
<Ellipse.RenderTransform><ScaleTransform/></Ellipse.RenderTransform>
|
||||
<Ellipse.Style>
|
||||
@@ -723,15 +747,24 @@
|
||||
<Setter Property="Opacity" Value="0"/>
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding ConnectionPhase}" Value="connected">
|
||||
<Setter Property="Opacity" Value="0.18"/>
|
||||
<DataTrigger.EnterActions>
|
||||
<BeginStoryboard>
|
||||
<Storyboard>
|
||||
<Storyboard FillBehavior="Stop">
|
||||
<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)"
|
||||
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)"
|
||||
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>
|
||||
</BeginStoryboard>
|
||||
</DataTrigger.EnterActions>
|
||||
@@ -740,44 +773,80 @@
|
||||
</Style>
|
||||
</Ellipse.Style>
|
||||
</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">
|
||||
<Ellipse.RenderTransform><RotateTransform/></Ellipse.RenderTransform>
|
||||
<Ellipse.RenderTransform><ScaleTransform/></Ellipse.RenderTransform>
|
||||
<Ellipse.Style>
|
||||
<Style TargetType="Ellipse">
|
||||
<Setter Property="Opacity" Value="0"/>
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding ConnectionPhase}" Value="connecting">
|
||||
<Setter Property="Opacity" Value="1"/>
|
||||
<DataTrigger Binding="{Binding ConnectionInProgress}" Value="True">
|
||||
<DataTrigger.EnterActions>
|
||||
<BeginStoryboard x:Name="ConnectOrbit">
|
||||
<Storyboard RepeatBehavior="Forever">
|
||||
<DoubleAnimation Storyboard.TargetProperty="(UIElement.RenderTransform).(RotateTransform.Angle)"
|
||||
From="0" To="360" Duration="0:0:2"/>
|
||||
<BeginStoryboard x:Name="ConnectionBreath">
|
||||
<Storyboard RepeatBehavior="Forever" AutoReverse="True">
|
||||
<DoubleAnimation Storyboard.TargetProperty="Opacity"
|
||||
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>
|
||||
</BeginStoryboard>
|
||||
</DataTrigger.EnterActions>
|
||||
<DataTrigger.ExitActions><StopStoryboard BeginStoryboardName="ConnectOrbit"/></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.ExitActions><StopStoryboard BeginStoryboardName="ConnectionBreath"/></DataTrigger.ExitActions>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Ellipse.Style>
|
||||
</Ellipse>
|
||||
<Image x:Name="StatusLogoImg" Width="38" Height="38"
|
||||
RenderOptions.BitmapScalingMode="HighQuality"/>
|
||||
<Button x:Name="BtnPower" Width="66" Height="66"
|
||||
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>
|
||||
|
||||
<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"
|
||||
Text="вкл — VLESS поверх WireGuard · выкл — напрямую через сетевой адаптер"/>
|
||||
</StackPanel>
|
||||
<ToggleButton Style="{StaticResource Switch}"
|
||||
<ToggleButton x:Name="WireguardSwitch" Style="{StaticResource Switch}"
|
||||
IsChecked="{Binding WireguardOn, Mode=TwoWay}"
|
||||
HorizontalAlignment="Right" VerticalAlignment="Center"/>
|
||||
</Grid>
|
||||
@@ -822,10 +891,6 @@
|
||||
<Button x:Name="BtnTabSettings" Style="{StaticResource Tab}" Content="НАСТРОЙКИ"
|
||||
Foreground="{Binding TabSettingsBrush}"/>
|
||||
</StackPanel>
|
||||
<Button Style="{StaticResource Soft}" Content="Очистить лог"
|
||||
Command="{Binding ClearLogCommand}" Visibility="{Binding LogVis}"
|
||||
HorizontalAlignment="Right" VerticalAlignment="Center"
|
||||
ToolTip="Удалить записи из окна лога"/>
|
||||
</Grid>
|
||||
|
||||
<!-- содержимое вкладки: два слоя, видим всегда ровно один -->
|
||||
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
using System.Reflection;
|
||||
|
||||
// Обновляйте вместе с CHANGELOG.md перед выпуском новой версии.
|
||||
[assembly: AssemblyVersion("0.1.0.0")]
|
||||
[assembly: AssemblyFileVersion("0.1.0.0")]
|
||||
[assembly: AssemblyInformationalVersion("0.1.0")]
|
||||
[assembly: AssemblyVersion("0.1.1.0")]
|
||||
[assembly: AssemblyFileVersion("0.1.1.0")]
|
||||
[assembly: AssemblyInformationalVersion("0.1.1")]
|
||||
|
||||
+21
-1
@@ -299,13 +299,33 @@ namespace SbLauncher
|
||||
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 StatusSub { get { return _statusSub; } set { _statusSub = value; Raise(); } }
|
||||
public string StatusKey { set { _statusKey = value; Raise("StatusBrush"); } }
|
||||
public Brush StatusBrush { get { return Theme.Res(_statusKey); } }
|
||||
public string ConnectText { get { return _connectText; } set { _connectText = value; Raise(); } }
|
||||
// Лёгкая анимация в 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 string LogText { get { return _log; } set { _log = value; Raise(); } }
|
||||
public Brush LogBrush { get { return Theme.Res("LogText"); } }
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<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 требуют администратора.
|
||||
Цена: 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
|
||||
$tagCommit = & git rev-list -n 1 $tag 2>$null
|
||||
& git show-ref --verify --quiet ("refs/tags/" + $tag)
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
$tagCommit = (& git rev-list -n 1 $tag).Trim()
|
||||
$headCommit = (& git rev-parse HEAD).Trim()
|
||||
if ($tagCommit.Trim() -ne $headCommit) {
|
||||
if ($tagCommit -ne $headCommit) {
|
||||
throw "Тег $tag уже указывает на другой коммит. Выберите новую версию."
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user