Dunkelmodus mit Umschalter in der Fensterleiste

Farbschema hell / dunkel / dem System folgen, umschaltbar per Klick, Stand wird in
server_settings.xml gemerkt (ServerSettings.Theme). Der Editor zeigt die Einstellung
automatisch als Auswahlfeld - kein Zusatzcode, weil sie ein Enum ist.

- App.axaml: 18 Farb-Token je Variante (Flaechen, Rahmen, Texte, Bedeutungsfarben,
  Zeilenfaerbung, Trading-Umschalter, Chat-Rollen). Die Standard-Steuerelemente stellt
  das FluentTheme selbst um.
- 22 fest verdrahtete Farben in 11 XAML-Dateien auf DynamicResource umgestellt - die
  wechseln damit von selbst mit.
- Umschalter (Sonne/Mond) sitzt in der gemeinsamen Fensterleiste, also auf JEDEM Fenster.

Der eigentliche Knackpunkt waren die Farben, die im Code gesetzt werden - die folgen dem
Thema NICHT von selbst:
- TradeRowPalette liefert jetzt Eigenschaften statt static readonly, loest also bei jedem
  Zugriff neu auf. Im Dunkeln gedaempfte Toene statt der hellen Pastelltoene, die dort
  blenden und den Text unlesbar machen wuerden.
- ChatEntry speichert die ROLLE statt eines fertigen Brush - dadurch stimmt der Verlauf
  nach dem Umschalten ohne Neuaufbau der Liste.
- Launcher-Umschalter, Dashboard-KPI und die Grid-Zeilenfarben zeichnen sich ueber
  ThemeManager.ThemeChanged neu.
- Das Terminal bleibt in beiden Schemata dunkel (Konsolen sind konventionell dunkel).

Nebenbei die Anzeige-Kultur gepinnt (stand ohnehin auf der Linux-Liste): auf Linux richtet
sie sich sonst nach LANG/LC_ALL, das unter systemd oft nicht gesetzt ist - dann faellt .NET
auf Invariant zurueck und aus '1.234,56 USDC' wird '1,234.56 USDC'. Maschinen-I/O laeuft
davon unabhaengig weiter invariant.

Smoke-UI prueft jetzt zusaetzlich, dass ALLE 18 Farben in BEIDEN Varianten aufloesen -
ein vertippter Ressourcenschluessel wuerde sonst still grau werden.

