V2 rebuild - with bugs -_-

This commit is contained in:
NinjaPug
2025-04-15 14:42:29 -04:00
parent 21b6ad3d75
commit 7899ec88ed
81 changed files with 6815 additions and 1 deletions

4
.gitignore vendored
View File

@@ -5,3 +5,7 @@
/PCPalService/bin /PCPalService/bin
/PCPalService/obj /PCPalService/obj
PCPalService/service_log.txt PCPalService/service_log.txt
/PCPal/.vs
/PCPal/Configurator/obj
/PCPal/Core/bin
/PCPal/Core/obj

View File

@@ -0,0 +1,69 @@
<?xml version = "1.0" encoding = "UTF-8" ?>
<Application xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:PCPal.Configurator"
x:Class="PCPal.Configurator.App">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Resources/Styles/Colors.xaml" />
<ResourceDictionary Source="Resources/Styles/Styles.xaml" />
<ResourceDictionary Source="Resources/Styles/Converters.xaml" />
</ResourceDictionary.MergedDictionaries>
<!-- Application Colors -->
<Color x:Key="Primary">#1E88E5</Color>
<Color x:Key="PrimaryDark">#1565C0</Color>
<Color x:Key="PrimaryLight">#E3F2FD</Color>
<Color x:Key="Secondary">#CFD8DC</Color>
<Color x:Key="Accent">#03A9F4</Color>
<Color x:Key="Background">#F4F6F8</Color>
<Color x:Key="Surface">#FFFFFF</Color>
<Color x:Key="TextPrimary">#333333</Color>
<Color x:Key="TextSecondary">#555555</Color>
<Color x:Key="BorderColor">#E1E4E8</Color>
<Color x:Key="Success">#4CAF50</Color>
<Color x:Key="Error">#F44336</Color>
<Color x:Key="Warning">#FF9800</Color>
<!-- Card Style -->
<Style x:Key="CardStyle" TargetType="Border">
<Setter Property="BackgroundColor" Value="{StaticResource Surface}" />
<Setter Property="Padding" Value="16" />
<Setter Property="StrokeShape" Value="RoundRectangle 8,8,8,8" />
<Setter Property="Stroke" Value="{StaticResource BorderColor}" />
<Setter Property="StrokeThickness" Value="1" />
<Setter Property="Shadow">
<Shadow Brush="#22000000"
Offset="0,2"
Radius="4" />
</Setter>
</Style>
<!-- Button Styles -->
<Style x:Key="PrimaryButton" TargetType="Button">
<Setter Property="BackgroundColor" Value="{StaticResource Primary}" />
<Setter Property="TextColor" Value="White" />
<Setter Property="Padding" Value="20,10" />
<Setter Property="FontSize" Value="14" />
<Setter Property="FontAttributes" Value="Bold" />
</Style>
<Style x:Key="OutlineButton" TargetType="Button">
<Setter Property="BackgroundColor" Value="Transparent" />
<Setter Property="TextColor" Value="{StaticResource Primary}" />
<Setter Property="BorderColor" Value="{StaticResource Primary}" />
<Setter Property="BorderWidth" Value="1" />
<Setter Property="Padding" Value="20,10" />
<Setter Property="FontSize" Value="14" />
</Style>
<Style x:Key="TextButton" TargetType="Button">
<Setter Property="BackgroundColor" Value="Transparent" />
<Setter Property="TextColor" Value="{StaticResource Primary}" />
<Setter Property="Padding" Value="10,5" />
<Setter Property="FontSize" Value="14" />
</Style>
</ResourceDictionary>
</Application.Resources>
</Application>

View File

@@ -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;
}
}

View File

@@ -0,0 +1,72 @@
<Shell xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="PCPal.Configurator.AppShell">
<Shell.ContentTemplate>
<Grid ColumnDefinitions="220,*">
<!-- Sidebar -->
<StackLayout Grid.Column="0"
Padding="10"
Spacing="10"
BackgroundColor="{StaticResource Surface}">
<!-- App logo and title -->
<HorizontalStackLayout Spacing="10" Margin="0,10,0,20">
<Image Source="app_icon.png" HeightRequest="32" WidthRequest="32" />
<Label Text="PCPal"
FontSize="22"
FontAttributes="Bold"
TextColor="{StaticResource Primary}"
VerticalOptions="Center" />
</HorizontalStackLayout>
<!-- Navigation menu -->
<CollectionView x:Name="NavMenu"
SelectionMode="Single"
SelectionChanged="OnNavMenuSelectionChanged">
<CollectionView.ItemsSource>
<x:Array Type="{x:Type x:String}">
<x:String>1602 LCD Display</x:String>
<x:String>4.6" TFT Display</x:String>
<x:String>OLED Display</x:String>
<x:String>Settings</x:String>
<x:String>Help</x:String>
</x:Array>
</CollectionView.ItemsSource>
<CollectionView.ItemTemplate>
<DataTemplate>
<Border Padding="15,10"
StrokeShape="RoundRectangle 8,8,8,8"
BackgroundColor="{Binding Source={x:Reference NavMenu}, Path=SelectedItem, Converter={StaticResource StringMatchConverter}, ConverterParameter={Binding .}}">
<HorizontalStackLayout Spacing="10">
<Label Text="{Binding .}"
VerticalOptions="Center"
TextColor="{Binding Source={x:Reference NavMenu}, Path=SelectedItem, Converter={StaticResource StringMatchTextConverter}, ConverterParameter={Binding .}}" />
</HorizontalStackLayout>
</Border>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
<!-- Connection status -->
<StackLayout VerticalOptions="EndAndExpand" Margin="0,30,0,10">
<HorizontalStackLayout Spacing="8">
<Ellipse Fill="{Binding IsConnected, Converter={StaticResource ConnectionStatusColorConverter}}"
WidthRequest="12"
HeightRequest="12" />
<Label Text="{Binding ConnectionStatus}"
FontSize="14"
TextColor="{StaticResource TextSecondary}" />
</HorizontalStackLayout>
<Label Text="{Binding LastUpdateTime, StringFormat='Updated: {0}'}"
FontSize="12"
TextColor="{StaticResource TextSecondary}"
Margin="0,5,0,0" />
</StackLayout>
</StackLayout>
<!-- Content area -->
<ContentView Grid.Column="1" x:Name="ContentContainer" />
</Grid>
</Shell.ContentTemplate>
</Shell>

View File

@@ -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<LcdConfigView>(),
"4.6 TFT Display" => _serviceProvider.GetService<TftConfigView>(),
"OLED Display" => _serviceProvider.GetService<OledConfigView>(),
"Settings" => _serviceProvider.GetService<SettingsView>(),
"Help" => _serviceProvider.GetService<HelpView>(),
_ => null
};
if (view != null)
{
ContentContainer.Content = view;
}
}
}
private async void StartConnectivityMonitoring()
{
var serialPortService = _serviceProvider.GetService<ISerialPortService>();
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}");
}
}
}
}
}

View File

@@ -0,0 +1,99 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net8.0-android;net8.0-ios;net8.0-maccatalyst</TargetFrameworks>
<TargetFrameworks Condition="$([MSBuild]::IsOSPlatform('windows'))">$(TargetFrameworks);net8.0-windows10.0.19041.0</TargetFrameworks>
<!-- Uncomment to also build the tizen app. You will need to install tizen by following this: https://github.com/Samsung/Tizen.NET -->
<!-- <TargetFrameworks>$(TargetFrameworks);net8.0-tizen</TargetFrameworks> -->
<!-- Note for MacCatalyst:
The default runtime is maccatalyst-x64, except in Release config, in which case the default is maccatalyst-x64;maccatalyst-arm64.
When specifying both architectures, use the plural <RuntimeIdentifiers> instead of the singular <RuntimeIdentifier>.
The Mac App Store will NOT accept apps with ONLY maccatalyst-arm64 indicated;
either BOTH runtimes must be indicated or ONLY macatalyst-x64. -->
<!-- For example: <RuntimeIdentifiers>maccatalyst-x64;maccatalyst-arm64</RuntimeIdentifiers> -->
<OutputType>Exe</OutputType>
<RootNamespace>Configurator</RootNamespace>
<UseMaui>true</UseMaui>
<SingleProject>true</SingleProject>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<!-- Display name -->
<ApplicationTitle>Configurator</ApplicationTitle>
<!-- App Identifier -->
<ApplicationId>com.companyname.configurator</ApplicationId>
<!-- Versions -->
<ApplicationDisplayVersion>1.0</ApplicationDisplayVersion>
<ApplicationVersion>1</ApplicationVersion>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'ios'">11.0</SupportedOSPlatformVersion>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'maccatalyst'">13.1</SupportedOSPlatformVersion>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'android'">21.0</SupportedOSPlatformVersion>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'windows'">10.0.17763.0</SupportedOSPlatformVersion>
<TargetPlatformMinVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'windows'">10.0.17763.0</TargetPlatformMinVersion>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'tizen'">6.5</SupportedOSPlatformVersion>
</PropertyGroup>
<ItemGroup>
<!-- App Icon -->
<MauiIcon Include="Resources\AppIcon\appicon.svg" ForegroundFile="Resources\AppIcon\appiconfg.svg" Color="#512BD4" />
<!-- Splash Screen -->
<MauiSplashScreen Include="Resources\Splash\splash.svg" Color="#512BD4" BaseSize="128,128" />
<!-- Images -->
<MauiImage Include="Resources\Images\*" />
<MauiImage Update="Resources\Images\dotnet_bot.png" Resize="True" BaseSize="300,185" />
<!-- Custom Fonts -->
<MauiFont Include="Resources\Fonts\*" />
<!-- Raw Assets (also remove the "Resources\Raw" prefix) -->
<MauiAsset Include="Resources\Raw\**" LogicalName="%(RecursiveDir)%(Filename)%(Extension)" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Maui.Controls" Version="$(MauiVersion)" />
<PackageReference Include="Microsoft.Maui.Controls.Compatibility" Version="$(MauiVersion)" />
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="8.0.1" />
</ItemGroup>
<ItemGroup>
<MauiXaml Update="Resources\Styles\Converters.xaml">
<Generator>MSBuild:Compile</Generator>
</MauiXaml>
<MauiXaml Update="Views\HelpView\HelpView.xaml">
<Generator>MSBuild:Compile</Generator>
</MauiXaml>
<MauiXaml Update="Views\LCD\LcdConfigView.xaml">
<Generator>MSBuild:Compile</Generator>
</MauiXaml>
<MauiXaml Update="Views\OLED\OledConfigView.xaml">
<Generator>MSBuild:Compile</Generator>
</MauiXaml>
<MauiXaml Update="Views\OLED\OledMarkupEditorView.xaml">
<Generator>MSBuild:Compile</Generator>
</MauiXaml>
<MauiXaml Update="Views\OLED\OledTemplatesView.xaml">
<Generator>MSBuild:Compile</Generator>
</MauiXaml>
<MauiXaml Update="Views\OLED\OledVisualEditorView.xaml">
<Generator>MSBuild:Compile</Generator>
</MauiXaml>
<MauiXaml Update="Views\Settings\SettingsView.xaml">
<Generator>MSBuild:Compile</Generator>
</MauiXaml>
<MauiXaml Update="Views\TFT\TftConfigView.xaml">
<Generator>MSBuild:Compile</Generator>
</MauiXaml>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Core\Core.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<IsFirstTimeProjectOpen>False</IsFirstTimeProjectOpen>
<ActiveDebugFramework>net8.0-windows10.0.19041.0</ActiveDebugFramework>
<ActiveDebugProfile>Windows Machine</ActiveDebugProfile>
</PropertyGroup>
<ItemGroup>
<None Update="App.xaml">
<SubType>Designer</SubType>
</None>
<None Update="AppShell.xaml">
<SubType>Designer</SubType>
</None>
<None Update="MainPage.xaml">
<SubType>Designer</SubType>
</None>
<None Update="Platforms\Windows\App.xaml">
<SubType>Designer</SubType>
</None>
<None Update="Platforms\Windows\Package.appxmanifest">
<SubType>Designer</SubType>
</None>
<None Update="Resources\Styles\Colors.xaml">
<SubType>Designer</SubType>
</None>
<None Update="Resources\Styles\Styles.xaml">
<SubType>Designer</SubType>
</None>
</ItemGroup>
</Project>

