v0.3.0: CI dotnet 8.0.424 pinned with isolated install
- Pin .NET SDK to 8.0.424 in all CI jobs to avoid floating channel drift - Install SDK to runner temp via DOTNET_INSTALL_DIR to bypass C:\\Program Files\\dotnet overwrite/extract conflicts on Gitea Windows runners - Simplify release job to dotnet publish + upload-artifact - Exclude memory-bank/ via .gitignore (internal dev memory stays local)
This commit is contained in:
18 files changed
+4122
No files matched your search
@@ -0,0 +1,433 @@
|
||||
<Application x:Class="RgbLedStripController.App"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
StartupUri="MainWindow.xaml">
|
||||
<Application.Resources>
|
||||
<Color x:Key="BackgroundColor">#FF090D12</Color>
|
||||
<Color x:Key="BackgroundSecondaryColor">#FF101721</Color>
|
||||
<Color x:Key="CardColor">#FF121923</Color>
|
||||
<Color x:Key="CardMutedColor">#FF161F2B</Color>
|
||||
<Color x:Key="CardBorderColor">#1FFFFFFF</Color>
|
||||
<Color x:Key="TextColor">#FFF4F7FB</Color>
|
||||
<Color x:Key="MutedTextColor">#FF9AA8BA</Color>
|
||||
<Color x:Key="AccentColor">#FF4CC2FF</Color>
|
||||
<Color x:Key="AccentDeepColor">#FF198BB7</Color>
|
||||
<Color x:Key="AccentWarmColor">#FFFF7A59</Color>
|
||||
<Color x:Key="SuccessColor">#FF3DD598</Color>
|
||||
<Color x:Key="WarningColor">#FFF6C453</Color>
|
||||
<Color x:Key="DangerColor">#FFFF6B7A</Color>
|
||||
<Color x:Key="SecondaryColor">#FF222E3F</Color>
|
||||
<Color x:Key="InputColor">#FF0E151F</Color>
|
||||
<Color x:Key="InputHoverColor">#FF121B28</Color>
|
||||
|
||||
<SolidColorBrush x:Key="BackgroundBrush" Color="{StaticResource BackgroundColor}" />
|
||||
<SolidColorBrush x:Key="BackgroundSecondaryBrush" Color="{StaticResource BackgroundSecondaryColor}" />
|
||||
<SolidColorBrush x:Key="CardBrush" Color="{StaticResource CardColor}" />
|
||||
<SolidColorBrush x:Key="CardMutedBrush" Color="{StaticResource CardMutedColor}" />
|
||||
<SolidColorBrush x:Key="CardBorderBrush" Color="{StaticResource CardBorderColor}" />
|
||||
<SolidColorBrush x:Key="TextBrush" Color="{StaticResource TextColor}" />
|
||||
<SolidColorBrush x:Key="MutedTextBrush" Color="{StaticResource MutedTextColor}" />
|
||||
<SolidColorBrush x:Key="AccentBrush" Color="{StaticResource AccentColor}" />
|
||||
<SolidColorBrush x:Key="AccentDeepBrush" Color="{StaticResource AccentDeepColor}" />
|
||||
<SolidColorBrush x:Key="AccentWarmBrush" Color="{StaticResource AccentWarmColor}" />
|
||||
<SolidColorBrush x:Key="SecondaryBrush" Color="{StaticResource SecondaryColor}" />
|
||||
<SolidColorBrush x:Key="InputBrush" Color="{StaticResource InputColor}" />
|
||||
<SolidColorBrush x:Key="InputHoverBrush" Color="{StaticResource InputHoverColor}" />
|
||||
|
||||
<SolidColorBrush x:Key="{x:Static SystemColors.WindowBrushKey}" Color="{StaticResource InputColor}" />
|
||||
<SolidColorBrush x:Key="{x:Static SystemColors.ControlBrushKey}" Color="{StaticResource InputColor}" />
|
||||
<SolidColorBrush x:Key="{x:Static SystemColors.WindowTextBrushKey}" Color="{StaticResource TextColor}" />
|
||||
<SolidColorBrush x:Key="{x:Static SystemColors.ControlTextBrushKey}" Color="{StaticResource TextColor}" />
|
||||
<SolidColorBrush x:Key="{x:Static SystemColors.HighlightBrushKey}" Color="{StaticResource AccentColor}" />
|
||||
<SolidColorBrush x:Key="{x:Static SystemColors.HighlightTextBrushKey}" Color="#FF091018" />
|
||||
<SolidColorBrush x:Key="{x:Static SystemColors.GrayTextBrushKey}" Color="{StaticResource MutedTextColor}" />
|
||||
<SolidColorBrush x:Key="{x:Static SystemColors.InactiveSelectionHighlightBrushKey}" Color="#33243B53" />
|
||||
<SolidColorBrush x:Key="{x:Static SystemColors.InactiveSelectionHighlightTextBrushKey}" Color="{StaticResource TextColor}" />
|
||||
|
||||
<Style TargetType="Window">
|
||||
<Setter Property="Background" Value="{StaticResource BackgroundBrush}" />
|
||||
<Setter Property="Foreground" Value="{StaticResource TextBrush}" />
|
||||
<Setter Property="FontFamily" Value="Segoe UI Variable Display" />
|
||||
<Setter Property="UseLayoutRounding" Value="True" />
|
||||
<Setter Property="SnapsToDevicePixels" Value="True" />
|
||||
</Style>
|
||||
|
||||
<Style x:Key="CardBorderStyle" TargetType="Border">
|
||||
<Setter Property="Background" Value="{StaticResource CardBrush}" />
|
||||
<Setter Property="BorderBrush" Value="{StaticResource CardBorderBrush}" />
|
||||
<Setter Property="BorderThickness" Value="1" />
|
||||
<Setter Property="CornerRadius" Value="16" />
|
||||
<Setter Property="Padding" Value="20" />
|
||||
</Style>
|
||||
|
||||
<Style x:Key="PanelCardStyle" TargetType="Border" BasedOn="{StaticResource CardBorderStyle}">
|
||||
<Setter Property="Background" Value="{StaticResource CardMutedBrush}" />
|
||||
<Setter Property="CornerRadius" Value="14" />
|
||||
<Setter Property="Padding" Value="18" />
|
||||
</Style>
|
||||
|
||||
<Style x:Key="SectionTitleStyle" TargetType="TextBlock">
|
||||
<Setter Property="FontFamily" Value="Bahnschrift SemiCondensed" />
|
||||
<Setter Property="FontWeight" Value="SemiBold" />
|
||||
<Setter Property="Foreground" Value="{StaticResource TextBrush}" />
|
||||
</Style>
|
||||
|
||||
<Style TargetType="Button">
|
||||
<Setter Property="Foreground" Value="#FF061019" />
|
||||
<Setter Property="Background" Value="{StaticResource AccentBrush}" />
|
||||
<Setter Property="BorderBrush" Value="{StaticResource AccentDeepBrush}" />
|
||||
<Setter Property="BorderThickness" Value="1" />
|
||||
<Setter Property="Padding" Value="14,10" />
|
||||
<Setter Property="FontWeight" Value="SemiBold" />
|
||||
<Setter Property="Cursor" Value="Hand" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Border x:Name="ButtonBorder"
|
||||
Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
CornerRadius="12">
|
||||
<ContentPresenter HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Margin="{TemplateBinding Padding}" />
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="ButtonBorder" Property="Opacity" Value="0.96" />
|
||||
</Trigger>
|
||||
<Trigger Property="IsPressed" Value="True">
|
||||
<Setter TargetName="ButtonBorder" Property="Opacity" Value="0.84" />
|
||||
</Trigger>
|
||||
<Trigger Property="IsEnabled" Value="False">
|
||||
<Setter TargetName="ButtonBorder" Property="Opacity" Value="0.40" />
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style x:Key="GhostButtonStyle" TargetType="Button" BasedOn="{StaticResource {x:Type Button}}">
|
||||
<Setter Property="Foreground" Value="{StaticResource TextBrush}" />
|
||||
<Setter Property="Background" Value="{StaticResource SecondaryBrush}" />
|
||||
<Setter Property="BorderBrush" Value="#36FFFFFF" />
|
||||
</Style>
|
||||
|
||||
<Style x:Key="DangerGhostButtonStyle" TargetType="Button" BasedOn="{StaticResource GhostButtonStyle}">
|
||||
<Setter Property="Foreground" Value="#FFFFC3CB" />
|
||||
<Setter Property="BorderBrush" Value="#4DFF6B7A" />
|
||||
<Setter Property="Background" Value="#261E2431" />
|
||||
</Style>
|
||||
|
||||
<Style x:Key="WarmGhostButtonStyle" TargetType="Button" BasedOn="{StaticResource GhostButtonStyle}">
|
||||
<Setter Property="Foreground" Value="{StaticResource AccentWarmBrush}" />
|
||||
<Setter Property="BorderBrush" Value="#55FF7A59" />
|
||||
<Setter Property="Background" Value="#221E2B33" />
|
||||
</Style>
|
||||
|
||||
<Style x:Key="SwatchButtonStyle" TargetType="Button" BasedOn="{StaticResource {x:Type Button}}">
|
||||
<Setter Property="Width" Value="32" />
|
||||
<Setter Property="Height" Value="32" />
|
||||
<Setter Property="Padding" Value="0" />
|
||||
<Setter Property="BorderBrush" Value="#55FFFFFF" />
|
||||
</Style>
|
||||
|
||||
<Style TargetType="ListBox">
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
<Setter Property="BorderThickness" Value="0" />
|
||||
<Setter Property="Padding" Value="0" />
|
||||
<Setter Property="ScrollViewer.VerticalScrollBarVisibility" Value="Hidden" />
|
||||
<Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Disabled" />
|
||||
</Style>
|
||||
|
||||
<Style TargetType="ListBoxItem">
|
||||
<Setter Property="Padding" Value="0" />
|
||||
<Setter Property="Margin" Value="0,0,0,10" />
|
||||
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="ListBoxItem">
|
||||
<Border x:Name="ItemBorder"
|
||||
Background="#FF151D29"
|
||||
BorderBrush="#18FFFFFF"
|
||||
BorderThickness="1"
|
||||
CornerRadius="14"
|
||||
Padding="14">
|
||||
<ContentPresenter />
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsSelected" Value="True">
|
||||
<Setter TargetName="ItemBorder" Property="Background" Value="#FF182435" />
|
||||
<Setter TargetName="ItemBorder" Property="BorderBrush" Value="{StaticResource AccentBrush}" />
|
||||
</Trigger>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="ItemBorder" Property="Background" Value="#FF182131" />
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style TargetType="TextBox">
|
||||
<Setter Property="Background" Value="{StaticResource InputBrush}" />
|
||||
<Setter Property="Foreground" Value="{StaticResource TextBrush}" />
|
||||
<Setter Property="BorderBrush" Value="#30FFFFFF" />
|
||||
<Setter Property="BorderThickness" Value="1" />
|
||||
<Setter Property="Padding" Value="12,10" />
|
||||
<Setter Property="FontSize" Value="14" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="TextBox">
|
||||
<Border x:Name="TextBoxBorder"
|
||||
Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
CornerRadius="12">
|
||||
<ScrollViewer x:Name="PART_ContentHost"
|
||||
Margin="{TemplateBinding Padding}" />
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="TextBoxBorder" Property="Background" Value="{StaticResource InputHoverBrush}" />
|
||||
</Trigger>
|
||||
<Trigger Property="IsKeyboardFocused" Value="True">
|
||||
<Setter TargetName="TextBoxBorder" Property="BorderBrush" Value="{StaticResource AccentBrush}" />
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style TargetType="ComboBoxItem">
|
||||
<Setter Property="Foreground" Value="{StaticResource TextBrush}" />
|
||||
<Setter Property="Background" Value="{StaticResource InputBrush}" />
|
||||
<Setter Property="Padding" Value="12,8" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="ComboBoxItem">
|
||||
<Border x:Name="ItemBorder"
|
||||
Background="{TemplateBinding Background}"
|
||||
CornerRadius="10">
|
||||
<ContentPresenter Margin="{TemplateBinding Padding}" />
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsHighlighted" Value="True">
|
||||
<Setter TargetName="ItemBorder" Property="Background" Value="#FF1C2737" />
|
||||
</Trigger>
|
||||
<Trigger Property="IsSelected" Value="True">
|
||||
<Setter TargetName="ItemBorder" Property="Background" Value="{StaticResource AccentBrush}" />
|
||||
<Setter Property="Foreground" Value="#FF081019" />
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style TargetType="ComboBox">
|
||||
<Setter Property="Background" Value="{StaticResource InputBrush}" />
|
||||
<Setter Property="Foreground" Value="{StaticResource TextBrush}" />
|
||||
<Setter Property="BorderBrush" Value="#30FFFFFF" />
|
||||
<Setter Property="BorderThickness" Value="1" />
|
||||
<Setter Property="Padding" Value="10,8" />
|
||||
<Setter Property="FontSize" Value="14" />
|
||||
<Setter Property="MinHeight" Value="42" />
|
||||
<Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Disabled" />
|
||||
</Style>
|
||||
|
||||
<Style x:Key="EffectComboBoxToggleStyle" TargetType="ToggleButton">
|
||||
<Setter Property="Focusable" Value="False" />
|
||||
<Setter Property="Background" Value="{StaticResource InputBrush}" />
|
||||
<Setter Property="BorderBrush" Value="#30FFFFFF" />
|
||||
<Setter Property="BorderThickness" Value="1" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="ToggleButton">
|
||||
<Border x:Name="ToggleBorder"
|
||||
Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
CornerRadius="14">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<ContentPresenter Margin="16,12,12,12"
|
||||
VerticalAlignment="Center"
|
||||
RecognizesAccessKey="True" />
|
||||
|
||||
<Border Grid.Column="1"
|
||||
Width="34"
|
||||
Height="34"
|
||||
Margin="0,6,6,6"
|
||||
CornerRadius="10"
|
||||
Background="#16FFFFFF">
|
||||
<Path x:Name="Chevron"
|
||||
Width="10"
|
||||
Height="6"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Data="M 0 0 L 5 5 L 10 0"
|
||||
Stroke="{StaticResource TextBrush}"
|
||||
StrokeThickness="1.8"
|
||||
Stretch="Fill" />
|
||||
</Border>
|
||||
</Grid>
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="ToggleBorder" Property="Background" Value="{StaticResource InputHoverBrush}" />
|
||||
<Setter TargetName="ToggleBorder" Property="BorderBrush" Value="#46FFFFFF" />
|
||||
</Trigger>
|
||||
<Trigger Property="IsChecked" Value="True">
|
||||
<Setter TargetName="ToggleBorder" Property="BorderBrush" Value="{StaticResource AccentBrush}" />
|
||||
<Setter TargetName="ToggleBorder" Property="Background" Value="#FF122031" />
|
||||
</Trigger>
|
||||
<Trigger Property="IsEnabled" Value="False">
|
||||
<Setter TargetName="ToggleBorder" Property="Opacity" Value="0.45" />
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style x:Key="EffectComboBoxStyle" TargetType="ComboBox" BasedOn="{StaticResource {x:Type ComboBox}}">
|
||||
<Setter Property="Background" Value="#FF111925" />
|
||||
<Setter Property="BorderBrush" Value="#30FFFFFF" />
|
||||
<Setter Property="BorderThickness" Value="1" />
|
||||
<Setter Property="Padding" Value="0" />
|
||||
<Setter Property="MaxDropDownHeight" Value="300" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="ComboBox">
|
||||
<Grid>
|
||||
<ToggleButton x:Name="DropDownToggle"
|
||||
Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
ClickMode="Press"
|
||||
Focusable="False"
|
||||
IsChecked="{Binding IsDropDownOpen, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}"
|
||||
Style="{StaticResource EffectComboBoxToggleStyle}">
|
||||
<ContentPresenter Content="{TemplateBinding SelectionBoxItem}"
|
||||
ContentTemplate="{TemplateBinding SelectionBoxItemTemplate}"
|
||||
ContentTemplateSelector="{TemplateBinding ItemTemplateSelector}"
|
||||
IsHitTestVisible="False" />
|
||||
</ToggleButton>
|
||||
|
||||
<Popup x:Name="PART_Popup"
|
||||
AllowsTransparency="True"
|
||||
Focusable="False"
|
||||
IsOpen="{TemplateBinding IsDropDownOpen}"
|
||||
Placement="Bottom"
|
||||
PopupAnimation="Fade"
|
||||
StaysOpen="False">
|
||||
<Border x:Name="PopupBorder"
|
||||
MinWidth="{Binding ActualWidth, RelativeSource={RelativeSource TemplatedParent}}"
|
||||
MaxHeight="{TemplateBinding MaxDropDownHeight}"
|
||||
Margin="0,8,0,0"
|
||||
Padding="8"
|
||||
Background="#FF0F1723"
|
||||
BorderBrush="#30FFFFFF"
|
||||
BorderThickness="1"
|
||||
CornerRadius="16">
|
||||
<ScrollViewer CanContentScroll="True"
|
||||
SnapsToDevicePixels="True"
|
||||
VerticalScrollBarVisibility="Auto">
|
||||
<ItemsPresenter KeyboardNavigation.DirectionalNavigation="Contained" />
|
||||
</ScrollViewer>
|
||||
</Border>
|
||||
</Popup>
|
||||
</Grid>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsKeyboardFocusWithin" Value="True">
|
||||
<Setter TargetName="DropDownToggle" Property="BorderBrush" Value="{StaticResource AccentBrush}" />
|
||||
</Trigger>
|
||||
<Trigger Property="HasItems" Value="False">
|
||||
<Setter TargetName="PopupBorder" Property="MinHeight" Value="56" />
|
||||
</Trigger>
|
||||
<Trigger Property="IsEnabled" Value="False">
|
||||
<Setter TargetName="DropDownToggle" Property="Opacity" Value="0.45" />
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style TargetType="CheckBox">
|
||||
<Setter Property="Foreground" Value="{StaticResource TextBrush}" />
|
||||
<Setter Property="FontSize" Value="13" />
|
||||
</Style>
|
||||
|
||||
<Style x:Key="TargetCheckBoxStyle" TargetType="CheckBox" BasedOn="{StaticResource {x:Type CheckBox}}">
|
||||
<Setter Property="Cursor" Value="Hand" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="CheckBox">
|
||||
<Border x:Name="Shell"
|
||||
Padding="10,8"
|
||||
Background="#FF101824"
|
||||
BorderBrush="#24FFFFFF"
|
||||
BorderThickness="1"
|
||||
CornerRadius="12">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="10" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<Border x:Name="TickBox"
|
||||
Width="20"
|
||||
Height="20"
|
||||
Background="#FF0B121B"
|
||||
BorderBrush="#3AFFFFFF"
|
||||
BorderThickness="1"
|
||||
CornerRadius="7">
|
||||
<TextBlock x:Name="CheckGlyph"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
FontSize="13"
|
||||
FontWeight="Bold"
|
||||
Foreground="#FF061019"
|
||||
Text="✓"
|
||||
Visibility="Collapsed" />
|
||||
</Border>
|
||||
|
||||
<ContentPresenter Grid.Column="2"
|
||||
VerticalAlignment="Center"
|
||||
RecognizesAccessKey="True" />
|
||||
</Grid>
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="Shell" Property="BorderBrush" Value="#45FFFFFF" />
|
||||
</Trigger>
|
||||
<Trigger Property="IsChecked" Value="True">
|
||||
<Setter TargetName="Shell" Property="Background" Value="#FF142235" />
|
||||
<Setter TargetName="Shell" Property="BorderBrush" Value="{StaticResource AccentBrush}" />
|
||||
<Setter TargetName="TickBox" Property="Background" Value="{StaticResource AccentBrush}" />
|
||||
<Setter TargetName="TickBox" Property="BorderBrush" Value="{StaticResource AccentBrush}" />
|
||||
<Setter TargetName="CheckGlyph" Property="Visibility" Value="Visible" />
|
||||
</Trigger>
|
||||
<Trigger Property="IsEnabled" Value="False">
|
||||
<Setter TargetName="Shell" Property="Opacity" Value="0.45" />
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style TargetType="Slider">
|
||||
<Setter Property="Margin" Value="0,10,0,0" />
|
||||
</Style>
|
||||
</Application.Resources>
|
||||
</Application>
|
||||
@@ -0,0 +1,17 @@
|
||||
using System;
|
||||
using Velopack;
|
||||
|
||||
namespace RgbLedStripController;
|
||||
|
||||
public partial class App : System.Windows.Application
|
||||
{
|
||||
[STAThread]
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
VelopackApp.Build().Run();
|
||||
|
||||
var app = new App();
|
||||
app.InitializeComponent();
|
||||
app.Run();
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 21 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 21 KiB |
@@ -0,0 +1,491 @@
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.CompilerServices;
|
||||
using MediaBrush = System.Windows.Media.Brush;
|
||||
using MediaColor = System.Windows.Media.Color;
|
||||
using SolidColorBrush = System.Windows.Media.SolidColorBrush;
|
||||
|
||||
namespace RgbLedStripController.Core;
|
||||
|
||||
public enum DeviceConnectionStatus
|
||||
{
|
||||
Disconnected,
|
||||
Scanning,
|
||||
Connecting,
|
||||
Connected,
|
||||
Faulted
|
||||
}
|
||||
|
||||
public enum LightingMode
|
||||
{
|
||||
Rgb,
|
||||
Effect,
|
||||
Grayscale,
|
||||
Temperature
|
||||
}
|
||||
|
||||
public sealed class BleScanResult : ObservableObject
|
||||
{
|
||||
private string _alias = string.Empty;
|
||||
private bool _isKnownCandidate;
|
||||
private bool _isLastConnected;
|
||||
|
||||
public required string DeviceId { get; init; }
|
||||
|
||||
public required string Name { get; init; }
|
||||
|
||||
public required string BluetoothAddress { get; init; }
|
||||
|
||||
public required ulong AddressValue { get; init; }
|
||||
|
||||
public short SignalStrength { get; init; }
|
||||
|
||||
public string Alias
|
||||
{
|
||||
get => _alias;
|
||||
set
|
||||
{
|
||||
if (!SetProperty(ref _alias, value ?? string.Empty))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
OnPropertyChanged(nameof(DisplayName));
|
||||
OnPropertyChanged(nameof(SecondaryLabel));
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsKnownCandidate
|
||||
{
|
||||
get => _isKnownCandidate;
|
||||
set
|
||||
{
|
||||
if (!SetProperty(ref _isKnownCandidate, value))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
OnPropertyChanged(nameof(Badge));
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsLastConnected
|
||||
{
|
||||
get => _isLastConnected;
|
||||
set
|
||||
{
|
||||
if (!SetProperty(ref _isLastConnected, value))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
OnPropertyChanged(nameof(Badge));
|
||||
}
|
||||
}
|
||||
|
||||
public string NativeName => string.IsNullOrWhiteSpace(Name)
|
||||
? "İsimsiz BLE cihazı"
|
||||
: Name;
|
||||
|
||||
public string DisplayName => string.IsNullOrWhiteSpace(Alias)
|
||||
? NativeName
|
||||
: Alias;
|
||||
|
||||
public string SecondaryLabel => string.IsNullOrWhiteSpace(Alias)
|
||||
? BluetoothAddress
|
||||
: $"{NativeName} - {BluetoothAddress}";
|
||||
|
||||
public string Badge => IsLastConnected
|
||||
? "Son cihaz"
|
||||
: IsKnownCandidate
|
||||
? "Aday"
|
||||
: "BLE";
|
||||
|
||||
public string SignalLabel => SignalStrength == short.MinValue
|
||||
? "RSSI bilinmiyor"
|
||||
: $"RSSI {SignalStrength} dBm";
|
||||
}
|
||||
|
||||
public sealed record DeviceProfile(
|
||||
string LocalAlias,
|
||||
string DeviceId,
|
||||
string BluetoothAddress,
|
||||
DateTimeOffset LastSeenUtc);
|
||||
|
||||
public sealed record EffectOption(byte PatternId, string Label, bool SupportsSpeed)
|
||||
{
|
||||
public string Code => $"{PatternId + 1:00}";
|
||||
}
|
||||
|
||||
public sealed record ColorPreset(string Label, string Hex)
|
||||
{
|
||||
public string ToolTipLabel => $"{Label} · {Hex}";
|
||||
}
|
||||
|
||||
public sealed record SettingsSnapshot(
|
||||
IReadOnlyList<DeviceProfile> KnownDevices,
|
||||
IReadOnlyDictionary<string, string> DeviceAliases,
|
||||
MediaColor SelectedColor,
|
||||
byte Brightness,
|
||||
byte EffectSpeed,
|
||||
LightingMode CurrentMode,
|
||||
byte GrayscaleLevel,
|
||||
byte TemperatureLevel,
|
||||
byte SelectedEffectId,
|
||||
bool IsPoweredOn);
|
||||
|
||||
public abstract class ObservableObject : INotifyPropertyChanged
|
||||
{
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
|
||||
protected bool SetProperty<T>(ref T field, T value, [CallerMemberName] string? propertyName = null)
|
||||
{
|
||||
if (EqualityComparer<T>.Default.Equals(field, value))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
field = value;
|
||||
OnPropertyChanged(propertyName);
|
||||
return true;
|
||||
}
|
||||
|
||||
protected void OnPropertyChanged([CallerMemberName] string? propertyName = null)
|
||||
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||
}
|
||||
|
||||
public sealed class ConnectedDeviceViewModel : ObservableObject
|
||||
{
|
||||
private string _alias = string.Empty;
|
||||
private bool _isControlTarget = true;
|
||||
private DeviceConnectionStatus _connectionStatus = DeviceConnectionStatus.Connecting;
|
||||
private string _statusText = "Bağlantı hazırlanıyor...";
|
||||
private MediaBrush _statusBrush = CreateFrozenBrush(MediaColor.FromRgb(59, 130, 246));
|
||||
|
||||
public required string DeviceId { get; init; }
|
||||
|
||||
public required string NativeName { get; init; }
|
||||
|
||||
public required string BluetoothAddress { get; init; }
|
||||
|
||||
public required ulong AddressValue { get; init; }
|
||||
|
||||
public short SignalStrength { get; init; }
|
||||
|
||||
public string Alias
|
||||
{
|
||||
get => _alias;
|
||||
set
|
||||
{
|
||||
if (!SetProperty(ref _alias, value ?? string.Empty))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
OnPropertyChanged(nameof(DisplayName));
|
||||
OnPropertyChanged(nameof(SecondaryLabel));
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsControlTarget
|
||||
{
|
||||
get => _isControlTarget;
|
||||
set => SetProperty(ref _isControlTarget, value);
|
||||
}
|
||||
|
||||
public DeviceConnectionStatus ConnectionStatus
|
||||
{
|
||||
get => _connectionStatus;
|
||||
set
|
||||
{
|
||||
if (!SetProperty(ref _connectionStatus, value))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_statusBrush = CreateFrozenBrush(GetStatusColor(value));
|
||||
OnPropertyChanged(nameof(StatusLabel));
|
||||
OnPropertyChanged(nameof(StatusBrush));
|
||||
}
|
||||
}
|
||||
|
||||
public string StatusText
|
||||
{
|
||||
get => _statusText;
|
||||
set => SetProperty(ref _statusText, value);
|
||||
}
|
||||
|
||||
public string DisplayName => string.IsNullOrWhiteSpace(Alias)
|
||||
? NativeName
|
||||
: Alias;
|
||||
|
||||
public string SecondaryLabel => string.IsNullOrWhiteSpace(Alias)
|
||||
? BluetoothAddress
|
||||
: $"{NativeName} - {BluetoothAddress}";
|
||||
|
||||
public string SignalLabel => SignalStrength == short.MinValue
|
||||
? "RSSI bilinmiyor"
|
||||
: $"RSSI {SignalStrength} dBm";
|
||||
|
||||
public string StatusLabel => ConnectionStatus switch
|
||||
{
|
||||
DeviceConnectionStatus.Connecting => "Bağlanıyor",
|
||||
DeviceConnectionStatus.Connected => "Hazır",
|
||||
DeviceConnectionStatus.Faulted => "Hata",
|
||||
DeviceConnectionStatus.Disconnected => "Beklemede",
|
||||
_ => "Boş"
|
||||
};
|
||||
|
||||
public MediaBrush StatusBrush => _statusBrush;
|
||||
|
||||
private static MediaColor GetStatusColor(DeviceConnectionStatus status)
|
||||
=> status switch
|
||||
{
|
||||
DeviceConnectionStatus.Connecting => MediaColor.FromRgb(59, 130, 246),
|
||||
DeviceConnectionStatus.Connected => MediaColor.FromRgb(16, 185, 129),
|
||||
DeviceConnectionStatus.Faulted => MediaColor.FromRgb(244, 63, 94),
|
||||
DeviceConnectionStatus.Disconnected => MediaColor.FromRgb(245, 158, 11),
|
||||
_ => MediaColor.FromRgb(107, 114, 128)
|
||||
};
|
||||
|
||||
private static MediaBrush CreateFrozenBrush(MediaColor color)
|
||||
{
|
||||
var brush = new SolidColorBrush(color);
|
||||
brush.Freeze();
|
||||
return brush;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class ControllerState : ObservableObject
|
||||
{
|
||||
private DeviceConnectionStatus _connectionStatus = DeviceConnectionStatus.Disconnected;
|
||||
private bool _isPoweredOn;
|
||||
private LightingMode _currentMode = LightingMode.Rgb;
|
||||
private MediaColor _selectedColor = MediaColor.FromRgb(255, 106, 78);
|
||||
private byte _brightness = 100;
|
||||
private byte _effectSpeed = 50;
|
||||
private byte _grayscaleLevel = 100;
|
||||
private byte _temperatureLevel = 50;
|
||||
private byte _selectedEffectId;
|
||||
private int _connectedDeviceCount;
|
||||
private int _controlTargetCount;
|
||||
private string _statusText = "Tarama için hazır.";
|
||||
private MediaBrush _statusBrush = CreateFrozenBrush(MediaColor.FromRgb(107, 114, 128));
|
||||
private MediaBrush _selectedBrush = CreateFrozenBrush(MediaColor.FromRgb(255, 106, 78));
|
||||
|
||||
public DeviceConnectionStatus ConnectionStatus
|
||||
{
|
||||
get => _connectionStatus;
|
||||
set
|
||||
{
|
||||
if (!SetProperty(ref _connectionStatus, value))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_statusBrush = CreateFrozenBrush(GetStatusColor(value));
|
||||
OnPropertyChanged(nameof(ConnectionLabel));
|
||||
OnPropertyChanged(nameof(StatusBrush));
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsPoweredOn
|
||||
{
|
||||
get => _isPoweredOn;
|
||||
set
|
||||
{
|
||||
if (!SetProperty(ref _isPoweredOn, value))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
OnPropertyChanged(nameof(PowerButtonLabel));
|
||||
}
|
||||
}
|
||||
|
||||
public LightingMode CurrentMode
|
||||
{
|
||||
get => _currentMode;
|
||||
set
|
||||
{
|
||||
if (!SetProperty(ref _currentMode, value))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
OnPropertyChanged(nameof(CurrentModeLabel));
|
||||
}
|
||||
}
|
||||
|
||||
public MediaColor SelectedColor
|
||||
{
|
||||
get => _selectedColor;
|
||||
set
|
||||
{
|
||||
if (!SetProperty(ref _selectedColor, value))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_selectedBrush = CreateFrozenBrush(value);
|
||||
OnPropertyChanged(nameof(SelectedBrush));
|
||||
OnPropertyChanged(nameof(SelectedHex));
|
||||
OnPropertyChanged(nameof(SelectedRgbLabel));
|
||||
}
|
||||
}
|
||||
|
||||
public byte Brightness
|
||||
{
|
||||
get => _brightness;
|
||||
set
|
||||
{
|
||||
if (!SetProperty(ref _brightness, value))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
OnPropertyChanged(nameof(BrightnessLabel));
|
||||
}
|
||||
}
|
||||
|
||||
public byte EffectSpeed
|
||||
{
|
||||
get => _effectSpeed;
|
||||
set
|
||||
{
|
||||
if (!SetProperty(ref _effectSpeed, value))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
OnPropertyChanged(nameof(EffectSpeedLabel));
|
||||
}
|
||||
}
|
||||
|
||||
public byte GrayscaleLevel
|
||||
{
|
||||
get => _grayscaleLevel;
|
||||
set
|
||||
{
|
||||
if (!SetProperty(ref _grayscaleLevel, value))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
OnPropertyChanged(nameof(GrayscaleLabel));
|
||||
}
|
||||
}
|
||||
|
||||
public byte TemperatureLevel
|
||||
{
|
||||
get => _temperatureLevel;
|
||||
set
|
||||
{
|
||||
if (!SetProperty(ref _temperatureLevel, value))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
OnPropertyChanged(nameof(TemperatureLabel));
|
||||
}
|
||||
}
|
||||
|
||||
public byte SelectedEffectId
|
||||
{
|
||||
get => _selectedEffectId;
|
||||
set => SetProperty(ref _selectedEffectId, value);
|
||||
}
|
||||
|
||||
public int ConnectedDeviceCount
|
||||
{
|
||||
get => _connectedDeviceCount;
|
||||
set
|
||||
{
|
||||
if (!SetProperty(ref _connectedDeviceCount, value))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
OnPropertyChanged(nameof(ConnectedSummary));
|
||||
}
|
||||
}
|
||||
|
||||
public int ControlTargetCount
|
||||
{
|
||||
get => _controlTargetCount;
|
||||
set
|
||||
{
|
||||
if (!SetProperty(ref _controlTargetCount, value))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
OnPropertyChanged(nameof(TargetSummary));
|
||||
}
|
||||
}
|
||||
|
||||
public string StatusText
|
||||
{
|
||||
get => _statusText;
|
||||
set => SetProperty(ref _statusText, value);
|
||||
}
|
||||
|
||||
public string ConnectionLabel => ConnectionStatus switch
|
||||
{
|
||||
DeviceConnectionStatus.Scanning => "Taranıyor",
|
||||
DeviceConnectionStatus.Connecting => "Bağlantı kuruluyor",
|
||||
DeviceConnectionStatus.Connected => "Hazır",
|
||||
DeviceConnectionStatus.Faulted => "Dikkat gerekiyor",
|
||||
_ => "Beklemede"
|
||||
};
|
||||
|
||||
public MediaBrush StatusBrush => _statusBrush;
|
||||
|
||||
public MediaBrush SelectedBrush => _selectedBrush;
|
||||
|
||||
public string SelectedHex => $"#{SelectedColor.R:X2}{SelectedColor.G:X2}{SelectedColor.B:X2}";
|
||||
|
||||
public string SelectedRgbLabel => $"RGB {SelectedColor.R}, {SelectedColor.G}, {SelectedColor.B}";
|
||||
|
||||
public string BrightnessLabel => $"%{Brightness}";
|
||||
|
||||
public string EffectSpeedLabel => $"%{EffectSpeed}";
|
||||
|
||||
public string GrayscaleLabel => $"%{GrayscaleLevel}";
|
||||
|
||||
public string TemperatureLabel => $"%{TemperatureLevel}";
|
||||
|
||||
public string CurrentModeLabel => CurrentMode switch
|
||||
{
|
||||
LightingMode.Effect => "Efekt",
|
||||
LightingMode.Grayscale => "Gri ton",
|
||||
LightingMode.Temperature => "Beyaz sıcaklığı",
|
||||
_ => "RGB"
|
||||
};
|
||||
|
||||
public string ConnectedSummary => ConnectedDeviceCount == 0
|
||||
? "Bağlı cihaz yok"
|
||||
: $"{ConnectedDeviceCount} cihaz hazır";
|
||||
|
||||
public string TargetSummary => ControlTargetCount == 0
|
||||
? "Komut alacak cihaz seçilmedi"
|
||||
: $"{ControlTargetCount} cihaz komut alacak";
|
||||
|
||||
public string PowerButtonLabel => IsPoweredOn ? "Hedefleri kapat" : "Hedefleri aç";
|
||||
|
||||
private static MediaColor GetStatusColor(DeviceConnectionStatus status)
|
||||
=> status switch
|
||||
{
|
||||
DeviceConnectionStatus.Scanning => MediaColor.FromRgb(56, 189, 248),
|
||||
DeviceConnectionStatus.Connecting => MediaColor.FromRgb(96, 165, 250),
|
||||
DeviceConnectionStatus.Connected => MediaColor.FromRgb(16, 185, 129),
|
||||
DeviceConnectionStatus.Faulted => MediaColor.FromRgb(244, 63, 94),
|
||||
_ => MediaColor.FromRgb(107, 114, 128)
|
||||
};
|
||||
|
||||
private static MediaBrush CreateFrozenBrush(MediaColor color)
|
||||
{
|
||||
var brush = new SolidColorBrush(color);
|
||||
brush.Freeze();
|
||||
return brush;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,632 @@
|
||||
<Window x:Class="RgbLedStripController.MainWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d"
|
||||
Title="RGB Led Strip Controller"
|
||||
Height="800"
|
||||
Width="1320"
|
||||
MinWidth="1040"
|
||||
MinHeight="700"
|
||||
WindowStartupLocation="CenterScreen">
|
||||
<Window.Resources>
|
||||
<BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter" />
|
||||
|
||||
<DataTemplate x:Key="EffectOptionTemplate">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="10" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<Border Padding="8,4"
|
||||
Background="#18FFFFFF"
|
||||
CornerRadius="8">
|
||||
<TextBlock FontSize="11"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{StaticResource MutedTextBrush}"
|
||||
Text="{Binding Code}" />
|
||||
</Border>
|
||||
|
||||
<TextBlock Grid.Column="2"
|
||||
VerticalAlignment="Center"
|
||||
FontSize="13"
|
||||
FontWeight="SemiBold"
|
||||
Text="{Binding Label}" />
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</Window.Resources>
|
||||
|
||||
<Border Margin="12"
|
||||
Padding="24"
|
||||
Background="#FF0D131B"
|
||||
BorderBrush="#18FFFFFF"
|
||||
BorderThickness="1"
|
||||
CornerRadius="22"
|
||||
ClipToBounds="True">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="16" />
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="12" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<Border Grid.Row="0" Style="{StaticResource CardBorderStyle}">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<StackPanel>
|
||||
<TextBlock Style="{StaticResource SectionTitleStyle}"
|
||||
FontSize="30"
|
||||
Text="RGB Led Strip Controller" />
|
||||
<TextBlock Margin="0,8,0,0"
|
||||
FontSize="14"
|
||||
Foreground="{StaticResource MutedTextBrush}"
|
||||
Text="Windows 10 için sade, çok fonksiyonlu multi ELK-BLEDOM protokolüne uygun rgb led şerit kontrolcüsü." />
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Grid.Column="1"
|
||||
Margin="24,0,0,0"
|
||||
VerticalAlignment="Center">
|
||||
<Border HorizontalAlignment="Right"
|
||||
Padding="12,6"
|
||||
CornerRadius="12"
|
||||
Background="{Binding State.StatusBrush}">
|
||||
<TextBlock FontSize="13"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="Black"
|
||||
Text="{Binding State.ConnectionLabel}" />
|
||||
</Border>
|
||||
<TextBlock Margin="0,12,0,0"
|
||||
HorizontalAlignment="Right"
|
||||
FontSize="13"
|
||||
Text="{Binding State.ConnectedSummary}" />
|
||||
<TextBlock Margin="0,4,0,0"
|
||||
HorizontalAlignment="Right"
|
||||
FontSize="13"
|
||||
Foreground="{StaticResource MutedTextBrush}"
|
||||
Text="{Binding State.TargetSummary}" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<Grid Grid.Row="2">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="320" />
|
||||
<ColumnDefinition Width="18" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<Border Grid.Column="0" Style="{StaticResource CardBorderStyle}">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="14" />
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="14" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="12" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="12" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<TextBlock Style="{StaticResource SectionTitleStyle}"
|
||||
FontSize="22"
|
||||
Text="Cihazlar" />
|
||||
|
||||
<Button Grid.Column="1"
|
||||
Click="ScanClicked"
|
||||
IsEnabled="{Binding CanScan}">
|
||||
<TextBlock FontSize="13" Text="Tara" />
|
||||
</Button>
|
||||
</Grid>
|
||||
|
||||
<ListBox Grid.Row="2"
|
||||
ItemsSource="{Binding Devices}"
|
||||
SelectedItem="{Binding SelectedDevice, Mode=TwoWay}">
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<TextBlock FontSize="15"
|
||||
FontWeight="SemiBold"
|
||||
Text="{Binding DisplayName}" />
|
||||
|
||||
<Border Grid.Column="1"
|
||||
Margin="12,0,0,0"
|
||||
Padding="8,3"
|
||||
CornerRadius="8"
|
||||
Background="#213CC7FF"
|
||||
VerticalAlignment="Center">
|
||||
<TextBlock FontSize="11"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{StaticResource AccentBrush}"
|
||||
Text="{Binding Badge}" />
|
||||
</Border>
|
||||
</Grid>
|
||||
|
||||
<StackPanel Grid.Row="1" Margin="0,8,0,0">
|
||||
<TextBlock FontSize="12"
|
||||
Foreground="{StaticResource MutedTextBrush}"
|
||||
Text="{Binding SecondaryLabel}" />
|
||||
<TextBlock Margin="0,3,0,0"
|
||||
FontSize="12"
|
||||
Foreground="{StaticResource MutedTextBrush}"
|
||||
Text="{Binding SignalLabel}" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
|
||||
<Border Grid.Row="4" Style="{StaticResource PanelCardStyle}">
|
||||
<StackPanel>
|
||||
<TextBlock FontSize="12"
|
||||
Foreground="{StaticResource MutedTextBrush}"
|
||||
Text="Seçili cihaz" />
|
||||
<TextBlock Margin="0,8,0,0"
|
||||
FontSize="15"
|
||||
FontWeight="SemiBold"
|
||||
Text="{Binding SelectedDeviceSummaryDisplay}"
|
||||
TextWrapping="Wrap" />
|
||||
<TextBlock Margin="0,10,0,0"
|
||||
FontSize="12"
|
||||
Foreground="{StaticResource MutedTextBrush}"
|
||||
Text="{Binding LastKnownDeviceCaptionDisplay}"
|
||||
TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<Grid Grid.Row="6">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="12" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<TextBox Grid.Column="0"
|
||||
Text="{Binding AliasEditorText, UpdateSourceTrigger=PropertyChanged}" />
|
||||
|
||||
<Button Grid.Column="2"
|
||||
Click="SaveAliasClicked"
|
||||
IsEnabled="{Binding CanSaveAlias}">
|
||||
<TextBlock FontSize="13" Text="Takma adı kaydet" />
|
||||
</Button>
|
||||
</Grid>
|
||||
|
||||
<Grid Grid.Row="8">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="12" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<Button Grid.Column="0"
|
||||
Click="ConnectClicked"
|
||||
IsEnabled="{Binding CanConnect}">
|
||||
<TextBlock FontSize="14" Text="Bağlan" />
|
||||
</Button>
|
||||
|
||||
<Button Grid.Column="2"
|
||||
Style="{StaticResource GhostButtonStyle}"
|
||||
Click="DisconnectAllClicked"
|
||||
IsEnabled="{Binding CanDisconnectAll}">
|
||||
<TextBlock FontSize="14" Text="Tümünü kes" />
|
||||
</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<Grid Grid.Column="2">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="16" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<Border Style="{StaticResource CardBorderStyle}">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="14" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<TextBlock Style="{StaticResource SectionTitleStyle}"
|
||||
FontSize="22"
|
||||
Text="Bağlı cihazlar" />
|
||||
|
||||
<ListBox Grid.Row="2"
|
||||
ItemsSource="{Binding ConnectedDevices}">
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="182" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<StackPanel>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<TextBlock FontSize="15"
|
||||
FontWeight="SemiBold"
|
||||
Text="{Binding DisplayName}" />
|
||||
|
||||
<Border Grid.Column="1"
|
||||
Margin="12,0,0,0"
|
||||
Padding="8,3"
|
||||
CornerRadius="8"
|
||||
Background="{Binding StatusBrush}"
|
||||
VerticalAlignment="Center">
|
||||
<TextBlock FontSize="11"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="Black"
|
||||
Text="{Binding StatusLabel}" />
|
||||
</Border>
|
||||
</Grid>
|
||||
|
||||
<TextBlock Margin="0,8,0,0"
|
||||
FontSize="12"
|
||||
Foreground="{StaticResource MutedTextBrush}"
|
||||
Text="{Binding SecondaryLabel}" />
|
||||
<TextBlock Margin="0,3,0,0"
|
||||
FontSize="12"
|
||||
Foreground="{StaticResource MutedTextBrush}"
|
||||
Text="{Binding SignalLabel}" />
|
||||
<TextBlock Margin="0,8,0,0"
|
||||
FontSize="13"
|
||||
Text="{Binding StatusText}"
|
||||
TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Grid.Column="1"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Center">
|
||||
<CheckBox Content="Komuta dahil"
|
||||
IsChecked="{Binding IsControlTarget, Mode=TwoWay}"
|
||||
Margin="0,0,0,10"
|
||||
Style="{StaticResource TargetCheckBoxStyle}" />
|
||||
<Button Style="{StaticResource DangerGhostButtonStyle}"
|
||||
Click="DisconnectConnectedClicked"
|
||||
Tag="{Binding}">
|
||||
<TextBlock FontSize="13" Text="Bağlantıyı kes" />
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<Border Grid.Row="2" Style="{StaticResource CardBorderStyle}">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="1*" />
|
||||
<ColumnDefinition Width="16" />
|
||||
<ColumnDefinition Width="1.15*" />
|
||||
<ColumnDefinition Width="16" />
|
||||
<ColumnDefinition Width="0.95*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<Border Grid.Column="0" Style="{StaticResource PanelCardStyle}">
|
||||
<StackPanel>
|
||||
<TextBlock Style="{StaticResource SectionTitleStyle}"
|
||||
FontSize="20"
|
||||
Text="Güç ve parlaklık" />
|
||||
<Button Margin="0,16,0,0"
|
||||
Click="PowerClicked"
|
||||
IsEnabled="{Binding CanUseControls}">
|
||||
<TextBlock FontSize="14" Text="{Binding State.PowerButtonLabel}" />
|
||||
</Button>
|
||||
<TextBlock Margin="0,22,0,0"
|
||||
FontSize="13"
|
||||
Foreground="{StaticResource MutedTextBrush}"
|
||||
Text="Parlaklık" />
|
||||
<DockPanel Margin="0,6,0,0">
|
||||
<TextBlock DockPanel.Dock="Right"
|
||||
FontSize="15"
|
||||
FontWeight="SemiBold"
|
||||
Text="{Binding State.BrightnessLabel}" />
|
||||
</DockPanel>
|
||||
<Slider x:Name="BrightnessSlider"
|
||||
Minimum="0"
|
||||
Maximum="100"
|
||||
TickFrequency="5"
|
||||
Value="{Binding State.Brightness}"
|
||||
IsEnabled="{Binding CanUseBrightnessControl}"
|
||||
LostMouseCapture="BrightnessCommitLostMouseCapture"
|
||||
PreviewKeyUp="BrightnessCommitKeyUp"
|
||||
PreviewMouseLeftButtonUp="BrightnessCommitMouseUp"
|
||||
ValueChanged="BrightnessChanged" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<Border Grid.Column="2" Style="{StaticResource PanelCardStyle}">
|
||||
<StackPanel>
|
||||
<TextBlock Style="{StaticResource SectionTitleStyle}"
|
||||
FontSize="20"
|
||||
Text="Renk" />
|
||||
<StackPanel Orientation="Horizontal" Margin="0,16,0,0">
|
||||
<Border Width="74"
|
||||
Height="74"
|
||||
CornerRadius="12"
|
||||
Background="{Binding State.SelectedBrush}" />
|
||||
<StackPanel Margin="14,4,0,0"
|
||||
VerticalAlignment="Center">
|
||||
<TextBlock FontSize="16"
|
||||
FontWeight="SemiBold"
|
||||
Text="{Binding State.SelectedHex}" />
|
||||
<TextBlock Margin="0,4,0,0"
|
||||
FontSize="12"
|
||||
Foreground="{StaticResource MutedTextBrush}"
|
||||
Text="{Binding State.SelectedRgbLabel}" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
|
||||
<TextBlock Margin="0,18,0,0"
|
||||
FontSize="12"
|
||||
Foreground="{StaticResource MutedTextBrush}"
|
||||
Text="Hızlı renkler" />
|
||||
<ItemsControl Margin="0,14,0,0"
|
||||
ItemsSource="{Binding ColorPresets}">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<UniformGrid Columns="7" />
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Button Width="38"
|
||||
Height="38"
|
||||
Margin="0,0,10,10"
|
||||
Background="{Binding Hex}"
|
||||
Tag="{Binding Hex}"
|
||||
ToolTip="{Binding ToolTipLabel}"
|
||||
Click="PresetColorClicked"
|
||||
Style="{StaticResource SwatchButtonStyle}" />
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
|
||||
<Button Margin="0,4,0,0"
|
||||
Style="{StaticResource WarmGhostButtonStyle}"
|
||||
Click="SelectColorClicked"
|
||||
IsEnabled="{Binding CanUseControls}">
|
||||
<TextBlock FontSize="14" Text="Renk seçici" />
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<Border Grid.Column="4" Style="{StaticResource PanelCardStyle}">
|
||||
<StackPanel>
|
||||
<TextBlock Style="{StaticResource SectionTitleStyle}"
|
||||
FontSize="20"
|
||||
Text="Mod" />
|
||||
<TextBlock Margin="0,16,0,0"
|
||||
FontSize="12"
|
||||
Foreground="{StaticResource MutedTextBrush}"
|
||||
Text="Aktif mod" />
|
||||
<TextBlock Margin="0,8,0,0"
|
||||
FontSize="15"
|
||||
FontWeight="SemiBold"
|
||||
Text="{Binding State.CurrentModeLabel}" />
|
||||
<UniformGrid Margin="0,14,0,0"
|
||||
Columns="4">
|
||||
<Button Margin="0,0,8,0"
|
||||
Click="SwitchRgbModeClicked"
|
||||
IsEnabled="{Binding CanUseControls}">
|
||||
<TextBlock FontSize="13" Text="RGB" />
|
||||
</Button>
|
||||
<Button Margin="0,0,8,0"
|
||||
Click="SwitchEffectModeClicked"
|
||||
IsEnabled="{Binding CanUseControls}">
|
||||
<TextBlock FontSize="13" Text="Efekt" />
|
||||
</Button>
|
||||
<Button Click="SwitchGrayscaleModeClicked"
|
||||
IsEnabled="{Binding CanUseControls}">
|
||||
<TextBlock FontSize="13" Text="Gri ton" />
|
||||
</Button>
|
||||
<Button Margin="8,0,0,0"
|
||||
Click="SwitchTemperatureModeClicked"
|
||||
IsEnabled="{Binding CanUseControls}">
|
||||
<TextBlock FontSize="13" Text="Beyaz" />
|
||||
</Button>
|
||||
</UniformGrid>
|
||||
<StackPanel Margin="0,18,0,0"
|
||||
Visibility="{Binding CanShowEffectControls, Converter={StaticResource BooleanToVisibilityConverter}}">
|
||||
<TextBlock FontSize="12"
|
||||
Foreground="{StaticResource MutedTextBrush}"
|
||||
Text="Hazır efekt" />
|
||||
<ComboBox x:Name="EffectsComboBox"
|
||||
Margin="0,10,0,0"
|
||||
Style="{StaticResource EffectComboBoxStyle}"
|
||||
ItemTemplate="{StaticResource EffectOptionTemplate}"
|
||||
ItemsSource="{Binding Effects}"
|
||||
SelectedValuePath="PatternId"
|
||||
SelectedValue="{Binding State.SelectedEffectId}"
|
||||
SelectionChanged="EffectChanged"
|
||||
IsEnabled="{Binding CanUseControls}" />
|
||||
</StackPanel>
|
||||
<StackPanel Margin="0,18,0,0"
|
||||
Visibility="{Binding CanShowEffectSpeed, Converter={StaticResource BooleanToVisibilityConverter}}">
|
||||
<TextBlock FontSize="12"
|
||||
Foreground="{StaticResource MutedTextBrush}"
|
||||
Text="Efekt hızı" />
|
||||
<DockPanel Margin="0,6,0,0">
|
||||
<TextBlock DockPanel.Dock="Right"
|
||||
FontSize="15"
|
||||
FontWeight="SemiBold"
|
||||
Text="{Binding State.EffectSpeedLabel}" />
|
||||
</DockPanel>
|
||||
<Slider x:Name="EffectSpeedSlider"
|
||||
Minimum="0"
|
||||
Maximum="100"
|
||||
TickFrequency="5"
|
||||
Value="{Binding State.EffectSpeed}"
|
||||
IsEnabled="{Binding CanUseControls}"
|
||||
LostMouseCapture="EffectSpeedCommitLostMouseCapture"
|
||||
PreviewKeyUp="EffectSpeedCommitKeyUp"
|
||||
PreviewMouseLeftButtonUp="EffectSpeedCommitMouseUp"
|
||||
ValueChanged="EffectSpeedChanged" />
|
||||
</StackPanel>
|
||||
<StackPanel Margin="0,18,0,0"
|
||||
Visibility="{Binding CanShowGrayscaleControls, Converter={StaticResource BooleanToVisibilityConverter}}">
|
||||
<TextBlock FontSize="12"
|
||||
Foreground="{StaticResource MutedTextBrush}"
|
||||
Text="Gri ton" />
|
||||
<DockPanel Margin="0,6,0,0">
|
||||
<TextBlock DockPanel.Dock="Right"
|
||||
FontSize="15"
|
||||
FontWeight="SemiBold"
|
||||
Text="{Binding State.GrayscaleLabel}" />
|
||||
</DockPanel>
|
||||
<Slider x:Name="GrayscaleSlider"
|
||||
Minimum="0"
|
||||
Maximum="100"
|
||||
TickFrequency="5"
|
||||
Value="{Binding State.GrayscaleLevel}"
|
||||
IsEnabled="{Binding CanUseControls}"
|
||||
LostMouseCapture="GrayscaleCommitLostMouseCapture"
|
||||
PreviewKeyUp="GrayscaleCommitKeyUp"
|
||||
PreviewMouseLeftButtonUp="GrayscaleCommitMouseUp"
|
||||
ValueChanged="GrayscaleChanged" />
|
||||
</StackPanel>
|
||||
<StackPanel Margin="0,18,0,0"
|
||||
Visibility="{Binding CanShowTemperatureControls, Converter={StaticResource BooleanToVisibilityConverter}}">
|
||||
<TextBlock FontSize="12"
|
||||
Foreground="{StaticResource MutedTextBrush}"
|
||||
Text="Beyaz sıcaklığı" />
|
||||
<DockPanel Margin="0,6,0,0">
|
||||
<TextBlock DockPanel.Dock="Right"
|
||||
FontSize="15"
|
||||
FontWeight="SemiBold"
|
||||
Text="{Binding State.TemperatureLabel}" />
|
||||
</DockPanel>
|
||||
<Slider x:Name="TemperatureSlider"
|
||||
Minimum="0"
|
||||
Maximum="100"
|
||||
TickFrequency="5"
|
||||
Value="{Binding State.TemperatureLevel}"
|
||||
IsEnabled="{Binding CanUseControls}"
|
||||
LostMouseCapture="TemperatureCommitLostMouseCapture"
|
||||
PreviewKeyUp="TemperatureCommitKeyUp"
|
||||
PreviewMouseLeftButtonUp="TemperatureCommitMouseUp"
|
||||
ValueChanged="TemperatureChanged" />
|
||||
<UniformGrid Margin="0,14,0,0"
|
||||
Columns="3">
|
||||
<Button Margin="0,0,8,0"
|
||||
Tag="0"
|
||||
Click="TemperaturePresetClicked"
|
||||
IsEnabled="{Binding CanUseControls}">
|
||||
<TextBlock FontSize="12" Text="Soğuk" />
|
||||
</Button>
|
||||
<Button Margin="0,0,8,0"
|
||||
Tag="50"
|
||||
Click="TemperaturePresetClicked"
|
||||
IsEnabled="{Binding CanUseControls}">
|
||||
<TextBlock FontSize="12" Text="Doğal" />
|
||||
</Button>
|
||||
<Button Click="TemperaturePresetClicked"
|
||||
Tag="100"
|
||||
IsEnabled="{Binding CanUseControls}">
|
||||
<TextBlock FontSize="12" Text="Sıcak" />
|
||||
</Button>
|
||||
</UniformGrid>
|
||||
</StackPanel>
|
||||
<TextBlock Margin="0,20,0,0"
|
||||
FontSize="12"
|
||||
Foreground="{StaticResource MutedTextBrush}"
|
||||
Text="Durum" />
|
||||
<TextBlock Margin="0,8,0,0"
|
||||
FontSize="15"
|
||||
FontWeight="SemiBold"
|
||||
Text="{Binding State.StatusText}"
|
||||
TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<Border Grid.Row="4"
|
||||
Padding="14,12"
|
||||
Background="#99111822"
|
||||
BorderBrush="#18FFFFFF"
|
||||
BorderThickness="1"
|
||||
CornerRadius="14">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="16" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="16" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<TextBlock VerticalAlignment="Center"
|
||||
FontSize="12"
|
||||
Foreground="{StaticResource MutedTextBrush}">
|
||||
<Run Text="Made by " />
|
||||
<Hyperlink NavigateUri="https://fristy.net"
|
||||
RequestNavigate="FooterLinkClicked">
|
||||
<Run Text="@fristyxd" />
|
||||
</Hyperlink>
|
||||
</TextBlock>
|
||||
|
||||
<StackPanel Grid.Column="2"
|
||||
VerticalAlignment="Center"
|
||||
HorizontalAlignment="Center">
|
||||
<TextBlock HorizontalAlignment="Center"
|
||||
FontSize="12"
|
||||
FontWeight="SemiBold"
|
||||
Text="{Binding VersionDisplay}" />
|
||||
<TextBlock Margin="0,4,0,0"
|
||||
HorizontalAlignment="Center"
|
||||
FontSize="12"
|
||||
Foreground="{StaticResource MutedTextBrush}"
|
||||
Text="{Binding UpdateStatusText}"
|
||||
TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
|
||||
<Button Grid.Column="4"
|
||||
Click="CheckForUpdatesClicked"
|
||||
IsEnabled="{Binding CanCheckForUpdates}">
|
||||
<TextBlock FontSize="13"
|
||||
Text="{Binding UpdateButtonLabel}" />
|
||||
</Button>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Window>
|
||||
@@ -0,0 +1,860 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Collections.Specialized;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media.Imaging;
|
||||
using System.Windows.Navigation;
|
||||
using Forms = System.Windows.Forms;
|
||||
using RgbLedStripController.Core;
|
||||
using RgbLedStripController.Services;
|
||||
using Velopack;
|
||||
using Velopack.Sources;
|
||||
using MediaColor = System.Windows.Media.Color;
|
||||
using WpfButton = System.Windows.Controls.Button;
|
||||
|
||||
namespace RgbLedStripController;
|
||||
|
||||
public partial class MainWindow : Window, INotifyPropertyChanged
|
||||
{
|
||||
private const string GiteaRepoUrl = "https://git.fristy.net/fristy/rgb-led-strip-controller";
|
||||
private readonly MultiDeviceController _controller;
|
||||
private bool _effectSpeedCommitPending;
|
||||
private bool _grayscaleCommitPending;
|
||||
private bool _isBusy;
|
||||
private bool _isCheckingForUpdates;
|
||||
private bool _brightnessCommitPending;
|
||||
private bool _temperatureCommitPending;
|
||||
private bool _isInitializing = true;
|
||||
private bool _updatePendingRestart;
|
||||
private BleScanResult? _selectedDevice;
|
||||
private string _aliasEditorText = string.Empty;
|
||||
private string _updateStatusText = "G\u00fcncelleme kontrol\u00fc haz\u0131r.";
|
||||
|
||||
public MainWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
TrySetWindowIcon();
|
||||
|
||||
State = new ControllerState();
|
||||
Effects = ElkBledomProtocol.CreateEffectOptions();
|
||||
ColorPresets = CreateColorPresets();
|
||||
Devices = new ObservableCollection<BleScanResult>();
|
||||
ConnectedDevices = new ObservableCollection<ConnectedDeviceViewModel>();
|
||||
ConnectedDevices.CollectionChanged += HandleConnectedDevicesCollectionChanged;
|
||||
|
||||
_controller = new MultiDeviceController(new SettingsStore(), State);
|
||||
State.PropertyChanged += HandleStateChanged;
|
||||
|
||||
DataContext = this;
|
||||
Loaded += HandleLoaded;
|
||||
}
|
||||
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
|
||||
public ControllerState State { get; }
|
||||
|
||||
public ObservableCollection<BleScanResult> Devices { get; }
|
||||
|
||||
public ObservableCollection<ConnectedDeviceViewModel> ConnectedDevices { get; }
|
||||
|
||||
public IReadOnlyList<EffectOption> Effects { get; }
|
||||
|
||||
public IReadOnlyList<ColorPreset> ColorPresets { get; }
|
||||
|
||||
public string VersionDisplay { get; } = $"v{GetApplicationVersion()}";
|
||||
|
||||
public string UpdateStatusText
|
||||
{
|
||||
get => _updateStatusText;
|
||||
private set => SetProperty(ref _updateStatusText, value);
|
||||
}
|
||||
|
||||
public bool CanCheckForUpdates => !_isCheckingForUpdates;
|
||||
|
||||
public string UpdateButtonLabel => _isCheckingForUpdates
|
||||
? "Kontrol ediliyor..."
|
||||
: _updatePendingRestart
|
||||
? "G\u00fcncellemeyi uygula"
|
||||
: "G\u00fcncellemeleri kontrol et";
|
||||
|
||||
public BleScanResult? SelectedDevice
|
||||
{
|
||||
get => _selectedDevice;
|
||||
set
|
||||
{
|
||||
if (!SetProperty(ref _selectedDevice, value))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
AliasEditorText = value?.Alias ?? string.Empty;
|
||||
NotifySelectedDeviceChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public string AliasEditorText
|
||||
{
|
||||
get => _aliasEditorText;
|
||||
set => SetProperty(ref _aliasEditorText, value);
|
||||
}
|
||||
|
||||
public bool CanScan => !_isBusy;
|
||||
|
||||
public bool CanConnect => !_isBusy
|
||||
&& SelectedDevice is not null
|
||||
&& ConnectedDevices.All(device => !string.Equals(device.BluetoothAddress, SelectedDevice.BluetoothAddress, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
public bool CanSaveAlias => !_isBusy && SelectedDevice is not null;
|
||||
|
||||
public bool CanUseControls => !_isBusy && State.ControlTargetCount > 0;
|
||||
|
||||
public bool CanUseBrightnessControl => CanUseControls && State.CurrentMode != LightingMode.Grayscale;
|
||||
|
||||
public bool CanDisconnectAll => !_isBusy && ConnectedDevices.Count > 0;
|
||||
|
||||
public bool CanShowEffectControls => State.CurrentMode == LightingMode.Effect;
|
||||
|
||||
public bool CanShowEffectSpeed => CanShowEffectControls && Effects.Any(item => item.PatternId == State.SelectedEffectId && item.SupportsSpeed);
|
||||
|
||||
public bool CanShowGrayscaleControls => State.CurrentMode == LightingMode.Grayscale;
|
||||
|
||||
public bool CanShowTemperatureControls => State.CurrentMode == LightingMode.Temperature;
|
||||
|
||||
public string LastKnownDeviceCaptionDisplay => _controller.LastKnownDevice is null
|
||||
? "Henüz kayıtlı cihaz yok."
|
||||
: $"Son kayıt: {_controller.LastKnownDevice.LocalAlias} ({_controller.LastKnownDevice.BluetoothAddress})";
|
||||
|
||||
public string SelectedDeviceSummaryDisplay => SelectedDevice is null
|
||||
? "Bir cihaz seç."
|
||||
: $"{SelectedDevice.DisplayName} - {SelectedDevice.BluetoothAddress}";
|
||||
|
||||
public string LastKnownDeviceCaption => _controller.LastKnownDevice is null
|
||||
? "Henüz cihaz hafızası yok."
|
||||
: $"{_controller.LastKnownDevice.LocalAlias} ({_controller.LastKnownDevice.BluetoothAddress})";
|
||||
|
||||
public string SelectedDeviceSummary => SelectedDevice is null
|
||||
? "Seçim yapılmadı."
|
||||
: $"{SelectedDevice.DisplayName} - {SelectedDevice.BluetoothAddress}";
|
||||
|
||||
protected override async void OnClosed(EventArgs e)
|
||||
{
|
||||
State.PropertyChanged -= HandleStateChanged;
|
||||
ConnectedDevices.CollectionChanged -= HandleConnectedDevicesCollectionChanged;
|
||||
await _controller.DisposeAsync();
|
||||
base.OnClosed(e);
|
||||
}
|
||||
|
||||
private async void HandleLoaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
await RunBusyAsync(async () =>
|
||||
{
|
||||
await _controller.InitializeAsync();
|
||||
EffectsComboBox.SelectedValue = State.SelectedEffectId;
|
||||
BrightnessSlider.Value = State.Brightness;
|
||||
EffectSpeedSlider.Value = State.EffectSpeed;
|
||||
GrayscaleSlider.Value = State.GrayscaleLevel;
|
||||
TemperatureSlider.Value = State.TemperatureLevel;
|
||||
NotifyLastKnownDeviceChanged();
|
||||
}, treatErrorAsFault: true);
|
||||
|
||||
_isInitializing = false;
|
||||
_ = CheckForUpdatesAsync(isManualCheck: false);
|
||||
}
|
||||
|
||||
private async void ScanClicked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
await RunBusyAsync(async () =>
|
||||
{
|
||||
var scanned = await _controller.ScanForDevicesAsync(TimeSpan.FromSeconds(10));
|
||||
Devices.Clear();
|
||||
foreach (var device in scanned)
|
||||
{
|
||||
Devices.Add(device);
|
||||
}
|
||||
|
||||
SelectedDevice = Devices.FirstOrDefault(device => device.IsLastConnected) ?? Devices.FirstOrDefault();
|
||||
});
|
||||
}
|
||||
|
||||
private async void ConnectClicked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (SelectedDevice is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await RunBusyAsync(async () =>
|
||||
{
|
||||
var connected = await _controller.ConnectAsync(SelectedDevice);
|
||||
if (ConnectedDevices.All(device => !string.Equals(device.BluetoothAddress, connected.BluetoothAddress, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
ConnectedDevices.Add(connected);
|
||||
}
|
||||
|
||||
SelectedDevice.IsLastConnected = true;
|
||||
NotifyLastKnownDeviceChanged();
|
||||
}, treatErrorAsFault: true);
|
||||
}
|
||||
|
||||
private async void SaveAliasClicked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (SelectedDevice is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await RunBusyAsync(async () =>
|
||||
{
|
||||
await _controller.SetAliasAsync(SelectedDevice, AliasEditorText);
|
||||
NotifySelectedDeviceChanged();
|
||||
NotifyLastKnownDeviceChanged();
|
||||
});
|
||||
}
|
||||
|
||||
private async void DisconnectConnectedClicked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (sender is not WpfButton { Tag: ConnectedDeviceViewModel device })
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await RunBusyAsync(async () =>
|
||||
{
|
||||
await _controller.DisconnectAsync(device.BluetoothAddress);
|
||||
ConnectedDevices.Remove(device);
|
||||
});
|
||||
}
|
||||
|
||||
private async void DisconnectAllClicked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
await RunBusyAsync(async () =>
|
||||
{
|
||||
await _controller.DisconnectAllAsync();
|
||||
ConnectedDevices.Clear();
|
||||
});
|
||||
}
|
||||
|
||||
private async void PowerClicked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
await RunBusyAsync(
|
||||
async () => await _controller.ApplyPowerAsync(!State.IsPoweredOn),
|
||||
useBusyFlag: false,
|
||||
treatErrorAsFault: true);
|
||||
}
|
||||
|
||||
private async void SelectColorClicked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (!CanUseControls)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
using var colorDialog = new Forms.ColorDialog
|
||||
{
|
||||
FullOpen = true,
|
||||
Color = System.Drawing.Color.FromArgb(State.SelectedColor.R, State.SelectedColor.G, State.SelectedColor.B)
|
||||
};
|
||||
|
||||
if (colorDialog.ShowDialog() != Forms.DialogResult.OK)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var color = MediaColor.FromRgb(colorDialog.Color.R, colorDialog.Color.G, colorDialog.Color.B);
|
||||
await RunBusyAsync(
|
||||
async () => await _controller.QueueColorAsync(color),
|
||||
useBusyFlag: false,
|
||||
treatErrorAsFault: true);
|
||||
}
|
||||
|
||||
private async void PresetColorClicked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (!CanUseControls || sender is not WpfButton { Tag: string tag })
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var color = ParseHexColor(tag);
|
||||
await RunBusyAsync(
|
||||
async () => await _controller.QueueColorAsync(color),
|
||||
useBusyFlag: false,
|
||||
treatErrorAsFault: true);
|
||||
}
|
||||
|
||||
private void BrightnessChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
|
||||
{
|
||||
if (_isInitializing)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
State.Brightness = (byte)Math.Round(e.NewValue);
|
||||
if (!CanUseBrightnessControl)
|
||||
{
|
||||
_brightnessCommitPending = false;
|
||||
return;
|
||||
}
|
||||
|
||||
_brightnessCommitPending = true;
|
||||
}
|
||||
|
||||
private async void BrightnessCommitMouseUp(object sender, MouseButtonEventArgs e)
|
||||
=> await CommitBrightnessIfPendingAsync();
|
||||
|
||||
private async void BrightnessCommitLostMouseCapture(object sender, System.Windows.Input.MouseEventArgs e)
|
||||
=> await CommitBrightnessIfPendingAsync();
|
||||
|
||||
private async void BrightnessCommitKeyUp(object sender, System.Windows.Input.KeyEventArgs e)
|
||||
=> await CommitBrightnessIfPendingAsync();
|
||||
|
||||
private async void EffectChanged(object sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
if (_isInitializing)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (EffectsComboBox.SelectedValue is not byte patternId || !CanUseControls)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
State.SelectedEffectId = patternId;
|
||||
await RunBusyAsync(
|
||||
async () => await _controller.ApplyEffectAsync(patternId),
|
||||
useBusyFlag: false,
|
||||
treatErrorAsFault: true);
|
||||
}
|
||||
|
||||
private async void SwitchRgbModeClicked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (!CanUseControls)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await RunBusyAsync(
|
||||
async () => await _controller.QueueColorAsync(State.SelectedColor),
|
||||
useBusyFlag: false,
|
||||
treatErrorAsFault: true);
|
||||
}
|
||||
|
||||
private async void SwitchEffectModeClicked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (!CanUseControls)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await RunBusyAsync(
|
||||
async () => await _controller.ApplyEffectAsync(State.SelectedEffectId),
|
||||
useBusyFlag: false,
|
||||
treatErrorAsFault: true);
|
||||
}
|
||||
|
||||
private async void SwitchGrayscaleModeClicked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (!CanUseControls)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await RunBusyAsync(
|
||||
async () => await _controller.ApplyGrayscaleModeAsync(),
|
||||
useBusyFlag: false,
|
||||
treatErrorAsFault: true);
|
||||
}
|
||||
|
||||
private async void SwitchTemperatureModeClicked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (!CanUseControls)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await RunBusyAsync(
|
||||
async () => await _controller.ApplyTemperatureModeAsync(),
|
||||
useBusyFlag: false,
|
||||
treatErrorAsFault: true);
|
||||
}
|
||||
|
||||
private void EffectSpeedChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
|
||||
{
|
||||
if (_isInitializing)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
State.EffectSpeed = (byte)Math.Round(e.NewValue);
|
||||
if (!CanUseControls || !CanShowEffectSpeed)
|
||||
{
|
||||
_effectSpeedCommitPending = false;
|
||||
return;
|
||||
}
|
||||
|
||||
_effectSpeedCommitPending = true;
|
||||
}
|
||||
|
||||
private async void EffectSpeedCommitMouseUp(object sender, MouseButtonEventArgs e)
|
||||
=> await CommitEffectSpeedIfPendingAsync();
|
||||
|
||||
private async void EffectSpeedCommitLostMouseCapture(object sender, System.Windows.Input.MouseEventArgs e)
|
||||
=> await CommitEffectSpeedIfPendingAsync();
|
||||
|
||||
private async void EffectSpeedCommitKeyUp(object sender, System.Windows.Input.KeyEventArgs e)
|
||||
=> await CommitEffectSpeedIfPendingAsync();
|
||||
|
||||
private void GrayscaleChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
|
||||
{
|
||||
if (_isInitializing)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
State.GrayscaleLevel = (byte)Math.Round(e.NewValue);
|
||||
if (!CanUseControls || !CanShowGrayscaleControls)
|
||||
{
|
||||
_grayscaleCommitPending = false;
|
||||
return;
|
||||
}
|
||||
|
||||
_grayscaleCommitPending = true;
|
||||
}
|
||||
|
||||
private async void GrayscaleCommitMouseUp(object sender, MouseButtonEventArgs e)
|
||||
=> await CommitGrayscaleIfPendingAsync();
|
||||
|
||||
private async void GrayscaleCommitLostMouseCapture(object sender, System.Windows.Input.MouseEventArgs e)
|
||||
=> await CommitGrayscaleIfPendingAsync();
|
||||
|
||||
private async void GrayscaleCommitKeyUp(object sender, System.Windows.Input.KeyEventArgs e)
|
||||
=> await CommitGrayscaleIfPendingAsync();
|
||||
|
||||
private void TemperatureChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
|
||||
{
|
||||
if (_isInitializing)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
State.TemperatureLevel = (byte)Math.Round(e.NewValue);
|
||||
if (!CanUseControls || !CanShowTemperatureControls)
|
||||
{
|
||||
_temperatureCommitPending = false;
|
||||
return;
|
||||
}
|
||||
|
||||
_temperatureCommitPending = true;
|
||||
}
|
||||
|
||||
private async void TemperatureCommitMouseUp(object sender, MouseButtonEventArgs e)
|
||||
=> await CommitTemperatureIfPendingAsync();
|
||||
|
||||
private async void TemperatureCommitLostMouseCapture(object sender, System.Windows.Input.MouseEventArgs e)
|
||||
=> await CommitTemperatureIfPendingAsync();
|
||||
|
||||
private async void TemperatureCommitKeyUp(object sender, System.Windows.Input.KeyEventArgs e)
|
||||
=> await CommitTemperatureIfPendingAsync();
|
||||
|
||||
private async void TemperaturePresetClicked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (!CanUseControls || sender is not WpfButton { Tag: var rawTag } || !byte.TryParse(rawTag?.ToString(), out var level))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
State.TemperatureLevel = level;
|
||||
TemperatureSlider.Value = level;
|
||||
_temperatureCommitPending = false;
|
||||
|
||||
await RunBusyAsync(
|
||||
async () => await _controller.ApplyTemperatureLevelAsync(level),
|
||||
useBusyFlag: false,
|
||||
treatErrorAsFault: true);
|
||||
}
|
||||
|
||||
private async void CheckForUpdatesClicked(object sender, RoutedEventArgs e)
|
||||
=> await CheckForUpdatesAsync(isManualCheck: true);
|
||||
|
||||
private async Task RunBusyAsync(Func<Task> action, bool useBusyFlag = true, bool treatErrorAsFault = false)
|
||||
{
|
||||
if (useBusyFlag && _isBusy)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (useBusyFlag)
|
||||
{
|
||||
_isBusy = true;
|
||||
NotifyBusyDependentProperties();
|
||||
}
|
||||
|
||||
await action();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (treatErrorAsFault)
|
||||
{
|
||||
State.ConnectionStatus = DeviceConnectionStatus.Faulted;
|
||||
}
|
||||
|
||||
State.StatusText = ex.Message;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (useBusyFlag)
|
||||
{
|
||||
_isBusy = false;
|
||||
NotifyBusyDependentProperties();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleStateChanged(object? sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(e.PropertyName)
|
||||
|| string.Equals(e.PropertyName, nameof(ControllerState.ControlTargetCount), StringComparison.Ordinal))
|
||||
{
|
||||
OnPropertyChanged(nameof(CanUseControls));
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(e.PropertyName)
|
||||
|| string.Equals(e.PropertyName, nameof(ControllerState.SelectedEffectId), StringComparison.Ordinal)
|
||||
|| string.Equals(e.PropertyName, nameof(ControllerState.CurrentMode), StringComparison.Ordinal))
|
||||
{
|
||||
if (!CanShowEffectSpeed)
|
||||
{
|
||||
_effectSpeedCommitPending = false;
|
||||
}
|
||||
|
||||
if (!CanShowGrayscaleControls)
|
||||
{
|
||||
_grayscaleCommitPending = false;
|
||||
}
|
||||
|
||||
if (!CanShowTemperatureControls)
|
||||
{
|
||||
_temperatureCommitPending = false;
|
||||
}
|
||||
|
||||
OnPropertyChanged(nameof(CanUseBrightnessControl));
|
||||
OnPropertyChanged(nameof(CanShowEffectControls));
|
||||
OnPropertyChanged(nameof(CanShowEffectSpeed));
|
||||
OnPropertyChanged(nameof(CanShowGrayscaleControls));
|
||||
OnPropertyChanged(nameof(CanShowTemperatureControls));
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleConnectedDevicesCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
OnPropertyChanged(nameof(CanConnect));
|
||||
OnPropertyChanged(nameof(CanDisconnectAll));
|
||||
}
|
||||
|
||||
private void NotifyBusyDependentProperties()
|
||||
{
|
||||
OnPropertyChanged(nameof(CanScan));
|
||||
OnPropertyChanged(nameof(CanConnect));
|
||||
OnPropertyChanged(nameof(CanSaveAlias));
|
||||
OnPropertyChanged(nameof(CanUseControls));
|
||||
OnPropertyChanged(nameof(CanUseBrightnessControl));
|
||||
OnPropertyChanged(nameof(CanDisconnectAll));
|
||||
}
|
||||
|
||||
private void NotifySelectedDeviceChanged()
|
||||
{
|
||||
OnPropertyChanged(nameof(CanConnect));
|
||||
OnPropertyChanged(nameof(CanSaveAlias));
|
||||
OnPropertyChanged(nameof(SelectedDeviceSummary));
|
||||
OnPropertyChanged(nameof(SelectedDeviceSummaryDisplay));
|
||||
}
|
||||
|
||||
private void NotifyLastKnownDeviceChanged()
|
||||
{
|
||||
OnPropertyChanged(nameof(LastKnownDeviceCaption));
|
||||
OnPropertyChanged(nameof(LastKnownDeviceCaptionDisplay));
|
||||
}
|
||||
|
||||
private async Task CommitBrightnessIfPendingAsync()
|
||||
{
|
||||
if (!_brightnessCommitPending || _isInitializing || !CanUseBrightnessControl)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_brightnessCommitPending = false;
|
||||
await RunBusyAsync(
|
||||
async () => await _controller.QueueBrightnessAsync(State.Brightness),
|
||||
useBusyFlag: false,
|
||||
treatErrorAsFault: true);
|
||||
}
|
||||
|
||||
private async Task CommitEffectSpeedIfPendingAsync()
|
||||
{
|
||||
if (!_effectSpeedCommitPending || _isInitializing || !CanUseControls || !CanShowEffectSpeed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_effectSpeedCommitPending = false;
|
||||
await RunBusyAsync(
|
||||
async () => await _controller.ApplyEffectSpeedAsync(State.EffectSpeed),
|
||||
useBusyFlag: false,
|
||||
treatErrorAsFault: true);
|
||||
}
|
||||
|
||||
private async Task CommitGrayscaleIfPendingAsync()
|
||||
{
|
||||
if (!_grayscaleCommitPending || _isInitializing || !CanUseControls || !CanShowGrayscaleControls)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_grayscaleCommitPending = false;
|
||||
await RunBusyAsync(
|
||||
async () => await _controller.ApplyGrayscaleLevelAsync(State.GrayscaleLevel),
|
||||
useBusyFlag: false,
|
||||
treatErrorAsFault: true);
|
||||
}
|
||||
|
||||
private async Task CommitTemperatureIfPendingAsync()
|
||||
{
|
||||
if (!_temperatureCommitPending || _isInitializing || !CanUseControls || !CanShowTemperatureControls)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_temperatureCommitPending = false;
|
||||
await RunBusyAsync(
|
||||
async () => await _controller.ApplyTemperatureLevelAsync(State.TemperatureLevel),
|
||||
useBusyFlag: false,
|
||||
treatErrorAsFault: true);
|
||||
}
|
||||
|
||||
private static IReadOnlyList<ColorPreset> CreateColorPresets()
|
||||
=> new[]
|
||||
{
|
||||
new ColorPreset("Kırmızı", "#FF0000"),
|
||||
new ColorPreset("Turuncu", "#FF9500"),
|
||||
new ColorPreset("Amber", "#FFCC00"),
|
||||
new ColorPreset("Sarı", "#FFD60A"),
|
||||
new ColorPreset("Lime", "#A8E10C"),
|
||||
new ColorPreset("Yeşil", "#34C759"),
|
||||
new ColorPreset("Turkuaz", "#30D5C8"),
|
||||
new ColorPreset("Camgöbeği", "#32ADE6"),
|
||||
new ColorPreset("Mavi", "#007AFF"),
|
||||
new ColorPreset("Mor", "#5856D6"),
|
||||
new ColorPreset("Macenta", "#FF00FF"),
|
||||
new ColorPreset("Pembe", "#FF2D55"),
|
||||
new ColorPreset("Buz mavisi", "#ADD8E6"),
|
||||
new ColorPreset("Sıcak beyaz", "#FDF4E3")
|
||||
};
|
||||
|
||||
private static MediaColor ParseHexColor(string hex)
|
||||
{
|
||||
var normalized = hex.TrimStart('#');
|
||||
if (normalized.Length != 6)
|
||||
{
|
||||
return MediaColor.FromRgb(255, 106, 78);
|
||||
}
|
||||
|
||||
return MediaColor.FromRgb(
|
||||
Convert.ToByte(normalized.Substring(0, 2), 16),
|
||||
Convert.ToByte(normalized.Substring(2, 2), 16),
|
||||
Convert.ToByte(normalized.Substring(4, 2), 16));
|
||||
}
|
||||
|
||||
private void FooterLinkClicked(object sender, RequestNavigateEventArgs e)
|
||||
{
|
||||
Process.Start(new ProcessStartInfo
|
||||
{
|
||||
FileName = e.Uri.AbsoluteUri,
|
||||
UseShellExecute = true
|
||||
});
|
||||
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
private void TrySetWindowIcon()
|
||||
{
|
||||
try
|
||||
{
|
||||
Icon = BitmapFrame.Create(new Uri("pack://application:,,,/Assets/AppIcon.ico", UriKind.Absolute));
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Window icon is cosmetic; startup should continue even if the icon resource is unavailable.
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CheckForUpdatesAsync(bool isManualCheck)
|
||||
{
|
||||
if (_isCheckingForUpdates)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_isCheckingForUpdates = true;
|
||||
NotifyUpdateProperties();
|
||||
|
||||
var updateManager = new UpdateManager(new GiteaSource(GiteaRepoUrl, null, false));
|
||||
if (!updateManager.IsInstalled)
|
||||
{
|
||||
_updatePendingRestart = false;
|
||||
UpdateStatusText = "Kurulu Velopack s\u00fcr\u00fcm\u00fc alg\u0131lanmad\u0131.";
|
||||
NotifyUpdateProperties();
|
||||
|
||||
if (isManualCheck)
|
||||
{
|
||||
System.Windows.MessageBox.Show(
|
||||
this,
|
||||
"G\u00fcncelleme denetimi sadece Setup.exe ile kurulan s\u00fcr\u00fcmlerde kullan\u0131labilir.",
|
||||
"G\u00fcncelleme",
|
||||
MessageBoxButton.OK,
|
||||
MessageBoxImage.Information);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (updateManager.UpdatePendingRestart is { } pendingAsset)
|
||||
{
|
||||
_updatePendingRestart = true;
|
||||
UpdateStatusText = "\u0130ndirilen g\u00fcncelleme uygulanmay\u0131 bekliyor.";
|
||||
NotifyUpdateProperties();
|
||||
|
||||
var applyPending = System.Windows.MessageBox.Show(
|
||||
this,
|
||||
"\u0130ndirilen bir g\u00fcncelleme haz\u0131r. Uygulay\u0131p uygulamay\u0131 yeniden ba\u015flatmak ister misin?",
|
||||
"G\u00fcncelleme haz\u0131r",
|
||||
MessageBoxButton.YesNo,
|
||||
MessageBoxImage.Question);
|
||||
|
||||
if (applyPending == MessageBoxResult.Yes)
|
||||
{
|
||||
UpdateStatusText = "G\u00fcncelleme uygulan\u0131yor...";
|
||||
await updateManager.WaitExitThenApplyUpdatesAsync(pendingAsset, restart: true);
|
||||
Close();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
_updatePendingRestart = false;
|
||||
UpdateStatusText = isManualCheck
|
||||
? "G\u00fcncellemeler kontrol ediliyor..."
|
||||
: "Ba\u015flang\u0131\u00e7 g\u00fcncelleme kontrol\u00fc yap\u0131l\u0131yor...";
|
||||
NotifyUpdateProperties();
|
||||
|
||||
var update = await updateManager.CheckForUpdatesAsync();
|
||||
if (update is null)
|
||||
{
|
||||
UpdateStatusText = "G\u00fcncel s\u00fcr\u00fcm kullan\u0131l\u0131yor.";
|
||||
if (isManualCheck)
|
||||
{
|
||||
System.Windows.MessageBox.Show(
|
||||
this,
|
||||
"Yeni bir s\u00fcr\u00fcm bulunamad\u0131.",
|
||||
"G\u00fcncelleme",
|
||||
MessageBoxButton.OK,
|
||||
MessageBoxImage.Information);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
var targetVersion = update.TargetFullRelease.Version.ToString();
|
||||
UpdateStatusText = $"Yeni s\u00fcr\u00fcm haz\u0131r: v{targetVersion}";
|
||||
|
||||
var shouldApply = System.Windows.MessageBox.Show(
|
||||
this,
|
||||
$"v{targetVersion} bulundu. \u015eimdi indirip uygulamak ister misin?",
|
||||
"G\u00fcncelleme bulundu",
|
||||
MessageBoxButton.YesNo,
|
||||
MessageBoxImage.Question);
|
||||
|
||||
if (shouldApply != MessageBoxResult.Yes)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
UpdateStatusText = $"v{targetVersion} indiriliyor...";
|
||||
await updateManager.DownloadUpdatesAsync(update, progress =>
|
||||
{
|
||||
Dispatcher.Invoke(() =>
|
||||
{
|
||||
UpdateStatusText = $"v{targetVersion} indiriliyor... %{progress}";
|
||||
});
|
||||
});
|
||||
|
||||
_updatePendingRestart = true;
|
||||
UpdateStatusText = "G\u00fcncelleme uygulan\u0131yor...";
|
||||
NotifyUpdateProperties();
|
||||
await updateManager.WaitExitThenApplyUpdatesAsync(update.TargetFullRelease, restart: true);
|
||||
Close();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_updatePendingRestart = false;
|
||||
UpdateStatusText = $"G\u00fcncelleme hatas\u0131: {ex.Message}";
|
||||
NotifyUpdateProperties();
|
||||
|
||||
if (isManualCheck)
|
||||
{
|
||||
System.Windows.MessageBox.Show(
|
||||
this,
|
||||
ex.Message,
|
||||
"G\u00fcncelleme hatas\u0131",
|
||||
MessageBoxButton.OK,
|
||||
MessageBoxImage.Warning);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isCheckingForUpdates = false;
|
||||
NotifyUpdateProperties();
|
||||
}
|
||||
}
|
||||
|
||||
private void NotifyUpdateProperties()
|
||||
{
|
||||
OnPropertyChanged(nameof(CanCheckForUpdates));
|
||||
OnPropertyChanged(nameof(UpdateButtonLabel));
|
||||
}
|
||||
|
||||
private static string GetApplicationVersion()
|
||||
{
|
||||
var assembly = Assembly.GetExecutingAssembly();
|
||||
var informationalVersion = assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion;
|
||||
if (!string.IsNullOrWhiteSpace(informationalVersion))
|
||||
{
|
||||
return informationalVersion;
|
||||
}
|
||||
|
||||
return assembly.GetName().Version?.ToString(3) ?? "0.0.0";
|
||||
}
|
||||
|
||||
private bool SetProperty<T>(ref T field, T value, [CallerMemberName] string? propertyName = null)
|
||||
{
|
||||
if (EqualityComparer<T>.Default.Equals(field, value))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
field = value;
|
||||
OnPropertyChanged(propertyName);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void OnPropertyChanged([CallerMemberName] string? propertyName = null)
|
||||
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>net8.0-windows10.0.19041.0</TargetFramework>
|
||||
<UseWPF>true</UseWPF>
|
||||
<UseWindowsForms>true</UseWindowsForms>
|
||||
<Version>0.3.0</Version>
|
||||
<AssemblyVersion>0.3.0.0</AssemblyVersion>
|
||||
<FileVersion>0.3.0.0</FileVersion>
|
||||
<InformationalVersion>0.3.0</InformationalVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<EnableWindowsTargeting>true</EnableWindowsTargeting>
|
||||
<SupportedOSPlatformVersion>10.0.19041.0</SupportedOSPlatformVersion>
|
||||
<TargetPlatformMinVersion>10.0.19041.0</TargetPlatformMinVersion>
|
||||
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||
<ApplicationIcon>Assets\AppIcon.ico</ApplicationIcon>
|
||||
<SelfContained>true</SelfContained>
|
||||
<PublishSingleFile>true</PublishSingleFile>
|
||||
<PublishTrimmed>false</PublishTrimmed>
|
||||
<IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<StartupObject>RgbLedStripController.App</StartupObject>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ApplicationDefinition Remove="App.xaml" />
|
||||
<Page Include="App.xaml" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Resource Include="Assets\AppIcon.ico" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Velopack" Version="0.0.1298" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,427 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Globalization;
|
||||
using RgbLedStripController.Core;
|
||||
using Windows.Devices.Bluetooth;
|
||||
using Windows.Devices.Bluetooth.Advertisement;
|
||||
using Windows.Devices.Bluetooth.GenericAttributeProfile;
|
||||
using Windows.Devices.Enumeration;
|
||||
using Windows.Foundation;
|
||||
using Windows.Storage.Streams;
|
||||
|
||||
namespace RgbLedStripController.Services;
|
||||
|
||||
public sealed class BleDeviceService : IBleTransport
|
||||
{
|
||||
private readonly SemaphoreSlim _connectionLock = new(1, 1);
|
||||
private readonly List<GattCharacteristic> _writeCharacteristics = [];
|
||||
private BleScanResult? _targetDevice;
|
||||
private BluetoothLEDevice? _device;
|
||||
private GattDeviceService? _service;
|
||||
private GattCharacteristic? _preferredCharacteristic;
|
||||
private bool _isManualDisconnect;
|
||||
|
||||
public event EventHandler<DeviceConnectionStatus>? ConnectionStatusChanged;
|
||||
|
||||
public async Task<IReadOnlyList<BleScanResult>> ScanAsync(TimeSpan duration, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var captures = new ConcurrentDictionary<ulong, ScanCapture>();
|
||||
var activeDuration = duration <= TimeSpan.FromSeconds(4)
|
||||
? duration
|
||||
: TimeSpan.FromMilliseconds(duration.TotalMilliseconds * 0.65);
|
||||
var passiveDuration = duration - activeDuration;
|
||||
|
||||
await RunScanPassAsync(captures, BluetoothLEScanningMode.Active, activeDuration, cancellationToken);
|
||||
if (passiveDuration > TimeSpan.Zero)
|
||||
{
|
||||
await RunScanPassAsync(captures, BluetoothLEScanningMode.Passive, passiveDuration, cancellationToken);
|
||||
}
|
||||
|
||||
await MergeWindowsDeviceInventoryAsync(captures);
|
||||
|
||||
return captures.Values
|
||||
.Select(capture => new BleScanResult
|
||||
{
|
||||
DeviceId = FormatAddress(capture.Address),
|
||||
Name = string.IsNullOrWhiteSpace(capture.Name) ? "İsimsiz BLE cihazı" : capture.Name,
|
||||
BluetoothAddress = FormatAddress(capture.Address),
|
||||
AddressValue = capture.Address,
|
||||
IsKnownCandidate = IsKnownCandidate(capture.Name),
|
||||
IsLastConnected = false,
|
||||
SignalStrength = capture.SignalStrength,
|
||||
Alias = string.Empty
|
||||
})
|
||||
.OrderByDescending(item => item.IsKnownCandidate)
|
||||
.ThenByDescending(item => item.SignalStrength)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
public async Task ConnectAsync(BleScanResult device, CancellationToken cancellationToken = default)
|
||||
{
|
||||
_targetDevice = device;
|
||||
_isManualDisconnect = false;
|
||||
RaiseConnectionStatus(DeviceConnectionStatus.Connecting);
|
||||
await EnsureCharacteristicReadyAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<bool> DiscoverAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
await EnsureCharacteristicReadyAsync(cancellationToken);
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task WriteAsync(ReadOnlyMemory<byte> payload, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await EnsureCharacteristicReadyAsync(cancellationToken);
|
||||
await WriteCoreAsync(payload, cancellationToken, allowRetry: true);
|
||||
}
|
||||
|
||||
public Task DisconnectAsync()
|
||||
{
|
||||
_isManualDisconnect = true;
|
||||
ClearDevice(clearTarget: true);
|
||||
RaiseConnectionStatus(DeviceConnectionStatus.Disconnected);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
await DisconnectAsync();
|
||||
_connectionLock.Dispose();
|
||||
}
|
||||
|
||||
private async Task EnsureCharacteristicReadyAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
if (_targetDevice is null)
|
||||
{
|
||||
throw new InvalidOperationException("Target BLE device is not selected.");
|
||||
}
|
||||
|
||||
await _connectionLock.WaitAsync(cancellationToken);
|
||||
|
||||
try
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
if (_device is null)
|
||||
{
|
||||
await CreateDeviceAsync(cancellationToken);
|
||||
}
|
||||
|
||||
if (_device is null)
|
||||
{
|
||||
throw new InvalidOperationException("BluetoothLEDevice could not be opened.");
|
||||
}
|
||||
|
||||
if (_preferredCharacteristic is not null || _writeCharacteristics.Count > 0)
|
||||
{
|
||||
RaiseConnectionStatus(DeviceConnectionStatus.Connected);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_device.ConnectionStatus != BluetoothConnectionStatus.Connected)
|
||||
{
|
||||
await CreateDeviceAsync(cancellationToken);
|
||||
}
|
||||
|
||||
var serviceResult = await _device.GetGattServicesForUuidAsync(
|
||||
ElkBledomProtocol.ServiceUuid,
|
||||
BluetoothCacheMode.Uncached);
|
||||
|
||||
if (serviceResult.Status != GattCommunicationStatus.Success || serviceResult.Services.Count == 0)
|
||||
{
|
||||
throw new InvalidOperationException("fff0 service was not found.");
|
||||
}
|
||||
|
||||
ClearGatt();
|
||||
_service = serviceResult.Services[0];
|
||||
|
||||
var characteristicResult = await _service.GetCharacteristicsAsync(BluetoothCacheMode.Uncached);
|
||||
if (characteristicResult.Status != GattCommunicationStatus.Success || characteristicResult.Characteristics.Count == 0)
|
||||
{
|
||||
throw new InvalidOperationException("No writable BLE characteristic was found.");
|
||||
}
|
||||
|
||||
_writeCharacteristics.Clear();
|
||||
_writeCharacteristics.AddRange(
|
||||
characteristicResult.Characteristics
|
||||
.Where(IsWritableCharacteristic)
|
||||
.OrderByDescending(characteristic => characteristic.Uuid == ElkBledomProtocol.CharacteristicUuid));
|
||||
|
||||
if (_writeCharacteristics.Count == 0)
|
||||
{
|
||||
throw new InvalidOperationException("No writable BLE characteristic was found.");
|
||||
}
|
||||
|
||||
_preferredCharacteristic = _writeCharacteristics[0];
|
||||
RaiseConnectionStatus(DeviceConnectionStatus.Connected);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_connectionLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task WriteCoreAsync(ReadOnlyMemory<byte> payload, CancellationToken cancellationToken, bool allowRetry)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
if (_preferredCharacteristic is null && _writeCharacteristics.Count == 0)
|
||||
{
|
||||
throw new InvalidOperationException("BLE characteristic is not ready.");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var payloadBytes = payload.ToArray();
|
||||
Exception? lastError = null;
|
||||
|
||||
foreach (var characteristic in EnumerateWriteTargets())
|
||||
{
|
||||
try
|
||||
{
|
||||
var writer = new DataWriter();
|
||||
writer.WriteBytes(payloadBytes);
|
||||
|
||||
var preferredOption = characteristic.CharacteristicProperties.HasFlag(GattCharacteristicProperties.WriteWithoutResponse)
|
||||
? GattWriteOption.WriteWithoutResponse
|
||||
: GattWriteOption.WriteWithResponse;
|
||||
|
||||
var status = await characteristic.WriteValueAsync(writer.DetachBuffer(), preferredOption);
|
||||
if (status != GattCommunicationStatus.Success)
|
||||
{
|
||||
lastError = new InvalidOperationException($"BLE write failed: {status}");
|
||||
continue;
|
||||
}
|
||||
|
||||
_preferredCharacteristic = characteristic;
|
||||
RaiseConnectionStatus(DeviceConnectionStatus.Connected);
|
||||
return;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
lastError = ex;
|
||||
}
|
||||
}
|
||||
|
||||
throw lastError ?? new InvalidOperationException("BLE write failed.");
|
||||
}
|
||||
catch when (allowRetry)
|
||||
{
|
||||
ClearGatt();
|
||||
await EnsureCharacteristicReadyAsync(cancellationToken);
|
||||
await WriteCoreAsync(payload, cancellationToken, allowRetry: false);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CreateDeviceAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
if (_targetDevice is null)
|
||||
{
|
||||
throw new InvalidOperationException("Target BLE device is not selected.");
|
||||
}
|
||||
|
||||
ClearDevice(clearTarget: false);
|
||||
|
||||
_device = await BluetoothLEDevice.FromBluetoothAddressAsync(_targetDevice.AddressValue);
|
||||
if (_device is null)
|
||||
{
|
||||
throw new InvalidOperationException("Bluetooth device could not be opened.");
|
||||
}
|
||||
|
||||
_device.ConnectionStatusChanged += HandleConnectionStatusChanged;
|
||||
}
|
||||
|
||||
private void HandleConnectionStatusChanged(BluetoothLEDevice sender, object args)
|
||||
{
|
||||
if (sender.ConnectionStatus == BluetoothConnectionStatus.Disconnected)
|
||||
{
|
||||
ClearGatt();
|
||||
|
||||
if (!_isManualDisconnect)
|
||||
{
|
||||
RaiseConnectionStatus(DeviceConnectionStatus.Disconnected);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
_isManualDisconnect = false;
|
||||
RaiseConnectionStatus(DeviceConnectionStatus.Connected);
|
||||
}
|
||||
|
||||
private void ClearGatt()
|
||||
{
|
||||
_preferredCharacteristic = null;
|
||||
_writeCharacteristics.Clear();
|
||||
_service?.Dispose();
|
||||
_service = null;
|
||||
}
|
||||
|
||||
private void ClearDevice(bool clearTarget)
|
||||
{
|
||||
ClearGatt();
|
||||
|
||||
if (_device is not null)
|
||||
{
|
||||
_device.ConnectionStatusChanged -= HandleConnectionStatusChanged;
|
||||
_device.Dispose();
|
||||
_device = null;
|
||||
}
|
||||
|
||||
if (clearTarget)
|
||||
{
|
||||
_targetDevice = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void RaiseConnectionStatus(DeviceConnectionStatus status)
|
||||
=> ConnectionStatusChanged?.Invoke(this, status);
|
||||
|
||||
private static bool IsKnownCandidate(string? name)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var normalized = name.ToUpperInvariant();
|
||||
return normalized.Contains("ELK-BLEDOM")
|
||||
|| normalized.Contains("ELK-BLEDOB")
|
||||
|| normalized.Contains("LOTUS")
|
||||
|| normalized.Contains("DUOCO");
|
||||
}
|
||||
|
||||
private static async Task RunScanPassAsync(
|
||||
ConcurrentDictionary<ulong, ScanCapture> captures,
|
||||
BluetoothLEScanningMode mode,
|
||||
TimeSpan duration,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (duration <= TimeSpan.Zero)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var watcher = new BluetoothLEAdvertisementWatcher
|
||||
{
|
||||
ScanningMode = mode,
|
||||
AllowExtendedAdvertisements = true
|
||||
};
|
||||
|
||||
TypedEventHandler<BluetoothLEAdvertisementWatcher, BluetoothLEAdvertisementReceivedEventArgs> receivedHandler = (_, args) =>
|
||||
{
|
||||
captures.AddOrUpdate(
|
||||
args.BluetoothAddress,
|
||||
_ => new ScanCapture(args.BluetoothAddress, args.Advertisement.LocalName, args.RawSignalStrengthInDBm),
|
||||
(_, existing) => existing.Merge(args.Advertisement.LocalName, args.RawSignalStrengthInDBm));
|
||||
};
|
||||
|
||||
watcher.Received += receivedHandler;
|
||||
watcher.Start();
|
||||
|
||||
try
|
||||
{
|
||||
await Task.Delay(duration, cancellationToken);
|
||||
}
|
||||
finally
|
||||
{
|
||||
watcher.Stop();
|
||||
watcher.Received -= receivedHandler;
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task MergeWindowsDeviceInventoryAsync(ConcurrentDictionary<ulong, ScanCapture> captures)
|
||||
{
|
||||
try
|
||||
{
|
||||
var additionalProperties = new[]
|
||||
{
|
||||
"System.Devices.Aep.DeviceAddress"
|
||||
};
|
||||
|
||||
var devices = await DeviceInformation.FindAllAsync(
|
||||
BluetoothLEDevice.GetDeviceSelector(),
|
||||
additionalProperties);
|
||||
|
||||
foreach (var device in devices)
|
||||
{
|
||||
if (!IsKnownCandidate(device.Name))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!device.Properties.TryGetValue("System.Devices.Aep.DeviceAddress", out var addressValue)
|
||||
|| addressValue is not string addressText
|
||||
|| !TryParseBluetoothAddress(addressText, out var address))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
captures.AddOrUpdate(
|
||||
address,
|
||||
_ => new ScanCapture(address, device.Name, short.MinValue),
|
||||
(_, existing) => existing.Merge(device.Name, short.MinValue));
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Best effort only.
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsWritableCharacteristic(GattCharacteristic characteristic)
|
||||
=> characteristic.CharacteristicProperties.HasFlag(GattCharacteristicProperties.Write)
|
||||
|| characteristic.CharacteristicProperties.HasFlag(GattCharacteristicProperties.WriteWithoutResponse);
|
||||
|
||||
private IEnumerable<GattCharacteristic> EnumerateWriteTargets()
|
||||
{
|
||||
if (_preferredCharacteristic is not null)
|
||||
{
|
||||
yield return _preferredCharacteristic;
|
||||
}
|
||||
|
||||
foreach (var characteristic in _writeCharacteristics)
|
||||
{
|
||||
if (!ReferenceEquals(characteristic, _preferredCharacteristic))
|
||||
{
|
||||
yield return characteristic;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static string FormatAddress(ulong address)
|
||||
{
|
||||
var bytes = BitConverter.GetBytes(address).Take(6).Reverse().ToArray();
|
||||
return string.Join(":", bytes.Select(item => item.ToString("X2")));
|
||||
}
|
||||
|
||||
private static bool TryParseBluetoothAddress(string? value, out ulong address)
|
||||
{
|
||||
address = 0;
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var normalized = value.Replace(":", string.Empty).Replace("-", string.Empty).Trim();
|
||||
return ulong.TryParse(normalized, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out address);
|
||||
}
|
||||
|
||||
private sealed record ScanCapture(ulong Address, string Name, short SignalStrength)
|
||||
{
|
||||
public ScanCapture Merge(string? name, short signalStrength)
|
||||
=> this with
|
||||
{
|
||||
Name = string.IsNullOrWhiteSpace(name) ? Name : name!,
|
||||
SignalStrength = signalStrength == short.MinValue ? SignalStrength : signalStrength
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
using RgbLedStripController.Core;
|
||||
using MediaColor = System.Windows.Media.Color;
|
||||
|
||||
namespace RgbLedStripController.Services;
|
||||
|
||||
public static class ElkBledomProtocol
|
||||
{
|
||||
public static readonly Guid ServiceUuid = Guid.Parse("0000fff0-0000-1000-8000-00805f9b34fb");
|
||||
public static readonly Guid CharacteristicUuid = Guid.Parse("0000fff3-0000-1000-8000-00805f9b34fb");
|
||||
|
||||
public static byte[] BuildPower(bool isOn)
|
||||
{
|
||||
var flag = isOn ? (byte)0x01 : (byte)0x00;
|
||||
return new byte[] { 0x7E, 0x04, 0x04, flag, 0x00, flag, 0xFF, 0x00, 0xEF };
|
||||
}
|
||||
|
||||
public static byte[] BuildLegacyPower(bool isOn)
|
||||
{
|
||||
var flag = isOn ? (byte)0x01 : (byte)0x00;
|
||||
return new byte[] { 0x7E, 0x00, 0x04, flag, 0x00, 0x00, 0x00, 0x00, 0xEF };
|
||||
}
|
||||
|
||||
public static byte[] BuildColor(MediaColor color)
|
||||
=> new byte[] { 0x7E, 0x07, 0x05, 0x03, color.R, color.G, color.B, 0x10, 0xEF };
|
||||
|
||||
public static byte[] BuildLegacyColor(MediaColor color)
|
||||
=> new byte[] { 0x7E, 0x00, 0x05, 0x03, color.R, color.G, color.B, 0x00, 0xEF };
|
||||
|
||||
public static byte[] BuildBrightness(byte brightness)
|
||||
{
|
||||
var value = (byte)Math.Clamp((int)brightness, 0, 100);
|
||||
return new byte[] { 0x7E, 0x04, 0x01, value, 0xFF, 0xFF, 0xFF, 0x00, 0xEF };
|
||||
}
|
||||
|
||||
public static byte[] BuildLegacyBrightness(byte brightness)
|
||||
{
|
||||
var value = (byte)Math.Clamp((int)brightness, 0, 100);
|
||||
return new byte[] { 0x7E, 0x00, 0x01, value, 0x00, 0x00, 0x00, 0x00, 0xEF };
|
||||
}
|
||||
|
||||
public static byte[] BuildEffect(byte patternId)
|
||||
{
|
||||
var clamped = (byte)Math.Clamp((int)patternId, 0, 28);
|
||||
return new byte[] { 0x7E, 0x05, 0x03, (byte)(clamped + 128), 0x03, 0xFF, 0xFF, 0x00, 0xEF };
|
||||
}
|
||||
|
||||
public static byte[] BuildEffectSpeed(byte speed)
|
||||
{
|
||||
var value = (byte)Math.Clamp((int)speed, 0, 100);
|
||||
return new byte[] { 0x7E, 0x00, 0x02, value, 0x00, 0x00, 0x00, 0x00, 0xEF };
|
||||
}
|
||||
|
||||
public static byte[] BuildGrayscaleMode()
|
||||
=> new byte[] { 0x7E, 0x00, 0x03, 0x00, 0x01, 0x00, 0x00, 0x00, 0xEF };
|
||||
|
||||
public static byte[] BuildGrayscaleLevel(byte level)
|
||||
{
|
||||
var value = (byte)Math.Clamp((int)level, 0, 100);
|
||||
return new byte[] { 0x7E, 0x00, 0x05, 0x01, value, 0x00, 0x00, 0x00, 0xEF };
|
||||
}
|
||||
|
||||
public static byte[] BuildTemperatureMode(byte level)
|
||||
{
|
||||
var mapped = (byte)(0x80 + Math.Round(Math.Clamp((double)level, 0, 100) * 10d / 100d));
|
||||
return new byte[] { 0x7E, 0x00, 0x03, mapped, 0x02, 0x00, 0x00, 0x00, 0xEF };
|
||||
}
|
||||
|
||||
public static byte[] BuildTemperatureLevel(byte level)
|
||||
{
|
||||
var value = (byte)Math.Clamp((int)level, 0, 100);
|
||||
return new byte[] { 0x7E, 0x00, 0x05, 0x02, value, 0x00, 0x00, 0x00, 0xEF };
|
||||
}
|
||||
|
||||
public static byte[] BuildRgbModeReset()
|
||||
=> new byte[] { 0x7E, 0x00, 0x03, 0x00, 0x05, 0x00, 0x00, 0x00, 0xEF };
|
||||
|
||||
public static bool EffectSupportsSpeed(byte patternId)
|
||||
=> patternId >= 0x07 && patternId <= 0x1C;
|
||||
|
||||
public static IReadOnlyList<EffectOption> CreateEffectOptions()
|
||||
=> new[]
|
||||
{
|
||||
new EffectOption(0x00, "Tek Renk Kırmızı", false),
|
||||
new EffectOption(0x01, "Tek Renk Yeşil", false),
|
||||
new EffectOption(0x02, "Tek Renk Mavi", false),
|
||||
new EffectOption(0x03, "Tek Renk Sarı", false),
|
||||
new EffectOption(0x04, "Tek Renk Camgöbeği", false),
|
||||
new EffectOption(0x05, "Tek Renk Macenta", false),
|
||||
new EffectOption(0x06, "Tek Renk Beyaz", false),
|
||||
new EffectOption(0x07, "Atlama RGB", true),
|
||||
new EffectOption(0x08, "Atlama Gökkuşağı", true),
|
||||
new EffectOption(0x09, "Geçiş RGB", true),
|
||||
new EffectOption(0x0A, "Geçiş Gökkuşağı", true),
|
||||
new EffectOption(0x0B, "Geçiş Kırmızı", true),
|
||||
new EffectOption(0x0C, "Geçiş Yeşil", true),
|
||||
new EffectOption(0x0D, "Geçiş Mavi", true),
|
||||
new EffectOption(0x0E, "Geçiş Sarı", true),
|
||||
new EffectOption(0x0F, "Geçiş Camgöbeği", true),
|
||||
new EffectOption(0x10, "Geçiş Macenta", true),
|
||||
new EffectOption(0x11, "Geçiş Beyaz", true),
|
||||
new EffectOption(0x12, "Geçiş Kırmızı-Yeşil", true),
|
||||
new EffectOption(0x13, "Geçiş Kırmızı-Mavi", true),
|
||||
new EffectOption(0x14, "Geçiş Yeşil-Mavi", true),
|
||||
new EffectOption(0x15, "Yanıp Sön Gökkuşağı", true),
|
||||
new EffectOption(0x16, "Yanıp Sön Kırmızı", true),
|
||||
new EffectOption(0x17, "Yanıp Sön Yeşil", true),
|
||||
new EffectOption(0x18, "Yanıp Sön Mavi", true),
|
||||
new EffectOption(0x19, "Yanıp Sön Sarı", true),
|
||||
new EffectOption(0x1A, "Yanıp Sön Camgöbeği", true),
|
||||
new EffectOption(0x1B, "Yanıp Sön Macenta", true),
|
||||
new EffectOption(0x1C, "Yanıp Sön Beyaz", true)
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using RgbLedStripController.Core;
|
||||
|
||||
namespace RgbLedStripController.Services;
|
||||
|
||||
public interface IBleTransport : IAsyncDisposable
|
||||
{
|
||||
event EventHandler<DeviceConnectionStatus>? ConnectionStatusChanged;
|
||||
|
||||
Task<IReadOnlyList<BleScanResult>> ScanAsync(TimeSpan duration, CancellationToken cancellationToken = default);
|
||||
|
||||
Task ConnectAsync(BleScanResult device, CancellationToken cancellationToken = default);
|
||||
|
||||
Task<bool> DiscoverAsync(CancellationToken cancellationToken = default);
|
||||
|
||||
Task WriteAsync(ReadOnlyMemory<byte> payload, CancellationToken cancellationToken = default);
|
||||
|
||||
Task DisconnectAsync();
|
||||
}
|
||||
@@ -0,0 +1,729 @@
|
||||
using System.ComponentModel;
|
||||
using RgbLedStripController.Core;
|
||||
using MediaColor = System.Windows.Media.Color;
|
||||
|
||||
namespace RgbLedStripController.Services;
|
||||
|
||||
public sealed class MultiDeviceController : IAsyncDisposable
|
||||
{
|
||||
private readonly SettingsStore _settingsStore;
|
||||
private readonly SemaphoreSlim _writeLock = new(1, 1);
|
||||
private readonly object _stateLock = new();
|
||||
private readonly object _pendingLock = new();
|
||||
private readonly Dictionary<string, DeviceConnectionContext> _connections = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly Dictionary<string, string> _aliases = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly List<DeviceProfile> _knownDevices = [];
|
||||
private MediaColor? _pendingColor;
|
||||
private byte? _pendingBrightness;
|
||||
private bool _pendingEnsurePoweredOn;
|
||||
|
||||
public MultiDeviceController(SettingsStore settingsStore, ControllerState state)
|
||||
{
|
||||
_settingsStore = settingsStore;
|
||||
State = state;
|
||||
}
|
||||
|
||||
public ControllerState State { get; }
|
||||
|
||||
public DeviceProfile? LastKnownDevice
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_stateLock)
|
||||
{
|
||||
return _knownDevices
|
||||
.OrderByDescending(device => device.LastSeenUtc)
|
||||
.FirstOrDefault();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task InitializeAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var snapshot = await _settingsStore.LoadAsync(cancellationToken);
|
||||
|
||||
lock (_stateLock)
|
||||
{
|
||||
_aliases.Clear();
|
||||
foreach (var pair in snapshot.DeviceAliases)
|
||||
{
|
||||
_aliases[pair.Key] = pair.Value;
|
||||
}
|
||||
|
||||
_knownDevices.Clear();
|
||||
_knownDevices.AddRange(snapshot.KnownDevices);
|
||||
}
|
||||
|
||||
State.SelectedColor = snapshot.SelectedColor;
|
||||
State.Brightness = snapshot.Brightness;
|
||||
State.EffectSpeed = snapshot.EffectSpeed;
|
||||
State.CurrentMode = snapshot.CurrentMode;
|
||||
State.GrayscaleLevel = snapshot.GrayscaleLevel;
|
||||
State.TemperatureLevel = snapshot.TemperatureLevel;
|
||||
State.SelectedEffectId = snapshot.SelectedEffectId;
|
||||
State.IsPoweredOn = snapshot.IsPoweredOn;
|
||||
State.StatusText = LastKnownDevice is null
|
||||
? "Tarama için hazır."
|
||||
: $"Son bilinen cihaz: {LastKnownDevice.LocalAlias}";
|
||||
|
||||
RecalculateAggregateState();
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<BleScanResult>> ScanForDevicesAsync(TimeSpan duration, CancellationToken cancellationToken = default)
|
||||
{
|
||||
State.ConnectionStatus = DeviceConnectionStatus.Scanning;
|
||||
State.StatusText = "BLE cihazları taranıyor...";
|
||||
|
||||
await using var scanner = new BleDeviceService();
|
||||
var results = await scanner.ScanAsync(duration, cancellationToken);
|
||||
foreach (var item in results)
|
||||
{
|
||||
var alias = GetAlias(item.BluetoothAddress);
|
||||
item.Alias = alias;
|
||||
item.IsLastConnected = string.Equals(item.BluetoothAddress, LastKnownDevice?.BluetoothAddress, StringComparison.OrdinalIgnoreCase);
|
||||
item.IsKnownCandidate = item.IsKnownCandidate || !string.IsNullOrWhiteSpace(alias);
|
||||
}
|
||||
|
||||
var candidateCount = results.Count(item => item.IsKnownCandidate);
|
||||
State.StatusText = results.Count == 0
|
||||
? "Tarama bitti, uygun cihaz bulunamadı."
|
||||
: $"{results.Count} BLE cihazı bulundu, {candidateCount} tanesi aday görünüyor.";
|
||||
|
||||
RecalculateAggregateState();
|
||||
return results
|
||||
.OrderByDescending(device => device.IsLastConnected)
|
||||
.ThenByDescending(device => device.IsKnownCandidate)
|
||||
.ThenByDescending(device => device.SignalStrength)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
public async Task<ConnectedDeviceViewModel> ConnectAsync(BleScanResult device, CancellationToken cancellationToken = default)
|
||||
{
|
||||
DeviceConnectionContext? existing;
|
||||
lock (_stateLock)
|
||||
{
|
||||
_connections.TryGetValue(device.BluetoothAddress, out existing);
|
||||
}
|
||||
|
||||
if (existing is not null)
|
||||
{
|
||||
await EnsureConnectedAsync(existing, cancellationToken);
|
||||
return existing.ViewModel;
|
||||
}
|
||||
|
||||
var viewModel = new ConnectedDeviceViewModel
|
||||
{
|
||||
DeviceId = device.DeviceId,
|
||||
NativeName = device.NativeName,
|
||||
BluetoothAddress = device.BluetoothAddress,
|
||||
AddressValue = device.AddressValue,
|
||||
SignalStrength = device.SignalStrength,
|
||||
Alias = GetAlias(device.BluetoothAddress),
|
||||
ConnectionStatus = DeviceConnectionStatus.Connecting,
|
||||
StatusText = "Bağlantı kuruluyor..."
|
||||
};
|
||||
|
||||
var transport = new BleDeviceService();
|
||||
var context = new DeviceConnectionContext(device, viewModel, transport);
|
||||
transport.ConnectionStatusChanged += HandleConnectionStatusChanged;
|
||||
viewModel.PropertyChanged += HandleConnectedDevicePropertyChanged;
|
||||
|
||||
lock (_stateLock)
|
||||
{
|
||||
_connections[device.BluetoothAddress] = context;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await EnsureConnectedAsync(context, cancellationToken);
|
||||
viewModel.StatusText = "Komut almaya hazır.";
|
||||
UpsertKnownDevice(device);
|
||||
await PersistStateAsync(cancellationToken);
|
||||
RecalculateAggregateState();
|
||||
return viewModel;
|
||||
}
|
||||
catch
|
||||
{
|
||||
await RemoveConnectionAsync(device.BluetoothAddress, cancellationToken);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task DisconnectAsync(string bluetoothAddress, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await RemoveConnectionAsync(bluetoothAddress, cancellationToken);
|
||||
State.StatusText = "Seçili cihaz bağlantıdan çıkarıldı.";
|
||||
RecalculateAggregateState();
|
||||
await PersistStateAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public async Task DisconnectAllAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
string[] addresses;
|
||||
lock (_stateLock)
|
||||
{
|
||||
addresses = _connections.Keys.ToArray();
|
||||
}
|
||||
|
||||
foreach (var address in addresses)
|
||||
{
|
||||
await RemoveConnectionAsync(address, cancellationToken);
|
||||
}
|
||||
|
||||
State.StatusText = "Tüm cihaz bağlantıları kapatıldı.";
|
||||
RecalculateAggregateState();
|
||||
await PersistStateAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public async Task SetAliasAsync(BleScanResult device, string alias, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var trimmed = alias.Trim();
|
||||
DeviceConnectionContext? context;
|
||||
|
||||
lock (_stateLock)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(trimmed))
|
||||
{
|
||||
_aliases.Remove(device.BluetoothAddress);
|
||||
device.Alias = string.Empty;
|
||||
}
|
||||
else
|
||||
{
|
||||
_aliases[device.BluetoothAddress] = trimmed;
|
||||
device.Alias = trimmed;
|
||||
}
|
||||
|
||||
_connections.TryGetValue(device.BluetoothAddress, out context);
|
||||
}
|
||||
|
||||
if (context is not null)
|
||||
{
|
||||
context.ViewModel.Alias = device.Alias;
|
||||
}
|
||||
|
||||
UpsertKnownDevice(device);
|
||||
State.StatusText = string.IsNullOrWhiteSpace(device.Alias)
|
||||
? $"{device.NativeName} için varsayılan isim kullanılacak."
|
||||
: $"{device.NativeName} için takma ad kaydedildi.";
|
||||
|
||||
await PersistStateAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public async Task ApplyPowerAsync(bool isOn, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await BroadcastAsync(
|
||||
async transport => await transport.WriteAsync(ElkBledomProtocol.BuildPower(isOn), cancellationToken),
|
||||
successMessage: isOn ? "Işıklar açıldı." : "Işıklar kapatıldı.",
|
||||
cancellationToken);
|
||||
|
||||
State.IsPoweredOn = isOn;
|
||||
await PersistStateAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public async Task QueueColorAsync(MediaColor color)
|
||||
{
|
||||
EnsureTargetsAvailable();
|
||||
var shouldPowerOn = !State.IsPoweredOn;
|
||||
|
||||
State.CurrentMode = LightingMode.Rgb;
|
||||
State.SelectedColor = color;
|
||||
State.IsPoweredOn = true;
|
||||
State.StatusText = "Renk uygulanıyor...";
|
||||
|
||||
lock (_pendingLock)
|
||||
{
|
||||
_pendingColor = color;
|
||||
_pendingEnsurePoweredOn |= shouldPowerOn;
|
||||
}
|
||||
|
||||
await DrainPendingWritesAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
public async Task QueueBrightnessAsync(byte brightness)
|
||||
{
|
||||
EnsureTargetsAvailable();
|
||||
var shouldPowerOn = !State.IsPoweredOn && brightness > 0;
|
||||
|
||||
State.Brightness = brightness;
|
||||
if (brightness > 0)
|
||||
{
|
||||
State.IsPoweredOn = true;
|
||||
}
|
||||
|
||||
State.StatusText = "Parlaklık uygulanıyor...";
|
||||
|
||||
lock (_pendingLock)
|
||||
{
|
||||
_pendingBrightness = brightness;
|
||||
_pendingEnsurePoweredOn |= shouldPowerOn;
|
||||
}
|
||||
|
||||
await DrainPendingWritesAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
public async Task ApplyEffectAsync(byte patternId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
EnsureTargetsAvailable();
|
||||
await DrainPendingWritesAsync(cancellationToken);
|
||||
|
||||
State.CurrentMode = LightingMode.Effect;
|
||||
State.SelectedEffectId = patternId;
|
||||
State.IsPoweredOn = true;
|
||||
|
||||
await BroadcastAsync(
|
||||
async transport =>
|
||||
{
|
||||
await WriteWithPacingAsync(transport, ElkBledomProtocol.BuildPower(true), cancellationToken);
|
||||
await WriteWithPacingAsync(transport, ElkBledomProtocol.BuildEffect(patternId), cancellationToken);
|
||||
if (ElkBledomProtocol.EffectSupportsSpeed(patternId))
|
||||
{
|
||||
await WriteWithPacingAsync(transport, ElkBledomProtocol.BuildEffectSpeed(State.EffectSpeed), cancellationToken);
|
||||
}
|
||||
},
|
||||
$"Efekt {patternId + 1:00} uygulandı.",
|
||||
cancellationToken);
|
||||
|
||||
await PersistStateAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public async Task ApplyGrayscaleModeAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
EnsureTargetsAvailable();
|
||||
State.CurrentMode = LightingMode.Grayscale;
|
||||
State.IsPoweredOn = true;
|
||||
|
||||
await BroadcastAsync(
|
||||
async transport =>
|
||||
{
|
||||
await WriteWithPacingAsync(transport, ElkBledomProtocol.BuildPower(true), cancellationToken);
|
||||
await WriteWithPacingAsync(transport, ElkBledomProtocol.BuildGrayscaleMode(), cancellationToken);
|
||||
await WriteWithPacingAsync(transport, ElkBledomProtocol.BuildGrayscaleLevel(State.GrayscaleLevel), cancellationToken);
|
||||
},
|
||||
"Gri ton modu uygulandı.",
|
||||
cancellationToken);
|
||||
|
||||
await PersistStateAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public async Task ApplyGrayscaleLevelAsync(byte level, CancellationToken cancellationToken = default)
|
||||
{
|
||||
EnsureTargetsAvailable();
|
||||
State.CurrentMode = LightingMode.Grayscale;
|
||||
State.GrayscaleLevel = level;
|
||||
State.IsPoweredOn = true;
|
||||
State.StatusText = "Gri ton uygulanıyor...";
|
||||
|
||||
await BroadcastAsync(
|
||||
async transport => await WriteWithPacingAsync(transport, ElkBledomProtocol.BuildGrayscaleLevel(level), cancellationToken),
|
||||
"Gri ton uygulandı.",
|
||||
cancellationToken);
|
||||
|
||||
await PersistStateAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public async Task ApplyTemperatureModeAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
EnsureTargetsAvailable();
|
||||
State.CurrentMode = LightingMode.Temperature;
|
||||
State.IsPoweredOn = true;
|
||||
|
||||
await BroadcastAsync(
|
||||
async transport =>
|
||||
{
|
||||
await WriteWithPacingAsync(transport, ElkBledomProtocol.BuildPower(true), cancellationToken);
|
||||
await WriteWithPacingAsync(transport, ElkBledomProtocol.BuildTemperatureMode(State.TemperatureLevel), cancellationToken);
|
||||
await WriteWithPacingAsync(transport, ElkBledomProtocol.BuildTemperatureLevel(State.TemperatureLevel), cancellationToken);
|
||||
},
|
||||
"Beyaz sıcaklığı modu uygulandı.",
|
||||
cancellationToken);
|
||||
|
||||
await PersistStateAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public async Task ApplyTemperatureLevelAsync(byte level, CancellationToken cancellationToken = default)
|
||||
{
|
||||
EnsureTargetsAvailable();
|
||||
State.CurrentMode = LightingMode.Temperature;
|
||||
State.TemperatureLevel = level;
|
||||
State.IsPoweredOn = true;
|
||||
State.StatusText = "Beyaz sıcaklığı uygulanıyor...";
|
||||
|
||||
await BroadcastAsync(
|
||||
async transport => await WriteWithPacingAsync(transport, ElkBledomProtocol.BuildTemperatureLevel(level), cancellationToken),
|
||||
"Beyaz sıcaklığı uygulandı.",
|
||||
cancellationToken);
|
||||
|
||||
await PersistStateAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public async Task ApplyEffectSpeedAsync(byte speed, CancellationToken cancellationToken = default)
|
||||
{
|
||||
State.EffectSpeed = speed;
|
||||
if (!ElkBledomProtocol.EffectSupportsSpeed(State.SelectedEffectId))
|
||||
{
|
||||
await PersistStateAsync(cancellationToken);
|
||||
return;
|
||||
}
|
||||
|
||||
EnsureTargetsAvailable();
|
||||
State.StatusText = "Efekt hızı uygulanıyor...";
|
||||
|
||||
await BroadcastAsync(
|
||||
async transport => await WriteWithPacingAsync(transport, ElkBledomProtocol.BuildEffectSpeed(speed), cancellationToken),
|
||||
"Efekt hızı uygulandı.",
|
||||
cancellationToken);
|
||||
|
||||
await PersistStateAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
CancelPendingFlush();
|
||||
|
||||
DeviceConnectionContext[] contexts;
|
||||
lock (_stateLock)
|
||||
{
|
||||
contexts = _connections.Values.ToArray();
|
||||
_connections.Clear();
|
||||
}
|
||||
|
||||
foreach (var context in contexts)
|
||||
{
|
||||
context.Transport.ConnectionStatusChanged -= HandleConnectionStatusChanged;
|
||||
context.ViewModel.PropertyChanged -= HandleConnectedDevicePropertyChanged;
|
||||
await context.Transport.DisposeAsync();
|
||||
}
|
||||
|
||||
_writeLock.Dispose();
|
||||
}
|
||||
|
||||
private async Task EnsureConnectedAsync(DeviceConnectionContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
context.ViewModel.ConnectionStatus = DeviceConnectionStatus.Connecting;
|
||||
context.ViewModel.StatusText = "Bağlantı kuruluyor...";
|
||||
await context.Transport.ConnectAsync(context.Device, cancellationToken);
|
||||
var discovered = await context.Transport.DiscoverAsync(cancellationToken);
|
||||
if (!discovered)
|
||||
{
|
||||
context.ViewModel.ConnectionStatus = DeviceConnectionStatus.Faulted;
|
||||
context.ViewModel.StatusText = "fff0 / fff3 bulunamadı.";
|
||||
throw new InvalidOperationException("Hedef cihazda fff0 / fff3 bulunamadı.");
|
||||
}
|
||||
|
||||
context.ViewModel.ConnectionStatus = DeviceConnectionStatus.Connected;
|
||||
context.ViewModel.StatusText = "Komut almaya hazır.";
|
||||
}
|
||||
|
||||
private async Task BroadcastAsync(Func<IBleTransport, Task> sendAsync, string successMessage, CancellationToken cancellationToken)
|
||||
{
|
||||
var targets = GetTargetConnections();
|
||||
if (targets.Count == 0)
|
||||
{
|
||||
throw new InvalidOperationException("Komut gönderebilmek için en az bir hedef cihaz seçmelisin.");
|
||||
}
|
||||
|
||||
await _writeLock.WaitAsync(cancellationToken);
|
||||
|
||||
try
|
||||
{
|
||||
var successCount = 0;
|
||||
var failureCount = 0;
|
||||
var firstFailureMessage = string.Empty;
|
||||
|
||||
foreach (var context in targets)
|
||||
{
|
||||
context.ViewModel.ConnectionStatus = DeviceConnectionStatus.Connecting;
|
||||
context.ViewModel.StatusText = "Komut gönderiliyor...";
|
||||
|
||||
try
|
||||
{
|
||||
await sendAsync(context.Transport);
|
||||
context.ViewModel.ConnectionStatus = DeviceConnectionStatus.Connected;
|
||||
context.ViewModel.StatusText = successMessage;
|
||||
UpsertKnownDevice(context.Device);
|
||||
successCount++;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
context.ViewModel.ConnectionStatus = DeviceConnectionStatus.Faulted;
|
||||
context.ViewModel.StatusText = $"Hata: {ex.Message}";
|
||||
failureCount++;
|
||||
if (string.IsNullOrWhiteSpace(firstFailureMessage))
|
||||
{
|
||||
firstFailureMessage = ex.Message;
|
||||
}
|
||||
}
|
||||
|
||||
await Task.Delay(25, cancellationToken);
|
||||
}
|
||||
|
||||
State.StatusText = BuildStatusMessage(successMessage, targets.Count, successCount, failureCount, firstFailureMessage);
|
||||
RecalculateAggregateState();
|
||||
}
|
||||
finally
|
||||
{
|
||||
_writeLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DrainPendingWritesAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
MediaColor? color;
|
||||
byte? brightness;
|
||||
bool ensurePoweredOn;
|
||||
|
||||
lock (_pendingLock)
|
||||
{
|
||||
color = _pendingColor;
|
||||
brightness = _pendingBrightness;
|
||||
ensurePoweredOn = _pendingEnsurePoweredOn;
|
||||
_pendingColor = null;
|
||||
_pendingBrightness = null;
|
||||
_pendingEnsurePoweredOn = false;
|
||||
}
|
||||
|
||||
if (!color.HasValue && !brightness.HasValue)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await BroadcastAsync(
|
||||
async transport =>
|
||||
{
|
||||
var shouldResetRgbMode = color.HasValue || brightness.HasValue;
|
||||
var shouldPowerOnBeforeStaticWrite = color.HasValue || brightness.GetValueOrDefault() > 0 || ensurePoweredOn;
|
||||
var targetColor = color ?? State.SelectedColor;
|
||||
var targetBrightness = brightness ?? State.Brightness;
|
||||
if (shouldResetRgbMode)
|
||||
{
|
||||
if (shouldPowerOnBeforeStaticWrite)
|
||||
{
|
||||
await WriteWithPacingAsync(transport, ElkBledomProtocol.BuildPower(true), cancellationToken);
|
||||
await WriteWithPacingAsync(transport, ElkBledomProtocol.BuildLegacyPower(true), cancellationToken);
|
||||
}
|
||||
|
||||
await WriteWithPacingAsync(transport, ElkBledomProtocol.BuildRgbModeReset(), cancellationToken);
|
||||
}
|
||||
|
||||
if (color.HasValue || brightness.HasValue)
|
||||
{
|
||||
await WriteWithPacingAsync(transport, ElkBledomProtocol.BuildColor(targetColor), cancellationToken);
|
||||
await WriteWithPacingAsync(transport, ElkBledomProtocol.BuildLegacyColor(targetColor), cancellationToken);
|
||||
await WriteWithPacingAsync(transport, ElkBledomProtocol.BuildBrightness(targetBrightness), cancellationToken);
|
||||
await WriteWithPacingAsync(transport, ElkBledomProtocol.BuildLegacyBrightness(targetBrightness), cancellationToken);
|
||||
}
|
||||
},
|
||||
"Değerler uygulandı.",
|
||||
cancellationToken);
|
||||
|
||||
await PersistStateAsync(cancellationToken);
|
||||
}
|
||||
|
||||
private void CancelPendingFlush()
|
||||
{
|
||||
lock (_pendingLock)
|
||||
{
|
||||
_pendingColor = null;
|
||||
_pendingBrightness = null;
|
||||
_pendingEnsurePoweredOn = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RemoveConnectionAsync(string bluetoothAddress, CancellationToken cancellationToken)
|
||||
{
|
||||
DeviceConnectionContext? context;
|
||||
lock (_stateLock)
|
||||
{
|
||||
if (!_connections.TryGetValue(bluetoothAddress, out context))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_connections.Remove(bluetoothAddress);
|
||||
}
|
||||
|
||||
if (context is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
context.Transport.ConnectionStatusChanged -= HandleConnectionStatusChanged;
|
||||
context.ViewModel.PropertyChanged -= HandleConnectedDevicePropertyChanged;
|
||||
await context.Transport.DisconnectAsync();
|
||||
await context.Transport.DisposeAsync();
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
}
|
||||
|
||||
private async Task PersistStateAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
List<DeviceProfile> knownDevicesSnapshot;
|
||||
Dictionary<string, string> aliasesSnapshot;
|
||||
lock (_stateLock)
|
||||
{
|
||||
knownDevicesSnapshot = _knownDevices
|
||||
.OrderByDescending(device => device.LastSeenUtc)
|
||||
.ToList();
|
||||
aliasesSnapshot = new Dictionary<string, string>(_aliases, StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
var snapshot = new SettingsSnapshot(
|
||||
knownDevicesSnapshot,
|
||||
aliasesSnapshot,
|
||||
State.SelectedColor,
|
||||
State.Brightness,
|
||||
State.EffectSpeed,
|
||||
State.CurrentMode,
|
||||
State.GrayscaleLevel,
|
||||
State.TemperatureLevel,
|
||||
State.SelectedEffectId,
|
||||
State.IsPoweredOn);
|
||||
|
||||
await _settingsStore.SaveAsync(snapshot, cancellationToken);
|
||||
}
|
||||
|
||||
private void UpsertKnownDevice(BleScanResult device)
|
||||
{
|
||||
var alias = GetAlias(device.BluetoothAddress);
|
||||
var profile = new DeviceProfile(
|
||||
string.IsNullOrWhiteSpace(alias) ? device.NativeName : alias,
|
||||
device.DeviceId,
|
||||
device.BluetoothAddress,
|
||||
DateTimeOffset.UtcNow);
|
||||
|
||||
lock (_stateLock)
|
||||
{
|
||||
var existingIndex = _knownDevices.FindIndex(item => string.Equals(item.BluetoothAddress, device.BluetoothAddress, StringComparison.OrdinalIgnoreCase));
|
||||
if (existingIndex >= 0)
|
||||
{
|
||||
_knownDevices[existingIndex] = profile;
|
||||
}
|
||||
else
|
||||
{
|
||||
_knownDevices.Add(profile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private string GetAlias(string bluetoothAddress)
|
||||
{
|
||||
lock (_stateLock)
|
||||
{
|
||||
return _aliases.TryGetValue(bluetoothAddress, out var alias)
|
||||
? alias
|
||||
: string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
private IReadOnlyList<DeviceConnectionContext> GetTargetConnections()
|
||||
{
|
||||
lock (_stateLock)
|
||||
{
|
||||
return _connections.Values
|
||||
.Where(context => context.ViewModel.IsControlTarget)
|
||||
.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleConnectionStatusChanged(object? sender, DeviceConnectionStatus status)
|
||||
{
|
||||
DeviceConnectionContext? context;
|
||||
lock (_stateLock)
|
||||
{
|
||||
context = _connections.Values.FirstOrDefault(item => ReferenceEquals(item.Transport, sender));
|
||||
}
|
||||
|
||||
if (context is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
context.ViewModel.ConnectionStatus = status;
|
||||
context.ViewModel.StatusText = status switch
|
||||
{
|
||||
DeviceConnectionStatus.Connected => "Komut almaya hazır.",
|
||||
DeviceConnectionStatus.Disconnected => "Bağlantı koptu, sonraki yazmada tekrar kurulacak.",
|
||||
DeviceConnectionStatus.Connecting => "Bağlantı kuruluyor...",
|
||||
DeviceConnectionStatus.Faulted => "Bağlantı hata verdi.",
|
||||
_ => context.ViewModel.StatusText
|
||||
};
|
||||
|
||||
RecalculateAggregateState();
|
||||
}
|
||||
|
||||
private void HandleConnectedDevicePropertyChanged(object? sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
if (e.PropertyName == nameof(ConnectedDeviceViewModel.IsControlTarget))
|
||||
{
|
||||
RecalculateAggregateState();
|
||||
}
|
||||
}
|
||||
|
||||
private void RecalculateAggregateState()
|
||||
{
|
||||
DeviceConnectionContext[] connections;
|
||||
lock (_stateLock)
|
||||
{
|
||||
connections = _connections.Values.ToArray();
|
||||
}
|
||||
|
||||
State.ConnectedDeviceCount = connections.Length;
|
||||
State.ControlTargetCount = connections.Count(context => context.ViewModel.IsControlTarget);
|
||||
State.ConnectionStatus = connections.Any(context => context.ViewModel.ConnectionStatus == DeviceConnectionStatus.Connected)
|
||||
? DeviceConnectionStatus.Connected
|
||||
: connections.Any(context => context.ViewModel.ConnectionStatus == DeviceConnectionStatus.Connecting)
|
||||
? DeviceConnectionStatus.Connecting
|
||||
: connections.Any(context => context.ViewModel.ConnectionStatus == DeviceConnectionStatus.Faulted)
|
||||
? DeviceConnectionStatus.Faulted
|
||||
: DeviceConnectionStatus.Disconnected;
|
||||
}
|
||||
|
||||
private void EnsureTargetsAvailable()
|
||||
{
|
||||
lock (_stateLock)
|
||||
{
|
||||
if (!_connections.Values.Any(context => context.ViewModel.IsControlTarget))
|
||||
{
|
||||
throw new InvalidOperationException("Komut gönderebilmek için en az bir hedef cihaz seçmelisin.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task WriteWithPacingAsync(IBleTransport transport, ReadOnlyMemory<byte> payload, CancellationToken cancellationToken)
|
||||
{
|
||||
await transport.WriteAsync(payload, cancellationToken);
|
||||
await Task.Delay(90, cancellationToken);
|
||||
}
|
||||
|
||||
private static string BuildStatusMessage(string successMessage, int totalCount, int successCount, int failureCount, string firstFailureMessage)
|
||||
{
|
||||
if (failureCount == 0)
|
||||
{
|
||||
return successMessage;
|
||||
}
|
||||
|
||||
if (successCount == 0)
|
||||
{
|
||||
return string.IsNullOrWhiteSpace(firstFailureMessage)
|
||||
? $"{totalCount} hedefte işlem başarısız oldu."
|
||||
: $"İşlem başarısız oldu: {firstFailureMessage}";
|
||||
}
|
||||
|
||||
return $"{successMessage} {successCount}/{totalCount} hedef başarılı, {failureCount} hata.";
|
||||
}
|
||||
|
||||
private sealed class DeviceConnectionContext
|
||||
{
|
||||
public DeviceConnectionContext(BleScanResult device, ConnectedDeviceViewModel viewModel, IBleTransport transport)
|
||||
{
|
||||
Device = device;
|
||||
ViewModel = viewModel;
|
||||
Transport = transport;
|
||||
}
|
||||
|
||||
public BleScanResult Device { get; }
|
||||
|
||||
public ConnectedDeviceViewModel ViewModel { get; }
|
||||
|
||||
public IBleTransport Transport { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
using RgbLedStripController.Core;
|
||||
using MediaColor = System.Windows.Media.Color;
|
||||
|
||||
namespace RgbLedStripController.Services;
|
||||
|
||||
public sealed class SettingsStore
|
||||
{
|
||||
private readonly string _settingsPath;
|
||||
private readonly SemaphoreSlim _writeLock = new(1, 1);
|
||||
private readonly JsonSerializerOptions _jsonOptions = new()
|
||||
{
|
||||
WriteIndented = true,
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
||||
};
|
||||
|
||||
public SettingsStore()
|
||||
{
|
||||
var settingsDirectory = Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||||
"RgbLedStripController");
|
||||
|
||||
_settingsPath = Path.Combine(settingsDirectory, "settings.json");
|
||||
}
|
||||
|
||||
public async Task<SettingsSnapshot> LoadAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!File.Exists(_settingsPath))
|
||||
{
|
||||
return CreateDefaultSnapshot();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await using var stream = File.OpenRead(_settingsPath);
|
||||
var stored = await JsonSerializer.DeserializeAsync<StoredSettings>(stream, _jsonOptions, cancellationToken);
|
||||
|
||||
return new SettingsSnapshot(
|
||||
stored?.KnownDevices ?? [],
|
||||
new Dictionary<string, string>(stored?.DeviceAliases ?? new Dictionary<string, string>(), StringComparer.OrdinalIgnoreCase),
|
||||
ParseColor(stored?.SelectedColorHex),
|
||||
stored?.Brightness ?? 100,
|
||||
stored?.EffectSpeed ?? 50,
|
||||
stored?.CurrentMode ?? LightingMode.Rgb,
|
||||
stored?.GrayscaleLevel ?? 100,
|
||||
stored?.TemperatureLevel ?? 50,
|
||||
stored?.SelectedEffectId ?? 0,
|
||||
stored?.IsPoweredOn ?? false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return CreateDefaultSnapshot();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task SaveAsync(SettingsSnapshot snapshot, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await _writeLock.WaitAsync(cancellationToken);
|
||||
|
||||
try
|
||||
{
|
||||
var directory = Path.GetDirectoryName(_settingsPath);
|
||||
if (!string.IsNullOrWhiteSpace(directory))
|
||||
{
|
||||
Directory.CreateDirectory(directory);
|
||||
}
|
||||
|
||||
await using var stream = File.Create(_settingsPath);
|
||||
var stored = new StoredSettings
|
||||
{
|
||||
KnownDevices = snapshot.KnownDevices.ToList(),
|
||||
DeviceAliases = new Dictionary<string, string>(snapshot.DeviceAliases, StringComparer.OrdinalIgnoreCase),
|
||||
SelectedColorHex = $"#{snapshot.SelectedColor.R:X2}{snapshot.SelectedColor.G:X2}{snapshot.SelectedColor.B:X2}",
|
||||
Brightness = snapshot.Brightness,
|
||||
EffectSpeed = snapshot.EffectSpeed,
|
||||
CurrentMode = snapshot.CurrentMode,
|
||||
GrayscaleLevel = snapshot.GrayscaleLevel,
|
||||
TemperatureLevel = snapshot.TemperatureLevel,
|
||||
SelectedEffectId = snapshot.SelectedEffectId,
|
||||
IsPoweredOn = snapshot.IsPoweredOn
|
||||
};
|
||||
|
||||
await JsonSerializer.SerializeAsync(stream, stored, _jsonOptions, cancellationToken);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_writeLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
private static SettingsSnapshot CreateDefaultSnapshot()
|
||||
=> new(
|
||||
[],
|
||||
new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase),
|
||||
MediaColor.FromRgb(255, 106, 78),
|
||||
100,
|
||||
50,
|
||||
LightingMode.Rgb,
|
||||
100,
|
||||
50,
|
||||
0,
|
||||
false);
|
||||
|
||||
private static MediaColor ParseColor(string? value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value) || value.Length != 7 || value[0] != '#')
|
||||
{
|
||||
return MediaColor.FromRgb(255, 106, 78);
|
||||
}
|
||||
|
||||
return MediaColor.FromRgb(
|
||||
Convert.ToByte(value.Substring(1, 2), 16),
|
||||
Convert.ToByte(value.Substring(3, 2), 16),
|
||||
Convert.ToByte(value.Substring(5, 2), 16));
|
||||
}
|
||||
|
||||
private sealed class StoredSettings
|
||||
{
|
||||
public List<DeviceProfile> KnownDevices { get; set; } = [];
|
||||
|
||||
public Dictionary<string, string> DeviceAliases { get; set; } = new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
public string? SelectedColorHex { get; set; }
|
||||
|
||||
public byte Brightness { get; set; }
|
||||
|
||||
public byte EffectSpeed { get; set; }
|
||||
|
||||
public LightingMode CurrentMode { get; set; }
|
||||
|
||||
public byte GrayscaleLevel { get; set; }
|
||||
|
||||
public byte TemperatureLevel { get; set; }
|
||||
|
||||
public byte SelectedEffectId { get; set; }
|
||||
|
||||
public bool IsPoweredOn { get; set; }
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user