From c3db5af9233a63ef4cf6ab6e704d622848542a14 Mon Sep 17 00:00:00 2001 From: lincube Date: Sun, 22 Mar 2026 04:57:19 +0800 Subject: [PATCH] 0.7.4 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 首先我加了CI课程表json的读取,然后把天气时钟这个老问题也修了。 --- LanMountainDesktop/Localization/en-US.json | 30 +++- LanMountainDesktop/Localization/zh-CN.json | 26 ++- .../Models/AppSettingsSnapshot.cs | 2 + .../Models/ComponentSettingsSnapshot.cs | 5 + .../ClassIslandScheduleDataService.cs | 72 ++++++-- .../Services/Settings/SettingsContracts.cs | 8 +- .../Settings/SettingsDomainServices.cs | 19 ++- .../Services/SystemWallpaperProvider.cs | 65 ++++++++ .../Services/WallpaperImageBrushFactory.cs | 12 +- .../WallpaperSettingsPageViewModel.cs | 156 ++++++++++++++++-- .../ClassScheduleComponentEditor.axaml | 41 +++++ .../ClassScheduleComponentEditor.axaml.cs | 64 ++++++- .../Components/ClassScheduleWidget.axaml.cs | 6 +- .../OfficeRecentDocumentsWidget.axaml | 2 +- .../Components/WeatherClockWidget.axaml.cs | 45 +++-- .../Views/MainWindow.SettingsHardCut.Stubs.cs | 81 ++++++++- LanMountainDesktop/Views/MainWindow.axaml.cs | 3 + .../SettingsPages/AboutSettingsPage.axaml | 4 +- .../SettingsPages/WallpaperSettingsPage.axaml | 72 +++++++- 19 files changed, 646 insertions(+), 67 deletions(-) create mode 100644 LanMountainDesktop/Services/SystemWallpaperProvider.cs diff --git a/LanMountainDesktop/Localization/en-US.json b/LanMountainDesktop/Localization/en-US.json index 0d4ae9f..3c676f2 100644 --- a/LanMountainDesktop/Localization/en-US.json +++ b/LanMountainDesktop/Localization/en-US.json @@ -38,6 +38,27 @@ "settings.wallpaper.title": "Wallpaper", "settings.wallpaper.description": "Pick an image or video to apply as the app window wallpaper immediately.", "settings.wallpaper.current_label": "Current Wallpaper", + "settings.wallpaper.type_label": "Wallpaper Type", + "settings.wallpaper.type.image": "Image", + "settings.wallpaper.type.solid_color": "Solid Color", + "settings.wallpaper.type.system": "System Wallpaper", + "settings.wallpaper.system.label": "System Wallpaper", + "settings.wallpaper.system.unavailable": "Unable to read system wallpaper", + "settings.wallpaper.refresh_interval": "Refresh Interval", + "settings.wallpaper.refresh_now": "Refresh Now", + "settings.wallpaper.refresh.30s": "30 seconds", + "settings.wallpaper.refresh.1m": "1 minute", + "settings.wallpaper.refresh.5m": "5 minutes", + "settings.wallpaper.refresh.10m": "10 minutes", + "settings.wallpaper.refresh.15m": "15 minutes", + "settings.wallpaper.refresh.30m": "30 minutes", + "settings.wallpaper.refresh.1h": "1 hour", + "settings.wallpaper.refresh.2h": "2 hours", + "settings.wallpaper.refresh.4h": "4 hours", + "settings.wallpaper.refresh.8h": "8 hours", + "settings.wallpaper.refresh.12h": "12 hours", + "settings.wallpaper.refresh.24h": "24 hours", + "settings.wallpaper.color_label": "Wallpaper Color", "settings.wallpaper.placement_label": "Placement", "settings.wallpaper.placement_desc": "Adjust how the image fills the desktop.", "settings.wallpaper.pick_button": "Browse Files", @@ -217,7 +238,14 @@ "schedule.settings.unnamed": "Unnamed Schedule", "schedule.settings.delete": "Delete", "schedule.settings.picker_title": "Select ClassIsland schedule file", - "schedule.settings.picker_file_type": "ClassIsland CSES schedule", + "schedule.settings.picker_file_type.all": "ClassIsland Schedule Files", + "schedule.settings.picker_file_type.json": "ClassIsland Profile (JSON)", + "schedule.settings.picker_file_type.cses": "CSES Schedule (YAML)", + "schedule.settings.semester.title": "Semester Settings", + "schedule.settings.semester.start_date": "Semester Start Date", + "schedule.settings.semester.week_cycle": "Week Cycle", + "schedule.settings.semester.week_cycle_desc": "Set the week rotation cycle for multi-week schedules (e.g., 2 for odd/even weeks).", + "schedule.settings.semester.week_cycle_format": "{0}-week rotation", "worldclock.settings.title": "World Clock Settings", "worldclock.settings.desc": "Choose a time zone for each of the four clocks.", "worldclock.settings.clock_1": "Clock 1", diff --git a/LanMountainDesktop/Localization/zh-CN.json b/LanMountainDesktop/Localization/zh-CN.json index 5037b02..dd3f02d 100644 --- a/LanMountainDesktop/Localization/zh-CN.json +++ b/LanMountainDesktop/Localization/zh-CN.json @@ -41,6 +41,23 @@ "settings.wallpaper.type_label": "壁纸类型", "settings.wallpaper.type.image": "图片", "settings.wallpaper.type.solid_color": "纯色", + "settings.wallpaper.type.system": "系统壁纸", + "settings.wallpaper.system.label": "系统壁纸", + "settings.wallpaper.system.unavailable": "无法读取系统壁纸", + "settings.wallpaper.refresh_interval": "刷新频率", + "settings.wallpaper.refresh_now": "立即刷新", + "settings.wallpaper.refresh.30s": "30 秒", + "settings.wallpaper.refresh.1m": "1 分钟", + "settings.wallpaper.refresh.5m": "5 分钟", + "settings.wallpaper.refresh.10m": "10 分钟", + "settings.wallpaper.refresh.15m": "15 分钟", + "settings.wallpaper.refresh.30m": "30 分钟", + "settings.wallpaper.refresh.1h": "1 小时", + "settings.wallpaper.refresh.2h": "2 小时", + "settings.wallpaper.refresh.4h": "4 小时", + "settings.wallpaper.refresh.8h": "8 小时", + "settings.wallpaper.refresh.12h": "12 小时", + "settings.wallpaper.refresh.24h": "24 小时", "settings.wallpaper.color_label": "壁纸颜色", "settings.wallpaper.custom_color_tooltip": "自定义颜色", "settings.wallpaper.custom_color_apply": "应用", @@ -216,7 +233,14 @@ "schedule.settings.unnamed": "未命名课表", "schedule.settings.delete": "删除", "schedule.settings.picker_title": "选择 ClassIsland 课表文件", - "schedule.settings.picker_file_type": "ClassIsland CSES 课表", + "schedule.settings.picker_file_type.all": "ClassIsland 课表文件", + "schedule.settings.picker_file_type.json": "ClassIsland 档案 (JSON)", + "schedule.settings.picker_file_type.cses": "CSES 课表 (YAML)", + "schedule.settings.semester.title": "学期设置", + "schedule.settings.semester.start_date": "学期开始日期", + "schedule.settings.semester.week_cycle": "周循环", + "schedule.settings.semester.week_cycle_desc": "设置多周课表轮换周期,用于计算当前是第几周。", + "schedule.settings.semester.week_cycle_format": "{0} 周轮换", "worldclock.settings.title": "世界时钟设置", "worldclock.settings.desc": "分别为四个时钟选择时区。", "worldclock.settings.clock_1": "时钟 1", diff --git a/LanMountainDesktop/Models/AppSettingsSnapshot.cs b/LanMountainDesktop/Models/AppSettingsSnapshot.cs index 315970e..cb9455e 100644 --- a/LanMountainDesktop/Models/AppSettingsSnapshot.cs +++ b/LanMountainDesktop/Models/AppSettingsSnapshot.cs @@ -33,6 +33,8 @@ public sealed class AppSettingsSnapshot public string WallpaperPlacement { get; set; } = "Fill"; + public int SystemWallpaperRefreshIntervalSeconds { get; set; } = 300; + public int SettingsTabIndex { get; set; } = 0; public string? SettingsTabTag { get; set; } diff --git a/LanMountainDesktop/Models/ComponentSettingsSnapshot.cs b/LanMountainDesktop/Models/ComponentSettingsSnapshot.cs index eb25147..b147c26 100644 --- a/LanMountainDesktop/Models/ComponentSettingsSnapshot.cs +++ b/LanMountainDesktop/Models/ComponentSettingsSnapshot.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; namespace LanMountainDesktop.Models; @@ -12,6 +13,10 @@ public sealed class ComponentSettingsSnapshot public string ActiveImportedClassScheduleId { get; set; } = string.Empty; + public DateOnly? SemesterStartDate { get; set; } + + public int SemesterWeekCycle { get; set; } = 1; + public bool StudyEnvironmentShowDisplayDb { get; set; } = true; public bool StudyEnvironmentShowDbfs { get; set; } diff --git a/LanMountainDesktop/Services/ClassIslandScheduleDataService.cs b/LanMountainDesktop/Services/ClassIslandScheduleDataService.cs index 3128a6d..927b525 100644 --- a/LanMountainDesktop/Services/ClassIslandScheduleDataService.cs +++ b/LanMountainDesktop/Services/ClassIslandScheduleDataService.cs @@ -12,7 +12,7 @@ namespace LanMountainDesktop.Services; public interface IClassIslandScheduleDataService { - ClassIslandScheduleReadResult Load(string? inputPath = null, string? profileFileName = null); + ClassIslandScheduleReadResult Load(string? inputPath = null, string? profileFileName = null, DateOnly? semesterStartDate = null, int semesterWeekCycle = 1); bool TryResolveClassPlanForDate( ClassIslandScheduleSnapshot snapshot, @@ -43,7 +43,7 @@ public sealed class ClassIslandScheduleDataService : IClassIslandScheduleDataSer .IgnoreUnmatchedProperties() .Build(); - public ClassIslandScheduleReadResult Load(string? inputPath = null, string? profileFileName = null) + public ClassIslandScheduleReadResult Load(string? inputPath = null, string? profileFileName = null, DateOnly? semesterStartDate = null, int semesterWeekCycle = 1) { var warnings = new List(); try @@ -73,11 +73,11 @@ public sealed class ClassIslandScheduleDataService : IClassIslandScheduleDataSer ClassIslandScheduleSnapshot snapshot; if (source.SourceKind == ScheduleSourceKind.Cses) { - snapshot = ParseCsesSnapshot(source); + snapshot = ParseCsesSnapshot(source, semesterStartDate, semesterWeekCycle); } else { - var cycleRule = ParseCycleRule(source.SettingsPath, warnings); + var cycleRule = ParseCycleRule(source.SettingsPath, warnings, semesterStartDate, semesterWeekCycle); var profileJson = ReadJson(source.ProfilePath); snapshot = ParseProfileSnapshot(profileJson.RootElement, source, cycleRule); } @@ -412,22 +412,50 @@ public sealed class ClassIslandScheduleDataService : IClassIslandScheduleDataSer return null; } - private static ClassIslandScheduleCycleRule ParseCycleRule(string? settingsPath, List warnings) + private static ClassIslandScheduleCycleRule ParseCycleRule( + string? settingsPath, + List warnings, + DateOnly? semesterStartDate = null, + int semesterWeekCycle = 1) { - if (string.IsNullOrWhiteSpace(settingsPath) || !File.Exists(settingsPath)) + DateOnly? singleWeekStartDate = semesterStartDate; + int maxCycle = semesterWeekCycle > 1 ? semesterWeekCycle : 4; + var offsetList = new List { -1, -1, 0, 0, 0, 0, 0, 0 }; + + if (!string.IsNullOrWhiteSpace(settingsPath) && File.Exists(settingsPath)) { - warnings.Add("ClassIsland Settings.json not found, using default cycle rule."); - return new ClassIslandScheduleCycleRule(null, 4, new List { -1, -1, 0, 0, 0 }); + using var json = ReadJson(settingsPath); + var root = json.RootElement; + + if (!singleWeekStartDate.HasValue) + { + singleWeekStartDate = TryReadDateOnly(root, "SingleWeekStartTime"); + } + + if (semesterWeekCycle <= 1) + { + maxCycle = TryReadInt(root, "MultiWeekRotationMaxCycle", 4); + } + + var settingsOffsetList = ReadIntList(root, "MultiWeekRotationOffset"); + if (settingsOffsetList.Count >= 2) + { + offsetList = settingsOffsetList; + } + } + else + { + warnings.Add("ClassIsland Settings.json not found, using semester settings from component."); } - using var json = ReadJson(settingsPath); - var root = json.RootElement; - var singleWeekStartDate = TryReadDateOnly(root, "SingleWeekStartTime"); - var maxCycle = TryReadInt(root, "MultiWeekRotationMaxCycle", 4); - var offsetList = ReadIntList(root, "MultiWeekRotationOffset"); - if (offsetList.Count < 2) + if (maxCycle < 2) { - offsetList = new List { -1, -1, 0, 0, 0 }; + maxCycle = 2; + } + + while (offsetList.Count <= maxCycle) + { + offsetList.Add(0); } return new ClassIslandScheduleCycleRule( @@ -469,7 +497,10 @@ public sealed class ClassIslandScheduleDataService : IClassIslandScheduleDataSer ClassPlanGroups: groups); } - private static ClassIslandScheduleSnapshot ParseCsesSnapshot(ResolvedSource source) + private static ClassIslandScheduleSnapshot ParseCsesSnapshot( + ResolvedSource source, + DateOnly? semesterStartDate = null, + int semesterWeekCycle = 1) { var yaml = File.ReadAllText(source.ProfilePath); var csesProfile = CsesDeserializer.Deserialize(yaml) ?? new CsesProfileDto(); @@ -600,12 +631,19 @@ public sealed class ClassIslandScheduleDataService : IClassIslandScheduleDataSer [GlobalClassPlanGroupId] = new ClassIslandClassPlanGroup(GlobalClassPlanGroupId, "Global", IsGlobal: true) }; + var maxCycle = semesterWeekCycle > 1 ? semesterWeekCycle : 4; + var offsetList = new List { -1, -1, 0, 0, 0, 0, 0, 0 }; + while (offsetList.Count <= maxCycle) + { + offsetList.Add(0); + } + return new ClassIslandScheduleSnapshot( SourceRootPath: source.SourceRootPath, ProfilePath: source.ProfilePath, ProfileFileName: source.ProfileFileName, LoadedAt: DateTimeOffset.Now, - CycleRule: new ClassIslandScheduleCycleRule(null, 4, new List { -1, -1, 0, 0, 0 }), + CycleRule: new ClassIslandScheduleCycleRule(semesterStartDate, Math.Clamp(maxCycle, 2, 32), offsetList), SelectedClassPlanGroupId: DefaultClassPlanGroupId, TempClassPlanGroupId: null, IsTempClassPlanGroupEnabled: false, diff --git a/LanMountainDesktop/Services/Settings/SettingsContracts.cs b/LanMountainDesktop/Services/Settings/SettingsContracts.cs index 53d30d9..3c716ac 100644 --- a/LanMountainDesktop/Services/Settings/SettingsContracts.cs +++ b/LanMountainDesktop/Services/Settings/SettingsContracts.cs @@ -16,7 +16,13 @@ public enum WallpaperMediaType } public sealed record GridSettingsState(int ShortSideCells, string SpacingPreset, int EdgeInsetPercent); -public sealed record WallpaperSettingsState(string? WallpaperPath, string Type, string? Color, string Placement, string? CustomColor = null); +public sealed record WallpaperSettingsState( + string? WallpaperPath, + string Type, + string? Color, + string Placement, + string? CustomColor = null, + int SystemWallpaperRefreshIntervalSeconds = 300); public sealed record ThemeAppearanceSettingsState( bool IsNightMode, string? ThemeColor, diff --git a/LanMountainDesktop/Services/Settings/SettingsDomainServices.cs b/LanMountainDesktop/Services/Settings/SettingsDomainServices.cs index 1f8142d..41a683f 100644 --- a/LanMountainDesktop/Services/Settings/SettingsDomainServices.cs +++ b/LanMountainDesktop/Services/Settings/SettingsDomainServices.cs @@ -101,7 +101,9 @@ internal sealed class WallpaperSettingsService : IWallpaperSettingsService : snapshot.WallpaperPath, normalizedType, snapshot.WallpaperColor, - snapshot.WallpaperPlacement); + snapshot.WallpaperPlacement, + CustomColor: null, + SystemWallpaperRefreshIntervalSeconds: NormalizeRefreshInterval(snapshot.SystemWallpaperRefreshIntervalSeconds)); } public void Save(WallpaperSettingsState state) @@ -128,6 +130,7 @@ internal sealed class WallpaperSettingsService : IWallpaperSettingsService snapshot.WallpaperPlacement = string.IsNullOrWhiteSpace(state.Placement) ? "Fill" : state.Placement.Trim(); + snapshot.SystemWallpaperRefreshIntervalSeconds = NormalizeRefreshInterval(state.SystemWallpaperRefreshIntervalSeconds); _settingsService.SaveSnapshot( SettingsScope.App, snapshot, @@ -136,9 +139,21 @@ internal sealed class WallpaperSettingsService : IWallpaperSettingsService nameof(AppSettingsSnapshot.WallpaperPath), nameof(AppSettingsSnapshot.WallpaperType), nameof(AppSettingsSnapshot.WallpaperColor), - nameof(AppSettingsSnapshot.WallpaperPlacement) + nameof(AppSettingsSnapshot.WallpaperPlacement), + nameof(AppSettingsSnapshot.SystemWallpaperRefreshIntervalSeconds) ]); } + + private static int NormalizeRefreshInterval(int seconds) + { + return seconds switch + { + <= 0 => 300, + < 30 => 30, + > 86400 => 86400, + _ => seconds + }; + } } internal sealed class WallpaperMediaService : IWallpaperMediaService diff --git a/LanMountainDesktop/Services/SystemWallpaperProvider.cs b/LanMountainDesktop/Services/SystemWallpaperProvider.cs new file mode 100644 index 0000000..8640ef2 --- /dev/null +++ b/LanMountainDesktop/Services/SystemWallpaperProvider.cs @@ -0,0 +1,65 @@ +using System; +using System.IO; +using System.Runtime.InteropServices; +using Avalonia.Media.Imaging; +using Microsoft.Win32; + +namespace LanMountainDesktop.Services; + +public interface ISystemWallpaperProvider +{ + bool IsSupported { get; } + string? GetWallpaperPath(); + event EventHandler? WallpaperChanged; +} + +internal sealed class SystemWallpaperProvider : ISystemWallpaperProvider, IDisposable +{ + public bool IsSupported => RuntimeInformation.IsOSPlatform(OSPlatform.Windows); + + public event EventHandler? WallpaperChanged; + + public string? GetWallpaperPath() + { + if (!IsSupported) + { + return null; + } + + try + { + using var key = Registry.CurrentUser.OpenSubKey(@"Control Panel\Desktop"); + var wallpaperPath = key?.GetValue("Wallpaper") as string; + + if (string.IsNullOrWhiteSpace(wallpaperPath)) + { + return null; + } + + if (!File.Exists(wallpaperPath)) + { + return null; + } + + return wallpaperPath; + } + catch + { + return null; + } + } + + public void Dispose() + { + } +} + +public static class HostSystemWallpaperProvider +{ + private static ISystemWallpaperProvider? _instance; + + public static ISystemWallpaperProvider GetOrCreate() + { + return _instance ??= new SystemWallpaperProvider(); + } +} diff --git a/LanMountainDesktop/Services/WallpaperImageBrushFactory.cs b/LanMountainDesktop/Services/WallpaperImageBrushFactory.cs index aba8813..d061ad7 100644 --- a/LanMountainDesktop/Services/WallpaperImageBrushFactory.cs +++ b/LanMountainDesktop/Services/WallpaperImageBrushFactory.cs @@ -5,13 +5,13 @@ using Avalonia.Media.Imaging; namespace LanMountainDesktop.Services; -internal static class WallpaperImageBrushFactory +public static class WallpaperImageBrushFactory { - internal const string Fill = "Fill"; - internal const string Fit = "Fit"; - internal const string StretchMode = "Stretch"; - internal const string Center = "Center"; - internal const string Tile = "Tile"; + public const string Fill = "Fill"; + public const string Fit = "Fit"; + public const string StretchMode = "Stretch"; + public const string Center = "Center"; + public const string Tile = "Tile"; public static string NormalizePlacement(string? placement) { diff --git a/LanMountainDesktop/ViewModels/WallpaperSettingsPageViewModel.cs b/LanMountainDesktop/ViewModels/WallpaperSettingsPageViewModel.cs index eeaffe1..d4a122f 100644 --- a/LanMountainDesktop/ViewModels/WallpaperSettingsPageViewModel.cs +++ b/LanMountainDesktop/ViewModels/WallpaperSettingsPageViewModel.cs @@ -1,7 +1,8 @@ using System; using System.Collections.Generic; -using System.Collections.ObjectModel; +using System.IO; using System.Linq; +using System.Runtime.InteropServices; using System.Threading.Tasks; using Avalonia.Media; using Avalonia.Media.Imaging; @@ -15,6 +16,7 @@ namespace LanMountainDesktop.ViewModels; public sealed partial class WallpaperSettingsPageViewModel : ViewModelBase { private readonly ISettingsFacadeService _settingsFacade; + private readonly ISystemWallpaperProvider _systemWallpaperProvider; private readonly LocalizationService _localizationService = new(); private readonly string _languageCode; private bool _isInitializing; @@ -22,9 +24,11 @@ public sealed partial class WallpaperSettingsPageViewModel : ViewModelBase public WallpaperSettingsPageViewModel(ISettingsFacadeService settingsFacade) { _settingsFacade = settingsFacade; + _systemWallpaperProvider = HostSystemWallpaperProvider.GetOrCreate(); _languageCode = _localizationService.NormalizeLanguageCode(_settingsFacade.Region.Get().LanguageCode); WallpaperPlacements = CreateWallpaperPlacements(); WallpaperTypes = CreateWallpaperTypes(); + RefreshIntervals = CreateRefreshIntervals(); PresetColors = CreatePresetColors(); RefreshLocalizedText(); @@ -35,8 +39,11 @@ public sealed partial class WallpaperSettingsPageViewModel : ViewModelBase public IReadOnlyList WallpaperPlacements { get; } public IReadOnlyList WallpaperTypes { get; } + public IReadOnlyList RefreshIntervals { get; } public IReadOnlyList PresetColors { get; } + public bool IsSystemWallpaperSupported => RuntimeInformation.IsOSPlatform(OSPlatform.Windows); + [ObservableProperty] private string _wallpaperPath = string.Empty; @@ -49,6 +56,9 @@ public sealed partial class WallpaperSettingsPageViewModel : ViewModelBase [ObservableProperty] private SelectionOption _selectedWallpaperPlacement = null!; + [ObservableProperty] + private SelectionOption _selectedRefreshInterval = null!; + [ObservableProperty] private string _wallpaperHeader = string.Empty; @@ -73,6 +83,18 @@ public sealed partial class WallpaperSettingsPageViewModel : ViewModelBase [ObservableProperty] private string _filePickerTitle = string.Empty; + [ObservableProperty] + private string _systemWallpaperLabel = string.Empty; + + [ObservableProperty] + private string _refreshIntervalLabel = string.Empty; + + [ObservableProperty] + private string _refreshButtonTooltip = string.Empty; + + [ObservableProperty] + private string _systemWallpaperStatus = string.Empty; + [ObservableProperty] private bool _isImageOrVideo; @@ -82,13 +104,15 @@ public sealed partial class WallpaperSettingsPageViewModel : ViewModelBase [ObservableProperty] private bool _isImage; + [ObservableProperty] + private bool _isSystemWallpaper; + [ObservableProperty] private Bitmap? _previewImage; [ObservableProperty] private IBrush? _previewBrush; - // 自定义颜色持久化 [ObservableProperty] private Color _customColor = Colors.White; @@ -110,7 +134,11 @@ public sealed partial class WallpaperSettingsPageViewModel : ViewModelBase string.Equals(option.Value, wallpaperPlacement, StringComparison.OrdinalIgnoreCase)) ?? WallpaperPlacements[0]; - // 加载自定义颜色 + var refreshIntervalSeconds = wallpaper.SystemWallpaperRefreshIntervalSeconds; + SelectedRefreshInterval = RefreshIntervals.FirstOrDefault(option => + GetIntervalSeconds(option.Value) == refreshIntervalSeconds) + ?? RefreshIntervals[2]; + if (!string.IsNullOrWhiteSpace(wallpaper.CustomColor) && Color.TryParse(wallpaper.CustomColor, out var customColor)) { CustomColor = customColor; @@ -119,6 +147,7 @@ public sealed partial class WallpaperSettingsPageViewModel : ViewModelBase UpdateVisibility(); UpdatePreviewFromCurrentSelection(); + UpdateSystemWallpaperStatus(); } partial void OnSelectedWallpaperTypeChanged(SelectionOption value) @@ -132,8 +161,9 @@ public sealed partial class WallpaperSettingsPageViewModel : ViewModelBase private void UpdateVisibility() { IsImage = SelectedWallpaperType?.Value == "Image"; - IsImageOrVideo = IsImage; + IsImageOrVideo = IsImage || SelectedWallpaperType?.Value == "SystemWallpaper"; IsSolidColor = SelectedWallpaperType?.Value == "SolidColor"; + IsSystemWallpaper = SelectedWallpaperType?.Value == "SystemWallpaper"; } partial void OnSelectedColorChanged(string? value) @@ -145,13 +175,18 @@ public sealed partial class WallpaperSettingsPageViewModel : ViewModelBase partial void OnCustomColorChanged(Color value) { CustomColorBrush = new SolidColorBrush(value); - // 将自定义颜色应用到壁纸 var colorHex = $"#{value.A:X2}{value.R:X2}{value.G:X2}{value.B:X2}"; SelectedColor = colorHex; if (_isInitializing) return; SaveWallpaper(); } + partial void OnSelectedRefreshIntervalChanged(SelectionOption value) + { + if (_isInitializing) return; + SaveWallpaper(); + } + public async Task ImportWallpaperAsync(string sourcePath) { var importedPath = await _settingsFacade.WallpaperMedia.ImportAssetAsync(sourcePath); @@ -170,6 +205,12 @@ public sealed partial class WallpaperSettingsPageViewModel : ViewModelBase private void UpdatePreviewFromCurrentSelection() { + if (IsSystemWallpaper) + { + UpdateSystemWallpaperPreview(); + return; + } + if (!IsImage) { ClearPreviewImage(); @@ -180,10 +221,24 @@ public sealed partial class WallpaperSettingsPageViewModel : ViewModelBase UpdatePreviewImage(WallpaperPath); } - private void UpdatePreviewImage(string path) + private void UpdateSystemWallpaperPreview() + { + var systemPath = _systemWallpaperProvider.GetWallpaperPath(); + if (string.IsNullOrWhiteSpace(systemPath)) + { + ClearPreviewImage(); + SystemWallpaperStatus = L("settings.wallpaper.system.unavailable", "Unable to read system wallpaper"); + return; + } + + SystemWallpaperStatus = systemPath; + UpdatePreviewImage(systemPath); + } + + private void UpdatePreviewImage(string? path) { var previousPreview = PreviewImage; - if (string.IsNullOrWhiteSpace(path) || !System.IO.File.Exists(path)) + if (string.IsNullOrWhiteSpace(path) || !File.Exists(path)) { previousPreview?.Dispose(); PreviewImage = null; @@ -193,7 +248,7 @@ public sealed partial class WallpaperSettingsPageViewModel : ViewModelBase try { - using var stream = System.IO.File.OpenRead(path); + using var stream = File.OpenRead(path); var bitmap = new Bitmap(stream); PreviewImage = bitmap; PreviewBrush = WallpaperImageBrushFactory.Create(bitmap, SelectedWallpaperPlacement?.Value); @@ -215,9 +270,21 @@ public sealed partial class WallpaperSettingsPageViewModel : ViewModelBase previousPreview?.Dispose(); } + private void UpdateSystemWallpaperStatus() + { + if (!IsSystemWallpaper) return; + UpdateSystemWallpaperPreview(); + } + + [RelayCommand] + private void RefreshSystemWallpaper() + { + UpdateSystemWallpaperPreview(); + } + partial void OnSelectedWallpaperPlacementChanged(SelectionOption value) { - if (IsImage && PreviewImage is not null) + if ((IsImage || IsSystemWallpaper) && PreviewImage is not null) { PreviewBrush = WallpaperImageBrushFactory.Create(PreviewImage, value?.Value); } @@ -236,16 +303,46 @@ public sealed partial class WallpaperSettingsPageViewModel : ViewModelBase { var selectedType = SelectedWallpaperType?.Value ?? "Image"; var selectedPlacement = SelectedWallpaperPlacement?.Value ?? WallpaperImageBrushFactory.Fill; - var normalizedPath = SelectedWallpaperType?.Value == "SolidColor" || string.IsNullOrWhiteSpace(WallpaperPath) - ? null - : WallpaperPath; + var refreshIntervalSeconds = GetIntervalSeconds(SelectedRefreshInterval?.Value); + + string? normalizedPath; + if (selectedType == "SolidColor" || selectedType == "SystemWallpaper") + { + normalizedPath = null; + } + else + { + normalizedPath = string.IsNullOrWhiteSpace(WallpaperPath) ? null : WallpaperPath; + } + var customColorHex = $"#{CustomColor.A:X2}{CustomColor.R:X2}{CustomColor.G:X2}{CustomColor.B:X2}"; _settingsFacade.Wallpaper.Save(new WallpaperSettingsState( normalizedPath, selectedType, SelectedColor, selectedPlacement, - customColorHex)); + customColorHex, + refreshIntervalSeconds)); + } + + private static int GetIntervalSeconds(string? value) + { + return value switch + { + "30s" => 30, + "1m" => 60, + "5m" => 300, + "10m" => 600, + "15m" => 900, + "30m" => 1800, + "1h" => 3600, + "2h" => 7200, + "4h" => 14400, + "8h" => 28800, + "12h" => 43200, + "24h" => 86400, + _ => 300 + }; } private IReadOnlyList CreateWallpaperPlacements() @@ -262,10 +359,36 @@ public sealed partial class WallpaperSettingsPageViewModel : ViewModelBase private IReadOnlyList CreateWallpaperTypes() { - return - [ + var types = new List + { new SelectionOption("Image", L("settings.wallpaper.type.image", "Image")), new SelectionOption("SolidColor", L("settings.wallpaper.type.solid_color", "Solid Color")) + }; + + if (IsSystemWallpaperSupported) + { + types.Add(new SelectionOption("SystemWallpaper", L("settings.wallpaper.type.system", "System Wallpaper"))); + } + + return types; + } + + private IReadOnlyList CreateRefreshIntervals() + { + return + [ + new SelectionOption("30s", L("settings.wallpaper.refresh.30s", "30 seconds")), + new SelectionOption("1m", L("settings.wallpaper.refresh.1m", "1 minute")), + new SelectionOption("5m", L("settings.wallpaper.refresh.5m", "5 minutes")), + new SelectionOption("10m", L("settings.wallpaper.refresh.10m", "10 minutes")), + new SelectionOption("15m", L("settings.wallpaper.refresh.15m", "15 minutes")), + new SelectionOption("30m", L("settings.wallpaper.refresh.30m", "30 minutes")), + new SelectionOption("1h", L("settings.wallpaper.refresh.1h", "1 hour")), + new SelectionOption("2h", L("settings.wallpaper.refresh.2h", "2 hours")), + new SelectionOption("4h", L("settings.wallpaper.refresh.4h", "4 hours")), + new SelectionOption("8h", L("settings.wallpaper.refresh.8h", "8 hours")), + new SelectionOption("12h", L("settings.wallpaper.refresh.12h", "12 hours")), + new SelectionOption("24h", L("settings.wallpaper.refresh.24h", "24 hours")) ]; } @@ -289,6 +412,9 @@ public sealed partial class WallpaperSettingsPageViewModel : ViewModelBase WallpaperPlacementDescription = L("settings.wallpaper.placement_desc", "Adjust how the image fills the desktop."); ImportWallpaperButtonText = L("settings.wallpaper.pick_button", "Import Wallpaper"); FilePickerTitle = L("filepicker.title", "Select wallpaper"); + SystemWallpaperLabel = L("settings.wallpaper.system.label", "System Wallpaper"); + RefreshIntervalLabel = L("settings.wallpaper.refresh_interval", "Refresh Interval"); + RefreshButtonTooltip = L("settings.wallpaper.refresh_now", "Refresh Now"); } private string L(string key, string fallback) diff --git a/LanMountainDesktop/Views/ComponentEditors/ClassScheduleComponentEditor.axaml b/LanMountainDesktop/Views/ComponentEditors/ClassScheduleComponentEditor.axaml index 35d8d99..9a4485e 100644 --- a/LanMountainDesktop/Views/ComponentEditors/ClassScheduleComponentEditor.axaml +++ b/LanMountainDesktop/Views/ComponentEditors/ClassScheduleComponentEditor.axaml @@ -2,6 +2,8 @@ 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:material="clr-namespace:Material.Styles;assembly=Material.Styles" + xmlns:materialAssists="clr-namespace:Material.Styles.Assists;assembly=Material.Styles" mc:Ignorable="d" x:Class="LanMountainDesktop.Views.ComponentEditors.ClassScheduleComponentEditor"> @@ -36,6 +38,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/LanMountainDesktop/Views/ComponentEditors/ClassScheduleComponentEditor.axaml.cs b/LanMountainDesktop/Views/ComponentEditors/ClassScheduleComponentEditor.axaml.cs index d87b312..cbb0dcd 100644 --- a/LanMountainDesktop/Views/ComponentEditors/ClassScheduleComponentEditor.axaml.cs +++ b/LanMountainDesktop/Views/ComponentEditors/ClassScheduleComponentEditor.axaml.cs @@ -76,6 +76,11 @@ public partial class ClassScheduleComponentEditor : ComponentEditorViewBase FollowSystemColorSchemeItem.Content = L("component.color_scheme.follow_system", "Follow system color scheme"); UseNativeColorSchemeItem.Content = L("component.color_scheme.native", "Use component custom color scheme"); + SemesterSettingsHeaderTextBlock.Text = L("schedule.settings.semester.title", "Semester Settings"); + SemesterStartDateLabel.Text = L("schedule.settings.semester.start_date", "Semester Start Date"); + WeekCycleLabel.Text = L("schedule.settings.semester.week_cycle", "Week Cycle"); + WeekCycleDescription.Text = L("schedule.settings.semester.week_cycle_desc", "Set the week rotation cycle for multi-week schedules (e.g., 2 for odd/even weeks)."); + AddScheduleButton.Content = L("schedule.settings.add", "Add Schedule"); EmptyStateTextBlock.Text = L("schedule.settings.empty", "No imported schedules yet."); @@ -85,9 +90,25 @@ public partial class ClassScheduleComponentEditor : ComponentEditorViewBase string.Equals(colorSchemeSource, ThemeAppearanceValues.ColorSchemeFollowSystem, StringComparison.OrdinalIgnoreCase) ? FollowSystemColorSchemeItem : UseNativeColorSchemeItem; + + if (snapshot.SemesterStartDate.HasValue) + { + SemesterStartDatePicker.SelectedDate = snapshot.SemesterStartDate.Value.ToDateTime(TimeOnly.MinValue); + } + + var weekCycle = Math.Clamp(snapshot.SemesterWeekCycle, 1, 7); + WeekCycleComboBox.SelectedIndex = weekCycle - 1; + + UpdateWeekCycleDescription(weekCycle); _suppressEvents = false; } + private void UpdateWeekCycleDescription(int weekCycle) + { + var format = L("schedule.settings.semester.week_cycle_format", "{0}-week rotation"); + WeekCycleDescription.Text = string.Format(format, weekCycle); + } + private void OnColorSchemeSelectionChanged(object? sender, SelectionChangedEventArgs e) { _ = sender; @@ -106,6 +127,39 @@ public partial class ClassScheduleComponentEditor : ComponentEditorViewBase SaveSnapshot(snapshot, nameof(ComponentSettingsSnapshot.ColorSchemeSource)); } + private void OnSemesterStartDateChanged(object? sender, SelectionChangedEventArgs e) + { + if (_suppressEvents) + { + return; + } + + var snapshot = LoadSnapshot(); + if (SemesterStartDatePicker.SelectedDate.HasValue) + { + snapshot.SemesterStartDate = DateOnly.FromDateTime(SemesterStartDatePicker.SelectedDate.Value); + } + else + { + snapshot.SemesterStartDate = null; + } + SaveSnapshot(snapshot, nameof(ComponentSettingsSnapshot.SemesterStartDate)); + } + + private void OnWeekCycleSelectionChanged(object? sender, SelectionChangedEventArgs e) + { + if (_suppressEvents) + { + return; + } + + var weekCycle = WeekCycleComboBox.SelectedIndex + 1; + var snapshot = LoadSnapshot(); + snapshot.SemesterWeekCycle = weekCycle; + SaveSnapshot(snapshot, nameof(ComponentSettingsSnapshot.SemesterWeekCycle)); + UpdateWeekCycleDescription(weekCycle); + } + private async void OnAddScheduleClick(object? sender, RoutedEventArgs e) { _ = sender; @@ -122,7 +176,15 @@ public partial class ClassScheduleComponentEditor : ComponentEditorViewBase AllowMultiple = false, FileTypeFilter = [ - new FilePickerFileType(L("schedule.settings.picker_file_type", "ClassIsland CSES Schedule")) + new FilePickerFileType(L("schedule.settings.picker_file_type.all", "ClassIsland Schedule Files")) + { + Patterns = ["*.json", "*.cses", "*.yaml", "*.yml"] + }, + new FilePickerFileType(L("schedule.settings.picker_file_type.json", "ClassIsland Profile (JSON)")) + { + Patterns = ["*.json"] + }, + new FilePickerFileType(L("schedule.settings.picker_file_type.cses", "CSES Schedule (YAML)")) { Patterns = ["*.cses", "*.yaml", "*.yml"] } diff --git a/LanMountainDesktop/Views/Components/ClassScheduleWidget.axaml.cs b/LanMountainDesktop/Views/Components/ClassScheduleWidget.axaml.cs index bea6a2e..d32d540 100644 --- a/LanMountainDesktop/Views/Components/ClassScheduleWidget.axaml.cs +++ b/LanMountainDesktop/Views/Components/ClassScheduleWidget.axaml.cs @@ -253,7 +253,11 @@ public partial class ClassScheduleWidget : UserControl, IDesktopComponentWidget, var today = DateOnly.FromDateTime(now); var importedSchedulePath = ResolveImportedSchedulePath(componentSettings); - var readResult = _scheduleService.Load(importedSchedulePath); + var readResult = _scheduleService.Load( + importedSchedulePath, + profileFileName: null, + semesterStartDate: componentSettings.SemesterStartDate, + semesterWeekCycle: componentSettings.SemesterWeekCycle); if (!readResult.Success || readResult.Snapshot is null) { _courseItems = Array.Empty(); diff --git a/LanMountainDesktop/Views/Components/OfficeRecentDocumentsWidget.axaml b/LanMountainDesktop/Views/Components/OfficeRecentDocumentsWidget.axaml index 43d4efc..ff36e99 100644 --- a/LanMountainDesktop/Views/Components/OfficeRecentDocumentsWidget.axaml +++ b/LanMountainDesktop/Views/Components/OfficeRecentDocumentsWidget.axaml @@ -10,7 +10,7 @@ x:Class="LanMountainDesktop.Views.Components.OfficeRecentDocumentsWidget"> = Math.Max(40, TimeTextBlock.FontSize * 1.72); + var showDateLine = leftContentWidth >= Math.Max(36, timeFontSize * 1.4) && contentHeight >= 38; DateWeatherStack.IsVisible = showDateLine; - WeatherIconImage.IsVisible = showDateLine && leftContentWidth >= Math.Max(56, DateTextBlock.FontSize * 2.4); + WeatherIconImage.IsVisible = showDateLine && leftContentWidth >= Math.Max(48, dateFontSize * 3.2); var dateReservedWidth = WeatherIconImage.IsVisible ? weatherIconSize + DateWeatherStack.Spacing @@ -477,14 +484,22 @@ public partial class WeatherClockWidget : UserControl, IDesktopComponentWidget, : CreateBrush("#F8FAFF"); AnalogDialBorder.BorderBrush = CreateBrush(isNightMode ? "#34DDE7FF" : "#12000000"); - TimeTextBlock.Foreground = WeatherTypographyAccessibility.CreateReadableBrush( - isNightMode ? "#F8FBFF" : "#10131A", - backgroundSamples, - WeatherTypographyAccessibility.WcagLargeTextContrast); - DateTextBlock.Foreground = WeatherTypographyAccessibility.CreateReadableBrush( - isNightMode ? "#BCC8DD" : "#7A7E87", - backgroundSamples, - WeatherTypographyAccessibility.WcagNormalTextContrast); + if (isNightMode) + { + TimeTextBlock.Foreground = WeatherTypographyAccessibility.CreateReadableBrush( + "#F8FBFF", + backgroundSamples, + WeatherTypographyAccessibility.WcagLargeTextContrast); + DateTextBlock.Foreground = WeatherTypographyAccessibility.CreateReadableBrush( + "#BCC8DD", + backgroundSamples, + WeatherTypographyAccessibility.WcagNormalTextContrast); + } + else + { + TimeTextBlock.Foreground = CreateBrush("#10131A"); + DateTextBlock.Foreground = CreateBrush("#7A7E87"); + } _hourHandLine.Stroke = CreateBrush(isNightMode ? "#F1F5FF" : "#232938"); _minuteHandLine.Stroke = CreateBrush(isNightMode ? "#D6E0F2" : "#2F3749"); diff --git a/LanMountainDesktop/Views/MainWindow.SettingsHardCut.Stubs.cs b/LanMountainDesktop/Views/MainWindow.SettingsHardCut.Stubs.cs index d26b4ad..b83c06c 100644 --- a/LanMountainDesktop/Views/MainWindow.SettingsHardCut.Stubs.cs +++ b/LanMountainDesktop/Views/MainWindow.SettingsHardCut.Stubs.cs @@ -215,17 +215,21 @@ public partial class MainWindow string? savedWallpaperPath, string? type = null, string? color = null, - string? placement = null) + string? placement = null, + int systemWallpaperRefreshIntervalSeconds = 300) { _wallpaperPath = string.IsNullOrWhiteSpace(savedWallpaperPath) ? null : savedWallpaperPath; _wallpaperType = string.IsNullOrWhiteSpace(type) ? "Image" : type.Trim(); _wallpaperPlacement = WallpaperImageBrushFactory.NormalizePlacement(placement); _wallpaperSolidColor = TryParseColor(color, out var parsedColor) ? parsedColor : null; _wallpaperDisplayState = WallpaperDisplayState.NoWallpaperConfigured; + _systemWallpaperRefreshIntervalSeconds = systemWallpaperRefreshIntervalSeconds; _wallpaperBitmap?.Dispose(); _wallpaperBitmap = null; + StopSystemWallpaperTimer(); + if (string.Equals(_wallpaperType, "SolidColor", StringComparison.OrdinalIgnoreCase)) { _wallpaperMediaType = WallpaperMediaType.SolidColor; @@ -235,6 +239,14 @@ public partial class MainWindow return; } + if (string.Equals(_wallpaperType, "SystemWallpaper", StringComparison.OrdinalIgnoreCase)) + { + _wallpaperMediaType = WallpaperMediaType.Image; + LoadSystemWallpaper(); + StartSystemWallpaperTimer(); + return; + } + if (string.IsNullOrWhiteSpace(_wallpaperPath)) { _wallpaperMediaType = WallpaperMediaType.None; @@ -273,6 +285,69 @@ public partial class MainWindow } } + private void LoadSystemWallpaper() + { + var systemPath = _systemWallpaperProvider.GetWallpaperPath(); + if (string.IsNullOrWhiteSpace(systemPath) || !File.Exists(systemPath)) + { + _wallpaperDisplayState = WallpaperDisplayState.TemporarilyUnavailable; + _wallpaperBitmap?.Dispose(); + _wallpaperBitmap = null; + return; + } + + try + { + using var stream = File.OpenRead(systemPath); + _wallpaperBitmap?.Dispose(); + _wallpaperBitmap = new Bitmap(stream); + _wallpaperPath = systemPath; + _wallpaperDisplayState = WallpaperDisplayState.CurrentValidWallpaper; + CacheLastValidWallpaperBitmap(systemPath); + } + catch + { + _wallpaperDisplayState = WallpaperDisplayState.TemporarilyUnavailable; + _wallpaperBitmap?.Dispose(); + _wallpaperBitmap = null; + } + } + + private void StartSystemWallpaperTimer() + { + StopSystemWallpaperTimer(); + + var intervalSeconds = Math.Clamp(_systemWallpaperRefreshIntervalSeconds, 30, 86400); + _systemWallpaperRefreshTimer = new DispatcherTimer + { + Interval = TimeSpan.FromSeconds(intervalSeconds) + }; + _systemWallpaperRefreshTimer.Tick += OnSystemWallpaperRefreshTimerTick; + _systemWallpaperRefreshTimer.Start(); + } + + private void StopSystemWallpaperTimer() + { + if (_systemWallpaperRefreshTimer is not null) + { + _systemWallpaperRefreshTimer.Stop(); + _systemWallpaperRefreshTimer.Tick -= OnSystemWallpaperRefreshTimerTick; + _systemWallpaperRefreshTimer = null; + } + } + + private void OnSystemWallpaperRefreshTimerTick(object? sender, EventArgs e) + { + if (!string.Equals(_wallpaperType, "SystemWallpaper", StringComparison.OrdinalIgnoreCase)) + { + StopSystemWallpaperTimer(); + return; + } + + LoadSystemWallpaper(); + ApplyWallpaperBrush(); + } + private void ApplyWallpaperBrush() { DesktopWallpaperImageLayer.Background = null; @@ -480,7 +555,8 @@ public partial class MainWindow snapshot.WallpaperPath, snapshot.WallpaperType, snapshot.WallpaperColor, - snapshot.WallpaperPlacement); + snapshot.WallpaperPlacement, + snapshot.SystemWallpaperRefreshIntervalSeconds); if (!snapshot.IsNightMode.HasValue) { _isNightMode = CalculateCurrentBackgroundLuminance() < LightBackgroundLuminanceThreshold; @@ -523,6 +599,7 @@ public partial class MainWindow ? latestWallpaperState.Color : null, WallpaperPlacement = latestWallpaperState.Placement, + SystemWallpaperRefreshIntervalSeconds = latestWallpaperState.SystemWallpaperRefreshIntervalSeconds, LanguageCode = _languageCode, TimeZoneId = _timeZoneService.CurrentTimeZone.Id, WeatherLocationMode = latestWeatherState.LocationMode, diff --git a/LanMountainDesktop/Views/MainWindow.axaml.cs b/LanMountainDesktop/Views/MainWindow.axaml.cs index 0372d6f..d3d753e 100644 --- a/LanMountainDesktop/Views/MainWindow.axaml.cs +++ b/LanMountainDesktop/Views/MainWindow.axaml.cs @@ -122,6 +122,9 @@ public partial class MainWindow : Window, ISettingsWindowAnchorProvider private Color? _wallpaperSolidColor; private string? _wallpaperPath; private string _wallpaperStatus = "Current background uses solid color."; + private int _systemWallpaperRefreshIntervalSeconds = 300; + private DispatcherTimer? _systemWallpaperRefreshTimer; + private readonly ISystemWallpaperProvider _systemWallpaperProvider = HostSystemWallpaperProvider.GetOrCreate(); private IReadOnlyList _recommendedColors = Array.Empty(); private IReadOnlyList _monetColors = Array.Empty(); private Color _selectedThemeColor = Color.Parse("#FF3B82F6"); diff --git a/LanMountainDesktop/Views/SettingsPages/AboutSettingsPage.axaml b/LanMountainDesktop/Views/SettingsPages/AboutSettingsPage.axaml index c56f94a..fc835ef 100644 --- a/LanMountainDesktop/Views/SettingsPages/AboutSettingsPage.axaml +++ b/LanMountainDesktop/Views/SettingsPages/AboutSettingsPage.axaml @@ -77,10 +77,10 @@ - + - + diff --git a/LanMountainDesktop/Views/SettingsPages/WallpaperSettingsPage.axaml b/LanMountainDesktop/Views/SettingsPages/WallpaperSettingsPage.axaml index 22ba8c0..55dae68 100644 --- a/LanMountainDesktop/Views/SettingsPages/WallpaperSettingsPage.axaml +++ b/LanMountainDesktop/Views/SettingsPages/WallpaperSettingsPage.axaml @@ -29,6 +29,11 @@ + + + + @@ -135,6 +140,19 @@ + + + + + + @@ -183,10 +201,60 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +