From bcf4be6d5034e303d07b5199275fa0086cd39c63 Mon Sep 17 00:00:00 2001 From: lincube Date: Mon, 16 Mar 2026 15:19:46 +0800 Subject: [PATCH] 0.6.1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 课表组件修复。加入最近文档组件。 --- .../class-schedule-enhancement/checklist.md | 24 +++ .../specs/class-schedule-enhancement/spec.md | 101 +++++++++++ .../specs/class-schedule-enhancement/tasks.md | 61 +++++++ .../ComponentSystem/BuiltInComponentIds.cs | 3 +- .../ComponentColorSchemeHelper.cs | 37 ++++ .../ComponentSystem/ComponentRegistry.cs | 11 +- LanMountainDesktop/Localization/en-US.json | 4 +- LanMountainDesktop/Localization/zh-CN.json | 4 +- .../Models/ComponentSettingsSnapshot.cs | 2 + .../Services/AppearanceThemeService.cs | 10 +- .../ClassIslandScheduleDataService.cs | 4 +- .../Services/OfficeRecentDocumentsService.cs | 153 ++++++++++++++++ LanMountainDesktop/Services/ShortcutHelper.cs | 32 ++++ .../Services/ThemeAppearanceValues.cs | 3 + .../ClassScheduleComponentEditor.axaml | 16 ++ .../ClassScheduleComponentEditor.axaml.cs | 43 ++++- .../StudyEnvironmentComponentEditor.axaml | 21 ++- .../StudyEnvironmentComponentEditor.axaml.cs | 36 ++++ .../Components/BaiduHotSearchWidget.axaml.cs | 17 +- .../Components/ClassScheduleWidget.axaml.cs | 166 ++++++++++++++++-- .../DesktopComponentRuntimeRegistry.cs | 7 +- .../OfficeRecentDocumentViewModel.cs | 10 ++ .../OfficeRecentDocumentsWidget.axaml | 108 ++++++++++++ .../OfficeRecentDocumentsWidget.axaml.cs | 124 +++++++++++++ .../StudyEnvironmentWidget.axaml.cs | 13 +- 25 files changed, 982 insertions(+), 28 deletions(-) create mode 100644 .trae/specs/class-schedule-enhancement/checklist.md create mode 100644 .trae/specs/class-schedule-enhancement/spec.md create mode 100644 .trae/specs/class-schedule-enhancement/tasks.md create mode 100644 LanMountainDesktop/ComponentSystem/ComponentColorSchemeHelper.cs create mode 100644 LanMountainDesktop/Services/OfficeRecentDocumentsService.cs create mode 100644 LanMountainDesktop/Services/ShortcutHelper.cs create mode 100644 LanMountainDesktop/Views/Components/OfficeRecentDocumentViewModel.cs create mode 100644 LanMountainDesktop/Views/Components/OfficeRecentDocumentsWidget.axaml create mode 100644 LanMountainDesktop/Views/Components/OfficeRecentDocumentsWidget.axaml.cs diff --git a/.trae/specs/class-schedule-enhancement/checklist.md b/.trae/specs/class-schedule-enhancement/checklist.md new file mode 100644 index 0000000..774439b --- /dev/null +++ b/.trae/specs/class-schedule-enhancement/checklist.md @@ -0,0 +1,24 @@ +# Checklist + +## 1. 课表单双周解析修复 + +- [x] 单周课程(WeekCountDiv=1)在单周正确显示 +- [x] 双周课程(WeekCountDiv=2)在双周正确显示 +- [x] 每周课程(WeekCountDiv=0)在所有周正确显示 +- [x] 多周轮转(2-32周)正确计算当前周期位置 + +## 2. 课程动态移动功能 + +- [x] 课程结束自动从视图移除 +- [x] 新课程自动移入视图可见区域 +- [x] 当日课程全部结束后自动切换到次日课程表 + +## 3. 拖动交互功能 + +- [x] 课程表支持上下拖动滚动 +- [x] 拖动操作流畅、响应及时 + +## 4. 自动复位功能 + +- [x] 用户手动拖动后,标记拖动状态 +- [x] 当前课程变化时自动复位到最新进行中课程 diff --git a/.trae/specs/class-schedule-enhancement/spec.md b/.trae/specs/class-schedule-enhancement/spec.md new file mode 100644 index 0000000..52c2d97 --- /dev/null +++ b/.trae/specs/class-schedule-enhancement/spec.md @@ -0,0 +1,101 @@ +# 课程表组件功能优化规格说明书 + +## Why + +当前课程表组件存在以下问题: +1. 单双周课程解析逻辑存在缺陷,无法正确识别单周/双周/每周模式 +2. 课程无法动态移动,第一列始终显示进行中的课程,但存在无法正常移动的问题 +3. 缺少用户拖动交互功能 +4. 缺少拖动后的自动复位机制 + +## What Changes + +- 修复 ClassIsland 课程单双周解析逻辑 +- 实现课程动态移动机制(当前课程结束自动上移) +- 实现课程表上下拖动交互功能 +- 实现自动复位功能(课程结束后视图复位到最新进行中课程) + +## Impact + +### Affected specs +- 课程表组件功能规范 + +### Affected code +- `Services/ClassIslandScheduleDataService.cs` - 课表解析服务 +- `Views/Components/ClassScheduleWidget.axaml.cs` - 课表组件 + +--- + +## ADDED Requirements + +### Requirement: 单双周课程解析 + +系统 SHALL 能够正确解析包含单双周信息的课程数据。 + +#### Scenario: 单周课程 +- **WHEN** 课程设置为单周上课 +- **THEN** 课程仅在单周显示 + +#### Scenario: 双周课程 +- **WHEN** 课程设置为双周上课 +- **THEN** 课程仅在双周显示 + +#### Scenario: 每周课程 +- **WHEN** 课程设置为每周上课 +- **THEN** 课程在所有周显示 + +--- + +### Requirement: 课程动态移动 + +系统 SHALL 实现课程的动态移动机制。 + +#### Scenario: 课程结束自动上移 +- **WHEN** 当前进行中的课程结束 +- **THEN** 课程列表自动向上移动 +- **AND THEN** 下一个进行中或即将开始的课程移至视图可见区域 + +#### Scenario: 新课程移入视图 +- **WHEN** 新的课程即将开始 +- **THEN** 该课程自动移至视图可见区域 + +#### Scenario: 当日课程全部结束 +- **WHEN** 当日所有课程已结束 +- **THEN** 自动显示次日课程表 + +--- + +### Requirement: 拖动交互功能 + +系统 SHALL 提供课程表的上下拖动功能。 + +#### Scenario: 拖动查看课程 +- **WHEN** 用户在课程表区域进行上下拖动 +- **THEN** 课程列表随拖动方向滚动 +- **AND THEN** 拖动操作流畅、响应及时 + +--- + +### Requirement: 自动复位功能 + +系统 SHALL 在用户手动拖动后自动复位到当前课程。 + +#### Scenario: 当前课程结束触发复位 +- **WHEN** 用户手动拖动课程表后,当前课程结束 +- **THEN** 视图自动复位到显示最新进行中课程的位置 + +--- + +## MODIFIED Requirements + +### Requirement: 课程解析逻辑 + +**当前**: 单双周解析可能存在缺陷 + +**修改后**: 正确识别 WeekCountDiv 和 WeekCountDivTotal 参数,准确判断单周/双周/每周模式 + +--- + +## REMOVED Requirements + +(无) diff --git a/.trae/specs/class-schedule-enhancement/tasks.md b/.trae/specs/class-schedule-enhancement/tasks.md new file mode 100644 index 0000000..ee25b88 --- /dev/null +++ b/.trae/specs/class-schedule-enhancement/tasks.md @@ -0,0 +1,61 @@ +# Tasks + +## 1. 课表单双周解析修复 + +- [x] Task 1.1: 分析 ClassIsland 课表单双周数据结构 + - [x] 分析 ClassIsland Schedule.json 和 Profile.json 中的周数规则字段 + - [x] 确认 WeekCountDiv 和 WeekCountDivTotal 的含义和取值范围 + +- [x] Task 1.2: 修复 GetCyclePositionsByDate 方法 + - [x] 检查单周开始日期的计算逻辑 + - [x] 修复周期位置计算公式 + +- [x] Task 1.3: 修复 CheckRegularClassPlan 方法 + - [x] 验证 weekCountDiv 和 weekCountDivTotal 的匹配逻辑 + - [x] 确保单周=1、双周=2、每周=0 的正确处理 + +## 2. 课程动态移动功能 + +- [x] Task 2.1: 分析当前课程状态检测逻辑 + - [x] 查看如何判断课程是否为"当前进行中" + +- [x] Task 2.2: 实现定时刷新机制 + - [x] 增加更频繁的刷新定时器(每分钟检查一次) + - [x] 实现课程状态变化检测 + +- [x] Task 2.3: 实现动态移动逻辑 + - [x] 课程结束后自动上移 + - [x] 新课程自动移入视图 + +- [x] Task 2.4: 实现次日课程切换 + - [x] 当日所有课程结束后自动切换到次日 + +## 3. 拖动交互功能 + +- [x] Task 3.1: 实现 ScrollViewer 包裹 + - [x] 修改 XAML 使用 ScrollViewer 包裹课程列表 + +- [x] Task 3.2: 实现拖动手势处理 + - [x] 添加 PointerPressed/PointerMoved/PointerReleased 处理 + - [x] 实现平滑滚动逻辑 + +## 4. 自动复位功能 + +- [x] Task 4.1: 记录用户拖动状态 + - [x] 添加用户是否手动拖动的标志位 + +- [x] Task 4.2: 实现自动复位逻辑 + - [x] 检测当前课程变化 + - [x] 当用户手动拖动且当前课程变化时自动复位 + +# Task Dependencies + +- Task 1.1 -> Task 1.2 -> Task 1.3 +- Task 2.1 -> Task 2.2 -> Task 2.3 -> Task 2.4 +- Task 3.1 -> Task 3.2 +- Task 4.1 -> Task 4.2 + +# Parallelizable Tasks + +- Task 1.x (解析修复) 与 Task 3.x (拖动) 可以并行开发 +- Task 2.x (动态移动) 可以在 Task 1 完成后进行 diff --git a/LanMountainDesktop/ComponentSystem/BuiltInComponentIds.cs b/LanMountainDesktop/ComponentSystem/BuiltInComponentIds.cs index b5dd046..ab7a099 100644 --- a/LanMountainDesktop/ComponentSystem/BuiltInComponentIds.cs +++ b/LanMountainDesktop/ComponentSystem/BuiltInComponentIds.cs @@ -1,4 +1,4 @@ -namespace LanMountainDesktop.ComponentSystem; +namespace LanMountainDesktop.ComponentSystem; public static class BuiltInComponentIds { @@ -40,4 +40,5 @@ public static class BuiltInComponentIds public const string DesktopWhiteboard = "DesktopWhiteboard"; public const string DesktopBlackboardLandscape = "DesktopBlackboardLandscape"; public const string DesktopBrowser = "DesktopBrowser"; + public const string DesktopOfficeRecentDocuments = "DesktopOfficeRecentDocuments"; } diff --git a/LanMountainDesktop/ComponentSystem/ComponentColorSchemeHelper.cs b/LanMountainDesktop/ComponentSystem/ComponentColorSchemeHelper.cs new file mode 100644 index 0000000..2b68ddd --- /dev/null +++ b/LanMountainDesktop/ComponentSystem/ComponentColorSchemeHelper.cs @@ -0,0 +1,37 @@ +using System; +using LanMountainDesktop.Services; +using LanMountainDesktop.Views; + +namespace LanMountainDesktop.ComponentSystem; + +public static class ComponentColorSchemeHelper +{ + public static bool ShouldUseMonetColor(string? componentColorScheme, string globalThemeColorMode) + { + if (string.Equals(componentColorScheme, ThemeAppearanceValues.ColorSchemeNative, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + if (string.Equals(componentColorScheme, ThemeAppearanceValues.ColorSchemeFollowSystem, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + + return !string.Equals(globalThemeColorMode, ThemeAppearanceValues.ColorModeDefaultNeutral, StringComparison.OrdinalIgnoreCase); + } + + public static string GetCurrentGlobalThemeColorMode() + { + try + { + var service = HostAppearanceThemeProvider.GetOrCreate(); + var appearance = service.GetCurrent(); + return appearance?.ThemeColorMode ?? ThemeAppearanceValues.ColorModeDefaultNeutral; + } + catch + { + return ThemeAppearanceValues.ColorModeDefaultNeutral; + } + } +} diff --git a/LanMountainDesktop/ComponentSystem/ComponentRegistry.cs b/LanMountainDesktop/ComponentSystem/ComponentRegistry.cs index 79bb063..6c62cab 100644 --- a/LanMountainDesktop/ComponentSystem/ComponentRegistry.cs +++ b/LanMountainDesktop/ComponentSystem/ComponentRegistry.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using LanMountainDesktop.ComponentSystem.Extensions; @@ -327,6 +327,15 @@ public sealed class ComponentRegistry AllowStatusBarPlacement: false, AllowDesktopPlacement: true, ResizeMode: DesktopComponentResizeMode.Free), + new DesktopComponentDefinition( + BuiltInComponentIds.DesktopOfficeRecentDocuments, + "Office Recent Documents", + "Folder", + "File", + MinWidthCells: 4, + MinHeightCells: 2, + AllowStatusBarPlacement: false, + AllowDesktopPlacement: true), new DesktopComponentDefinition( BuiltInComponentIds.Date, "Calendar", diff --git a/LanMountainDesktop/Localization/en-US.json b/LanMountainDesktop/Localization/en-US.json index 2e56b90..bbd595c 100644 --- a/LanMountainDesktop/Localization/en-US.json +++ b/LanMountainDesktop/Localization/en-US.json @@ -255,7 +255,6 @@ "settings.color.use_system_chrome_toggle": "Use system window chrome", "settings.color.theme_color_label": "Theme accent color", "settings.appearance.theme_color_mode_label": "Theme color source", - "settings.appearance.system_material_label": "System material", "settings.appearance.theme_color_mode.neutral": "Default neutral", "settings.appearance.theme_color_mode.user": "User theme color Monet", "settings.appearance.theme_color_mode.wallpaper": "Wallpaper Monet", @@ -265,6 +264,8 @@ "settings.appearance.theme_color_preview.app": "Currently previewing colors extracted from the app wallpaper.", "settings.appearance.theme_color_preview.system": "Currently previewing colors extracted from the system wallpaper.", "settings.appearance.theme_color_preview.fallback": "No usable wallpaper was found. The app is using a fallback accent.", + "component.color_scheme.follow_system": "Follow system color scheme", + "component.color_scheme.native": "Use component custom color scheme", "settings.appearance.system_material.none": "None", "settings.appearance.system_material.mica": "Mica", "settings.appearance.system_material.acrylic": "Acrylic", @@ -580,6 +581,7 @@ "component.whiteboard": "Blackboard (Portrait)", "component.blackboard_landscape": "Blackboard (Landscape)", "component.browser": "Browser", + "component.office_recent_documents": "Recent Documents", "component.holiday_calendar": "Holiday Calendar", "component.study_environment": "Environment", "component.study_session_control": "Study Session Control", diff --git a/LanMountainDesktop/Localization/zh-CN.json b/LanMountainDesktop/Localization/zh-CN.json index eade1f7..e6451c9 100644 --- a/LanMountainDesktop/Localization/zh-CN.json +++ b/LanMountainDesktop/Localization/zh-CN.json @@ -260,7 +260,6 @@ "settings.color.use_system_chrome_toggle": "使用系统窗口标题栏", "settings.color.theme_color_label": "主题强调色", "settings.appearance.theme_color_mode_label": "主题色来源", - "settings.appearance.system_material_label": "系统材质", "settings.appearance.theme_color_mode.neutral": "默认中性", "settings.appearance.theme_color_mode.user": "用户主题色 Monet", "settings.appearance.theme_color_mode.wallpaper": "壁纸 Monet 取色", @@ -270,6 +269,8 @@ "settings.appearance.theme_color_preview.app": "当前正在预览从应用壁纸提取的颜色。", "settings.appearance.theme_color_preview.system": "当前正在预览从系统壁纸提取的颜色。", "settings.appearance.theme_color_preview.fallback": "没有可用壁纸,当前使用回退强调色。", + "component.color_scheme.follow_system": "跟随系统配色", + "component.color_scheme.native": "使用组件自定义配色", "settings.appearance.system_material.none": "无", "settings.appearance.system_material.mica": "Mica", "settings.appearance.system_material.acrylic": "Acrylic", @@ -585,6 +586,7 @@ "component.whiteboard": "竖向小黑板", "component.blackboard_landscape": "横向小黑板", "component.browser": "浏览器", + "component.office_recent_documents": "最近文档", "component.holiday_calendar": "节假日日历", "component.study_environment": "环境", "component.study_session_control": "自习时段控制", diff --git a/LanMountainDesktop/Models/ComponentSettingsSnapshot.cs b/LanMountainDesktop/Models/ComponentSettingsSnapshot.cs index f5cae91..ff15f1a 100644 --- a/LanMountainDesktop/Models/ComponentSettingsSnapshot.cs +++ b/LanMountainDesktop/Models/ComponentSettingsSnapshot.cs @@ -6,6 +6,8 @@ public sealed class ComponentSettingsSnapshot { public string DailyArtworkMirrorSource { get; set; } = DailyArtworkMirrorSources.Overseas; + public string? ColorSchemeSource { get; set; } + public List ImportedClassSchedules { get; set; } = []; public string ActiveImportedClassScheduleId { get; set; } = string.Empty; diff --git a/LanMountainDesktop/Services/AppearanceThemeService.cs b/LanMountainDesktop/Services/AppearanceThemeService.cs index c3a6292..f3a4691 100644 --- a/LanMountainDesktop/Services/AppearanceThemeService.cs +++ b/LanMountainDesktop/Services/AppearanceThemeService.cs @@ -248,6 +248,15 @@ internal sealed class WindowMaterialService : IWindowMaterialService { ArgumentNullException.ThrowIfNull(window); + var normalizedMode = ThemeAppearanceValues.NormalizeSystemMaterialMode(materialMode); + + if (normalizedMode == ThemeAppearanceValues.MaterialNone) + { + window.Background = Brushes.White; + window.TransparencyLevelHint = [WindowTransparencyLevel.None]; + return; + } + window.Background = Brushes.Transparent; if (!OperatingSystem.IsWindows() || !IsTransparencyEnabled()) @@ -259,7 +268,6 @@ internal sealed class WindowMaterialService : IWindowMaterialService return; } - var normalizedMode = ThemeAppearanceValues.NormalizeSystemMaterialMode(materialMode); window.TransparencyLevelHint = normalizedMode switch { ThemeAppearanceValues.MaterialMica => diff --git a/LanMountainDesktop/Services/ClassIslandScheduleDataService.cs b/LanMountainDesktop/Services/ClassIslandScheduleDataService.cs index 267cdb6..3128a6d 100644 --- a/LanMountainDesktop/Services/ClassIslandScheduleDataService.cs +++ b/LanMountainDesktop/Services/ClassIslandScheduleDataService.cs @@ -163,7 +163,7 @@ public sealed class ClassIslandScheduleDataService : IClassIslandScheduleDataSer var totalElapsedWeeks = (int)Math.Floor( (referenceDate.ToDateTime(TimeOnly.MinValue) - cycleRule.SingleWeekStartDate.Value.ToDateTime(TimeOnly.MinValue)).TotalDays / 7d); - for (var cycleLength = 2; cycleLength <= maxCycle; cycleLength++) + for (var cycleLength = 1; cycleLength <= maxCycle; cycleLength++) { var cycleOffset = cycleLength < cycleRule.MultiWeekRotationOffset.Count ? cycleRule.MultiWeekRotationOffset[cycleLength] @@ -668,7 +668,7 @@ public sealed class ClassIslandScheduleDataService : IClassIslandScheduleDataSer return true; } - if (weekCountDivTotal <= 1 || weekCountDivTotal >= cyclePositions.Count) + if (weekCountDivTotal <= 0 || weekCountDivTotal >= cyclePositions.Count) { return false; } diff --git a/LanMountainDesktop/Services/OfficeRecentDocumentsService.cs b/LanMountainDesktop/Services/OfficeRecentDocumentsService.cs new file mode 100644 index 0000000..344401e --- /dev/null +++ b/LanMountainDesktop/Services/OfficeRecentDocumentsService.cs @@ -0,0 +1,153 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Text.Json; +using LanMountainDesktop.Services.Settings; + +namespace LanMountainDesktop.Services; + +public interface IOfficeRecentDocumentsService +{ + List GetRecentDocuments(int maxCount = 20); + void OpenDocument(string filePath); +} + +public sealed class OfficeRecentDocument +{ + public string FileName { get; set; } = string.Empty; + public string FilePath { get; set; } = string.Empty; + public string Extension { get; set; } = string.Empty; + public DateTime LastModifiedTime { get; set; } + public long FileSizeBytes { get; set; } + public string IconGlyph { get; set; } = string.Empty; +} + +public sealed class OfficeRecentDocumentsService : IOfficeRecentDocumentsService +{ + private static readonly string[] OfficeExtensions = { ".doc", ".docx", ".dot", ".dotx", ".rtf" }; + private static readonly string[] ExcelExtensions = { ".xls", ".xlsx", ".xlsm", ".xlsb", ".csv" }; + private static readonly string[] PowerPointExtensions = { ".ppt", ".pptx", ".pptm", ".pps", ".ppsx" }; + + public List GetRecentDocuments(int maxCount = 20) + { + var documents = new List(); + var recentPaths = GetRecentFolders(); + + foreach (var recentPath in recentPaths) + { + if (!Directory.Exists(recentPath)) + { + continue; + } + + try + { + var files = Directory.GetFiles(recentPath, "*.lnk"); + foreach (var lnkPath in files) + { + var targetPath = GetShortcutTarget(lnkPath); + if (string.IsNullOrEmpty(targetPath)) + { + continue; + } + + var extension = Path.GetExtension(targetPath).ToLowerInvariant(); + if (!IsOfficeFile(extension)) + { + continue; + } + + if (!System.IO.File.Exists(targetPath)) + { + continue; + } + + try + { + var fileInfo = new FileInfo(targetPath); + var doc = new OfficeRecentDocument + { + FileName = Path.GetFileNameWithoutExtension(targetPath), + FilePath = targetPath, + Extension = extension, + LastModifiedTime = fileInfo.LastWriteTime, + FileSizeBytes = fileInfo.Length, + IconGlyph = GetIconGlyph(extension) + }; + + if (!documents.Any(d => d.FilePath == targetPath)) + { + documents.Add(doc); + } + } + catch + { + } + } + } + catch + { + } + } + + return documents + .OrderByDescending(d => d.LastModifiedTime) + .Take(maxCount) + .ToList(); + } + + public void OpenDocument(string filePath) + { + try + { + var startInfo = new ProcessStartInfo + { + FileName = filePath, + UseShellExecute = true + }; + Process.Start(startInfo); + } + catch + { + } + } + + private static List GetRecentFolders() + { + var folders = new List(); + + var appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); + folders.Add(Path.Combine(appData, "Microsoft", "Word", "Recent")); + folders.Add(Path.Combine(appData, "Microsoft", "Excel", "Recent")); + folders.Add(Path.Combine(appData, "Microsoft", "PowerPoint", "Recent")); + + return folders; + } + + private static bool IsOfficeFile(string extension) + { + return OfficeExtensions.Contains(extension) || + ExcelExtensions.Contains(extension) || + PowerPointExtensions.Contains(extension); + } + + private static string GetIconGlyph(string extension) + { + return extension switch + { + ".doc" or ".docx" or ".dot" or ".dotx" or ".rtf" => "\uE8A5", + ".xls" or ".xlsx" or ".xlsm" or ".xlsb" or ".csv" => "\uE9F9", + ".ppt" or ".pptx" or ".pptm" or ".pps" or ".ppsx" => "\uE8A1", + _ => "\uE8A5" + }; + } + + private static string? GetShortcutTarget(string lnkPath) + { + return ShortcutHelper.GetShortcutTarget(lnkPath); + } +} diff --git a/LanMountainDesktop/Services/ShortcutHelper.cs b/LanMountainDesktop/Services/ShortcutHelper.cs new file mode 100644 index 0000000..be7c5a2 --- /dev/null +++ b/LanMountainDesktop/Services/ShortcutHelper.cs @@ -0,0 +1,32 @@ +using System.Runtime.InteropServices; + +namespace LanMountainDesktop.Services; + +internal static class ShortcutHelper +{ + [ComImport] + [Guid("72C24DD5-D70A-438B-8A42-98424B88AFB8")] + internal class WshShell { } + + [ComImport] + [Guid("F935DC21-1CF0-11D0-ADB9-00C04FD58A0B")] + [InterfaceType(ComInterfaceType.InterfaceIsIDispatch)] + internal interface IWshShortcut + { + string TargetPath { get; set; } + } + + public static string? GetShortcutTarget(string lnkPath) + { + try + { + dynamic shell = new WshShell(); + dynamic shortcut = shell.CreateShortcut(lnkPath); + return shortcut.TargetPath; + } + catch + { + return null; + } + } +} diff --git a/LanMountainDesktop/Services/ThemeAppearanceValues.cs b/LanMountainDesktop/Services/ThemeAppearanceValues.cs index 7f1093d..15dce16 100644 --- a/LanMountainDesktop/Services/ThemeAppearanceValues.cs +++ b/LanMountainDesktop/Services/ThemeAppearanceValues.cs @@ -10,6 +10,9 @@ public static class ThemeAppearanceValues public const string ColorModeSeedMonet = "seed_monet"; public const string ColorModeWallpaperMonet = "wallpaper_monet"; + public const string ColorSchemeFollowSystem = "follow_system"; + public const string ColorSchemeNative = "native"; + public const string MaterialNone = "none"; public const string MaterialMica = "mica"; public const string MaterialAcrylic = "acrylic"; diff --git a/LanMountainDesktop/Views/ComponentEditors/ClassScheduleComponentEditor.axaml b/LanMountainDesktop/Views/ComponentEditors/ClassScheduleComponentEditor.axaml index 392dae9..d751933 100644 --- a/LanMountainDesktop/Views/ComponentEditors/ClassScheduleComponentEditor.axaml +++ b/LanMountainDesktop/Views/ComponentEditors/ClassScheduleComponentEditor.axaml @@ -17,6 +17,22 @@ + + + + + + + + + + diff --git a/LanMountainDesktop/Views/ComponentEditors/ClassScheduleComponentEditor.axaml.cs b/LanMountainDesktop/Views/ComponentEditors/ClassScheduleComponentEditor.axaml.cs index dac428c..a3112a2 100644 --- a/LanMountainDesktop/Views/ComponentEditors/ClassScheduleComponentEditor.axaml.cs +++ b/LanMountainDesktop/Views/ComponentEditors/ClassScheduleComponentEditor.axaml.cs @@ -11,13 +11,15 @@ using Avalonia.Media; using Avalonia.Platform.Storage; using LanMountainDesktop.ComponentSystem; using LanMountainDesktop.Models; +using LanMountainDesktop.Services; namespace LanMountainDesktop.Views.ComponentEditors; public partial class ClassScheduleComponentEditor : ComponentEditorViewBase { private readonly List _importedSchedules = []; - private string _activeScheduleId = string.Empty; + private string? _activeScheduleId; + private bool _suppressEvents; public ClassScheduleComponentEditor() : this(null) @@ -62,10 +64,49 @@ public partial class ClassScheduleComponentEditor : ComponentEditorViewBase private void ApplyState() { + var snapshot = LoadSnapshot(); + var colorSchemeSource = snapshot.ColorSchemeSource; + HeadlineTextBlock.Text = Context?.Definition.DisplayName ?? "Class Schedule"; DescriptionTextBlock.Text = L("schedule.settings.desc", "导入 ClassIsland 的 CSES 课表文件并选择启用项。"); + + ColorSchemeHeaderTextBlock.Text = L("component.settings.color_scheme", "配色方案"); + FollowSystemRadioButton.Content = L("component.color_scheme.follow_system", "跟随系统配色"); + UseNativeRadioButton.Content = L("component.color_scheme.native", "使用组件自定义配色"); + AddScheduleButton.Content = L("schedule.settings.add", "添加课表"); EmptyStateTextBlock.Text = L("schedule.settings.empty", "暂无导入课表"); + + _suppressEvents = true; + + if (string.IsNullOrEmpty(colorSchemeSource) || + colorSchemeSource == ThemeAppearanceValues.ColorSchemeFollowSystem) + { + FollowSystemRadioButton.IsChecked = true; + } + else + { + UseNativeRadioButton.IsChecked = true; + } + + _suppressEvents = false; + } + + private void OnColorSchemeChanged(object? sender, RoutedEventArgs e) + { + if (_suppressEvents) + { + return; + } + + var useNative = UseNativeRadioButton.IsChecked == true; + var colorSchemeSource = useNative + ? ThemeAppearanceValues.ColorSchemeNative + : ThemeAppearanceValues.ColorSchemeFollowSystem; + + var snapshot = LoadSnapshot(); + snapshot.ColorSchemeSource = colorSchemeSource; + SaveSnapshot(snapshot, nameof(ComponentSettingsSnapshot.ColorSchemeSource)); } private async void OnAddScheduleClick(object? sender, RoutedEventArgs e) diff --git a/LanMountainDesktop/Views/ComponentEditors/StudyEnvironmentComponentEditor.axaml b/LanMountainDesktop/Views/ComponentEditors/StudyEnvironmentComponentEditor.axaml index c3291ed..04780e4 100644 --- a/LanMountainDesktop/Views/ComponentEditors/StudyEnvironmentComponentEditor.axaml +++ b/LanMountainDesktop/Views/ComponentEditors/StudyEnvironmentComponentEditor.axaml @@ -2,10 +2,11 @@ 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:ui="using:FluentAvalonia.UI.Controls" mc:Ignorable="d" x:Class="LanMountainDesktop.Views.ComponentEditors.StudyEnvironmentComponentEditor"> - + + + + + + + + + + @@ -27,7 +44,7 @@ - = 0) + { + if (_isUserScrolling) + { + _isUserScrolling = false; + } + ScrollToCurrentCourse(newCurrentCourseIndex); + } + + if (_lastRefreshDate != currentDate && currentDate > _lastRefreshDate) + { + _lastRefreshDate = currentDate; + } + } + + private int FindCurrentCourseIndex() + { + for (var i = 0; i < _courseItems.Count; i++) + { + if (_courseItems[i].IsCurrent) + { + return i; + } + } + return -1; + } + + private void ScrollToCurrentCourse(int courseIndex) + { + if (courseIndex < 0 || courseIndex >= _courseItems.Count) + { + return; + } + + if (courseIndex < CourseListPanel.Children.Count) + { + var targetChild = CourseListPanel.Children[courseIndex]; + var bounds = targetChild.Bounds; + ContentScrollViewer.Offset = new Vector(0, bounds.Position.Y); + } } public void RefreshFromSettings() @@ -134,44 +228,75 @@ public partial class ClassScheduleWidget : UserControl, IDesktopComponentWidget, _componentId, _placementId); _languageCode = _localizationService.NormalizeLanguageCode(appSettings.LanguageCode); + _componentColorScheme = componentSettings.ColorSchemeSource; var now = _timeZoneService?.GetCurrentTime() ?? DateTime.Now; - UpdateHeader(now); + var today = DateOnly.FromDateTime(now); var importedSchedulePath = ResolveImportedSchedulePath(componentSettings); var readResult = _scheduleService.Load(importedSchedulePath); if (!readResult.Success || readResult.Snapshot is null) { _courseItems = Array.Empty(); + UpdateHeader(now); ShowStatus(L("schedule.widget.no_source", "未读取到 ClassIsland 课表")); RenderScheduleItems(); return; } var snapshot = readResult.Snapshot; - var today = DateOnly.FromDateTime(now); + if (!_scheduleService.TryResolveClassPlanForDate(snapshot, today, out var resolvedClassPlan)) { - _courseItems = Array.Empty(); - ShowStatus(L("schedule.widget.no_class_today", "今天没有课程")); - RenderScheduleItems(); - return; + var nextDay = today.AddDays(1); + if (_scheduleService.TryResolveClassPlanForDate(snapshot, nextDay, out var nextDayClassPlan)) + { + resolvedClassPlan = nextDayClassPlan; + today = nextDay; + } + else + { + _courseItems = Array.Empty(); + UpdateHeader(now); + ShowStatus(L("schedule.widget.no_class_today", "今天没有课程")); + RenderScheduleItems(); + return; + } } if (!snapshot.TimeLayouts.TryGetValue(resolvedClassPlan.ClassPlan.TimeLayoutId, out var layout)) { _courseItems = Array.Empty(); + UpdateHeader(now); ShowStatus(L("schedule.widget.layout_missing", "课表时间布局缺失")); RenderScheduleItems(); return; } - _courseItems = BuildCourseItemViewModels(snapshot, resolvedClassPlan.ClassPlan, layout, now); + var adjustedNow = today == DateOnly.FromDateTime(now) ? now : DateTime.Today.AddHours(8); + _courseItems = BuildCourseItemViewModels(snapshot, resolvedClassPlan.ClassPlan, layout, adjustedNow); + + if (_courseItems.Count == 0) + { + var nextDay = today.AddDays(1); + if (_scheduleService.TryResolveClassPlanForDate(snapshot, nextDay, out var nextDayClassPlan) && + snapshot.TimeLayouts.TryGetValue(nextDayClassPlan.ClassPlan.TimeLayoutId, out var nextLayout)) + { + today = nextDay; + adjustedNow = DateTime.Today.AddHours(8); + _courseItems = BuildCourseItemViewModels(snapshot, nextDayClassPlan.ClassPlan, nextLayout, adjustedNow); + } + } + + UpdateHeader(today.ToDateTime(TimeOnly.MinValue)); + if (_courseItems.Count == 0) { ShowStatus(L("schedule.widget.no_class_today", "今天没有课程")); } else { + var currentIndex = FindCurrentCourseIndex(); + _lastCurrentCourseIndex = currentIndex; HideStatus(); } @@ -336,6 +461,10 @@ public partial class ClassScheduleWidget : UserControl, IDesktopComponentWidget, return; } + var useMonetColor = ComponentColorSchemeHelper.ShouldUseMonetColor( + _componentColorScheme, + ComponentColorSchemeHelper.GetCurrentGlobalThemeColorMode()); + var scale = ResolveScale(); var bulletSize = Math.Clamp(10 * scale, 5, 12); var courseNameSize = Math.Clamp(42 * scale, 14, 42); @@ -350,7 +479,9 @@ public partial class ClassScheduleWidget : UserControl, IDesktopComponentWidget, var primaryBrush = CreateBrush(_isNightVisual ? "#F9FBFF" : "#151821"); var secondaryBrush = CreateBrush(_isNightVisual ? "#848B99" : "#667084"); - var currentBrush = CreateBrush("#FF4D5A"); + var currentBrush = useMonetColor + ? CreateBrush("#FF4FC3F7") + : CreateBrush("#FF4D5A"); var normalBulletBrush = CreateBrush(_isNightVisual ? "#B8BEC9" : "#9AA3B2"); var visibleItems = _courseItems.Take(maxVisibleItems).ToList(); @@ -438,9 +569,22 @@ public partial class ClassScheduleWidget : UserControl, IDesktopComponentWidget, private void ApplyAdaptiveLayout() { + if (Bounds.Width <= 0 || Bounds.Height <= 0) + { + return; + } + var scale = ResolveScale(); _isNightVisual = ResolveNightMode(); + var useMonetColor = ComponentColorSchemeHelper.ShouldUseMonetColor( + _componentColorScheme, + ComponentColorSchemeHelper.GetCurrentGlobalThemeColorMode()); + + var slashBrush = useMonetColor + ? CreateBrush("#FF4FC3F7") + : CreateBrush("#FF3250"); + var cornerRadius = Math.Clamp(_currentCellSize * 0.45, 24, 44); RootBorder.CornerRadius = new CornerRadius(cornerRadius); RootBorder.Background = _isNightVisual @@ -468,7 +612,7 @@ public partial class ClassScheduleWidget : UserControl, IDesktopComponentWidget, MonthTextBlock.Foreground = CreateBrush(_isNightVisual ? "#F8FAFF" : "#131722"); DayTextBlock.Foreground = CreateBrush(_isNightVisual ? "#F8FAFF" : "#131722"); - SlashTextBlock.Foreground = CreateBrush("#FF3250"); + SlashTextBlock.Foreground = slashBrush; WeekdayTextBlock.Foreground = CreateBrush(_isNightVisual ? "#C6CBD5" : "#4B5463"); ClassCountTextBlock.Foreground = CreateBrush(_isNightVisual ? "#8D95A4" : "#738095"); StatusTextBlock.Foreground = CreateBrush(_isNightVisual ? "#9AA2B1" : "#4B5565"); diff --git a/LanMountainDesktop/Views/Components/DesktopComponentRuntimeRegistry.cs b/LanMountainDesktop/Views/Components/DesktopComponentRuntimeRegistry.cs index 1567855..d827f58 100644 --- a/LanMountainDesktop/Views/Components/DesktopComponentRuntimeRegistry.cs +++ b/LanMountainDesktop/Views/Components/DesktopComponentRuntimeRegistry.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Reflection; @@ -439,6 +439,11 @@ public sealed class DesktopComponentRuntimeRegistry "component.browser", () => new BrowserWidget(), cellSize => Math.Clamp(cellSize * 0.24, 10, 24)), + new DesktopComponentRuntimeRegistration( + BuiltInComponentIds.DesktopOfficeRecentDocuments, + "component.office_recent_documents", + () => new OfficeRecentDocumentsWidget(), + cellSize => Math.Clamp(cellSize * 0.50, 10, 24)), new DesktopComponentRuntimeRegistration( BuiltInComponentIds.HolidayCalendar, "component.holiday_calendar", diff --git a/LanMountainDesktop/Views/Components/OfficeRecentDocumentViewModel.cs b/LanMountainDesktop/Views/Components/OfficeRecentDocumentViewModel.cs new file mode 100644 index 0000000..3806a73 --- /dev/null +++ b/LanMountainDesktop/Views/Components/OfficeRecentDocumentViewModel.cs @@ -0,0 +1,10 @@ +using System; + +namespace LanMountainDesktop.Views.Components; + +public sealed class OfficeRecentDocumentViewModel +{ + public string FileName { get; set; } = string.Empty; + public string FilePath { get; set; } = string.Empty; + public string TimeAgo { get; set; } = string.Empty; +} diff --git a/LanMountainDesktop/Views/Components/OfficeRecentDocumentsWidget.axaml b/LanMountainDesktop/Views/Components/OfficeRecentDocumentsWidget.axaml new file mode 100644 index 0000000..cfc050d --- /dev/null +++ b/LanMountainDesktop/Views/Components/OfficeRecentDocumentsWidget.axaml @@ -0,0 +1,108 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/LanMountainDesktop/Views/Components/OfficeRecentDocumentsWidget.axaml.cs b/LanMountainDesktop/Views/Components/OfficeRecentDocumentsWidget.axaml.cs new file mode 100644 index 0000000..54a634c --- /dev/null +++ b/LanMountainDesktop/Views/Components/OfficeRecentDocumentsWidget.axaml.cs @@ -0,0 +1,124 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Avalonia.Controls; +using Avalonia.Input; +using LanMountainDesktop.Services; +using LanMountainDesktop.Views.Components; + +namespace LanMountainDesktop.Views.Components; + +public partial class OfficeRecentDocumentsWidget : UserControl, IDesktopComponentWidget, IDesktopPageVisibilityAwareComponentWidget +{ + private readonly IOfficeRecentDocumentsService _recentDocumentsService; + private List _documents = new(); + private bool _isOnActivePage; + private bool _isEditMode; + private bool _isLoading; + + public OfficeRecentDocumentsWidget() + { + InitializeComponent(); + _recentDocumentsService = new OfficeRecentDocumentsService(); + } + + public void ApplyCellSize(double cellSize) + { + if (RootBorder is null) + { + return; + } + + var scale = cellSize / 100.0; + RootBorder.CornerRadius = new Avalonia.CornerRadius(Math.Max(8, 34 * scale)); + } + + public void SetDesktopPageContext(bool isOnActivePage, bool isEditMode) + { + _isOnActivePage = isOnActivePage; + _isEditMode = isEditMode; + + if (_isOnActivePage && !_isLoading) + { + LoadDocuments(); + } + } + + private void LoadDocuments() + { + try + { + _isLoading = true; + StatusTextBlock.IsVisible = false; + + _documents = _recentDocumentsService.GetRecentDocuments(20); + + if (_documents.Count == 0) + { + StatusTextBlock.Text = "暂无最近文档"; + StatusTextBlock.IsVisible = true; + return; + } + + UpdateDisplay(); + } + catch + { + StatusTextBlock.Text = "加载失败"; + StatusTextBlock.IsVisible = true; + } + finally + { + _isLoading = false; + } + } + + private void UpdateDisplay() + { + var displayItems = _documents.Select(d => new OfficeRecentDocumentViewModel + { + FileName = d.FileName, + FilePath = d.FilePath, + TimeAgo = GetTimeAgo(d.LastModifiedTime) + }).ToList(); + + DocumentsItemsControl.ItemsSource = displayItems; + } + + private static string GetTimeAgo(DateTime dateTime) + { + var span = DateTime.Now - dateTime; + + if (span.TotalMinutes < 1) + return "刚刚"; + if (span.TotalMinutes < 60) + return $"{(int)span.TotalMinutes} 分钟前"; + if (span.TotalHours < 24) + return $"{(int)span.TotalHours} 小时前"; + if (span.TotalDays < 7) + return $"{(int)span.TotalDays} 天前"; + if (span.TotalDays < 30) + return $"{(int)(span.TotalDays / 7)} 周前"; + + return dateTime.ToString("MM/dd"); + } + + private void OnRefreshPointerPressed(object? sender, PointerPressedEventArgs e) + { + LoadDocuments(); + } + + private void OnDocumentCardPointerPressed(object? sender, PointerPressedEventArgs e) + { + if (sender is Border border && border.DataContext is { } data) + { + var filePathProperty = data.GetType().GetProperty("FilePath"); + var filePath = filePathProperty?.GetValue(data) as string; + + if (!string.IsNullOrEmpty(filePath)) + { + _recentDocumentsService.OpenDocument(filePath); + } + } + } +} diff --git a/LanMountainDesktop/Views/Components/StudyEnvironmentWidget.axaml.cs b/LanMountainDesktop/Views/Components/StudyEnvironmentWidget.axaml.cs index 1e8c3fd..c90d93f 100644 --- a/LanMountainDesktop/Views/Components/StudyEnvironmentWidget.axaml.cs +++ b/LanMountainDesktop/Views/Components/StudyEnvironmentWidget.axaml.cs @@ -4,6 +4,7 @@ using Avalonia; using Avalonia.Controls; using Avalonia.Media; using Avalonia.Threading; +using LanMountainDesktop.ComponentSystem; using LanMountainDesktop.Models; using LanMountainDesktop.Services; @@ -24,6 +25,7 @@ public partial class StudyEnvironmentWidget : UserControl, IDesktopComponentWidg private double _currentCellSize = 48; private bool _showDisplayDb = true; private bool _showDbfs; + private string? _componentColorScheme; private string _languageCode = "zh-CN"; private bool _isAttached; private bool _isOnActivePage = true; @@ -147,6 +149,7 @@ public partial class StudyEnvironmentWidget : UserControl, IDesktopComponentWidg _languageCode = _localizationService.NormalizeLanguageCode(appSnapshot.LanguageCode); _showDisplayDb = componentSnapshot.StudyEnvironmentShowDisplayDb; _showDbfs = componentSnapshot.StudyEnvironmentShowDbfs; + _componentColorScheme = componentSnapshot.ColorSchemeSource; if (!_showDisplayDb && !_showDbfs) { _showDisplayDb = true; @@ -287,22 +290,26 @@ public partial class StudyEnvironmentWidget : UserControl, IDesktopComponentWidg private IBrush ResolveStatusBrush(StudyAnalyticsSnapshot snapshot) { + var useMonetColor = ComponentColorSchemeHelper.ShouldUseMonetColor( + _componentColorScheme, + ComponentColorSchemeHelper.GetCurrentGlobalThemeColorMode()); + if (snapshot.State == StudyAnalyticsRuntimeState.Unsupported || snapshot.State == StudyAnalyticsRuntimeState.Error || snapshot.StreamStatus == NoiseStreamStatus.Error) { - return CreateBrush("#FFFF7B7B"); + return useMonetColor ? CreateBrush("#FF6FD7A2") : CreateBrush("#FFFF7B7B"); } if (snapshot.StreamStatus == NoiseStreamStatus.Noisy) { - return CreateBrush("#FFFFB14A"); + return useMonetColor ? CreateBrush("#FF4FC3F7") : CreateBrush("#FFFFB14A"); } if (snapshot.State == StudyAnalyticsRuntimeState.Running && snapshot.StreamStatus == NoiseStreamStatus.Quiet) { - return CreateBrush("#FF6FD7A2"); + return useMonetColor ? CreateBrush("#FF81C784") : CreateBrush("#FF6FD7A2"); } return TryResolveThemeBrush("AdaptiveTextPrimaryBrush", "#FFEFF3FF");