mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-20 23:54:26 +08:00
0.2.5
课表组件、天气组件全面升级。
This commit is contained in:
@@ -0,0 +1,345 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
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 LanMontainDesktop.Models;
|
||||
using LanMontainDesktop.Services;
|
||||
|
||||
namespace LanMontainDesktop.Views.Components;
|
||||
|
||||
public partial class ClassScheduleSettingsWindow : UserControl
|
||||
{
|
||||
private readonly AppSettingsService _appSettingsService = new();
|
||||
private readonly LocalizationService _localizationService = new();
|
||||
private readonly List<ImportedClassScheduleSnapshot> _importedSchedules = [];
|
||||
private string _activeScheduleId = string.Empty;
|
||||
private string _languageCode = "zh-CN";
|
||||
|
||||
public event EventHandler? SettingsChanged;
|
||||
|
||||
public ClassScheduleSettingsWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
LoadState();
|
||||
ApplyLocalization();
|
||||
RenderImportedSchedules();
|
||||
}
|
||||
|
||||
private void LoadState()
|
||||
{
|
||||
var snapshot = _appSettingsService.Load();
|
||||
_languageCode = _localizationService.NormalizeLanguageCode(snapshot.LanguageCode);
|
||||
|
||||
_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 ApplyLocalization()
|
||||
{
|
||||
TitleTextBlock.Text = L("schedule.settings.title", "课表导入");
|
||||
DescriptionTextBlock.Text = L(
|
||||
"schedule.settings.desc",
|
||||
"导入 ClassIsland 的 CSES 课表文件并选择启用项。");
|
||||
AddScheduleButtonTextBlock.Text = L("schedule.settings.add", "添加课表");
|
||||
EmptyStateTextBlock.Text = L("schedule.settings.empty", "暂无导入课表");
|
||||
}
|
||||
|
||||
private async void OnAddScheduleClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
var topLevel = TopLevel.GetTopLevel(this);
|
||||
var storageProvider = topLevel?.StorageProvider;
|
||||
if (storageProvider is null)
|
||||
{
|
||||
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;
|
||||
SaveState();
|
||||
RenderImportedSchedules();
|
||||
return;
|
||||
}
|
||||
|
||||
var displayName = Path.GetFileNameWithoutExtension(importedPath)?.Trim();
|
||||
if (string.IsNullOrWhiteSpace(displayName))
|
||||
{
|
||||
displayName = L("schedule.settings.unnamed", "未命名课表");
|
||||
}
|
||||
|
||||
var imported = new ImportedClassScheduleSnapshot
|
||||
{
|
||||
Id = Guid.NewGuid().ToString("N"),
|
||||
DisplayName = displayName,
|
||||
FilePath = importedPath
|
||||
};
|
||||
|
||||
_importedSchedules.Add(imported);
|
||||
_activeScheduleId = imported.Id;
|
||||
SaveState();
|
||||
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),
|
||||
"LanMontainDesktop",
|
||||
"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();
|
||||
|
||||
if (_importedSchedules.Count == 0)
|
||||
{
|
||||
EmptyStateTextBlock.IsVisible = true;
|
||||
return;
|
||||
}
|
||||
|
||||
EmptyStateTextBlock.IsVisible = false;
|
||||
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,
|
||||
FontSize = 14,
|
||||
FontWeight = FontWeight.SemiBold,
|
||||
Foreground = ResolveThemeBrush("AdaptiveTextPrimaryBrush", "#FFEFF3FF"),
|
||||
TextTrimming = TextTrimming.CharacterEllipsis
|
||||
};
|
||||
|
||||
var path = new TextBlock
|
||||
{
|
||||
Text = item.FilePath,
|
||||
FontSize = 11,
|
||||
Foreground = ResolveThemeBrush("AdaptiveTextSecondaryBrush", "#FF99A2B5"),
|
||||
TextTrimming = TextTrimming.CharacterEllipsis,
|
||||
TextWrapping = TextWrapping.NoWrap
|
||||
};
|
||||
|
||||
var textStack = new StackPanel
|
||||
{
|
||||
Spacing = 4,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Children = { title, path }
|
||||
};
|
||||
|
||||
var deleteButton = new Button
|
||||
{
|
||||
Content = L("schedule.settings.delete", "删除"),
|
||||
Tag = item.Id,
|
||||
Padding = new Thickness(10, 6),
|
||||
MinWidth = 64,
|
||||
HorizontalAlignment = HorizontalAlignment.Right,
|
||||
VerticalAlignment = VerticalAlignment.Center
|
||||
};
|
||||
deleteButton.Click += OnDeleteScheduleClick;
|
||||
|
||||
var rowGrid = new Grid
|
||||
{
|
||||
ColumnDefinitions = new ColumnDefinitions("Auto,*,Auto"),
|
||||
ColumnSpacing = 10
|
||||
};
|
||||
rowGrid.Children.Add(selector);
|
||||
rowGrid.Children.Add(textStack);
|
||||
rowGrid.Children.Add(deleteButton);
|
||||
Grid.SetColumn(selector, 0);
|
||||
Grid.SetColumn(textStack, 1);
|
||||
Grid.SetColumn(deleteButton, 2);
|
||||
|
||||
var rowBorder = new Border
|
||||
{
|
||||
Padding = new Thickness(10, 8),
|
||||
CornerRadius = new CornerRadius(12),
|
||||
Background = ResolveThemeBrush("AdaptiveSurfaceRaisedBrush", "#1AFFFFFF"),
|
||||
BorderBrush = ResolveThemeBrush("AdaptiveButtonBorderBrush", "#22000000"),
|
||||
BorderThickness = new Thickness(1),
|
||||
Child = rowGrid
|
||||
};
|
||||
|
||||
ScheduleItemsPanel.Children.Add(rowBorder);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnScheduleSelectionChanged(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
if (sender is not RadioButton button ||
|
||||
button.IsChecked != true ||
|
||||
button.Tag is not string scheduleId)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.Equals(_activeScheduleId, scheduleId, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_activeScheduleId = scheduleId;
|
||||
SaveState();
|
||||
}
|
||||
|
||||
private void OnDeleteScheduleClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
if (sender is not Button button || button.Tag is not string scheduleId)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var target = _importedSchedules.FirstOrDefault(item =>
|
||||
string.Equals(item.Id, scheduleId, StringComparison.OrdinalIgnoreCase));
|
||||
if (target is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_importedSchedules.Remove(target);
|
||||
TryDeleteImportedFile(target.FilePath);
|
||||
if (string.Equals(_activeScheduleId, scheduleId, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
_activeScheduleId = _importedSchedules.Count > 0 ? _importedSchedules[0].Id : string.Empty;
|
||||
}
|
||||
|
||||
SaveState();
|
||||
RenderImportedSchedules();
|
||||
}
|
||||
|
||||
private void SaveState()
|
||||
{
|
||||
var snapshot = _appSettingsService.Load();
|
||||
snapshot.ImportedClassSchedules = _importedSchedules
|
||||
.Select(item => new ImportedClassScheduleSnapshot
|
||||
{
|
||||
Id = item.Id,
|
||||
DisplayName = item.DisplayName,
|
||||
FilePath = item.FilePath
|
||||
})
|
||||
.ToList();
|
||||
snapshot.ActiveImportedClassScheduleId = _activeScheduleId ?? string.Empty;
|
||||
_appSettingsService.Save(snapshot);
|
||||
SettingsChanged?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
private string L(string key, string fallback)
|
||||
{
|
||||
return _localizationService.GetString(_languageCode, key, fallback);
|
||||
}
|
||||
|
||||
private static void TryDeleteImportedFile(string? filePath)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(filePath) || !File.Exists(filePath))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
File.Delete(filePath);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Keep settings operation resilient even when file deletion fails.
|
||||
}
|
||||
}
|
||||
|
||||
private IBrush ResolveThemeBrush(string key, string fallbackHex)
|
||||
{
|
||||
if (this.TryFindResource(key, out var value) && value is IBrush brush)
|
||||
{
|
||||
return brush;
|
||||
}
|
||||
|
||||
return new SolidColorBrush(Color.Parse(fallbackHex));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user