mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-22 00:54:26 +08:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bcf4be6d50 |
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
|
public static class BuiltInComponentIds
|
||||||
{
|
{
|
||||||
@@ -40,4 +40,5 @@ public static class BuiltInComponentIds
|
|||||||
public const string DesktopWhiteboard = "DesktopWhiteboard";
|
public const string DesktopWhiteboard = "DesktopWhiteboard";
|
||||||
public const string DesktopBlackboardLandscape = "DesktopBlackboardLandscape";
|
public const string DesktopBlackboardLandscape = "DesktopBlackboardLandscape";
|
||||||
public const string DesktopBrowser = "DesktopBrowser";
|
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.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using LanMountainDesktop.ComponentSystem.Extensions;
|
using LanMountainDesktop.ComponentSystem.Extensions;
|
||||||
@@ -327,6 +327,15 @@ public sealed class ComponentRegistry
|
|||||||
AllowStatusBarPlacement: false,
|
AllowStatusBarPlacement: false,
|
||||||
AllowDesktopPlacement: true,
|
AllowDesktopPlacement: true,
|
||||||
ResizeMode: DesktopComponentResizeMode.Free),
|
ResizeMode: DesktopComponentResizeMode.Free),
|
||||||
|
new DesktopComponentDefinition(
|
||||||
|
BuiltInComponentIds.DesktopOfficeRecentDocuments,
|
||||||
|
"Office Recent Documents",
|
||||||
|
"Folder",
|
||||||
|
"File",
|
||||||
|
MinWidthCells: 4,
|
||||||
|
MinHeightCells: 2,
|
||||||
|
AllowStatusBarPlacement: false,
|
||||||
|
AllowDesktopPlacement: true),
|
||||||
new DesktopComponentDefinition(
|
new DesktopComponentDefinition(
|
||||||
BuiltInComponentIds.Date,
|
BuiltInComponentIds.Date,
|
||||||
"Calendar",
|
"Calendar",
|
||||||
|
|||||||
@@ -255,7 +255,6 @@
|
|||||||
"settings.color.use_system_chrome_toggle": "Use system window chrome",
|
"settings.color.use_system_chrome_toggle": "Use system window chrome",
|
||||||
"settings.color.theme_color_label": "Theme accent color",
|
"settings.color.theme_color_label": "Theme accent color",
|
||||||
"settings.appearance.theme_color_mode_label": "Theme color source",
|
"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.neutral": "Default neutral",
|
||||||
"settings.appearance.theme_color_mode.user": "User theme color Monet",
|
"settings.appearance.theme_color_mode.user": "User theme color Monet",
|
||||||
"settings.appearance.theme_color_mode.wallpaper": "Wallpaper 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.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.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.",
|
"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.none": "None",
|
||||||
"settings.appearance.system_material.mica": "Mica",
|
"settings.appearance.system_material.mica": "Mica",
|
||||||
"settings.appearance.system_material.acrylic": "Acrylic",
|
"settings.appearance.system_material.acrylic": "Acrylic",
|
||||||
@@ -580,6 +581,7 @@
|
|||||||
"component.whiteboard": "Blackboard (Portrait)",
|
"component.whiteboard": "Blackboard (Portrait)",
|
||||||
"component.blackboard_landscape": "Blackboard (Landscape)",
|
"component.blackboard_landscape": "Blackboard (Landscape)",
|
||||||
"component.browser": "Browser",
|
"component.browser": "Browser",
|
||||||
|
"component.office_recent_documents": "Recent Documents",
|
||||||
"component.holiday_calendar": "Holiday Calendar",
|
"component.holiday_calendar": "Holiday Calendar",
|
||||||
"component.study_environment": "Environment",
|
"component.study_environment": "Environment",
|
||||||
"component.study_session_control": "Study Session Control",
|
"component.study_session_control": "Study Session Control",
|
||||||
|
|||||||
@@ -260,7 +260,6 @@
|
|||||||
"settings.color.use_system_chrome_toggle": "使用系统窗口标题栏",
|
"settings.color.use_system_chrome_toggle": "使用系统窗口标题栏",
|
||||||
"settings.color.theme_color_label": "主题强调色",
|
"settings.color.theme_color_label": "主题强调色",
|
||||||
"settings.appearance.theme_color_mode_label": "主题色来源",
|
"settings.appearance.theme_color_mode_label": "主题色来源",
|
||||||
"settings.appearance.system_material_label": "系统材质",
|
|
||||||
"settings.appearance.theme_color_mode.neutral": "默认中性",
|
"settings.appearance.theme_color_mode.neutral": "默认中性",
|
||||||
"settings.appearance.theme_color_mode.user": "用户主题色 Monet",
|
"settings.appearance.theme_color_mode.user": "用户主题色 Monet",
|
||||||
"settings.appearance.theme_color_mode.wallpaper": "壁纸 Monet 取色",
|
"settings.appearance.theme_color_mode.wallpaper": "壁纸 Monet 取色",
|
||||||
@@ -270,6 +269,8 @@
|
|||||||
"settings.appearance.theme_color_preview.app": "当前正在预览从应用壁纸提取的颜色。",
|
"settings.appearance.theme_color_preview.app": "当前正在预览从应用壁纸提取的颜色。",
|
||||||
"settings.appearance.theme_color_preview.system": "当前正在预览从系统壁纸提取的颜色。",
|
"settings.appearance.theme_color_preview.system": "当前正在预览从系统壁纸提取的颜色。",
|
||||||
"settings.appearance.theme_color_preview.fallback": "没有可用壁纸,当前使用回退强调色。",
|
"settings.appearance.theme_color_preview.fallback": "没有可用壁纸,当前使用回退强调色。",
|
||||||
|
"component.color_scheme.follow_system": "跟随系统配色",
|
||||||
|
"component.color_scheme.native": "使用组件自定义配色",
|
||||||
"settings.appearance.system_material.none": "无",
|
"settings.appearance.system_material.none": "无",
|
||||||
"settings.appearance.system_material.mica": "Mica",
|
"settings.appearance.system_material.mica": "Mica",
|
||||||
"settings.appearance.system_material.acrylic": "Acrylic",
|
"settings.appearance.system_material.acrylic": "Acrylic",
|
||||||
@@ -585,6 +586,7 @@
|
|||||||
"component.whiteboard": "竖向小黑板",
|
"component.whiteboard": "竖向小黑板",
|
||||||
"component.blackboard_landscape": "横向小黑板",
|
"component.blackboard_landscape": "横向小黑板",
|
||||||
"component.browser": "浏览器",
|
"component.browser": "浏览器",
|
||||||
|
"component.office_recent_documents": "最近文档",
|
||||||
"component.holiday_calendar": "节假日日历",
|
"component.holiday_calendar": "节假日日历",
|
||||||
"component.study_environment": "环境",
|
"component.study_environment": "环境",
|
||||||
"component.study_session_control": "自习时段控制",
|
"component.study_session_control": "自习时段控制",
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ public sealed class ComponentSettingsSnapshot
|
|||||||
{
|
{
|
||||||
public string DailyArtworkMirrorSource { get; set; } = DailyArtworkMirrorSources.Overseas;
|
public string DailyArtworkMirrorSource { get; set; } = DailyArtworkMirrorSources.Overseas;
|
||||||
|
|
||||||
|
public string? ColorSchemeSource { get; set; }
|
||||||
|
|
||||||
public List<ImportedClassScheduleSnapshot> ImportedClassSchedules { get; set; } = [];
|
public List<ImportedClassScheduleSnapshot> ImportedClassSchedules { get; set; } = [];
|
||||||
|
|
||||||
public string ActiveImportedClassScheduleId { get; set; } = string.Empty;
|
public string ActiveImportedClassScheduleId { get; set; } = string.Empty;
|
||||||
|
|||||||
@@ -248,6 +248,15 @@ internal sealed class WindowMaterialService : IWindowMaterialService
|
|||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(window);
|
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;
|
window.Background = Brushes.Transparent;
|
||||||
|
|
||||||
if (!OperatingSystem.IsWindows() || !IsTransparencyEnabled())
|
if (!OperatingSystem.IsWindows() || !IsTransparencyEnabled())
|
||||||
@@ -259,7 +268,6 @@ internal sealed class WindowMaterialService : IWindowMaterialService
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var normalizedMode = ThemeAppearanceValues.NormalizeSystemMaterialMode(materialMode);
|
|
||||||
window.TransparencyLevelHint = normalizedMode switch
|
window.TransparencyLevelHint = normalizedMode switch
|
||||||
{
|
{
|
||||||
ThemeAppearanceValues.MaterialMica =>
|
ThemeAppearanceValues.MaterialMica =>
|
||||||
|
|||||||
@@ -163,7 +163,7 @@ public sealed class ClassIslandScheduleDataService : IClassIslandScheduleDataSer
|
|||||||
var totalElapsedWeeks = (int)Math.Floor(
|
var totalElapsedWeeks = (int)Math.Floor(
|
||||||
(referenceDate.ToDateTime(TimeOnly.MinValue) - cycleRule.SingleWeekStartDate.Value.ToDateTime(TimeOnly.MinValue)).TotalDays / 7d);
|
(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
|
var cycleOffset = cycleLength < cycleRule.MultiWeekRotationOffset.Count
|
||||||
? cycleRule.MultiWeekRotationOffset[cycleLength]
|
? cycleRule.MultiWeekRotationOffset[cycleLength]
|
||||||
@@ -668,7 +668,7 @@ public sealed class ClassIslandScheduleDataService : IClassIslandScheduleDataSer
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (weekCountDivTotal <= 1 || weekCountDivTotal >= cyclePositions.Count)
|
if (weekCountDivTotal <= 0 || weekCountDivTotal >= cyclePositions.Count)
|
||||||
{
|
{
|
||||||
return false;
|
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 ColorModeSeedMonet = "seed_monet";
|
||||||
public const string ColorModeWallpaperMonet = "wallpaper_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 MaterialNone = "none";
|
||||||
public const string MaterialMica = "mica";
|
public const string MaterialMica = "mica";
|
||||||
public const string MaterialAcrylic = "acrylic";
|
public const string MaterialAcrylic = "acrylic";
|
||||||
|
|||||||
@@ -17,6 +17,22 @@
|
|||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Border>
|
</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"
|
<Border Classes="component-editor-card"
|
||||||
Padding="20">
|
Padding="20">
|
||||||
<StackPanel Spacing="12">
|
<StackPanel Spacing="12">
|
||||||
|
|||||||
@@ -11,13 +11,15 @@ using Avalonia.Media;
|
|||||||
using Avalonia.Platform.Storage;
|
using Avalonia.Platform.Storage;
|
||||||
using LanMountainDesktop.ComponentSystem;
|
using LanMountainDesktop.ComponentSystem;
|
||||||
using LanMountainDesktop.Models;
|
using LanMountainDesktop.Models;
|
||||||
|
using LanMountainDesktop.Services;
|
||||||
|
|
||||||
namespace LanMountainDesktop.Views.ComponentEditors;
|
namespace LanMountainDesktop.Views.ComponentEditors;
|
||||||
|
|
||||||
public partial class ClassScheduleComponentEditor : ComponentEditorViewBase
|
public partial class ClassScheduleComponentEditor : ComponentEditorViewBase
|
||||||
{
|
{
|
||||||
private readonly List<ImportedClassScheduleSnapshot> _importedSchedules = [];
|
private readonly List<ImportedClassScheduleSnapshot> _importedSchedules = [];
|
||||||
private string _activeScheduleId = string.Empty;
|
private string? _activeScheduleId;
|
||||||
|
private bool _suppressEvents;
|
||||||
|
|
||||||
public ClassScheduleComponentEditor()
|
public ClassScheduleComponentEditor()
|
||||||
: this(null)
|
: this(null)
|
||||||
@@ -62,10 +64,49 @@ public partial class ClassScheduleComponentEditor : ComponentEditorViewBase
|
|||||||
|
|
||||||
private void ApplyState()
|
private void ApplyState()
|
||||||
{
|
{
|
||||||
|
var snapshot = LoadSnapshot();
|
||||||
|
var colorSchemeSource = snapshot.ColorSchemeSource;
|
||||||
|
|
||||||
HeadlineTextBlock.Text = Context?.Definition.DisplayName ?? "Class Schedule";
|
HeadlineTextBlock.Text = Context?.Definition.DisplayName ?? "Class Schedule";
|
||||||
DescriptionTextBlock.Text = L("schedule.settings.desc", "导入 ClassIsland 的 CSES 课表文件并选择启用项。");
|
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", "添加课表");
|
AddScheduleButton.Content = L("schedule.settings.add", "添加课表");
|
||||||
EmptyStateTextBlock.Text = L("schedule.settings.empty", "暂无导入课表");
|
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)
|
private async void OnAddScheduleClick(object? sender, RoutedEventArgs e)
|
||||||
|
|||||||
@@ -2,10 +2,11 @@
|
|||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
xmlns:ui="using:FluentAvalonia.UI.Controls"
|
||||||
mc:Ignorable="d"
|
mc:Ignorable="d"
|
||||||
x:Class="LanMountainDesktop.Views.ComponentEditors.StudyEnvironmentComponentEditor">
|
x:Class="LanMountainDesktop.Views.ComponentEditors.StudyEnvironmentComponentEditor">
|
||||||
<StackPanel Spacing="16">
|
<StackPanel Spacing="16">
|
||||||
<Border Classes="component-editor-hero-card"
|
<Border Classes="component-editor-hero_card"
|
||||||
Padding="24">
|
Padding="24">
|
||||||
<StackPanel Spacing="8">
|
<StackPanel Spacing="8">
|
||||||
<TextBlock x:Name="HeadlineTextBlock"
|
<TextBlock x:Name="HeadlineTextBlock"
|
||||||
@@ -17,6 +18,22 @@
|
|||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Border>
|
</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"
|
<Border Classes="component-editor-card"
|
||||||
Padding="20">
|
Padding="20">
|
||||||
<StackPanel Spacing="12">
|
<StackPanel Spacing="12">
|
||||||
@@ -27,7 +44,7 @@
|
|||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
<Border Classes="component-editor-card"
|
<Border Classes="component-editor_card"
|
||||||
Padding="20">
|
Padding="20">
|
||||||
<StackPanel Spacing="12">
|
<StackPanel Spacing="12">
|
||||||
<TextBlock x:Name="DbfsHeaderTextBlock"
|
<TextBlock x:Name="DbfsHeaderTextBlock"
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using Avalonia.Interactivity;
|
using Avalonia.Interactivity;
|
||||||
using LanMountainDesktop.ComponentSystem;
|
using LanMountainDesktop.ComponentSystem;
|
||||||
using LanMountainDesktop.Models;
|
using LanMountainDesktop.Models;
|
||||||
|
using LanMountainDesktop.Services;
|
||||||
|
|
||||||
namespace LanMountainDesktop.Views.ComponentEditors;
|
namespace LanMountainDesktop.Views.ComponentEditors;
|
||||||
|
|
||||||
@@ -25,6 +26,8 @@ public partial class StudyEnvironmentComponentEditor : ComponentEditorViewBase
|
|||||||
var snapshot = LoadSnapshot();
|
var snapshot = LoadSnapshot();
|
||||||
var showDisplayDb = snapshot.StudyEnvironmentShowDisplayDb;
|
var showDisplayDb = snapshot.StudyEnvironmentShowDisplayDb;
|
||||||
var showDbfs = snapshot.StudyEnvironmentShowDbfs;
|
var showDbfs = snapshot.StudyEnvironmentShowDbfs;
|
||||||
|
var colorSchemeSource = snapshot.ColorSchemeSource;
|
||||||
|
|
||||||
if (!showDisplayDb && !showDbfs)
|
if (!showDisplayDb && !showDbfs)
|
||||||
{
|
{
|
||||||
showDisplayDb = true;
|
showDisplayDb = true;
|
||||||
@@ -32,16 +35,49 @@ public partial class StudyEnvironmentComponentEditor : ComponentEditorViewBase
|
|||||||
|
|
||||||
HeadlineTextBlock.Text = Context?.Definition.DisplayName ?? "Study Environment";
|
HeadlineTextBlock.Text = Context?.Definition.DisplayName ?? "Study Environment";
|
||||||
DescriptionTextBlock.Text = L("study.environment.settings.desc", "配置右侧实时噪音值显示内容。");
|
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");
|
DisplayDbToggleSwitch.Content = L("study.environment.settings.show_display_db", "显示 display dB");
|
||||||
DbfsToggleSwitch.Content = L("study.environment.settings.show_dbfs", "显示 dBFS");
|
DbfsToggleSwitch.Content = L("study.environment.settings.show_dbfs", "显示 dBFS");
|
||||||
HintTextBlock.Text = L("study.environment.settings.hint", "至少启用一种显示方式。");
|
HintTextBlock.Text = L("study.environment.settings.hint", "至少启用一种显示方式。");
|
||||||
|
|
||||||
_suppressEvents = true;
|
_suppressEvents = true;
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(colorSchemeSource) ||
|
||||||
|
colorSchemeSource == ThemeAppearanceValues.ColorSchemeFollowSystem)
|
||||||
|
{
|
||||||
|
FollowSystemRadioButton.IsChecked = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
UseNativeRadioButton.IsChecked = true;
|
||||||
|
}
|
||||||
|
|
||||||
DisplayDbToggleSwitch.IsChecked = showDisplayDb;
|
DisplayDbToggleSwitch.IsChecked = showDisplayDb;
|
||||||
DbfsToggleSwitch.IsChecked = showDbfs;
|
DbfsToggleSwitch.IsChecked = showDbfs;
|
||||||
_suppressEvents = false;
|
_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)
|
private void OnToggleChanged(object? sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
_ = sender;
|
_ = sender;
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ using Avalonia.Layout;
|
|||||||
using Avalonia.Media;
|
using Avalonia.Media;
|
||||||
using Avalonia.Styling;
|
using Avalonia.Styling;
|
||||||
using Avalonia.Threading;
|
using Avalonia.Threading;
|
||||||
|
using LanMountainDesktop.ComponentSystem;
|
||||||
using LanMountainDesktop.Models;
|
using LanMountainDesktop.Models;
|
||||||
using LanMountainDesktop.Services;
|
using LanMountainDesktop.Services;
|
||||||
|
|
||||||
@@ -50,6 +51,7 @@ public partial class BaiduHotSearchWidget : UserControl, IDesktopComponentWidget
|
|||||||
private bool _autoRefreshEnabled = true;
|
private bool _autoRefreshEnabled = true;
|
||||||
private string _sourceType = BaiduHotSearchSourceTypes.Official;
|
private string _sourceType = BaiduHotSearchSourceTypes.Official;
|
||||||
private bool _isNightVisual = true;
|
private bool _isNightVisual = true;
|
||||||
|
private string? _componentColorScheme;
|
||||||
|
|
||||||
private sealed record HotItemVisual(
|
private sealed record HotItemVisual(
|
||||||
Border Host,
|
Border Host,
|
||||||
@@ -180,17 +182,25 @@ public partial class BaiduHotSearchWidget : UserControl, IDesktopComponentWidget
|
|||||||
|
|
||||||
private void ApplyNightModeVisual()
|
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"));
|
CardBorder.Background = new SolidColorBrush(_isNightVisual ? Color.Parse("#1B2129") : Color.Parse("#FCFCFD"));
|
||||||
RootBorder.BorderBrush = new SolidColorBrush(_isNightVisual ? Color.Parse("#33FFFFFF") : Color.Parse("#00000000"));
|
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"));
|
RefreshButton.Background = new SolidColorBrush(_isNightVisual ? Color.Parse("#2D3440") : Color.Parse("#EFF1F5"));
|
||||||
RefreshGlyphIcon.Foreground = new SolidColorBrush(_isNightVisual ? Color.Parse("#A8B1C2") : Color.Parse("#5E6671"));
|
RefreshGlyphIcon.Foreground = new SolidColorBrush(_isNightVisual ? Color.Parse("#A8B1C2") : Color.Parse("#5E6671"));
|
||||||
|
|
||||||
foreach (var visual in _hotItemVisuals)
|
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"));
|
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;
|
enabled = snapshot.BaiduHotSearchAutoRefreshEnabled;
|
||||||
intervalMinutes = NormalizeAutoRefreshIntervalMinutes(snapshot.BaiduHotSearchAutoRefreshIntervalMinutes);
|
intervalMinutes = NormalizeAutoRefreshIntervalMinutes(snapshot.BaiduHotSearchAutoRefreshIntervalMinutes);
|
||||||
sourceType = BaiduHotSearchSourceTypes.Normalize(snapshot.BaiduHotSearchSourceType);
|
sourceType = BaiduHotSearchSourceTypes.Normalize(snapshot.BaiduHotSearchSourceType);
|
||||||
|
_componentColorScheme = snapshot.ColorSchemeSource;
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
// Keep fallback defaults.
|
_componentColorScheme = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
_autoRefreshEnabled = enabled;
|
_autoRefreshEnabled = enabled;
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Avalonia;
|
using Avalonia;
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Input;
|
||||||
using Avalonia.Media;
|
using Avalonia.Media;
|
||||||
using Avalonia.Styling;
|
using Avalonia.Styling;
|
||||||
using Avalonia.Threading;
|
using Avalonia.Threading;
|
||||||
@@ -25,9 +26,17 @@ public partial class ClassScheduleWidget : UserControl, IDesktopComponentWidget,
|
|||||||
|
|
||||||
private readonly DispatcherTimer _refreshTimer = new()
|
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 ISettingsService _settingsService = HostSettingsFacadeProvider.GetOrCreate().Settings;
|
||||||
private readonly LocalizationService _localizationService = new();
|
private readonly LocalizationService _localizationService = new();
|
||||||
private readonly IClassIslandScheduleDataService _scheduleService = new ClassIslandScheduleDataService();
|
private readonly IClassIslandScheduleDataService _scheduleService = new ClassIslandScheduleDataService();
|
||||||
@@ -39,6 +48,7 @@ public partial class ClassScheduleWidget : UserControl, IDesktopComponentWidget,
|
|||||||
private string _languageCode = "zh-CN";
|
private string _languageCode = "zh-CN";
|
||||||
private string _componentId = BuiltInComponentIds.DesktopClassSchedule;
|
private string _componentId = BuiltInComponentIds.DesktopClassSchedule;
|
||||||
private string _placementId = string.Empty;
|
private string _placementId = string.Empty;
|
||||||
|
private string? _componentColorScheme;
|
||||||
|
|
||||||
public ClassScheduleWidget()
|
public ClassScheduleWidget()
|
||||||
{
|
{
|
||||||
@@ -50,6 +60,10 @@ public partial class ClassScheduleWidget : UserControl, IDesktopComponentWidget,
|
|||||||
SizeChanged += OnSizeChanged;
|
SizeChanged += OnSizeChanged;
|
||||||
ActualThemeVariantChanged += OnActualThemeVariantChanged;
|
ActualThemeVariantChanged += OnActualThemeVariantChanged;
|
||||||
|
|
||||||
|
ContentScrollViewer.PointerPressed += OnScrollViewerPointerPressed;
|
||||||
|
ContentScrollViewer.PointerMoved += OnScrollViewerPointerMoved;
|
||||||
|
ContentScrollViewer.PointerReleased += OnScrollViewerPointerReleased;
|
||||||
|
|
||||||
ApplyCellSize(_currentCellSize);
|
ApplyCellSize(_currentCellSize);
|
||||||
RefreshSchedule();
|
RefreshSchedule();
|
||||||
}
|
}
|
||||||
@@ -107,9 +121,89 @@ public partial class ClassScheduleWidget : UserControl, IDesktopComponentWidget,
|
|||||||
RefreshSchedule();
|
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)
|
private void OnRefreshTimerTick(object? sender, EventArgs e)
|
||||||
{
|
{
|
||||||
|
var now = _timeZoneService?.GetCurrentTime() ?? DateTime.Now;
|
||||||
|
var currentDate = DateOnly.FromDateTime(now);
|
||||||
|
|
||||||
|
var previousCourseIndex = _lastCurrentCourseIndex;
|
||||||
|
|
||||||
RefreshSchedule();
|
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()
|
public void RefreshFromSettings()
|
||||||
@@ -134,44 +228,75 @@ public partial class ClassScheduleWidget : UserControl, IDesktopComponentWidget,
|
|||||||
_componentId,
|
_componentId,
|
||||||
_placementId);
|
_placementId);
|
||||||
_languageCode = _localizationService.NormalizeLanguageCode(appSettings.LanguageCode);
|
_languageCode = _localizationService.NormalizeLanguageCode(appSettings.LanguageCode);
|
||||||
|
_componentColorScheme = componentSettings.ColorSchemeSource;
|
||||||
var now = _timeZoneService?.GetCurrentTime() ?? DateTime.Now;
|
var now = _timeZoneService?.GetCurrentTime() ?? DateTime.Now;
|
||||||
UpdateHeader(now);
|
var today = DateOnly.FromDateTime(now);
|
||||||
|
|
||||||
var importedSchedulePath = ResolveImportedSchedulePath(componentSettings);
|
var importedSchedulePath = ResolveImportedSchedulePath(componentSettings);
|
||||||
var readResult = _scheduleService.Load(importedSchedulePath);
|
var readResult = _scheduleService.Load(importedSchedulePath);
|
||||||
if (!readResult.Success || readResult.Snapshot is null)
|
if (!readResult.Success || readResult.Snapshot is null)
|
||||||
{
|
{
|
||||||
_courseItems = Array.Empty<CourseItemViewModel>();
|
_courseItems = Array.Empty<CourseItemViewModel>();
|
||||||
|
UpdateHeader(now);
|
||||||
ShowStatus(L("schedule.widget.no_source", "未读取到 ClassIsland 课表"));
|
ShowStatus(L("schedule.widget.no_source", "未读取到 ClassIsland 课表"));
|
||||||
RenderScheduleItems();
|
RenderScheduleItems();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var snapshot = readResult.Snapshot;
|
var snapshot = readResult.Snapshot;
|
||||||
var today = DateOnly.FromDateTime(now);
|
|
||||||
if (!_scheduleService.TryResolveClassPlanForDate(snapshot, today, out var resolvedClassPlan))
|
if (!_scheduleService.TryResolveClassPlanForDate(snapshot, today, out var resolvedClassPlan))
|
||||||
{
|
{
|
||||||
_courseItems = Array.Empty<CourseItemViewModel>();
|
var nextDay = today.AddDays(1);
|
||||||
ShowStatus(L("schedule.widget.no_class_today", "今天没有课程"));
|
if (_scheduleService.TryResolveClassPlanForDate(snapshot, nextDay, out var nextDayClassPlan))
|
||||||
RenderScheduleItems();
|
{
|
||||||
return;
|
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))
|
if (!snapshot.TimeLayouts.TryGetValue(resolvedClassPlan.ClassPlan.TimeLayoutId, out var layout))
|
||||||
{
|
{
|
||||||
_courseItems = Array.Empty<CourseItemViewModel>();
|
_courseItems = Array.Empty<CourseItemViewModel>();
|
||||||
|
UpdateHeader(now);
|
||||||
ShowStatus(L("schedule.widget.layout_missing", "课表时间布局缺失"));
|
ShowStatus(L("schedule.widget.layout_missing", "课表时间布局缺失"));
|
||||||
RenderScheduleItems();
|
RenderScheduleItems();
|
||||||
return;
|
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)
|
if (_courseItems.Count == 0)
|
||||||
{
|
{
|
||||||
ShowStatus(L("schedule.widget.no_class_today", "今天没有课程"));
|
ShowStatus(L("schedule.widget.no_class_today", "今天没有课程"));
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
var currentIndex = FindCurrentCourseIndex();
|
||||||
|
_lastCurrentCourseIndex = currentIndex;
|
||||||
HideStatus();
|
HideStatus();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -336,6 +461,10 @@ public partial class ClassScheduleWidget : UserControl, IDesktopComponentWidget,
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var useMonetColor = ComponentColorSchemeHelper.ShouldUseMonetColor(
|
||||||
|
_componentColorScheme,
|
||||||
|
ComponentColorSchemeHelper.GetCurrentGlobalThemeColorMode());
|
||||||
|
|
||||||
var scale = ResolveScale();
|
var scale = ResolveScale();
|
||||||
var bulletSize = Math.Clamp(10 * scale, 5, 12);
|
var bulletSize = Math.Clamp(10 * scale, 5, 12);
|
||||||
var courseNameSize = Math.Clamp(42 * scale, 14, 42);
|
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 primaryBrush = CreateBrush(_isNightVisual ? "#F9FBFF" : "#151821");
|
||||||
var secondaryBrush = CreateBrush(_isNightVisual ? "#848B99" : "#667084");
|
var secondaryBrush = CreateBrush(_isNightVisual ? "#848B99" : "#667084");
|
||||||
var currentBrush = CreateBrush("#FF4D5A");
|
var currentBrush = useMonetColor
|
||||||
|
? CreateBrush("#FF4FC3F7")
|
||||||
|
: CreateBrush("#FF4D5A");
|
||||||
var normalBulletBrush = CreateBrush(_isNightVisual ? "#B8BEC9" : "#9AA3B2");
|
var normalBulletBrush = CreateBrush(_isNightVisual ? "#B8BEC9" : "#9AA3B2");
|
||||||
|
|
||||||
var visibleItems = _courseItems.Take(maxVisibleItems).ToList();
|
var visibleItems = _courseItems.Take(maxVisibleItems).ToList();
|
||||||
@@ -438,9 +569,22 @@ public partial class ClassScheduleWidget : UserControl, IDesktopComponentWidget,
|
|||||||
|
|
||||||
private void ApplyAdaptiveLayout()
|
private void ApplyAdaptiveLayout()
|
||||||
{
|
{
|
||||||
|
if (Bounds.Width <= 0 || Bounds.Height <= 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var scale = ResolveScale();
|
var scale = ResolveScale();
|
||||||
_isNightVisual = ResolveNightMode();
|
_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);
|
var cornerRadius = Math.Clamp(_currentCellSize * 0.45, 24, 44);
|
||||||
RootBorder.CornerRadius = new CornerRadius(cornerRadius);
|
RootBorder.CornerRadius = new CornerRadius(cornerRadius);
|
||||||
RootBorder.Background = _isNightVisual
|
RootBorder.Background = _isNightVisual
|
||||||
@@ -468,7 +612,7 @@ public partial class ClassScheduleWidget : UserControl, IDesktopComponentWidget,
|
|||||||
|
|
||||||
MonthTextBlock.Foreground = CreateBrush(_isNightVisual ? "#F8FAFF" : "#131722");
|
MonthTextBlock.Foreground = CreateBrush(_isNightVisual ? "#F8FAFF" : "#131722");
|
||||||
DayTextBlock.Foreground = CreateBrush(_isNightVisual ? "#F8FAFF" : "#131722");
|
DayTextBlock.Foreground = CreateBrush(_isNightVisual ? "#F8FAFF" : "#131722");
|
||||||
SlashTextBlock.Foreground = CreateBrush("#FF3250");
|
SlashTextBlock.Foreground = slashBrush;
|
||||||
WeekdayTextBlock.Foreground = CreateBrush(_isNightVisual ? "#C6CBD5" : "#4B5463");
|
WeekdayTextBlock.Foreground = CreateBrush(_isNightVisual ? "#C6CBD5" : "#4B5463");
|
||||||
ClassCountTextBlock.Foreground = CreateBrush(_isNightVisual ? "#8D95A4" : "#738095");
|
ClassCountTextBlock.Foreground = CreateBrush(_isNightVisual ? "#8D95A4" : "#738095");
|
||||||
StatusTextBlock.Foreground = CreateBrush(_isNightVisual ? "#9AA2B1" : "#4B5565");
|
StatusTextBlock.Foreground = CreateBrush(_isNightVisual ? "#9AA2B1" : "#4B5565");
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
@@ -439,6 +439,11 @@ public sealed class DesktopComponentRuntimeRegistry
|
|||||||
"component.browser",
|
"component.browser",
|
||||||
() => new BrowserWidget(),
|
() => new BrowserWidget(),
|
||||||
cellSize => Math.Clamp(cellSize * 0.24, 10, 24)),
|
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(
|
new DesktopComponentRuntimeRegistration(
|
||||||
BuiltInComponentIds.HolidayCalendar,
|
BuiltInComponentIds.HolidayCalendar,
|
||||||
"component.holiday_calendar",
|
"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.Controls;
|
||||||
using Avalonia.Media;
|
using Avalonia.Media;
|
||||||
using Avalonia.Threading;
|
using Avalonia.Threading;
|
||||||
|
using LanMountainDesktop.ComponentSystem;
|
||||||
using LanMountainDesktop.Models;
|
using LanMountainDesktop.Models;
|
||||||
using LanMountainDesktop.Services;
|
using LanMountainDesktop.Services;
|
||||||
|
|
||||||
@@ -24,6 +25,7 @@ public partial class StudyEnvironmentWidget : UserControl, IDesktopComponentWidg
|
|||||||
private double _currentCellSize = 48;
|
private double _currentCellSize = 48;
|
||||||
private bool _showDisplayDb = true;
|
private bool _showDisplayDb = true;
|
||||||
private bool _showDbfs;
|
private bool _showDbfs;
|
||||||
|
private string? _componentColorScheme;
|
||||||
private string _languageCode = "zh-CN";
|
private string _languageCode = "zh-CN";
|
||||||
private bool _isAttached;
|
private bool _isAttached;
|
||||||
private bool _isOnActivePage = true;
|
private bool _isOnActivePage = true;
|
||||||
@@ -147,6 +149,7 @@ public partial class StudyEnvironmentWidget : UserControl, IDesktopComponentWidg
|
|||||||
_languageCode = _localizationService.NormalizeLanguageCode(appSnapshot.LanguageCode);
|
_languageCode = _localizationService.NormalizeLanguageCode(appSnapshot.LanguageCode);
|
||||||
_showDisplayDb = componentSnapshot.StudyEnvironmentShowDisplayDb;
|
_showDisplayDb = componentSnapshot.StudyEnvironmentShowDisplayDb;
|
||||||
_showDbfs = componentSnapshot.StudyEnvironmentShowDbfs;
|
_showDbfs = componentSnapshot.StudyEnvironmentShowDbfs;
|
||||||
|
_componentColorScheme = componentSnapshot.ColorSchemeSource;
|
||||||
if (!_showDisplayDb && !_showDbfs)
|
if (!_showDisplayDb && !_showDbfs)
|
||||||
{
|
{
|
||||||
_showDisplayDb = true;
|
_showDisplayDb = true;
|
||||||
@@ -287,22 +290,26 @@ public partial class StudyEnvironmentWidget : UserControl, IDesktopComponentWidg
|
|||||||
|
|
||||||
private IBrush ResolveStatusBrush(StudyAnalyticsSnapshot snapshot)
|
private IBrush ResolveStatusBrush(StudyAnalyticsSnapshot snapshot)
|
||||||
{
|
{
|
||||||
|
var useMonetColor = ComponentColorSchemeHelper.ShouldUseMonetColor(
|
||||||
|
_componentColorScheme,
|
||||||
|
ComponentColorSchemeHelper.GetCurrentGlobalThemeColorMode());
|
||||||
|
|
||||||
if (snapshot.State == StudyAnalyticsRuntimeState.Unsupported ||
|
if (snapshot.State == StudyAnalyticsRuntimeState.Unsupported ||
|
||||||
snapshot.State == StudyAnalyticsRuntimeState.Error ||
|
snapshot.State == StudyAnalyticsRuntimeState.Error ||
|
||||||
snapshot.StreamStatus == NoiseStreamStatus.Error)
|
snapshot.StreamStatus == NoiseStreamStatus.Error)
|
||||||
{
|
{
|
||||||
return CreateBrush("#FFFF7B7B");
|
return useMonetColor ? CreateBrush("#FF6FD7A2") : CreateBrush("#FFFF7B7B");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (snapshot.StreamStatus == NoiseStreamStatus.Noisy)
|
if (snapshot.StreamStatus == NoiseStreamStatus.Noisy)
|
||||||
{
|
{
|
||||||
return CreateBrush("#FFFFB14A");
|
return useMonetColor ? CreateBrush("#FF4FC3F7") : CreateBrush("#FFFFB14A");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (snapshot.State == StudyAnalyticsRuntimeState.Running &&
|
if (snapshot.State == StudyAnalyticsRuntimeState.Running &&
|
||||||
snapshot.StreamStatus == NoiseStreamStatus.Quiet)
|
snapshot.StreamStatus == NoiseStreamStatus.Quiet)
|
||||||
{
|
{
|
||||||
return CreateBrush("#FF6FD7A2");
|
return useMonetColor ? CreateBrush("#FF81C784") : CreateBrush("#FF6FD7A2");
|
||||||
}
|
}
|
||||||
|
|
||||||
return TryResolveThemeBrush("AdaptiveTextPrimaryBrush", "#FFEFF3FF");
|
return TryResolveThemeBrush("AdaptiveTextPrimaryBrush", "#FFEFF3FF");
|
||||||
|
|||||||
Reference in New Issue
Block a user