diff --git a/.gitignore b/.gitignore
index 33e8a66..20af7cd 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,3 +5,7 @@
/PCPalService/bin
/PCPalService/obj
PCPalService/service_log.txt
+/PCPal/.vs
+/PCPal/Configurator/obj
+/PCPal/Core/bin
+/PCPal/Core/obj
diff --git a/PCPal/Configurator/App.xaml b/PCPal/Configurator/App.xaml
new file mode 100644
index 0000000..c7dcc46
--- /dev/null
+++ b/PCPal/Configurator/App.xaml
@@ -0,0 +1,69 @@
+
+
+
+
+
+
+
+
+
+
+
+ #1E88E5
+ #1565C0
+ #E3F2FD
+ #CFD8DC
+ #03A9F4
+ #F4F6F8
+ #FFFFFF
+ #333333
+ #555555
+ #E1E4E8
+ #4CAF50
+ #F44336
+ #FF9800
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/PCPal/Configurator/App.xaml.cs b/PCPal/Configurator/App.xaml.cs
new file mode 100644
index 0000000..c91c800
--- /dev/null
+++ b/PCPal/Configurator/App.xaml.cs
@@ -0,0 +1,22 @@
+namespace PCPal.Configurator;
+
+public partial class App : Application
+{
+ public App()
+ {
+ InitializeComponent();
+ MainPage = new AppShell();
+ }
+
+ protected override Window CreateWindow(IActivationState activationState)
+ {
+ Window window = base.CreateWindow(activationState);
+
+ // Configure window properties
+ window.Title = "PCPal Configurator";
+ window.MinimumWidth = 1000;
+ window.MinimumHeight = 700;
+
+ return window;
+ }
+}
\ No newline at end of file
diff --git a/PCPal/Configurator/AppShell.xaml b/PCPal/Configurator/AppShell.xaml
new file mode 100644
index 0000000..ebb1483
--- /dev/null
+++ b/PCPal/Configurator/AppShell.xaml
@@ -0,0 +1,72 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 1602 LCD Display
+ 4.6" TFT Display
+ OLED Display
+ Settings
+ Help
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/PCPal/Configurator/AppShell.xaml.cs b/PCPal/Configurator/AppShell.xaml.cs
new file mode 100644
index 0000000..a39d112
--- /dev/null
+++ b/PCPal/Configurator/AppShell.xaml.cs
@@ -0,0 +1,130 @@
+using PCPal.Core.Services;
+using PCPal.Configurator.ViewModels;
+using PCPal.Configurator.Views;
+using PCPal.Configurator.Views.LCD;
+using PCPal.Configurator.Views.OLED;
+using PCPal.Configurator.Views.TFT;
+using System.ComponentModel;
+//using UIKit;
+
+namespace PCPal.Configurator;
+
+public partial class AppShell : Shell, INotifyPropertyChanged
+{
+ private bool _isConnected;
+ private string _connectionStatus;
+ private DateTime _lastUpdateTime;
+
+ private readonly IServiceProvider _serviceProvider;
+
+ public bool IsConnected
+ {
+ get => _isConnected;
+ set
+ {
+ if (_isConnected != value)
+ {
+ _isConnected = value;
+ OnPropertyChanged();
+ }
+ }
+ }
+
+ public string ConnectionStatus
+ {
+ get => _connectionStatus;
+ set
+ {
+ if (_connectionStatus != value)
+ {
+ _connectionStatus = value;
+ OnPropertyChanged();
+ }
+ }
+ }
+
+ public DateTime LastUpdateTime
+ {
+ get => _lastUpdateTime;
+ set
+ {
+ if (_lastUpdateTime != value)
+ {
+ _lastUpdateTime = value;
+ OnPropertyChanged();
+ }
+ }
+ }
+
+ public AppShell()
+ {
+ InitializeComponent();
+
+ _serviceProvider = IPlatformApplication.Current.Services;
+
+ // Set initial connection status
+ IsConnected = false;
+ ConnectionStatus = "Not connected";
+ LastUpdateTime = DateTime.Now;
+
+ // Start with LCD view
+ NavMenu.SelectedItem = "1602 LCD Display";
+
+ // Start connection monitoring in the background
+ StartConnectivityMonitoring();
+ }
+
+ private void OnNavMenuSelectionChanged(object sender, SelectionChangedEventArgs e)
+ {
+ if (e.CurrentSelection.FirstOrDefault() is string selection)
+ {
+ ContentView view = selection switch
+ {
+ "1602 LCD Display" => _serviceProvider.GetService(),
+ "4.6 TFT Display" => _serviceProvider.GetService(),
+ "OLED Display" => _serviceProvider.GetService(),
+ "Settings" => _serviceProvider.GetService(),
+ "Help" => _serviceProvider.GetService(),
+ _ => null
+ };
+
+ if (view != null)
+ {
+ ContentContainer.Content = view;
+ }
+ }
+ }
+
+ private async void StartConnectivityMonitoring()
+ {
+ var serialPortService = _serviceProvider.GetService();
+ if (serialPortService != null)
+ {
+ // Subscribe to connection status changes
+ serialPortService.ConnectionStatusChanged += (sender, isConnected) =>
+ {
+ MainThread.BeginInvokeOnMainThread(() =>
+ {
+ IsConnected = isConnected;
+ ConnectionStatus = isConnected ? "Connected to " + serialPortService.CurrentPort : "Not connected";
+ LastUpdateTime = DateTime.Now;
+ });
+ };
+
+ // Start periodic connection check
+ while (true)
+ {
+ await Task.Delay(5000);
+ try
+ {
+ await serialPortService.CheckConnectionAsync();
+ }
+ catch (Exception ex)
+ {
+ // Log error but don't crash the app
+ System.Diagnostics.Debug.WriteLine($"Connection check error: {ex.Message}");
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/PCPal/Configurator/Configurator.csproj b/PCPal/Configurator/Configurator.csproj
new file mode 100644
index 0000000..edfffad
--- /dev/null
+++ b/PCPal/Configurator/Configurator.csproj
@@ -0,0 +1,99 @@
+
+
+
+ net8.0-android;net8.0-ios;net8.0-maccatalyst
+ $(TargetFrameworks);net8.0-windows10.0.19041.0
+
+
+
+
+
+
+ Exe
+ Configurator
+ true
+ true
+ enable
+ enable
+
+
+ Configurator
+
+
+ com.companyname.configurator
+
+
+ 1.0
+ 1
+
+ 11.0
+ 13.1
+ 21.0
+ 10.0.17763.0
+ 10.0.17763.0
+ 6.5
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ MSBuild:Compile
+
+
+ MSBuild:Compile
+
+
+ MSBuild:Compile
+
+
+ MSBuild:Compile
+
+
+ MSBuild:Compile
+
+
+ MSBuild:Compile
+
+
+ MSBuild:Compile
+
+
+ MSBuild:Compile
+
+
+ MSBuild:Compile
+
+
+
+
+
+
+
+
diff --git a/PCPal/Configurator/Configurator.csproj.user b/PCPal/Configurator/Configurator.csproj.user
new file mode 100644
index 0000000..891593c
--- /dev/null
+++ b/PCPal/Configurator/Configurator.csproj.user
@@ -0,0 +1,31 @@
+
+
+
+ False
+ net8.0-windows10.0.19041.0
+ Windows Machine
+
+
+
+ Designer
+
+
+ Designer
+
+
+ Designer
+
+
+ Designer
+
+
+ Designer
+
+
+ Designer
+
+
+ Designer
+
+
+
\ No newline at end of file
diff --git a/PCPal/Configurator/Controls/OledPreviewCanvas.cs b/PCPal/Configurator/Controls/OledPreviewCanvas.cs
new file mode 100644
index 0000000..c86455e
--- /dev/null
+++ b/PCPal/Configurator/Controls/OledPreviewCanvas.cs
@@ -0,0 +1,606 @@
+//using Android.Sax;
+using Microsoft.Maui.Controls.Shapes;
+using PCPal.Configurator.ViewModels;
+using PCPal.Core.Models;
+using System.Collections.ObjectModel;
+using System.Collections.Specialized;
+
+namespace PCPal.Configurator.Controls;
+
+public class OledPreviewCanvas : GraphicsView
+{
+ // Bindable properties for the control
+ public static readonly BindableProperty ElementsProperty = BindableProperty.Create(
+ nameof(Elements),
+ typeof(IList),
+ typeof(OledPreviewCanvas),
+ null,
+ propertyChanged: OnElementsChanged);
+
+ public static readonly BindableProperty SelectedElementProperty = BindableProperty.Create(
+ nameof(SelectedElement),
+ typeof(OledElement),
+ typeof(OledPreviewCanvas),
+ null,
+ BindingMode.TwoWay,
+ propertyChanged: OnSelectedElementChanged);
+
+ public static readonly BindableProperty IsEditableProperty = BindableProperty.Create(
+ nameof(IsEditable),
+ typeof(bool),
+ typeof(OledPreviewCanvas),
+ false);
+
+ public static readonly BindableProperty ScaleProperty = BindableProperty.Create(
+ nameof(Scale),
+ typeof(float),
+ typeof(OledPreviewCanvas),
+ 1.0f,
+ propertyChanged: OnScaleChanged);
+
+ public static readonly BindableProperty WidthProperty = BindableProperty.Create(
+ nameof(Width),
+ typeof(int),
+ typeof(OledPreviewCanvas),
+ 256);
+
+ public static readonly BindableProperty HeightProperty = BindableProperty.Create(
+ nameof(Height),
+ typeof(int),
+ typeof(OledPreviewCanvas),
+ 64);
+
+ // Property accessors
+ public IList Elements
+ {
+ get => (IList)GetValue(ElementsProperty);
+ set => SetValue(ElementsProperty, value);
+ }
+
+ public OledElement SelectedElement
+ {
+ get => (OledElement)GetValue(SelectedElementProperty);
+ set => SetValue(SelectedElementProperty, value);
+ }
+
+ public bool IsEditable
+ {
+ get => (bool)GetValue(IsEditableProperty);
+ set => SetValue(IsEditableProperty, value);
+ }
+
+ public float Scale
+ {
+ get => (float)GetValue(ScaleProperty);
+ set => SetValue(ScaleProperty, value);
+ }
+
+ public new int Width
+ {
+ get => (int)GetValue(WidthProperty);
+ set => SetValue(WidthProperty, value);
+ }
+
+ public new int Height
+ {
+ get => (int)GetValue(HeightProperty);
+ set => SetValue(HeightProperty, value);
+ }
+
+ // Constructor
+ public OledPreviewCanvas()
+ {
+ // Set default drawing
+ Drawable = new OledCanvasDrawable(this);
+
+ // Set up interaction handlers if editable
+ StartInteraction += OnStartInteraction;
+ DragInteraction += OnDragInteraction;
+ EndInteraction += OnEndInteraction;
+
+ // Set up initial size
+ WidthRequest = 256 * Scale;
+ HeightRequest = 64 * Scale;
+ }
+
+ // Element collection change handler
+ private static void OnElementsChanged(BindableObject bindable, object oldValue, object newValue)
+ {
+ var canvas = (OledPreviewCanvas)bindable;
+
+ // If old value is INotifyCollectionChanged, unsubscribe
+ if (oldValue is INotifyCollectionChanged oldCollection)
+ {
+ oldCollection.CollectionChanged -= canvas.OnCollectionChanged;
+ }
+
+ // If new value is INotifyCollectionChanged, subscribe
+ if (newValue is INotifyCollectionChanged newCollection)
+ {
+ newCollection.CollectionChanged += canvas.OnCollectionChanged;
+ }
+
+ // Invalidate the canvas to redraw
+ canvas.Invalidate();
+ }
+
+ // Selected element change handler
+ private static void OnSelectedElementChanged(BindableObject bindable, object oldValue, object newValue)
+ {
+ var canvas = (OledPreviewCanvas)bindable;
+ canvas.Invalidate();
+ }
+
+ // Scale change handler
+ private static void OnScaleChanged(BindableObject bindable, object oldValue, object newValue)
+ {
+ var canvas = (OledPreviewCanvas)bindable;
+ float scale = (float)newValue;
+
+ // Update the size of the canvas based on the scale
+ canvas.WidthRequest = canvas.Width * scale;
+ canvas.HeightRequest = canvas.Height * scale;
+
+ canvas.Invalidate();
+ }
+
+ // Collection changed event handler
+ private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
+ {
+ Invalidate();
+ }
+
+ // Interaction handlers for element selection and manipulation
+ private OledElement draggedElement;
+ private Point dragStartPoint;
+
+ private void OnStartInteraction(object sender, TouchEventArgs e)
+ {
+ if (!IsEditable) return;
+
+ var point = e.Touches[0];
+ dragStartPoint = point;
+
+ // Check if an element was clicked
+ if (Elements != null)
+ {
+ // Need to adjust for scale
+ float x = (float)point.X / Scale;
+ float y = (float)point.Y / Scale;
+
+ foreach (var element in Elements)
+ {
+ if (element is TextElement textElement)
+ {
+ // Simple bounding box check
+ if (x >= textElement.X && x <= textElement.X + 100 &&
+ y >= textElement.Y - 20 && y <= textElement.Y)
+ {
+ // Find the OledElement that corresponds to this PreviewElement
+ var oledElement = FindOledElementForPreviewElement(textElement);
+ if (oledElement != null)
+ {
+ draggedElement = oledElement;
+ SelectedElement = oledElement;
+ return;
+ }
+ }
+ }
+ else if (element is BarElement barElement)
+ {
+ if (x >= barElement.X && x <= barElement.X + barElement.Width &&
+ y >= barElement.Y && y <= barElement.Y + barElement.Height)
+ {
+ var oledElement = FindOledElementForPreviewElement(barElement);
+ if (oledElement != null)
+ {
+ draggedElement = oledElement;
+ SelectedElement = oledElement;
+ return;
+ }
+ }
+ }
+ else if (element is RectElement rectElement)
+ {
+ if (x >= rectElement.X && x <= rectElement.X + rectElement.Width &&
+ y >= rectElement.Y && y <= rectElement.Y + rectElement.Height)
+ {
+ var oledElement = FindOledElementForPreviewElement(rectElement);
+ if (oledElement != null)
+ {
+ draggedElement = oledElement;
+ SelectedElement = oledElement;
+ return;
+ }
+ }
+ }
+ else if (element is LineElement lineElement)
+ {
+ // Simplified line hit detection
+ float lineLength = (float)Math.Sqrt(
+ Math.Pow(lineElement.X2 - lineElement.X1, 2) +
+ Math.Pow(lineElement.Y2 - lineElement.Y1, 2));
+
+ // Check if point is close to the line
+ float distance = DistancePointToLine(
+ x, y,
+ lineElement.X1, lineElement.Y1,
+ lineElement.X2, lineElement.Y2);
+
+ if (distance < 10) // 10 pixel tolerance
+ {
+ var oledElement = FindOledElementForPreviewElement(lineElement);
+ if (oledElement != null)
+ {
+ draggedElement = oledElement;
+ SelectedElement = oledElement;
+ return;
+ }
+ }
+ }
+ else if (element is IconElement iconElement)
+ {
+ if (x >= iconElement.X && x <= iconElement.X + 24 &&
+ y >= iconElement.Y && y <= iconElement.Y + 24)
+ {
+ var oledElement = FindOledElementForPreviewElement(iconElement);
+ if (oledElement != null)
+ {
+ draggedElement = oledElement;
+ SelectedElement = oledElement;
+ return;
+ }
+ }
+ }
+ }
+
+ // No element was clicked, deselect
+ SelectedElement = null;
+ }
+ }
+
+ private void OnDragInteraction(object sender, TouchEventArgs e)
+ {
+ if (!IsEditable || draggedElement == null) return;
+
+ var point = e.Touches[0];
+
+ // Calculate the delta from the start point
+ float deltaX = (float)(point.X - dragStartPoint.X) / Scale;
+ float deltaY = (float)(point.Y - dragStartPoint.Y) / Scale;
+
+ // Update the position of the dragged element
+ draggedElement.X += (int)deltaX;
+ draggedElement.Y += (int)deltaY;
+
+ // Keep element within bounds
+ draggedElement.X = Math.Max(0, Math.Min(Width - 10, draggedElement.X));
+ draggedElement.Y = Math.Max(0, Math.Min(Height - 10, draggedElement.Y));
+
+ // Update the start point for the next move
+ dragStartPoint = point;
+
+ // Notify property changes
+ var viewModel = BindingContext as OledConfigViewModel;
+ if (viewModel != null)
+ {
+ // Update the view model properties to reflect the new position
+ viewModel.OnPropertyChanged(nameof(viewModel.SelectedElementX));
+ viewModel.OnPropertyChanged(nameof(viewModel.SelectedElementY));
+
+ // Update the markup
+ viewModel.UpdateMarkupFromElements();
+ }
+
+ // Invalidate the canvas to redraw
+ Invalidate();
+ }
+
+ private void OnEndInteraction(object sender, TouchEventArgs e)
+ {
+ draggedElement = null;
+ }
+
+ // Helper methods
+ private OledElement FindOledElementForPreviewElement(PreviewElement previewElement)
+ {
+ var viewModel = BindingContext as OledConfigViewModel;
+ if (viewModel == null || viewModel.OledElements == null) return null;
+
+ foreach (var oledElement in viewModel.OledElements)
+ {
+ // Match based on position and type
+ if (previewElement is TextElement textElement && oledElement.Type == "text")
+ {
+ if (oledElement.X == textElement.X && oledElement.Y == textElement.Y)
+ {
+ return oledElement;
+ }
+ }
+ else if (previewElement is BarElement barElement && oledElement.Type == "bar")
+ {
+ if (oledElement.X == barElement.X && oledElement.Y == barElement.Y)
+ {
+ return oledElement;
+ }
+ }
+ else if (previewElement is RectElement rectElement)
+ {
+ if ((oledElement.Type == "rect" || oledElement.Type == "box") &&
+ oledElement.X == rectElement.X && oledElement.Y == rectElement.Y)
+ {
+ return oledElement;
+ }
+ }
+ else if (previewElement is LineElement lineElement && oledElement.Type == "line")
+ {
+ if (oledElement.X == lineElement.X1 && oledElement.Y == lineElement.Y1)
+ {
+ return oledElement;
+ }
+ }
+ else if (previewElement is IconElement iconElement && oledElement.Type == "icon")
+ {
+ if (oledElement.X == iconElement.X && oledElement.Y == iconElement.Y)
+ {
+ return oledElement;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ private float DistancePointToLine(float px, float py, float x1, float y1, float x2, float y2)
+ {
+ float lineLength = (float)Math.Sqrt(Math.Pow(x2 - x1, 2) + Math.Pow(y2 - y1, 2));
+ if (lineLength == 0) return (float)Math.Sqrt(Math.Pow(px - x1, 2) + Math.Pow(py - y1, 2));
+
+ float t = ((px - x1) * (x2 - x1) + (py - y1) * (y2 - y1)) / (lineLength * lineLength);
+ t = Math.Max(0, Math.Min(1, t));
+
+ float projX = x1 + t * (x2 - x1);
+ float projY = y1 + t * (y2 - y1);
+
+ return (float)Math.Sqrt(Math.Pow(px - projX, 2) + Math.Pow(py - projY, 2));
+ }
+}
+
+// The drawable that renders the OLED canvas
+public class OledCanvasDrawable : IDrawable
+{
+ private readonly OledPreviewCanvas _canvas;
+
+ public OledCanvasDrawable(OledPreviewCanvas canvas)
+ {
+ _canvas = canvas;
+ }
+
+ public void Draw(ICanvas canvas, RectF dirtyRect)
+ {
+ float scale = _canvas.Scale;
+
+ // Clear background
+ canvas.FillColor = Colors.Black;
+ canvas.FillRectangle(0, 0, dirtyRect.Width, dirtyRect.Height);
+
+ // Draw grid if requested
+ if (_canvas.IsEditable && _canvas.Parent?.BindingContext is OledConfigViewModel viewModel && viewModel.ShowGridLines)
+ {
+ canvas.StrokeColor = new Color(64, 64, 64, 64); // Semi-transparent gray
+ canvas.StrokeSize = 1;
+
+ // Draw vertical grid lines
+ for (int x = 0; x <= _canvas.Width; x += 10)
+ {
+ canvas.DrawLine(x * scale, 0, x * scale, _canvas.Height * scale);
+ }
+
+ // Draw horizontal grid lines
+ for (int y = 0; y <= _canvas.Height; y += 10)
+ {
+ canvas.DrawLine(0, y * scale, _canvas.Width * scale, y * scale);
+ }
+ }
+
+ // Draw elements
+ if (_canvas.Elements != null)
+ {
+ foreach (var element in _canvas.Elements)
+ {
+ // Check if this element is selected
+ bool isSelected = false;
+ if (_canvas.SelectedElement != null && _canvas.IsEditable)
+ {
+ if (element is TextElement textElement && _canvas.SelectedElement.Type == "text")
+ {
+ isSelected = _canvas.SelectedElement.X == textElement.X && _canvas.SelectedElement.Y == textElement.Y;
+ }
+ else if (element is BarElement barElement && _canvas.SelectedElement.Type == "bar")
+ {
+ isSelected = _canvas.SelectedElement.X == barElement.X && _canvas.SelectedElement.Y == barElement.Y;
+ }
+ else if (element is RectElement rectElement &&
+ (_canvas.SelectedElement.Type == "rect" || _canvas.SelectedElement.Type == "box"))
+ {
+ isSelected = _canvas.SelectedElement.X == rectElement.X && _canvas.SelectedElement.Y == rectElement.Y;
+ }
+ else if (element is LineElement lineElement && _canvas.SelectedElement.Type == "line")
+ {
+ isSelected = _canvas.SelectedElement.X == lineElement.X1 && _canvas.SelectedElement.Y == lineElement.Y1;
+ }
+ else if (element is IconElement iconElement && _canvas.SelectedElement.Type == "icon")
+ {
+ isSelected = _canvas.SelectedElement.X == iconElement.X && _canvas.SelectedElement.Y == iconElement.Y;
+ }
+ }
+
+ // Draw the element with appropriate styling
+ DrawElement(canvas, element, scale, isSelected);
+ }
+ }
+ }
+
+ private void DrawElement(ICanvas canvas, PreviewElement element, float scale, bool isSelected)
+ {
+ // Set selection highlighting if needed
+ if (isSelected)
+ {
+ canvas.StrokeColor = Colors.Cyan;
+ canvas.StrokeSize = 2;
+ }
+ else
+ {
+ canvas.StrokeColor = Colors.White;
+ canvas.StrokeSize = 1;
+ }
+
+ canvas.FillColor = Colors.White;
+
+ if (element is TextElement textElement)
+ {
+ float fontSize;
+ switch (textElement.Size)
+ {
+ case 1: fontSize = 8 * scale; break;
+ case 2: fontSize = 12 * scale; break;
+ case 3: fontSize = 16 * scale; break;
+ default: fontSize = 8 * scale; break;
+ }
+
+ canvas.FontSize = fontSize;
+ canvas.FontColor = Colors.White;
+ canvas.DrawString(
+ textElement.Text,
+ textElement.X * scale,
+ textElement.Y * scale,
+ HorizontalAlignment.Left);
+
+ // Draw selection indicator for text elements
+ if (isSelected)
+ {
+ var metrics = canvas.GetStringSize(textElement.Text, Microsoft.Maui.Graphics.Font.Default, fontSize);
+ canvas.DrawRectangle(
+ textElement.X * scale - 2,
+ textElement.Y * scale - metrics.Height - 2,
+ metrics.Width + 4,
+ metrics.Height + 4);
+ }
+ }
+ else if (element is BarElement barElement)
+ {
+ // Draw outline
+ canvas.DrawRectangle(
+ barElement.X * scale,
+ barElement.Y * scale,
+ barElement.Width * scale,
+ barElement.Height * scale);
+
+ // Draw fill based on value
+ int fillWidth = (int)(barElement.Width * (barElement.Value / 100.0));
+ if (fillWidth > 0)
+ {
+ canvas.FillRectangle(
+ (barElement.X + 1) * scale,
+ (barElement.Y + 1) * scale,
+ (fillWidth - 1) * scale,
+ (barElement.Height - 2) * scale);
+ }
+
+ // Draw selection indicator
+ if (isSelected)
+ {
+ canvas.StrokeColor = Colors.Cyan;
+ canvas.DrawRectangle(
+ (barElement.X - 2) * scale,
+ (barElement.Y - 2) * scale,
+ (barElement.Width + 4) * scale,
+ (barElement.Height + 4) * scale);
+ }
+ }
+ else if (element is RectElement rectElement)
+ {
+ if (rectElement.Filled)
+ {
+ // Filled box
+ canvas.FillRectangle(
+ rectElement.X * scale,
+ rectElement.Y * scale,
+ rectElement.Width * scale,
+ rectElement.Height * scale);
+ }
+ else
+ {
+ // Outline rectangle
+ canvas.DrawRectangle(
+ rectElement.X * scale,
+ rectElement.Y * scale,
+ rectElement.Width * scale,
+ rectElement.Height * scale);
+ }
+
+ // Draw selection indicator
+ if (isSelected)
+ {
+ canvas.StrokeColor = Colors.Cyan;
+ canvas.DrawRectangle(
+ (rectElement.X - 2) * scale,
+ (rectElement.Y - 2) * scale,
+ (rectElement.Width + 4) * scale,
+ (rectElement.Height + 4) * scale);
+ }
+ }
+ else if (element is LineElement lineElement)
+ {
+ canvas.DrawLine(
+ lineElement.X1 * scale,
+ lineElement.Y1 * scale,
+ lineElement.X2 * scale,
+ lineElement.Y2 * scale);
+
+ // Draw selection indicator
+ if (isSelected)
+ {
+ canvas.StrokeColor = Colors.Cyan;
+ canvas.StrokeSize = 3;
+ canvas.DrawLine(
+ lineElement.X1 * scale,
+ lineElement.Y1 * scale,
+ lineElement.X2 * scale,
+ lineElement.Y2 * scale);
+
+ // Draw endpoints
+ canvas.FillCircle(lineElement.X1 * scale, lineElement.Y1 * scale, 4);
+ canvas.FillCircle(lineElement.X2 * scale, lineElement.Y2 * scale, 4);
+ }
+ }
+ else if (element is IconElement iconElement)
+ {
+ // Draw a placeholder for the icon
+ canvas.DrawRectangle(
+ iconElement.X * scale,
+ iconElement.Y * scale,
+ 24 * scale,
+ 24 * scale);
+
+ // Draw icon name as text
+ canvas.FontSize = 8 * scale;
+ canvas.DrawString(
+ iconElement.Name,
+ (iconElement.X + 2) * scale,
+ (iconElement.Y + 12) * scale,
+ HorizontalAlignment.Left);
+
+ // Draw selection indicator
+ if (isSelected)
+ {
+ canvas.StrokeColor = Colors.Cyan;
+ canvas.DrawRectangle(
+ (iconElement.X - 2) * scale,
+ (iconElement.Y - 2) * scale,
+ (24 + 4) * scale,
+ (24 + 4) * scale);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/PCPal/Configurator/MainPage.xaml b/PCPal/Configurator/MainPage.xaml
new file mode 100644
index 0000000..7e93f88
--- /dev/null
+++ b/PCPal/Configurator/MainPage.xaml
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/PCPal/Configurator/MainPage.xaml.cs b/PCPal/Configurator/MainPage.xaml.cs
new file mode 100644
index 0000000..3d63788
--- /dev/null
+++ b/PCPal/Configurator/MainPage.xaml.cs
@@ -0,0 +1,25 @@
+namespace Configurator
+{
+ public partial class MainPage : ContentPage
+ {
+ int count = 0;
+
+ public MainPage()
+ {
+ InitializeComponent();
+ }
+
+ private void OnCounterClicked(object sender, EventArgs e)
+ {
+ count++;
+
+ if (count == 1)
+ CounterBtn.Text = $"Clicked {count} time";
+ else
+ CounterBtn.Text = $"Clicked {count} times";
+
+ SemanticScreenReader.Announce(CounterBtn.Text);
+ }
+ }
+
+}
diff --git a/PCPal/Configurator/MauiProgram.cs b/PCPal/Configurator/MauiProgram.cs
new file mode 100644
index 0000000..2944686
--- /dev/null
+++ b/PCPal/Configurator/MauiProgram.cs
@@ -0,0 +1,61 @@
+using Microsoft.Extensions.Logging;
+using PCPal.Core.Services;
+using PCPal.Configurator.ViewModels;
+using PCPal.Configurator.Views;
+using PCPal.Configurator.Views.LCD;
+using PCPal.Configurator.Views.OLED;
+using PCPal.Configurator.Views.TFT;
+using PCPal.Configurator.Converters;
+
+namespace PCPal.Configurator;
+
+public static class MauiProgram
+{
+ public static MauiApp CreateMauiApp()
+ {
+ var builder = MauiApp.CreateBuilder();
+ builder
+ .UseMauiApp()
+ .ConfigureFonts(fonts =>
+ {
+ fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
+ fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
+ fonts.AddFont("Consolas.ttf", "Consolas");
+ });
+
+ // Register services
+ builder.Services.AddSingleton();
+ builder.Services.AddSingleton();
+ builder.Services.AddSingleton();
+
+ // Register views and view models
+ // LCD
+ builder.Services.AddTransient();
+ builder.Services.AddTransient();
+
+ // OLED
+ builder.Services.AddTransient();
+ builder.Services.AddTransient();
+ builder.Services.AddTransient();
+ builder.Services.AddTransient();
+ builder.Services.AddTransient();
+
+ // TFT
+ builder.Services.AddTransient();
+ builder.Services.AddTransient();
+
+ // Settings
+ builder.Services.AddTransient();
+ builder.Services.AddTransient();
+
+ // Help
+ builder.Services.AddTransient();
+ builder.Services.AddTransient();
+
+#if DEBUG
+ builder.Logging.AddDebug();
+#endif
+
+ return builder.Build();
+ }
+}
\ No newline at end of file
diff --git a/PCPal/Configurator/Platforms/Android/AndroidManifest.xml b/PCPal/Configurator/Platforms/Android/AndroidManifest.xml
new file mode 100644
index 0000000..e9937ad
--- /dev/null
+++ b/PCPal/Configurator/Platforms/Android/AndroidManifest.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/PCPal/Configurator/Platforms/Android/MainActivity.cs b/PCPal/Configurator/Platforms/Android/MainActivity.cs
new file mode 100644
index 0000000..5ff6f79
--- /dev/null
+++ b/PCPal/Configurator/Platforms/Android/MainActivity.cs
@@ -0,0 +1,10 @@
+using Android.App;
+using Android.Content.PM;
+using Android.OS;
+
+namespace Configurator;
+
+[Activity(Theme = "@style/Maui.SplashTheme", MainLauncher = true, LaunchMode = LaunchMode.SingleTop, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation | ConfigChanges.UiMode | ConfigChanges.ScreenLayout | ConfigChanges.SmallestScreenSize | ConfigChanges.Density)]
+public class MainActivity : MauiAppCompatActivity
+{
+}
diff --git a/PCPal/Configurator/Platforms/Android/MainApplication.cs b/PCPal/Configurator/Platforms/Android/MainApplication.cs
new file mode 100644
index 0000000..c919777
--- /dev/null
+++ b/PCPal/Configurator/Platforms/Android/MainApplication.cs
@@ -0,0 +1,16 @@
+using Android.App;
+using Android.Runtime;
+using PCPal.Configurator;
+
+namespace Configurator;
+
+[Application]
+public class MainApplication : MauiApplication
+{
+ public MainApplication(IntPtr handle, JniHandleOwnership ownership)
+ : base(handle, ownership)
+ {
+ }
+
+ protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
+}
diff --git a/PCPal/Configurator/Platforms/Android/Resources/values/colors.xml b/PCPal/Configurator/Platforms/Android/Resources/values/colors.xml
new file mode 100644
index 0000000..c04d749
--- /dev/null
+++ b/PCPal/Configurator/Platforms/Android/Resources/values/colors.xml
@@ -0,0 +1,6 @@
+
+
+ #512BD4
+ #2B0B98
+ #2B0B98
+
\ No newline at end of file
diff --git a/PCPal/Configurator/Platforms/MacCatalyst/AppDelegate.cs b/PCPal/Configurator/Platforms/MacCatalyst/AppDelegate.cs
new file mode 100644
index 0000000..7192755
--- /dev/null
+++ b/PCPal/Configurator/Platforms/MacCatalyst/AppDelegate.cs
@@ -0,0 +1,10 @@
+using Foundation;
+using PCPal.Configurator;
+
+namespace Configurator;
+
+[Register("AppDelegate")]
+public class AppDelegate : MauiUIApplicationDelegate
+{
+ protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
+}
diff --git a/PCPal/Configurator/Platforms/MacCatalyst/Entitlements.plist b/PCPal/Configurator/Platforms/MacCatalyst/Entitlements.plist
new file mode 100644
index 0000000..de4adc9
--- /dev/null
+++ b/PCPal/Configurator/Platforms/MacCatalyst/Entitlements.plist
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+ com.apple.security.app-sandbox
+
+
+ com.apple.security.network.client
+
+
+
+
diff --git a/PCPal/Configurator/Platforms/MacCatalyst/Info.plist b/PCPal/Configurator/Platforms/MacCatalyst/Info.plist
new file mode 100644
index 0000000..7268977
--- /dev/null
+++ b/PCPal/Configurator/Platforms/MacCatalyst/Info.plist
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ UIDeviceFamily
+
+ 2
+
+ UIRequiredDeviceCapabilities
+
+ arm64
+
+ UISupportedInterfaceOrientations
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+ UISupportedInterfaceOrientations~ipad
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationPortraitUpsideDown
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+ XSAppIconAssets
+ Assets.xcassets/appicon.appiconset
+
+
diff --git a/PCPal/Configurator/Platforms/MacCatalyst/Program.cs b/PCPal/Configurator/Platforms/MacCatalyst/Program.cs
new file mode 100644
index 0000000..4beda7c
--- /dev/null
+++ b/PCPal/Configurator/Platforms/MacCatalyst/Program.cs
@@ -0,0 +1,15 @@
+using ObjCRuntime;
+using UIKit;
+
+namespace Configurator;
+
+public class Program
+{
+ // This is the main entry point of the application.
+ static void Main(string[] args)
+ {
+ // if you want to use a different Application Delegate class from "AppDelegate"
+ // you can specify it here.
+ UIApplication.Main(args, null, typeof(AppDelegate));
+ }
+}
diff --git a/PCPal/Configurator/Platforms/Tizen/Main.cs b/PCPal/Configurator/Platforms/Tizen/Main.cs
new file mode 100644
index 0000000..a16acdf
--- /dev/null
+++ b/PCPal/Configurator/Platforms/Tizen/Main.cs
@@ -0,0 +1,16 @@
+using System;
+using Microsoft.Maui;
+using Microsoft.Maui.Hosting;
+
+namespace Configurator;
+
+class Program : MauiApplication
+{
+ protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
+
+ static void Main(string[] args)
+ {
+ var app = new Program();
+ app.Run(args);
+ }
+}
diff --git a/PCPal/Configurator/Platforms/Tizen/tizen-manifest.xml b/PCPal/Configurator/Platforms/Tizen/tizen-manifest.xml
new file mode 100644
index 0000000..1d7df3d
--- /dev/null
+++ b/PCPal/Configurator/Platforms/Tizen/tizen-manifest.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+ maui-appicon-placeholder
+
+
+
+
+ http://tizen.org/privilege/internet
+
+
+
+
\ No newline at end of file
diff --git a/PCPal/Configurator/Platforms/Windows/App.xaml b/PCPal/Configurator/Platforms/Windows/App.xaml
new file mode 100644
index 0000000..a5dc973
--- /dev/null
+++ b/PCPal/Configurator/Platforms/Windows/App.xaml
@@ -0,0 +1,8 @@
+
+
+
diff --git a/PCPal/Configurator/Platforms/Windows/App.xaml.cs b/PCPal/Configurator/Platforms/Windows/App.xaml.cs
new file mode 100644
index 0000000..2057245
--- /dev/null
+++ b/PCPal/Configurator/Platforms/Windows/App.xaml.cs
@@ -0,0 +1,25 @@
+using Microsoft.UI.Xaml;
+using PCPal.Configurator;
+
+// To learn more about WinUI, the WinUI project structure,
+// and more about our project templates, see: http://aka.ms/winui-project-info.
+
+namespace Configurator.WinUI;
+
+///
+/// Provides application-specific behavior to supplement the default Application class.
+///
+public partial class App : MauiWinUIApplication
+{
+ ///
+ /// Initializes the singleton application object. This is the first line of authored code
+ /// executed, and as such is the logical equivalent of main() or WinMain().
+ ///
+ public App()
+ {
+ this.InitializeComponent();
+ }
+
+ protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
+}
+
diff --git a/PCPal/Configurator/Platforms/Windows/Package.appxmanifest b/PCPal/Configurator/Platforms/Windows/Package.appxmanifest
new file mode 100644
index 0000000..25db648
--- /dev/null
+++ b/PCPal/Configurator/Platforms/Windows/Package.appxmanifest
@@ -0,0 +1,46 @@
+
+
+
+
+
+
+
+
+ $placeholder$
+ User Name
+ $placeholder$.png
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/PCPal/Configurator/Platforms/Windows/app.manifest b/PCPal/Configurator/Platforms/Windows/app.manifest
new file mode 100644
index 0000000..21553f6
--- /dev/null
+++ b/PCPal/Configurator/Platforms/Windows/app.manifest
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+ true/PM
+ PerMonitorV2, PerMonitor
+
+
+
diff --git a/PCPal/Configurator/Platforms/iOS/AppDelegate.cs b/PCPal/Configurator/Platforms/iOS/AppDelegate.cs
new file mode 100644
index 0000000..7192755
--- /dev/null
+++ b/PCPal/Configurator/Platforms/iOS/AppDelegate.cs
@@ -0,0 +1,10 @@
+using Foundation;
+using PCPal.Configurator;
+
+namespace Configurator;
+
+[Register("AppDelegate")]
+public class AppDelegate : MauiUIApplicationDelegate
+{
+ protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
+}
diff --git a/PCPal/Configurator/Platforms/iOS/Info.plist b/PCPal/Configurator/Platforms/iOS/Info.plist
new file mode 100644
index 0000000..0004a4f
--- /dev/null
+++ b/PCPal/Configurator/Platforms/iOS/Info.plist
@@ -0,0 +1,32 @@
+
+
+
+
+ LSRequiresIPhoneOS
+
+ UIDeviceFamily
+
+ 1
+ 2
+
+ UIRequiredDeviceCapabilities
+
+ arm64
+
+ UISupportedInterfaceOrientations
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+ UISupportedInterfaceOrientations~ipad
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationPortraitUpsideDown
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+ XSAppIconAssets
+ Assets.xcassets/appicon.appiconset
+
+
diff --git a/PCPal/Configurator/Platforms/iOS/Program.cs b/PCPal/Configurator/Platforms/iOS/Program.cs
new file mode 100644
index 0000000..4beda7c
--- /dev/null
+++ b/PCPal/Configurator/Platforms/iOS/Program.cs
@@ -0,0 +1,15 @@
+using ObjCRuntime;
+using UIKit;
+
+namespace Configurator;
+
+public class Program
+{
+ // This is the main entry point of the application.
+ static void Main(string[] args)
+ {
+ // if you want to use a different Application Delegate class from "AppDelegate"
+ // you can specify it here.
+ UIApplication.Main(args, null, typeof(AppDelegate));
+ }
+}
diff --git a/PCPal/Configurator/Platforms/iOS/Resources/PrivacyInfo.xcprivacy b/PCPal/Configurator/Platforms/iOS/Resources/PrivacyInfo.xcprivacy
new file mode 100644
index 0000000..24ab3b4
--- /dev/null
+++ b/PCPal/Configurator/Platforms/iOS/Resources/PrivacyInfo.xcprivacy
@@ -0,0 +1,51 @@
+
+
+
+
+
+ NSPrivacyAccessedAPITypes
+
+
+ NSPrivacyAccessedAPIType
+ NSPrivacyAccessedAPICategoryFileTimestamp
+ NSPrivacyAccessedAPITypeReasons
+
+ C617.1
+
+
+
+ NSPrivacyAccessedAPIType
+ NSPrivacyAccessedAPICategorySystemBootTime
+ NSPrivacyAccessedAPITypeReasons
+
+ 35F9.1
+
+
+
+ NSPrivacyAccessedAPIType
+ NSPrivacyAccessedAPICategoryDiskSpace
+ NSPrivacyAccessedAPITypeReasons
+
+ E174.1
+
+
+
+
+
+
diff --git a/PCPal/Configurator/Properties/launchSettings.json b/PCPal/Configurator/Properties/launchSettings.json
new file mode 100644
index 0000000..edf8aad
--- /dev/null
+++ b/PCPal/Configurator/Properties/launchSettings.json
@@ -0,0 +1,8 @@
+{
+ "profiles": {
+ "Windows Machine": {
+ "commandName": "MsixPackage",
+ "nativeDebugging": false
+ }
+ }
+}
\ No newline at end of file
diff --git a/PCPal/Configurator/Resources/AppIcon/appicon.svg b/PCPal/Configurator/Resources/AppIcon/appicon.svg
new file mode 100644
index 0000000..9d63b65
--- /dev/null
+++ b/PCPal/Configurator/Resources/AppIcon/appicon.svg
@@ -0,0 +1,4 @@
+
+
\ No newline at end of file
diff --git a/PCPal/Configurator/Resources/AppIcon/appiconfg.svg b/PCPal/Configurator/Resources/AppIcon/appiconfg.svg
new file mode 100644
index 0000000..21dfb25
--- /dev/null
+++ b/PCPal/Configurator/Resources/AppIcon/appiconfg.svg
@@ -0,0 +1,8 @@
+
+
+
\ No newline at end of file
diff --git a/PCPal/Configurator/Resources/Fonts/OpenSans-Regular.ttf b/PCPal/Configurator/Resources/Fonts/OpenSans-Regular.ttf
new file mode 100644
index 0000000..ee3f28f
Binary files /dev/null and b/PCPal/Configurator/Resources/Fonts/OpenSans-Regular.ttf differ
diff --git a/PCPal/Configurator/Resources/Fonts/OpenSans-Semibold.ttf b/PCPal/Configurator/Resources/Fonts/OpenSans-Semibold.ttf
new file mode 100644
index 0000000..bc81019
Binary files /dev/null and b/PCPal/Configurator/Resources/Fonts/OpenSans-Semibold.ttf differ
diff --git a/PCPal/Configurator/Resources/Images/dotnet_bot.png b/PCPal/Configurator/Resources/Images/dotnet_bot.png
new file mode 100644
index 0000000..f93ce02
Binary files /dev/null and b/PCPal/Configurator/Resources/Images/dotnet_bot.png differ
diff --git a/PCPal/Configurator/Resources/Raw/AboutAssets.txt b/PCPal/Configurator/Resources/Raw/AboutAssets.txt
new file mode 100644
index 0000000..89dc758
--- /dev/null
+++ b/PCPal/Configurator/Resources/Raw/AboutAssets.txt
@@ -0,0 +1,15 @@
+Any raw assets you want to be deployed with your application can be placed in
+this directory (and child directories). Deployment of the asset to your application
+is automatically handled by the following `MauiAsset` Build Action within your `.csproj`.
+
+
+
+These files will be deployed with your package and will be accessible using Essentials:
+
+ async Task LoadMauiAsset()
+ {
+ using var stream = await FileSystem.OpenAppPackageFileAsync("AboutAssets.txt");
+ using var reader = new StreamReader(stream);
+
+ var contents = reader.ReadToEnd();
+ }
diff --git a/PCPal/Configurator/Resources/Splash/splash.svg b/PCPal/Configurator/Resources/Splash/splash.svg
new file mode 100644
index 0000000..21dfb25
--- /dev/null
+++ b/PCPal/Configurator/Resources/Splash/splash.svg
@@ -0,0 +1,8 @@
+
+
+
\ No newline at end of file
diff --git a/PCPal/Configurator/Resources/Styles/Colors.xaml b/PCPal/Configurator/Resources/Styles/Colors.xaml
new file mode 100644
index 0000000..30307a5
--- /dev/null
+++ b/PCPal/Configurator/Resources/Styles/Colors.xaml
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+ #512BD4
+ #ac99ea
+ #242424
+ #DFD8F7
+ #9880e5
+ #2B0B98
+
+ White
+ Black
+ #D600AA
+ #190649
+ #1f1f1f
+
+ #E1E1E1
+ #C8C8C8
+ #ACACAC
+ #919191
+ #6E6E6E
+ #404040
+ #212121
+ #141414
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/PCPal/Configurator/Resources/Styles/Converters.xaml b/PCPal/Configurator/Resources/Styles/Converters.xaml
new file mode 100644
index 0000000..4ca021d
--- /dev/null
+++ b/PCPal/Configurator/Resources/Styles/Converters.xaml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/PCPal/Configurator/Resources/Styles/Converters.xaml.cs b/PCPal/Configurator/Resources/Styles/Converters.xaml.cs
new file mode 100644
index 0000000..f39faef
--- /dev/null
+++ b/PCPal/Configurator/Resources/Styles/Converters.xaml.cs
@@ -0,0 +1,299 @@
+using System.Globalization;
+
+namespace PCPal.Configurator.Converters;
+
+// Converts a boolean to a color (used for selected tabs)
+public class BoolToColorConverter : IValueConverter
+{
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ if (value is bool isSelected && isSelected)
+ {
+ // Return the parameter color if true (selected)
+ if (parameter is Color color)
+ {
+ return color;
+ }
+ return Application.Current.Resources["Primary"] as Color ?? Colors.Transparent;
+ }
+
+ // Return transparent if false (not selected)
+ return Colors.Transparent;
+ }
+
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ throw new NotImplementedException();
+ }
+}
+
+// Converts a boolean to a text color (used for selected tabs)
+public class BoolToTextColorConverter : IValueConverter
+{
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ if (value is bool isSelected && isSelected)
+ {
+ // Return white for selected items
+ if (parameter is Color color)
+ {
+ return color;
+ }
+ return Colors.White;
+ }
+
+ // Return gray for non-selected items
+ return Color.FromArgb("#555555");
+ }
+
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ throw new NotImplementedException();
+ }
+}
+
+// Converts connection status to a color
+public class ConnectionStatusColorConverter : IValueConverter
+{
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ if (value is bool isConnected && isConnected)
+ {
+ // Green for connected
+ return Colors.Green;
+ }
+
+ // Red for disconnected
+ return Colors.Red;
+ }
+
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ throw new NotImplementedException();
+ }
+}
+
+// Converts a string to a color based on matching
+public class StringMatchConverter : IValueConverter
+{
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ // For matching menu items
+ if (value is string currentValue && parameter is string targetValue)
+ {
+ if (currentValue == targetValue)
+ {
+ // Handle different return types based on an additional parameter
+ if (parameter is string param2 && param2 == "TextColor")
+ {
+ // Return text color for selected item
+ return Application.Current.Resources["Primary"] as Color ?? Colors.Black;
+ }
+
+ // Return background color for selected item
+ return Application.Current.Resources["PrimaryLight"] as Color ?? Colors.LightBlue;
+ }
+ }
+
+ // Return default values
+ if (parameter is string param && param == "TextColor")
+ {
+ return Application.Current.Resources["TextSecondary"] as Color ?? Colors.Gray;
+ }
+
+ // Default background is transparent
+ return Colors.Transparent;
+ }
+
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ throw new NotImplementedException();
+ }
+}
+
+// Inverts a boolean
+public class InverseBoolConverter : IValueConverter
+{
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ if (value is bool boolValue)
+ {
+ return !boolValue;
+ }
+
+ return false;
+ }
+
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ if (value is bool boolValue)
+ {
+ return !boolValue;
+ }
+
+ return false;
+ }
+}
+
+// Converts a menu name to an icon
+public class MenuIconConverter : IValueConverter
+{
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ if (value is string menuItem)
+ {
+ return menuItem switch
+ {
+ "1602 LCD Display" => "icon_lcd.png",
+ "4.6\" TFT Display" => "icon_tft.png",
+ "OLED Display" => "icon_oled.png",
+ "Settings" => "icon_settings.png",
+ "Help" => "icon_help.png",
+ _ => "icon_default.png"
+ };
+ }
+
+ return "icon_default.png";
+ }
+
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ throw new NotImplementedException();
+ }
+}
+
+// Converts selected item to background color
+public class SelectedItemColorConverter : IValueConverter
+{
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ if (value is bool isSelected && isSelected)
+ {
+ if (parameter is Color color)
+ {
+ return color;
+ }
+ return Application.Current.Resources["PrimaryLight"] as Color ?? Colors.LightBlue;
+ }
+
+ return Colors.Transparent;
+ }
+
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ throw new NotImplementedException();
+ }
+}
+
+// Converts selected item to text color
+public class SelectedTextColorConverter : IValueConverter
+{
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ if (value is bool isSelected && isSelected)
+ {
+ if (parameter is Color color)
+ {
+ return color;
+ }
+ return Application.Current.Resources["Primary"] as Color ?? Colors.Blue;
+ }
+
+ return Application.Current.Resources["TextSecondary"] as Color ?? Colors.Gray;
+ }
+
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ throw new NotImplementedException();
+ }
+}
+
+// Formats bytes to human-readable size
+public class BytesToSizeConverter : IValueConverter
+{
+ private static readonly string[] SizeSuffixes = { "B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB" };
+
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ if (value is long size)
+ {
+ return BytesToString(size);
+ }
+
+ return "0 B";
+ }
+
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ throw new NotImplementedException();
+ }
+
+ private static string BytesToString(long value, int decimalPlaces = 1)
+ {
+ if (value < 0) { return "-" + BytesToString(-value, decimalPlaces); }
+
+ int i = 0;
+ decimal dValue = value;
+ while (Math.Round(dValue, decimalPlaces) >= 1000)
+ {
+ dValue /= 1024;
+ i++;
+ }
+
+ return string.Format("{0:n" + decimalPlaces + "} {1}", dValue, SizeSuffixes[i]);
+ }
+}
+
+// Converts a date/time to relative time (e.g., "just now", "5 minutes ago")
+public class RelativeTimeConverter : IValueConverter
+{
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ if (value is DateTime dateTime)
+ {
+ var elapsed = DateTime.Now - dateTime;
+
+ if (elapsed.TotalSeconds < 60)
+ return "just now";
+ if (elapsed.TotalMinutes < 60)
+ return $"{Math.Floor(elapsed.TotalMinutes)} minutes ago";
+ if (elapsed.TotalHours < 24)
+ return $"{Math.Floor(elapsed.TotalHours)} hours ago";
+ if (elapsed.TotalDays < 7)
+ return $"{Math.Floor(elapsed.TotalDays)} days ago";
+
+ return dateTime.ToString("g");
+ }
+
+ return "unknown time";
+ }
+
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ throw new NotImplementedException();
+ }
+}
+
+// Add this new converter for text color specifically
+public class StringMatchTextConverter : IValueConverter
+{
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ if (value is string currentValue && parameter is string targetValue)
+ {
+ if (currentValue == targetValue)
+ {
+ // Return text color for selected item
+ return Application.Current.Resources["Primary"] as Color ?? Colors.Blue;
+ }
+ }
+
+ // Return default text color
+ return Application.Current.Resources["TextSecondary"] as Color ?? Colors.Gray;
+ }
+
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ throw new NotImplementedException();
+ }
+}
\ No newline at end of file
diff --git a/PCPal/Configurator/Resources/Styles/Styles.xaml b/PCPal/Configurator/Resources/Styles/Styles.xaml
new file mode 100644
index 0000000..4e3b560
--- /dev/null
+++ b/PCPal/Configurator/Resources/Styles/Styles.xaml
@@ -0,0 +1,119 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/PCPal/Configurator/ViewModels/BaseViewModel.cs b/PCPal/Configurator/ViewModels/BaseViewModel.cs
new file mode 100644
index 0000000..cee402d
--- /dev/null
+++ b/PCPal/Configurator/ViewModels/BaseViewModel.cs
@@ -0,0 +1,42 @@
+using System.ComponentModel;
+using System.Runtime.CompilerServices;
+
+namespace PCPal.Configurator.ViewModels;
+
+public abstract class BaseViewModel : INotifyPropertyChanged
+{
+ private bool _isBusy;
+ private string _title;
+
+ public bool IsBusy
+ {
+ get => _isBusy;
+ set => SetProperty(ref _isBusy, value);
+ }
+
+ public string Title
+ {
+ get => _title;
+ set => SetProperty(ref _title, value);
+ }
+
+ public event PropertyChangedEventHandler PropertyChanged;
+
+ // Changed from protected to public so it can be called from outside
+ public virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
+ {
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+ }
+
+ protected bool SetProperty(ref T storage, T value, [CallerMemberName] string propertyName = null)
+ {
+ if (EqualityComparer.Default.Equals(storage, value))
+ {
+ return false;
+ }
+
+ storage = value;
+ OnPropertyChanged(propertyName);
+ return true;
+ }
+}
\ No newline at end of file
diff --git a/PCPal/Configurator/ViewModels/HelpViewModel.cs b/PCPal/Configurator/ViewModels/HelpViewModel.cs
new file mode 100644
index 0000000..a82f806
--- /dev/null
+++ b/PCPal/Configurator/ViewModels/HelpViewModel.cs
@@ -0,0 +1,49 @@
+using System.Windows.Input;
+
+namespace PCPal.Configurator.ViewModels;
+
+public class HelpViewModel : BaseViewModel
+{
+ private string _searchQuery;
+
+ public string SearchQuery
+ {
+ get => _searchQuery;
+ set => SetProperty(ref _searchQuery, value);
+ }
+
+ public ICommand SearchCommand { get; }
+ public ICommand OpenUrlCommand { get; }
+
+ public HelpViewModel()
+ {
+ Title = "Help & Documentation";
+
+ // Initialize commands
+ SearchCommand = new Command(Search);
+ OpenUrlCommand = new Command(OpenUrl);
+ }
+
+ private void Search(string query)
+ {
+ // In a real implementation, this would search through help topics
+ // For now, we'll just update the search query property
+ SearchQuery = query;
+ }
+
+ private async void OpenUrl(string url)
+ {
+ if (string.IsNullOrEmpty(url))
+ return;
+
+ try
+ {
+ await Browser.OpenAsync(url, BrowserLaunchMode.SystemPreferred);
+ }
+ catch (Exception ex)
+ {
+ // Handle error or notify user
+ await Shell.Current.DisplayAlert("Error", $"Cannot open URL: {ex.Message}", "OK");
+ }
+ }
+}
\ No newline at end of file
diff --git a/PCPal/Configurator/ViewModels/LcdConfigViewModel.cs b/PCPal/Configurator/ViewModels/LcdConfigViewModel.cs
new file mode 100644
index 0000000..4705740
--- /dev/null
+++ b/PCPal/Configurator/ViewModels/LcdConfigViewModel.cs
@@ -0,0 +1,383 @@
+using PCPal.Core.Services;
+using System.Collections.ObjectModel;
+using System.Diagnostics;
+using System.Windows.Input;
+
+namespace PCPal.Configurator.ViewModels;
+
+public class LcdConfigViewModel : BaseViewModel
+{
+ private readonly ISensorService _sensorService;
+ private readonly IConfigurationService _configService;
+ private readonly ISerialPortService _serialPortService;
+
+ private ObservableCollection _sensorOptions = new();
+ private string _line1Selection;
+ private string _line1CustomText;
+ private string _line1PostText;
+ private string _line2Selection;
+ private string _line2CustomText;
+ private string _line2PostText;
+ private string _line1Preview;
+ private string _line2Preview;
+
+ private Timer _previewUpdateTimer;
+
+ public ObservableCollection SensorOptions
+ {
+ get => _sensorOptions;
+ set => SetProperty(ref _sensorOptions, value);
+ }
+
+ public string Line1Selection
+ {
+ get => _line1Selection;
+ set
+ {
+ if (SetProperty(ref _line1Selection, value))
+ {
+ OnPropertyChanged(nameof(IsLine1CustomTextEnabled));
+ OnPropertyChanged(nameof(IsLine1PostTextEnabled));
+ UpdatePreview();
+ }
+ }
+ }
+
+ public string Line1CustomText
+ {
+ get => _line1CustomText;
+ set
+ {
+ if (SetProperty(ref _line1CustomText, value))
+ {
+ UpdatePreview();
+ }
+ }
+ }
+
+ public string Line1PostText
+ {
+ get => _line1PostText;
+ set
+ {
+ if (SetProperty(ref _line1PostText, value))
+ {
+ UpdatePreview();
+ }
+ }
+ }
+
+ public string Line2Selection
+ {
+ get => _line2Selection;
+ set
+ {
+ if (SetProperty(ref _line2Selection, value))
+ {
+ OnPropertyChanged(nameof(IsLine2CustomTextEnabled));
+ OnPropertyChanged(nameof(IsLine2PostTextEnabled));
+ UpdatePreview();
+ }
+ }
+ }
+
+ public string Line2CustomText
+ {
+ get => _line2CustomText;
+ set
+ {
+ if (SetProperty(ref _line2CustomText, value))
+ {
+ UpdatePreview();
+ }
+ }
+ }
+
+ public string Line2PostText
+ {
+ get => _line2PostText;
+ set
+ {
+ if (SetProperty(ref _line2PostText, value))
+ {
+ UpdatePreview();
+ }
+ }
+ }
+
+ public string Line1Preview
+ {
+ get => _line1Preview;
+ set => SetProperty(ref _line1Preview, value);
+ }
+
+ public string Line2Preview
+ {
+ get => _line2Preview;
+ set => SetProperty(ref _line2Preview, value);
+ }
+
+ public bool IsLine1CustomTextEnabled => Line1Selection == "Custom Text" || !string.IsNullOrEmpty(Line1Selection);
+ public bool IsLine1PostTextEnabled => Line1Selection != "Custom Text" && !string.IsNullOrEmpty(Line1Selection);
+ public bool IsLine2CustomTextEnabled => Line2Selection == "Custom Text" || !string.IsNullOrEmpty(Line2Selection);
+ public bool IsLine2PostTextEnabled => Line2Selection != "Custom Text" && !string.IsNullOrEmpty(Line2Selection);
+
+ public ICommand SaveConfigCommand { get; }
+ public ICommand TestConnectionCommand { get; }
+
+ public LcdConfigViewModel(
+ ISensorService sensorService,
+ IConfigurationService configService,
+ ISerialPortService serialPortService)
+ {
+ _sensorService = sensorService;
+ _configService = configService;
+ _serialPortService = serialPortService;
+
+ SaveConfigCommand = new Command(async () => await SaveConfigAsync());
+ TestConnectionCommand = new Command(async () => await TestConnectionAsync());
+
+ // Setup timer for preview updates
+ _previewUpdateTimer = new Timer(async (_) => await UpdateSensorDataAsync(), null, Timeout.Infinite, Timeout.Infinite);
+ }
+
+ public async Task Initialize()
+ {
+ IsBusy = true;
+
+ try
+ {
+ await _sensorService.UpdateSensorValuesAsync();
+
+ // Load sensor options
+ await LoadSensorOptionsAsync();
+
+ // Load configuration
+ await LoadConfigAsync();
+
+ // Start preview updates
+ _previewUpdateTimer.Change(0, 2000); // Update every 2 seconds
+
+ // Initial preview update
+ await UpdateSensorDataAsync();
+ }
+ catch (Exception ex)
+ {
+ await Shell.Current.DisplayAlert("Error", $"Failed to initialize: {ex.Message}", "OK");
+ }
+ finally
+ {
+ IsBusy = false;
+ }
+ }
+
+ private async Task LoadSensorOptionsAsync()
+ {
+ await _sensorService.UpdateSensorValuesAsync();
+
+ var sensorGroups = _sensorService.GetAllSensorsGrouped();
+ var options = new List();
+
+ foreach (var group in sensorGroups)
+ {
+ foreach (var sensor in group.Sensors)
+ {
+ // Only include load, temperature, and data sensors for simplicity
+ if (sensor.SensorType == LibreHardwareMonitor.Hardware.SensorType.Load ||
+ sensor.SensorType == LibreHardwareMonitor.Hardware.SensorType.Temperature ||
+ sensor.SensorType == LibreHardwareMonitor.Hardware.SensorType.Data)
+ {
+ options.Add($"{group.Type}: {sensor.Name} ({sensor.SensorType})");
+ }
+ }
+ }
+
+ options.Add("Custom Text");
+
+ SensorOptions = new ObservableCollection(options);
+ }
+
+ private async Task LoadConfigAsync()
+ {
+ var config = await _configService.LoadConfigAsync();
+
+ // If config is found, load into UI
+ Line1Selection = config.Line1Selection;
+ Line1CustomText = config.Line1CustomText;
+ Line1PostText = config.Line1PostText;
+ Line2Selection = config.Line2Selection;
+ Line2CustomText = config.Line2CustomText;
+ Line2PostText = config.Line2PostText;
+
+ // Set defaults if not configured
+ if (string.IsNullOrEmpty(Line1Selection) && SensorOptions.Count > 0)
+ {
+ // Try to find a CPU load sensor as default
+ var cpuOption = SensorOptions.FirstOrDefault(s => s.Contains("Cpu") && s.Contains("Load"));
+ Line1Selection = cpuOption ?? SensorOptions.First();
+ Line1CustomText = "CPU ";
+ Line1PostText = "%";
+ }
+
+ if (string.IsNullOrEmpty(Line2Selection) && SensorOptions.Count > 1)
+ {
+ // Try to find a Memory sensor as default
+ var memoryOption = SensorOptions.FirstOrDefault(s => s.Contains("Memory") && s.Contains("Data"));
+ Line2Selection = memoryOption ?? SensorOptions[1];
+ Line2CustomText = "Memory ";
+ Line2PostText = "GB";
+ }
+ }
+
+ private async Task SaveConfigAsync()
+ {
+ try
+ {
+ IsBusy = true;
+
+ var config = await _configService.LoadConfigAsync();
+
+ // Update LCD configuration
+ config.ScreenType = "1602";
+ config.Line1Selection = Line1Selection;
+ config.Line1CustomText = Line1CustomText;
+ config.Line1PostText = Line1PostText;
+ config.Line2Selection = Line2Selection;
+ config.Line2CustomText = Line2CustomText;
+ config.Line2PostText = Line2PostText;
+
+ await _configService.SaveConfigAsync(config);
+
+ // Send configuration to device if connected
+ if (_serialPortService.IsConnected)
+ {
+ await _serialPortService.SendCommandAsync($"CMD:LCD,0,{Line1Preview}");
+ await _serialPortService.SendCommandAsync($"CMD:LCD,1,{Line2Preview}");
+ }
+
+ await Shell.Current.DisplayAlert("Success", "Configuration saved successfully!", "OK");
+ }
+ catch (Exception ex)
+ {
+ await Shell.Current.DisplayAlert("Error", $"Failed to save configuration: {ex.Message}", "OK");
+ }
+ finally
+ {
+ IsBusy = false;
+ }
+ }
+
+ private async Task TestConnectionAsync()
+ {
+ try
+ {
+ IsBusy = true;
+
+ if (!_serialPortService.IsConnected)
+ {
+ var result = await _serialPortService.ConnectToFirstAvailableAsync();
+
+ if (!result)
+ {
+ await Shell.Current.DisplayAlert("Connection Failed",
+ "Could not connect to PCPal device. Please check that it's connected to your computer.", "OK");
+ return;
+ }
+ }
+
+ // Send test message to the LCD
+ await _serialPortService.SendCommandAsync($"CMD:LCD,0,PCPal Test");
+ await _serialPortService.SendCommandAsync($"CMD:LCD,1,Connection OK!");
+
+ await Shell.Current.DisplayAlert("Connection Successful",
+ $"Connected to PCPal device on {_serialPortService.CurrentPort}", "OK");
+
+ // Restore actual content after 3 seconds
+ await Task.Delay(3000);
+ await _serialPortService.SendCommandAsync($"CMD:LCD,0,{Line1Preview}");
+ await _serialPortService.SendCommandAsync($"CMD:LCD,1,{Line2Preview}");
+ }
+ catch (Exception ex)
+ {
+ await Shell.Current.DisplayAlert("Error", $"Connection test failed: {ex.Message}", "OK");
+ }
+ finally
+ {
+ IsBusy = false;
+ }
+ }
+
+ private async Task UpdateSensorDataAsync()
+ {
+ try
+ {
+ await _sensorService.UpdateSensorValuesAsync();
+ UpdatePreview();
+ }
+ catch (Exception ex)
+ {
+ // Log error but don't display to user since this happens in background
+ Debug.WriteLine($"Error updating sensor data: {ex.Message}");
+ }
+ }
+
+ private void UpdatePreview()
+ {
+ Line1Preview = GetSensorLinePreview(Line1Selection, Line1CustomText, Line1PostText);
+ Line2Preview = GetSensorLinePreview(Line2Selection, Line2CustomText, Line2PostText);
+ }
+
+ private string GetSensorLinePreview(string selection, string prefix, string suffix)
+ {
+ if (string.IsNullOrEmpty(selection))
+ {
+ return string.Empty;
+ }
+
+ if (selection == "Custom Text")
+ {
+ return prefix ?? string.Empty;
+ }
+
+ try
+ {
+ // Parse sensor selection to get hardware type, sensor name and type
+ var parts = selection.Split(':', 2);
+ if (parts.Length != 2)
+ {
+ return "Error: Invalid selection";
+ }
+
+ var hardwareTypeStr = parts[0].Trim();
+ var sensorInfo = parts[1].Trim();
+
+ int idx = sensorInfo.LastIndexOf('(');
+ if (idx == -1)
+ {
+ return "Error: Invalid format";
+ }
+
+ string sensorName = sensorInfo.Substring(0, idx).Trim();
+ string sensorTypeStr = sensorInfo.Substring(idx + 1).Trim().TrimEnd(')');
+
+ // Get sensor value
+ var sensorGroups = _sensorService.GetAllSensorsGrouped();
+ var sensor = sensorGroups
+ .SelectMany(g => g.Sensors)
+ .FirstOrDefault(s =>
+ s.SensorType.ToString() == sensorTypeStr &&
+ s.Name == sensorName);
+
+ if (sensor != null)
+ {
+ return $"{prefix ?? ""}{sensor.FormattedValue}{suffix ?? ""}";
+ }
+
+ return "N/A";
+ }
+ catch
+ {
+ return "Error";
+ }
+ }
+}
\ No newline at end of file
diff --git a/PCPal/Configurator/ViewModels/OledConfigViewModel.cs b/PCPal/Configurator/ViewModels/OledConfigViewModel.cs
new file mode 100644
index 0000000..ffd2341
--- /dev/null
+++ b/PCPal/Configurator/ViewModels/OledConfigViewModel.cs
@@ -0,0 +1,1252 @@
+using PCPal.Core.Models;
+using PCPal.Core.Services;
+using PCPal.Configurator.Views.OLED;
+using System.Collections.ObjectModel;
+using System.Windows.Input;
+using System.Text.RegularExpressions;
+//using Javax.Xml.Transform;
+using System.Diagnostics;
+
+namespace PCPal.Configurator.ViewModels;
+
+public class OledConfigViewModel : BaseViewModel
+{
+ private readonly ISensorService _sensorService;
+ private readonly IConfigurationService _configService;
+ private readonly ISerialPortService _serialPortService;
+
+ // Tab selection
+ private bool _isVisualEditorSelected;
+ private bool _isMarkupEditorSelected;
+ private bool _isTemplatesSelected;
+ private ContentView _currentView;
+
+ // Markup editor data
+ private string _oledMarkup;
+ private List _previewElements;
+
+ // Visual editor data
+ private ObservableCollection _oledElements;
+ private OledElement _selectedElement;
+ private bool _showGridLines;
+ private float _zoomLevel;
+ private string _currentSensorFilter;
+ private ObservableCollection _filteredSensors;
+
+ // Common properties
+ private ObservableCollection _availableSensors;
+
+ // Templates properties
+ private ObservableCollection _templateList;
+ private Template _selectedTemplate;
+ private ObservableCollection _customTemplates;
+ private string _newTemplateName;
+
+ // Views
+ private readonly OledVisualEditorView _visualEditorView;
+ private readonly OledMarkupEditorView _markupEditorView;
+ private readonly OledTemplatesView _templatesView;
+
+ // Timer for sensor updates
+ private Timer _sensorUpdateTimer;
+
+ #region Properties
+
+ // Tab selection properties
+ public bool IsVisualEditorSelected
+ {
+ get => _isVisualEditorSelected;
+ set => SetProperty(ref _isVisualEditorSelected, value);
+ }
+
+ public bool IsMarkupEditorSelected
+ {
+ get => _isMarkupEditorSelected;
+ set => SetProperty(ref _isMarkupEditorSelected, value);
+ }
+
+ public bool IsTemplatesSelected
+ {
+ get => _isTemplatesSelected;
+ set => SetProperty(ref _isTemplatesSelected, value);
+ }
+
+ public ContentView CurrentView
+ {
+ get => _currentView;
+ set => SetProperty(ref _currentView, value);
+ }
+
+ // Markup editor properties
+ public string OledMarkup
+ {
+ get => _oledMarkup;
+ set => SetProperty(ref _oledMarkup, value);
+ }
+
+ public List PreviewElements
+ {
+ get => _previewElements;
+ set => SetProperty(ref _previewElements, value);
+ }
+
+ // Visual editor properties
+ public ObservableCollection OledElements
+ {
+ get => _oledElements;
+ set => SetProperty(ref _oledElements, value);
+ }
+
+ public OledElement SelectedElement
+ {
+ get => _selectedElement;
+ set
+ {
+ if (SetProperty(ref _selectedElement, value))
+ {
+ OnPropertyChanged(nameof(HasSelectedElement));
+ OnPropertyChanged(nameof(IsTextElementSelected));
+ OnPropertyChanged(nameof(IsBarElementSelected));
+ OnPropertyChanged(nameof(IsRectangleElementSelected));
+ OnPropertyChanged(nameof(IsLineElementSelected));
+ OnPropertyChanged(nameof(IsIconElementSelected));
+
+ // Update element properties
+ OnPropertyChanged(nameof(SelectedElementX));
+ OnPropertyChanged(nameof(SelectedElementY));
+ OnPropertyChanged(nameof(SelectedElementText));
+ OnPropertyChanged(nameof(SelectedElementSize));
+ OnPropertyChanged(nameof(SelectedElementWidth));
+ OnPropertyChanged(nameof(SelectedElementHeight));
+ OnPropertyChanged(nameof(SelectedElementValue));
+ OnPropertyChanged(nameof(SelectedElementX2));
+ OnPropertyChanged(nameof(SelectedElementY2));
+ OnPropertyChanged(nameof(SelectedElementIconName));
+ OnPropertyChanged(nameof(SelectedElementSensor));
+ }
+ }
+ }
+
+ public bool HasSelectedElement => SelectedElement != null;
+ public bool IsTextElementSelected => SelectedElement?.Type == "text";
+ public bool IsBarElementSelected => SelectedElement?.Type == "bar";
+ public bool IsRectangleElementSelected => SelectedElement?.Type == "rect" || SelectedElement?.Type == "box";
+ public bool IsLineElementSelected => SelectedElement?.Type == "line";
+ public bool IsIconElementSelected => SelectedElement?.Type == "icon";
+
+ public bool ShowGridLines
+ {
+ get => _showGridLines;
+ set => SetProperty(ref _showGridLines, value);
+ }
+
+ public float ZoomLevel
+ {
+ get => _zoomLevel;
+ set => SetProperty(ref _zoomLevel, value);
+ }
+
+ public string CurrentSensorFilter
+ {
+ get => _currentSensorFilter;
+ set
+ {
+ if (SetProperty(ref _currentSensorFilter, value))
+ {
+ ApplySensorFilter();
+ }
+ }
+ }
+
+ public ObservableCollection FilteredSensors
+ {
+ get => _filteredSensors;
+ set => SetProperty(ref _filteredSensors, value);
+ }
+
+ // Common properties
+ public ObservableCollection AvailableSensors
+ {
+ get => _availableSensors;
+ set => SetProperty(ref _availableSensors, value);
+ }
+
+ // Templates properties
+ public ObservableCollection TemplateList
+ {
+ get => _templateList;
+ set => SetProperty(ref _templateList, value);
+ }
+
+ public Template SelectedTemplate
+ {
+ get => _selectedTemplate;
+ set
+ {
+ if (SetProperty(ref _selectedTemplate, value))
+ {
+ OnPropertyChanged(nameof(HasSelectedTemplate));
+ }
+ }
+ }
+
+ public bool HasSelectedTemplate => SelectedTemplate != null;
+
+ public ObservableCollection CustomTemplates
+ {
+ get => _customTemplates;
+ set => SetProperty(ref _customTemplates, value);
+ }
+
+ public string NewTemplateName
+ {
+ get => _newTemplateName;
+ set => SetProperty(ref _newTemplateName, value);
+ }
+
+ // Selected element properties
+ public string SelectedElementX
+ {
+ get => SelectedElement?.X.ToString() ?? string.Empty;
+ set
+ {
+ if (SelectedElement != null && int.TryParse(value, out int x))
+ {
+ SelectedElement.X = x;
+ UpdateMarkupFromElements();
+ OnPropertyChanged(nameof(SelectedElementX));
+ }
+ }
+ }
+
+ public string SelectedElementY
+ {
+ get => SelectedElement?.Y.ToString() ?? string.Empty;
+ set
+ {
+ if (SelectedElement != null && int.TryParse(value, out int y))
+ {
+ SelectedElement.Y = y;
+ UpdateMarkupFromElements();
+ OnPropertyChanged(nameof(SelectedElementY));
+ }
+ }
+ }
+
+ public string SelectedElementText
+ {
+ get => SelectedElement?.Properties.GetValueOrDefault("content") ?? string.Empty;
+ set
+ {
+ if (SelectedElement != null)
+ {
+ SelectedElement.Properties["content"] = value;
+ UpdateMarkupFromElements();
+ OnPropertyChanged(nameof(SelectedElementText));
+ }
+ }
+ }
+
+ public string SelectedElementSize
+ {
+ get => SelectedElement?.Properties.GetValueOrDefault("size") ?? "1";
+ set
+ {
+ if (SelectedElement != null)
+ {
+ SelectedElement.Properties["size"] = value;
+ UpdateMarkupFromElements();
+ OnPropertyChanged(nameof(SelectedElementSize));
+ }
+ }
+ }
+
+ public string SelectedElementWidth
+ {
+ get => SelectedElement?.Properties.GetValueOrDefault("width") ?? string.Empty;
+ set
+ {
+ if (SelectedElement != null && int.TryParse(value, out int width))
+ {
+ SelectedElement.Properties["width"] = width.ToString();
+ UpdateMarkupFromElements();
+ OnPropertyChanged(nameof(SelectedElementWidth));
+ }
+ }
+ }
+
+ public string SelectedElementHeight
+ {
+ get => SelectedElement?.Properties.GetValueOrDefault("height") ?? string.Empty;
+ set
+ {
+ if (SelectedElement != null && int.TryParse(value, out int height))
+ {
+ SelectedElement.Properties["height"] = height.ToString();
+ UpdateMarkupFromElements();
+ OnPropertyChanged(nameof(SelectedElementHeight));
+ }
+ }
+ }
+
+ public float SelectedElementValue
+ {
+ get
+ {
+ if (SelectedElement != null && float.TryParse(SelectedElement.Properties.GetValueOrDefault("value"), out float value))
+ {
+ return value;
+ }
+ return 0;
+ }
+ set
+ {
+ if (SelectedElement != null)
+ {
+ SelectedElement.Properties["value"] = value.ToString("F0");
+ UpdateMarkupFromElements();
+ OnPropertyChanged(nameof(SelectedElementValue));
+ }
+ }
+ }
+
+ public string SelectedElementX2
+ {
+ get => SelectedElement?.Properties.GetValueOrDefault("x2") ?? string.Empty;
+ set
+ {
+ if (SelectedElement != null && int.TryParse(value, out int x2))
+ {
+ SelectedElement.Properties["x2"] = x2.ToString();
+ UpdateMarkupFromElements();
+ OnPropertyChanged(nameof(SelectedElementX2));
+ }
+ }
+ }
+
+ public string SelectedElementY2
+ {
+ get => SelectedElement?.Properties.GetValueOrDefault("y2") ?? string.Empty;
+ set
+ {
+ if (SelectedElement != null && int.TryParse(value, out int y2))
+ {
+ SelectedElement.Properties["y2"] = y2.ToString();
+ UpdateMarkupFromElements();
+ OnPropertyChanged(nameof(SelectedElementY2));
+ }
+ }
+ }
+
+ public string SelectedElementIconName
+ {
+ get => SelectedElement?.Properties.GetValueOrDefault("name") ?? string.Empty;
+ set
+ {
+ if (SelectedElement != null)
+ {
+ SelectedElement.Properties["name"] = value;
+ UpdateMarkupFromElements();
+ OnPropertyChanged(nameof(SelectedElementIconName));
+ }
+ }
+ }
+
+ public string SelectedElementSensor
+ {
+ get => SelectedElement?.Properties.GetValueOrDefault("sensor") ?? string.Empty;
+ set
+ {
+ if (SelectedElement != null)
+ {
+ SelectedElement.Properties["sensor"] = value;
+ UpdateMarkupFromElements();
+ OnPropertyChanged(nameof(SelectedElementSensor));
+ }
+ }
+ }
+
+ // Lists for populating pickers
+ public List FontSizes => new List { "1", "2", "3" };
+
+ #endregion
+
+ #region Commands
+
+ // Tab selection commands
+ public ICommand SwitchToVisualEditorCommand { get; }
+ public ICommand SwitchToMarkupEditorCommand { get; }
+ public ICommand SwitchToTemplatesCommand { get; }
+
+ // Common commands
+ public ICommand SaveConfigCommand { get; }
+ public ICommand PreviewCommand { get; }
+ public ICommand ResetCommand { get; }
+
+ // Visual editor commands
+ public ICommand AddElementCommand { get; }
+ public ICommand DeleteElementCommand { get; }
+ public ICommand ZoomInCommand { get; }
+ public ICommand ZoomOutCommand { get; }
+ public ICommand FilterSensorsCommand { get; }
+ public ICommand AddSensorToDisplayCommand { get; }
+ public ICommand BrowseIconsCommand { get; }
+
+ // Markup editor commands
+ public ICommand InsertMarkupCommand { get; }
+ public ICommand InsertSensorVariableCommand { get; }
+ public ICommand LoadExampleCommand { get; }
+
+ // Templates commands
+ public ICommand UseTemplateCommand { get; }
+ public ICommand SaveAsTemplateCommand { get; }
+ public ICommand UseCustomTemplateCommand { get; }
+ public ICommand DeleteCustomTemplateCommand { get; }
+
+ #endregion
+
+ public OledConfigViewModel(
+ ISensorService sensorService,
+ IConfigurationService configService,
+ ISerialPortService serialPortService)
+ {
+ _sensorService = sensorService;
+ _configService = configService;
+ _serialPortService = serialPortService;
+
+ // Initialize collections
+ _oledElements = new ObservableCollection();
+ _previewElements = new List();
+ _availableSensors = new ObservableCollection();
+ _filteredSensors = new ObservableCollection();
+ _templateList = new ObservableCollection();
+ _customTemplates = new ObservableCollection();
+
+ // Create views
+ _visualEditorView = new OledVisualEditorView { BindingContext = this };
+ _markupEditorView = new OledMarkupEditorView { BindingContext = this };
+ _templatesView = new OledTemplatesView { BindingContext = this };
+
+ // Default values
+ _isVisualEditorSelected = true;
+ _currentView = _visualEditorView;
+ _showGridLines = false;
+ _zoomLevel = 3.0f;
+ _currentSensorFilter = "All";
+
+ // Tab selection commands
+ SwitchToVisualEditorCommand = new Command(() => SwitchTab("visual"));
+ SwitchToMarkupEditorCommand = new Command(() => SwitchTab("markup"));
+ SwitchToTemplatesCommand = new Command(() => SwitchTab("templates"));
+
+ // Common commands
+ SaveConfigCommand = new Command(async () => await SaveConfigAsync());
+ PreviewCommand = new Command(async () => await PreviewOnDeviceAsync());
+ ResetCommand = new Command(async () => await ResetLayoutAsync());
+
+ // Visual editor commands
+ AddElementCommand = new Command(type => AddElement(type));
+ DeleteElementCommand = new Command(DeleteSelectedElement);
+ ZoomInCommand = new Command(ZoomIn);
+ ZoomOutCommand = new Command(ZoomOut);
+ FilterSensorsCommand = new Command(filter => CurrentSensorFilter = filter);
+ AddSensorToDisplayCommand = new Command(sensorId => AddSensorToDisplay(sensorId));
+ BrowseIconsCommand = new Command(async () => await BrowseIconsAsync());
+
+ // Markup editor commands
+ InsertMarkupCommand = new Command(type => InsertMarkupTemplate(type));
+ InsertSensorVariableCommand = new Command(async () => await InsertSensorVariableAsync());
+ LoadExampleCommand = new Command(async () => await LoadExampleMarkupAsync());
+
+ // Templates commands
+ UseTemplateCommand = new Command(async () => await UseSelectedTemplateAsync());
+ SaveAsTemplateCommand = new Command(async () => await SaveAsTemplateAsync());
+ UseCustomTemplateCommand = new Command(async (template) => await UseCustomTemplateAsync(template));
+ DeleteCustomTemplateCommand = new Command(async (template) => await DeleteCustomTemplateAsync(template));
+
+ // Setup timer for preview updates
+ _sensorUpdateTimer = new Timer(async (_) => await UpdateSensorDataAsync(), null, Timeout.Infinite, Timeout.Infinite);
+ }
+
+ public async Task Initialize()
+ {
+ IsBusy = true;
+
+ try
+ {
+ // Load sensor data
+ await _sensorService.UpdateSensorValuesAsync();
+ await LoadSensorsAsync();
+
+ // Load configuration
+ await LoadConfigAsync();
+
+ // Load templates
+ await LoadTemplatesAsync();
+
+ // Switch to visual editor by default
+ SwitchTab("visual");
+
+ // Start sensor updates
+ _sensorUpdateTimer.Change(0, 2000); // Update every 2 seconds
+ }
+ catch (Exception ex)
+ {
+ await Shell.Current.DisplayAlert("Error", $"Failed to initialize: {ex.Message}", "OK");
+ }
+ finally
+ {
+ IsBusy = false;
+ }
+ }
+
+ private async Task LoadSensorsAsync()
+ {
+ await _sensorService.UpdateSensorValuesAsync();
+
+ var sensorGroups = _sensorService.GetAllSensorsGrouped();
+ var sensors = new List();
+
+ foreach (var group in sensorGroups)
+ {
+ foreach (var sensor in group.Sensors)
+ {
+ sensors.Add(sensor);
+ }
+ }
+
+ AvailableSensors = new ObservableCollection(sensors);
+ FilteredSensors = new ObservableCollection(sensors);
+ }
+
+ private async Task LoadConfigAsync()
+ {
+ var config = await _configService.LoadConfigAsync();
+
+ // If OLED markup exists, load it
+ if (!string.IsNullOrEmpty(config.OledMarkup))
+ {
+ OledMarkup = config.OledMarkup;
+ await ParseMarkupToElementsAsync(OledMarkup);
+ UpdatePreviewFromMarkup();
+ }
+ else
+ {
+ // Load an example if no config exists
+ await LoadExampleMarkupAsync();
+ }
+ }
+
+ private async Task LoadTemplatesAsync()
+ {
+ // Predefined templates
+ var templates = new List
+ {
+ new Template
+ {
+ Name = "System Monitor",
+ Description = "Shows CPU, GPU, and memory usage with progress bars",
+ Markup = await CreateSystemMonitorTemplateAsync()
+ },
+ new Template
+ {
+ Name = "Temperature Monitor",
+ Description = "Shows temperatures of key components",
+ Markup = await CreateTemperatureMonitorTemplateAsync()
+ },
+ new Template
+ {
+ Name = "Network Monitor",
+ Description = "Shows network activity and throughput",
+ Markup = await CreateNetworkMonitorTemplateAsync()
+ },
+ new Template
+ {
+ Name = "Storage Monitor",
+ Description = "Shows disk space usage and activity",
+ Markup = await CreateStorageMonitorTemplateAsync()
+ }
+ };
+
+ // Process templates to create preview elements
+ foreach (var template in templates)
+ {
+ template.PreviewElements = await ParseMarkupToPreviewElements(template.Markup);
+ }
+
+ TemplateList = new ObservableCollection(templates);
+
+ // Load custom templates
+ var config = await _configService.LoadConfigAsync();
+ if (config.SavedProfiles != null && config.SavedProfiles.Any())
+ {
+ var customTemplates = new List();
+
+ foreach (var profile in config.SavedProfiles)
+ {
+ if (profile.ScreenType == "OLED")
+ {
+ var template = new Template
+ {
+ Name = profile.Name,
+ Description = "Custom template",
+ Markup = profile.ConfigData
+ };
+
+ template.PreviewElements = await ParseMarkupToPreviewElements(template.Markup);
+ customTemplates.Add(template);
+ }
+ }
+
+ CustomTemplates = new ObservableCollection(customTemplates);
+ }
+ }
+
+ private void SwitchTab(string tab)
+ {
+ IsVisualEditorSelected = tab == "visual";
+ IsMarkupEditorSelected = tab == "markup";
+ IsTemplatesSelected = tab == "templates";
+
+ switch (tab)
+ {
+ case "visual":
+ CurrentView = _visualEditorView;
+ break;
+ case "markup":
+ CurrentView = _markupEditorView;
+ break;
+ case "templates":
+ CurrentView = _templatesView;
+ break;
+ }
+ }
+
+ private async Task SaveConfigAsync()
+ {
+ try
+ {
+ IsBusy = true;
+
+ var config = await _configService.LoadConfigAsync();
+
+ // Update OLED configuration
+ config.ScreenType = "OLED";
+ config.OledMarkup = OledMarkup;
+
+ await _configService.SaveConfigAsync(config);
+
+ // Send configuration to device if connected
+ if (_serialPortService.IsConnected)
+ {
+ string processedMarkup = _sensorService.ProcessVariablesInMarkup(OledMarkup);
+ await _serialPortService.SendCommandAsync($"CMD:OLED,{processedMarkup}");
+ }
+
+ await Shell.Current.DisplayAlert("Success", "Configuration saved successfully!", "OK");
+ }
+ catch (Exception ex)
+ {
+ await Shell.Current.DisplayAlert("Error", $"Failed to save configuration: {ex.Message}", "OK");
+ }
+ finally
+ {
+ IsBusy = false;
+ }
+ }
+
+ private async Task PreviewOnDeviceAsync()
+ {
+ try
+ {
+ IsBusy = true;
+
+ if (!_serialPortService.IsConnected)
+ {
+ var result = await _serialPortService.ConnectToFirstAvailableAsync();
+
+ if (!result)
+ {
+ await Shell.Current.DisplayAlert("Connection Failed",
+ "Could not connect to PCPal device. Please check that it's connected to your computer.", "OK");
+ return;
+ }
+ }
+
+ // Send markup to the OLED display
+ string processedMarkup = _sensorService.ProcessVariablesInMarkup(OledMarkup);
+ await _serialPortService.SendCommandAsync($"CMD:OLED,{processedMarkup}");
+
+ await Shell.Current.DisplayAlert("Preview Sent",
+ "Your design has been sent to the device. It has not been saved permanently.", "OK");
+ }
+ catch (Exception ex)
+ {
+ await Shell.Current.DisplayAlert("Error", $"Preview failed: {ex.Message}", "OK");
+ }
+ finally
+ {
+ IsBusy = false;
+ }
+ }
+
+ private async Task ResetLayoutAsync()
+ {
+ bool confirm = await Shell.Current.DisplayAlert(
+ "Reset Layout",
+ "Are you sure you want to reset your layout? This will discard all your changes.",
+ "Reset", "Cancel");
+
+ if (confirm)
+ {
+ await LoadExampleMarkupAsync();
+ }
+ }
+
+ public void UpdatePreviewFromMarkup()
+ {
+ try
+ {
+ // Parse the markup into preview elements
+ var markupParser = new MarkupParser(_sensorService.GetAllSensorValues());
+ PreviewElements = markupParser.ParseMarkup(OledMarkup);
+ }
+ catch (Exception ex)
+ {
+ Debug.WriteLine($"Error parsing markup: {ex.Message}");
+ }
+ }
+
+ private async Task UpdateSensorDataAsync()
+ {
+ try
+ {
+ await _sensorService.UpdateSensorValuesAsync();
+ UpdatePreviewFromMarkup();
+
+ // Update available sensors
+ await LoadSensorsAsync();
+ }
+ catch (Exception ex)
+ {
+ // Log error but don't display to user since this happens in background
+ Debug.WriteLine($"Error updating sensor data: {ex.Message}");
+ }
+ }
+
+ private void ApplySensorFilter()
+ {
+ if (string.IsNullOrEmpty(CurrentSensorFilter) || CurrentSensorFilter == "All")
+ {
+ FilteredSensors = new ObservableCollection(AvailableSensors);
+ return;
+ }
+
+ var filtered = AvailableSensors.Where(s =>
+ s.HardwareName.Contains(CurrentSensorFilter, StringComparison.OrdinalIgnoreCase)).ToList();
+
+ FilteredSensors = new ObservableCollection(filtered);
+ }
+
+ private void AddElement(string type)
+ {
+ var element = new OledElement
+ {
+ Type = type,
+ X = 10,
+ Y = 10
+ };
+
+ // Set default properties based on type
+ switch (type)
+ {
+ case "text":
+ element.Properties["size"] = "1";
+ element.Properties["content"] = "New Text";
+ break;
+
+ case "bar":
+ element.Properties["width"] = "100";
+ element.Properties["height"] = "8";
+ element.Properties["value"] = "50";
+ break;
+
+ case "rect":
+ case "box":
+ element.Properties["width"] = "20";
+ element.Properties["height"] = "10";
+ break;
+
+ case "line":
+ element.Properties["x2"] = "30";
+ element.Properties["y2"] = "30";
+ break;
+
+ case "icon":
+ element.Properties["name"] = "cpu";
+ break;
+ }
+
+ OledElements.Add(element);
+ SelectedElement = element;
+
+ // Update markup
+ UpdateMarkupFromElements();
+ }
+
+ private void DeleteSelectedElement()
+ {
+ if (SelectedElement != null)
+ {
+ OledElements.Remove(SelectedElement);
+ SelectedElement = null;
+
+ // Update markup
+ UpdateMarkupFromElements();
+ }
+ }
+
+ public void UpdateMarkupFromElements()
+ {
+ var sb = new System.Text.StringBuilder();
+
+ foreach (var element in OledElements)
+ {
+ sb.AppendLine(element.ToMarkup());
+ }
+
+ OledMarkup = sb.ToString();
+ UpdatePreviewFromMarkup();
+ }
+
+ private async Task ParseMarkupToElementsAsync(string markup)
+ {
+ if (string.IsNullOrEmpty(markup))
+ {
+ OledElements.Clear();
+ return;
+ }
+
+ var elements = new List();
+
+ // Parse text elements
+ foreach (Match match in Regex.Matches(markup, @"([^<]*)"))
+ {
+ var element = new OledElement { Type = "text" };
+ element.X = int.Parse(match.Groups[1].Value);
+ element.Y = int.Parse(match.Groups[2].Value);
+
+ if (match.Groups[3].Success)
+ {
+ element.Properties["size"] = match.Groups[3].Value;
+ }
+ else
+ {
+ element.Properties["size"] = "1";
+ }
+
+ element.Properties["content"] = match.Groups[4].Value;
+ elements.Add(element);
+ }
+
+ // Parse bar elements
+ foreach (Match match in Regex.Matches(markup, @""))
+ {
+ var element = new OledElement { Type = "bar" };
+ element.X = int.Parse(match.Groups[1].Value);
+ element.Y = int.Parse(match.Groups[2].Value);
+ element.Properties["width"] = match.Groups[3].Value;
+ element.Properties["height"] = match.Groups[4].Value;
+ element.Properties["value"] = match.Groups[5].Value;
+ elements.Add(element);
+ }
+
+ // Parse rect elements
+ foreach (Match match in Regex.Matches(markup, @""))
+ {
+ var element = new OledElement { Type = "rect" };
+ element.X = int.Parse(match.Groups[1].Value);
+ element.Y = int.Parse(match.Groups[2].Value);
+ element.Properties["width"] = match.Groups[3].Value;
+ element.Properties["height"] = match.Groups[4].Value;
+ elements.Add(element);
+ }
+
+ // Parse box elements
+ foreach (Match match in Regex.Matches(markup, @""))
+ {
+ var element = new OledElement { Type = "box" };
+ element.X = int.Parse(match.Groups[1].Value);
+ element.Y = int.Parse(match.Groups[2].Value);
+ element.Properties["width"] = match.Groups[3].Value;
+ element.Properties["height"] = match.Groups[4].Value;
+ elements.Add(element);
+ }
+
+ // Parse line elements
+ foreach (Match match in Regex.Matches(markup, @""))
+ {
+ var element = new OledElement { Type = "line" };
+ element.X = int.Parse(match.Groups[1].Value);
+ element.Y = int.Parse(match.Groups[2].Value);
+ element.Properties["x2"] = match.Groups[3].Value;
+ element.Properties["y2"] = match.Groups[4].Value;
+ elements.Add(element);
+ }
+
+ // Parse icon elements
+ foreach (Match match in Regex.Matches(markup, @""))
+ {
+ var element = new OledElement { Type = "icon" };
+ element.X = int.Parse(match.Groups[1].Value);
+ element.Y = int.Parse(match.Groups[2].Value);
+ element.Properties["name"] = match.Groups[3].Value;
+ elements.Add(element);
+ }
+
+ // Update the collection on the UI thread
+ await MainThread.InvokeOnMainThreadAsync(() =>
+ {
+ OledElements.Clear();
+ foreach (var element in elements)
+ {
+ OledElements.Add(element);
+ }
+ });
+ }
+
+ private async Task> ParseMarkupToPreviewElements(string markup)
+ {
+ try
+ {
+ var markupParser = new MarkupParser(_sensorService.GetAllSensorValues());
+ return markupParser.ParseMarkup(markup);
+ }
+ catch (Exception ex)
+ {
+ Debug.WriteLine($"Error parsing markup for preview: {ex.Message}");
+ return new List();
+ }
+ }
+
+ private void ZoomIn()
+ {
+ if (ZoomLevel < 5.0f)
+ {
+ ZoomLevel += 0.5f;
+ }
+ }
+
+ private void ZoomOut()
+ {
+ if (ZoomLevel > 1.0f)
+ {
+ ZoomLevel -= 0.5f;
+ }
+ }
+
+ private void AddSensorToDisplay(string sensorId)
+ {
+ // Find the sensor
+ var sensor = AvailableSensors.FirstOrDefault(s => s.Id == sensorId);
+
+ if (sensor == null)
+ {
+ return;
+ }
+
+ // Determine appropriate Y position (avoid overlap)
+ int yPos = 15;
+ if (OledElements.Any())
+ {
+ yPos = OledElements.Max(e => e.Y) + 15;
+ }
+
+ // Create text element with the sensor variable
+ var element = new OledElement
+ {
+ Type = "text",
+ X = 10,
+ Y = yPos
+ };
+
+ element.Properties["size"] = "1";
+ element.Properties["content"] = $"{sensor.Name}: {{{sensorId}}} {sensor.Unit}";
+
+ OledElements.Add(element);
+ SelectedElement = element;
+
+ // Create a progress bar if it's a load/percentage sensor
+ if (sensor.SensorType == LibreHardwareMonitor.Hardware.SensorType.Load ||
+ sensor.SensorType == LibreHardwareMonitor.Hardware.SensorType.Level)
+ {
+ var barElement = new OledElement
+ {
+ Type = "bar",
+ X = 10,
+ Y = yPos + 5
+ };
+
+ barElement.Properties["width"] = "100";
+ barElement.Properties["height"] = "8";
+ barElement.Properties["value"] = $"{{{sensorId}}}";
+
+ OledElements.Add(barElement);
+ }
+
+ // Update markup
+ UpdateMarkupFromElements();
+ }
+
+ private async Task BrowseIconsAsync()
+ {
+ // A mock implementation - in a real app, you'd implement a proper icon browser
+ var icons = new string[] { "cpu", "gpu", "ram", "disk", "network", "fan" };
+ string result = await Shell.Current.DisplayActionSheet("Select an icon", "Cancel", null, icons);
+
+ if (!string.IsNullOrEmpty(result) && result != "Cancel")
+ {
+ SelectedElementIconName = result;
+ }
+ }
+
+ private void InsertMarkupTemplate(string type)
+ {
+ string template = string.Empty;
+
+ switch (type)
+ {
+ case "text":
+ template = "Sample Text";
+ break;
+
+ case "bar":
+ template = "";
+ break;
+ }
+
+ if (!string.IsNullOrEmpty(template))
+ {
+ OledMarkup += Environment.NewLine + template;
+ UpdatePreviewFromMarkup();
+ }
+ }
+
+ private async Task InsertSensorVariableAsync()
+ {
+ // Create a list of sensor options
+ var options = AvailableSensors.Select(s => $"{s.DisplayName} ({s.Id})").ToArray();
+
+ string result = await Shell.Current.DisplayActionSheet("Select a sensor", "Cancel", null, options);
+
+ if (!string.IsNullOrEmpty(result) && result != "Cancel")
+ {
+ // Extract sensor ID
+ string id = result.Substring(result.LastIndexOf('(') + 1).TrimEnd(')');
+
+ // Insert the variable at the current cursor position (not implemented in this mock-up)
+ // In a real implementation, you'd need to track the cursor position in the editor
+ OledMarkup += $"{{{id}}}";
+ UpdatePreviewFromMarkup();
+ }
+ }
+
+ private async Task LoadExampleMarkupAsync()
+ {
+ OledMarkup = await _sensorService.CreateExampleMarkupAsync();
+ await ParseMarkupToElementsAsync(OledMarkup);
+ UpdatePreviewFromMarkup();
+ }
+
+ private async Task CreateSystemMonitorTemplateAsync()
+ {
+ return await _sensorService.CreateExampleMarkupAsync();
+ }
+
+ private async Task CreateTemperatureMonitorTemplateAsync()
+ {
+ var sb = new System.Text.StringBuilder();
+
+ string cpuTemp = _sensorService.FindFirstSensorOfType(LibreHardwareMonitor.Hardware.HardwareType.Cpu,
+ LibreHardwareMonitor.Hardware.SensorType.Temperature);
+
+ string gpuTemp = _sensorService.FindFirstSensorOfType(LibreHardwareMonitor.Hardware.HardwareType.GpuNvidia,
+ LibreHardwareMonitor.Hardware.SensorType.Temperature);
+
+ sb.AppendLine("Temperatures");
+
+ if (!string.IsNullOrEmpty(cpuTemp))
+ {
+ sb.AppendLine($"CPU: {{{cpuTemp}}}°C");
+ }
+
+ if (!string.IsNullOrEmpty(gpuTemp))
+ {
+ sb.AppendLine($"GPU: {{{gpuTemp}}}°C");
+ }
+
+ return sb.ToString();
+ }
+
+ private async Task CreateNetworkMonitorTemplateAsync()
+ {
+ var sb = new System.Text.StringBuilder();
+
+ sb.AppendLine("Network Monitor");
+ sb.AppendLine("Coming soon!");
+
+ return sb.ToString();
+ }
+
+ private async Task CreateStorageMonitorTemplateAsync()
+ {
+ var sb = new System.Text.StringBuilder();
+
+ sb.AppendLine("Storage Monitor");
+ sb.AppendLine("Coming soon!");
+
+ return sb.ToString();
+ }
+
+ private async Task UseSelectedTemplateAsync()
+ {
+ if (SelectedTemplate == null)
+ {
+ return;
+ }
+
+ bool confirm = await Shell.Current.DisplayAlert(
+ "Apply Template",
+ "Are you sure you want to apply this template? This will replace your current design.",
+ "Apply", "Cancel");
+
+ if (confirm)
+ {
+ OledMarkup = SelectedTemplate.Markup;
+ await ParseMarkupToElementsAsync(OledMarkup);
+ UpdatePreviewFromMarkup();
+ SwitchTab("markup");
+ }
+ }
+
+ private async Task SaveAsTemplateAsync()
+ {
+ if (string.IsNullOrWhiteSpace(NewTemplateName))
+ {
+ await Shell.Current.DisplayAlert("Error", "Please enter a name for your template.", "OK");
+ return;
+ }
+
+ // Check for duplicate names
+ if (CustomTemplates.Any(t => t.Name == NewTemplateName))
+ {
+ bool replace = await Shell.Current.DisplayAlert(
+ "Template Exists",
+ "A template with this name already exists. Do you want to replace it?",
+ "Replace", "Cancel");
+
+ if (!replace)
+ {
+ return;
+ }
+
+ // Remove the existing template
+ var existingTemplate = CustomTemplates.First(t => t.Name == NewTemplateName);
+ CustomTemplates.Remove(existingTemplate);
+ }
+
+ // Create new template
+ var template = new Template
+ {
+ Name = NewTemplateName,
+ Description = "Custom template",
+ Markup = OledMarkup
+ };
+
+ template.PreviewElements = await ParseMarkupToPreviewElements(template.Markup);
+ CustomTemplates.Add(template);
+
+ // Save to configuration
+ var config = await _configService.LoadConfigAsync();
+
+ if (config.SavedProfiles == null)
+ {
+ config.SavedProfiles = new List();
+ }
+
+ // Remove existing profile with same name
+ config.SavedProfiles.RemoveAll(p => p.Name == NewTemplateName && p.ScreenType == "OLED");
+
+ // Add new profile
+ config.SavedProfiles.Add(new DisplayProfile
+ {
+ Name = NewTemplateName,
+ ScreenType = "OLED",
+ ConfigData = OledMarkup
+ });
+
+ await _configService.SaveConfigAsync(config);
+
+ // Clear the input field
+ NewTemplateName = string.Empty;
+
+ await Shell.Current.DisplayAlert("Success", "Template saved successfully!", "OK");
+ }
+
+ private async Task UseCustomTemplateAsync(Template template)
+ {
+ if (template == null)
+ {
+ return;
+ }
+
+ bool confirm = await Shell.Current.DisplayAlert(
+ "Apply Template",
+ "Are you sure you want to apply this template? This will replace your current design.",
+ "Apply", "Cancel");
+
+ if (confirm)
+ {
+ OledMarkup = template.Markup;
+ await ParseMarkupToElementsAsync(OledMarkup);
+ UpdatePreviewFromMarkup();
+ SwitchTab("markup");
+ }
+ }
+
+ private async Task DeleteCustomTemplateAsync(Template template)
+ {
+ if (template == null)
+ {
+ return;
+ }
+
+ bool confirm = await Shell.Current.DisplayAlert(
+ "Delete Template",
+ $"Are you sure you want to delete the template '{template.Name}'?",
+ "Delete", "Cancel");
+
+ if (confirm)
+ {
+ CustomTemplates.Remove(template);
+
+ // Update configuration
+ var config = await _configService.LoadConfigAsync();
+
+ if (config.SavedProfiles != null)
+ {
+ config.SavedProfiles.RemoveAll(p => p.Name == template.Name && p.ScreenType == "OLED");
+ await _configService.SaveConfigAsync(config);
+ }
+ }
+ }
+}
+
+public class Template
+{
+ public string Name { get; set; }
+ public string Description { get; set; }
+ public string Markup { get; set; }
+ public List PreviewElements { get; set; } = new List();
+ public bool IsSelected { get; set; }
+}
\ No newline at end of file
diff --git a/PCPal/Configurator/ViewModels/SettingsViewModel.cs b/PCPal/Configurator/ViewModels/SettingsViewModel.cs
new file mode 100644
index 0000000..799f942
--- /dev/null
+++ b/PCPal/Configurator/ViewModels/SettingsViewModel.cs
@@ -0,0 +1,445 @@
+using PCPal.Core.Services;
+using System.Collections.ObjectModel;
+using System.Diagnostics;
+using System.IO.Ports;
+using System.Windows.Input;
+
+namespace PCPal.Configurator.ViewModels;
+
+public class SettingsViewModel : BaseViewModel
+{
+ private readonly IConfigurationService _configService;
+ private readonly ISerialPortService _serialPortService;
+
+ private ObservableCollection _availablePorts = new();
+ private string _selectedPort;
+ private bool _isAutoDetectEnabled;
+ private string _connectionStatus;
+ private bool _isConnected;
+ private float _refreshRate;
+ private bool _startWithWindows;
+ private bool _minimizeToTray;
+ private ObservableCollection _availableThemes = new();
+ private string _selectedTheme;
+ private string _serviceStatus;
+ private bool _isServiceRunning;
+
+ public ObservableCollection AvailablePorts
+ {
+ get => _availablePorts;
+ set => SetProperty(ref _availablePorts, value);
+ }
+
+ public string SelectedPort
+ {
+ get => _selectedPort;
+ set => SetProperty(ref _selectedPort, value);
+ }
+
+ public bool IsAutoDetectEnabled
+ {
+ get => _isAutoDetectEnabled;
+ set => SetProperty(ref _isAutoDetectEnabled, value);
+ }
+
+ public string ConnectionStatus
+ {
+ get => _connectionStatus;
+ set => SetProperty(ref _connectionStatus, value);
+ }
+
+ public bool IsConnected
+ {
+ get => _isConnected;
+ set => SetProperty(ref _isConnected, value);
+ }
+
+ public float RefreshRate
+ {
+ get => _refreshRate;
+ set => SetProperty(ref _refreshRate, value);
+ }
+
+ public bool StartWithWindows
+ {
+ get => _startWithWindows;
+ set => SetProperty(ref _startWithWindows, value);
+ }
+
+ public bool MinimizeToTray
+ {
+ get => _minimizeToTray;
+ set => SetProperty(ref _minimizeToTray, value);
+ }
+
+ public ObservableCollection AvailableThemes
+ {
+ get => _availableThemes;
+ set => SetProperty(ref _availableThemes, value);
+ }
+
+ public string SelectedTheme
+ {
+ get => _selectedTheme;
+ set => SetProperty(ref _selectedTheme, value);
+ }
+
+ public string ServiceStatus
+ {
+ get => _serviceStatus;
+ set => SetProperty(ref _serviceStatus, value);
+ }
+
+ public bool IsServiceRunning
+ {
+ get => _isServiceRunning;
+ set => SetProperty(ref _isServiceRunning, value);
+ }
+
+ // Commands
+ public ICommand TestConnectionCommand { get; }
+ public ICommand SaveSettingsCommand { get; }
+ public ICommand ExportSettingsCommand { get; }
+ public ICommand ImportSettingsCommand { get; }
+ public ICommand ResetSettingsCommand { get; }
+ public ICommand RefreshServiceStatusCommand { get; }
+ public ICommand StartServiceCommand { get; }
+ public ICommand StopServiceCommand { get; }
+ public ICommand RestartServiceCommand { get; }
+ public ICommand CheckForUpdatesCommand { get; }
+
+ public SettingsViewModel(IConfigurationService configService, ISerialPortService serialPortService)
+ {
+ Title = "Settings";
+
+ _configService = configService;
+ _serialPortService = serialPortService;
+
+ // Default values
+ IsConnected = false;
+ ConnectionStatus = "Not connected";
+ RefreshRate = 5.0f;
+ IsAutoDetectEnabled = true;
+ ServiceStatus = "Unknown";
+ IsServiceRunning = false;
+
+ // Initialize themes
+ AvailableThemes = new ObservableCollection
+ {
+ "System Default",
+ "Light",
+ "Dark"
+ };
+ SelectedTheme = "System Default";
+
+ // Initialize commands
+ TestConnectionCommand = new Command(async () => await TestConnectionAsync());
+ SaveSettingsCommand = new Command(async () => await SaveSettingsAsync());
+ ExportSettingsCommand = new Command(async () => await ExportSettingsAsync());
+ ImportSettingsCommand = new Command(async () => await ImportSettingsAsync());
+ ResetSettingsCommand = new Command(async () => await ResetSettingsAsync());
+ RefreshServiceStatusCommand = new Command(async () => await RefreshServiceStatusAsync());
+ StartServiceCommand = new Command(async () => await StartServiceAsync());
+ StopServiceCommand = new Command(async () => await StopServiceAsync());
+ RestartServiceCommand = new Command(async () => await RestartServiceAsync());
+ CheckForUpdatesCommand = new Command(async () => await CheckForUpdatesAsync());
+
+ // Subscribe to serial port connection changes
+ _serialPortService.ConnectionStatusChanged += OnConnectionStatusChanged;
+ }
+
+ public async Task Initialize()
+ {
+ IsBusy = true;
+
+ try
+ {
+ // Load available ports
+ await RefreshPortsAsync();
+
+ // Load settings
+ await LoadSettingsAsync();
+
+ // Check service status
+ await RefreshServiceStatusAsync();
+
+ // Check connection status
+ await _serialPortService.CheckConnectionAsync();
+ }
+ catch (Exception ex)
+ {
+ await Shell.Current.DisplayAlert("Error", $"Failed to initialize settings: {ex.Message}", "OK");
+ }
+ finally
+ {
+ IsBusy = false;
+ }
+ }
+
+ private async Task RefreshPortsAsync()
+ {
+ try
+ {
+ var ports = SerialPort.GetPortNames();
+ AvailablePorts = new ObservableCollection(ports);
+
+ if (AvailablePorts.Count > 0 && string.IsNullOrEmpty(SelectedPort))
+ {
+ SelectedPort = AvailablePorts[0];
+ }
+ }
+ catch (Exception ex)
+ {
+ Debug.WriteLine($"Error refreshing ports: {ex.Message}");
+ }
+ }
+
+ private async Task LoadSettingsAsync()
+ {
+ try
+ {
+ var config = await _configService.LoadConfigAsync();
+
+ // Port settings
+ if (!string.IsNullOrEmpty(config.LastUsedPort) && AvailablePorts.Contains(config.LastUsedPort))
+ {
+ SelectedPort = config.LastUsedPort;
+ }
+
+ // Other settings (these would be in extended config in a real implementation)
+ // For now, we're using default values
+ RefreshRate = 5.0f;
+ StartWithWindows = false;
+ MinimizeToTray = true;
+ SelectedTheme = "System Default";
+ }
+ catch (Exception ex)
+ {
+ Debug.WriteLine($"Error loading settings: {ex.Message}");
+ }
+ }
+
+ private async Task TestConnectionAsync()
+ {
+ try
+ {
+ IsBusy = true;
+
+ bool success;
+ if (IsAutoDetectEnabled)
+ {
+ success = await _serialPortService.ConnectToFirstAvailableAsync();
+ }
+ else
+ {
+ success = await _serialPortService.ConnectAsync(SelectedPort);
+ }
+
+ if (success)
+ {
+ await Shell.Current.DisplayAlert("Connection Test",
+ $"Successfully connected to PCPal device on {_serialPortService.CurrentPort}", "OK");
+ }
+ else
+ {
+ await Shell.Current.DisplayAlert("Connection Test",
+ "Failed to connect to PCPal device. Please check your connection and try again.", "OK");
+ }
+ }
+ catch (Exception ex)
+ {
+ await Shell.Current.DisplayAlert("Error", $"Connection test failed: {ex.Message}", "OK");
+ }
+ finally
+ {
+ IsBusy = false;
+ }
+ }
+
+ private async Task SaveSettingsAsync()
+ {
+ try
+ {
+ IsBusy = true;
+
+ var config = await _configService.LoadConfigAsync();
+
+ // Update settings
+ config.LastUsedPort = IsAutoDetectEnabled ? null : SelectedPort;
+
+ // Save settings
+ await _configService.SaveConfigAsync(config);
+
+ // In a real implementation, we would also:
+ // - Save refresh rate to the service configuration
+ // - Configure Windows startup settings
+ // - Configure minimize to tray behavior
+ // - Set the application theme
+
+ await Shell.Current.DisplayAlert("Success", "Settings saved successfully!", "OK");
+ }
+ catch (Exception ex)
+ {
+ await Shell.Current.DisplayAlert("Error", $"Failed to save settings: {ex.Message}", "OK");
+ }
+ finally
+ {
+ IsBusy = false;
+ }
+ }
+
+ private async Task ExportSettingsAsync()
+ {
+ try
+ {
+ // In a real implementation, we would:
+ // - Show a file picker dialog
+ // - Export settings to the selected file
+
+ await Shell.Current.DisplayAlert("Export", "This feature is not implemented in the demo.", "OK");
+ }
+ catch (Exception ex)
+ {
+ await Shell.Current.DisplayAlert("Error", $"Failed to export settings: {ex.Message}", "OK");
+ }
+ }
+
+ private async Task ImportSettingsAsync()
+ {
+ try
+ {
+ // In a real implementation, we would:
+ // - Show a file picker dialog
+ // - Import settings from the selected file
+ // - Apply the imported settings
+
+ await Shell.Current.DisplayAlert("Import", "This feature is not implemented in the demo.", "OK");
+ }
+ catch (Exception ex)
+ {
+ await Shell.Current.DisplayAlert("Error", $"Failed to import settings: {ex.Message}", "OK");
+ }
+ }
+
+ private async Task ResetSettingsAsync()
+ {
+ try
+ {
+ bool confirm = await Shell.Current.DisplayAlert("Reset Settings",
+ "Are you sure you want to reset all settings to default? This cannot be undone.",
+ "Reset", "Cancel");
+
+ if (!confirm)
+ return;
+
+ var config = new PCPal.Core.Models.DisplayConfig();
+ await _configService.SaveConfigAsync(config);
+
+ // Reload settings
+ await LoadSettingsAsync();
+
+ await Shell.Current.DisplayAlert("Success", "Settings have been reset to default.", "OK");
+ }
+ catch (Exception ex)
+ {
+ await Shell.Current.DisplayAlert("Error", $"Failed to reset settings: {ex.Message}", "OK");
+ }
+ }
+
+ private async Task RefreshServiceStatusAsync()
+ {
+ try
+ {
+ // In a real implementation, we would check the Windows Service status
+ // For now, we'll just simulate it
+ var random = new Random();
+ IsServiceRunning = random.Next(2) == 1; // 50% chance of running
+
+ ServiceStatus = IsServiceRunning ? "Running" : "Stopped";
+ }
+ catch (Exception ex)
+ {
+ ServiceStatus = "Error checking status";
+ Debug.WriteLine($"Error checking service status: {ex.Message}");
+ }
+ }
+
+ private async Task StartServiceAsync()
+ {
+ try
+ {
+ // In a real implementation, we would start the Windows Service
+ // For now, we'll just simulate it
+ await Task.Delay(500); // Simulate some delay
+
+ IsServiceRunning = true;
+ ServiceStatus = "Running";
+
+ await Shell.Current.DisplayAlert("Success", "PCPal Service has been started.", "OK");
+ }
+ catch (Exception ex)
+ {
+ await Shell.Current.DisplayAlert("Error", $"Failed to start service: {ex.Message}", "OK");
+ }
+ }
+
+ private async Task StopServiceAsync()
+ {
+ try
+ {
+ // In a real implementation, we would stop the Windows Service
+ // For now, we'll just simulate it
+ await Task.Delay(500); // Simulate some delay
+
+ IsServiceRunning = false;
+ ServiceStatus = "Stopped";
+
+ await Shell.Current.DisplayAlert("Success", "PCPal Service has been stopped.", "OK");
+ }
+ catch (Exception ex)
+ {
+ await Shell.Current.DisplayAlert("Error", $"Failed to stop service: {ex.Message}", "OK");
+ }
+ }
+
+ private async Task RestartServiceAsync()
+ {
+ try
+ {
+ // In a real implementation, we would restart the Windows Service
+ // For now, we'll just simulate it
+ await Task.Delay(1000); // Simulate some delay
+
+ IsServiceRunning = true;
+ ServiceStatus = "Running";
+
+ await Shell.Current.DisplayAlert("Success", "PCPal Service has been restarted.", "OK");
+ }
+ catch (Exception ex)
+ {
+ await Shell.Current.DisplayAlert("Error", $"Failed to restart service: {ex.Message}", "OK");
+ }
+ }
+
+ private async Task CheckForUpdatesAsync()
+ {
+ try
+ {
+ await Shell.Current.DisplayAlert("Check for Updates",
+ "You are currently running the latest version (1.0.0).", "OK");
+ }
+ catch (Exception ex)
+ {
+ await Shell.Current.DisplayAlert("Error", $"Failed to check for updates: {ex.Message}", "OK");
+ }
+ }
+
+ private void OnConnectionStatusChanged(object sender, bool isConnected)
+ {
+ MainThread.BeginInvokeOnMainThread(() =>
+ {
+ IsConnected = isConnected;
+ ConnectionStatus = isConnected ? $"Connected to {_serialPortService.CurrentPort}" : "Not connected";
+ });
+ }
+}
\ No newline at end of file
diff --git a/PCPal/Configurator/ViewModels/TftConfigViewModel.cs b/PCPal/Configurator/ViewModels/TftConfigViewModel.cs
new file mode 100644
index 0000000..780e08c
--- /dev/null
+++ b/PCPal/Configurator/ViewModels/TftConfigViewModel.cs
@@ -0,0 +1,39 @@
+using System.Windows.Input;
+
+namespace PCPal.Configurator.ViewModels;
+
+public class TftConfigViewModel : BaseViewModel
+{
+ public ICommand NotifyCommand { get; }
+
+ public TftConfigViewModel()
+ {
+ Title = "TFT Display Configuration";
+
+ // Initialize commands
+ NotifyCommand = new Command(async () => await NotifyWhenAvailableAsync());
+ }
+
+ private async Task NotifyWhenAvailableAsync()
+ {
+ // Display a prompt for the user's email
+ string result = await Shell.Current.DisplayPromptAsync(
+ "Notification Sign-up",
+ "Enter your email to be notified when TFT display support becomes available:",
+ "Subscribe",
+ "Cancel",
+ "email@example.com",
+ keyboard: Keyboard.Email);
+
+ if (!string.IsNullOrWhiteSpace(result))
+ {
+ // In a real app, we would save this email to a notification list
+
+ // Display confirmation
+ await Shell.Current.DisplayAlert(
+ "Thank You!",
+ "We'll notify you when TFT display support is ready. Your email has been registered for updates.",
+ "OK");
+ }
+ }
+}
\ No newline at end of file
diff --git a/PCPal/Configurator/Views/HelpView/HelpView.xaml b/PCPal/Configurator/Views/HelpView/HelpView.xaml
new file mode 100644
index 0000000..1f33ceb
--- /dev/null
+++ b/PCPal/Configurator/Views/HelpView/HelpView.xaml
@@ -0,0 +1,213 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/PCPal/Configurator/Views/HelpView/HelpView.xaml.cs b/PCPal/Configurator/Views/HelpView/HelpView.xaml.cs
new file mode 100644
index 0000000..7665076
--- /dev/null
+++ b/PCPal/Configurator/Views/HelpView/HelpView.xaml.cs
@@ -0,0 +1,12 @@
+using PCPal.Configurator.ViewModels;
+
+namespace PCPal.Configurator.Views;
+
+public partial class HelpView : ContentView
+{
+ public HelpView(HelpViewModel viewModel)
+ {
+ InitializeComponent();
+ BindingContext = viewModel;
+ }
+}
\ No newline at end of file
diff --git a/PCPal/Configurator/Views/LCD/LcdConfigView.xaml b/PCPal/Configurator/Views/LCD/LcdConfigView.xaml
new file mode 100644
index 0000000..f5c743c
--- /dev/null
+++ b/PCPal/Configurator/Views/LCD/LcdConfigView.xaml
@@ -0,0 +1,174 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/PCPal/Configurator/Views/LCD/LcdConfigView.xaml.cs b/PCPal/Configurator/Views/LCD/LcdConfigView.xaml.cs
new file mode 100644
index 0000000..817fb16
--- /dev/null
+++ b/PCPal/Configurator/Views/LCD/LcdConfigView.xaml.cs
@@ -0,0 +1,15 @@
+using PCPal.Configurator.ViewModels;
+
+namespace PCPal.Configurator.Views.LCD;
+
+public partial class LcdConfigView : ContentView
+{
+ public LcdConfigView(LcdConfigViewModel viewModel)
+ {
+ InitializeComponent();
+ BindingContext = viewModel;
+
+ // Initialize the view model when the view is loaded
+ this.Loaded += (s, e) => viewModel.Initialize();
+ }
+}
\ No newline at end of file
diff --git a/PCPal/Configurator/Views/OLED/OledConfigView.xaml b/PCPal/Configurator/Views/OLED/OledConfigView.xaml
new file mode 100644
index 0000000..6eebfc4
--- /dev/null
+++ b/PCPal/Configurator/Views/OLED/OledConfigView.xaml
@@ -0,0 +1,87 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/PCPal/Configurator/Views/OLED/OledConfigView.xaml.cs b/PCPal/Configurator/Views/OLED/OledConfigView.xaml.cs
new file mode 100644
index 0000000..355b78c
--- /dev/null
+++ b/PCPal/Configurator/Views/OLED/OledConfigView.xaml.cs
@@ -0,0 +1,15 @@
+using PCPal.Configurator.ViewModels;
+
+namespace PCPal.Configurator.Views.OLED;
+
+public partial class OledConfigView : ContentView
+{
+ public OledConfigView(OledConfigViewModel viewModel)
+ {
+ InitializeComponent();
+ BindingContext = viewModel;
+
+ // Initialize the view model when the view is loaded
+ this.Loaded += (s, e) => viewModel.Initialize();
+ }
+}
\ No newline at end of file
diff --git a/PCPal/Configurator/Views/OLED/OledMarkupEditorView.xaml b/PCPal/Configurator/Views/OLED/OledMarkupEditorView.xaml
new file mode 100644
index 0000000..1a23733
--- /dev/null
+++ b/PCPal/Configurator/Views/OLED/OledMarkupEditorView.xaml
@@ -0,0 +1,115 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/PCPal/Configurator/Views/OLED/OledMarkupEditorView.xaml.cs b/PCPal/Configurator/Views/OLED/OledMarkupEditorView.xaml.cs
new file mode 100644
index 0000000..27b7163
--- /dev/null
+++ b/PCPal/Configurator/Views/OLED/OledMarkupEditorView.xaml.cs
@@ -0,0 +1,28 @@
+using PCPal.Configurator.ViewModels;
+
+namespace PCPal.Configurator.Views.OLED;
+
+public partial class OledMarkupEditorView : ContentView
+{
+ private OledConfigViewModel _viewModel;
+
+ public OledMarkupEditorView()
+ {
+ InitializeComponent();
+ this.Loaded += OledMarkupEditorView_Loaded;
+ }
+
+ private void OledMarkupEditorView_Loaded(object sender, EventArgs e)
+ {
+ _viewModel = BindingContext as OledConfigViewModel;
+ }
+
+ private void Editor_TextChanged(object sender, TextChangedEventArgs e)
+ {
+ if (_viewModel != null)
+ {
+ // Notify the view model that the markup has changed
+ _viewModel.UpdatePreviewFromMarkup();
+ }
+ }
+}
\ No newline at end of file
diff --git a/PCPal/Configurator/Views/OLED/OledTemplatesView.xaml b/PCPal/Configurator/Views/OLED/OledTemplatesView.xaml
new file mode 100644
index 0000000..8b9a145
--- /dev/null
+++ b/PCPal/Configurator/Views/OLED/OledTemplatesView.xaml
@@ -0,0 +1,132 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/PCPal/Configurator/Views/OLED/OledTemplatesView.xaml.cs b/PCPal/Configurator/Views/OLED/OledTemplatesView.xaml.cs
new file mode 100644
index 0000000..dfff855
--- /dev/null
+++ b/PCPal/Configurator/Views/OLED/OledTemplatesView.xaml.cs
@@ -0,0 +1,9 @@
+namespace PCPal.Configurator.Views.OLED;
+
+public partial class OledTemplatesView : ContentView
+{
+ public OledTemplatesView()
+ {
+ InitializeComponent();
+ }
+}
\ No newline at end of file
diff --git a/PCPal/Configurator/Views/OLED/OledVisualEditorView.xaml b/PCPal/Configurator/Views/OLED/OledVisualEditorView.xaml
new file mode 100644
index 0000000..d466ed8
--- /dev/null
+++ b/PCPal/Configurator/Views/OLED/OledVisualEditorView.xaml
@@ -0,0 +1,290 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/PCPal/Configurator/Views/OLED/OledVisualEditorView.xaml.cs b/PCPal/Configurator/Views/OLED/OledVisualEditorView.xaml.cs
new file mode 100644
index 0000000..730ebf5
--- /dev/null
+++ b/PCPal/Configurator/Views/OLED/OledVisualEditorView.xaml.cs
@@ -0,0 +1,9 @@
+namespace PCPal.Configurator.Views.OLED;
+
+public partial class OledVisualEditorView : ContentView
+{
+ public OledVisualEditorView()
+ {
+ InitializeComponent();
+ }
+}
\ No newline at end of file
diff --git a/PCPal/Configurator/Views/Settings/SettingsView.xaml b/PCPal/Configurator/Views/Settings/SettingsView.xaml
new file mode 100644
index 0000000..0fa7547
--- /dev/null
+++ b/PCPal/Configurator/Views/Settings/SettingsView.xaml
@@ -0,0 +1,252 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/PCPal/Configurator/Views/Settings/SettingsView.xaml.cs b/PCPal/Configurator/Views/Settings/SettingsView.xaml.cs
new file mode 100644
index 0000000..b4444ff
--- /dev/null
+++ b/PCPal/Configurator/Views/Settings/SettingsView.xaml.cs
@@ -0,0 +1,15 @@
+using PCPal.Configurator.ViewModels;
+
+namespace PCPal.Configurator.Views;
+
+public partial class SettingsView : ContentView
+{
+ public SettingsView(SettingsViewModel viewModel)
+ {
+ InitializeComponent();
+ BindingContext = viewModel;
+
+ // Initialize the view model when the view is loaded
+ this.Loaded += (s, e) => viewModel.Initialize();
+ }
+}
\ No newline at end of file
diff --git a/PCPal/Configurator/Views/TFT/TftConfigView.xaml b/PCPal/Configurator/Views/TFT/TftConfigView.xaml
new file mode 100644
index 0000000..f49bbec
--- /dev/null
+++ b/PCPal/Configurator/Views/TFT/TftConfigView.xaml
@@ -0,0 +1,85 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/PCPal/Configurator/Views/TFT/TftConfigView.xaml.cs b/PCPal/Configurator/Views/TFT/TftConfigView.xaml.cs
new file mode 100644
index 0000000..2bbc39b
--- /dev/null
+++ b/PCPal/Configurator/Views/TFT/TftConfigView.xaml.cs
@@ -0,0 +1,12 @@
+using PCPal.Configurator.ViewModels;
+
+namespace PCPal.Configurator.Views.TFT;
+
+public partial class TftConfigView : ContentView
+{
+ public TftConfigView(TftConfigViewModel viewModel)
+ {
+ InitializeComponent();
+ this.BindingContext = viewModel;
+ }
+}
\ No newline at end of file
diff --git a/PCPal/Core/Core.csproj b/PCPal/Core/Core.csproj
new file mode 100644
index 0000000..27a08d8
--- /dev/null
+++ b/PCPal/Core/Core.csproj
@@ -0,0 +1,15 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
diff --git a/PCPal/Core/Models/DisplayConfig.cs b/PCPal/Core/Models/DisplayConfig.cs
new file mode 100644
index 0000000..970f40a
--- /dev/null
+++ b/PCPal/Core/Models/DisplayConfig.cs
@@ -0,0 +1,119 @@
+using System.Collections.ObjectModel;
+using System.Text.RegularExpressions;
+using LibreHardwareMonitor.Hardware;
+
+namespace PCPal.Core.Models;
+
+public class DisplayConfig
+{
+ // Common settings
+ public string LastUsedPort { get; set; }
+ public string ScreenType { get; set; } // "1602", "TFT4_6", or "OLED"
+
+ // LCD-specific settings
+ public string Line1Selection { get; set; }
+ public string Line1CustomText { get; set; }
+ public string Line2Selection { get; set; }
+ public string Line2CustomText { get; set; }
+ public string Line1PostText { get; set; }
+ public string Line2PostText { get; set; }
+
+ // OLED-specific settings
+ public string OledMarkup { get; set; }
+ public string LastIconDirectory { get; set; }
+
+ // TFT-specific settings (placeholder for future implementation)
+ public string TftLayout { get; set; }
+
+ // A new option for storing multiple display profiles
+ public List SavedProfiles { get; set; } = new List();
+}
+
+public class DisplayProfile
+{
+ public string Name { get; set; }
+ public string ScreenType { get; set; }
+ public string ConfigData { get; set; } // JSON serialized configuration for this profile
+}
+
+public class SensorItem
+{
+ public string Id { get; set; }
+ public string Name { get; set; }
+ public string HardwareName { get; set; }
+ public float Value { get; set; }
+ public SensorType SensorType { get; set; }
+ public string FormattedValue { get; set; }
+ public string Unit { get; set; }
+
+ public string DisplayName => $"{HardwareName} {Name}";
+ public string FullValueText => $"{FormattedValue} {Unit}";
+}
+
+public class SensorGroup
+{
+ public string Type { get; set; }
+ public string Icon { get; set; }
+ public ObservableCollection Sensors { get; set; }
+}
+
+public class OledElement
+{
+ public string Type { get; set; } // text, bar, rect, box, line, icon
+ public Dictionary Properties { get; set; } = new Dictionary();
+ public int X { get; set; }
+ public int Y { get; set; }
+
+ // Helper method to generate markup
+ public string ToMarkup()
+ {
+ switch (Type.ToLower())
+ {
+ case "text":
+ return $"{Properties.GetValueOrDefault("content", "")}";
+
+ case "bar":
+ return $"";
+
+ case "rect":
+ return $"";
+
+ case "box":
+ return $"";
+
+ case "line":
+ return $"";
+
+ case "icon":
+ return $"";
+
+ default:
+ return "";
+ }
+ }
+
+ // Static method to parse markup into element
+ public static OledElement FromMarkup(string markup)
+ {
+ var element = new OledElement();
+
+ // Simple parsing for demonstration - would need more robust implementation
+ if (markup.StartsWith("([^<]*)").Groups[1].Value;
+
+ element.X = int.Parse(x);
+ element.Y = int.Parse(y);
+ element.Properties["size"] = size;
+ element.Properties["content"] = content;
+ }
+ // Similar parsing for other element types would go here
+
+ return element;
+ }
+}
\ No newline at end of file
diff --git a/PCPal/Core/Models/PreviewElement.cs b/PCPal/Core/Models/PreviewElement.cs
new file mode 100644
index 0000000..d2602db
--- /dev/null
+++ b/PCPal/Core/Models/PreviewElement.cs
@@ -0,0 +1,180 @@
+using System.Text.RegularExpressions;
+
+namespace PCPal.Core.Models;
+
+// Base class for all preview elements
+public abstract class PreviewElement
+{
+}
+
+// Text element for displaying text on the OLED
+public class TextElement : PreviewElement
+{
+ public int X { get; set; }
+ public int Y { get; set; }
+ public int Size { get; set; } // 1, 2, or 3 for font size
+ public string Text { get; set; }
+}
+
+// Bar element for progress bars
+public class BarElement : PreviewElement
+{
+ public int X { get; set; }
+ public int Y { get; set; }
+ public int Width { get; set; }
+ public int Height { get; set; }
+ public float Value { get; set; } // 0-100
+}
+
+// Rectangle element for drawing outlines
+public class RectElement : PreviewElement
+{
+ public int X { get; set; }
+ public int Y { get; set; }
+ public int Width { get; set; }
+ public int Height { get; set; }
+ public bool Filled { get; set; } // True for filled box, false for outline
+}
+
+// Line element for drawing lines
+public class LineElement : PreviewElement
+{
+ public int X1 { get; set; }
+ public int Y1 { get; set; }
+ public int X2 { get; set; }
+ public int Y2 { get; set; }
+}
+
+// Icon element for displaying icons
+public class IconElement : PreviewElement
+{
+ public int X { get; set; }
+ public int Y { get; set; }
+ public string Name { get; set; }
+}
+
+// Simple markup parser for converting markup to preview elements
+public class MarkupParser
+{
+ private readonly Dictionary _sensorValues;
+
+ public MarkupParser(Dictionary sensorValues)
+ {
+ _sensorValues = sensorValues ?? new Dictionary();
+ }
+
+ public string ProcessVariables(string markup)
+ {
+ if (string.IsNullOrEmpty(markup))
+ return string.Empty;
+
+ // Replace variables with actual values
+ foreach (var sensor in _sensorValues)
+ {
+ // Look for {variable} syntax in the markup
+ string variablePattern = $"{{{sensor.Key}}}";
+
+ // Format value based on type (integers vs decimals)
+ string formattedValue;
+ if (Math.Abs(sensor.Value - Math.Round(sensor.Value)) < 0.01)
+ {
+ formattedValue = $"{sensor.Value:F0}";
+ }
+ else
+ {
+ formattedValue = $"{sensor.Value:F1}";
+ }
+
+ markup = markup.Replace(variablePattern, formattedValue);
+ }
+
+ return markup;
+ }
+
+ public List ParseMarkup(string markup)
+ {
+ var elements = new List();
+
+ if (string.IsNullOrEmpty(markup))
+ return elements;
+
+ // Process variables in the markup first
+ markup = ProcessVariables(markup);
+
+ // Parse text elements - Hello
+ foreach (Match match in Regex.Matches(markup, @"([^<]*)"))
+ {
+ elements.Add(new TextElement
+ {
+ X = int.Parse(match.Groups[1].Value),
+ Y = int.Parse(match.Groups[2].Value),
+ Size = match.Groups[3].Success ? int.Parse(match.Groups[3].Value) : 1,
+ Text = match.Groups[4].Value
+ });
+ }
+
+ // Parse bar elements -
+ foreach (Match match in Regex.Matches(markup, @""))
+ {
+ elements.Add(new BarElement
+ {
+ X = int.Parse(match.Groups[1].Value),
+ Y = int.Parse(match.Groups[2].Value),
+ Width = int.Parse(match.Groups[3].Value),
+ Height = int.Parse(match.Groups[4].Value),
+ Value = int.Parse(match.Groups[5].Value)
+ });
+ }
+
+ // Parse rect elements -
+ foreach (Match match in Regex.Matches(markup, @""))
+ {
+ elements.Add(new RectElement
+ {
+ X = int.Parse(match.Groups[1].Value),
+ Y = int.Parse(match.Groups[2].Value),
+ Width = int.Parse(match.Groups[3].Value),
+ Height = int.Parse(match.Groups[4].Value),
+ Filled = false
+ });
+ }
+
+ // Parse box elements -
+ foreach (Match match in Regex.Matches(markup, @""))
+ {
+ elements.Add(new RectElement
+ {
+ X = int.Parse(match.Groups[1].Value),
+ Y = int.Parse(match.Groups[2].Value),
+ Width = int.Parse(match.Groups[3].Value),
+ Height = int.Parse(match.Groups[4].Value),
+ Filled = true
+ });
+ }
+
+ // Parse line elements -
+ foreach (Match match in Regex.Matches(markup, @""))
+ {
+ elements.Add(new LineElement
+ {
+ X1 = int.Parse(match.Groups[1].Value),
+ Y1 = int.Parse(match.Groups[2].Value),
+ X2 = int.Parse(match.Groups[3].Value),
+ Y2 = int.Parse(match.Groups[4].Value)
+ });
+ }
+
+ // Parse icon elements -
+ foreach (Match match in Regex.Matches(markup, @""))
+ {
+ elements.Add(new IconElement
+ {
+ X = int.Parse(match.Groups[1].Value),
+ Y = int.Parse(match.Groups[2].Value),
+ Name = match.Groups[3].Value
+ });
+ }
+
+ return elements;
+ }
+}
\ No newline at end of file
diff --git a/PCPal/Core/Services/ConfigurationService.cs b/PCPal/Core/Services/ConfigurationService.cs
new file mode 100644
index 0000000..49778b9
--- /dev/null
+++ b/PCPal/Core/Services/ConfigurationService.cs
@@ -0,0 +1,100 @@
+using Microsoft.Extensions.Logging;
+using Newtonsoft.Json;
+using PCPal.Core.Models;
+using System.Diagnostics;
+using System.Xml;
+
+namespace PCPal.Core.Services;
+
+public interface IConfigurationService
+{
+ Task LoadConfigAsync();
+ Task SaveConfigAsync(DisplayConfig config);
+ Task GetLastIconDirectoryAsync();
+ Task SaveLastIconDirectoryAsync(string path);
+}
+
+public class ConfigurationService : IConfigurationService
+{
+ private readonly string _configPath;
+ private readonly ILogger _logger;
+
+ public ConfigurationService(ILogger logger)
+ {
+ _logger = logger;
+ _configPath = GetConfigPath();
+ }
+
+ public async Task LoadConfigAsync()
+ {
+ try
+ {
+ if (File.Exists(_configPath))
+ {
+ string json = await File.ReadAllTextAsync(_configPath);
+ var config = JsonConvert.DeserializeObject(json);
+ return config ?? new DisplayConfig();
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error loading configuration");
+ }
+
+ return new DisplayConfig();
+ }
+
+ public async Task SaveConfigAsync(DisplayConfig config)
+ {
+ try
+ {
+ string directory = Path.GetDirectoryName(_configPath);
+ if (!Directory.Exists(directory))
+ {
+ Directory.CreateDirectory(directory);
+ }
+
+ string json = JsonConvert.SerializeObject(config, Newtonsoft.Json.Formatting.Indented);
+ await File.WriteAllTextAsync(_configPath, json);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error saving configuration");
+ }
+ }
+
+ public async Task GetLastIconDirectoryAsync()
+ {
+ try
+ {
+ var config = await LoadConfigAsync();
+ return config.LastIconDirectory ?? string.Empty;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error getting last icon directory");
+ return string.Empty;
+ }
+ }
+
+ public async Task SaveLastIconDirectoryAsync(string path)
+ {
+ try
+ {
+ var config = await LoadConfigAsync();
+ config.LastIconDirectory = path;
+ await SaveConfigAsync(config);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error saving last icon directory");
+ }
+ }
+
+ private string GetConfigPath()
+ {
+ string folder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "PCPal");
+ Directory.CreateDirectory(folder);
+ return Path.Combine(folder, "config.json");
+ }
+}
\ No newline at end of file
diff --git a/PCPal/Core/Services/SensorService.cs b/PCPal/Core/Services/SensorService.cs
new file mode 100644
index 0000000..19ad090
--- /dev/null
+++ b/PCPal/Core/Services/SensorService.cs
@@ -0,0 +1,294 @@
+using LibreHardwareMonitor.Hardware;
+using PCPal.Core.Models;
+using System.Collections.ObjectModel;
+
+namespace PCPal.Core.Services;
+
+public interface ISensorService : IDisposable
+{
+ Task UpdateSensorValuesAsync();
+ Dictionary GetAllSensorValues();
+ float? GetSensorValue(string sensorId);
+ string FindFirstSensorOfType(HardwareType hardwareType, SensorType sensorType);
+ ObservableCollection GetAllSensorsGrouped();
+ string GetSensorVariableName(string hardwareName, string sensorName);
+ Task CreateExampleMarkupAsync();
+ string ProcessVariablesInMarkup(string markup);
+}
+
+public class SensorService : ISensorService
+{
+ private readonly Computer _computer;
+ private readonly Dictionary _sensorValues = new();
+ private bool _isDisposed = false;
+
+ public event EventHandler SensorValuesUpdated;
+
+ public SensorService()
+ {
+ _computer = new Computer
+ {
+ IsCpuEnabled = true,
+ IsGpuEnabled = true,
+ IsMemoryEnabled = true,
+ IsMotherboardEnabled = true,
+ IsControllerEnabled = true,
+ IsStorageEnabled = true
+ };
+ _computer.Open();
+ }
+
+ public async Task UpdateSensorValuesAsync()
+ {
+ await Task.Run(() =>
+ {
+ foreach (var hardware in _computer.Hardware)
+ {
+ hardware.Update();
+
+ foreach (var sensor in hardware.Sensors)
+ {
+ if (sensor.Value.HasValue)
+ {
+ string sensorId = GetSensorVariableName(hardware.Name, sensor.Name);
+ _sensorValues[sensorId] = sensor.Value.Value;
+ }
+ }
+ }
+
+ SensorValuesUpdated?.Invoke(this, EventArgs.Empty);
+ });
+ }
+
+ public Dictionary GetAllSensorValues()
+ {
+ return new Dictionary(_sensorValues);
+ }
+
+ public float? GetSensorValue(string sensorId)
+ {
+ if (_sensorValues.TryGetValue(sensorId, out float value))
+ {
+ return value;
+ }
+ return null;
+ }
+
+ public string FindFirstSensorOfType(HardwareType hardwareType, SensorType sensorType)
+ {
+ foreach (var hardware in _computer.Hardware)
+ {
+ if (hardware.HardwareType == hardwareType)
+ {
+ foreach (var sensor in hardware.Sensors)
+ {
+ if (sensor.SensorType == sensorType && sensor.Value.HasValue)
+ {
+ return GetSensorVariableName(hardware.Name, sensor.Name);
+ }
+ }
+ }
+ }
+ return string.Empty;
+ }
+
+ public ObservableCollection GetAllSensorsGrouped()
+ {
+ var result = new ObservableCollection();
+
+ // Group sensors by hardware type
+ var hardwareGroups = new Dictionary();
+
+ foreach (var hardware in _computer.Hardware)
+ {
+ hardware.Update();
+
+ if (!hardwareGroups.ContainsKey(hardware.HardwareType))
+ {
+ var group = new SensorGroup
+ {
+ Type = hardware.HardwareType.ToString(),
+ Icon = GetIconForHardwareType(hardware.HardwareType),
+ Sensors = new ObservableCollection()
+ };
+ hardwareGroups[hardware.HardwareType] = group;
+ result.Add(group);
+ }
+
+ var currentGroup = hardwareGroups[hardware.HardwareType];
+
+ foreach (var sensor in hardware.Sensors)
+ {
+ if (sensor.Value.HasValue)
+ {
+ string sensorId = GetSensorVariableName(hardware.Name, sensor.Name);
+ var sensorItem = new SensorItem
+ {
+ Id = sensorId,
+ Name = sensor.Name,
+ HardwareName = hardware.Name,
+ Value = sensor.Value.Value,
+ SensorType = sensor.SensorType,
+ FormattedValue = FormatSensorValue(sensor.Value.Value, sensor.SensorType),
+ Unit = GetSensorUnit(sensor.SensorType)
+ };
+
+ currentGroup.Sensors.Add(sensorItem);
+ }
+ }
+ }
+
+ return result;
+ }
+
+ public string GetSensorVariableName(string hardwareName, string sensorName)
+ {
+ string name = $"{hardwareName}_{sensorName}"
+ .Replace(" ", "_")
+ .Replace("%", "Percent")
+ .Replace("#", "Num")
+ .Replace("/", "_")
+ .Replace("\\", "_")
+ .Replace("(", "")
+ .Replace(")", "")
+ .Replace(",", "");
+
+ return name;
+ }
+
+ public static string GetSensorUnit(SensorType sensorType)
+ {
+ return sensorType switch
+ {
+ SensorType.Temperature => "°C",
+ SensorType.Load => "%",
+ SensorType.Clock => "MHz",
+ SensorType.Power => "W",
+ SensorType.Fan => "RPM",
+ SensorType.Flow => "L/h",
+ SensorType.Control => "%",
+ SensorType.Level => "%",
+ SensorType.Data => "GB",
+ _ => ""
+ };
+ }
+
+ public static string FormatSensorValue(float value, SensorType sensorType)
+ {
+ return sensorType switch
+ {
+ SensorType.Temperature => value.ToString("F1"),
+ SensorType.Clock => value.ToString("F0"),
+ SensorType.Load => value.ToString("F1"),
+ SensorType.Fan => value.ToString("F0"),
+ SensorType.Power => value.ToString("F1"),
+ SensorType.Data => (value > 1024) ? (value / 1024).ToString("F1") : value.ToString("F1"),
+ _ => value.ToString("F1")
+ };
+ }
+
+ private string GetIconForHardwareType(HardwareType type)
+ {
+ return type switch
+ {
+ HardwareType.Cpu => "icon_cpu.png",
+ HardwareType.GpuNvidia or HardwareType.GpuAmd => "icon_gpu.png",
+ HardwareType.Memory => "icon_memory.png",
+ HardwareType.Storage => "icon_storage.png",
+ HardwareType.Motherboard => "icon_motherboard.png",
+ HardwareType.Network => "icon_network.png",
+ _ => "icon_hardware.png"
+ };
+ }
+
+ public async Task CreateExampleMarkupAsync()
+ {
+ await UpdateSensorValuesAsync();
+
+ var sb = new System.Text.StringBuilder();
+
+ string cpuLoad = FindFirstSensorOfType(HardwareType.Cpu, SensorType.Load);
+ string cpuTemp = FindFirstSensorOfType(HardwareType.Cpu, SensorType.Temperature);
+ string gpuLoad = FindFirstSensorOfType(HardwareType.GpuNvidia, SensorType.Load);
+ string gpuTemp = FindFirstSensorOfType(HardwareType.GpuNvidia, SensorType.Temperature);
+ string ramUsed = FindFirstSensorOfType(HardwareType.Memory, SensorType.Data);
+
+ sb.AppendLine("System Monitor");
+
+ if (!string.IsNullOrEmpty(cpuLoad) && !string.IsNullOrEmpty(cpuTemp))
+ {
+ sb.AppendLine($"CPU: {{{cpuLoad}}}% ({{{cpuTemp}}}°C)");
+ sb.AppendLine($"");
+ }
+ else if (!string.IsNullOrEmpty(cpuLoad))
+ {
+ sb.AppendLine($"CPU: {{{cpuLoad}}}%");
+ sb.AppendLine($"");
+ }
+
+ if (!string.IsNullOrEmpty(gpuLoad) && !string.IsNullOrEmpty(gpuTemp))
+ {
+ sb.AppendLine($"GPU: {{{gpuLoad}}}% ({{{gpuTemp}}}°C)");
+ sb.AppendLine($"");
+ }
+ else if (!string.IsNullOrEmpty(gpuLoad))
+ {
+ sb.AppendLine($"GPU: {{{gpuLoad}}}%");
+ sb.AppendLine($"");
+ }
+
+ if (!string.IsNullOrEmpty(ramUsed))
+ {
+ sb.AppendLine($"RAM: {{{ramUsed}}} GB");
+ }
+
+ return sb.ToString();
+ }
+
+ public string ProcessVariablesInMarkup(string markup)
+ {
+ if (string.IsNullOrEmpty(markup))
+ return string.Empty;
+
+ // Replace variables with actual values
+ foreach (var sensor in _sensorValues)
+ {
+ // Look for {variable} syntax in the markup
+ string variablePattern = $"{{{sensor.Key}}}";
+
+ // Format value based on type (integers vs decimals)
+ string formattedValue;
+ if (Math.Abs(sensor.Value - Math.Round(sensor.Value)) < 0.01)
+ {
+ formattedValue = $"{sensor.Value:F0}";
+ }
+ else
+ {
+ formattedValue = $"{sensor.Value:F1}";
+ }
+
+ markup = markup.Replace(variablePattern, formattedValue);
+ }
+
+ return markup;
+ }
+
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ protected virtual void Dispose(bool disposing)
+ {
+ if (!_isDisposed)
+ {
+ if (disposing)
+ {
+ _computer.Close();
+ }
+
+ _isDisposed = true;
+ }
+ }
+}
\ No newline at end of file
diff --git a/PCPal/Core/Services/SerialPortService.cs b/PCPal/Core/Services/SerialPortService.cs
new file mode 100644
index 0000000..1390bbf
--- /dev/null
+++ b/PCPal/Core/Services/SerialPortService.cs
@@ -0,0 +1,239 @@
+using System.IO.Ports;
+using Microsoft.Extensions.Logging;
+
+namespace PCPal.Core.Services;
+
+public interface ISerialPortService
+{
+ bool IsConnected { get; }
+ string CurrentPort { get; }
+ Task ConnectAsync(string port);
+ Task ConnectToFirstAvailableAsync();
+ Task DisconnectAsync();
+ Task SendCommandAsync(string command);
+ Task AutoDetectDeviceAsync();
+ Task CheckConnectionAsync();
+ event EventHandler ConnectionStatusChanged;
+}
+
+public class SerialPortService : ISerialPortService
+{
+ private SerialPort _serialPort;
+ private bool _isConnected;
+ private string _currentPort;
+ private readonly ILogger _logger;
+
+ public bool IsConnected => _isConnected;
+ public string CurrentPort => _currentPort;
+
+ public event EventHandler ConnectionStatusChanged;
+
+ public SerialPortService(ILogger logger)
+ {
+ _logger = logger;
+ _isConnected = false;
+ _currentPort = string.Empty;
+ }
+
+ public async Task ConnectAsync(string port)
+ {
+ try
+ {
+ // Disconnect if already connected
+ if (_isConnected)
+ {
+ await DisconnectAsync();
+ }
+
+ // Connect to the specified port
+ _serialPort = new SerialPort(port, 115200)
+ {
+ ReadTimeout = 1000,
+ WriteTimeout = 1000
+ };
+
+ _serialPort.Open();
+
+ // Verify that this is a PCPal device
+ _serialPort.WriteLine("CMD:GET_DISPLAY_TYPE");
+ await Task.Delay(500); // Wait for response
+
+ string response = _serialPort.ReadExisting();
+ if (response.Contains("DISPLAY_TYPE:"))
+ {
+ _isConnected = true;
+ _currentPort = port;
+ ConnectionStatusChanged?.Invoke(this, true);
+ _logger.LogInformation($"Connected to device on port {port}");
+ return true;
+ }
+
+ // Not a valid PCPal device, close the connection
+ _serialPort.Close();
+ _serialPort.Dispose();
+ return false;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, $"Error connecting to port {port}");
+
+ if (_serialPort != null && _serialPort.IsOpen)
+ {
+ _serialPort.Close();
+ _serialPort.Dispose();
+ }
+
+ return false;
+ }
+ }
+
+ public async Task ConnectToFirstAvailableAsync()
+ {
+ string devicePort = await AutoDetectDeviceAsync();
+
+ if (!string.IsNullOrEmpty(devicePort))
+ {
+ return await ConnectAsync(devicePort);
+ }
+
+ return false;
+ }
+
+ public async Task DisconnectAsync()
+ {
+ if (_serialPort != null && _serialPort.IsOpen)
+ {
+ try
+ {
+ _serialPort.Close();
+ _serialPort.Dispose();
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error disconnecting from device");
+ }
+ finally
+ {
+ _serialPort = null;
+ _isConnected = false;
+ _currentPort = string.Empty;
+ ConnectionStatusChanged?.Invoke(this, false);
+ }
+ }
+
+ await Task.CompletedTask;
+ }
+
+ public async Task SendCommandAsync(string command)
+ {
+ if (!_isConnected || _serialPort == null || !_serialPort.IsOpen)
+ {
+ return false;
+ }
+
+ try
+ {
+ await Task.Run(() => _serialPort.WriteLine(command));
+ return true;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error sending command to device");
+
+ // Mark as disconnected on error
+ _isConnected = false;
+ _currentPort = string.Empty;
+ ConnectionStatusChanged?.Invoke(this, false);
+
+ return false;
+ }
+ }
+
+ public async Task AutoDetectDeviceAsync()
+ {
+ string[] ports = SerialPort.GetPortNames();
+
+ foreach (string port in ports)
+ {
+ try
+ {
+ using SerialPort testPort = new SerialPort(port, 115200)
+ {
+ ReadTimeout = 1000,
+ WriteTimeout = 1000
+ };
+
+ testPort.Open();
+ testPort.WriteLine("CMD:GET_DISPLAY_TYPE");
+
+ await Task.Delay(500); // Wait for response
+
+ string response = testPort.ReadExisting();
+ if (response.Contains("DISPLAY_TYPE:1602A") ||
+ response.Contains("DISPLAY_TYPE:OLED") ||
+ response.Contains("DISPLAY_TYPE:TFT"))
+ {
+ return port;
+ }
+
+ testPort.Close();
+ }
+ catch
+ {
+ // Skip ports that can't be opened
+ continue;
+ }
+ }
+
+ return string.Empty;
+ }
+
+ public async Task CheckConnectionAsync()
+ {
+ if (_serialPort == null || !_serialPort.IsOpen)
+ {
+ if (_isConnected)
+ {
+ _isConnected = false;
+ _currentPort = string.Empty;
+ ConnectionStatusChanged?.Invoke(this, false);
+ }
+
+ // Try to reconnect
+ return await ConnectToFirstAvailableAsync();
+ }
+
+ try
+ {
+ // Send a ping command to verify connection
+ await Task.Run(() => _serialPort.WriteLine("CMD:PING"));
+ await Task.Delay(100);
+
+ string response = _serialPort.ReadExisting();
+ bool connected = response.Contains("PONG");
+
+ if (_isConnected != connected)
+ {
+ _isConnected = connected;
+ if (!connected)
+ {
+ _currentPort = string.Empty;
+ }
+ ConnectionStatusChanged?.Invoke(this, connected);
+ }
+
+ return connected;
+ }
+ catch
+ {
+ if (_isConnected)
+ {
+ _isConnected = false;
+ _currentPort = string.Empty;
+ ConnectionStatusChanged?.Invoke(this, false);
+ }
+
+ return false;
+ }
+ }
+}
\ No newline at end of file
diff --git a/PCPal/PCPal.csproj b/PCPal/PCPal.csproj
new file mode 100644
index 0000000..1b28a01
--- /dev/null
+++ b/PCPal/PCPal.csproj
@@ -0,0 +1,9 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+
+
diff --git a/PCPal/PCPal.sln b/PCPal/PCPal.sln
new file mode 100644
index 0000000..df013bb
--- /dev/null
+++ b/PCPal/PCPal.sln
@@ -0,0 +1,33 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.13.35828.75
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Configurator", "Configurator\Configurator.csproj", "{81B2AE6B-97A5-4B1B-810F-A1CFAA70925D}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Core", "Core\Core.csproj", "{5ED06CAD-956C-C94D-BC0C-E89E30A887C2}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {81B2AE6B-97A5-4B1B-810F-A1CFAA70925D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {81B2AE6B-97A5-4B1B-810F-A1CFAA70925D}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {81B2AE6B-97A5-4B1B-810F-A1CFAA70925D}.Debug|Any CPU.Deploy.0 = Debug|Any CPU
+ {81B2AE6B-97A5-4B1B-810F-A1CFAA70925D}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {81B2AE6B-97A5-4B1B-810F-A1CFAA70925D}.Release|Any CPU.Build.0 = Release|Any CPU
+ {81B2AE6B-97A5-4B1B-810F-A1CFAA70925D}.Release|Any CPU.Deploy.0 = Release|Any CPU
+ {5ED06CAD-956C-C94D-BC0C-E89E30A887C2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {5ED06CAD-956C-C94D-BC0C-E89E30A887C2}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {5ED06CAD-956C-C94D-BC0C-E89E30A887C2}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {5ED06CAD-956C-C94D-BC0C-E89E30A887C2}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {EA0E6C05-F73F-4404-9809-6B72C0989961}
+ EndGlobalSection
+EndGlobal
diff --git a/PCPal/Program.cs b/PCPal/Program.cs
new file mode 100644
index 0000000..1760df1
--- /dev/null
+++ b/PCPal/Program.cs
@@ -0,0 +1,6 @@
+var builder = WebApplication.CreateBuilder(args);
+var app = builder.Build();
+
+app.MapGet("/", () => "Hello World!");
+
+app.Run();
diff --git a/PCPal/Properties/launchSettings.json b/PCPal/Properties/launchSettings.json
new file mode 100644
index 0000000..4b817ab
--- /dev/null
+++ b/PCPal/Properties/launchSettings.json
@@ -0,0 +1,29 @@
+{
+ "$schema": "http://json.schemastore.org/launchsettings.json",
+ "iisSettings": {
+ "windowsAuthentication": false,
+ "anonymousAuthentication": true,
+ "iisExpress": {
+ "applicationUrl": "http://localhost:2682",
+ "sslPort": 0
+ }
+ },
+ "profiles": {
+ "http": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "applicationUrl": "http://localhost:5199",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "IIS Express": {
+ "commandName": "IISExpress",
+ "launchBrowser": true,
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ }
+}
diff --git a/PCPal/appsettings.Development.json b/PCPal/appsettings.Development.json
new file mode 100644
index 0000000..0c208ae
--- /dev/null
+++ b/PCPal/appsettings.Development.json
@@ -0,0 +1,8 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ }
+}
diff --git a/PCPal/appsettings.json b/PCPal/appsettings.json
new file mode 100644
index 0000000..10f68b8
--- /dev/null
+++ b/PCPal/appsettings.json
@@ -0,0 +1,9 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "AllowedHosts": "*"
+}
diff --git a/PCPalConfigurator/Configurator/MauiProgram.cs b/PCPalConfigurator/Configurator/MauiProgram.cs
new file mode 100644
index 0000000..a2f2881
--- /dev/null
+++ b/PCPalConfigurator/Configurator/MauiProgram.cs
@@ -0,0 +1,61 @@
+using Microsoft.Extensions.Logging;
+using PCPal.Core.Services;
+using PCPal.Configurator.ViewModels;
+using PCPal.Configurator.Views;
+using PCPal.Configurator.Views.LCD;
+using PCPal.Configurator.Views.OLED;
+using PCPal.Configurator.Views.TFT;
+using PCPal.Configurator.Controls;
+
+namespace PCPal.Configurator;
+
+public static class MauiProgram
+{
+ public static MauiApp CreateMauiApp()
+ {
+ var builder = MauiApp.CreateBuilder();
+ builder
+ .UseMauiApp()
+ .ConfigureFonts(fonts =>
+ {
+ fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
+ fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
+ fonts.AddFont("Consolas.ttf", "Consolas");
+ });
+
+ // Register services
+ builder.Services.AddSingleton();
+ builder.Services.AddSingleton();
+ builder.Services.AddSingleton();
+
+ // Register views and view models
+ // LCD
+ builder.Services.AddTransient();
+ builder.Services.AddTransient();
+
+ // OLED
+ builder.Services.AddTransient();
+ builder.Services.AddTransient();
+ builder.Services.AddTransient();
+ builder.Services.AddTransient();
+ builder.Services.AddTransient();
+
+ // TFT
+ builder.Services.AddTransient();
+ builder.Services.AddTransient();
+
+ // Settings
+ builder.Services.AddTransient();
+ builder.Services.AddTransient();
+
+ // Help
+ builder.Services.AddTransient();
+ builder.Services.AddTransient();
+
+#if DEBUG
+ builder.Logging.AddDebug();
+#endif
+
+ return builder.Build();
+ }
+}
\ No newline at end of file
diff --git a/PCPalConfigurator/PCPalConfigurator.csproj b/PCPalConfigurator/PCPal.csproj
similarity index 100%
rename from PCPalConfigurator/PCPalConfigurator.csproj
rename to PCPalConfigurator/PCPal.csproj
diff --git a/PCPalConfigurator/PCPalConfigurator.csproj.user b/PCPalConfigurator/PCPal.csproj.user
similarity index 100%
rename from PCPalConfigurator/PCPalConfigurator.csproj.user
rename to PCPalConfigurator/PCPal.csproj.user
diff --git a/PCPalConfigurator/PCPalConfigurator.sln b/PCPalConfigurator/PCPal.sln
similarity index 100%
rename from PCPalConfigurator/PCPalConfigurator.sln
rename to PCPalConfigurator/PCPal.sln
diff --git a/PCPalConfigurator/Properties/Settings.Designer.cs b/PCPalConfigurator/Properties/Settings.Designer.cs
index 23018e9..5e45ea5 100644
--- a/PCPalConfigurator/Properties/Settings.Designer.cs
+++ b/PCPalConfigurator/Properties/Settings.Designer.cs
@@ -8,7 +8,7 @@
//
//------------------------------------------------------------------------------
-namespace PCPalConfigurator.Properties {
+namespace PCPal.Properties {
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
diff --git a/PCPalConfigurator/Service/Worker.cs b/PCPalConfigurator/Service/Worker.cs
new file mode 100644
index 0000000..9e50659
--- /dev/null
+++ b/PCPalConfigurator/Service/Worker.cs
@@ -0,0 +1,12 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace PCPalConfigurator.Service
+{
+ class Worker
+ {
+ }
+}