View File

@@ -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<PreviewElement>),
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<PreviewElement> Elements
{
get => (IList<PreviewElement>)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);
}
}
}
}

View File

@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Configurator.MainPage">
<ScrollView>
<VerticalStackLayout
Padding="30,0"
Spacing="25">
<Image
Source="dotnet_bot.png"
HeightRequest="185"
Aspect="AspectFit"
SemanticProperties.Description="dot net bot in a race car number eight" />
<Label
Text="Hello, World!"
Style="{StaticResource Headline}"
SemanticProperties.HeadingLevel="Level1" />
<Label
Text="Welcome to &#10;.NET Multi-platform App UI"
Style="{StaticResource SubHeadline}"
SemanticProperties.HeadingLevel="Level2"
SemanticProperties.Description="Welcome to dot net Multi platform App U I" />
<Button
x:Name="CounterBtn"
Text="Click me"
SemanticProperties.Hint="Counts the number of times you click"
Clicked="OnCounterClicked"
HorizontalOptions="Fill" />
</VerticalStackLayout>
</ScrollView>
</ContentPage>

View File

@@ -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);
}
}
}

View File

@@ -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<App>()
.ConfigureFonts(fonts =>
{
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
fonts.AddFont("Consolas.ttf", "Consolas");
});
// Register services
builder.Services.AddSingleton<ISensorService, SensorService>();
builder.Services.AddSingleton<ISerialPortService, SerialPortService>();
builder.Services.AddSingleton<IConfigurationService, ConfigurationService>();
// Register views and view models
// LCD
builder.Services.AddTransient<LcdConfigView>();
builder.Services.AddTransient<LcdConfigViewModel>();
// OLED
builder.Services.AddTransient<OledConfigView>();
builder.Services.AddTransient<OledConfigViewModel>();
builder.Services.AddTransient<OledVisualEditorView>();
builder.Services.AddTransient<OledMarkupEditorView>();
builder.Services.AddTransient<OledTemplatesView>();
// TFT
builder.Services.AddTransient<TftConfigView>();
builder.Services.AddTransient<TftConfigViewModel>();
// Settings
builder.Services.AddTransient<SettingsView>();
builder.Services.AddTransient<SettingsViewModel>();
// Help
builder.Services.AddTransient<HelpView>();
builder.Services.AddTransient<HelpViewModel>();
#if DEBUG
builder.Logging.AddDebug();
#endif
return builder.Build();
}
}

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application android:allowBackup="true" android:icon="@mipmap/appicon" android:roundIcon="@mipmap/appicon_round" android:supportsRtl="true"></application>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
</manifest>

View File

@@ -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
{
}

View File

@@ -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();
}

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">#512BD4</color>
<color name="colorPrimaryDark">#2B0B98</color>
<color name="colorAccent">#2B0B98</color>
</resources>

View File

@@ -0,0 +1,10 @@
using Foundation;
using PCPal.Configurator;
namespace Configurator;
[Register("AppDelegate")]
public class AppDelegate : MauiUIApplicationDelegate
{
protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
}

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<!-- See https://aka.ms/maui-publish-app-store#add-entitlements for more information about adding entitlements.-->
<dict>
<!-- App Sandbox must be enabled to distribute a MacCatalyst app through the Mac App Store. -->
<key>com.apple.security.app-sandbox</key>
<true/>
<!-- When App Sandbox is enabled, this value is required to open outgoing network connections. -->
<key>com.apple.security.network.client</key>
<true/>
</dict>
</plist>

View File

@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<!-- The Mac App Store requires you specify if the app uses encryption. -->
<!-- Please consult https://developer.apple.com/documentation/bundleresources/information_property_list/itsappusesnonexemptencryption -->
<!-- <key>ITSAppUsesNonExemptEncryption</key> -->
<!-- Please indicate <true/> or <false/> here. -->
<!-- Specify the category for your app here. -->
<!-- Please consult https://developer.apple.com/documentation/bundleresources/information_property_list/lsapplicationcategorytype -->
<!-- <key>LSApplicationCategoryType</key> -->
<!-- <string>public.app-category.YOUR-CATEGORY-HERE</string> -->
<key>UIDeviceFamily</key>
<array>
<integer>2</integer>
</array>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>arm64</string>
</array>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>XSAppIconAssets</key>
<string>Assets.xcassets/appicon.appiconset</string>
</dict>
</plist>

View File

@@ -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));
}
}

View File

@@ -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);
}
}

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="maui-application-id-placeholder" version="0.0.0" api-version="8" xmlns="http://tizen.org/ns/packages">
<profile name="common" />
<ui-application appid="maui-application-id-placeholder" exec="Configurator.dll" multiple="false" nodisplay="false" taskmanage="true" type="dotnet" launch_mode="single">
<label>maui-application-title-placeholder</label>
<icon>maui-appicon-placeholder</icon>
<metadata key="http://tizen.org/metadata/prefer_dotnet_aot" value="true" />
</ui-application>
<shortcut-list />
<privileges>
<privilege>http://tizen.org/privilege/internet</privilege>
</privileges>
<dependencies />
<provides-appdefined-privileges />
</manifest>

View File

@@ -0,0 +1,8 @@
<maui:MauiWinUIApplication
x:Class="Configurator.WinUI.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:maui="using:Microsoft.Maui"
xmlns:local="using:Configurator.WinUI">
</maui:MauiWinUIApplication>

View File

@@ -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;
/// <summary>
/// Provides application-specific behavior to supplement the default Application class.
/// </summary>
public partial class App : MauiWinUIApplication
{
/// <summary>
/// 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().
/// </summary>
public App()
{
this.InitializeComponent();
}
protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
}

View File

@@ -0,0 +1,46 @@
<?xml version="1.0" encoding="utf-8"?>
<Package
xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
xmlns:mp="http://schemas.microsoft.com/appx/2014/phone/manifest"
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
IgnorableNamespaces="uap rescap">
<Identity Name="maui-package-name-placeholder" Publisher="CN=User Name" Version="0.0.0.0" />
<mp:PhoneIdentity PhoneProductId="7F379C91-BED0-4B10-9F4B-D5675708EFA8" PhonePublisherId="00000000-0000-0000-0000-000000000000"/>
<Properties>
<DisplayName>$placeholder$</DisplayName>
<PublisherDisplayName>User Name</PublisherDisplayName>
<Logo>$placeholder$.png</Logo>
</Properties>
<Dependencies>
<TargetDeviceFamily Name="Windows.Universal" MinVersion="10.0.17763.0" MaxVersionTested="10.0.19041.0" />
<TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.17763.0" MaxVersionTested="10.0.19041.0" />
</Dependencies>
<Resources>
<Resource Language="x-generate" />
</Resources>
<Applications>
<Application Id="App" Executable="$targetnametoken$.exe" EntryPoint="$targetentrypoint$">
<uap:VisualElements
DisplayName="$placeholder$"
Description="$placeholder$"
Square150x150Logo="$placeholder$.png"
Square44x44Logo="$placeholder$.png"
BackgroundColor="transparent">
<uap:DefaultTile Square71x71Logo="$placeholder$.png" Wide310x150Logo="$placeholder$.png" Square310x310Logo="$placeholder$.png" />
<uap:SplashScreen Image="$placeholder$.png" />
</uap:VisualElements>
</Application>
</Applications>
<Capabilities>
<rescap:Capability Name="runFullTrust" />
</Capabilities>
</Package>

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<assemblyIdentity version="1.0.0.0" name="Configurator.WinUI.app"/>
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<!-- The combination of below two tags have the following effect:
1) Per-Monitor for >= Windows 10 Anniversary Update
2) System < Windows 10 Anniversary Update
-->
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/PM</dpiAware>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2, PerMonitor</dpiAwareness>
</windowsSettings>
</application>
</assembly>

View File

@@ -0,0 +1,10 @@
using Foundation;
using PCPal.Configurator;
namespace Configurator;
[Register("AppDelegate")]
public class AppDelegate : MauiUIApplicationDelegate
{
protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
}

View File

@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UIDeviceFamily</key>
<array>
<integer>1</integer>
<integer>2</integer>
</array>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>arm64</string>
</array>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>XSAppIconAssets</key>
<string>Assets.xcassets/appicon.appiconset</string>
</dict>
</plist>

View File

@@ -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));
}
}

View File

@@ -0,0 +1,51 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
This is the minimum required version of the Apple Privacy Manifest for .NET MAUI apps.
The contents below are needed because of APIs that are used in the .NET framework and .NET MAUI SDK.
You are responsible for adding extra entries as needed for your application.
More information: https://aka.ms/maui-privacy-manifest
-->
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSPrivacyAccessedAPITypes</key>
<array>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryFileTimestamp</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>C617.1</string>
</array>
</dict>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategorySystemBootTime</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>35F9.1</string>
</array>
</dict>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryDiskSpace</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>E174.1</string>
</array>
</dict>
<!--
The entry below is only needed when you're using the Preferences API in your app.
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>CA92.1</string>
</array>
</dict> -->
</array>
</dict>
</plist>

View File

@@ -0,0 +1,8 @@
{
"profiles": {
"Windows Machine": {
"commandName": "MsixPackage",
"nativeDebugging": false
}
}
}

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="456" height="456" viewBox="0 0 456 456" version="1.1" xmlns="http://www.w3.org/2000/svg">
<rect x="0" y="0" width="456" height="456" fill="#512BD4" />
</svg>

After

Width:  |  Height:  |  Size: 228 B

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="456" height="456" viewBox="0 0 456 456" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<path d="m 105.50037,281.60863 c -2.70293,0 -5.00091,-0.90042 -6.893127,-2.70209 -1.892214,-1.84778 -2.837901,-4.04181 -2.837901,-6.58209 0,-2.58722 0.945687,-4.80389 2.837901,-6.65167 1.892217,-1.84778 4.190197,-2.77167 6.893127,-2.77167 2.74819,0 5.06798,0.92389 6.96019,2.77167 1.93749,1.84778 2.90581,4.06445 2.90581,6.65167 0,2.54028 -0.96832,4.73431 -2.90581,6.58209 -1.89221,1.80167 -4.212,2.70209 -6.96019,2.70209 z" style="fill:#ffffff;fill-rule:nonzero;stroke-width:0.838376" />
<path d="M 213.56111,280.08446 H 195.99044 L 149.69953,207.0544 c -1.17121,-1.84778 -2.14037,-3.76515 -2.90581,-5.75126 h -0.40578 c 0.36051,2.12528 0.54076,6.67515 0.54076,13.6496 v 65.13172 h -15.54349 v -99.36009 h 18.71925 l 44.7374,71.29798 c 1.89222,2.95695 3.1087,4.98917 3.64945,6.09751 h 0.26996 c -0.45021,-2.6325 -0.67573,-7.09015 -0.67573,-13.37293 v -64.02256 h 15.47557 z" style="fill:#ffffff;fill-rule:nonzero;stroke-width:0.838376" />
<path d="m 289.25134,280.08446 h -54.40052 v -99.36009 h 52.23835 v 13.99669 h -36.15411 v 28.13085 h 33.31621 v 13.9271 h -33.31621 v 29.37835 h 38.31628 z" style="fill:#ffffff;fill-rule:nonzero;stroke-width:0.838376" />
<path d="M 366.56466,194.72106 H 338.7222 v 85.3634 h -16.08423 v -85.3634 h -27.77455 v -13.99669 h 71.70124 z" style="fill:#ffffff;fill-rule:nonzero;stroke-width:0.838376" />
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

View File

