3 Commits
11 changed files with 191 additions and 38 deletions
+3
View File
@@ -3,7 +3,10 @@
<component name="AvaloniaProject">
<option name="projectPerEditor">
<map>
<entry key="NetworkDiagram/EditDeviceWindow.axaml" value="NetworkDiagram/NetworkDiagram.csproj" />
<entry key="NetworkDiagram/ExportOptionsWindow.axaml" value="NetworkDiagram/NetworkDiagram.csproj" />
<entry key="NetworkDiagram/Localization/Strings.cs.axaml" value="NetworkDiagram/NetworkDiagram.csproj" />
<entry key="NetworkDiagram/Localization/Strings.en.axaml" value="NetworkDiagram/NetworkDiagram.csproj" />
<entry key="NetworkDiagram/MainWindow.axaml" value="NetworkDiagram/NetworkDiagram.csproj" />
<entry key="NetworkDiagramAvalonia/App.axaml" value="NetworkDiagramAvalonia/NetworkDiagramAvalonia.csproj" />
<entry key="NetworkDiagramAvalonia/Views/MainWindow.axaml" value="NetworkDiagramAvalonia/NetworkDiagramAvalonia.csproj" />
+2
View File
@@ -4,6 +4,8 @@
<option name="locations">
<list>
<option value="$PROJECT_DIR$/NetworkDiagram/bin/Release/net10.0-windows/win-x64/publish" />
<option value="$PROJECT_DIR$/NetworkDiagram/bin/Release/net10.0/linux-x64/publish" />
<option value="$PROJECT_DIR$/NetworkDiagram/bin/Release/net10.0/linux-arm64/publish" />
</list>
</option>
</component>
Binary file not shown.
+10 -6
View File
@@ -5,16 +5,20 @@
WindowStartupLocation="CenterOwner" CanResize="False"
Background="{DynamicResource BgWhite}"
Icon="/Assets/router.jpg">
<Grid Margin="25" RowDefinitions="Auto,Auto,Auto,*,Auto">
<Grid Margin="25" RowDefinitions="Auto,Auto,Auto,Auto,Auto,*,Auto">
<TextBlock Text="{DynamicResource DeviceNameLabel}" Margin="0,0,0,8" FontWeight="SemiBold" Foreground="{DynamicResource PrimaryBlue}"/>
<TextBox x:Name="NameBox" Grid.Row="1" Margin="0,0,0,20"/>
<TextBox x:Name="NameBox" Grid.Row="1" Margin="0,0,0,15"/>
<TextBlock Grid.Row="2" Text="{DynamicResource IpLabel}" Margin="0,0,0,8" FontWeight="SemiBold" Foreground="{DynamicResource PrimaryBlue}"/>
<TextBox x:Name="IpBox" Grid.Row="3" Margin="0,0,0,25"
AcceptsReturn="True" VerticalContentAlignment="Top" MinHeight="80"/>
<TextBlock Grid.Row="2" Text="{DynamicResource DescLabel}" Margin="0,0,0,8" FontWeight="SemiBold" Foreground="{DynamicResource PrimaryBlue}"/>
<TextBox x:Name="DescBox" Grid.Row="3" Margin="0,0,0,15"
AcceptsReturn="True" VerticalContentAlignment="Top" MinHeight="60"/>
<StackPanel Grid.Row="4" Orientation="Horizontal" HorizontalAlignment="Right">
<TextBlock Grid.Row="4" Text="{DynamicResource IpLabel}" Margin="0,0,0,8" FontWeight="SemiBold" Foreground="{DynamicResource PrimaryBlue}"/>
<TextBox x:Name="IpBox" Grid.Row="5" Margin="0,0,0,25"
AcceptsReturn="True" VerticalContentAlignment="Top" MinHeight="60"/>
<StackPanel Grid.Row="6" Orientation="Horizontal" HorizontalAlignment="Right">
<Button Content="{DynamicResource CancelBtn}" Classes="SecondaryButtonStyle" Click="Cancel_Click" Width="90" Margin="0,0,12,0"/>
<Button Content="{DynamicResource SaveDialogBtn}" Classes="ModernButtonStyle" Click="Save_Click" Width="90"/>
</StackPanel>
+2
View File
@@ -19,12 +19,14 @@ namespace NetworkDiagram
InitializeComponent();
_device = device;
NameBox.Text = device.Name;
DescBox.Text = device.Description;
IpBox.Text = string.Join(Environment.NewLine, device.IpAddresses);
}
private void Save_Click(object? sender, RoutedEventArgs e)
{
_device.Name = NameBox.Text ?? string.Empty;
_device.Description = DescBox.Text ?? string.Empty;
_device.IpAddresses = (IpBox.Text ?? string.Empty)
.Split(new[] { Environment.NewLine, "\n", "\r" }, StringSplitOptions.RemoveEmptyEntries)
.ToList();
+5 -6
View File
@@ -4,11 +4,10 @@
Title="{DynamicResource ExportOptionsTitle}" Width="400" Height="180"
WindowStartupLocation="CenterOwner" CanResize="False"
Background="{DynamicResource BgWhite}">
<StackPanel Margin="20" Spacing="20">
<TextBlock Text="{DynamicResource ExportWhiteBgMsg}" TextWrapping="Wrap" HorizontalAlignment="Center" TextAlignment="Center"/>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Spacing="20">
<Button Content="Yes (White)" Click="White_Click" Width="100" Classes="ModernButtonStyle"/>
<Button Content="No (Transparent)" Click="Transparent_Click" Width="140" Classes="SecondaryButtonStyle"/>
</StackPanel>
<StackPanel Margin="20" Spacing="10">
<TextBlock Text="{DynamicResource ExportWhiteBgMsg}" TextWrapping="Wrap" HorizontalAlignment="Center" TextAlignment="Center" Margin="0,0,0,10"/>
<Button Content="{DynamicResource ExportWhiteBtn}" Click="White_Click" HorizontalAlignment="Stretch" Classes="ModernButtonStyle"/>
<Button Content="{DynamicResource ExportTransparentBtn}" Click="Transparent_Click" HorizontalAlignment="Stretch" Classes="SecondaryButtonStyle"/>
<Button Content="{DynamicResource ExportBWBtn}" Click="BlackAndWhite_Click" HorizontalAlignment="Stretch" Classes="SecondaryButtonStyle"/>
</StackPanel>
</Window>
+15 -2
View File
@@ -3,6 +3,14 @@ using Avalonia.Interactivity;
namespace NetworkDiagram
{
public enum ExportOption
{
Cancel,
Transparent,
White,
BlackAndWhite
}
public partial class ExportOptionsWindow : Window
{
public ExportOptionsWindow()
@@ -12,12 +20,17 @@ namespace NetworkDiagram
private void White_Click(object? sender, RoutedEventArgs e)
{
Close(true);
Close(ExportOption.White);
}
private void Transparent_Click(object? sender, RoutedEventArgs e)
{
Close(false);
Close(ExportOption.Transparent);
}
private void BlackAndWhite_Click(object? sender, RoutedEventArgs e)
{
Close(ExportOption.BlackAndWhite);
}
}
}
+7 -3
View File
@@ -11,14 +11,18 @@
<x:String x:Key="DevicesHeader">Zařízení</x:String>
<!-- Edit Dialog -->
<x:String x:Key="EditTitle">Upravit zařízení</x:String>
<x:String x:Key="EditTitle">Upravit Zařízení</x:String>
<x:String x:Key="DeviceNameLabel">Název zařízení:</x:String>
<x:String x:Key="IpLabel">IP adresy (jedna na řádek):</x:String>
<x:String x:Key="DescLabel">Popis:</x:String>
<x:String x:Key="IpLabel">IP Adresy (jedna na řádek):</x:String>
<x:String x:Key="CancelBtn">Zrušit</x:String>
<x:String x:Key="SaveDialogBtn">Uložit</x:String>
<!-- Messages -->
<x:String x:Key="ExportWhiteBgMsg">Chcete bílé pozadí? (Ne = průhlednost)</x:String>
<x:String x:Key="ExportWhiteBgMsg">Vyberte styl pozadí a barev:</x:String>
<x:String x:Key="ExportWhiteBtn">Bílé pozadí</x:String>
<x:String x:Key="ExportTransparentBtn">Průhledné pozadí</x:String>
<x:String x:Key="ExportBWBtn">Pro tisk (šedotisk)</x:String>
<x:String x:Key="ExportOptionsTitle">Možnosti exportu</x:String>
<x:String x:Key="ExportSuccess">Export byl úspěšný!</x:String>
<x:String x:Key="EmptyDiagramMsg">Diagram je prázdný.</x:String>
+5 -1
View File
@@ -13,12 +13,16 @@
<!-- Edit Dialog -->
<x:String x:Key="EditTitle">Edit Device</x:String>
<x:String x:Key="DeviceNameLabel">Device Name:</x:String>
<x:String x:Key="DescLabel">Description:</x:String>
<x:String x:Key="IpLabel">IP Addresses (one per line):</x:String>
<x:String x:Key="CancelBtn">Cancel</x:String>
<x:String x:Key="SaveDialogBtn">Save</x:String>
<!-- Messages -->
<x:String x:Key="ExportWhiteBgMsg">Do you want a white background? (No will result in transparency)</x:String>
<x:String x:Key="ExportWhiteBgMsg">Select export background and color style:</x:String>
<x:String x:Key="ExportWhiteBtn">White Background</x:String>
<x:String x:Key="ExportTransparentBtn">Transparent Background</x:String>
<x:String x:Key="ExportBWBtn">Printer Friendly (Grayscale)</x:String>
<x:String x:Key="ExportOptionsTitle">Export Options</x:String>
<x:String x:Key="ExportSuccess">Export successful!</x:String>
<x:String x:Key="EmptyDiagramMsg">Diagram is empty.</x:String>
+127 -18
View File
@@ -18,6 +18,7 @@ using Avalonia.Media.Imaging;
using Avalonia.Platform.Storage;
using Avalonia.Threading;
using Avalonia.VisualTree;
using System.Runtime.InteropServices;
using NetworkDiagram.Models;
using Avalonia.Controls.Templates;
@@ -342,6 +343,13 @@ namespace NetworkDiagram
nameText.Bind(TextBlock.TextProperty, new Avalonia.Data.Binding("Name"));
stack.Children.Add(nameText);
var textDark = Application.Current!.FindResource("TextDark");
var descText = new TextBlock { FontSize = 11, FontStyle = FontStyle.Italic, HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center, TextWrapping = TextWrapping.Wrap, TextAlignment = TextAlignment.Center, Opacity = 0.8 };
if (textDark is IBrush td) descText.Foreground = td;
descText.Bind(TextBlock.TextProperty, new Avalonia.Data.Binding("Description"));
descText.Bind(TextBlock.IsVisibleProperty, new Avalonia.Data.Binding("Description") { Converter = Avalonia.Data.Converters.StringConverters.IsNotNullOrEmpty });
stack.Children.Add(descText);
var ips = new ItemsControl();
ips.Bind(ItemsControl.ItemsSourceProperty, new Avalonia.Data.Binding("IpAddresses"));
@@ -732,8 +740,8 @@ namespace NetworkDiagram
if (_currentDiagram.Devices.Count == 0) return;
var prompt = new ExportOptionsWindow();
var result = await prompt.ShowDialog<bool?>(this);
if (result == null) return;
var result = await prompt.ShowDialog<ExportOption>(this);
if (result == ExportOption.Cancel) return;
var topLevel = GetTopLevel(this);
if (topLevel == null) return;
@@ -759,7 +767,7 @@ namespace NetworkDiagram
var tempCanvas = new Canvas {
Width = width,
Height = height,
Background = result.Value ? Brushes.White : Brushes.Transparent
Background = (result == ExportOption.White || result == ExportOption.BlackAndWhite) ? Brushes.White : Brushes.Transparent
};
// Create a container to host temp canvas for rendering
@@ -768,13 +776,28 @@ namespace NetworkDiagram
var rtb = new RenderTargetBitmap(new PixelSize(width, height), new Vector(96, 96));
// Fetch resources once for efficiency and consistency
var primaryBlue = Application.Current!.FindResource("PrimaryBlue") as IBrush;
var accentBlue = Application.Current!.FindResource("AccentBlue") as IBrush;
var textDark = Application.Current!.FindResource("TextDark") as IBrush ?? Brushes.Black;
var gridBlue = Application.Current!.FindResource("GridBlue") as IBrush;
bool isGrayscale = result == ExportOption.BlackAndWhite;
if (isGrayscale)
{
primaryBlue = Brushes.Black;
accentBlue = Brushes.Black;
textDark = Brushes.Black;
gridBlue = Brushes.Black;
}
// 1. Draw connections
foreach (var conn in _currentDiagram.Connections)
{
if (_connectionLines.TryGetValue(conn, out var line))
{
var tempLine = new Line {
Stroke = line.Stroke,
Stroke = isGrayscale ? Brushes.Black : line.Stroke,
StrokeThickness = line.StrokeThickness,
StrokeDashArray = line.StrokeDashArray,
StartPoint = new Point(line.StartPoint.X - minX, line.StartPoint.Y - minY),
@@ -794,33 +817,62 @@ namespace NetworkDiagram
CornerRadius = new CornerRadius(8),
Padding = new Thickness(12),
Background = Brushes.White,
BorderBrush = (IBrush)Application.Current!.FindResource("GridBlue")!,
BorderThickness = new Thickness(1),
Width = original.Bounds.Width > 0 ? original.Bounds.Width : 120,
Height = original.Bounds.Height > 0 ? original.Bounds.Height : 100,
DataContext = device
BorderBrush = gridBlue ?? Brushes.Blue,
BorderThickness = new Thickness(isGrayscale ? 2 : 1),
MinWidth = 100,
MinHeight = 80,
MaxWidth = 180
};
var stack = new StackPanel { Spacing = 4 };
var img = new Image { Width = 64, Height = 64 };
img.Bind(Image.SourceProperty, new Avalonia.Data.Binding("IconPath") { Converter = ImageConverter.Instance });
var img = new Image { Width = 64, Height = 64, HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center };
if (ImageConverter.Instance.Convert(device.IconPath, typeof(IImage), null, System.Globalization.CultureInfo.CurrentCulture) is IImage iimg)
{
img.Source = iimg;
}
var txt = new TextBlock {
Text = device.Name,
FontWeight = FontWeight.Bold,
FontSize = isGrayscale ? 14 : 12,
HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center,
Foreground = (IBrush)Application.Current!.FindResource("PrimaryBlue")!
TextWrapping = TextWrapping.Wrap,
TextAlignment = TextAlignment.Center,
Foreground = primaryBlue
};
stack.Children.Add(img);
stack.Children.Add(txt);
var ips = new ItemsControl { ItemsSource = device.IpAddresses };
ips.ItemTemplate = new FuncDataTemplate<string>((val, _) => new TextBlock {
Text = val, FontSize = 10, HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center,
Foreground = (IBrush)Application.Current!.FindResource("AccentBlue")!
});
stack.Children.Add(ips);
if (!string.IsNullOrWhiteSpace(device.Description))
{
var desc = new TextBlock {
Text = device.Description,
FontSize = isGrayscale ? 18 : 11,
FontWeight = isGrayscale ? FontWeight.Bold : FontWeight.Normal,
FontStyle = FontStyle.Italic,
HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center,
TextWrapping = TextWrapping.Wrap,
TextAlignment = TextAlignment.Center,
Foreground = textDark,
Opacity = isGrayscale ? 1.0 : 0.8
};
stack.Children.Add(desc);
}
foreach (var ip in device.IpAddresses)
{
var tb = new TextBlock {
Text = ip,
FontSize = isGrayscale ? 14 : 10,
FontWeight = isGrayscale ? FontWeight.Bold : FontWeight.Normal,
HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center,
TextWrapping = TextWrapping.Wrap,
Foreground = accentBlue
};
stack.Children.Add(tb);
}
clone.Child = stack;
Canvas.SetLeft(clone, device.X - minX);
@@ -836,10 +888,67 @@ namespace NetworkDiagram
await Task.Delay(100);
rtb.Render(container);
if (result == ExportOption.BlackAndWhite)
{
using var ms = new MemoryStream();
rtb.Save(ms);
ms.Position = 0;
var wb = WriteableBitmap.Decode(ms);
ApplyGrayscale(wb);
using var stream = await file.OpenWriteAsync();
wb.Save(stream);
}
else
{
using var stream = await file.OpenWriteAsync();
rtb.Save(stream);
}
}
}
private void ApplyGrayscale(WriteableBitmap bitmap)
{
using (var lockedBitmap = bitmap.Lock())
{
int totalBytes = lockedBitmap.RowBytes * lockedBitmap.Size.Height;
byte[] buffer = new byte[totalBytes];
Marshal.Copy(lockedBitmap.Address, buffer, 0, totalBytes);
for (int y = 0; y < lockedBitmap.Size.Height; y++)
{
int rowOffset = y * lockedBitmap.RowBytes;
for (int x = 0; x < lockedBitmap.Size.Width; x++)
{
int i = rowOffset + x * 4;
if (i + 3 >= totalBytes) continue;
byte b = buffer[i];
byte g = buffer[i + 1];
byte r = buffer[i + 2];
byte a = buffer[i + 3];
if (a < 128)
{
// Transparent -> White
buffer[i] = 255;
buffer[i + 1] = 255;
buffer[i + 2] = 255;
}
else
{
// Standard luminance for grayscale
byte val = (byte)(0.299 * r + 0.587 * g + 0.114 * b);
buffer[i] = val;
buffer[i + 1] = val;
buffer[i + 2] = val;
}
buffer[i + 3] = 255; // Always opaque for printing
}
}
Marshal.Copy(buffer, 0, lockedBitmap.Address, totalBytes);
}
}
#endregion
}
}
+13
View File
@@ -38,6 +38,19 @@ namespace NetworkDiagram.Models
}
}
private string _description = string.Empty;
[JsonPropertyName("Description")]
public string Description
{
get => _description;
set
{
_description = value;
OnPropertyChanged();
}
}
[JsonPropertyName("TemplateName")]
public string TemplateName { get; set; } = string.Empty;