mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-25 11:14:26 +08:00
settings_re8
This commit is contained in:
@@ -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}";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user