mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-20 23:54:26 +08:00
0.6.1
课表组件修复。加入最近文档组件。
This commit is contained in:
24
.trae/specs/class-schedule-enhancement/checklist.md
Normal file
24
.trae/specs/class-schedule-enhancement/checklist.md
Normal file
@@ -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] 当前课程变化时自动复位到最新进行中课程
|
||||
101
.trae/specs/class-schedule-enhancement/spec.md
Normal file
101
.trae/specs/class-schedule-enhancement/spec.md
Normal file
@@ -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
|
||||
|
||||
(无)
|
||||
61
.trae/specs/class-schedule-enhancement/tasks.md
Normal file
61
.trae/specs/class-schedule-enhancement/tasks.md
Normal file
@@ -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 完成后进行
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "自习时段控制",
|
||||
|
||||
@@ -6,6 +6,8 @@ public sealed class ComponentSettingsSnapshot
|
||||
{
|
||||
public string DailyArtworkMirrorSource { get; set; } = DailyArtworkMirrorSources.Overseas;
|
||||
|
||||
public string? ColorSchemeSource { get; set; }
|
||||
|
||||
public List<ImportedClassScheduleSnapshot> ImportedClassSchedules { get; set; } = [];
|
||||
|
||||
public string ActiveImportedClassScheduleId { get; set; } = string.Empty;
|
||||
|
||||
@@ -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 =>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
153
LanMountainDesktop/Services/OfficeRecentDocumentsService.cs
Normal file
153
LanMountainDesktop/Services/OfficeRecentDocumentsService.cs
Normal file
@@ -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<OfficeRecentDocument> 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<OfficeRecentDocument> GetRecentDocuments(int maxCount = 20)
|
||||
{
|
||||
var documents = new List<OfficeRecentDocument>();
|
||||
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<string> GetRecentFolders()
|
||||
{
|
||||
var folders = new List<string>();
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
32
LanMountainDesktop/Services/ShortcutHelper.cs
Normal file
32
LanMountainDesktop/Services/ShortcutHelper.cs
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
@@ -17,6 +17,22 @@
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<Border Classes="component-editor-card"
|
||||
Padding="20">
|
||||
<StackPanel Spacing="12">
|
||||
<TextBlock x:Name="ColorSchemeHeaderTextBlock"
|
||||
Classes="component-editor-section-title" />
|
||||
<StackPanel Spacing="8">
|
||||
<RadioButton x:Name="FollowSystemRadioButton"
|
||||
GroupName="ColorScheme"
|
||||
IsCheckedChanged="OnColorSchemeChanged" />
|
||||
<RadioButton x:Name="UseNativeRadioButton"
|
||||
GroupName="ColorScheme"
|
||||
IsCheckedChanged="OnColorSchemeChanged" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<Border Classes="component-editor-card"
|
||||
Padding="20">
|
||||
<StackPanel Spacing="12">
|
||||
|
||||
@@ -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<ImportedClassScheduleSnapshot> _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)
|
||||
|
||||
@@ -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">
|
||||
<StackPanel Spacing="16">
|
||||
<Border Classes="component-editor-hero-card"
|
||||
<Border Classes="component-editor-hero_card"
|
||||
Padding="24">
|
||||
<StackPanel Spacing="8">
|
||||
<TextBlock x:Name="HeadlineTextBlock"
|
||||
@@ -17,6 +18,22 @@
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<Border Classes="component-editor-card"
|
||||
Padding="20">
|
||||
<StackPanel Spacing="12">
|
||||
<TextBlock x:Name="ColorSchemeHeaderTextBlock"
|
||||
Classes="component-editor-section-title" />
|
||||
<StackPanel Spacing="8">
|
||||
<RadioButton x:Name="FollowSystemRadioButton"
|
||||
GroupName="ColorScheme"
|
||||
IsCheckedChanged="OnColorSchemeChanged" />
|
||||
<RadioButton x:Name="UseNativeRadioButton"
|
||||
GroupName="ColorScheme"
|
||||
IsCheckedChanged="OnColorSchemeChanged" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<Border Classes="component-editor-card"
|
||||
Padding="20">
|
||||
<StackPanel Spacing="12">
|
||||
@@ -27,7 +44,7 @@
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<Border Classes="component-editor-card"
|
||||
<Border Classes="component-editor_card"
|
||||
Padding="20">
|
||||
<StackPanel Spacing="12">
|
||||
<TextBlock x:Name="DbfsHeaderTextBlock"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using Avalonia.Interactivity;
|
||||
using LanMountainDesktop.ComponentSystem;
|
||||
using LanMountainDesktop.Models;
|
||||
using LanMountainDesktop.Services;
|
||||
|
||||
namespace LanMountainDesktop.Views.ComponentEditors;
|
||||
|
||||
@@ -25,6 +26,8 @@ public partial class StudyEnvironmentComponentEditor : ComponentEditorViewBase
|
||||
var snapshot = LoadSnapshot();
|
||||
var showDisplayDb = snapshot.StudyEnvironmentShowDisplayDb;
|
||||
var showDbfs = snapshot.StudyEnvironmentShowDbfs;
|
||||
var colorSchemeSource = snapshot.ColorSchemeSource;
|
||||
|
||||
if (!showDisplayDb && !showDbfs)
|
||||
{
|
||||
showDisplayDb = true;
|
||||
@@ -32,16 +35,49 @@ public partial class StudyEnvironmentComponentEditor : ComponentEditorViewBase
|
||||
|
||||
HeadlineTextBlock.Text = Context?.Definition.DisplayName ?? "Study Environment";
|
||||
DescriptionTextBlock.Text = L("study.environment.settings.desc", "配置右侧实时噪音值显示内容。");
|
||||
|
||||
ColorSchemeHeaderTextBlock.Text = L("component.settings.color_scheme", "配色方案");
|
||||
FollowSystemRadioButton.Content = L("component.color_scheme.follow_system", "跟随系统配色");
|
||||
UseNativeRadioButton.Content = L("component.color_scheme.native", "使用组件自定义配色");
|
||||
|
||||
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;
|
||||
|
||||
if (string.IsNullOrEmpty(colorSchemeSource) ||
|
||||
colorSchemeSource == ThemeAppearanceValues.ColorSchemeFollowSystem)
|
||||
{
|
||||
FollowSystemRadioButton.IsChecked = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
UseNativeRadioButton.IsChecked = true;
|
||||
}
|
||||
|
||||
DisplayDbToggleSwitch.IsChecked = showDisplayDb;
|
||||
DbfsToggleSwitch.IsChecked = showDbfs;
|
||||
_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 void OnToggleChanged(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
_ = sender;
|
||||
|
||||
@@ -13,6 +13,7 @@ using Avalonia.Layout;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Styling;
|
||||
using Avalonia.Threading;
|
||||
using LanMountainDesktop.ComponentSystem;
|
||||
using LanMountainDesktop.Models;
|
||||
using LanMountainDesktop.Services;
|
||||
|
||||
@@ -50,6 +51,7 @@ public partial class BaiduHotSearchWidget : UserControl, IDesktopComponentWidget
|
||||
private bool _autoRefreshEnabled = true;
|
||||
private string _sourceType = BaiduHotSearchSourceTypes.Official;
|
||||
private bool _isNightVisual = true;
|
||||
private string? _componentColorScheme;
|
||||
|
||||
private sealed record HotItemVisual(
|
||||
Border Host,
|
||||
@@ -180,17 +182,25 @@ public partial class BaiduHotSearchWidget : UserControl, IDesktopComponentWidget
|
||||
|
||||
private void ApplyNightModeVisual()
|
||||
{
|
||||
var useMonetColor = ComponentColorSchemeHelper.ShouldUseMonetColor(
|
||||
_componentColorScheme,
|
||||
ComponentColorSchemeHelper.GetCurrentGlobalThemeColorMode());
|
||||
|
||||
var brandColor = useMonetColor
|
||||
? (_isNightVisual ? Color.Parse("#9FABFF") : Color.Parse("#4F6BEB"))
|
||||
: (_isNightVisual ? Color.Parse("#5D93FF") : Color.Parse("#2932E1"));
|
||||
|
||||
CardBorder.Background = new SolidColorBrush(_isNightVisual ? Color.Parse("#1B2129") : Color.Parse("#FCFCFD"));
|
||||
RootBorder.BorderBrush = new SolidColorBrush(_isNightVisual ? Color.Parse("#33FFFFFF") : Color.Parse("#00000000"));
|
||||
|
||||
BrandTextBlock.Foreground = new SolidColorBrush(_isNightVisual ? Color.Parse("#5D93FF") : Color.Parse("#2932E1"));
|
||||
BrandTextBlock.Foreground = new SolidColorBrush(brandColor);
|
||||
|
||||
RefreshButton.Background = new SolidColorBrush(_isNightVisual ? Color.Parse("#2D3440") : Color.Parse("#EFF1F5"));
|
||||
RefreshGlyphIcon.Foreground = new SolidColorBrush(_isNightVisual ? Color.Parse("#A8B1C2") : Color.Parse("#5E6671"));
|
||||
|
||||
foreach (var visual in _hotItemVisuals)
|
||||
{
|
||||
visual.IndexTextBlock.Foreground = new SolidColorBrush(_isNightVisual ? Color.Parse("#5D93FF") : Color.Parse("#2932E1"));
|
||||
visual.IndexTextBlock.Foreground = new SolidColorBrush(brandColor);
|
||||
visual.TitleTextBlock.Foreground = new SolidColorBrush(_isNightVisual ? Color.Parse("#E8EAED") : Color.Parse("#202327"));
|
||||
}
|
||||
|
||||
@@ -488,10 +498,11 @@ public partial class BaiduHotSearchWidget : UserControl, IDesktopComponentWidget
|
||||
enabled = snapshot.BaiduHotSearchAutoRefreshEnabled;
|
||||
intervalMinutes = NormalizeAutoRefreshIntervalMinutes(snapshot.BaiduHotSearchAutoRefreshIntervalMinutes);
|
||||
sourceType = BaiduHotSearchSourceTypes.Normalize(snapshot.BaiduHotSearchSourceType);
|
||||
_componentColorScheme = snapshot.ColorSchemeSource;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Keep fallback defaults.
|
||||
_componentColorScheme = null;
|
||||
}
|
||||
|
||||
_autoRefreshEnabled = enabled;
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Styling;
|
||||
using Avalonia.Threading;
|
||||
@@ -25,9 +26,17 @@ public partial class ClassScheduleWidget : UserControl, IDesktopComponentWidget,
|
||||
|
||||
private readonly DispatcherTimer _refreshTimer = new()
|
||||
{
|
||||
Interval = TimeSpan.FromMinutes(4)
|
||||
Interval = TimeSpan.FromMinutes(1)
|
||||
};
|
||||
|
||||
private int _lastCurrentCourseIndex = -1;
|
||||
private DateOnly _lastRefreshDate = DateOnly.MinValue;
|
||||
|
||||
private bool _isUserScrolling;
|
||||
private Vector _lastScrollOffset;
|
||||
private Point _dragStartPoint;
|
||||
private Point _lastDragPoint;
|
||||
|
||||
private ISettingsService _settingsService = HostSettingsFacadeProvider.GetOrCreate().Settings;
|
||||
private readonly LocalizationService _localizationService = new();
|
||||
private readonly IClassIslandScheduleDataService _scheduleService = new ClassIslandScheduleDataService();
|
||||
@@ -39,6 +48,7 @@ public partial class ClassScheduleWidget : UserControl, IDesktopComponentWidget,
|
||||
private string _languageCode = "zh-CN";
|
||||
private string _componentId = BuiltInComponentIds.DesktopClassSchedule;
|
||||
private string _placementId = string.Empty;
|
||||
private string? _componentColorScheme;
|
||||
|
||||
public ClassScheduleWidget()
|
||||
{
|
||||
@@ -50,6 +60,10 @@ public partial class ClassScheduleWidget : UserControl, IDesktopComponentWidget,
|
||||
SizeChanged += OnSizeChanged;
|
||||
ActualThemeVariantChanged += OnActualThemeVariantChanged;
|
||||
|
||||
ContentScrollViewer.PointerPressed += OnScrollViewerPointerPressed;
|
||||
ContentScrollViewer.PointerMoved += OnScrollViewerPointerMoved;
|
||||
ContentScrollViewer.PointerReleased += OnScrollViewerPointerReleased;
|
||||
|
||||
ApplyCellSize(_currentCellSize);
|
||||
RefreshSchedule();
|
||||
}
|
||||
@@ -107,9 +121,89 @@ public partial class ClassScheduleWidget : UserControl, IDesktopComponentWidget,
|
||||
RefreshSchedule();
|
||||
}
|
||||
|
||||
private void OnScrollViewerPointerPressed(object? sender, PointerPressedEventArgs e)
|
||||
{
|
||||
_isUserScrolling = true;
|
||||
_dragStartPoint = e.GetCurrentPoint(ContentScrollViewer).Position;
|
||||
_lastDragPoint = _dragStartPoint;
|
||||
_lastScrollOffset = ContentScrollViewer.Offset;
|
||||
}
|
||||
|
||||
private void OnScrollViewerPointerMoved(object? sender, PointerEventArgs e)
|
||||
{
|
||||
if (!_isUserScrolling)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var currentPoint = e.GetCurrentPoint(ContentScrollViewer);
|
||||
var currentPosition = currentPoint.Position;
|
||||
var deltaY = currentPosition.Y - _lastDragPoint.Y;
|
||||
|
||||
var newOffset = _lastScrollOffset;
|
||||
newOffset = newOffset.WithY(newOffset.Y - deltaY);
|
||||
|
||||
ContentScrollViewer.Offset = newOffset;
|
||||
_lastDragPoint = currentPosition;
|
||||
}
|
||||
|
||||
private void OnScrollViewerPointerReleased(object? sender, PointerReleasedEventArgs e)
|
||||
{
|
||||
_lastScrollOffset = ContentScrollViewer.Offset;
|
||||
}
|
||||
|
||||
private void OnRefreshTimerTick(object? sender, EventArgs e)
|
||||
{
|
||||
var now = _timeZoneService?.GetCurrentTime() ?? DateTime.Now;
|
||||
var currentDate = DateOnly.FromDateTime(now);
|
||||
|
||||
var previousCourseIndex = _lastCurrentCourseIndex;
|
||||
|
||||
RefreshSchedule();
|
||||
|
||||
var newCurrentCourseIndex = FindCurrentCourseIndex();
|
||||
_lastCurrentCourseIndex = newCurrentCourseIndex;
|
||||
|
||||
if (previousCourseIndex != newCurrentCourseIndex && newCurrentCourseIndex >= 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<CourseItemViewModel>();
|
||||
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))
|
||||
{
|
||||
var nextDay = today.AddDays(1);
|
||||
if (_scheduleService.TryResolveClassPlanForDate(snapshot, nextDay, out var nextDayClassPlan))
|
||||
{
|
||||
resolvedClassPlan = nextDayClassPlan;
|
||||
today = nextDay;
|
||||
}
|
||||
else
|
||||
{
|
||||
_courseItems = Array.Empty<CourseItemViewModel>();
|
||||
UpdateHeader(now);
|
||||
ShowStatus(L("schedule.widget.no_class_today", "今天没有课程"));
|
||||
RenderScheduleItems();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!snapshot.TimeLayouts.TryGetValue(resolvedClassPlan.ClassPlan.TimeLayoutId, out var layout))
|
||||
{
|
||||
_courseItems = Array.Empty<CourseItemViewModel>();
|
||||
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");
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
<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"
|
||||
xmlns:fi="using:FluentIcons.Avalonia"
|
||||
xmlns:vm="using:LanMountainDesktop.Views.Components"
|
||||
mc:Ignorable="d"
|
||||
d:DesignWidth="640"
|
||||
d:DesignHeight="320"
|
||||
x:Class="LanMountainDesktop.Views.Components.OfficeRecentDocumentsWidget">
|
||||
|
||||
<Border x:Name="RootBorder"
|
||||
CornerRadius="34"
|
||||
Background="{DynamicResource CardBackgroundFillColorDefaultBrush}"
|
||||
ClipToBounds="True"
|
||||
BorderThickness="0"
|
||||
Padding="0">
|
||||
<Grid>
|
||||
<Border x:Name="AccentCorner"
|
||||
Width="140"
|
||||
Height="140"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Top"
|
||||
Margin="0,-40,-40,0"
|
||||
CornerRadius="70"
|
||||
Background="{DynamicResource SystemAccentColorLight2Brush}"
|
||||
Opacity="0.2"
|
||||
IsHitTestVisible="False" />
|
||||
|
||||
<Grid RowDefinitions="Auto,*" RowSpacing="8" Margin="16,14,16,14">
|
||||
<Grid Grid.Row="0" ColumnDefinitions="*,Auto">
|
||||
<TextBlock x:Name="HeaderTextBlock"
|
||||
Text="最近文档"
|
||||
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
|
||||
FontSize="18"
|
||||
FontWeight="SemiBold"
|
||||
VerticalAlignment="Center" />
|
||||
<Button x:Name="RefreshButton"
|
||||
Grid.Column="1"
|
||||
Width="28"
|
||||
Height="28"
|
||||
CornerRadius="14"
|
||||
Background="Transparent"
|
||||
BorderBrush="Transparent"
|
||||
BorderThickness="0"
|
||||
Padding="0"
|
||||
Focusable="False"
|
||||
PointerPressed="OnRefreshPointerPressed">
|
||||
<fi:SymbolIcon Symbol="ArrowSync"
|
||||
FontSize="14"
|
||||
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}" />
|
||||
</Button>
|
||||
</Grid>
|
||||
|
||||
<ScrollViewer Grid.Row="1"
|
||||
HorizontalScrollBarVisibility="Auto"
|
||||
VerticalScrollBarVisibility="Disabled"
|
||||
Margin="0,4,0,0">
|
||||
<ItemsControl x:Name="DocumentsItemsControl">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<StackPanel Orientation="Horizontal" Spacing="8" />
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:OfficeRecentDocumentViewModel">
|
||||
<Border x:Name="DocumentCard"
|
||||
Width="130"
|
||||
Height="90"
|
||||
CornerRadius="10"
|
||||
Background="{DynamicResource AdaptiveGlassPanelBackgroundBrush}"
|
||||
Padding="10"
|
||||
Cursor="Hand"
|
||||
PointerPressed="OnDocumentCardPointerPressed">
|
||||
<Grid RowDefinitions="Auto,*,Auto">
|
||||
<TextBlock Grid.Row="0"
|
||||
Text="{Binding FileName}"
|
||||
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
|
||||
FontSize="12"
|
||||
FontWeight="Medium"
|
||||
TextTrimming="CharacterEllipsis"
|
||||
MaxLines="2"
|
||||
TextWrapping="Wrap"
|
||||
VerticalAlignment="Top" />
|
||||
<TextBlock Grid.Row="2"
|
||||
Text="{Binding TimeAgo}"
|
||||
Foreground="{DynamicResource AdaptiveTextTertiaryBrush}"
|
||||
FontSize="10"
|
||||
TextTrimming="CharacterEllipsis"
|
||||
MaxLines="1" />
|
||||
</Grid>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
|
||||
<TextBlock x:Name="StatusTextBlock"
|
||||
IsVisible="False"
|
||||
Text="暂无最近文档"
|
||||
Foreground="{DynamicResource AdaptiveTextTertiaryBrush}"
|
||||
FontSize="14"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
</Border>
|
||||
</UserControl>
|
||||
@@ -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<OfficeRecentDocument> _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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user