settings_re8

This commit is contained in:
lincube
2026-03-14 22:45:09 +08:00
parent 91f9f3d6fb
commit 689be7b585
54 changed files with 5356 additions and 30 deletions

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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", "当前源:国外镜像(艺术馆推荐)");
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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