@@ -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`.
<MauiAsset Include="Resources\Raw\**" LogicalName="%(RecursiveDir)%(Filename)%(Extension)" />
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();
}

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="456" height="456" viewBox="0 0 456 456" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<path d="m 105.50037,281.60863 c -2.70293,0 -5.00091,-0.90042 -6.893127,-2.70209 -1.892214,-1.84778 -2.837901,-4.04181 -2.837901,-6.58209 0,-2.58722 0.945687,-4.80389 2.837901,-6.65167 1.892217,-1.84778 4.190197,-2.77167 6.893127,-2.77167 2.74819,0 5.06798,0.92389 6.96019,2.77167 1.93749,1.84778 2.90581,4.06445 2.90581,6.65167 0,2.54028 -0.96832,4.73431 -2.90581,6.58209 -1.89221,1.80167 -4.212,2.70209 -6.96019,2.70209 z" style="fill:#ffffff;fill-rule:nonzero;stroke-width:0.838376" />
<path d="M 213.56111,280.08446 H 195.99044 L 149.69953,207.0544 c -1.17121,-1.84778 -2.14037,-3.76515 -2.90581,-5.75126 h -0.40578 c 0.36051,2.12528 0.54076,6.67515 0.54076,13.6496 v 65.13172 h -15.54349 v -99.36009 h 18.71925 l 44.7374,71.29798 c 1.89222,2.95695 3.1087,4.98917 3.64945,6.09751 h 0.26996 c -0.45021,-2.6325 -0.67573,-7.09015 -0.67573,-13.37293 v -64.02256 h 15.47557 z" style="fill:#ffffff;fill-rule:nonzero;stroke-width:0.838376" />
<path d="m 289.25134,280.08446 h -54.40052 v -99.36009 h 52.23835 v 13.99669 h -36.15411 v 28.13085 h 33.31621 v 13.9271 h -33.31621 v 29.37835 h 38.31628 z" style="fill:#ffffff;fill-rule:nonzero;stroke-width:0.838376" />
<path d="M 366.56466,194.72106 H 338.7222 v 85.3634 h -16.08423 v -85.3634 h -27.77455 v -13.99669 h 71.70124 z" style="fill:#ffffff;fill-rule:nonzero;stroke-width:0.838376" />
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -0,0 +1,45 @@
<?xml version="1.0" encoding="UTF-8" ?>
<?xaml-comp compile="true" ?>
<ResourceDictionary
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml">
<!-- Note: For Android please see also Platforms\Android\Resources\values\colors.xml -->
<Color x:Key="Primary">#512BD4</Color>
<Color x:Key="PrimaryDark">#ac99ea</Color>
<Color x:Key="PrimaryDarkText">#242424</Color>
<Color x:Key="Secondary">#DFD8F7</Color>
<Color x:Key="SecondaryDarkText">#9880e5</Color>
<Color x:Key="Tertiary">#2B0B98</Color>
<Color x:Key="White">White</Color>
<Color x:Key="Black">Black</Color>
<Color x:Key="Magenta">#D600AA</Color>
<Color x:Key="MidnightBlue">#190649</Color>
<Color x:Key="OffBlack">#1f1f1f</Color>
<Color x:Key="Gray100">#E1E1E1</Color>
<Color x:Key="Gray200">#C8C8C8</Color>
<Color x:Key="Gray300">#ACACAC</Color>
<Color x:Key="Gray400">#919191</Color>
<Color x:Key="Gray500">#6E6E6E</Color>
<Color x:Key="Gray600">#404040</Color>
<Color x:Key="Gray900">#212121</Color>
<Color x:Key="Gray950">#141414</Color>
<SolidColorBrush x:Key="PrimaryBrush" Color="{StaticResource Primary}"/>
<SolidColorBrush x:Key="SecondaryBrush" Color="{StaticResource Secondary}"/>
<SolidColorBrush x:Key="TertiaryBrush" Color="{StaticResource Tertiary}"/>
<SolidColorBrush x:Key="WhiteBrush" Color="{StaticResource White}"/>
<SolidColorBrush x:Key="BlackBrush" Color="{StaticResource Black}"/>
<SolidColorBrush x:Key="Gray100Brush" Color="{StaticResource Gray100}"/>
<SolidColorBrush x:Key="Gray200Brush" Color="{StaticResource Gray200}"/>
<SolidColorBrush x:Key="Gray300Brush" Color="{StaticResource Gray300}"/>
<SolidColorBrush x:Key="Gray400Brush" Color="{StaticResource Gray400}"/>
<SolidColorBrush x:Key="Gray500Brush" Color="{StaticResource Gray500}"/>
<SolidColorBrush x:Key="Gray600Brush" Color="{StaticResource Gray600}"/>
<SolidColorBrush x:Key="Gray900Brush" Color="{StaticResource Gray900}"/>
<SolidColorBrush x:Key="Gray950Brush" Color="{StaticResource Gray950}"/>
</ResourceDictionary>

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8" ?>
<ResourceDictionary xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:converters="clr-namespace:PCPal.Configurator.Converters">
<!-- Value Converters -->
<converters:BoolToColorConverter x:Key="BoolToColorConverter" />
<converters:BoolToTextColorConverter x:Key="BoolToTextColorConverter" />
<converters:ConnectionStatusColorConverter x:Key="ConnectionStatusColorConverter" />
<converters:StringMatchConverter x:Key="StringMatchConverter" />
<converters:InverseBoolConverter x:Key="InverseBoolConverter" />
<converters:MenuIconConverter x:Key="MenuIconConverter" />
<converters:SelectedItemColorConverter x:Key="SelectedItemColorConverter" />
<converters:SelectedTextColorConverter x:Key="SelectedTextColorConverter" />
<converters:BytesToSizeConverter x:Key="BytesToSizeConverter" />
<converters:RelativeTimeConverter x:Key="RelativeTimeConverter" />
</ResourceDictionary>

View File

@@ -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();
}
}

View File

@@ -0,0 +1,119 @@
<?xml version="1.0" encoding="UTF-8" ?>
<?xaml-comp compile="true" ?>
<ResourceDictionary
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml">
<!-- Default Styles -->
<Style TargetType="ActivityIndicator">
<Setter Property="Color" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource Primary}}" />
</Style>
<Style TargetType="Button">
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource Primary}}" />
<Setter Property="BackgroundColor" Value="{AppThemeBinding Light={StaticResource Surface}, Dark=#333333}" />
<Setter Property="FontFamily" Value="OpenSansRegular"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="CornerRadius" Value="4"/>
<Setter Property="Padding" Value="14,10"/>
<Setter Property="MinimumHeightRequest" Value="36"/>
</Style>
<Style TargetType="CheckBox">
<Setter Property="Color" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource Primary}}" />
<Setter Property="MinimumHeightRequest" Value="24"/>
<Setter Property="MinimumWidthRequest" Value="24"/>
</Style>
<Style TargetType="Frame">
<Setter Property="HasShadow" Value="False" />
<Setter Property="BorderColor" Value="{StaticResource BorderColor}" />
<Setter Property="CornerRadius" Value="8" />
<Setter Property="BackgroundColor" Value="{AppThemeBinding Light={StaticResource Surface}, Dark=#333333}" />
</Style>
<Style TargetType="Entry">
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource TextPrimary}, Dark=White}" />
<Setter Property="BackgroundColor" Value="{AppThemeBinding Light={StaticResource Surface}, Dark=#333333}" />
<Setter Property="FontFamily" Value="OpenSansRegular"/>
<Setter Property="FontSize" Value="14" />
<Setter Property="PlaceholderColor" Value="{AppThemeBinding Light=#AAAAAA, Dark=#777777}" />
<Setter Property="MinimumHeightRequest" Value="36"/>
</Style>
<Style TargetType="Label">
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource TextPrimary}, Dark=White}" />
<Setter Property="FontFamily" Value="OpenSansRegular" />
<Setter Property="FontSize" Value="14" />
</Style>
<Style TargetType="Editor">
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource TextPrimary}, Dark=White}" />
<Setter Property="BackgroundColor" Value="{AppThemeBinding Light={StaticResource Surface}, Dark=#333333}" />
<Setter Property="FontFamily" Value="OpenSansRegular"/>
<Setter Property="FontSize" Value="14" />
<Setter Property="PlaceholderColor" Value="{AppThemeBinding Light=#AAAAAA, Dark=#777777}" />
</Style>
<Style TargetType="Picker">
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource TextPrimary}, Dark=White}" />
<Setter Property="BackgroundColor" Value="{AppThemeBinding Light={StaticResource Surface}, Dark=#333333}" />
<Setter Property="FontFamily" Value="OpenSansRegular"/>
<Setter Property="FontSize" Value="14" />
<Setter Property="MinimumHeightRequest" Value="36"/>
</Style>
<Style TargetType="ProgressBar">
<Setter Property="ProgressColor" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource Primary}}" />
</Style>
<Style TargetType="RadioButton">
<Setter Property="BackgroundColor" Value="{AppThemeBinding Light={StaticResource Surface}, Dark=#333333}"/>
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource TextPrimary}, Dark=White}" />
<Setter Property="FontFamily" Value="OpenSansRegular"/>
<Setter Property="FontSize" Value="14"/>
</Style>
<Style TargetType="Slider">
<Setter Property="MinimumTrackColor" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource Primary}}" />
<Setter Property="MaximumTrackColor" Value="{AppThemeBinding Light={StaticResource Secondary}, Dark={StaticResource Secondary}}" />
<Setter Property="ThumbColor" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource Primary}}" />
</Style>
<Style TargetType="Switch">
<Setter Property="OnColor" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource Primary}}" />
<Setter Property="ThumbColor" Value="{AppThemeBinding Light={StaticResource Surface}, Dark=White}" />
</Style>
<Style TargetType="TabBar">
<Setter Property="BarBackgroundColor" Value="{AppThemeBinding Light={StaticResource Surface}, Dark=#333333}" />
<Setter Property="BarTextColor" Value="{AppThemeBinding Light=#777777, Dark=#777777}" />
<Setter Property="BarSelectedTextColor" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource Primary}}" />
</Style>
<!-- Custom Styles -->
<Style x:Key="HeaderLabelStyle" TargetType="Label">
<Setter Property="FontSize" Value="22" />
<Setter Property="FontAttributes" Value="Bold" />
<Setter Property="TextColor" Value="{StaticResource TextPrimary}" />
</Style>
<Style x:Key="SubHeaderLabelStyle" TargetType="Label">
<Setter Property="FontSize" Value="18" />
<Setter Property="FontAttributes" Value="Bold" />
<Setter Property="TextColor" Value="{StaticResource TextPrimary}" />
</Style>
<Style x:Key="BodyLabelStyle" TargetType="Label">
<Setter Property="FontSize" Value="14" />
<Setter Property="TextColor" Value="{StaticResource TextSecondary}" />
</Style>
<Style x:Key="SmallLabelStyle" TargetType="Label">
<Setter Property="FontSize" Value="12" />
<Setter Property="TextColor" Value="{StaticResource TextSecondary}" />
</Style>
</ResourceDictionary>

View File

@@ -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<T>(ref T storage, T value, [CallerMemberName] string propertyName = null)
{
if (EqualityComparer<T>.Default.Equals(storage, value))
{
return false;
}
storage = value;
OnPropertyChanged(propertyName);
return true;
}
}

View File

@@ -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<string>(Search);
OpenUrlCommand = new Command<string>(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");
}
}
}

View File

@@ -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<string> _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<string> 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<string>();
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<string>(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";
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -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<string> _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<string> _availableThemes = new();
private string _selectedTheme;
private string _serviceStatus;
private bool _isServiceRunning;
public ObservableCollection<string> 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<string> 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<string>
{
"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<string>(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";
});
}
}

View File

@@ -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");
}
}
}

View File

@@ -0,0 +1,213 @@
<?xml version="1.0" encoding="utf-8" ?>
<ContentView xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:viewmodels="clr-namespace:PCPal.Configurator.ViewModels"
x:Class="PCPal.Configurator.Views.HelpView"
x:DataType="viewmodels:HelpViewModel">
<Grid RowDefinitions="Auto,*" Padding="20">
<!-- Header -->
<VerticalStackLayout Grid.Row="0" Spacing="5" Margin="0,0,0,15">
<Label Text="Help &amp; Documentation"
FontSize="22"
FontAttributes="Bold"
TextColor="{StaticResource TextPrimary}" />
<Label Text="Learn how to use PCPal Configurator and get the most out of your hardware monitoring displays"
FontSize="14"
TextColor="{StaticResource TextSecondary}" />
<BoxView HeightRequest="1" Color="{StaticResource BorderColor}" Margin="0,10,0,0" />
</VerticalStackLayout>
<!-- Content -->
<ScrollView Grid.Row="1">
<VerticalStackLayout Spacing="20">
<!-- Getting Started Section -->
<Border Style="{StaticResource CardStyle}">
<VerticalStackLayout Spacing="10">
<Label Text="Getting Started"
FontSize="18"
FontAttributes="Bold"
TextColor="{StaticResource TextPrimary}" />
<Label Text="PCPal is a system monitoring tool that displays real-time hardware information on external display devices connected to your computer. The PCPal Configurator lets you customize what information is shown and how it appears."
TextColor="{StaticResource TextSecondary}"
TextType="Html" />
<Grid ColumnDefinitions="Auto,*" RowDefinitions="Auto,Auto,Auto" ColumnSpacing="15" RowSpacing="10">
<Label Grid.Row="0" Grid.Column="0" Text="1." FontAttributes="Bold" />
<Label Grid.Row="0" Grid.Column="1"
Text="Start by selecting the type of display you have connected (1602 LCD, TFT, or OLED)"
TextColor="{StaticResource TextSecondary}" />
<Label Grid.Row="1" Grid.Column="0" Text="2." FontAttributes="Bold" />
<Label Grid.Row="1" Grid.Column="1"
Text="Configure the display layout and choose which system metrics to show"
TextColor="{StaticResource TextSecondary}" />
<Label Grid.Row="2" Grid.Column="0" Text="3." FontAttributes="Bold" />
<Label Grid.Row="2" Grid.Column="1"
Text="Save your configuration and test the connection to your display"
TextColor="{StaticResource TextSecondary}" />
</Grid>
</VerticalStackLayout>
</Border>
<!-- LCD Help Section -->
<Border Style="{StaticResource CardStyle}">
<VerticalStackLayout Spacing="10">
<Label Text="1602 LCD Configuration"
FontSize="18"
FontAttributes="Bold"
TextColor="{StaticResource TextPrimary}" />
<Label Text="The 1602 LCD display has two lines of 16 characters each. You can configure what information is shown on each line."
TextColor="{StaticResource TextSecondary}" />
<Label Text="For each line, you can:" FontAttributes="Bold" Margin="0,10,0,5" />
<VerticalStackLayout Spacing="5" Margin="20,0,0,0">
<Label Text="• Select a data source (CPU load, temperature, memory usage, etc.)"
TextColor="{StaticResource TextSecondary}" />
<Label Text="• Add prefix text that appears before the value"
TextColor="{StaticResource TextSecondary}" />
<Label Text="• Add suffix/unit text that appears after the value"
TextColor="{StaticResource TextSecondary}" />
</VerticalStackLayout>
<Label Text="Example: Setting prefix to 'CPU' and suffix to '%' with the CPU load data source would display: 'CPU 85%'"
TextColor="{StaticResource TextSecondary}"
Margin="0,10,0,0" />
</VerticalStackLayout>
</Border>
<!-- OLED Help Section -->
<Border Style="{StaticResource CardStyle}">
<VerticalStackLayout Spacing="10">
<Label Text="OLED Display Configuration"
FontSize="18"
FontAttributes="Bold"
TextColor="{StaticResource TextPrimary}" />
<Label Text="The OLED display offers a graphical interface with 256x64 pixels. You can create custom layouts using the visual editor or markup language."
TextColor="{StaticResource TextSecondary}" />
<Label Text="Visual Editor" FontAttributes="Bold" Margin="0,10,0,5" />
<Label Text="Drag and drop elements onto the canvas and customize their properties. Available elements include text, progress bars, rectangles, lines, and icons."
TextColor="{StaticResource TextSecondary}" />
<Label Text="Markup Editor" FontAttributes="Bold" Margin="0,10,0,5" />
<Label Text="For advanced users, you can write markup code directly. The markup language supports the following elements:"
TextColor="{StaticResource TextSecondary}" />
<Frame BackgroundColor="#f8f9fa" Padding="10" CornerRadius="4" Margin="0,5,0,5">
<VerticalStackLayout Spacing="5">
<Label Text="&lt;text x=0 y=10 size=1&gt;Hello World&lt;/text&gt;"
FontFamily="Consolas"
FontSize="13" />
<Label Text="&lt;bar x=0 y=20 w=100 h=8 val=75 /&gt;"
FontFamily="Consolas"
FontSize="13" />
<Label Text="&lt;rect x=0 y=0 w=20 h=10 /&gt;"
FontFamily="Consolas"
FontSize="13" />
<Label Text="&lt;box x=0 y=0 w=20 h=10 /&gt;"
FontFamily="Consolas"
FontSize="13" />
<Label Text="&lt;line x1=0 y1=0 x2=20 y2=20 /&gt;"
FontFamily="Consolas"
FontSize="13" />
<Label Text="&lt;icon x=0 y=0 name=cpu /&gt;"
FontFamily="Consolas"
FontSize="13" />
</VerticalStackLayout>
</Frame>
<Label Text="Sensor Variables" FontAttributes="Bold" Margin="0,10,0,5" />
<Label Text="You can include live sensor data using variables in the format {SensorName}. For example:"
TextColor="{StaticResource TextSecondary}" />
<Frame BackgroundColor="#f8f9fa" Padding="10" CornerRadius="4" Margin="0,5,0,5">
<Label Text="&lt;text x=0 y=30 size=1&gt;CPU: {CPU_Core_i7_Total_Load}%&lt;/text&gt;"
FontFamily="Consolas"
FontSize="13" />
</Frame>
</VerticalStackLayout>
</Border>
<!-- Troubleshooting Section -->
<Border Style="{StaticResource CardStyle}">
<VerticalStackLayout Spacing="10">
<Label Text="Troubleshooting"
FontSize="18"
FontAttributes="Bold"
TextColor="{StaticResource TextPrimary}" />
<Grid ColumnDefinitions="*,*" RowDefinitions="Auto,Auto,Auto,Auto" ColumnSpacing="15" RowSpacing="15">
<Label Grid.Row="0" Grid.Column="0" Text="Display not detected:" FontAttributes="Bold" />
<Label Grid.Row="0" Grid.Column="1"
Text="Ensure the display is properly connected via USB and that the PCPal Service is running."
TextColor="{StaticResource TextSecondary}" />
<Label Grid.Row="1" Grid.Column="0" Text="Sensor data not showing:" FontAttributes="Bold" />
<Label Grid.Row="1" Grid.Column="1"
Text="Some hardware may not expose certain sensors. Try selecting different sensors or restart the application."
TextColor="{StaticResource TextSecondary}" />
<Label Grid.Row="2" Grid.Column="0" Text="Text not displaying correctly:" FontAttributes="Bold" />
<Label Grid.Row="2" Grid.Column="1"
Text="For LCD displays, ensure text doesn't exceed 16 characters per line. For OLED, check that elements are within the 256x64 pixel boundary."
TextColor="{StaticResource TextSecondary}" />
<Label Grid.Row="3" Grid.Column="0" Text="Service not working:" FontAttributes="Bold" />
<Label Grid.Row="3" Grid.Column="1"
Text="Check Windows Services to ensure the PCPal Service is running. If not, try restarting it or reinstalling."
TextColor="{StaticResource TextSecondary}" />
</Grid>
</VerticalStackLayout>
</Border>
<!-- Additional Resources Section -->
<Border Style="{StaticResource CardStyle}">
<VerticalStackLayout Spacing="10">
<Label Text="Additional Resources"
FontSize="18"
FontAttributes="Bold"
TextColor="{StaticResource TextPrimary}" />
<VerticalStackLayout Spacing="5">
<Label Text="Project Repository:" FontAttributes="Bold" />
<Label Text="github.com/user/pcpal"
TextColor="{StaticResource Primary}"
TextDecorations="Underline" />
<Label Text="Documentation:" FontAttributes="Bold" Margin="0,10,0,0" />
<Label Text="wiki.github.com/user/pcpal/documentation"
TextColor="{StaticResource Primary}"
TextDecorations="Underline" />
<Label Text="Report Issues:" FontAttributes="Bold" Margin="0,10,0,0" />
<Label Text="github.com/user/pcpal/issues"
TextColor="{StaticResource Primary}"
TextDecorations="Underline" />
</VerticalStackLayout>
</VerticalStackLayout>
</Border>
<!-- Version Information -->
<Label Text="PCPal Configurator v1.0.0 | © 2025"
HorizontalOptions="Center"
TextColor="{StaticResource TextSecondary}"
FontSize="12"
Margin="0,0,0,20" />
</VerticalStackLayout>
</ScrollView>
</Grid>
</ContentView>

View File

@@ -0,0 +1,12 @@
using PCPal.Configurator.ViewModels;
namespace PCPal.Configurator.Views;
public partial class HelpView : ContentView
{
public HelpView(HelpViewModel viewModel)
{
InitializeComponent();
BindingContext = viewModel;
}
}

View File

@@ -0,0 +1,174 @@
<?xml version="1.0" encoding="utf-8" ?>
<ContentView xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:viewmodels="clr-namespace:PCPal.Configurator.ViewModels"
x:Class="PCPal.Configurator.Views.LCD.LcdConfigView"
x:DataType="viewmodels:LcdConfigViewModel">
<Grid RowDefinitions="Auto,*,Auto" Padding="20">
<!-- Header -->
<VerticalStackLayout Grid.Row="0" Spacing="5" Margin="0,0,0,15">
<Label Text="1602 LCD Configuration"
FontSize="22"
FontAttributes="Bold"
TextColor="{StaticResource TextPrimary}" />
<Label Text="Configure what information will be displayed on your 16x2 character LCD"
FontSize="14"
TextColor="{StaticResource TextSecondary}" />
<BoxView HeightRequest="1" Color="{StaticResource BorderColor}" Margin="0,10,0,0" />
</VerticalStackLayout>
<!-- Configuration Content -->
<ScrollView Grid.Row="1">
<VerticalStackLayout Spacing="20">
<!-- Line 1 Configuration -->
<Border Style="{StaticResource CardStyle}">
<VerticalStackLayout Spacing="15">
<Label Text="Line 1"
FontSize="18"
FontAttributes="Bold"
TextColor="{StaticResource TextPrimary}" />
<Grid ColumnDefinitions="Auto,*" RowDefinitions="Auto,Auto,Auto" ColumnSpacing="15" RowSpacing="15">
<!-- Data Source -->
<Label Grid.Row="0" Grid.Column="0"
Text="Data Source:"
VerticalOptions="Center" />
<Picker Grid.Row="0" Grid.Column="1"
ItemsSource="{Binding SensorOptions}"
SelectedItem="{Binding Line1Selection}"
Title="Select data source"
FontSize="14" />
<!-- Prefix Text -->
<Label Grid.Row="1" Grid.Column="0"
Text="Prefix Text:"
VerticalOptions="Center" />
<Entry Grid.Row="1" Grid.Column="1"
Text="{Binding Line1CustomText}"
Placeholder="Text before value (e.g. 'CPU ')"
IsEnabled="{Binding IsLine1CustomTextEnabled}" />
<!-- Suffix/Units -->
<Label Grid.Row="2" Grid.Column="0"
Text="Suffix/Units:"
VerticalOptions="Center" />
<Entry Grid.Row="2" Grid.Column="1"
Text="{Binding Line1PostText}"
Placeholder="Text after value (e.g. '%')"
IsEnabled="{Binding IsLine1PostTextEnabled}" />
</Grid>
</VerticalStackLayout>
</Border>
<!-- Line 2 Configuration -->
<Border Style="{StaticResource CardStyle}">
<VerticalStackLayout Spacing="15">
<Label Text="Line 2"
FontSize="18"
FontAttributes="Bold"
TextColor="{StaticResource TextPrimary}" />
<Grid ColumnDefinitions="Auto,*" RowDefinitions="Auto,Auto,Auto" ColumnSpacing="15" RowSpacing="15">
<!-- Data Source -->
<Label Grid.Row="0" Grid.Column="0"
Text="Data Source:"
VerticalOptions="Center" />
<Picker Grid.Row="0" Grid.Column="1"
ItemsSource="{Binding SensorOptions}"
SelectedItem="{Binding Line2Selection}"
Title="Select data source"
FontSize="14" />
<!-- Prefix Text -->
<Label Grid.Row="1" Grid.Column="0"
Text="Prefix Text:"
VerticalOptions="Center" />
<Entry Grid.Row="1" Grid.Column="1"
Text="{Binding Line2CustomText}"
Placeholder="Text before value (e.g. 'RAM ')"
IsEnabled="{Binding IsLine2CustomTextEnabled}" />
<!-- Suffix/Units -->
<Label Grid.Row="2" Grid.Column="0"
Text="Suffix/Units:"
VerticalOptions="Center" />
<Entry Grid.Row="2" Grid.Column="1"
Text="{Binding Line2PostText}"
Placeholder="Text after value (e.g. 'GB')"
IsEnabled="{Binding IsLine2PostTextEnabled}" />
</Grid>
</VerticalStackLayout>
</Border>
<!-- Live Preview -->
<Border Style="{StaticResource CardStyle}">
<VerticalStackLayout Spacing="15">
<Label Text="Live Preview"
FontSize="18"
FontAttributes="Bold"
TextColor="{StaticResource TextPrimary}" />
<Frame BackgroundColor="#00192f"
BorderColor="#222222"
CornerRadius="4"
Padding="0"
HorizontalOptions="Center">
<Grid RowDefinitions="Auto,Auto" WidthRequest="580" HeightRequest="160">
<Rectangle Grid.Row="0"
Fill="#001422"
HeightRequest="40"
Margin="20,20,20,10" />
<Label Grid.Row="0"
Text="{Binding Line1Preview}"
FontFamily="Consolas"
FontSize="28"
TextColor="#00ff00"
HorizontalOptions="Start"
VerticalOptions="Center"
Margin="25,20,0,10" />
<Rectangle Grid.Row="1"
Fill="#001422"
HeightRequest="40"
Margin="20,10,20,20" />
<Label Grid.Row="1"
Text="{Binding Line2Preview}"
FontFamily="Consolas"
FontSize="28"
TextColor="#00ff00"
HorizontalOptions="Start"
VerticalOptions="Center"
Margin="25,10,0,20" />
</Grid>
</Frame>
</VerticalStackLayout>
</Border>
</VerticalStackLayout>
</ScrollView>
<!-- Action Buttons -->
<HorizontalStackLayout Grid.Row="2" Spacing="15" HorizontalOptions="End" Margin="0,15,0,0">
<Button Text="Test Connection"
Style="{StaticResource OutlineButton}"
Command="{Binding TestConnectionCommand}" />
<Button Text="Save Configuration"
Style="{StaticResource PrimaryButton}"
Command="{Binding SaveConfigCommand}" />
</HorizontalStackLayout>
</Grid>
</ContentView>

View File

@@ -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();
}
}

View File

@@ -0,0 +1,87 @@
<?xml version="1.0" encoding="utf-8" ?>
<ContentView xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:viewmodels="clr-namespace:PCPal.Configurator.ViewModels"
xmlns:oled="clr-namespace:PCPal.Configurator.Views.OLED"
x:Class="PCPal.Configurator.Views.OLED.OledConfigView"
x:DataType="viewmodels:OledConfigViewModel">
<Grid RowDefinitions="Auto,Auto,*,Auto" Padding="20">
<!-- Header -->
<VerticalStackLayout Grid.Row="0" Spacing="5" Margin="0,0,0,15">
<Label Text="OLED Display Configuration"
FontSize="22"
FontAttributes="Bold"
TextColor="{StaticResource TextPrimary}" />
<Label Text="Design your custom OLED display layout with the visual editor or markup"
FontSize="14"
TextColor="{StaticResource TextSecondary}" />
<BoxView HeightRequest="1" Color="{StaticResource BorderColor}" Margin="0,10,0,0" />
</VerticalStackLayout>
<!-- Tabs -->
<HorizontalStackLayout Grid.Row="1" Spacing="0">
<Border BackgroundColor="{Binding IsVisualEditorSelected, Converter={StaticResource BoolToColorConverter}, ConverterParameter={StaticResource Primary}}"
StrokeShape="RoundRectangle 4,4,0,0"
StrokeThickness="1"
Stroke="{StaticResource BorderColor}"
Padding="20,10"
WidthRequest="160">
<Label Text="Visual Editor"
TextColor="{Binding IsVisualEditorSelected, Converter={StaticResource BoolToColorConverter}, ConverterParameter=White}"
HorizontalOptions="Center" />
<Border.GestureRecognizers>
<TapGestureRecognizer Command="{Binding SwitchToVisualEditorCommand}" />
</Border.GestureRecognizers>
</Border>
<Border BackgroundColor="{Binding IsMarkupEditorSelected, Converter={StaticResource BoolToColorConverter}, ConverterParameter={StaticResource Primary}}"
StrokeShape="RoundRectangle 4,4,0,0"
StrokeThickness="1"
Stroke="{StaticResource BorderColor}"
Padding="20,10"
WidthRequest="160">
<Label Text="Markup Editor"
TextColor="{Binding IsMarkupEditorSelected, Converter={StaticResource BoolToColorConverter}, ConverterParameter=White}"
HorizontalOptions="Center" />
<Border.GestureRecognizers>
<TapGestureRecognizer Command="{Binding SwitchToMarkupEditorCommand}" />
</Border.GestureRecognizers>
</Border>
<Border BackgroundColor="{Binding IsTemplatesSelected, Converter={StaticResource BoolToColorConverter}, ConverterParameter={StaticResource Primary}}"
StrokeShape="RoundRectangle 4,4,0,0"
StrokeThickness="1"
Stroke="{StaticResource BorderColor}"
Padding="20,10"
WidthRequest="160">
<Label Text="Templates"
TextColor="{Binding IsTemplatesSelected, Converter={StaticResource BoolToColorConverter}, ConverterParameter=White}"
HorizontalOptions="Center" />
<Border.GestureRecognizers>
<TapGestureRecognizer Command="{Binding SwitchToTemplatesCommand}" />
</Border.GestureRecognizers>
</Border>
</HorizontalStackLayout>
<!-- Content based on selected tab -->
<ContentView Grid.Row="2" Content="{Binding CurrentView}" />
<!-- Action Buttons -->
<HorizontalStackLayout Grid.Row="3" Spacing="15" HorizontalOptions="End" Margin="0,15,0,0">
<Button Text="Reset"
Style="{StaticResource OutlineButton}"
Command="{Binding ResetCommand}" />
<Button Text="Preview"
Style="{StaticResource OutlineButton}"
Command="{Binding PreviewCommand}" />
<Button Text="Save Configuration"
Style="{StaticResource PrimaryButton}"
Command="{Binding SaveConfigCommand}" />
</HorizontalStackLayout>
</Grid>
</ContentView>

View File

@@ -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();
}
}

View File

@@ -0,0 +1,115 @@
<?xml version="1.0" encoding="utf-8" ?>
<ContentView xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:viewmodels="clr-namespace:PCPal.Configurator.ViewModels"
xmlns:controls="clr-namespace:PCPal.Configurator.Controls"
x:Class="PCPal.Configurator.Views.OLED.OledMarkupEditorView">
<Grid ColumnDefinitions="*,Auto" RowDefinitions="*,Auto" Margin="0,15,0,0">
<!-- Markup Editor Area -->
<Border Grid.Column="0" Grid.Row="0" Style="{StaticResource CardStyle}" Padding="0">
<Grid RowDefinitions="Auto,*">
<Label Text="OLED Markup"
Margin="15,10"
FontSize="16"
FontAttributes="Bold" />
<Editor Grid.Row="1"
Text="{Binding OledMarkup}"
FontFamily="Consolas"
FontSize="14"
Margin="10,0,10,10"
AutoSize="TextChanges"
TextChanged="Editor_TextChanged" />
</Grid>
</Border>
<!-- Preview Area -->
<Border Grid.Column="1" Grid.Row="0" Style="{StaticResource CardStyle}" Margin="15,0,0,0">
<Grid RowDefinitions="Auto,*">
<Label Text="Live Preview"
Margin="5,10"
FontSize="16"
FontAttributes="Bold" />
<Grid Grid.Row="1" BackgroundColor="Black" Padding="10">
<controls:OledPreviewCanvas Elements="{Binding PreviewElements}"
Width="256"
Height="64"
HorizontalOptions="Center"
VerticalOptions="Center"
Scale="2"
IsEditable="False" />
</Grid>
</Grid>
</Border>
<!-- Helper Panel -->
<Border Grid.Column="0" Grid.ColumnSpan="2" Grid.Row="1" Style="{StaticResource CardStyle}" Margin="0,15,0,0">
<Grid ColumnDefinitions="*,*,*" RowDefinitions="Auto,*">
<Label Text="Markup Tags Reference"
Grid.Column="0"
FontSize="16"
FontAttributes="Bold" />
<Label Text="Sensor Variables"
Grid.Column="1"
FontSize="16"
FontAttributes="Bold" />
<Label Text="Quick Insert"
Grid.Column="2"
FontSize="16"
FontAttributes="Bold" />
<ScrollView Grid.Row="1" Grid.Column="0" MaximumHeightRequest="150" Margin="0,10,10,0">
<VerticalStackLayout Spacing="5">
<Label Text="Text: &lt;text x=0 y=10 size=1&gt;Hello&lt;/text&gt;" FontFamily="Consolas" FontSize="12" />
<Label Text="Bar: &lt;bar x=0 y=20 w=100 h=8 val=75 /&gt;" FontFamily="Consolas" FontSize="12" />
<Label Text="Rectangle: &lt;rect x=0 y=0 w=20 h=10 /&gt;" FontFamily="Consolas" FontSize="12" />
<Label Text="Filled Box: &lt;box x=0 y=0 w=20 h=10 /&gt;" FontFamily="Consolas" FontSize="12" />
<Label Text="Line: &lt;line x1=0 y1=0 x2=20 y2=20 /&gt;" FontFamily="Consolas" FontSize="12" />
<Label Text="Icon: &lt;icon x=0 y=0 name=cpu /&gt;" FontFamily="Consolas" FontSize="12" />
</VerticalStackLayout>
</ScrollView>
<ScrollView Grid.Row="1" Grid.Column="1" MaximumHeightRequest="150" Margin="0,10,10,0">
<CollectionView ItemsSource="{Binding AvailableSensors}">
<CollectionView.ItemTemplate>
<DataTemplate>
<Grid ColumnDefinitions="*,Auto" Padding="5">
<Label Text="{Binding Id, StringFormat='{{{0}}}'}"
FontFamily="Consolas"
FontSize="12"
VerticalOptions="Center" />
<Label Grid.Column="1"
Text="{Binding FullValueText}"
FontSize="12"
VerticalOptions="Center" />
</Grid>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
</ScrollView>
<VerticalStackLayout Grid.Row="1" Grid.Column="2" Spacing="10" Margin="0,10,0,0">
<Button Text="Insert Text Element"
Command="{Binding InsertMarkupCommand}"
CommandParameter="text" />
<Button Text="Insert Progress Bar"
Command="{Binding InsertMarkupCommand}"
CommandParameter="bar" />
<Button Text="Insert Sensor Variable"
Command="{Binding InsertSensorVariableCommand}" />
<Button Text="Load Example"
Command="{Binding LoadExampleCommand}" />
</VerticalStackLayout>
</Grid>
</Border>
</Grid>
</ContentView>

View File

@@ -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();
}
}
}

View File

@@ -0,0 +1,132 @@
<?xml version="1.0" encoding="utf-8" ?>
<ContentView xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:viewmodels="clr-namespace:PCPal.Configurator.ViewModels"
xmlns:controls="clr-namespace:PCPal.Configurator.Controls"
x:Class="PCPal.Configurator.Views.OLED.OledTemplatesView">
<Grid Margin="0,15,0,0">
<ScrollView>
<VerticalStackLayout Spacing="20">
<Label Text="Select a Template to Start With"
FontSize="18"
FontAttributes="Bold" />
<!-- Template Gallery -->
<CollectionView ItemsSource="{Binding TemplateList}"
SelectionMode="Single"
SelectedItem="{Binding SelectedTemplate}">
<CollectionView.ItemsLayout>
<GridItemsLayout Orientation="Vertical"
Span="2"
VerticalItemSpacing="15"
HorizontalItemSpacing="15" />
</CollectionView.ItemsLayout>
<CollectionView.ItemTemplate>
<DataTemplate>
<Border Style="{StaticResource CardStyle}"
BackgroundColor="{Binding IsSelected, Converter={StaticResource BoolToColorConverter}, ConverterParameter={StaticResource PrimaryLight}}">
<Grid RowDefinitions="Auto,Auto,*" Padding="0">
<Label Text="{Binding Name}"
FontSize="16"
FontAttributes="Bold"
Margin="0,0,0,10" />
<Grid Grid.Row="1" BackgroundColor="Black" HeightRequest="100">
<controls:OledPreviewCanvas Elements="{Binding PreviewElements}"
Width="256"
Height="64"
HorizontalOptions="Center"
VerticalOptions="Center"
Scale="1.5"
IsEditable="False" />
</Grid>
<Label Grid.Row="2"
Text="{Binding Description}"
FontSize="12"
Margin="0,10,0,0" />
</Grid>
</Border>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
<!-- Template Details -->
<Border Style="{StaticResource CardStyle}"
IsVisible="{Binding HasSelectedTemplate}">
<Grid RowDefinitions="Auto,Auto,Auto" ColumnDefinitions="*,Auto">
<Label Text="{Binding SelectedTemplate.Name}"
FontSize="18"
FontAttributes="Bold" />
<Label Grid.Row="1"
Text="{Binding SelectedTemplate.Description}"
FontSize="14"
TextColor="{StaticResource TextSecondary}"
Margin="0,5,0,15" />
<Label Grid.Row="2"
Text="This template will replace your current design. You can customize it after applying."
FontSize="12"
TextColor="{StaticResource Warning}" />
<Button Grid.Column="1"
Grid.RowSpan="3"
Text="Use This Template"
Style="{StaticResource PrimaryButton}"
Command="{Binding UseTemplateCommand}"
VerticalOptions="Center" />
</Grid>
</Border>
<!-- Custom Templates Section -->
<BoxView HeightRequest="1" Color="{StaticResource BorderColor}" />
<Label Text="Your Custom Templates"
FontSize="18"
FontAttributes="Bold" />
<Grid ColumnDefinitions="*,Auto" RowDefinitions="Auto,Auto">
<Entry Placeholder="Save current design as template..."
Text="{Binding NewTemplateName}" />
<Button Grid.Column="1"
Text="Save"
Style="{StaticResource PrimaryButton}"
Command="{Binding SaveAsTemplateCommand}" />
<CollectionView Grid.Row="1"
Grid.ColumnSpan="2"
ItemsSource="{Binding CustomTemplates}"
EmptyView="You haven't saved any custom templates yet."
Margin="0,15,0,0">
<CollectionView.ItemTemplate>
<DataTemplate>
<Grid ColumnDefinitions="*,Auto,Auto" Padding="10">
<Label Text="{Binding Name}"
VerticalOptions="Center" />
<Button Grid.Column="1"
Text="Apply"
Style="{StaticResource OutlineButton}"
Command="{Binding Path=UseCustomTemplateCommand, Source={RelativeSource AncestorType={x:Type viewmodels:OledConfigViewModel}}}"
CommandParameter="{Binding .}"
Margin="0,0,10,0" />
<Button Grid.Column="2"
Text="Delete"
Style="{StaticResource TextButton}"
TextColor="{StaticResource Error}"
Command="{Binding Path=DeleteCustomTemplateCommand, Source={RelativeSource AncestorType={x:Type viewmodels:OledConfigViewModel}}}"
CommandParameter="{Binding .}" />
</Grid>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
</Grid>
</VerticalStackLayout>
</ScrollView>
</Grid>
</ContentView>

View File

@@ -0,0 +1,9 @@
namespace PCPal.Configurator.Views.OLED;
public partial class OledTemplatesView : ContentView
{
public OledTemplatesView()
{
InitializeComponent();
}
}

View File

@@ -0,0 +1,290 @@
<?xml version="1.0" encoding="utf-8" ?>
<ContentView xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:viewmodels="clr-namespace:PCPal.Configurator.ViewModels"
xmlns:controls="clr-namespace:PCPal.Configurator.Controls"
x:Class="PCPal.Configurator.Views.OLED.OledVisualEditorView">
<Grid ColumnDefinitions="*,250" RowDefinitions="*,Auto" Margin="0,15,0,0">
<!-- Main Editor Area -->
<Grid Grid.Column="0" Grid.Row="0" BackgroundColor="Black">
<controls:OledPreviewCanvas x:Name="EditorCanvas"
Elements="{Binding OledElements}"
Width="256"
Height="64"
HorizontalOptions="Center"
VerticalOptions="Center"
Scale="3"
SelectedElement="{Binding SelectedElement}"
IsEditable="True" />
<!-- Show grid lines option -->
<CheckBox IsChecked="{Binding ShowGridLines}"
HorizontalOptions="Start"
VerticalOptions="Start"
Margin="5"
Color="White" />
<Label Text="Show Grid"
TextColor="White"
HorizontalOptions="Start"
VerticalOptions="Start"
Margin="35,10,0,0"
FontSize="12" />
<!-- Zoom controls -->
<HorizontalStackLayout HorizontalOptions="End" VerticalOptions="Start" Margin="0,10,10,0">
<Button Text="-"
Command="{Binding ZoomOutCommand}"
WidthRequest="40"
HeightRequest="40"
FontSize="16"
Padding="0" />
<Label Text="{Binding ZoomLevel, StringFormat='{0}x'}"
TextColor="White"
VerticalOptions="Center"
Margin="10,0"
FontSize="14" />
<Button Text="+"
Command="{Binding ZoomInCommand}"
WidthRequest="40"
HeightRequest="40"
FontSize="16"
Padding="0" />
</HorizontalStackLayout>
</Grid>
<!-- Component Palette -->
<Border Grid.Column="1"
Grid.Row="0"
Style="{StaticResource CardStyle}"
Margin="15,0,0,0">
<VerticalStackLayout Spacing="15">
<Label Text="Components"
FontSize="18"
FontAttributes="Bold" />
<!-- Component buttons -->
<Button Text="Add Text"
Command="{Binding AddElementCommand}"
CommandParameter="text"
HorizontalOptions="Fill" />
<Button Text="Add Progress Bar"
Command="{Binding AddElementCommand}"
CommandParameter="bar"
HorizontalOptions="Fill" />
<Button Text="Add Rectangle"
Command="{Binding AddElementCommand}"
CommandParameter="rect"
HorizontalOptions="Fill" />
<Button Text="Add Filled Box"
Command="{Binding AddElementCommand}"
CommandParameter="box"
HorizontalOptions="Fill" />
<Button Text="Add Line"
Command="{Binding AddElementCommand}"
CommandParameter="line"
HorizontalOptions="Fill" />
<Button Text="Add Icon"
Command="{Binding AddElementCommand}"
CommandParameter="icon"
HorizontalOptions="Fill" />
<!-- Selected element properties -->
<BoxView HeightRequest="1"
Color="{StaticResource BorderColor}"
Margin="0,10" />
<Label Text="Element Properties"
FontSize="16"
FontAttributes="Bold" />
<!-- Dynamic properties display based on selected element type -->
<VerticalStackLayout x:Name="ElementPropertiesPanel"
Spacing="10"
IsVisible="{Binding HasSelectedElement}">
<!-- Common position properties -->
<Grid ColumnDefinitions="Auto,*,Auto,*" RowDefinitions="Auto" ColumnSpacing="10" RowSpacing="10">
<Label Grid.Column="0" Text="X:" VerticalOptions="Center" />
<Entry Grid.Column="1"
Text="{Binding SelectedElementX}"
Keyboard="Numeric"
HorizontalTextAlignment="End" />
<Label Grid.Column="2" Text="Y:" VerticalOptions="Center" />
<Entry Grid.Column="3"
Text="{Binding SelectedElementY}"
Keyboard="Numeric"
HorizontalTextAlignment="End" />
</Grid>
<!-- Type-specific properties - shown/hidden based on element type -->
<!-- Text Properties -->
<VerticalStackLayout IsVisible="{Binding IsTextElementSelected}" Spacing="10">
<Label Text="Text Content:" />
<Entry Text="{Binding SelectedElementText}" />
<Label Text="Font Size:" />
<Picker SelectedItem="{Binding SelectedElementSize}"
ItemsSource="{Binding FontSizes}" />
</VerticalStackLayout>
<!-- Bar Properties -->
<VerticalStackLayout IsVisible="{Binding IsBarElementSelected}" Spacing="10">
<Label Text="Width:" />
<Entry Text="{Binding SelectedElementWidth}" Keyboard="Numeric" />
<Label Text="Height:" />
<Entry Text="{Binding SelectedElementHeight}" Keyboard="Numeric" />
<Label Text="Value (0-100):" />
<HorizontalStackLayout>
<Slider Value="{Binding SelectedElementValue}"
Maximum="100"
Minimum="0"
WidthRequest="150" />
<Label Text="{Binding SelectedElementValue, StringFormat='{0:F0}'}"
VerticalOptions="Center"
Margin="10,0,0,0" />
</HorizontalStackLayout>
<Label Text="Link to Sensor:" />
<Picker SelectedItem="{Binding SelectedElementSensor}"
ItemsSource="{Binding AvailableSensors}" />
</VerticalStackLayout>
<!-- Rectangle/Box Properties -->
<VerticalStackLayout IsVisible="{Binding IsRectangleElementSelected}" Spacing="10">
<Label Text="Width:" />
<Entry Text="{Binding SelectedElementWidth}" Keyboard="Numeric" />
<Label Text="Height:" />
<Entry Text="{Binding SelectedElementHeight}" Keyboard="Numeric" />
</VerticalStackLayout>
<!-- Line Properties -->
<VerticalStackLayout IsVisible="{Binding IsLineElementSelected}" Spacing="10">
<Label Text="End Point X:" />
<Entry Text="{Binding SelectedElementX2}" Keyboard="Numeric" />
<Label Text="End Point Y:" />
<Entry Text="{Binding SelectedElementY2}" Keyboard="Numeric" />
</VerticalStackLayout>
<!-- Icon Properties -->
<VerticalStackLayout IsVisible="{Binding IsIconElementSelected}" Spacing="10">
<Label Text="Icon Name:" />
<HorizontalStackLayout>
<Entry Text="{Binding SelectedElementIconName}" WidthRequest="150" />
<Button Text="Browse..."
Command="{Binding BrowseIconsCommand}"
Margin="5,0,0,0" />
</HorizontalStackLayout>
</VerticalStackLayout>
<!-- Delete button -->
<Button Text="Delete Element"
Command="{Binding DeleteElementCommand}"
BackgroundColor="{StaticResource Error}"
TextColor="White"
Margin="0,10,0,0" />
</VerticalStackLayout>
</VerticalStackLayout>
</Border>
<!-- Sensor Selector -->
<Border Grid.Column="0"
Grid.ColumnSpan="2"
Grid.Row="1"
Style="{StaticResource CardStyle}"
Margin="0,15,0,0">
<VerticalStackLayout Spacing="15">
<Label Text="Available Sensors"
FontSize="18"
FontAttributes="Bold" />
<!-- Sensor category tabs -->
<HorizontalStackLayout Spacing="10" Margin="0,0,0,10">
<ScrollView Orientation="Horizontal" HorizontalScrollBarVisibility="Never">
<HorizontalStackLayout Spacing="10">
<Button Text="CPU"
Command="{Binding FilterSensorsCommand}"
CommandParameter="Cpu"
CornerRadius="15"
Padding="15,0"
MinimumWidthRequest="100"
BackgroundColor="{Binding CurrentSensorFilter, Converter={StaticResource StringMatchConverter}, ConverterParameter='Cpu'}" />
<Button Text="GPU"
Command="{Binding FilterSensorsCommand}"
CommandParameter="Gpu"
CornerRadius="15"
Padding="15,0"
MinimumWidthRequest="100"
BackgroundColor="{Binding CurrentSensorFilter, Converter={StaticResource StringMatchConverter}, ConverterParameter='Gpu'}" />
<Button Text="Memory"
Command="{Binding FilterSensorsCommand}"
CommandParameter="Memory"
CornerRadius="15"
Padding="15,0"
MinimumWidthRequest="100"
BackgroundColor="{Binding CurrentSensorFilter, Converter={StaticResource StringMatchConverter}, ConverterParameter='Memory'}" />
<Button Text="Storage"
Command="{Binding FilterSensorsCommand}"
CommandParameter="Storage"
CornerRadius="15"
Padding="15,0"
MinimumWidthRequest="100"
BackgroundColor="{Binding CurrentSensorFilter, Converter={StaticResource StringMatchConverter}, ConverterParameter='Storage'}" />
<Button Text="All"
Command="{Binding FilterSensorsCommand}"
CommandParameter="All"
CornerRadius="15"
Padding="15,0"
MinimumWidthRequest="100"
BackgroundColor="{Binding CurrentSensorFilter, Converter={StaticResource StringMatchConverter}, ConverterParameter='All'}" />
</HorizontalStackLayout>
</ScrollView>
</HorizontalStackLayout>
<!-- Sensor list -->
<CollectionView ItemsSource="{Binding FilteredSensors}"
HeightRequest="150">
<CollectionView.ItemTemplate>
<DataTemplate>
<Grid ColumnDefinitions="*,Auto,Auto"
Padding="10"
BackgroundColor="{StaticResource Surface}">
<Label Grid.Column="0"
Text="{Binding DisplayName}"
VerticalOptions="Center" />
<Label Grid.Column="1"
Text="{Binding FullValueText}"
VerticalOptions="Center"
Margin="10,0" />
<Button Grid.Column="2"
Text="Add to Display"
CommandParameter="{Binding Id}"
Command="{Binding Path=AddSensorToDisplayCommand, Source={RelativeSource AncestorType={x:Type viewmodels:OledConfigViewModel}}}" />
</Grid>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
</VerticalStackLayout>
</Border>
</Grid>
</ContentView>

View File

@@ -0,0 +1,9 @@
namespace PCPal.Configurator.Views.OLED;
public partial class OledVisualEditorView : ContentView
{
public OledVisualEditorView()
{
InitializeComponent();
}
}

View File

@@ -0,0 +1,252 @@
<?xml version="1.0" encoding="utf-8" ?>
<ContentView xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:viewmodels="clr-namespace:PCPal.Configurator.ViewModels"
x:Class="PCPal.Configurator.Views.SettingsView"
x:DataType="viewmodels:SettingsViewModel">
<Grid RowDefinitions="Auto,*" Padding="20">
<!-- Header -->
<VerticalStackLayout Grid.Row="0" Spacing="5" Margin="0,0,0,15">
<Label Text="Settings"
FontSize="22"
FontAttributes="Bold"
TextColor="{StaticResource TextPrimary}" />
<Label Text="Configure application and service preferences"
FontSize="14"
TextColor="{StaticResource TextSecondary}" />
<BoxView HeightRequest="1" Color="{StaticResource BorderColor}" Margin="0,10,0,0" />
</VerticalStackLayout>
<!-- Content -->
<ScrollView Grid.Row="1">
<VerticalStackLayout Spacing="20">
<!-- Connection Settings -->
<Border Style="{StaticResource CardStyle}">
<VerticalStackLayout Spacing="15">
<Label Text="Connection Settings"
FontSize="18"
FontAttributes="Bold"
TextColor="{StaticResource TextPrimary}" />
<Grid ColumnDefinitions="Auto,*" RowDefinitions="Auto,Auto,Auto" ColumnSpacing="15" RowSpacing="15">
<!-- Serial Port Selection -->
<Label Grid.Row="0" Grid.Column="0"
Text="COM Port:"
VerticalOptions="Center" />
<HorizontalStackLayout Grid.Row="0" Grid.Column="1" Spacing="10">
<Picker ItemsSource="{Binding AvailablePorts}"
SelectedItem="{Binding SelectedPort}"
WidthRequest="150"
IsEnabled="{Binding IsAutoDetectEnabled, Converter={StaticResource InverseBoolConverter}}" />
<CheckBox IsChecked="{Binding IsAutoDetectEnabled}"
VerticalOptions="Center" />
<Label Text="Auto-detect display"
VerticalOptions="Center" />
</HorizontalStackLayout>
<!-- Connection Test -->
<Label Grid.Row="1" Grid.Column="0"
Text="Connection:"
VerticalOptions="Center" />
<HorizontalStackLayout Grid.Row="1" Grid.Column="1" Spacing="10">
<Button Text="Test Connection"
Command="{Binding TestConnectionCommand}"
WidthRequest="150" />
<Label Text="{Binding ConnectionStatus}"
VerticalOptions="Center"
TextColor="{Binding IsConnected, Converter={StaticResource ConnectionStatusColorConverter}}" />
</HorizontalStackLayout>
<!-- Refresh Rate -->
<Label Grid.Row="2" Grid.Column="0"
Text="Refresh Rate:"
VerticalOptions="Center" />
<HorizontalStackLayout Grid.Row="2" Grid.Column="1" Spacing="10">
<Slider Value="{Binding RefreshRate}"
Maximum="10"
Minimum="1"
WidthRequest="150" />
<Label Text="{Binding RefreshRate, StringFormat='{0:F0} seconds'}"
VerticalOptions="Center"
MinimumWidthRequest="100" />
</HorizontalStackLayout>
</Grid>
</VerticalStackLayout>
</Border>
<!-- Application Settings -->
<Border Style="{StaticResource CardStyle}">
<VerticalStackLayout Spacing="15">
<Label Text="Application Settings"
FontSize="18"
FontAttributes="Bold"
TextColor="{StaticResource TextPrimary}" />
<Grid ColumnDefinitions="Auto,*" RowDefinitions="Auto,Auto,Auto" ColumnSpacing="15" RowSpacing="15">
<!-- Start with Windows -->
<Label Grid.Row="0" Grid.Column="0"
Text="Startup:"
VerticalOptions="Center" />
<HorizontalStackLayout Grid.Row="0" Grid.Column="1" Spacing="10">
<CheckBox IsChecked="{Binding StartWithWindows}"
VerticalOptions="Center" />
<Label Text="Start PCPal Service with Windows"
VerticalOptions="Center" />
</HorizontalStackLayout>
<!-- Minimize to Tray -->
<Label Grid.Row="1" Grid.Column="0"
Text="System Tray:"
VerticalOptions="Center" />
<HorizontalStackLayout Grid.Row="1" Grid.Column="1" Spacing="10">
<CheckBox IsChecked="{Binding MinimizeToTray}"
VerticalOptions="Center" />
<Label Text="Minimize to system tray when closed"
VerticalOptions="Center" />
</HorizontalStackLayout>
<!-- Theme -->
<Label Grid.Row="2" Grid.Column="0"
Text="Theme:"
VerticalOptions="Center" />
<Picker Grid.Row="2" Grid.Column="1"
ItemsSource="{Binding AvailableThemes}"
SelectedItem="{Binding SelectedTheme}"
WidthRequest="150"
HorizontalOptions="Start" />
</Grid>
</VerticalStackLayout>
</Border>
<!-- Data Management -->
<Border Style="{StaticResource CardStyle}">
<VerticalStackLayout Spacing="15">
<Label Text="Data Management"
FontSize="18"
FontAttributes="Bold"
TextColor="{StaticResource TextPrimary}" />
<Grid ColumnDefinitions="Auto,*" RowDefinitions="Auto,Auto" ColumnSpacing="15" RowSpacing="15">
<!-- Export Settings -->
<Label Grid.Row="0" Grid.Column="0"
Text="Settings:"
VerticalOptions="Center" />
<HorizontalStackLayout Grid.Row="0" Grid.Column="1" Spacing="10">
<Button Text="Export Settings"
Command="{Binding ExportSettingsCommand}" />
<Button Text="Import Settings"
Command="{Binding ImportSettingsCommand}" />
</HorizontalStackLayout>
<!-- Reset Settings -->
<Label Grid.Row="1" Grid.Column="0"
Text="Reset:"
VerticalOptions="Center" />
<Button Grid.Row="1" Grid.Column="1"
Text="Reset All Settings to Default"
Command="{Binding ResetSettingsCommand}"
BackgroundColor="{StaticResource Error}"
TextColor="White"
HorizontalOptions="Start" />
</Grid>
</VerticalStackLayout>
</Border>
<!-- Service Management -->
<Border Style="{StaticResource CardStyle}">
<VerticalStackLayout Spacing="15">
<Label Text="PCPal Service Management"
FontSize="18"
FontAttributes="Bold"
TextColor="{StaticResource TextPrimary}" />
<Grid ColumnDefinitions="Auto,*" RowDefinitions="Auto,Auto" ColumnSpacing="15" RowSpacing="15">
<!-- Service Status -->
<Label Grid.Row="0" Grid.Column="0"
Text="Status:"
VerticalOptions="Center" />
<HorizontalStackLayout Grid.Row="0" Grid.Column="1" Spacing="10">
<Label Text="{Binding ServiceStatus}"
VerticalOptions="Center"
FontAttributes="Bold"
TextColor="{Binding IsServiceRunning, Converter={StaticResource ConnectionStatusColorConverter}}" />
<Button Text="Refresh Status"
Command="{Binding RefreshServiceStatusCommand}"
Margin="20,0,0,0" />
</HorizontalStackLayout>
<!-- Service Controls -->
<Label Grid.Row="1" Grid.Column="0"
Text="Controls:"
VerticalOptions="Center" />
<HorizontalStackLayout Grid.Row="1" Grid.Column="1" Spacing="10">
<Button Text="Start Service"
Command="{Binding StartServiceCommand}"
IsEnabled="{Binding IsServiceRunning, Converter={StaticResource InverseBoolConverter}}" />
<Button Text="Stop Service"
Command="{Binding StopServiceCommand}"
IsEnabled="{Binding IsServiceRunning}" />
<Button Text="Restart Service"
Command="{Binding RestartServiceCommand}" />
</HorizontalStackLayout>
</Grid>
</VerticalStackLayout>
</Border>
<!-- About Section -->
<Border Style="{StaticResource CardStyle}">
<VerticalStackLayout Spacing="10">
<Label Text="About PCPal"
FontSize="18"
FontAttributes="Bold"
TextColor="{StaticResource TextPrimary}" />
<Label Text="PCPal is a hardware monitoring tool that displays real-time system information on external display devices."
TextColor="{StaticResource TextSecondary}" />
<VerticalStackLayout Spacing="5" Margin="0,10,0,0">
<Label Text="Version: 1.0.0" />
<Label Text="© 2025 Christopher Koch aka NinjaPug" />
<Label Text="License: MIT" />
</VerticalStackLayout>
<Button Text="Check for Updates"
Command="{Binding CheckForUpdatesCommand}"
HorizontalOptions="Start"
Margin="0,10,0,0" />
</VerticalStackLayout>
</Border>
<!-- Save Button -->
<Button Text="Save Settings"
Command="{Binding SaveSettingsCommand}"
Style="{StaticResource PrimaryButton}"
HorizontalOptions="End"
Margin="0,0,0,20" />
</VerticalStackLayout>
</ScrollView>
</Grid>
</ContentView>

View File

@@ -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();
}
}

View File

@@ -0,0 +1,85 @@
<?xml version="1.0" encoding="utf-8" ?>
<ContentView xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:viewmodels="clr-namespace:PCPal.Configurator.ViewModels"
x:Class="PCPal.Configurator.Views.TFT.TftConfigView">
<Grid RowDefinitions="Auto,*" Padding="20">
<!-- Header -->
<VerticalStackLayout Grid.Row="0" Spacing="5" Margin="0,0,0,15">
<Label Text="4.6&quot; TFT Display Configuration"
FontSize="22"
FontAttributes="Bold"
TextColor="{StaticResource TextPrimary}" />
<Label Text="Configure your TFT display layout and information"
FontSize="14"
TextColor="{StaticResource TextSecondary}" />
<BoxView HeightRequest="1" Color="{StaticResource BorderColor}" Margin="0,10,0,0" />
</VerticalStackLayout>
<!-- Content -->
<Grid Grid.Row="1" RowDefinitions="*,Auto">
<!-- Coming Soon Message -->
<Border Grid.Row="0" Style="{StaticResource CardStyle}" VerticalOptions="Center">
<Border.StrokeShape>
<RoundRectangle CornerRadius="8" />
</Border.StrokeShape>
<VerticalStackLayout Spacing="20" HorizontalOptions="Center" Padding="30">
<Image Source="coming_soon.png"
WidthRequest="150"
HeightRequest="150"
HorizontalOptions="Center" />
<Label Text="TFT Display Support Coming Soon!"
FontSize="24"
FontAttributes="Bold"
TextColor="{StaticResource Primary}"
HorizontalOptions="Center" />
<Label Text="We're actively working on TFT display support. This feature will allow you to create rich, colorful layouts for your 4.6&quot; TFT display module."
TextColor="{StaticResource TextSecondary}"
HorizontalOptions="Center"
HorizontalTextAlignment="Center"
MaxLines="3"
LineBreakMode="WordWrap" />
<Label Text="Planned features include:"
FontAttributes="Bold"
Margin="0,10,0,0"
HorizontalOptions="Center" />
<VerticalStackLayout Spacing="5" HorizontalOptions="Center">
<Label Text="• Custom themes and background images"
TextColor="{StaticResource TextSecondary}"
HorizontalOptions="Start" />
<Label Text="• Multiple screen layouts with animation"
TextColor="{StaticResource TextSecondary}"
HorizontalOptions="Start" />
<Label Text="• Rich graphing and visualization capabilities"
TextColor="{StaticResource TextSecondary}"
HorizontalOptions="Start" />
<Label Text="• Touch screen controls and interaction"
TextColor="{StaticResource TextSecondary}"
HorizontalOptions="Start" />
</VerticalStackLayout>
<Button Text="Notify Me When Available"
Command="{Binding NotifyCommand}"
BackgroundColor="{StaticResource Primary}"
TextColor="White"
HorizontalOptions="Center"
Margin="0,10,0,0" />
</VerticalStackLayout>
</Border>
<!-- Version Info -->
<Label Grid.Row="1"
Text="Estimated availability: Q3 2025"
TextColor="{StaticResource TextSecondary}"
HorizontalOptions="Center"
Margin="0,20,0,0" />
</Grid>
</Grid>
</ContentView>

View File

@@ -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;
}
}

15
PCPal/Core/Core.csproj Normal file
View File

@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="LibreHardwareMonitorLib" Version="0.9.4" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.2" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
</ItemGroup>
</Project>

View File

@@ -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<DisplayProfile> SavedProfiles { get; set; } = new List<DisplayProfile>();
}
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<SensorItem> Sensors { get; set; }
}
public class OledElement
{
public string Type { get; set; } // text, bar, rect, box, line, icon
public Dictionary<string, string> Properties { get; set; } = new Dictionary<string, string>();
public int X { get; set; }
public int Y { get; set; }
// Helper method to generate markup
public string ToMarkup()
{
switch (Type.ToLower())
{
case "text":
return $"<text x={X} y={Y} size={Properties.GetValueOrDefault("size", "1")}>{Properties.GetValueOrDefault("content", "")}</text>";
case "bar":
return $"<bar x={X} y={Y} w={Properties.GetValueOrDefault("width", "100")} h={Properties.GetValueOrDefault("height", "8")} val={Properties.GetValueOrDefault("value", "0")} />";
case "rect":
return $"<rect x={X} y={Y} w={Properties.GetValueOrDefault("width", "20")} h={Properties.GetValueOrDefault("height", "10")} />";
case "box":
return $"<box x={X} y={Y} w={Properties.GetValueOrDefault("width", "20")} h={Properties.GetValueOrDefault("height", "10")} />";
case "line":
return $"<line x1={X} y1={Y} x2={Properties.GetValueOrDefault("x2", "20")} y2={Properties.GetValueOrDefault("y2", "20")} />";
case "icon":
return $"<icon x={X} y={Y} name={Properties.GetValueOrDefault("name", "cpu")} />";
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("<text"))
{
element.Type = "text";
// Parse x and y attributes
var x = Regex.Match(markup, @"x=(\d+)").Groups[1].Value;
var y = Regex.Match(markup, @"y=(\d+)").Groups[1].Value;
var size = Regex.Match(markup, @"size=(\d+)").Groups[1].Value;
var content = Regex.Match(markup, @">([^<]*)</text>").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;
}
}

View File

@@ -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<string, float> _sensorValues;
public MarkupParser(Dictionary<string, float> sensorValues)
{
_sensorValues = sensorValues ?? new Dictionary<string, float>();
}
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<PreviewElement> ParseMarkup(string markup)
{
var elements = new List<PreviewElement>();
if (string.IsNullOrEmpty(markup))
return elements;
// Process variables in the markup first
markup = ProcessVariables(markup);
// Parse text elements - <text x=0 y=10 size=1>Hello</text>
foreach (Match match in Regex.Matches(markup, @"<text\s+x=(\d+)\s+y=(\d+)(?:\s+size=(\d+))?>([^<]*)</text>"))
{
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 - <bar x=0 y=20 w=100 h=8 val=75 />
foreach (Match match in Regex.Matches(markup, @"<bar\s+x=(\d+)\s+y=(\d+)\s+w=(\d+)\s+h=(\d+)\s+val=(\d+)\s*/>"))
{
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 - <rect x=0 y=0 w=20 h=10 />
foreach (Match match in Regex.Matches(markup, @"<rect\s+x=(\d+)\s+y=(\d+)\s+w=(\d+)\s+h=(\d+)\s*/>"))
{
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 - <box x=0 y=0 w=20 h=10 />
foreach (Match match in Regex.Matches(markup, @"<box\s+x=(\d+)\s+y=(\d+)\s+w=(\d+)\s+h=(\d+)\s*/>"))
{
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 - <line x1=0 y1=0 x2=20 y2=20 />
foreach (Match match in Regex.Matches(markup, @"<line\s+x1=(\d+)\s+y1=(\d+)\s+x2=(\d+)\s+y2=(\d+)\s*/>"))
{
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 - <icon x=0 y=0 name=cpu />
foreach (Match match in Regex.Matches(markup, @"<icon\s+x=(\d+)\s+y=(\d+)\s+name=([a-zA-Z0-9_]+)\s*/>"))
{
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;
}
}

View File

@@ -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<DisplayConfig> LoadConfigAsync();
Task SaveConfigAsync(DisplayConfig config);
Task<string> GetLastIconDirectoryAsync();
Task SaveLastIconDirectoryAsync(string path);
}
public class ConfigurationService : IConfigurationService
{
private readonly string _configPath;
private readonly ILogger<ConfigurationService> _logger;
public ConfigurationService(ILogger<ConfigurationService> logger)
{
_logger = logger;
_configPath = GetConfigPath();
}
public async Task<DisplayConfig> LoadConfigAsync()
{
try
{
if (File.Exists(_configPath))
{
string json = await File.ReadAllTextAsync(_configPath);
var config = JsonConvert.DeserializeObject<DisplayConfig>(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<string> 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");
}
}

View File

@@ -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<string, float> GetAllSensorValues();
float? GetSensorValue(string sensorId);
string FindFirstSensorOfType(HardwareType hardwareType, SensorType sensorType);
ObservableCollection<SensorGroup> GetAllSensorsGrouped();
string GetSensorVariableName(string hardwareName, string sensorName);
Task<string> CreateExampleMarkupAsync();
string ProcessVariablesInMarkup(string markup);
}
public class SensorService : ISensorService
{
private readonly Computer _computer;
private readonly Dictionary<string, float> _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<string, float> GetAllSensorValues()
{
return new Dictionary<string, float>(_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<SensorGroup> GetAllSensorsGrouped()
{
var result = new ObservableCollection<SensorGroup>();
// Group sensors by hardware type
var hardwareGroups = new Dictionary<HardwareType, SensorGroup>();
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<SensorItem>()
};
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<string> 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("<text x=0 y=12 size=2>System Monitor</text>");
if (!string.IsNullOrEmpty(cpuLoad) && !string.IsNullOrEmpty(cpuTemp))
{
sb.AppendLine($"<text x=0 y=30 size=1>CPU: {{{cpuLoad}}}% ({{{cpuTemp}}}°C)</text>");
sb.AppendLine($"<bar x=0 y=35 w=128 h=6 val={{{cpuLoad}}} />");
}
else if (!string.IsNullOrEmpty(cpuLoad))
{
sb.AppendLine($"<text x=0 y=30 size=1>CPU: {{{cpuLoad}}}%</text>");
sb.AppendLine($"<bar x=0 y=35 w=128 h=6 val={{{cpuLoad}}} />");
}
if (!string.IsNullOrEmpty(gpuLoad) && !string.IsNullOrEmpty(gpuTemp))
{
sb.AppendLine($"<text x=130 y=30 size=1>GPU: {{{gpuLoad}}}% ({{{gpuTemp}}}°C)</text>");
sb.AppendLine($"<bar x=130 y=35 w=120 h=6 val={{{gpuLoad}}} />");
}
else if (!string.IsNullOrEmpty(gpuLoad))
{
sb.AppendLine($"<text x=130 y=30 size=1>GPU: {{{gpuLoad}}}%</text>");
sb.AppendLine($"<bar x=130 y=35 w=120 h=6 val={{{gpuLoad}}} />");
}
if (!string.IsNullOrEmpty(ramUsed))
{
sb.AppendLine($"<text x=0 y=50 size=1>RAM: {{{ramUsed}}} GB</text>");
}
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;
}
}
}

View File

@@ -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<bool> ConnectAsync(string port);
Task<bool> ConnectToFirstAvailableAsync();
Task DisconnectAsync();
Task<bool> SendCommandAsync(string command);
Task<string> AutoDetectDeviceAsync();
Task<bool> CheckConnectionAsync();
event EventHandler<bool> ConnectionStatusChanged;
}
public class SerialPortService : ISerialPortService
{
private SerialPort _serialPort;
private bool _isConnected;
private string _currentPort;
private readonly ILogger<SerialPortService> _logger;
public bool IsConnected => _isConnected;
public string CurrentPort => _currentPort;
public event EventHandler<bool> ConnectionStatusChanged;
public SerialPortService(ILogger<SerialPortService> logger)
{
_logger = logger;
_isConnected = false;
_currentPort = string.Empty;
}
public async Task<bool> 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<bool> 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<bool> 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<string> 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<bool> 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;
}
}
}

9
PCPal/PCPal.csproj Normal file
View File

@@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
</Project>

33
PCPal/PCPal.sln Normal file
View File

@@ -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

6
PCPal/Program.cs Normal file
View File

@@ -0,0 +1,6 @@
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", () => "Hello World!");
app.Run();

View File

@@ -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"
}
}
}
}

View File

@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

9
PCPal/appsettings.json Normal file
View File

@@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}

View File

@@ -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<App>()
.ConfigureFonts(fonts =>
{
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
fonts.AddFont("Consolas.ttf", "Consolas");
});
// Register services
builder.Services.AddSingleton<ISensorService, SensorService>();
builder.Services.AddSingleton<ISerialPortService, SerialPortService>();
builder.Services.AddSingleton<IConfigurationService, ConfigurationService>();
// Register views and view models
// LCD
builder.Services.AddTransient<LcdConfigView>();
builder.Services.AddTransient<LcdConfigViewModel>();
// OLED
builder.Services.AddTransient<OledConfigView>();
builder.Services.AddTransient<OledConfigViewModel>();
builder.Services.AddTransient<OledVisualEditorView>();
builder.Services.AddTransient<OledMarkupEditorView>();
builder.Services.AddTransient<OledTemplatesView>();
// TFT
builder.Services.AddTransient<TftConfigView>();
builder.Services.AddTransient<TftConfigViewModel>();
// Settings
builder.Services.AddTransient<SettingsView>();
builder.Services.AddTransient<SettingsViewModel>();
// Help
builder.Services.AddTransient<HelpView>();
builder.Services.AddTransient<HelpViewModel>();
#if DEBUG
builder.Logging.AddDebug();
#endif
return builder.Build();
}
}

View File

@@ -8,7 +8,7 @@
// </auto-generated> // </auto-generated>
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
namespace PCPalConfigurator.Properties { namespace PCPal.Properties {
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]

View File

@@ -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
{
}
}