Verifiziert: Solution baut, 450 Tests gruen, --smoke-ui gruen (10 Fenster + Editor +
beide Farbschemata), App startet im gespeicherten Schema, Linux-Publish laeuft.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Richard
2026-08-06 22:41:25 +02:00
co-authored by Claude Opus 5
parent 669676ac60
commit 487ea4466d
23 changed files with 350 additions and 60 deletions
+88 -12
View File
@@ -1,7 +1,78 @@
<Application xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="PolyTrader.App.Avalonia.App"
RequestedThemeVariant="Light">
x:Class="PolyTrader.App.Avalonia.App">
<!--
Farbschema. RequestedThemeVariant wird NICHT hier festgelegt, sondern beim Start vom
ThemeManager aus den Server-Settings gesetzt (hell / dunkel / dem System folgen).
Alle projekteigenen Farben stehen als Token in beiden Varianten. Im XAML werden sie über
DynamicResource gebunden dadurch wechseln sie beim Umschalten von selbst. Nur wenige
Stellen setzen Farben im Code (Grid-Zeilen, Umschalter); die holen sie über
ThemeManager.Brush(...) und zeichnen sich bei ThemeChanged neu.
-->
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.ThemeDictionaries>
<ResourceDictionary x:Key="Light">
<!-- Flächen und Rahmen -->
<SolidColorBrush x:Key="AppSurfaceBrush" Color="#FAFAFA" />
<SolidColorBrush x:Key="AppSurfaceAltBrush" Color="#F3F3F3" />
<SolidColorBrush x:Key="AppCardBrush" Color="#F5F5F5" />
<SolidColorBrush x:Key="AppBorderBrush" Color="#DDDDDD" />
<!-- Texte -->
<SolidColorBrush x:Key="AppMutedTextBrush" Color="#777777" />
<SolidColorBrush x:Key="AppCaptionTextBrush" Color="#666666" />
<SolidColorBrush x:Key="AppReadOnlyTextBrush" Color="#444444" />
<!-- Bedeutungsfarben -->
<SolidColorBrush x:Key="AppPositiveBrush" Color="#2E7D32" />
<SolidColorBrush x:Key="AppNegativeBrush" Color="#C62828" />
<SolidColorBrush x:Key="AppWarningBrush" Color="#996600" />
<!-- Zeilenfärbung der Trade-Listen (Pastell auf hellem Grund) -->
<SolidColorBrush x:Key="AppTradeLossBrush" Color="#F5C8C8" />
<SolidColorBrush x:Key="AppTradeSmallWinBrush" Color="#D4F0D4" />
<SolidColorBrush x:Key="AppTradeBigWinBrush" Color="#8CD68C" />
<!-- Trading-Umschalter im Launcher -->
<SolidColorBrush x:Key="AppToggleActiveBrush" Color="#90EE90" />
<SolidColorBrush x:Key="AppToggleSellOnlyBrush" Color="#FFA500" />
<SolidColorBrush x:Key="AppToggleInactiveBrush" Color="#CD5C5C" />
<!-- Supervisor-Analyseverlauf -->
<SolidColorBrush x:Key="AppChatUserBrush" Color="#4682B4" />
<SolidColorBrush x:Key="AppChatAgentBrush" Color="#2E8B57" />
</ResourceDictionary>
<ResourceDictionary x:Key="Dark">
<!-- Flächen und Rahmen -->
<SolidColorBrush x:Key="AppSurfaceBrush" Color="#2B2B2B" />
<SolidColorBrush x:Key="AppSurfaceAltBrush" Color="#333333" />
<SolidColorBrush x:Key="AppCardBrush" Color="#2F2F2F" />
<SolidColorBrush x:Key="AppBorderBrush" Color="#484848" />
<!-- Texte: heller, aber nicht reinweiß sonst flimmert es auf Dunkel -->
<SolidColorBrush x:Key="AppMutedTextBrush" Color="#9A9A9A" />
<SolidColorBrush x:Key="AppCaptionTextBrush" Color="#AAAAAA" />
<SolidColorBrush x:Key="AppReadOnlyTextBrush" Color="#CFCFCF" />
<!-- Bedeutungsfarben: gleiche Aussage, aufgehellt für Kontrast auf Dunkel -->
<SolidColorBrush x:Key="AppPositiveBrush" Color="#81C784" />
<SolidColorBrush x:Key="AppNegativeBrush" Color="#EF5350" />
<SolidColorBrush x:Key="AppWarningBrush" Color="#FFB74D" />
<!-- Zeilenfärbung: gedämpft statt pastell die hellen Töne würden auf Dunkel
blenden und den Text unlesbar machen. Reihenfolge der Aussage bleibt. -->
<SolidColorBrush x:Key="AppTradeLossBrush" Color="#4E2A2A" />
<SolidColorBrush x:Key="AppTradeSmallWinBrush" Color="#2C3F2C" />
<SolidColorBrush x:Key="AppTradeBigWinBrush" Color="#3D6B3D" />
<!-- Trading-Umschalter -->
<SolidColorBrush x:Key="AppToggleActiveBrush" Color="#3F7A3F" />
<SolidColorBrush x:Key="AppToggleSellOnlyBrush" Color="#A76B1F" />
<SolidColorBrush x:Key="AppToggleInactiveBrush" Color="#8E3B3B" />
<!-- Supervisor-Analyseverlauf -->
<SolidColorBrush x:Key="AppChatUserBrush" Color="#7FB3D8" />
<SolidColorBrush x:Key="AppChatAgentBrush" Color="#68B68A" />
</ResourceDictionary>
</ResourceDictionary.ThemeDictionaries>
</ResourceDictionary>
</Application.Resources>
<Application.Styles>
<FluentTheme />
@@ -18,11 +89,11 @@
<Setter Property="FontSize" Value="12" />
</Style>
<!-- KPI-Kachel: entspricht den zweizeiligen Labels der WinForms-Oberflaeche
<!-- KPI-Kachel: entspricht den zweizeiligen Labels der WinForms-Oberfläche
("<Titel> \n <Wert>"), siehe docs/UI-SPEZIFIKATION-WinForms.md. -->
<Style Selector="Border.kpi">
<Setter Property="Background" Value="#F5F5F5" />
<Setter Property="BorderBrush" Value="#DDDDDD" />
<Setter Property="Background" Value="{DynamicResource AppCardBrush}" />
<Setter Property="BorderBrush" Value="{DynamicResource AppBorderBrush}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="CornerRadius" Value="4" />
<Setter Property="Padding" Value="12,8" />
@@ -31,17 +102,17 @@
</Style>
<Style Selector="Border.kpi > StackPanel > TextBlock.caption">
<Setter Property="FontSize" Value="11" />
<Setter Property="Foreground" Value="#666666" />
<Setter Property="Foreground" Value="{DynamicResource AppCaptionTextBrush}" />
</Style>
<Style Selector="Border.kpi > StackPanel > TextBlock.value">
<Setter Property="FontSize" Value="18" />
<Setter Property="FontWeight" Value="SemiBold" />
</Style>
<!-- Werkzeugleiste oben in jedem Fenster (Ersatz fuer ToolStrip). -->
<!-- Werkzeugleiste oben in jedem Fenster (Ersatz für ToolStrip). -->
<Style Selector="Border.toolbar">
<Setter Property="Background" Value="#FAFAFA" />
<Setter Property="BorderBrush" Value="#DDDDDD" />
<Setter Property="Background" Value="{DynamicResource AppSurfaceBrush}" />
<Setter Property="BorderBrush" Value="{DynamicResource AppBorderBrush}" />
<Setter Property="BorderThickness" Value="0,0,0,1" />
<Setter Property="Padding" Value="6,4" />
</Style>
@@ -50,13 +121,18 @@
<Setter Property="Padding" Value="10,4" />
</Style>
<!-- Statusleiste unten (Ersatz fuer StatusStrip). -->
<!-- Statusleiste unten (Ersatz für StatusStrip). -->
<Style Selector="Border.statusbar">
<Setter Property="Background" Value="#FAFAFA" />
<Setter Property="BorderBrush" Value="#DDDDDD" />
<Setter Property="Background" Value="{DynamicResource AppSurfaceBrush}" />
<Setter Property="BorderBrush" Value="{DynamicResource AppBorderBrush}" />
<Setter Property="BorderThickness" Value="0,1,0,0" />
<Setter Property="Padding" Value="8,4" />
</Style>
<!-- Gedämpfte Hinweistexte (Statuszeilen, Erklärungen unter Feldern). -->
<Style Selector="TextBlock.muted">
<Setter Property="Foreground" Value="{DynamicResource AppMutedTextBrush}" />
</Style>
</Application.Styles>
</Application>
+6
View File
@@ -28,6 +28,12 @@ namespace PolyTrader.App.Avalonia
{
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop && Services != null)
{
// Farbschema aus den Server-Settings übernehmen, BEVOR das erste Fenster entsteht
// sonst blitzt kurz das helle Standardschema auf.
var settings = Services.GetRequiredService<PolyTraderSharp.Models.ServerSettings>();
Shell.ThemeManager.Initialize(settings, "server_settings.xml",
Services.GetRequiredService<PolyTraderSharp.Services.TerminalLogger>());
var uiHost = Services.GetRequiredService<AvaloniaUiHost>();
CoreViews.Register(uiHost, Services);
@@ -29,7 +29,7 @@
</Style>
<Style Selector="TextBlock.fieldHint">
<Setter Property="FontSize" Value="11" />
<Setter Property="Foreground" Value="#777777" />
<Setter Property="Foreground" Value="{DynamicResource AppMutedTextBrush}" />
<Setter Property="TextWrapping" Value="Wrap" />
<Setter Property="Margin" Value="0,2,0,0" />
</Style>
@@ -106,7 +106,7 @@
<TextBlock Grid.Column="0" Classes="fieldLabel" Text="{Binding Label}" />
<StackPanel Grid.Column="1">
<SelectableTextBlock Text="{Binding Value}" VerticalAlignment="Center"
Foreground="#444444" />
Foreground="{DynamicResource AppReadOnlyTextBrush}" />
<TextBlock Classes="fieldHint" Text="{Binding Description}"
IsVisible="{Binding HasDescription}" />
</StackPanel>
@@ -14,7 +14,7 @@
(Avalonia kennt kein ToolStripItemAlignment.Right — die rechte Ausrichtung entsteht
dadurch, dass der letzte Eintrag im DockPanel rechts angedockt wird.)
-->
<Border Background="#F3F3F3" BorderBrush="#DDDDDD" BorderThickness="0,0,0,1" Padding="4,2">
<Border Background="{DynamicResource AppSurfaceAltBrush}" BorderBrush="{DynamicResource AppBorderBrush}" BorderThickness="0,0,0,1" Padding="4,2">
<ItemsControl ItemsSource="{Binding Entries}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
@@ -25,6 +25,7 @@
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="shell:WindowMenuEntry">
<Button Command="{Binding Command}"
ToolTip.Tip="{Binding ToolTip}"
Background="Transparent"
BorderThickness="0"
Padding="8,4"
@@ -36,7 +37,7 @@
IsVisible="{Binding Icon, Converter={x:Static ObjectConverters.IsNotNull}}" />
<TextBlock Text="{Binding CheckMark}"
VerticalAlignment="Center"
Foreground="#2E7D32" />
Foreground="{DynamicResource AppPositiveBrush}" />
<TextBlock Text="{Binding Title}"
VerticalAlignment="Center"
FontWeight="{Binding FontWeight}" />
+38
View File
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using Avalonia;
using Avalonia.Controls;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
@@ -91,6 +92,14 @@ namespace PolyTrader.App.Avalonia
var logger = new TerminalLogger();
bootLog = logger;
// Anzeige-Kultur festnageln: Auf Linux richtet sich die Kultur sonst nach LANG/LC_ALL,
// das unter systemd oft gar nicht gesetzt ist - dann faellt .NET auf Invariant zurueck
// und aus "1.234,56 USDC" wird "1,234.56 USDC". Maschinen-I/O (API, Logs, CSV) laeuft
// unabhaengig davon ohnehin invariant.
var uiCulture = new System.Globalization.CultureInfo("de-DE");
System.Globalization.CultureInfo.DefaultThreadCurrentCulture = uiCulture;
System.Globalization.CultureInfo.DefaultThreadCurrentUICulture = uiCulture;
ConfigureSecretProtection(logger);
// Anzeige-Zeitzone einmalig setzen (Logs, Auswertungen). Ein unbekannter Wert ist nicht
@@ -170,6 +179,7 @@ namespace PolyTrader.App.Avalonia
// Avalonia muss initialisiert sein, damit Fenster konstruiert werden koennen.
BuildAvaloniaApp().SetupWithoutStarting();
App.Services = host.Services;
ThemeManager.Initialize(host.Services.GetRequiredService<ServerSettings>(), ServerSettingsPath, logger);
var uiHost = host.Services.GetRequiredService<AvaloniaUiHost>();
CoreViews.Register(uiHost, host.Services);
@@ -241,6 +251,34 @@ namespace PolyTrader.App.Avalonia
Console.WriteLine($"[FEHLER] Einstellungs-Editor: {ex.GetType().Name}: {ex.Message}");
}
// Farbschema: jeder Token muss in BEIDEN Varianten aufloesen. Ein Tippfehler im
// Schluessel faellt sonst nicht auf - ThemeManager.Brush liefert dann still Grau.
string[] themeKeys =
{
"AppSurfaceBrush", "AppSurfaceAltBrush", "AppCardBrush", "AppBorderBrush",
"AppMutedTextBrush", "AppCaptionTextBrush", "AppReadOnlyTextBrush",
"AppPositiveBrush", "AppNegativeBrush", "AppWarningBrush",
"AppTradeLossBrush", "AppTradeSmallWinBrush", "AppTradeBigWinBrush",
"AppToggleActiveBrush", "AppToggleSellOnlyBrush", "AppToggleInactiveBrush",
"AppChatUserBrush", "AppChatAgentBrush"
};
foreach (var variant in new[] { global::Avalonia.Styling.ThemeVariant.Light,
global::Avalonia.Styling.ThemeVariant.Dark })
{
var missing = themeKeys.Where(k =>
!(global::Avalonia.Application.Current!.TryFindResource(k, variant, out object? v) && v is global::Avalonia.Media.IBrush))
.ToList();
if (missing.Count > 0)
{
failures++;
Console.WriteLine($"[FEHLER] Farbschema „{variant}“: fehlende Farben: {string.Join(", ", missing)}");
}
else
{
Console.WriteLine($"[OK] Farbschema „{variant}“: alle {themeKeys.Length} Farben vorhanden");
}
}
Console.WriteLine(failures == 0 ? "=== Smoke-UI OK ===" : $"=== Smoke-UI: {failures} Fehler ===");
return failures == 0 ? 0 : 1;
}
@@ -0,0 +1,102 @@
using System;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Media;
using Avalonia.Styling;
using PolyTraderSharp.Models;
using PolyTraderSharp.Services;
namespace PolyTrader.App.Avalonia.Shell
{
/// <summary>
/// Verwaltet das Farbschema (hell/dunkel/System) und macht es persistent.
///
/// <para>Die Standard-Steuerelemente stellt Avalonia selbst um, sobald
/// <see cref="global::Avalonia.Application.RequestedThemeVariant"/> wechselt für alles, was per
/// <c>DynamicResource</c> gebunden ist, gilt dasselbe. Farben, die im Code gesetzt werden
/// (Zeilenhintergründe, Umschalter-Flächen, KPI-Vorzeichen), aktualisieren sich dagegen NICHT von
/// selbst. Fenster mit solchen Stellen hängen sich deshalb an <see cref="ThemeChanged"/> und
/// zeichnen sie dort neu.</para>
/// </summary>
public static class ThemeManager
{
private static ServerSettings? _settings;
private static string? _settingsPath;
private static TerminalLogger? _logger;
/// <summary>Feuert nach jedem Wechsel Fenster zeichnen daraufhin ihre Code-Farben neu.</summary>
public static event Action? ThemeChanged;
/// <summary>Ist gerade das dunkle Schema aktiv (auch wenn es über „System" kam)?</summary>
public static bool IsDark =>
global::Avalonia.Application.Current?.ActualThemeVariant == ThemeVariant.Dark;
/// <summary>
/// Einmalig beim Start aufrufen: übernimmt das gespeicherte Schema und merkt sich, wohin
/// spätere Änderungen geschrieben werden.
/// </summary>
public static void Initialize(ServerSettings settings, string settingsPath, TerminalLogger? logger = null)
{
_settings = settings;
_settingsPath = settingsPath;
_logger = logger;
ApplyVariant(settings.Theme);
}
/// <summary>Wechselt hell ↔ dunkel und speichert die Wahl. „System" wird dabei zu einer festen Wahl.</summary>
public static void Toggle()
{
Set(IsDark ? AppThemeVariant.Hell : AppThemeVariant.Dunkel);
}
/// <summary>Setzt das Schema, speichert es und benachrichtigt die Fenster.</summary>
public static void Set(AppThemeVariant variant)
{
ApplyVariant(variant);
if (_settings != null && _settingsPath != null)
{
_settings.Theme = variant;
try
{
_settings.Save(_settingsPath);
}
catch (Exception ex)
{
// Ein fehlgeschlagenes Speichern darf die Umschaltung nicht verhindern
// sie gilt dann nur für diese Sitzung.
_logger?.Warning($"Farbschema konnte nicht gespeichert werden: {ex.Message}");
}
}
ThemeChanged?.Invoke();
}
private static void ApplyVariant(AppThemeVariant variant)
{
var app = global::Avalonia.Application.Current;
if (app == null) return;
app.RequestedThemeVariant = variant switch
{
AppThemeVariant.Hell => ThemeVariant.Light,
AppThemeVariant.Dunkel => ThemeVariant.Dark,
_ => ThemeVariant.Default // folgt dem Betriebssystem
};
}
/// <summary>
/// Löst eine Farbe aus den Themen-Ressourcen auf. Für Stellen, die ihre Farbe im Code setzen
/// müssen (Grid-Zeilen, Umschalter) im XAML nimmt man stattdessen <c>DynamicResource</c>.
/// </summary>
public static IBrush Brush(string key)
{
var app = global::Avalonia.Application.Current;
if (app != null && app.TryFindResource(key, app.ActualThemeVariant, out object? value) && value is IBrush b)
return b;
// Unbekannter Schlüssel ist ein Programmierfehler, darf die Oberfläche aber nicht kippen.
return Brushes.Gray;
}
}
}
@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
@@ -24,6 +24,7 @@ namespace PolyTrader.App.Avalonia.Shell
public sealed class WindowMenuEntry
{
public string Title { get; init; } = "";
public string? ToolTip { get; init; }
public Bitmap? Icon { get; init; }
/// <summary>Aktuelles Fenster wird fett dargestellt.</summary>
@@ -67,10 +68,15 @@ namespace PolyTrader.App.Avalonia.Shell
Rebuild();
_host.OpenStateChanged += OnOpenStateChanged;
ThemeManager.ThemeChanged += OnOpenStateChanged; // Symbol des Umschalters dreht sich
}
/// <summary>Registrierung lösen, wenn das Fenster geht kein Event-Leak über die Host-Lebensdauer.</summary>
public void Detach() => _host.OpenStateChanged -= OnOpenStateChanged;
public void Detach()
{
_host.OpenStateChanged -= OnOpenStateChanged;
ThemeManager.ThemeChanged -= OnOpenStateChanged;
}
private void OnOpenStateChanged()
{
@@ -104,6 +110,15 @@ namespace PolyTrader.App.Avalonia.Shell
});
}
// Farbschema-Umschalter: sitzt auf JEDEM Fenster, damit er ueberall erreichbar ist.
// Sonne = ins Helle wechseln, Mond = ins Dunkle.
Entries.Add(new WindowMenuEntry
{
Title = ThemeManager.IsDark ? "☀" : "🌙",
ToolTip = ThemeManager.IsDark ? "Zum hellen Farbschema wechseln" : "Zum dunklen Farbschema wechseln",
Command = new RelayCommand(ThemeManager.Toggle)
});
// Kontextabhaengige rechte Aktion wie in der WinForms-Fassung: nur der Launcher darf die
// App beenden (mit Sicherheitsabfrage), jedes andere Fenster schliesst nur sich selbst.
Entries.Add(_currentViewId == null
@@ -1,19 +1,24 @@
using System;
using Avalonia.Media;
using PolyTrader.App.Avalonia.Shell;
using PolyTrader.Modules.CopyTrading.Logic;
namespace PolyTrader.App.Avalonia.ViewModels
{
/// <summary>
/// Avalonia-Farbpalette zu den <see cref="TradeRowTint"/>-Kategorien. Die Schwellenlogik liegt
/// Farbpalette zu den <see cref="TradeRowTint"/>-Kategorien. Die Schwellenlogik liegt
/// (getestet) in <see cref="TradeRowColoring"/>; hier steht nur, wie die Kategorien aussehen.
/// Werte unverändert aus der WinForms-Oberfläche übernommen.
///
/// <para>Die Farben kommen aus den Themen-Ressourcen und werden bei JEDEM Zugriff aufgelöst
/// deshalb Eigenschaften statt <c>static readonly</c>. Nach einem Themenwechsel liefern sie
/// sofort die passende Variante; im hellen Schema die Pastelltöne der bisherigen Oberfläche,
/// im dunklen gedämpfte Entsprechungen (helle Pastelltöne würden dort blenden).</para>
/// </summary>
public static class TradeRowPalette
{
public static readonly IBrush Loss = new SolidColorBrush(Color.FromRgb(245, 200, 200)); // rot
public static readonly IBrush SmallWin = new SolidColorBrush(Color.FromRgb(212, 240, 212)); // hellgrün
public static readonly IBrush BigWin = new SolidColorBrush(Color.FromRgb(140, 214, 140)); // grün
public static IBrush Loss => ThemeManager.Brush("AppTradeLossBrush");
public static IBrush SmallWin => ThemeManager.Brush("AppTradeSmallWinBrush");
public static IBrush BigWin => ThemeManager.Brush("AppTradeBigWinBrush");
public static IBrush ForPnlPercent(decimal pnlPercent) => TradeRowColoring.ForPnlPercent(pnlPercent) switch
{
@@ -1,4 +1,5 @@
using Avalonia.Media;
using PolyTrader.App.Avalonia.Shell;
namespace PolyTrader.App.Avalonia.ViewModels
{
@@ -21,11 +22,24 @@ namespace PolyTrader.App.Avalonia.ViewModels
public string Message { get; init; } = string.Empty;
}
/// <summary>Ein Eintrag im Supervisor-Analyseverlauf (Frage oder Antwort).</summary>
/// <summary>Wer im Supervisor-Verlauf spricht bestimmt die Farbe der Kopfzeile.</summary>
public enum ChatRole { User, Agent, Error }
/// <summary>
/// Ein Eintrag im Supervisor-Analyseverlauf. Speichert die ROLLE, nicht die Farbe dadurch
/// stimmt die Darstellung nach einem Themenwechsel ohne Neuaufbau der Liste.
/// </summary>
public sealed class ChatEntry
{
public string Header { get; init; } = string.Empty;
public string Text { get; init; } = string.Empty;
public IBrush HeaderBrush { get; init; } = Brushes.SteelBlue;
public ChatRole Role { get; init; } = ChatRole.User;
public IBrush HeaderBrush => Role switch
{
ChatRole.Agent => ThemeManager.Brush("AppChatAgentBrush"),
ChatRole.Error => ThemeManager.Brush("AppNegativeBrush"),
_ => ThemeManager.Brush("AppChatUserBrush")
};
}
}
@@ -75,7 +75,7 @@
<Grid Grid.Row="1" RowDefinitions="*,*" ColumnDefinitions="*,*" Margin="0,12,0,0">
<Border Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2"
BorderBrush="#DDDDDD" BorderThickness="1" Margin="0,0,0,8">
BorderBrush="{DynamicResource AppBorderBrush}" BorderThickness="1" Margin="0,0,0,8">
<DockPanel>
<TextBlock DockPanel.Dock="Top" Margin="8,6"
FontWeight="SemiBold" Text="Equity-Kurve (kumulierter PnL)" />
@@ -87,7 +87,7 @@
</Border>
<Border Grid.Row="1" Grid.Column="0"
BorderBrush="#DDDDDD" BorderThickness="1" Margin="0,0,4,0">
BorderBrush="{DynamicResource AppBorderBrush}" BorderThickness="1" Margin="0,0,4,0">
<DockPanel>
<TextBlock DockPanel.Dock="Top" Margin="8,6"
FontWeight="SemiBold" Text="PnL je Modul" />
@@ -98,7 +98,7 @@
</Border>
<Border Grid.Row="1" Grid.Column="1"
BorderBrush="#DDDDDD" BorderThickness="1" Margin="4,0,0,0">
BorderBrush="{DynamicResource AppBorderBrush}" BorderThickness="1" Margin="4,0,0,0">
<DockPanel>
<TextBlock DockPanel.Dock="Top" Margin="8,6"
FontWeight="SemiBold" Text="PnL je Tag" />
@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
@@ -73,6 +73,11 @@ namespace PolyTrader.App.Avalonia.Views
RefreshData();
InitModulesTab();
// Die KPI-Farbe wird im Code gesetzt und wechselt daher nicht von selbst mit dem Thema.
void OnTheme() => ApplyScope();
Shell.ThemeManager.ThemeChanged += OnTheme;
Closed += (_, _) => Shell.ThemeManager.ThemeChanged -= OnTheme;
}
// ===== Daten laden / Scope =====
@@ -150,9 +155,7 @@ namespace PolyTrader.App.Avalonia.Views
var pnl = this.FindControl<TextBlock>("kpiPnl")!;
pnl.Text = $"{k.NetPnl:N2} USDC";
pnl.Foreground = new global::Avalonia.Media.SolidColorBrush(
k.NetPnl >= 0 ? global::Avalonia.Media.Color.FromRgb(0x2E, 0x7D, 0x32)
: global::Avalonia.Media.Color.FromRgb(0xC6, 0x28, 0x28));
pnl.Foreground = Shell.ThemeManager.Brush(k.NetPnl >= 0 ? "AppPositiveBrush" : "AppNegativeBrush");
this.FindControl<TextBlock>("kpiWin")!.Text = $"{k.WinRatePct:N1} %";
this.FindControl<TextBlock>("kpiTrades")!.Text = k.TradeCount.ToString();
@@ -57,7 +57,7 @@
IsVisible="{Binding Icon, Converter={x:Static ObjectConverters.IsNotNull}}" />
<StackPanel VerticalAlignment="Center">
<TextBlock Text="{Binding Title}" FontWeight="{Binding FontWeight}" />
<TextBlock Text="{Binding CheckMark}" FontSize="11" Foreground="#2E7D32" />
<TextBlock Text="{Binding CheckMark}" FontSize="11" Foreground="{DynamicResource AppPositiveBrush}" />
</StackPanel>
</StackPanel>
</Button>
@@ -66,7 +66,7 @@
</ItemsControl>
<TextBlock Name="lblPortInfo"
Foreground="#996600"
Foreground="{DynamicResource AppWarningBrush}"
TextWrapping="Wrap"
Text="Portierung läuft: Dashboard, Terminal, Einstellungen und die Modul-Fenster werden noch auf Avalonia umgestellt. Die Trading-Dienste laufen davon unabhängig." />
</StackPanel>
@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.ObjectModel;
using System.Linq;
using Avalonia.Controls;
@@ -62,10 +62,14 @@ namespace PolyTrader.App.Avalonia.Views
e.Cancel = true;
Dispatcher.UIThread.Post(_uiHost.RequestShutdown);
};
// Die Umschalterflaechen werden im Code gesetzt und folgen dem Thema nicht von selbst.
ThemeManager.ThemeChanged += UpdateTradingToggles;
Closed += (_, _) =>
{
_statusTimer.Stop();
_uiHost.OpenStateChanged -= OnOpenStateChanged;
ThemeManager.ThemeChanged -= UpdateTradingToggles;
};
}
@@ -106,17 +110,18 @@ namespace PolyTrader.App.Avalonia.Views
Apply(this.FindControl<Button>("btnLiveTrading")!, "LiveTrading", _state.LiveTradingMode);
Apply(this.FindControl<Button>("btnDemoTrading")!, "DemoTrading", _state.DemoTradingMode);
// Farbgebung aus der WinForms-Oberflaeche uebernommen (LightGreen/Orange/IndianRed).
// Aussage wie in der WinForms-Oberflaeche (gruen/orange/rot); die konkreten Toene
// kommen aus dem Thema, damit sie auf hellem wie dunklem Grund lesbar bleiben.
static void Apply(Button btn, string label, TradingMode mode)
{
(string text, Color color) = mode switch
(string text, string brushKey) = mode switch
{
TradingMode.Active => ($"{label} (AKTIV)", Colors.LightGreen),
TradingMode.SellOnly => ($"{label} (SELL-ONLY)", Colors.Orange),
_ => ($"{label} (DEAKTIVIERT)", Colors.IndianRed)
TradingMode.Active => ($"{label} (AKTIV)", "AppToggleActiveBrush"),
TradingMode.SellOnly => ($"{label} (SELL-ONLY)", "AppToggleSellOnlyBrush"),
_ => ($"{label} (DEAKTIVIERT)", "AppToggleInactiveBrush")
};
btn.Content = text;
btn.Background = new SolidColorBrush(color);
btn.Background = ThemeManager.Brush(brushKey);
}
}
@@ -89,7 +89,7 @@
</WrapPanel>
<TextBlock DockPanel.Dock="Top" Name="lblCurrencyNote" Margin="12,0"
Foreground="#777777" FontSize="11" TextWrapping="Wrap" />
Foreground="{DynamicResource AppMutedTextBrush}" FontSize="11" TextWrapping="Wrap" />
<DataGrid Name="gridMonthly" Margin="12" x:DataType="vm:MonthlyRow">
<DataGrid.Columns>
@@ -24,7 +24,7 @@
<Button Name="btnTraderRefresh" Content="Aktualisieren" />
<Button Name="btnTraderSave" Content="Speichern" />
<TextBlock Name="lblTraderHint" VerticalAlignment="Center"
Margin="12,0,0,0" Foreground="#777777" />
Margin="12,0,0,0" Foreground="{DynamicResource AppMutedTextBrush}" />
</StackPanel>
</Border>
@@ -46,7 +46,7 @@
</DataGrid.Columns>
</DataGrid>
<GridSplitter Grid.Column="1" Background="#DDDDDD" />
<GridSplitter Grid.Column="1" Background="{DynamicResource AppBorderBrush}" />
<DockPanel Grid.Column="2">
<HeaderedContentControl DockPanel.Dock="Bottom" Header="Zugewiesene Accounts"
@@ -147,7 +147,7 @@
<TextBlock Text="Konto:" VerticalAlignment="Center" />
<ComboBox Name="cbSettingsAccount" MinWidth="240" />
<Button Name="btnSettingsSave" Content="Speichern" />
<TextBlock Name="lblSettingsHint" VerticalAlignment="Center" Foreground="#777777" />
<TextBlock Name="lblSettingsHint" VerticalAlignment="Center" Foreground="{DynamicResource AppMutedTextBrush}" />
</StackPanel>
</Border>
@@ -93,6 +93,16 @@ namespace PolyTrader.App.Avalonia.Views.Modules
LoadOpenTrades();
LoadClosedTrades();
LoadAccountList();
// Zeilenhintergruende werden in LoadingRow gesetzt und wechseln daher nicht von selbst
// mit dem Thema. Ein Neuzuweisen der Quelle laesst die Zeilen neu erzeugen.
void OnTheme()
{
gridOpen.ItemsSource = null; gridOpen.ItemsSource = _openTrades;
gridClosed.ItemsSource = null; gridClosed.ItemsSource = _closedTrades;
}
Shell.ThemeManager.ThemeChanged += OnTheme;
Closed += (_, _) => Shell.ThemeManager.ThemeChanged -= OnTheme;
}
// ===== Master-Trader =====
@@ -16,7 +16,7 @@
<TextBlock Text="Konto:" VerticalAlignment="Center" />
<ComboBox Name="cbAccount" MinWidth="240" />
<Button Name="btnRefresh" Content="Aktualisieren" />
<TextBlock Name="lblStatus" VerticalAlignment="Center" Foreground="#777777" />
<TextBlock Name="lblStatus" VerticalAlignment="Center" Foreground="{DynamicResource AppMutedTextBrush}" />
</StackPanel>
</Border>
@@ -84,7 +84,7 @@
<Border Classes="toolbar" DockPanel.Dock="Top">
<StackPanel Orientation="Horizontal" Spacing="8">
<Button Name="btnSettingsSave" Content="Speichern" />
<TextBlock Name="lblSettingsHint" VerticalAlignment="Center" Foreground="#777777" />
<TextBlock Name="lblSettingsHint" VerticalAlignment="Center" Foreground="{DynamicResource AppMutedTextBrush}" />
</StackPanel>
</Border>
<controls:SettingsEditor Name="editorSettings" />
@@ -26,7 +26,7 @@
<TextBlock Text="Modell:" VerticalAlignment="Center" />
<TextBox Name="tbModel" MinWidth="240" Watermark="(Standardmodell)" />
<Button Name="btnClearChat" Content="Verlauf leeren" />
<TextBlock Name="lblChatStatus" VerticalAlignment="Center" Foreground="#777777" />
<TextBlock Name="lblChatStatus" VerticalAlignment="Center" Foreground="{DynamicResource AppMutedTextBrush}" />
</StackPanel>
</Border>
@@ -76,7 +76,7 @@
</DataGrid.Columns>
</DataGrid>
<GridSplitter Grid.Column="1" Background="#DDDDDD" />
<GridSplitter Grid.Column="1" Background="{DynamicResource AppBorderBrush}" />
<ScrollViewer Grid.Column="2">
<SelectableTextBlock Name="txtDossier" Margin="12" TextWrapping="Wrap"
@@ -1,9 +1,8 @@
using System;
using System;
using System.Collections.ObjectModel;
using System.Linq;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Avalonia.Media;
using Avalonia.Threading;
using PolyTrader.App.Avalonia.ViewModels;
using PolyTrader.Core.Analytics;
@@ -86,7 +85,7 @@ namespace PolyTrader.App.Avalonia.Views.Modules
var btn = this.FindControl<Button>("btnSend")!;
var status = this.FindControl<TextBlock>("lblChatStatus")!;
AddChat("Du", question, Brushes.SteelBlue);
AddChat("Du", question, ChatRole.User);
tbQuestion.Text = string.Empty;
btn.IsEnabled = false;
status.Text = "Der Supervisor denkt nach …";
@@ -102,13 +101,13 @@ namespace PolyTrader.App.Avalonia.Views.Modules
var result = await _agent.AskAsync(question, model, progress, profile);
AddChat($"Supervisor ({profile?.Name ?? "Allgemein"})", result.Answer, Brushes.SeaGreen);
AddChat($"Supervisor ({profile?.Name ?? "Allgemein"})", result.Answer, ChatRole.Agent);
status.Text = $"{result.ToolInvocations.Count} Tool-Aufruf(e) · " +
$"{result.PromptTokens + result.CompletionTokens} Token.";
}
catch (Exception ex)
{
AddChat("Fehler", ex.Message, Brushes.Firebrick);
AddChat("Fehler", ex.Message, ChatRole.Error);
status.Text = "Anfrage fehlgeschlagen.";
_logger.Error($"Supervisor-Anfrage fehlgeschlagen: {ex.Message}");
}
@@ -119,9 +118,9 @@ namespace PolyTrader.App.Avalonia.Views.Modules
}
}
private void AddChat(string who, string text, IBrush brush)
private void AddChat(string who, string text, ChatRole role)
{
_chat.Add(new ChatEntry { Header = $"[{DateTime.Now:HH:mm:ss}] {who}", Text = text, HeaderBrush = brush });
_chat.Add(new ChatEntry { Header = $"[{DateTime.Now:HH:mm:ss}] {who}", Text = text, Role = role });
this.FindControl<ScrollViewer>("chatScroll")!.ScrollToEnd();
}
@@ -62,7 +62,7 @@
</DataGrid.Columns>
</DataGrid>
<GridSplitter Grid.Column="1" Background="#DDDDDD" />
<GridSplitter Grid.Column="1" Background="{DynamicResource AppBorderBrush}" />
<controls:SettingsEditor Name="editorAccount" Grid.Column="2" />
</Grid>
@@ -17,7 +17,7 @@
Text="Offene Orders und laufende Jobs werden geordnet heruntergefahren. Das kann einen Moment dauern. Die Schaltfläche ist kurz gesperrt, damit das Beenden nicht versehentlich ausgelöst wird." />
<TextBlock Name="lblCountdown"
Foreground="#B71C1C"
Foreground="{DynamicResource AppNegativeBrush}"
Text="Beenden möglich in 10 s …" />
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Spacing="8">
@@ -30,7 +30,7 @@
<Button Name="btnCopyAll" Content="Alles kopieren" />
<TextBlock Text="Level:" VerticalAlignment="Center" />
<ComboBox Name="cbLogLevel" MinWidth="150" />
<TextBlock Name="lblLiveStatus" VerticalAlignment="Center" Foreground="#777777" />
<TextBlock Name="lblLiveStatus" VerticalAlignment="Center" Foreground="{DynamicResource AppMutedTextBrush}" />
</StackPanel>
</Border>
@@ -70,7 +70,7 @@
<TextBlock Text="Text:" VerticalAlignment="Center" />
<TextBox Name="tbViewerText" MinWidth="220" />
<Button Name="btnViewerLoad" Content="Laden" />
<TextBlock Name="lblViewerStatus" VerticalAlignment="Center" Foreground="#777777" />
<TextBlock Name="lblViewerStatus" VerticalAlignment="Center" Foreground="{DynamicResource AppMutedTextBrush}" />
</StackPanel>
</Border>