mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-28 05:14:25 +08:00
settings_re8
This commit is contained in:
135
LanMountainDesktop/Views/ComponentEditorWindow.axaml
Normal file
135
LanMountainDesktop/Views/ComponentEditorWindow.axaml
Normal file
@@ -0,0 +1,135 @@
|
||||
<Window xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:fa="clr-namespace:FluentIcons.Avalonia.Fluent;assembly=FluentIcons.Avalonia.Fluent"
|
||||
xmlns:mi="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
|
||||
xmlns:themes="clr-namespace:Material.Styles.Themes;assembly=Material.Styles"
|
||||
mc:Ignorable="d"
|
||||
x:Class="LanMountainDesktop.Views.ComponentEditorWindow"
|
||||
x:Name="RootWindow"
|
||||
Classes="component-editor-window"
|
||||
Width="720"
|
||||
Height="540"
|
||||
MinWidth="420"
|
||||
MinHeight="320"
|
||||
CanResize="True"
|
||||
SizeToContent="Manual"
|
||||
ShowInTaskbar="False"
|
||||
SystemDecorations="BorderOnly"
|
||||
Background="{DynamicResource EditorWindowBackgroundBrush}"
|
||||
Title="Component Editor">
|
||||
<Window.Resources>
|
||||
<!-- Material Design 3 Brushes -->
|
||||
<SolidColorBrush x:Key="EditorWindowBackgroundBrush" Color="#FFFEF7FF" />
|
||||
<SolidColorBrush x:Key="EditorSurfaceBrush" Color="#FFFEF7FF" />
|
||||
<SolidColorBrush x:Key="EditorSurfaceContainerBrush" Color="#FFF3EDF7" />
|
||||
<SolidColorBrush x:Key="EditorSurfaceContainerHighBrush" Color="#FFE6E0E9" />
|
||||
<SolidColorBrush x:Key="EditorTopAppBarBackgroundBrush" Color="#FFF3EDF7" />
|
||||
<SolidColorBrush x:Key="EditorHeaderIconBackgroundBrush" Color="#FFEADDFF" />
|
||||
<SolidColorBrush x:Key="EditorTitleBarButtonHoverBrush" Color="#121D1B20" />
|
||||
<SolidColorBrush x:Key="EditorPrimaryBrush" Color="#FF6750A4" />
|
||||
<SolidColorBrush x:Key="EditorOnPrimaryBrush" Color="#FFFFFFFF" />
|
||||
<SolidColorBrush x:Key="EditorSecondaryBrush" Color="#FF625B71" />
|
||||
<SolidColorBrush x:Key="EditorTertiaryBrush" Color="#FF7D5260" />
|
||||
<SolidColorBrush x:Key="EditorSelectFieldBackgroundBrush" Color="#FFE6E0E9" />
|
||||
<SolidColorBrush x:Key="EditorSelectFieldHoverBrush" Color="#FFE0DAE4" />
|
||||
<SolidColorBrush x:Key="EditorSelectFieldFocusBrush" Color="#FFDDD3E6" />
|
||||
<SolidColorBrush x:Key="EditorSelectOutlineBrush" Color="#FF79747E" />
|
||||
<SolidColorBrush x:Key="EditorSelectOutlineStrongBrush" Color="#FF6750A4" />
|
||||
<SolidColorBrush x:Key="EditorSelectMenuItemHoverBrush" Color="#1F6750A4" />
|
||||
<SolidColorBrush x:Key="EditorSelectMenuItemSelectedBrush" Color="#306750A4" />
|
||||
<SolidColorBrush x:Key="ComponentEditorHeroBackgroundBrush" Color="#FFEADDFF" />
|
||||
<SolidColorBrush x:Key="ComponentEditorCardBackgroundBrush" Color="#FFF3EDF7" />
|
||||
<SolidColorBrush x:Key="ComponentEditorCardBorderBrush" Color="#FFCAC4D0" />
|
||||
<SolidColorBrush x:Key="ComponentEditorPrimaryTextBrush" Color="#FF1D1B20" />
|
||||
<SolidColorBrush x:Key="ComponentEditorSecondaryTextBrush" Color="#FF49454F" />
|
||||
<SolidColorBrush x:Key="EditorDividerBrush" Color="#FFCAC4D0" />
|
||||
</Window.Resources>
|
||||
|
||||
<Window.Styles>
|
||||
<themes:CustomMaterialTheme BaseTheme="Light" PrimaryColor="#6750A4" SecondaryColor="#625B71" />
|
||||
<StyleInclude Source="avares://LanMountainDesktop/Styles/ComponentEditorThemeResources.axaml" />
|
||||
|
||||
<!-- MD3 Button Styles -->
|
||||
<Style Selector="Button.component-editor-footer-button">
|
||||
<Setter Property="CornerRadius" Value="20" />
|
||||
<Setter Property="Background" Value="{DynamicResource EditorPrimaryBrush}" />
|
||||
<Setter Property="Foreground" Value="{DynamicResource EditorOnPrimaryBrush}" />
|
||||
<Setter Property="Height" Value="40" />
|
||||
</Style>
|
||||
</Window.Styles>
|
||||
|
||||
<Grid Background="{DynamicResource EditorWindowBackgroundBrush}"
|
||||
RowDefinitions="Auto,*">
|
||||
<Border x:Name="CustomTitleBarHost"
|
||||
Padding="24,16"
|
||||
Background="{DynamicResource EditorWindowBackgroundBrush}"
|
||||
IsVisible="False"
|
||||
PointerPressed="OnWindowTitleBarPointerPressed">
|
||||
<Grid ColumnDefinitions="Auto,*,Auto"
|
||||
ColumnSpacing="16">
|
||||
<mi:MaterialIcon x:Name="HeaderIcon"
|
||||
Width="28"
|
||||
Height="28"
|
||||
Foreground="{DynamicResource EditorPrimaryBrush}"
|
||||
VerticalAlignment="Center" />
|
||||
|
||||
<StackPanel Grid.Column="1"
|
||||
Spacing="0"
|
||||
VerticalAlignment="Center">
|
||||
<TextBlock x:Name="TitleTextBlock"
|
||||
Classes="component-editor-headline"
|
||||
FontSize="20"
|
||||
FontWeight="SemiBold"
|
||||
TextTrimming="CharacterEllipsis" />
|
||||
</StackPanel>
|
||||
|
||||
<Button Grid.Column="2"
|
||||
Classes="component-editor-titlebar-button"
|
||||
VerticalAlignment="Center"
|
||||
Background="Transparent"
|
||||
BorderThickness="0"
|
||||
Width="40" Height="40"
|
||||
Padding="8"
|
||||
Click="OnCloseClick">
|
||||
<mi:MaterialIcon Kind="Close"
|
||||
Width="24" Height="24" />
|
||||
</Button>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<Panel Grid.Row="1">
|
||||
<ScrollViewer Classes="component-editor-scroll-host"
|
||||
HorizontalScrollBarVisibility="Disabled"
|
||||
VerticalScrollBarVisibility="Auto">
|
||||
<ContentControl x:Name="EditorContentHost"
|
||||
Margin="24,0,24,100"
|
||||
HorizontalContentAlignment="Stretch"
|
||||
VerticalContentAlignment="Stretch" />
|
||||
</ScrollViewer>
|
||||
|
||||
<!-- Floating Save Button (MD3 Style) -->
|
||||
<Button x:Name="SaveFAB"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Bottom"
|
||||
Margin="28"
|
||||
Width="64"
|
||||
Height="64"
|
||||
Background="{DynamicResource EditorPrimaryBrush}"
|
||||
Foreground="{DynamicResource EditorOnPrimaryBrush}"
|
||||
CornerRadius="18"
|
||||
Classes="accent"
|
||||
Click="OnCloseClick">
|
||||
<Button.Styles>
|
||||
<Style Selector="Button:pointerover">
|
||||
<Setter Property="RenderTransform" Value="scale(1.05)" />
|
||||
</Style>
|
||||
</Button.Styles>
|
||||
<mi:MaterialIcon Kind="Check"
|
||||
Width="32"
|
||||
Height="32" />
|
||||
</Button>
|
||||
</Panel>
|
||||
</Grid>
|
||||
</Window>
|
||||
256
LanMountainDesktop/Views/ComponentEditorWindow.axaml.cs
Normal file
256
LanMountainDesktop/Views/ComponentEditorWindow.axaml.cs
Normal file
@@ -0,0 +1,256 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Platform;
|
||||
using Avalonia.Styling;
|
||||
using FluentIcons.Common;
|
||||
using LanMountainDesktop.ComponentSystem;
|
||||
using LanMountainDesktop.Services;
|
||||
using LanMountainDesktop.Theme;
|
||||
using Material.Styles.Themes;
|
||||
using Material.Styles.Themes.Base;
|
||||
|
||||
using Material.Icons;
|
||||
using Material.Icons.Avalonia;
|
||||
|
||||
namespace LanMountainDesktop.Views;
|
||||
|
||||
public partial class ComponentEditorWindow : Window
|
||||
{
|
||||
private readonly Dictionary<string, Size> _sizeCache = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly CustomMaterialTheme _materialTheme;
|
||||
private DesktopComponentEditorDescriptor? _descriptor;
|
||||
private string? _currentComponentId;
|
||||
private bool _suppressAspectRatioCorrection;
|
||||
private Size _lastStableSize;
|
||||
|
||||
public ComponentEditorWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
_materialTheme = Styles.OfType<CustomMaterialTheme>().FirstOrDefault()
|
||||
?? throw new InvalidOperationException("Component editor Material theme is missing.");
|
||||
_lastStableSize = new Size(Width, Height);
|
||||
ApplyChromeMode(useSystemChrome: false);
|
||||
}
|
||||
|
||||
public void ApplyDescriptor(
|
||||
DesktopComponentEditorDescriptor descriptor,
|
||||
DesktopComponentEditorContext context)
|
||||
{
|
||||
_descriptor = descriptor ?? throw new ArgumentNullException(nameof(descriptor));
|
||||
_currentComponentId = context.ComponentId;
|
||||
|
||||
var editor = descriptor.CreateEditor(context);
|
||||
EditorContentHost.Content = editor;
|
||||
TitleTextBlock.Text = descriptor.Definition.DisplayName;
|
||||
HeaderIcon.Kind = ResolveSymbol(descriptor.Definition.IconKey);
|
||||
Title = descriptor.Definition.DisplayName;
|
||||
|
||||
ApplyPreferredSize(descriptor);
|
||||
}
|
||||
|
||||
public void ApplyChromeMode(bool useSystemChrome)
|
||||
{
|
||||
var preferSystemChrome = useSystemChrome || OperatingSystem.IsMacOS();
|
||||
if (preferSystemChrome)
|
||||
{
|
||||
ExtendClientAreaToDecorationsHint = true;
|
||||
ExtendClientAreaChromeHints = ExtendClientAreaChromeHints.PreferSystemChrome;
|
||||
ExtendClientAreaTitleBarHeightHint = -1;
|
||||
SystemDecorations = SystemDecorations.Full;
|
||||
CustomTitleBarHost.IsVisible = false;
|
||||
return;
|
||||
}
|
||||
|
||||
SystemDecorations = SystemDecorations.BorderOnly;
|
||||
ExtendClientAreaToDecorationsHint = true;
|
||||
ExtendClientAreaChromeHints = ExtendClientAreaChromeHints.NoChrome;
|
||||
ExtendClientAreaTitleBarHeightHint = 52;
|
||||
CustomTitleBarHost.IsVisible = true;
|
||||
}
|
||||
|
||||
internal void ApplyTheme(ComponentEditorThemePalette palette)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(palette);
|
||||
|
||||
RequestedThemeVariant = palette.IsNightMode ? ThemeVariant.Dark : ThemeVariant.Light;
|
||||
_materialTheme.BaseTheme = palette.IsNightMode ? BaseThemeMode.Dark : BaseThemeMode.Light;
|
||||
_materialTheme.PrimaryColor = palette.PrimaryColor;
|
||||
_materialTheme.SecondaryColor = palette.SecondaryColor;
|
||||
|
||||
SetBrushResource("EditorPrimaryBrush", palette.PrimaryColor);
|
||||
SetBrushResource("EditorOnPrimaryBrush", palette.IsNightMode ? Colors.Black : Colors.White);
|
||||
SetBrushResource("EditorSecondaryBrush", palette.SecondaryColor);
|
||||
SetBrushResource("EditorTertiaryBrush", palette.TertiaryColor);
|
||||
SetBrushResource("EditorWindowBackgroundBrush", palette.WindowBackgroundColor);
|
||||
SetBrushResource("EditorSurfaceBrush", palette.SurfaceColor);
|
||||
SetBrushResource("EditorSurfaceContainerBrush", palette.SurfaceContainerColor);
|
||||
SetBrushResource("EditorSurfaceContainerHighBrush", palette.SurfaceContainerHighColor);
|
||||
SetBrushResource("EditorSelectFieldBackgroundBrush", palette.SurfaceContainerHighColor);
|
||||
SetBrushResource(
|
||||
"EditorSelectFieldHoverBrush",
|
||||
ColorMath.Blend(palette.SurfaceContainerHighColor, palette.PrimaryColor, palette.IsNightMode ? 0.18 : 0.08));
|
||||
SetBrushResource(
|
||||
"EditorSelectFieldFocusBrush",
|
||||
ColorMath.Blend(palette.SurfaceContainerHighColor, palette.PrimaryColor, palette.IsNightMode ? 0.24 : 0.12));
|
||||
SetBrushResource("EditorSelectOutlineBrush", palette.OutlineColor);
|
||||
SetBrushResource(
|
||||
"EditorSelectOutlineStrongBrush",
|
||||
ColorMath.EnsureContrast(palette.PrimaryColor, palette.SurfaceContainerHighColor, 3.0));
|
||||
SetBrushResource(
|
||||
"EditorSelectMenuItemHoverBrush",
|
||||
ColorMath.Blend(palette.SurfaceContainerColor, palette.PrimaryColor, palette.IsNightMode ? 0.20 : 0.10));
|
||||
SetBrushResource(
|
||||
"EditorSelectMenuItemSelectedBrush",
|
||||
ColorMath.Blend(palette.SurfaceContainerColor, palette.PrimaryColor, palette.IsNightMode ? 0.30 : 0.16));
|
||||
SetBrushResource("EditorTopAppBarBackgroundBrush", palette.TopAppBarColor);
|
||||
SetBrushResource("EditorHeaderIconBackgroundBrush", palette.HeaderIconBackgroundColor);
|
||||
SetBrushResource("EditorTitleBarButtonHoverBrush", palette.TitleBarButtonHoverColor);
|
||||
SetBrushResource("ComponentEditorHeroBackgroundBrush", palette.SurfaceContainerHighColor);
|
||||
SetBrushResource("ComponentEditorCardBackgroundBrush", palette.SurfaceContainerColor);
|
||||
SetBrushResource("ComponentEditorCardBorderBrush", palette.OutlineColor);
|
||||
SetBrushResource("ComponentEditorPrimaryTextBrush", palette.OnSurfaceColor);
|
||||
SetBrushResource("ComponentEditorSecondaryTextBrush", palette.OnSurfaceVariantColor);
|
||||
SetBrushResource("EditorDividerBrush", palette.DividerColor);
|
||||
}
|
||||
|
||||
protected override void OnSizeChanged(SizeChangedEventArgs e)
|
||||
{
|
||||
base.OnSizeChanged(e);
|
||||
|
||||
if (_descriptor is null || _suppressAspectRatioCorrection)
|
||||
{
|
||||
_lastStableSize = e.NewSize;
|
||||
return;
|
||||
}
|
||||
|
||||
var correctedSize = CoerceSize(e.NewSize, e.PreviousSize, _descriptor);
|
||||
if (Math.Abs(correctedSize.Width - e.NewSize.Width) < 0.5 &&
|
||||
Math.Abs(correctedSize.Height - e.NewSize.Height) < 0.5)
|
||||
{
|
||||
_lastStableSize = correctedSize;
|
||||
CacheCurrentSize();
|
||||
return;
|
||||
}
|
||||
|
||||
_suppressAspectRatioCorrection = true;
|
||||
Width = correctedSize.Width;
|
||||
Height = correctedSize.Height;
|
||||
_suppressAspectRatioCorrection = false;
|
||||
_lastStableSize = correctedSize;
|
||||
CacheCurrentSize();
|
||||
}
|
||||
|
||||
private void SetBrushResource(string key, Color color)
|
||||
{
|
||||
Resources[key] = new SolidColorBrush(color);
|
||||
}
|
||||
|
||||
private void ApplyPreferredSize(DesktopComponentEditorDescriptor descriptor)
|
||||
{
|
||||
var width = descriptor.PreferredWidth;
|
||||
var height = descriptor.PreferredHeight;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(_currentComponentId) &&
|
||||
_sizeCache.TryGetValue(_currentComponentId, out var cached))
|
||||
{
|
||||
width = cached.Width;
|
||||
height = cached.Height;
|
||||
}
|
||||
|
||||
_suppressAspectRatioCorrection = true;
|
||||
MinWidth = descriptor.PreferredWidth * descriptor.MinScale;
|
||||
MinHeight = descriptor.PreferredHeight * descriptor.MinScale;
|
||||
MaxWidth = descriptor.PreferredWidth * descriptor.MaxScale;
|
||||
MaxHeight = descriptor.PreferredHeight * descriptor.MaxScale;
|
||||
Width = width;
|
||||
Height = height;
|
||||
_lastStableSize = new Size(width, height);
|
||||
_suppressAspectRatioCorrection = false;
|
||||
}
|
||||
|
||||
private void CacheCurrentSize()
|
||||
{
|
||||
if (_descriptor is null || string.IsNullOrWhiteSpace(_currentComponentId))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_sizeCache[_currentComponentId] = _lastStableSize;
|
||||
}
|
||||
|
||||
private static Size CoerceSize(Size currentSize, Size previousSize, DesktopComponentEditorDescriptor descriptor)
|
||||
{
|
||||
var preferredWidth = descriptor.PreferredWidth;
|
||||
var preferredHeight = descriptor.PreferredHeight;
|
||||
var aspectRatio = descriptor.AspectRatio;
|
||||
var minWidth = preferredWidth * descriptor.MinScale;
|
||||
var maxWidth = preferredWidth * descriptor.MaxScale;
|
||||
var minHeight = preferredHeight * descriptor.MinScale;
|
||||
var maxHeight = preferredHeight * descriptor.MaxScale;
|
||||
|
||||
var deltaWidth = Math.Abs(currentSize.Width - previousSize.Width);
|
||||
var deltaHeight = Math.Abs(currentSize.Height - previousSize.Height);
|
||||
|
||||
double width;
|
||||
double height;
|
||||
if (deltaWidth >= deltaHeight)
|
||||
{
|
||||
width = Math.Clamp(currentSize.Width, minWidth, maxWidth);
|
||||
height = Math.Clamp(width / aspectRatio, minHeight, maxHeight);
|
||||
width = Math.Clamp(height * aspectRatio, minWidth, maxWidth);
|
||||
}
|
||||
else
|
||||
{
|
||||
height = Math.Clamp(currentSize.Height, minHeight, maxHeight);
|
||||
width = Math.Clamp(height * aspectRatio, minWidth, maxWidth);
|
||||
height = Math.Clamp(width / aspectRatio, minHeight, maxHeight);
|
||||
}
|
||||
|
||||
return new Size(width, height);
|
||||
}
|
||||
|
||||
private void OnWindowTitleBarPointerPressed(object? sender, PointerPressedEventArgs e)
|
||||
{
|
||||
_ = sender;
|
||||
if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
|
||||
{
|
||||
BeginMoveDrag(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static MaterialIconKind ResolveSymbol(string iconKey)
|
||||
{
|
||||
return iconKey switch
|
||||
{
|
||||
"Clock" => MaterialIconKind.Clock,
|
||||
"Timer" => MaterialIconKind.Timer,
|
||||
"WeatherSunny" => MaterialIconKind.WeatherSunny,
|
||||
"CalendarDate" => MaterialIconKind.CalendarRange,
|
||||
"CalendarMonth" => MaterialIconKind.CalendarMonth,
|
||||
"MicOn" => MaterialIconKind.Microphone,
|
||||
"News" => MaterialIconKind.Newspaper,
|
||||
"Image" => MaterialIconKind.Image,
|
||||
"Book" => MaterialIconKind.BookOpenVariant,
|
||||
"History" => MaterialIconKind.History,
|
||||
"DataLine" => MaterialIconKind.ChartLine,
|
||||
"Edit" => MaterialIconKind.Pencil,
|
||||
"Calculator" => MaterialIconKind.Calculator,
|
||||
"Globe" => MaterialIconKind.Web,
|
||||
"Play" => MaterialIconKind.Play,
|
||||
_ => MaterialIconKind.Settings
|
||||
};
|
||||
}
|
||||
|
||||
private void OnCloseClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
_ = sender;
|
||||
_ = e;
|
||||
Close();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d"
|
||||
x:Class="LanMountainDesktop.Views.ComponentEditors.ClassScheduleComponentEditor">
|
||||
<StackPanel Spacing="16">
|
||||
<Border Classes="component-editor-hero-card"
|
||||
Padding="24">
|
||||
<StackPanel Spacing="8">
|
||||
<TextBlock x:Name="HeadlineTextBlock"
|
||||
Classes="component-editor-headline"
|
||||
TextWrapping="Wrap" />
|
||||
<TextBlock x:Name="DescriptionTextBlock"
|
||||
Classes="component-editor-secondary-text"
|
||||
TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<Border Classes="component-editor-card"
|
||||
Padding="20">
|
||||
<StackPanel Spacing="12">
|
||||
<Button x:Name="AddScheduleButton"
|
||||
HorizontalAlignment="Left"
|
||||
Classes="accent"
|
||||
Padding="16,10"
|
||||
Click="OnAddScheduleClick" />
|
||||
<TextBlock x:Name="EmptyStateTextBlock"
|
||||
Classes="component-editor-secondary-text"
|
||||
TextWrapping="Wrap" />
|
||||
<StackPanel x:Name="ScheduleItemsPanel"
|
||||
Spacing="10" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,284 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Layout;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Platform.Storage;
|
||||
using LanMountainDesktop.ComponentSystem;
|
||||
using LanMountainDesktop.Models;
|
||||
|
||||
namespace LanMountainDesktop.Views.ComponentEditors;
|
||||
|
||||
public partial class ClassScheduleComponentEditor : ComponentEditorViewBase
|
||||
{
|
||||
private readonly List<ImportedClassScheduleSnapshot> _importedSchedules = [];
|
||||
private string _activeScheduleId = string.Empty;
|
||||
|
||||
public ClassScheduleComponentEditor()
|
||||
: this(null)
|
||||
{
|
||||
}
|
||||
|
||||
public ClassScheduleComponentEditor(DesktopComponentEditorContext? context)
|
||||
: base(context)
|
||||
{
|
||||
InitializeComponent();
|
||||
LoadState();
|
||||
ApplyState();
|
||||
RenderImportedSchedules();
|
||||
}
|
||||
|
||||
private void LoadState()
|
||||
{
|
||||
var snapshot = LoadSnapshot();
|
||||
_importedSchedules.Clear();
|
||||
foreach (var item in snapshot.ImportedClassSchedules)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(item.Id) || string.IsNullOrWhiteSpace(item.FilePath))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
_importedSchedules.Add(new ImportedClassScheduleSnapshot
|
||||
{
|
||||
Id = item.Id.Trim(),
|
||||
DisplayName = item.DisplayName?.Trim() ?? string.Empty,
|
||||
FilePath = item.FilePath.Trim()
|
||||
});
|
||||
}
|
||||
|
||||
_activeScheduleId = snapshot.ActiveImportedClassScheduleId?.Trim() ?? string.Empty;
|
||||
if (_importedSchedules.Count > 0 &&
|
||||
!_importedSchedules.Any(item => string.Equals(item.Id, _activeScheduleId, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
_activeScheduleId = _importedSchedules[0].Id;
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyState()
|
||||
{
|
||||
HeadlineTextBlock.Text = Context?.Definition.DisplayName ?? "Class Schedule";
|
||||
DescriptionTextBlock.Text = L("schedule.settings.desc", "导入 ClassIsland 的 CSES 课表文件并选择启用项。");
|
||||
AddScheduleButton.Content = L("schedule.settings.add", "添加课表");
|
||||
EmptyStateTextBlock.Text = L("schedule.settings.empty", "暂无导入课表");
|
||||
}
|
||||
|
||||
private async void OnAddScheduleClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
_ = sender;
|
||||
_ = e;
|
||||
var topLevel = TopLevel.GetTopLevel(this);
|
||||
if (topLevel?.StorageProvider is not { } storageProvider)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var files = await storageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
|
||||
{
|
||||
Title = L("schedule.settings.picker_title", "选择 ClassIsland 课表文件"),
|
||||
AllowMultiple = false,
|
||||
FileTypeFilter =
|
||||
[
|
||||
new FilePickerFileType(L("schedule.settings.picker_file_type", "ClassIsland CSES 课表"))
|
||||
{
|
||||
Patterns = ["*.cses", "*.yaml", "*.yml"]
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
if (files.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var importedPath = await ImportScheduleFileAsync(files[0]);
|
||||
if (string.IsNullOrWhiteSpace(importedPath))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var existing = _importedSchedules.FirstOrDefault(item =>
|
||||
string.Equals(item.FilePath, importedPath, StringComparison.OrdinalIgnoreCase));
|
||||
if (existing is not null)
|
||||
{
|
||||
_activeScheduleId = existing.Id;
|
||||
}
|
||||
else
|
||||
{
|
||||
_importedSchedules.Add(new ImportedClassScheduleSnapshot
|
||||
{
|
||||
Id = Guid.NewGuid().ToString("N"),
|
||||
DisplayName = Path.GetFileNameWithoutExtension(importedPath)?.Trim()
|
||||
?? L("schedule.settings.unnamed", "未命名课表"),
|
||||
FilePath = importedPath
|
||||
});
|
||||
_activeScheduleId = _importedSchedules[^1].Id;
|
||||
}
|
||||
|
||||
PersistState();
|
||||
RenderImportedSchedules();
|
||||
}
|
||||
|
||||
private async Task<string?> ImportScheduleFileAsync(IStorageFile file)
|
||||
{
|
||||
try
|
||||
{
|
||||
var extension = Path.GetExtension(file.Name);
|
||||
if (string.IsNullOrWhiteSpace(extension))
|
||||
{
|
||||
extension = ".cses";
|
||||
}
|
||||
|
||||
var importedDirectory = Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||||
"LanMountainDesktop",
|
||||
"Schedules");
|
||||
Directory.CreateDirectory(importedDirectory);
|
||||
|
||||
var destinationPath = Path.Combine(
|
||||
importedDirectory,
|
||||
$"{DateTime.Now:yyyyMMdd_HHmmss}_{Guid.NewGuid():N}{extension}");
|
||||
|
||||
await using var sourceStream = await file.OpenReadAsync();
|
||||
await using var destinationStream = File.Create(destinationPath);
|
||||
await sourceStream.CopyToAsync(destinationStream);
|
||||
return destinationPath;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private void RenderImportedSchedules()
|
||||
{
|
||||
ScheduleItemsPanel.Children.Clear();
|
||||
EmptyStateTextBlock.IsVisible = _importedSchedules.Count == 0;
|
||||
if (_importedSchedules.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var item in _importedSchedules)
|
||||
{
|
||||
var selector = new RadioButton
|
||||
{
|
||||
GroupName = "class_schedule_imports",
|
||||
IsChecked = string.Equals(item.Id, _activeScheduleId, StringComparison.OrdinalIgnoreCase),
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Tag = item.Id
|
||||
};
|
||||
selector.IsCheckedChanged += OnScheduleSelectionChanged;
|
||||
|
||||
var title = new TextBlock
|
||||
{
|
||||
Text = string.IsNullOrWhiteSpace(item.DisplayName)
|
||||
? L("schedule.settings.unnamed", "未命名课表")
|
||||
: item.DisplayName,
|
||||
FontWeight = FontWeight.SemiBold
|
||||
};
|
||||
|
||||
var path = new TextBlock
|
||||
{
|
||||
Text = item.FilePath,
|
||||
FontSize = 11,
|
||||
Opacity = 0.7,
|
||||
TextTrimming = TextTrimming.CharacterEllipsis
|
||||
};
|
||||
|
||||
var deleteButton = new Button
|
||||
{
|
||||
Content = L("schedule.settings.delete", "删除"),
|
||||
Tag = item.Id,
|
||||
Padding = new Thickness(12, 8),
|
||||
HorizontalAlignment = HorizontalAlignment.Right
|
||||
};
|
||||
deleteButton.Click += OnDeleteScheduleClick;
|
||||
|
||||
var row = new Grid
|
||||
{
|
||||
ColumnDefinitions = new ColumnDefinitions("Auto,*,Auto"),
|
||||
ColumnSpacing = 12
|
||||
};
|
||||
var details = new StackPanel
|
||||
{
|
||||
Spacing = 4,
|
||||
Children = { title, path }
|
||||
};
|
||||
row.Children.Add(selector);
|
||||
row.Children.Add(details);
|
||||
row.Children.Add(deleteButton);
|
||||
Grid.SetColumn(details, 1);
|
||||
Grid.SetColumn(deleteButton, 2);
|
||||
|
||||
ScheduleItemsPanel.Children.Add(new Border
|
||||
{
|
||||
Padding = new Thickness(12, 10),
|
||||
CornerRadius = new CornerRadius(16),
|
||||
Background = Brushes.Transparent,
|
||||
BorderBrush = Brushes.Gray,
|
||||
BorderThickness = new Thickness(1),
|
||||
Child = row
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void OnScheduleSelectionChanged(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
_ = e;
|
||||
if (sender is not RadioButton radioButton || radioButton.IsChecked != true || radioButton.Tag is not string scheduleId)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_activeScheduleId = scheduleId;
|
||||
PersistState();
|
||||
}
|
||||
|
||||
private void OnDeleteScheduleClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
_ = e;
|
||||
if (sender is not Button button || button.Tag is not string scheduleId)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var removed = _importedSchedules.RemoveAll(item =>
|
||||
string.Equals(item.Id, scheduleId, StringComparison.OrdinalIgnoreCase));
|
||||
if (removed == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.Equals(_activeScheduleId, scheduleId, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
_activeScheduleId = _importedSchedules.FirstOrDefault()?.Id ?? string.Empty;
|
||||
}
|
||||
|
||||
PersistState();
|
||||
RenderImportedSchedules();
|
||||
}
|
||||
|
||||
private void PersistState()
|
||||
{
|
||||
var snapshot = LoadSnapshot();
|
||||
snapshot.ImportedClassSchedules = _importedSchedules
|
||||
.Select(item => new ImportedClassScheduleSnapshot
|
||||
{
|
||||
Id = item.Id,
|
||||
DisplayName = item.DisplayName,
|
||||
FilePath = item.FilePath
|
||||
})
|
||||
.ToList();
|
||||
snapshot.ActiveImportedClassScheduleId = _activeScheduleId;
|
||||
SaveSnapshot(
|
||||
snapshot,
|
||||
nameof(ComponentSettingsSnapshot.ImportedClassSchedules),
|
||||
nameof(ComponentSettingsSnapshot.ActiveImportedClassScheduleId));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d"
|
||||
x:Class="LanMountainDesktop.Views.ComponentEditors.ClockComponentEditor">
|
||||
<StackPanel Spacing="16">
|
||||
<Border Classes="component-editor-hero-card"
|
||||
Padding="24">
|
||||
<StackPanel Spacing="8">
|
||||
<TextBlock x:Name="HeadlineTextBlock"
|
||||
Classes="component-editor-headline"
|
||||
TextWrapping="Wrap" />
|
||||
<TextBlock x:Name="DescriptionTextBlock"
|
||||
Classes="component-editor-secondary-text"
|
||||
TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<Border Classes="component-editor-card"
|
||||
Padding="20">
|
||||
<StackPanel Spacing="12">
|
||||
<TextBlock x:Name="TimeZoneLabelTextBlock"
|
||||
Classes="component-editor-section-title" />
|
||||
<ComboBox x:Name="TimeZoneComboBox"
|
||||
Classes="component-editor-select"
|
||||
HorizontalAlignment="Stretch"
|
||||
SelectionChanged="OnTimeZoneSelectionChanged" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<Border Classes="component-editor-card"
|
||||
Padding="20">
|
||||
<StackPanel Spacing="12">
|
||||
<TextBlock x:Name="SecondHandLabelTextBlock"
|
||||
Classes="component-editor-section-title" />
|
||||
|
||||
<Border Classes="component-editor-segmented-host"
|
||||
HorizontalAlignment="Left">
|
||||
<Grid ColumnDefinitions="*,*"
|
||||
ColumnSpacing="4">
|
||||
<ToggleButton x:Name="TickRadioButton"
|
||||
Classes="component-editor-segmented-choice"
|
||||
Checked="OnSecondHandChanged"
|
||||
Unchecked="OnSecondHandChanged" />
|
||||
<ToggleButton x:Name="SweepRadioButton"
|
||||
Grid.Column="1"
|
||||
Classes="component-editor-segmented-choice"
|
||||
Checked="OnSecondHandChanged"
|
||||
Unchecked="OnSecondHandChanged" />
|
||||
</Grid>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,139 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Interactivity;
|
||||
using LanMountainDesktop.ComponentSystem;
|
||||
using LanMountainDesktop.Models;
|
||||
using LanMountainDesktop.Services;
|
||||
|
||||
namespace LanMountainDesktop.Views.ComponentEditors;
|
||||
|
||||
public partial class ClockComponentEditor : ComponentEditorViewBase
|
||||
{
|
||||
private readonly TimeZoneService _timeZoneService = new();
|
||||
private bool _suppressEvents;
|
||||
|
||||
public ClockComponentEditor()
|
||||
: this(null)
|
||||
{
|
||||
}
|
||||
|
||||
public ClockComponentEditor(DesktopComponentEditorContext? context)
|
||||
: base(context)
|
||||
{
|
||||
if (context is not null)
|
||||
{
|
||||
_timeZoneService.CurrentTimeZone = context.SettingsFacade.Region.GetTimeZoneService().CurrentTimeZone;
|
||||
}
|
||||
|
||||
InitializeComponent();
|
||||
ApplyState();
|
||||
}
|
||||
|
||||
private void ApplyState()
|
||||
{
|
||||
HeadlineTextBlock.Text = Context?.Definition.DisplayName ?? "Clock";
|
||||
DescriptionTextBlock.Text = L("clock.settings.desc", "配置时区和秒针动画。");
|
||||
TimeZoneLabelTextBlock.Text = L("clock.settings.timezone", "时区");
|
||||
SecondHandLabelTextBlock.Text = L("clock.settings.second_mode_label", "秒针方式");
|
||||
TickRadioButton.Content = L("clock.second_mode.tick", "跳针");
|
||||
SweepRadioButton.Content = L("clock.second_mode.sweep", "扫针");
|
||||
|
||||
var snapshot = LoadSnapshot();
|
||||
var configuredTimeZoneId = string.IsNullOrWhiteSpace(snapshot.DesktopClockTimeZoneId)
|
||||
? TimeZoneInfo.Local.Id
|
||||
: snapshot.DesktopClockTimeZoneId.Trim();
|
||||
|
||||
_suppressEvents = true;
|
||||
TimeZoneComboBox.Items.Clear();
|
||||
foreach (var timeZone in _timeZoneService.GetAllTimeZones()
|
||||
.OrderBy(zone => zone.GetUtcOffset(DateTime.UtcNow))
|
||||
.ThenBy(zone => zone.DisplayName, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
var item = new ComboBoxItem
|
||||
{
|
||||
Tag = timeZone.Id,
|
||||
Content = FormatTimeZone(timeZone)
|
||||
};
|
||||
item.Classes.Add("component-editor-select-item");
|
||||
TimeZoneComboBox.Items.Add(item);
|
||||
}
|
||||
|
||||
TimeZoneComboBox.SelectedItem = TimeZoneComboBox.Items
|
||||
.OfType<ComboBoxItem>()
|
||||
.FirstOrDefault(item => string.Equals(item.Tag as string, configuredTimeZoneId, StringComparison.OrdinalIgnoreCase))
|
||||
?? TimeZoneComboBox.Items.OfType<ComboBoxItem>().FirstOrDefault();
|
||||
var secondHandMode = ClockSecondHandMode.Normalize(snapshot.DesktopClockSecondHandMode);
|
||||
TickRadioButton.IsChecked = string.Equals(secondHandMode, ClockSecondHandMode.Tick, StringComparison.OrdinalIgnoreCase);
|
||||
SweepRadioButton.IsChecked = string.Equals(secondHandMode, ClockSecondHandMode.Sweep, StringComparison.OrdinalIgnoreCase);
|
||||
_suppressEvents = false;
|
||||
}
|
||||
|
||||
private void OnTimeZoneSelectionChanged(object? sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
_ = sender;
|
||||
_ = e;
|
||||
if (_suppressEvents)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
SaveState();
|
||||
}
|
||||
|
||||
private void OnSecondHandChanged(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
_ = sender;
|
||||
_ = e;
|
||||
if (_suppressEvents)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_suppressEvents = true;
|
||||
if (sender == TickRadioButton)
|
||||
{
|
||||
TickRadioButton.IsChecked = true;
|
||||
SweepRadioButton.IsChecked = false;
|
||||
}
|
||||
else if (sender == SweepRadioButton)
|
||||
{
|
||||
SweepRadioButton.IsChecked = true;
|
||||
TickRadioButton.IsChecked = false;
|
||||
}
|
||||
|
||||
if (TickRadioButton.IsChecked != true && SweepRadioButton.IsChecked != true)
|
||||
{
|
||||
TickRadioButton.IsChecked = true;
|
||||
}
|
||||
|
||||
_suppressEvents = false;
|
||||
SaveState();
|
||||
}
|
||||
|
||||
private void SaveState()
|
||||
{
|
||||
var snapshot = LoadSnapshot();
|
||||
snapshot.DesktopClockTimeZoneId = TimeZoneComboBox.SelectedItem is ComboBoxItem item && item.Tag is string timeZoneId
|
||||
? timeZoneId
|
||||
: TimeZoneInfo.Local.Id;
|
||||
snapshot.DesktopClockSecondHandMode = SweepRadioButton.IsChecked == true
|
||||
? ClockSecondHandMode.Sweep
|
||||
: ClockSecondHandMode.Tick;
|
||||
|
||||
SaveSnapshot(
|
||||
snapshot,
|
||||
nameof(ComponentSettingsSnapshot.DesktopClockTimeZoneId),
|
||||
nameof(ComponentSettingsSnapshot.DesktopClockSecondHandMode));
|
||||
}
|
||||
|
||||
private static string FormatTimeZone(TimeZoneInfo timeZone)
|
||||
{
|
||||
var offset = timeZone.GetUtcOffset(DateTime.UtcNow);
|
||||
var sign = offset >= TimeSpan.Zero ? "+" : "-";
|
||||
var totalMinutes = Math.Abs((int)offset.TotalMinutes);
|
||||
var hours = totalMinutes / 60;
|
||||
var minutes = totalMinutes % 60;
|
||||
return $"(UTC{sign}{hours:D2}:{minutes:D2}) {timeZone.StandardName}";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
using Avalonia.Controls;
|
||||
using LanMountainDesktop.ComponentSystem;
|
||||
using LanMountainDesktop.Models;
|
||||
using LanMountainDesktop.Services;
|
||||
|
||||
namespace LanMountainDesktop.Views.ComponentEditors;
|
||||
|
||||
public abstract class ComponentEditorViewBase : UserControl
|
||||
{
|
||||
protected ComponentEditorViewBase(DesktopComponentEditorContext? context)
|
||||
{
|
||||
Context = context;
|
||||
}
|
||||
|
||||
protected DesktopComponentEditorContext? Context { get; }
|
||||
|
||||
protected LocalizationService LocalizationService { get; } = new();
|
||||
|
||||
protected string LanguageCode =>
|
||||
LocalizationService.NormalizeLanguageCode(Context?.SettingsFacade.Region.Get().LanguageCode);
|
||||
|
||||
protected string L(string key, string fallback)
|
||||
{
|
||||
return LocalizationService.GetString(LanguageCode, key, fallback);
|
||||
}
|
||||
|
||||
protected ComponentSettingsSnapshot LoadSnapshot()
|
||||
{
|
||||
return Context?.ComponentSettingsAccessor.LoadSnapshot<ComponentSettingsSnapshot>() ?? new ComponentSettingsSnapshot();
|
||||
}
|
||||
|
||||
protected void SaveSnapshot(ComponentSettingsSnapshot snapshot, params string[] changedKeys)
|
||||
{
|
||||
if (Context is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Context.ComponentSettingsAccessor.SaveSnapshot(
|
||||
snapshot,
|
||||
changedKeys.Length == 0 ? null : changedKeys);
|
||||
Context.HostContext.RequestRefresh();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d"
|
||||
x:Class="LanMountainDesktop.Views.ComponentEditors.DailyArtworkComponentEditor">
|
||||
<StackPanel Spacing="16">
|
||||
<Border Classes="component-editor-card"
|
||||
Padding="20">
|
||||
<StackPanel Spacing="12">
|
||||
<TextBlock x:Name="SourceLabelTextBlock"
|
||||
Classes="component-editor-section-title" />
|
||||
<ComboBox x:Name="SourceComboBox"
|
||||
Classes="component-editor-select"
|
||||
HorizontalAlignment="Stretch"
|
||||
SelectionChanged="OnSourceSelectionChanged">
|
||||
<ComboBoxItem x:Name="DomesticItem"
|
||||
Classes="component-editor-select-item"
|
||||
Tag="Domestic" />
|
||||
<ComboBoxItem x:Name="OverseasItem"
|
||||
Classes="component-editor-select-item"
|
||||
Tag="Overseas" />
|
||||
</ComboBox>
|
||||
<TextBlock x:Name="StatusTextBlock"
|
||||
Classes="component-editor-secondary-text"
|
||||
TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,64 @@
|
||||
using System;
|
||||
using Avalonia.Controls;
|
||||
using LanMountainDesktop.ComponentSystem;
|
||||
using LanMountainDesktop.Models;
|
||||
|
||||
namespace LanMountainDesktop.Views.ComponentEditors;
|
||||
|
||||
public partial class DailyArtworkComponentEditor : ComponentEditorViewBase
|
||||
{
|
||||
private bool _suppressEvents;
|
||||
|
||||
public DailyArtworkComponentEditor()
|
||||
: this(null)
|
||||
{
|
||||
}
|
||||
|
||||
public DailyArtworkComponentEditor(DesktopComponentEditorContext? context)
|
||||
: base(context)
|
||||
{
|
||||
InitializeComponent();
|
||||
ApplyState();
|
||||
}
|
||||
|
||||
private void ApplyState()
|
||||
{
|
||||
SourceLabelTextBlock.Text = L("artwork.settings.source_label", "Mirror Source");
|
||||
DomesticItem.Content = L("artwork.settings.source_domestic", "Domestic Mirror");
|
||||
OverseasItem.Content = L("artwork.settings.source_overseas", "Overseas Mirror");
|
||||
|
||||
_suppressEvents = true;
|
||||
var source = DailyArtworkMirrorSources.Normalize(LoadSnapshot().DailyArtworkMirrorSource);
|
||||
SourceComboBox.SelectedItem = string.Equals(source, DailyArtworkMirrorSources.Domestic, StringComparison.OrdinalIgnoreCase)
|
||||
? DomesticItem
|
||||
: OverseasItem;
|
||||
UpdateStatus(source);
|
||||
_suppressEvents = false;
|
||||
}
|
||||
|
||||
private void OnSourceSelectionChanged(object? sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
_ = sender;
|
||||
_ = e;
|
||||
if (_suppressEvents)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var source = SourceComboBox.SelectedItem is ComboBoxItem item && item.Tag is string tag
|
||||
? DailyArtworkMirrorSources.Normalize(tag)
|
||||
: DailyArtworkMirrorSources.Overseas;
|
||||
|
||||
var snapshot = LoadSnapshot();
|
||||
snapshot.DailyArtworkMirrorSource = source;
|
||||
SaveSnapshot(snapshot, nameof(ComponentSettingsSnapshot.DailyArtworkMirrorSource));
|
||||
UpdateStatus(source);
|
||||
}
|
||||
|
||||
private void UpdateStatus(string source)
|
||||
{
|
||||
StatusTextBlock.Text = string.Equals(source, DailyArtworkMirrorSources.Domestic, StringComparison.OrdinalIgnoreCase)
|
||||
? L("artwork.settings.source_status_domestic", "当前源:国内镜像(优先中国网络)")
|
||||
: L("artwork.settings.source_status_overseas", "当前源:国外镜像(艺术馆推荐)");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d"
|
||||
x:Class="LanMountainDesktop.Views.ComponentEditors.InformationalComponentEditor">
|
||||
<StackPanel Spacing="16">
|
||||
<Border Classes="component-editor-card"
|
||||
Padding="20">
|
||||
<Grid ColumnDefinitions="Auto,*"
|
||||
RowDefinitions="Auto,Auto,Auto"
|
||||
ColumnSpacing="12"
|
||||
RowSpacing="8">
|
||||
<TextBlock x:Name="ComponentLabelTextBlock"
|
||||
Classes="component-editor-section-title" />
|
||||
<TextBlock x:Name="ComponentValueTextBlock"
|
||||
Grid.Column="1" />
|
||||
|
||||
<TextBlock Grid.Row="1"
|
||||
x:Name="PlacementLabelTextBlock"
|
||||
Classes="component-editor-section-title" />
|
||||
<TextBlock x:Name="PlacementValueTextBlock"
|
||||
Grid.Row="1"
|
||||
Grid.Column="1" />
|
||||
|
||||
<TextBlock Grid.Row="2"
|
||||
x:Name="ScopeLabelTextBlock"
|
||||
Classes="component-editor-section-title" />
|
||||
<TextBlock x:Name="ScopeValueTextBlock"
|
||||
Grid.Row="2"
|
||||
Grid.Column="1"
|
||||
Classes="component-editor-secondary-text"
|
||||
TextWrapping="Wrap" />
|
||||
</Grid>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,31 @@
|
||||
using LanMountainDesktop.ComponentSystem;
|
||||
|
||||
namespace LanMountainDesktop.Views.ComponentEditors;
|
||||
|
||||
public partial class InformationalComponentEditor : ComponentEditorViewBase
|
||||
{
|
||||
private readonly string _description;
|
||||
|
||||
public InformationalComponentEditor()
|
||||
: this(null, "This component currently exposes instance information only.")
|
||||
{
|
||||
}
|
||||
|
||||
public InformationalComponentEditor(DesktopComponentEditorContext? context, string description)
|
||||
: base(context)
|
||||
{
|
||||
_description = description;
|
||||
InitializeComponent();
|
||||
ApplyState();
|
||||
}
|
||||
|
||||
private void ApplyState()
|
||||
{
|
||||
ComponentLabelTextBlock.Text = L("component.editor.id_label", "Component");
|
||||
ComponentValueTextBlock.Text = Context?.ComponentId ?? "-";
|
||||
PlacementLabelTextBlock.Text = L("component.editor.placement_label", "Placement");
|
||||
PlacementValueTextBlock.Text = Context?.PlacementId ?? "-";
|
||||
ScopeLabelTextBlock.Text = L("component.editor.scope_label", "Scope");
|
||||
ScopeValueTextBlock.Text = L("component.editor.scope_instance", "Instance-scoped editor");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d"
|
||||
x:Class="LanMountainDesktop.Views.ComponentEditors.StudyEnvironmentComponentEditor">
|
||||
<StackPanel Spacing="16">
|
||||
<Border Classes="component-editor-hero-card"
|
||||
Padding="24">
|
||||
<StackPanel Spacing="8">
|
||||
<TextBlock x:Name="HeadlineTextBlock"
|
||||
Classes="component-editor-headline"
|
||||
TextWrapping="Wrap" />
|
||||
<TextBlock x:Name="DescriptionTextBlock"
|
||||
Classes="component-editor-secondary-text"
|
||||
TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<Border Classes="component-editor-card"
|
||||
Padding="20">
|
||||
<StackPanel Spacing="12">
|
||||
<TextBlock x:Name="DisplayDbHeaderTextBlock"
|
||||
Classes="component-editor-section-title" />
|
||||
<ToggleSwitch x:Name="DisplayDbToggleSwitch"
|
||||
IsCheckedChanged="OnToggleChanged" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<Border Classes="component-editor-card"
|
||||
Padding="20">
|
||||
<StackPanel Spacing="12">
|
||||
<TextBlock x:Name="DbfsHeaderTextBlock"
|
||||
Classes="component-editor-section-title" />
|
||||
<ToggleSwitch x:Name="DbfsToggleSwitch"
|
||||
IsCheckedChanged="OnToggleChanged" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<TextBlock x:Name="HintTextBlock"
|
||||
Classes="component-editor-secondary-text"
|
||||
Margin="12,0"
|
||||
TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,72 @@
|
||||
using Avalonia.Interactivity;
|
||||
using LanMountainDesktop.ComponentSystem;
|
||||
using LanMountainDesktop.Models;
|
||||
|
||||
namespace LanMountainDesktop.Views.ComponentEditors;
|
||||
|
||||
public partial class StudyEnvironmentComponentEditor : ComponentEditorViewBase
|
||||
{
|
||||
private bool _suppressEvents;
|
||||
|
||||
public StudyEnvironmentComponentEditor()
|
||||
: this(null)
|
||||
{
|
||||
}
|
||||
|
||||
public StudyEnvironmentComponentEditor(DesktopComponentEditorContext? context)
|
||||
: base(context)
|
||||
{
|
||||
InitializeComponent();
|
||||
ApplyState();
|
||||
}
|
||||
|
||||
private void ApplyState()
|
||||
{
|
||||
var snapshot = LoadSnapshot();
|
||||
var showDisplayDb = snapshot.StudyEnvironmentShowDisplayDb;
|
||||
var showDbfs = snapshot.StudyEnvironmentShowDbfs;
|
||||
if (!showDisplayDb && !showDbfs)
|
||||
{
|
||||
showDisplayDb = true;
|
||||
}
|
||||
|
||||
HeadlineTextBlock.Text = Context?.Definition.DisplayName ?? "Study Environment";
|
||||
DescriptionTextBlock.Text = L("study.environment.settings.desc", "配置右侧实时噪音值显示内容。");
|
||||
DisplayDbToggleSwitch.Content = L("study.environment.settings.show_display_db", "显示 display dB");
|
||||
DbfsToggleSwitch.Content = L("study.environment.settings.show_dbfs", "显示 dBFS");
|
||||
HintTextBlock.Text = L("study.environment.settings.hint", "至少启用一种显示方式。");
|
||||
|
||||
_suppressEvents = true;
|
||||
DisplayDbToggleSwitch.IsChecked = showDisplayDb;
|
||||
DbfsToggleSwitch.IsChecked = showDbfs;
|
||||
_suppressEvents = false;
|
||||
}
|
||||
|
||||
private void OnToggleChanged(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
_ = sender;
|
||||
_ = e;
|
||||
if (_suppressEvents)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var showDisplayDb = DisplayDbToggleSwitch.IsChecked == true;
|
||||
var showDbfs = DbfsToggleSwitch.IsChecked == true;
|
||||
if (!showDisplayDb && !showDbfs)
|
||||
{
|
||||
_suppressEvents = true;
|
||||
DisplayDbToggleSwitch.IsChecked = true;
|
||||
_suppressEvents = false;
|
||||
showDisplayDb = true;
|
||||
}
|
||||
|
||||
var snapshot = LoadSnapshot();
|
||||
snapshot.StudyEnvironmentShowDisplayDb = showDisplayDb;
|
||||
snapshot.StudyEnvironmentShowDbfs = showDbfs;
|
||||
SaveSnapshot(
|
||||
snapshot,
|
||||
nameof(ComponentSettingsSnapshot.StudyEnvironmentShowDisplayDb),
|
||||
nameof(ComponentSettingsSnapshot.StudyEnvironmentShowDbfs));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d"
|
||||
x:Class="LanMountainDesktop.Views.ComponentEditors.ToggleIntervalComponentEditor">
|
||||
<StackPanel Spacing="16">
|
||||
<Border Classes="component-editor-card"
|
||||
Padding="20">
|
||||
<Grid RowDefinitions="Auto,Auto"
|
||||
ColumnDefinitions="*,Auto"
|
||||
ColumnSpacing="16"
|
||||
RowSpacing="6">
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock x:Name="ToggleLabelTextBlock"
|
||||
Classes="component-editor-section-title" />
|
||||
<TextBlock x:Name="ToggleDescriptionTextBlock"
|
||||
Classes="component-editor-secondary-text"
|
||||
TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
<ToggleSwitch x:Name="EnabledToggleSwitch"
|
||||
Grid.Column="1"
|
||||
VerticalAlignment="Center"
|
||||
Checked="OnEnabledChanged"
|
||||
Unchecked="OnEnabledChanged" />
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<Border x:Name="IntervalCard"
|
||||
Classes="component-editor-card"
|
||||
Padding="20">
|
||||
<StackPanel Spacing="12">
|
||||
<TextBlock x:Name="IntervalLabelTextBlock"
|
||||
Classes="component-editor-section-title" />
|
||||
<ComboBox x:Name="IntervalComboBox"
|
||||
Classes="component-editor-select"
|
||||
HorizontalAlignment="Stretch"
|
||||
SelectionChanged="OnIntervalSelectionChanged" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<Border x:Name="ExtraSelectorCard"
|
||||
Classes="component-editor-card"
|
||||
Padding="20"
|
||||
IsVisible="False">
|
||||
<StackPanel Spacing="12">
|
||||
<TextBlock x:Name="ExtraSelectorLabelTextBlock"
|
||||
Classes="component-editor-section-title" />
|
||||
<ComboBox x:Name="ExtraSelectorComboBox"
|
||||
Classes="component-editor-select"
|
||||
HorizontalAlignment="Stretch"
|
||||
SelectionChanged="OnExtraSelectionChanged" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,204 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Interactivity;
|
||||
using LanMountainDesktop.ComponentSystem;
|
||||
using LanMountainDesktop.Models;
|
||||
using LanMountainDesktop.Services;
|
||||
|
||||
namespace LanMountainDesktop.Views.ComponentEditors;
|
||||
|
||||
public sealed record ComponentEditorSelectionOption(
|
||||
string Value,
|
||||
string LabelKey,
|
||||
string FallbackLabel);
|
||||
|
||||
public sealed class ToggleIntervalComponentEditorOptions
|
||||
{
|
||||
public required string DescriptionKey { get; init; }
|
||||
public required string DescriptionFallback { get; init; }
|
||||
public required string ToggleLabelKey { get; init; }
|
||||
public required string ToggleLabelFallback { get; init; }
|
||||
public string ToggleDescriptionKey { get; init; } = string.Empty;
|
||||
public string ToggleDescriptionFallback { get; init; } = string.Empty;
|
||||
public required string IntervalLabelKey { get; init; }
|
||||
public required string IntervalLabelFallback { get; init; }
|
||||
public required Func<ComponentSettingsSnapshot, bool> GetEnabled { get; init; }
|
||||
public required Action<ComponentSettingsSnapshot, bool> SetEnabled { get; init; }
|
||||
public required Func<ComponentSettingsSnapshot, int> GetInterval { get; init; }
|
||||
public required Action<ComponentSettingsSnapshot, int> SetInterval { get; init; }
|
||||
public required int DefaultInterval { get; init; }
|
||||
public IReadOnlyList<string> ChangedKeys { get; init; } = Array.Empty<string>();
|
||||
public string ExtraSelectorLabelKey { get; init; } = string.Empty;
|
||||
public string ExtraSelectorLabelFallback { get; init; } = string.Empty;
|
||||
public IReadOnlyList<ComponentEditorSelectionOption> ExtraOptions { get; init; } = Array.Empty<ComponentEditorSelectionOption>();
|
||||
public Func<ComponentSettingsSnapshot, string>? GetExtraValue { get; init; }
|
||||
public Action<ComponentSettingsSnapshot, string>? SetExtraValue { get; init; }
|
||||
}
|
||||
|
||||
public partial class ToggleIntervalComponentEditor : ComponentEditorViewBase
|
||||
{
|
||||
private static readonly IReadOnlyList<int> SupportedIntervals = RefreshIntervalCatalog.SupportedIntervalsMinutes;
|
||||
private readonly ToggleIntervalComponentEditorOptions _options;
|
||||
private bool _suppressEvents;
|
||||
|
||||
public ToggleIntervalComponentEditor()
|
||||
: this(null, new ToggleIntervalComponentEditorOptions
|
||||
{
|
||||
DescriptionKey = "component.editor.desc",
|
||||
DescriptionFallback = "Configure this component.",
|
||||
ToggleLabelKey = "component.editor.toggle",
|
||||
ToggleLabelFallback = "Enabled",
|
||||
IntervalLabelKey = "component.editor.interval",
|
||||
IntervalLabelFallback = "Refresh interval",
|
||||
DefaultInterval = 15,
|
||||
GetEnabled = _ => true,
|
||||
SetEnabled = (_, _) => { },
|
||||
GetInterval = _ => 15,
|
||||
SetInterval = (_, _) => { }
|
||||
})
|
||||
{
|
||||
}
|
||||
|
||||
public ToggleIntervalComponentEditor(
|
||||
DesktopComponentEditorContext? context,
|
||||
ToggleIntervalComponentEditorOptions options)
|
||||
: base(context)
|
||||
{
|
||||
_options = options;
|
||||
InitializeComponent();
|
||||
BuildOptions();
|
||||
ApplyState();
|
||||
}
|
||||
|
||||
private void BuildOptions()
|
||||
{
|
||||
IntervalComboBox.Items.Clear();
|
||||
foreach (var minutes in SupportedIntervals)
|
||||
{
|
||||
var item = new ComboBoxItem
|
||||
{
|
||||
Tag = minutes.ToString(),
|
||||
Content = L("refresh.frequency." + RefreshIntervalCatalog.ToLocalizationKeySuffix(minutes), RefreshIntervalCatalog.ToEnglishFallbackLabel(minutes))
|
||||
};
|
||||
item.Classes.Add("component-editor-select-item");
|
||||
IntervalComboBox.Items.Add(item);
|
||||
}
|
||||
|
||||
ExtraSelectorComboBox.Items.Clear();
|
||||
foreach (var option in _options.ExtraOptions)
|
||||
{
|
||||
var item = new ComboBoxItem
|
||||
{
|
||||
Tag = option.Value,
|
||||
Content = L(option.LabelKey, option.FallbackLabel)
|
||||
};
|
||||
item.Classes.Add("component-editor-select-item");
|
||||
ExtraSelectorComboBox.Items.Add(item);
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyState()
|
||||
{
|
||||
ToggleLabelTextBlock.Text = L(_options.ToggleLabelKey, _options.ToggleLabelFallback);
|
||||
ToggleDescriptionTextBlock.Text = string.IsNullOrWhiteSpace(_options.ToggleDescriptionKey)
|
||||
? L("component.editor.instance_scope", "Changes are stored per component instance.")
|
||||
: L(_options.ToggleDescriptionKey, _options.ToggleDescriptionFallback);
|
||||
IntervalLabelTextBlock.Text = L(_options.IntervalLabelKey, _options.IntervalLabelFallback);
|
||||
ExtraSelectorLabelTextBlock.Text = L(_options.ExtraSelectorLabelKey, _options.ExtraSelectorLabelFallback);
|
||||
ExtraSelectorCard.IsVisible = _options.ExtraOptions.Count > 0;
|
||||
|
||||
_suppressEvents = true;
|
||||
try
|
||||
{
|
||||
var snapshot = LoadSnapshot();
|
||||
var enabled = _options.GetEnabled(snapshot);
|
||||
EnabledToggleSwitch.IsChecked = enabled;
|
||||
IntervalCard.IsVisible = enabled;
|
||||
|
||||
var normalizedInterval = RefreshIntervalCatalog.Normalize(_options.GetInterval(snapshot), _options.DefaultInterval);
|
||||
IntervalComboBox.SelectedItem = IntervalComboBox.Items
|
||||
.OfType<ComboBoxItem>()
|
||||
.FirstOrDefault(item => item.Tag is string tag && int.TryParse(tag, out var minutes) && minutes == normalizedInterval);
|
||||
|
||||
if (_options.GetExtraValue is not null && _options.ExtraOptions.Count > 0)
|
||||
{
|
||||
var extraValue = _options.GetExtraValue(snapshot);
|
||||
ExtraSelectorComboBox.SelectedItem = ExtraSelectorComboBox.Items
|
||||
.OfType<ComboBoxItem>()
|
||||
.FirstOrDefault(item => string.Equals(item.Tag as string, extraValue, StringComparison.OrdinalIgnoreCase))
|
||||
?? ExtraSelectorComboBox.Items.OfType<ComboBoxItem>().FirstOrDefault();
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_suppressEvents = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnEnabledChanged(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
_ = sender;
|
||||
_ = e;
|
||||
if (_suppressEvents)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
IntervalCard.IsVisible = EnabledToggleSwitch.IsChecked == true;
|
||||
SaveState();
|
||||
}
|
||||
|
||||
private void OnIntervalSelectionChanged(object? sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
_ = sender;
|
||||
_ = e;
|
||||
if (_suppressEvents)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
SaveState();
|
||||
}
|
||||
|
||||
private void OnExtraSelectionChanged(object? sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
_ = sender;
|
||||
_ = e;
|
||||
if (_suppressEvents)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
SaveState();
|
||||
}
|
||||
|
||||
private void SaveState()
|
||||
{
|
||||
var snapshot = LoadSnapshot();
|
||||
_options.SetEnabled(snapshot, EnabledToggleSwitch.IsChecked == true);
|
||||
_options.SetInterval(snapshot, GetSelectedInterval());
|
||||
|
||||
if (_options.SetExtraValue is not null &&
|
||||
ExtraSelectorComboBox.SelectedItem is ComboBoxItem extraItem &&
|
||||
extraItem.Tag is string extraValue)
|
||||
{
|
||||
_options.SetExtraValue(snapshot, extraValue);
|
||||
}
|
||||
|
||||
SaveSnapshot(snapshot, _options.ChangedKeys.ToArray());
|
||||
}
|
||||
|
||||
private int GetSelectedInterval()
|
||||
{
|
||||
if (IntervalComboBox.SelectedItem is ComboBoxItem item &&
|
||||
item.Tag is string tag &&
|
||||
int.TryParse(tag, out var minutes))
|
||||
{
|
||||
return RefreshIntervalCatalog.Normalize(minutes, _options.DefaultInterval);
|
||||
}
|
||||
|
||||
return _options.DefaultInterval;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d"
|
||||
x:Class="LanMountainDesktop.Views.ComponentEditors.WorldClockComponentEditor">
|
||||
<StackPanel Spacing="16">
|
||||
<Border Classes="component-editor-hero-card"
|
||||
Padding="24">
|
||||
<StackPanel Spacing="8">
|
||||
<TextBlock x:Name="HeadlineTextBlock"
|
||||
Classes="component-editor-headline"
|
||||
TextWrapping="Wrap" />
|
||||
<TextBlock x:Name="DescriptionTextBlock"
|
||||
Classes="component-editor-secondary-text"
|
||||
TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<Border Classes="component-editor-card"
|
||||
Padding="20">
|
||||
<StackPanel Spacing="12">
|
||||
<TextBlock x:Name="ClockOneLabelTextBlock"
|
||||
Classes="component-editor-section-title" />
|
||||
<ComboBox x:Name="ClockOneComboBox"
|
||||
Classes="component-editor-select"
|
||||
HorizontalAlignment="Stretch"
|
||||
SelectionChanged="OnTimeZoneSelectionChanged" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<Border Classes="component-editor-card"
|
||||
Padding="20">
|
||||
<StackPanel Spacing="12">
|
||||
<TextBlock x:Name="ClockTwoLabelTextBlock"
|
||||
Classes="component-editor-section-title" />
|
||||
<ComboBox x:Name="ClockTwoComboBox"
|
||||
Classes="component-editor-select"
|
||||
HorizontalAlignment="Stretch"
|
||||
SelectionChanged="OnTimeZoneSelectionChanged" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<Border Classes="component-editor-card"
|
||||
Padding="20">
|
||||
<StackPanel Spacing="12">
|
||||
<TextBlock x:Name="ClockThreeLabelTextBlock"
|
||||
Classes="component-editor-section-title" />
|
||||
<ComboBox x:Name="ClockThreeComboBox"
|
||||
Classes="component-editor-select"
|
||||
HorizontalAlignment="Stretch"
|
||||
SelectionChanged="OnTimeZoneSelectionChanged" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<Border Classes="component-editor-card"
|
||||
Padding="20">
|
||||
<StackPanel Spacing="12">
|
||||
<TextBlock x:Name="ClockFourLabelTextBlock"
|
||||
Classes="component-editor-section-title" />
|
||||
<ComboBox x:Name="ClockFourComboBox"
|
||||
Classes="component-editor-select"
|
||||
HorizontalAlignment="Stretch"
|
||||
SelectionChanged="OnTimeZoneSelectionChanged" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<Border Classes="component-editor-card"
|
||||
Padding="20">
|
||||
<StackPanel Spacing="12">
|
||||
<TextBlock x:Name="SecondHandLabelTextBlock"
|
||||
Classes="component-editor-section-title" />
|
||||
|
||||
<Border Classes="component-editor-segmented-host"
|
||||
HorizontalAlignment="Left">
|
||||
<Grid ColumnDefinitions="*,*"
|
||||
ColumnSpacing="4">
|
||||
<ToggleButton x:Name="TickRadioButton"
|
||||
Classes="component-editor-segmented-choice"
|
||||
Checked="OnSecondHandChanged"
|
||||
Unchecked="OnSecondHandChanged" />
|
||||
<ToggleButton x:Name="SweepRadioButton"
|
||||
Grid.Column="1"
|
||||
Classes="component-editor-segmented-choice"
|
||||
Checked="OnSecondHandChanged"
|
||||
Unchecked="OnSecondHandChanged" />
|
||||
</Grid>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,152 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Interactivity;
|
||||
using LanMountainDesktop.ComponentSystem;
|
||||
using LanMountainDesktop.Models;
|
||||
using LanMountainDesktop.Services;
|
||||
|
||||
namespace LanMountainDesktop.Views.ComponentEditors;
|
||||
|
||||
public partial class WorldClockComponentEditor : ComponentEditorViewBase
|
||||
{
|
||||
private readonly TimeZoneService _timeZoneService = new();
|
||||
private readonly ComboBox[] _timeZoneCombos;
|
||||
private bool _suppressEvents;
|
||||
|
||||
public WorldClockComponentEditor()
|
||||
: this(null)
|
||||
{
|
||||
}
|
||||
|
||||
public WorldClockComponentEditor(DesktopComponentEditorContext? context)
|
||||
: base(context)
|
||||
{
|
||||
if (context is not null)
|
||||
{
|
||||
_timeZoneService.CurrentTimeZone = context.SettingsFacade.Region.GetTimeZoneService().CurrentTimeZone;
|
||||
}
|
||||
|
||||
InitializeComponent();
|
||||
_timeZoneCombos = [ClockOneComboBox, ClockTwoComboBox, ClockThreeComboBox, ClockFourComboBox];
|
||||
ApplyState();
|
||||
}
|
||||
|
||||
private void ApplyState()
|
||||
{
|
||||
HeadlineTextBlock.Text = Context?.Definition.DisplayName ?? "World Clock";
|
||||
DescriptionTextBlock.Text = L("worldclock.settings.desc", "分别为四个时钟选择时区。");
|
||||
ClockOneLabelTextBlock.Text = L("worldclock.settings.clock_1", "时钟 1");
|
||||
ClockTwoLabelTextBlock.Text = L("worldclock.settings.clock_2", "时钟 2");
|
||||
ClockThreeLabelTextBlock.Text = L("worldclock.settings.clock_3", "时钟 3");
|
||||
ClockFourLabelTextBlock.Text = L("worldclock.settings.clock_4", "时钟 4");
|
||||
SecondHandLabelTextBlock.Text = L("worldclock.settings.second_mode_label", "秒针方式");
|
||||
TickRadioButton.Content = L("clock.second_mode.tick", "跳针");
|
||||
SweepRadioButton.Content = L("clock.second_mode.sweep", "扫针");
|
||||
|
||||
var allTimeZones = _timeZoneService.GetAllTimeZones()
|
||||
.OrderBy(zone => zone.GetUtcOffset(DateTime.UtcNow))
|
||||
.ThenBy(zone => zone.DisplayName, StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
var selectedIds = WorldClockTimeZoneCatalog.NormalizeTimeZoneIds(LoadSnapshot().WorldClockTimeZoneIds, allTimeZones);
|
||||
|
||||
_suppressEvents = true;
|
||||
foreach (var combo in _timeZoneCombos)
|
||||
{
|
||||
combo.Items.Clear();
|
||||
foreach (var zone in allTimeZones)
|
||||
{
|
||||
var item = new ComboBoxItem
|
||||
{
|
||||
Tag = zone.Id,
|
||||
Content = FormatTimeZone(zone)
|
||||
};
|
||||
item.Classes.Add("component-editor-select-item");
|
||||
combo.Items.Add(item);
|
||||
}
|
||||
}
|
||||
|
||||
for (var index = 0; index < _timeZoneCombos.Length; index++)
|
||||
{
|
||||
var combo = _timeZoneCombos[index];
|
||||
var targetId = index < selectedIds.Count ? selectedIds[index] : TimeZoneInfo.Local.Id;
|
||||
combo.SelectedItem = combo.Items
|
||||
.OfType<ComboBoxItem>()
|
||||
.FirstOrDefault(item => string.Equals(item.Tag as string, targetId, StringComparison.OrdinalIgnoreCase))
|
||||
?? combo.Items.OfType<ComboBoxItem>().FirstOrDefault();
|
||||
}
|
||||
|
||||
var secondMode = ClockSecondHandMode.Normalize(LoadSnapshot().WorldClockSecondHandMode);
|
||||
TickRadioButton.IsChecked = string.Equals(secondMode, ClockSecondHandMode.Tick, StringComparison.OrdinalIgnoreCase);
|
||||
SweepRadioButton.IsChecked = string.Equals(secondMode, ClockSecondHandMode.Sweep, StringComparison.OrdinalIgnoreCase);
|
||||
_suppressEvents = false;
|
||||
}
|
||||
|
||||
private void OnTimeZoneSelectionChanged(object? sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
_ = sender;
|
||||
_ = e;
|
||||
if (_suppressEvents)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
SaveState();
|
||||
}
|
||||
|
||||
private void OnSecondHandChanged(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
_ = sender;
|
||||
_ = e;
|
||||
if (_suppressEvents)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_suppressEvents = true;
|
||||
if (sender == TickRadioButton)
|
||||
{
|
||||
TickRadioButton.IsChecked = true;
|
||||
SweepRadioButton.IsChecked = false;
|
||||
}
|
||||
else if (sender == SweepRadioButton)
|
||||
{
|
||||
SweepRadioButton.IsChecked = true;
|
||||
TickRadioButton.IsChecked = false;
|
||||
}
|
||||
|
||||
if (TickRadioButton.IsChecked != true && SweepRadioButton.IsChecked != true)
|
||||
{
|
||||
TickRadioButton.IsChecked = true;
|
||||
}
|
||||
|
||||
_suppressEvents = false;
|
||||
SaveState();
|
||||
}
|
||||
|
||||
private void SaveState()
|
||||
{
|
||||
var snapshot = LoadSnapshot();
|
||||
snapshot.WorldClockTimeZoneIds = _timeZoneCombos
|
||||
.Select(combo => combo.SelectedItem is ComboBoxItem item && item.Tag is string tag ? tag : TimeZoneInfo.Local.Id)
|
||||
.ToList();
|
||||
snapshot.WorldClockSecondHandMode = SweepRadioButton.IsChecked == true
|
||||
? ClockSecondHandMode.Sweep
|
||||
: ClockSecondHandMode.Tick;
|
||||
|
||||
SaveSnapshot(
|
||||
snapshot,
|
||||
nameof(ComponentSettingsSnapshot.WorldClockTimeZoneIds),
|
||||
nameof(ComponentSettingsSnapshot.WorldClockSecondHandMode));
|
||||
}
|
||||
|
||||
private static string FormatTimeZone(TimeZoneInfo timeZone)
|
||||
{
|
||||
var offset = timeZone.GetUtcOffset(DateTime.UtcNow);
|
||||
var sign = offset >= TimeSpan.Zero ? "+" : "-";
|
||||
var totalMinutes = Math.Abs((int)offset.TotalMinutes);
|
||||
var hours = totalMinutes / 60;
|
||||
var minutes = totalMinutes % 60;
|
||||
return $"(UTC{sign}{hours:D2}:{minutes:D2}) {timeZone.StandardName}";
|
||||
}
|
||||
}
|
||||
@@ -508,6 +508,17 @@ public partial class MainWindow
|
||||
|
||||
if (_selectedDesktopComponentHost is not null)
|
||||
{
|
||||
if (TryGetSelectedDesktopPlacement(out var selectedPlacement) &&
|
||||
_componentEditorRegistry.TryGetDescriptor(selectedPlacement.ComponentId, out _))
|
||||
{
|
||||
actions.Add(new TaskbarActionItem(
|
||||
TaskbarActionId.EditComponent,
|
||||
L("component.edit", "Edit"),
|
||||
"Edit",
|
||||
IsVisible: true,
|
||||
CommandKey: "component.edit"));
|
||||
}
|
||||
|
||||
actions.Add(new TaskbarActionItem(
|
||||
TaskbarActionId.DeleteComponent,
|
||||
L("component.delete", "Delete"),
|
||||
@@ -606,15 +617,12 @@ public partial class MainWindow
|
||||
action.Id == TaskbarActionId.DeleteComponent;
|
||||
var isHideAction = action.Id == TaskbarActionId.HideLauncherEntry;
|
||||
|
||||
Symbol iconSymbol;
|
||||
if (isDeleteAction || isHideAction)
|
||||
var iconSymbol = action.Id switch
|
||||
{
|
||||
iconSymbol = Symbol.Delete;
|
||||
}
|
||||
else
|
||||
{
|
||||
iconSymbol = Symbol.Add;
|
||||
}
|
||||
TaskbarActionId.EditComponent => Symbol.Edit,
|
||||
_ when isDeleteAction || isHideAction => Symbol.Delete,
|
||||
_ => Symbol.Add
|
||||
};
|
||||
|
||||
Control icon = new SymbolIcon
|
||||
{
|
||||
@@ -675,6 +683,9 @@ public partial class MainWindow
|
||||
case "component.delete":
|
||||
DeleteSelectedComponent();
|
||||
break;
|
||||
case "component.edit":
|
||||
OpenSelectedComponentEditor();
|
||||
break;
|
||||
case "launcher.hide":
|
||||
HideSelectedLauncherEntry();
|
||||
break;
|
||||
@@ -695,6 +706,11 @@ public partial class MainWindow
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.Equals(_componentEditorWindowService.CurrentPlacementId, placement.PlacementId, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
_componentEditorWindowService.Close();
|
||||
}
|
||||
|
||||
ClearTimeZoneServiceBindings(_selectedDesktopComponentHost);
|
||||
DisposeComponentIfNeeded(_selectedDesktopComponentHost);
|
||||
|
||||
@@ -713,6 +729,90 @@ public partial class MainWindow
|
||||
PersistSettings();
|
||||
}
|
||||
|
||||
private void OpenSelectedComponentEditor()
|
||||
{
|
||||
if (!TryGetSelectedDesktopPlacement(out var placement) ||
|
||||
!_componentEditorRegistry.TryGetDescriptor(placement.ComponentId, out var descriptor))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_componentEditorWindowService.Open(new ComponentEditorOpenRequest(
|
||||
Owner: this,
|
||||
Descriptor: descriptor,
|
||||
ComponentId: placement.ComponentId,
|
||||
PlacementId: placement.PlacementId,
|
||||
RefreshAction: () => RefreshDesktopComponentPlacement(placement.PlacementId)));
|
||||
}
|
||||
|
||||
private bool TryGetSelectedDesktopPlacement(out DesktopComponentPlacementSnapshot placement)
|
||||
{
|
||||
placement = null!;
|
||||
if (_selectedDesktopComponentHost?.Tag is not string placementId)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var matchedPlacement = _desktopComponentPlacements.FirstOrDefault(candidate =>
|
||||
string.Equals(candidate.PlacementId, placementId, StringComparison.OrdinalIgnoreCase));
|
||||
if (matchedPlacement is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
placement = matchedPlacement;
|
||||
return true;
|
||||
}
|
||||
|
||||
private void RefreshDesktopComponentPlacement(string placementId)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(placementId))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var placement = _desktopComponentPlacements.FirstOrDefault(candidate =>
|
||||
string.Equals(candidate.PlacementId, placementId, StringComparison.OrdinalIgnoreCase));
|
||||
if (placement is null ||
|
||||
!_desktopPageComponentGrids.TryGetValue(placement.PageIndex, out var pageGrid))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var host = pageGrid.Children
|
||||
.OfType<Border>()
|
||||
.FirstOrDefault(candidate => string.Equals(candidate.Tag as string, placementId, StringComparison.OrdinalIgnoreCase));
|
||||
if (host is null)
|
||||
{
|
||||
RestoreDesktopPageComponents(placement.PageIndex);
|
||||
ApplyTaskbarActionVisibility(GetCurrentTaskbarContext());
|
||||
return;
|
||||
}
|
||||
|
||||
var component = CreateDesktopComponentControl(placement.ComponentId, placement.PlacementId, placement.PageIndex);
|
||||
if (component is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (TryGetContentHost(host) is not Border contentHost)
|
||||
{
|
||||
RestoreDesktopPageComponents(placement.PageIndex);
|
||||
ApplyTaskbarActionVisibility(GetCurrentTaskbarContext());
|
||||
return;
|
||||
}
|
||||
|
||||
ClearTimeZoneServiceBindings(host);
|
||||
DisposeComponentIfNeeded(host);
|
||||
contentHost.Child = component;
|
||||
ApplyDesktopEditStateToHost(host, _isComponentLibraryOpen);
|
||||
UpdateDesktopPageAwareComponentContext();
|
||||
if (_selectedDesktopComponentHost == host)
|
||||
{
|
||||
ApplySelectionStateToHost(host, true);
|
||||
}
|
||||
}
|
||||
|
||||
private static void DisposeComponentIfNeeded(Border host)
|
||||
{
|
||||
if (TryGetContentHost(host) is Border contentHost && contentHost.Child is Control componentControl)
|
||||
@@ -724,7 +824,7 @@ public partial class MainWindow
|
||||
}
|
||||
}
|
||||
|
||||
// Component settings popup UI is removed in API-only settings hard-cut mode.
|
||||
// Legacy in-window popup editor is removed; component editing now routes through the Material editor window service.
|
||||
|
||||
private void AddDesktopPage()
|
||||
{
|
||||
@@ -1680,6 +1780,9 @@ public partial class MainWindow
|
||||
ApplySelectionStateToHost(_selectedDesktopComponentHost, false);
|
||||
_selectedDesktopComponentHost = null;
|
||||
}
|
||||
|
||||
_componentEditorWindowService.Close();
|
||||
ApplyTaskbarActionVisibility(GetCurrentTaskbarContext());
|
||||
}
|
||||
|
||||
private void BeginDesktopComponentMoveDrag(Border sourceHost, DesktopComponentPlacementSnapshot placement, PointerPressedEventArgs e)
|
||||
|
||||
@@ -158,7 +158,7 @@ public partial class MainWindow
|
||||
_weatherLongitude = snapshot.WeatherLongitude;
|
||||
_weatherAutoRefreshLocation = snapshot.WeatherAutoRefreshLocation;
|
||||
_weatherExcludedAlertsRaw = snapshot.WeatherExcludedAlerts ?? string.Empty;
|
||||
_weatherIconPackId = string.IsNullOrWhiteSpace(snapshot.WeatherIconPackId) ? "FluentRegular" : snapshot.WeatherIconPackId;
|
||||
_weatherIconPackId = string.IsNullOrWhiteSpace(snapshot.WeatherIconPackId) ? "HyperOS3" : snapshot.WeatherIconPackId;
|
||||
_weatherNoTlsRequests = snapshot.WeatherNoTlsRequests;
|
||||
}
|
||||
|
||||
@@ -488,6 +488,7 @@ public partial class MainWindow
|
||||
|
||||
private AppSettingsSnapshot BuildAppSettingsSnapshot()
|
||||
{
|
||||
var latestWeatherState = _weatherSettingsService.Get();
|
||||
return new AppSettingsSnapshot
|
||||
{
|
||||
GridShortSideCells = _targetShortSideCells,
|
||||
@@ -500,15 +501,16 @@ public partial class MainWindow
|
||||
WallpaperColor = _wallpaperSolidColor?.ToString(),
|
||||
LanguageCode = _languageCode,
|
||||
TimeZoneId = _timeZoneService.CurrentTimeZone.Id,
|
||||
WeatherLocationMode = _weatherLocationMode.ToString(),
|
||||
WeatherLocationKey = _weatherLocationKey,
|
||||
WeatherLocationName = _weatherLocationName,
|
||||
WeatherLatitude = _weatherLatitude,
|
||||
WeatherLongitude = _weatherLongitude,
|
||||
WeatherAutoRefreshLocation = _weatherAutoRefreshLocation,
|
||||
WeatherExcludedAlerts = _weatherExcludedAlertsRaw,
|
||||
WeatherIconPackId = _weatherIconPackId,
|
||||
WeatherNoTlsRequests = _weatherNoTlsRequests,
|
||||
WeatherLocationMode = latestWeatherState.LocationMode,
|
||||
WeatherLocationKey = latestWeatherState.LocationKey,
|
||||
WeatherLocationName = latestWeatherState.LocationName,
|
||||
WeatherLatitude = latestWeatherState.Latitude,
|
||||
WeatherLongitude = latestWeatherState.Longitude,
|
||||
WeatherAutoRefreshLocation = latestWeatherState.AutoRefreshLocation,
|
||||
WeatherLocationQuery = latestWeatherState.LocationQuery,
|
||||
WeatherExcludedAlerts = latestWeatherState.ExcludedAlerts,
|
||||
WeatherIconPackId = latestWeatherState.IconPackId,
|
||||
WeatherNoTlsRequests = latestWeatherState.NoTlsRequests,
|
||||
AutoStartWithWindows = _autoStartWithWindows,
|
||||
AppRenderMode = _selectedAppRenderMode,
|
||||
TopStatusComponentIds = [.. _topStatusComponentIds],
|
||||
|
||||
@@ -95,7 +95,9 @@ public partial class MainWindow : Window, ISettingsWindowAnchorProvider
|
||||
private readonly ICalculatorDataService _calculatorDataService = new CalculatorDataService();
|
||||
private readonly ComponentRegistry _componentRegistry;
|
||||
private readonly DesktopComponentRuntimeRegistry _componentRuntimeRegistry;
|
||||
private readonly DesktopComponentEditorRegistry _componentEditorRegistry;
|
||||
private readonly IComponentLibraryService _componentLibraryService;
|
||||
private readonly IComponentEditorWindowService _componentEditorWindowService;
|
||||
private readonly IEmbeddedComponentLibraryService _componentLibraryWindowService = new EmbeddedComponentLibraryService();
|
||||
private ComponentLibraryWindow? _detachedComponentLibraryWindow;
|
||||
private readonly FluentAvaloniaTheme? _fluentAvaloniaTheme;
|
||||
@@ -164,7 +166,7 @@ public partial class MainWindow : Window, ISettingsWindowAnchorProvider
|
||||
private double _weatherLongitude = 116.4074;
|
||||
private bool _weatherAutoRefreshLocation;
|
||||
private string _weatherExcludedAlertsRaw = string.Empty;
|
||||
private string _weatherIconPackId = "FluentRegular";
|
||||
private string _weatherIconPackId = "HyperOS3";
|
||||
private bool _weatherNoTlsRequests;
|
||||
private bool _autoStartWithWindows;
|
||||
private bool _suppressAutoStartToggleEvents;
|
||||
@@ -197,7 +199,11 @@ public partial class MainWindow : Window, ISettingsWindowAnchorProvider
|
||||
_componentRegistry,
|
||||
pluginRuntimeService,
|
||||
_settingsFacade);
|
||||
_componentEditorRegistry = DesktopComponentEditorRegistryFactory.Create(
|
||||
_componentRegistry,
|
||||
pluginRuntimeService);
|
||||
_componentLibraryService = new ComponentLibraryService(_componentRegistry, _componentRuntimeRegistry);
|
||||
_componentEditorWindowService = new ComponentEditorWindowService(_settingsFacade);
|
||||
_fluentAvaloniaTheme = Application.Current?.Styles.OfType<FluentAvaloniaTheme>().FirstOrDefault();
|
||||
_settingsService.Changed += OnSettingsChanged;
|
||||
PropertyChanged += OnWindowPropertyChanged;
|
||||
@@ -307,6 +313,7 @@ public partial class MainWindow : Window, ISettingsWindowAnchorProvider
|
||||
protected override void OnClosed(EventArgs e)
|
||||
{
|
||||
PersistSettings();
|
||||
_componentEditorWindowService.Close();
|
||||
if (_detachedComponentLibraryWindow is not null)
|
||||
{
|
||||
_detachedComponentLibraryWindow.AddComponentRequested -= OnDetachedComponentLibraryAddComponentRequested;
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:vm="using:LanMountainDesktop.ViewModels"
|
||||
xmlns:controls="using:LanMountainDesktop.Controls"
|
||||
xmlns:ui="using:FluentAvalonia.UI.Controls"
|
||||
xmlns:fi="using:FluentIcons.Avalonia.Fluent"
|
||||
x:Class="LanMountainDesktop.Views.SettingsPages.LauncherSettingsPage"
|
||||
x:DataType="vm:LauncherSettingsPageViewModel">
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||
<StackPanel Classes="settings-page-container">
|
||||
|
||||
<Border Classes="settings-section-card">
|
||||
<Grid ColumnDefinitions="Auto,*,Auto"
|
||||
ColumnSpacing="18">
|
||||
<Border Classes="settings-section-card-icon-host"
|
||||
Width="72"
|
||||
Height="72"
|
||||
Padding="10">
|
||||
<fi:SymbolIcon Symbol="Apps"
|
||||
FontSize="34"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center" />
|
||||
</Border>
|
||||
|
||||
<StackPanel Grid.Column="1"
|
||||
Spacing="4"
|
||||
VerticalAlignment="Center">
|
||||
<TextBlock Classes="settings-card-header"
|
||||
Text="{Binding LauncherHeader}" />
|
||||
<TextBlock Classes="settings-card-description"
|
||||
Text="{Binding LauncherSubtitle}" />
|
||||
<TextBlock Classes="settings-item-description"
|
||||
Margin="0,10,0,0"
|
||||
Text="{Binding HiddenHint}"
|
||||
TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Grid.Column="2"
|
||||
Spacing="4"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Center">
|
||||
<TextBlock Classes="settings-section-title"
|
||||
FontSize="28"
|
||||
HorizontalAlignment="Right"
|
||||
Margin="0"
|
||||
Text="{Binding HiddenCountText}" />
|
||||
<TextBlock Classes="settings-item-description"
|
||||
HorizontalAlignment="Right"
|
||||
Text="{Binding HiddenSummary}" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<controls:IconText Icon="Apps"
|
||||
Text="{Binding HiddenHeader}"
|
||||
Margin="0,0,0,4" />
|
||||
|
||||
<ui:SettingsExpander Classes="settings-expander-card"
|
||||
Header="{Binding HiddenHeader}"
|
||||
Description="{Binding HiddenDescription}"
|
||||
IsExpanded="True">
|
||||
<ui:SettingsExpander.IconSource>
|
||||
<fi:SymbolIconSource Symbol="Apps" />
|
||||
</ui:SettingsExpander.IconSource>
|
||||
<ui:SettingsExpanderItem>
|
||||
<StackPanel Spacing="8">
|
||||
<TextBlock Classes="settings-item-description"
|
||||
IsVisible="{Binding IsHiddenItemsEmpty}"
|
||||
Margin="0,0,0,4"
|
||||
Text="{Binding HiddenEmptyText}"
|
||||
TextWrapping="Wrap" />
|
||||
|
||||
<ItemsControl ItemsSource="{Binding HiddenItems}"
|
||||
IsVisible="{Binding HasHiddenItems}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:LauncherHiddenItemViewModel">
|
||||
<ui:SettingsExpanderItem Content="{Binding DisplayName}"
|
||||
Description="{Binding TypeLabel}"
|
||||
IsClickEnabled="False">
|
||||
<ui:SettingsExpanderItem.IconSource>
|
||||
<fi:SymbolIconSource Symbol="{Binding IconSymbol}" />
|
||||
</ui:SettingsExpanderItem.IconSource>
|
||||
<ui:SettingsExpanderItem.Footer>
|
||||
<Button Command="{Binding RestoreCommand}"
|
||||
Content="{Binding RestoreButtonText}"
|
||||
VerticalAlignment="Center" />
|
||||
</ui:SettingsExpanderItem.Footer>
|
||||
</ui:SettingsExpanderItem>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</StackPanel>
|
||||
</ui:SettingsExpanderItem>
|
||||
</ui:SettingsExpander>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,31 @@
|
||||
using LanMountainDesktop.PluginSdk;
|
||||
using LanMountainDesktop.Services.Settings;
|
||||
using LanMountainDesktop.ViewModels;
|
||||
|
||||
namespace LanMountainDesktop.Views.SettingsPages;
|
||||
|
||||
[SettingsPageInfo(
|
||||
"launcher",
|
||||
"App Launcher",
|
||||
SettingsPageCategory.Components,
|
||||
IconKey = "Apps",
|
||||
SortOrder = 10,
|
||||
Scope = SettingsScope.Launcher,
|
||||
TitleLocalizationKey = "settings.launcher.title",
|
||||
DescriptionLocalizationKey = "settings.launcher.description")]
|
||||
public partial class LauncherSettingsPage : SettingsPageBase
|
||||
{
|
||||
public LauncherSettingsPage()
|
||||
: this(new LauncherSettingsPageViewModel(HostSettingsFacadeProvider.GetOrCreate()))
|
||||
{
|
||||
}
|
||||
|
||||
public LauncherSettingsPage(LauncherSettingsPageViewModel viewModel)
|
||||
{
|
||||
ViewModel = viewModel;
|
||||
DataContext = ViewModel;
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
public LauncherSettingsPageViewModel ViewModel { get; }
|
||||
}
|
||||
263
LanMountainDesktop/Views/SettingsPages/WeatherSettingsPage.axaml
Normal file
263
LanMountainDesktop/Views/SettingsPages/WeatherSettingsPage.axaml
Normal file
@@ -0,0 +1,263 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:vm="using:LanMountainDesktop.ViewModels"
|
||||
xmlns:models="using:LanMountainDesktop.Models"
|
||||
xmlns:controls="using:LanMountainDesktop.Controls"
|
||||
xmlns:ui="using:FluentAvalonia.UI.Controls"
|
||||
xmlns:fi="using:FluentIcons.Avalonia.Fluent"
|
||||
x:Class="LanMountainDesktop.Views.SettingsPages.WeatherSettingsPage"
|
||||
x:DataType="vm:WeatherSettingsPageViewModel">
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||
<StackPanel Classes="settings-page-container">
|
||||
|
||||
<Border Classes="settings-section-card">
|
||||
<Grid ColumnDefinitions="Auto,*,Auto" ColumnSpacing="18">
|
||||
<Border Classes="settings-section-card-icon-host"
|
||||
Width="72"
|
||||
Height="72"
|
||||
Padding="10">
|
||||
<Image Source="{Binding PreviewIcon}"
|
||||
Stretch="Uniform" />
|
||||
</Border>
|
||||
|
||||
<StackPanel Grid.Column="1"
|
||||
Spacing="4"
|
||||
VerticalAlignment="Center">
|
||||
<TextBlock Classes="settings-card-header"
|
||||
Text="{Binding PreviewHeader}" />
|
||||
<TextBlock Classes="settings-card-description"
|
||||
Text="{Binding PreviewDescription}" />
|
||||
<TextBlock Classes="settings-section-title"
|
||||
FontSize="24"
|
||||
Margin="0,10,0,0"
|
||||
Text="{Binding PreviewTemperature}" />
|
||||
<TextBlock Classes="settings-item-label"
|
||||
Text="{Binding PreviewLocation}" />
|
||||
<TextBlock Classes="settings-item-description"
|
||||
Text="{Binding PreviewCondition}" />
|
||||
<TextBlock Classes="settings-item-description"
|
||||
Text="{Binding PreviewUpdated}" />
|
||||
<TextBlock Classes="settings-item-description"
|
||||
Text="{Binding PreviewStatus}" />
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Grid.Column="2"
|
||||
Spacing="12"
|
||||
VerticalAlignment="Center">
|
||||
<Button Classes="settings-accent-button"
|
||||
Command="{Binding RefreshPreviewCommand}"
|
||||
Content="{Binding RefreshButtonText}" />
|
||||
<ui:ProgressRing IsIndeterminate="True"
|
||||
IsVisible="{Binding IsRefreshingPreview}"
|
||||
Width="28"
|
||||
Height="28"
|
||||
HorizontalAlignment="Center" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<ui:SettingsExpander Classes="settings-expander-card"
|
||||
Header="{Binding LocationSourceHeader}"
|
||||
Description="{Binding LocationSourceDescription}">
|
||||
<ui:SettingsExpander.IconSource>
|
||||
<fi:SymbolIconSource Symbol="WeatherMoon" />
|
||||
</ui:SettingsExpander.IconSource>
|
||||
<ui:SettingsExpander.Footer>
|
||||
<ComboBox Width="220"
|
||||
ItemsSource="{Binding LocationModes}"
|
||||
SelectedItem="{Binding SelectedLocationMode}">
|
||||
<ComboBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:SelectionOption">
|
||||
<TextBlock Text="{Binding Label}" />
|
||||
</DataTemplate>
|
||||
</ComboBox.ItemTemplate>
|
||||
</ComboBox>
|
||||
</ui:SettingsExpander.Footer>
|
||||
<ui:SettingsExpanderItem>
|
||||
<TextBlock Classes="settings-item-description"
|
||||
Text="{Binding CurrentLocationSummary}"
|
||||
TextWrapping="Wrap" />
|
||||
</ui:SettingsExpanderItem>
|
||||
</ui:SettingsExpander>
|
||||
|
||||
<ui:SettingsExpander Classes="settings-expander-card"
|
||||
Header="{Binding CitySearchHeader}"
|
||||
Description="{Binding CitySearchDescription}"
|
||||
IsVisible="{Binding IsCitySearchMode}">
|
||||
<ui:SettingsExpander.IconSource>
|
||||
<fi:SymbolIconSource Symbol="Search" />
|
||||
</ui:SettingsExpander.IconSource>
|
||||
<ui:SettingsExpander.Footer>
|
||||
<Button Classes="settings-accent-button"
|
||||
Command="{Binding ApplyCitySelectionCommand}"
|
||||
Content="{Binding ApplyCityButtonText}" />
|
||||
</ui:SettingsExpander.Footer>
|
||||
<ui:SettingsExpanderItem>
|
||||
<StackPanel Spacing="14">
|
||||
<Grid ColumnDefinitions="*,Auto"
|
||||
ColumnSpacing="12">
|
||||
<TextBox Text="{Binding SearchKeyword}"
|
||||
Watermark="{Binding SearchPlaceholder}" />
|
||||
<Button Grid.Column="1"
|
||||
Command="{Binding SearchCommand}"
|
||||
Content="{Binding SearchButtonText}" />
|
||||
</Grid>
|
||||
|
||||
<ui:ProgressRing IsIndeterminate="True"
|
||||
IsVisible="{Binding IsSearching}"
|
||||
Width="24"
|
||||
Height="24"
|
||||
HorizontalAlignment="Left" />
|
||||
|
||||
<TextBlock Classes="settings-item-description"
|
||||
Text="{Binding SearchStatus}"
|
||||
TextWrapping="Wrap" />
|
||||
|
||||
<ListBox Classes="weather-settings-search-results"
|
||||
ItemsSource="{Binding SearchResults}"
|
||||
SelectedItem="{Binding SelectedSearchResult}">
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="models:WeatherLocation">
|
||||
<Grid ColumnDefinitions="Auto,*"
|
||||
ColumnSpacing="12">
|
||||
<fi:SymbolIcon Classes="icon-s"
|
||||
Margin="0,2,0,0"
|
||||
Symbol="City" />
|
||||
<StackPanel Grid.Column="1"
|
||||
Spacing="4">
|
||||
<TextBlock Classes="settings-item-label"
|
||||
Text="{Binding Name}" />
|
||||
<TextBlock Classes="settings-item-description"
|
||||
Text="{Binding Affiliation}" />
|
||||
<TextBlock Classes="settings-item-description"
|
||||
Text="{Binding LocationKey}" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
</StackPanel>
|
||||
</ui:SettingsExpanderItem>
|
||||
</ui:SettingsExpander>
|
||||
|
||||
<ui:SettingsExpander Classes="settings-expander-card"
|
||||
Header="{Binding CoordinatesHeader}"
|
||||
Description="{Binding CoordinatesDescription}"
|
||||
IsVisible="{Binding IsCoordinatesMode}">
|
||||
<ui:SettingsExpander.IconSource>
|
||||
<fi:SymbolIconSource Symbol="Location" />
|
||||
</ui:SettingsExpander.IconSource>
|
||||
<ui:SettingsExpander.Footer>
|
||||
<Button Classes="settings-accent-button"
|
||||
Command="{Binding ApplyCoordinatesCommand}"
|
||||
Content="{Binding ApplyCoordinatesButtonText}" />
|
||||
</ui:SettingsExpander.Footer>
|
||||
<ui:SettingsExpanderItem>
|
||||
<Grid ColumnDefinitions="*,*"
|
||||
Classes="settings-inline-pair">
|
||||
<StackPanel Classes="settings-item">
|
||||
<TextBlock Classes="settings-item-label"
|
||||
Text="{Binding LatitudeLabel}" />
|
||||
<NumericUpDown Minimum="-90"
|
||||
Maximum="90"
|
||||
Increment="0.0001"
|
||||
FormatString="F4"
|
||||
Value="{Binding Latitude}" />
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Grid.Column="1"
|
||||
Classes="settings-item">
|
||||
<TextBlock Classes="settings-item-label"
|
||||
Text="{Binding LongitudeLabel}" />
|
||||
<NumericUpDown Minimum="-180"
|
||||
Maximum="180"
|
||||
Increment="0.0001"
|
||||
FormatString="F4"
|
||||
Value="{Binding Longitude}" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</ui:SettingsExpanderItem>
|
||||
<ui:SettingsExpanderItem>
|
||||
<TextBox Text="{Binding LocationKey}"
|
||||
Watermark="{Binding LocationKeyPlaceholder}" />
|
||||
</ui:SettingsExpanderItem>
|
||||
<ui:SettingsExpanderItem>
|
||||
<TextBox Text="{Binding LocationName}"
|
||||
Watermark="{Binding LocationNamePlaceholder}" />
|
||||
</ui:SettingsExpanderItem>
|
||||
</ui:SettingsExpander>
|
||||
|
||||
<ui:SettingsExpander Classes="settings-expander-card"
|
||||
Header="{Binding LocationServicesHeader}"
|
||||
Description="{Binding LocationServicesDescription}">
|
||||
<ui:SettingsExpander.IconSource>
|
||||
<fi:SymbolIconSource Symbol="Location" />
|
||||
</ui:SettingsExpander.IconSource>
|
||||
<ui:SettingsExpander.Footer>
|
||||
<Button Classes="settings-accent-button"
|
||||
Command="{Binding UseCurrentLocationCommand}"
|
||||
Content="{Binding UseCurrentLocationButtonText}"
|
||||
IsVisible="{Binding IsLocationSupported}" />
|
||||
</ui:SettingsExpander.Footer>
|
||||
<ui:SettingsExpanderItem>
|
||||
<Grid ColumnDefinitions="*,Auto"
|
||||
ColumnSpacing="16">
|
||||
<StackPanel Classes="settings-item">
|
||||
<TextBlock Classes="settings-item-label"
|
||||
Text="{Binding AutoRefreshLabel}" />
|
||||
<TextBlock Classes="settings-item-description"
|
||||
Text="{Binding LocationActionStatus}"
|
||||
TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
<ToggleSwitch Grid.Column="1"
|
||||
IsChecked="{Binding AutoRefreshLocation}"
|
||||
IsEnabled="{Binding IsLocationSupported}" />
|
||||
</Grid>
|
||||
</ui:SettingsExpanderItem>
|
||||
<ui:SettingsExpanderItem IsVisible="{Binding IsRefreshingLocation}">
|
||||
<ui:ProgressRing IsIndeterminate="True"
|
||||
IsVisible="{Binding IsRefreshingLocation}"
|
||||
Width="28"
|
||||
Height="28"
|
||||
HorizontalAlignment="Left" />
|
||||
</ui:SettingsExpanderItem>
|
||||
</ui:SettingsExpander>
|
||||
|
||||
<ui:SettingsExpander Classes="settings-expander-card"
|
||||
Header="{Binding AlertFilterHeader}"
|
||||
Description="{Binding AlertFilterDescription}">
|
||||
<ui:SettingsExpander.IconSource>
|
||||
<fi:SymbolIconSource Symbol="Warning" />
|
||||
</ui:SettingsExpander.IconSource>
|
||||
<ui:SettingsExpander.Footer>
|
||||
<TextBox Width="360"
|
||||
MinHeight="120"
|
||||
AcceptsReturn="True"
|
||||
TextWrapping="Wrap"
|
||||
Text="{Binding ExcludedAlerts}" />
|
||||
</ui:SettingsExpander.Footer>
|
||||
</ui:SettingsExpander>
|
||||
|
||||
<ui:SettingsExpander Classes="settings-expander-card"
|
||||
Header="{Binding RequestHeader}"
|
||||
Description="{Binding RequestDescription}">
|
||||
<ui:SettingsExpander.IconSource>
|
||||
<fi:SymbolIconSource Symbol="ShieldDismiss" />
|
||||
</ui:SettingsExpander.IconSource>
|
||||
<ui:SettingsExpander.Footer>
|
||||
<ToggleSwitch IsChecked="{Binding NoTlsRequests}" />
|
||||
</ui:SettingsExpander.Footer>
|
||||
<ui:SettingsExpanderItem>
|
||||
<TextBlock Classes="settings-item-description"
|
||||
Text="{Binding NoTlsToggleText}"
|
||||
TextWrapping="Wrap" />
|
||||
</ui:SettingsExpanderItem>
|
||||
</ui:SettingsExpander>
|
||||
|
||||
<TextBlock Classes="settings-item-description"
|
||||
Margin="0,8,0,0"
|
||||
Text="{Binding FooterHint}"
|
||||
TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,47 @@
|
||||
using LanMountainDesktop.PluginSdk;
|
||||
using LanMountainDesktop.Services;
|
||||
using LanMountainDesktop.Services.Settings;
|
||||
using LanMountainDesktop.ViewModels;
|
||||
|
||||
namespace LanMountainDesktop.Views.SettingsPages;
|
||||
|
||||
[SettingsPageInfo(
|
||||
"weather",
|
||||
"Weather",
|
||||
SettingsPageCategory.Appearance,
|
||||
IconKey = "WeatherMoon",
|
||||
SortOrder = 18,
|
||||
TitleLocalizationKey = "settings.weather.title",
|
||||
DescriptionLocalizationKey = "settings.weather.description")]
|
||||
public partial class WeatherSettingsPage : SettingsPageBase
|
||||
{
|
||||
public WeatherSettingsPage()
|
||||
: this(CreateDefaultViewModel())
|
||||
{
|
||||
}
|
||||
|
||||
public WeatherSettingsPage(WeatherSettingsPageViewModel viewModel)
|
||||
{
|
||||
ViewModel = viewModel;
|
||||
DataContext = ViewModel;
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
public WeatherSettingsPageViewModel ViewModel { get; }
|
||||
|
||||
private static WeatherSettingsPageViewModel CreateDefaultViewModel()
|
||||
{
|
||||
var settingsFacade = HostSettingsFacadeProvider.GetOrCreate();
|
||||
var localizationService = new LocalizationService();
|
||||
var locationService = HostLocationServiceProvider.GetOrCreate();
|
||||
var weatherLocationRefreshService = new WeatherLocationRefreshService(
|
||||
settingsFacade,
|
||||
locationService,
|
||||
localizationService);
|
||||
return new WeatherSettingsPageViewModel(
|
||||
settingsFacade,
|
||||
localizationService,
|
||||
locationService,
|
||||
weatherLocationRefreshService);
|
||||
}
|
||||
}
|
||||
@@ -634,6 +634,8 @@ public partial class SettingsWindow : Window, ISettingsPageHostContext
|
||||
{
|
||||
"DesignIdeas" => Symbol.Color,
|
||||
"Image" => Symbol.Image,
|
||||
"WeatherMoon" => Symbol.WeatherMoon,
|
||||
"Apps" => Symbol.Apps,
|
||||
"GridDots" => Symbol.GridDots,
|
||||
"PuzzlePiece" => Symbol.PuzzlePiece,
|
||||
"Info" => Symbol.Info,
|
||||
|
||||
Reference in New Issue
Block a user