mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-20 23:54:26 +08:00
feat.便签组件
This commit is contained in:
112
.trae/skills/refactoring-insight/SKILL.md
Normal file
112
.trae/skills/refactoring-insight/SKILL.md
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
---
|
||||||
|
name: "refactoring-insight"
|
||||||
|
description: "Analyzes codebase for refactoring opportunities: large files, code duplication, god classes, naming inconsistencies, tight coupling, and missing abstractions. Invoke when user asks for refactoring insight/analysis or wants to improve code architecture."
|
||||||
|
---
|
||||||
|
|
||||||
|
# Refactoring Insight
|
||||||
|
|
||||||
|
Deep codebase analysis skill that identifies structural problems and produces prioritized refactoring recommendations.
|
||||||
|
|
||||||
|
## When to Invoke
|
||||||
|
|
||||||
|
- User asks for "refactoring insight", "refactoring analysis", "code quality analysis", "architecture review"
|
||||||
|
- User wants to understand what should be refactored in the codebase
|
||||||
|
- User asks "where are the code smells?" or "what needs refactoring?"
|
||||||
|
|
||||||
|
## Analysis Dimensions
|
||||||
|
|
||||||
|
Run all 6 dimensions in parallel where possible. For each dimension, use search agents to gather data, then synthesize findings.
|
||||||
|
|
||||||
|
### 1. Large Files / God Classes
|
||||||
|
|
||||||
|
- Find all .cs files over 300 lines, sorted by line count descending
|
||||||
|
- Identify partial classes and sum their total line count across files
|
||||||
|
- Flag classes with 15+ methods or constructors taking 8+ parameters
|
||||||
|
- Focus on: Views/, ViewModels/, Services/, plugins/
|
||||||
|
|
||||||
|
**Output**: Table of files with line counts and responsibility summary.
|
||||||
|
|
||||||
|
### 2. Code Duplication
|
||||||
|
|
||||||
|
Search for these specific duplication patterns:
|
||||||
|
|
||||||
|
- **Service boilerplate**: Repeated DI registration, `new` instantiation instead of DI
|
||||||
|
- **Data service pattern**: Services that fetch/parse/transform data similarly (Load → Map → Save)
|
||||||
|
- **Localization pattern**: `private readonly LocalizationService _localizationService = new();` and `L()` helper method repetitions
|
||||||
|
- **Helper method duplication**: Methods like `ResolveUnifiedMainRadiusValue`, `NormalizeConfig`, `ParticleState` classes copied across files
|
||||||
|
- **Error handling pattern**: Identical try-catch blocks repeated in multiple methods
|
||||||
|
- **Settings snapshot pattern**: `_settingsFacade.Settings.LoadSnapshot<T>(scope)` call sites
|
||||||
|
|
||||||
|
**Output**: List of duplicated patterns with file locations and line numbers.
|
||||||
|
|
||||||
|
### 3. Tight Coupling
|
||||||
|
|
||||||
|
- Services instantiated via `new` instead of DI injection
|
||||||
|
- ViewModels directly accessing infrastructure-layer APIs (e.g., `LoadSnapshot/SaveSnapshot`)
|
||||||
|
- Hard-coded dependencies (GitHub repo owner/name, default values)
|
||||||
|
- `Application.Current` upcasting to access services: `(Application.Current as App)?.SomeService`
|
||||||
|
- Platform-specific code embedded in cross-platform services without interface abstraction
|
||||||
|
|
||||||
|
**Output**: Table of coupling violations with severity (high/medium/low).
|
||||||
|
|
||||||
|
### 4. Naming Inconsistencies
|
||||||
|
|
||||||
|
- Service suffix inconsistency: `Service` vs `Store` vs `Helper` vs `Provider` vs `Manager` vs `Factory` for similar responsibilities
|
||||||
|
- Model suffix inconsistency: `Snapshot` vs `State` vs `Types` for similar concepts
|
||||||
|
- Platform prefix inconsistency: `Windows`/`Linux` full name vs `Mac` abbreviation
|
||||||
|
- Confusing names: services with similar names but different responsibilities (e.g., `NotificationService` vs `NotificationListenerService`)
|
||||||
|
|
||||||
|
**Output**: Categorized list of naming inconsistencies.
|
||||||
|
|
||||||
|
### 5. Missing Abstractions
|
||||||
|
|
||||||
|
- Services without corresponding interfaces (check for `I<ServiceName>` pattern)
|
||||||
|
- Common patterns that could be extracted into base classes:
|
||||||
|
- `SettingsPageViewModelBase` for shared ViewModel boilerplate
|
||||||
|
- `JsonFileSettingsService<TSnapshot>` for repeated settings persistence
|
||||||
|
- `SettingsDomainServiceBase<TState>` for Load-Map-Save pattern
|
||||||
|
- `DesktopComponentWidgetBase` for shared Widget code
|
||||||
|
- `ComponentEditorViewBase` enhancements (e.g., `_suppressEvents` pattern)
|
||||||
|
- Static singleton/Factory providers repeating thread-safe lazy-load boilerplate
|
||||||
|
|
||||||
|
**Output**: List of missing abstractions with proposed base class/interface names.
|
||||||
|
|
||||||
|
### 6. Misplaced Responsibilities
|
||||||
|
|
||||||
|
- Files in wrong directories (e.g., data access in Settings/, UI services mixed with data services)
|
||||||
|
- ViewModels containing business logic or file system operations
|
||||||
|
- Widget code-behind files with excessive logic (>200 lines)
|
||||||
|
- Platform-specific services not organized into subdirectories
|
||||||
|
|
||||||
|
**Output**: List of misplaced files/classes with recommended new locations.
|
||||||
|
|
||||||
|
## Output Format
|
||||||
|
|
||||||
|
Produce a structured report with:
|
||||||
|
|
||||||
|
1. **Summary table**: Total metrics (file count, duplication count, etc.)
|
||||||
|
2. **Priority-ranked findings**: P0 (must fix), P1 (should fix), P2 (recommended), P3 (nice to have)
|
||||||
|
3. **Each finding includes**: Problem description, affected files with links, specific line numbers, recommended action, estimated impact
|
||||||
|
|
||||||
|
### Priority Criteria
|
||||||
|
|
||||||
|
- **P0**: Files over 1000 lines with mixed responsibilities; patterns duplicated 10+ times; god classes with 20+ dependencies
|
||||||
|
- **P1**: Patterns duplicated 5-9 times; services without interfaces that are widely used; DI bypass affecting testability
|
||||||
|
- **P2**: Patterns duplicated 3-4 times; naming inconsistencies affecting readability; misplaced files
|
||||||
|
- **P3**: Minor naming variations; single-instance duplications; organizational improvements
|
||||||
|
|
||||||
|
## Project-Specific Context
|
||||||
|
|
||||||
|
This skill is aware of the LanMountainDesktop project structure:
|
||||||
|
|
||||||
|
- `LanMountainDesktop/Services/` — Business and infrastructure services
|
||||||
|
- `LanMountainDesktop/Services/Settings/` — Settings subsystem
|
||||||
|
- `LanMountainDesktop/ViewModels/` — View models
|
||||||
|
- `LanMountainDesktop/Views/Components/` — Desktop widget components
|
||||||
|
- `LanMountainDesktop/Views/ComponentEditors/` — Widget editor views
|
||||||
|
- `LanMountainDesktop/plugins/` — Plugin runtime
|
||||||
|
- `LanMountainDesktop.PluginSdk/` — Plugin SDK public API
|
||||||
|
- `LanMountainDesktop.Shared.Contracts/` — Host/plugin shared contracts
|
||||||
|
- `LanMountainDesktop.Appearance/` — Appearance and corner radius infrastructure
|
||||||
|
|
||||||
|
When analyzing, respect the project's architectural boundaries documented in `docs/ARCHITECTURE.md` and `docs/ECOSYSTEM_BOUNDARIES.md`.
|
||||||
67
CHANGELOG.md
67
CHANGELOG.md
@@ -1,18 +1,53 @@
|
|||||||
# 更新日志 / Changelog
|
# 更新日志 / Changelog
|
||||||
|
|
||||||
|
## [0.8.3.3](https://github.com/yourorg/LanMountainDesktop/releases/tag/v0.8.3.3) - 2026-04-12
|
||||||
## [0.8.3.2] - 2026-04-09
|
|
||||||
|
|
||||||
### 新增 (Added)
|
### 新增 (Added)
|
||||||
|
|
||||||
|
- ✨ **便签组件**: 全新便签组件上线,支持 Markdown 语法
|
||||||
|
- 支持丰富的 Markdown 格式:标题、列表、加粗、斜体、代码块等
|
||||||
|
- 便签内容自动保存,方便记录和管理日常备忘。丰富信息展示途径,让作业布置也可在阑山桌面完成。
|
||||||
|
- ✨ **白板主题自适应笔色**: 白板功能新增主题自适应笔色支持
|
||||||
|
- 根据当前主题自动调整画笔颜色,确保在不同主题下都有良好的书写体验
|
||||||
|
- 深色主题下自动切换为浅色笔迹,浅色主题下使用深色笔迹
|
||||||
|
|
||||||
|
### 变更 (Changed)
|
||||||
|
|
||||||
|
- 🎨 **融合桌面设置组件库样式更新**: 优化融合桌面设置页面的组件库样式
|
||||||
|
- 提升视觉一致性和用户体验
|
||||||
|
- 统一组件风格,与整体设计语言保持协调
|
||||||
|
|
||||||
|
### 修复 (Fixed)
|
||||||
|
|
||||||
|
- 🐛 **白板无法使用问题**: 修复了白板功能无法正常使用的问题
|
||||||
|
- 问题原因: 相关依赖或初始化逻辑异常导致白板功能失效
|
||||||
|
- 修复方案: 修复了白板的依赖加载和初始化流程,恢复正常使用
|
||||||
|
- 🐛 **央官网新闻组件显示问题**: 修复了央官网新闻组件的显示异常
|
||||||
|
- 优化组件渲染逻辑,确保新闻内容正确展示
|
||||||
|
- 🐛 **课程表组件显示问题**: 修复了课程表组件的显示异常
|
||||||
|
- 优化组件布局和渲染,确保课程信息正确显示
|
||||||
|
|
||||||
|
### 移除 (Removed)
|
||||||
|
|
||||||
|
- 无
|
||||||
|
|
||||||
|
***
|
||||||
|
|
||||||
|
## [0.8.3.2](https://github.com/yourorg/LanMountainDesktop/releases/tag/v0.8.3.2) - 2026-04-09
|
||||||
|
|
||||||
|
### 新增 (Added)
|
||||||
|
|
||||||
- ✨ **应用启动台图标卡片显示选项**: 新增应用启动台图标卡片显示设置
|
- ✨ **应用启动台图标卡片显示选项**: 新增应用启动台图标卡片显示设置
|
||||||
- 用户可在设置中选择是否显示应用图标的专属卡片背景
|
- 用户可在设置中选择是否显示应用图标的专属卡片背景
|
||||||
- 关闭后仅显示应用图标本身,更加简洁
|
- 关闭后仅显示应用图标本身,更加简洁
|
||||||
- 支持动态切换,实时预览效果
|
- 支持动态切换,实时预览效果
|
||||||
|
|
||||||
### 变更 (Changed)
|
### 变更 (Changed)
|
||||||
|
|
||||||
- 无
|
- 无
|
||||||
|
|
||||||
### 修复 (Fixed)
|
### 修复 (Fixed)
|
||||||
|
|
||||||
- 🐛 **应用启动台文件夹应用数量限制**: 修复了应用启动台文件夹无法查看超过12个应用的问题
|
- 🐛 **应用启动台文件夹应用数量限制**: 修复了应用启动台文件夹无法查看超过12个应用的问题
|
||||||
- 问题原因: 文件夹弹窗未实现滚动功能,应用列表超出显示区域后被截断
|
- 问题原因: 文件夹弹窗未实现滚动功能,应用列表超出显示区域后被截断
|
||||||
- 修复方案: 为文件夹内容区域添加滚动支持,允许用户滚动查看所有应用
|
- 修复方案: 为文件夹内容区域添加滚动支持,允许用户滚动查看所有应用
|
||||||
@@ -24,13 +59,15 @@
|
|||||||
- 修复方案: 在 `IncrementalUpdateItems()` 方法中同步更新课程项的文字颜色
|
- 修复方案: 在 `IncrementalUpdateItems()` 方法中同步更新课程项的文字颜色
|
||||||
|
|
||||||
### 移除 (Removed)
|
### 移除 (Removed)
|
||||||
|
|
||||||
- 🗑️ **更新页面重复标题**: 移除了更新页面中重复的更新标题,优化页面布局
|
- 🗑️ **更新页面重复标题**: 移除了更新页面中重复的更新标题,优化页面布局
|
||||||
|
|
||||||
---
|
***
|
||||||
|
|
||||||
## [0.8.3.1] - 2026-04-08
|
## [0.8.3.1](https://github.com/yourorg/LanMountainDesktop/releases/tag/v0.8.3.1) - 2026-04-08
|
||||||
|
|
||||||
### 新增 (Added)
|
### 新增 (Added)
|
||||||
|
|
||||||
- ✨ **快捷方式组件**: 新增快捷方式组件,可在阑山桌面内便捷打开系统应用与文件
|
- ✨ **快捷方式组件**: 新增快捷方式组件,可在阑山桌面内便捷打开系统应用与文件
|
||||||
- 支持创建快捷方式,统一管理应用和文件
|
- 支持创建快捷方式,统一管理应用和文件
|
||||||
- 提供单击打开和双击打开两种交互模式
|
- 提供单击打开和双击打开两种交互模式
|
||||||
@@ -38,38 +75,46 @@
|
|||||||
- 📝 初始化更新日志文档,为后续版本发布建立基础
|
- 📝 初始化更新日志文档,为后续版本发布建立基础
|
||||||
|
|
||||||
### 变更 (Changed)
|
### 变更 (Changed)
|
||||||
|
|
||||||
- 无
|
- 无
|
||||||
|
|
||||||
### 修复 (Fixed)
|
### 修复 (Fixed)
|
||||||
|
|
||||||
- 无
|
- 无
|
||||||
|
|
||||||
### 移除 (Removed)
|
### 移除 (Removed)
|
||||||
|
|
||||||
- 无
|
- 无
|
||||||
|
|
||||||
---
|
***
|
||||||
|
|
||||||
所有重要的更改都将记录在此文件中。
|
所有重要的更改都将记录在此文件中。
|
||||||
|
|
||||||
格式基于 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.0.0/),
|
格式基于 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.0.0/),
|
||||||
并且本项目遵循 [语义化版本](https://semver.org/lang/zh-CN/)。
|
并且本项目遵循 [语义化版本](https://semver.org/lang/zh-CN/)。
|
||||||
|
|
||||||
---
|
***
|
||||||
|
|
||||||
## [格式示例]
|
## \[格式示例]
|
||||||
|
|
||||||
### 新增 (Added)
|
### 新增 (Added)
|
||||||
|
|
||||||
- 待发布的新功能
|
- 待发布的新功能
|
||||||
|
|
||||||
### 变更 (Changed)
|
### 变更 (Changed)
|
||||||
|
|
||||||
- 待发布的变更
|
- 待发布的变更
|
||||||
|
|
||||||
### 修复 (Fixed)
|
### 修复 (Fixed)
|
||||||
|
|
||||||
- 待发布的修复
|
- 待发布的修复
|
||||||
|
|
||||||
### 移除 (Removed)
|
### 移除 (Removed)
|
||||||
|
|
||||||
- 待发布的移除项
|
- 待发布的移除项
|
||||||
|
|
||||||
---
|
***
|
||||||
|
|
||||||
## 版本说明
|
## 版本说明
|
||||||
|
|
||||||
### 版本号规则
|
### 版本号规则
|
||||||
@@ -101,10 +146,6 @@
|
|||||||
- 🔒 **安全**: 安全相关
|
- 🔒 **安全**: 安全相关
|
||||||
- 🌐 **国际化**: 国际化/本地化
|
- 🌐 **国际化**: 国际化/本地化
|
||||||
|
|
||||||
---
|
***
|
||||||
|
|
||||||
## 链接
|
## 链接
|
||||||
|
|
||||||
[Unreleased]: https://github.com/yourorg/LanMountainDesktop/compare/v0.8.3.2...HEAD
|
|
||||||
[0.8.3.2]: https://github.com/yourorg/LanMountainDesktop/releases/tag/v0.8.3.2
|
|
||||||
[0.8.3.1]: https://github.com/yourorg/LanMountainDesktop/releases/tag/v0.8.3.1
|
|
||||||
|
|||||||
@@ -47,4 +47,5 @@ public static class BuiltInComponentIds
|
|||||||
public const string DesktopFileManager = "DesktopFileManager";
|
public const string DesktopFileManager = "DesktopFileManager";
|
||||||
public const string DesktopNotificationBox = "DesktopNotificationBox";
|
public const string DesktopNotificationBox = "DesktopNotificationBox";
|
||||||
public const string DesktopShortcut = "DesktopShortcut";
|
public const string DesktopShortcut = "DesktopShortcut";
|
||||||
|
public const string DesktopStickyNote = "DesktopStickyNote";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -327,6 +327,16 @@ public sealed class ComponentRegistry
|
|||||||
AllowStatusBarPlacement: false,
|
AllowStatusBarPlacement: false,
|
||||||
AllowDesktopPlacement: true,
|
AllowDesktopPlacement: true,
|
||||||
ResizeMode: DesktopComponentResizeMode.Free),
|
ResizeMode: DesktopComponentResizeMode.Free),
|
||||||
|
new DesktopComponentDefinition(
|
||||||
|
BuiltInComponentIds.DesktopStickyNote,
|
||||||
|
"Sticky Note",
|
||||||
|
"Notepad",
|
||||||
|
"Board",
|
||||||
|
MinWidthCells: 2,
|
||||||
|
MinHeightCells: 2,
|
||||||
|
AllowStatusBarPlacement: false,
|
||||||
|
AllowDesktopPlacement: true,
|
||||||
|
ResizeMode: DesktopComponentResizeMode.Free),
|
||||||
new DesktopComponentDefinition(
|
new DesktopComponentDefinition(
|
||||||
BuiltInComponentIds.DesktopBrowser,
|
BuiltInComponentIds.DesktopBrowser,
|
||||||
"Browser",
|
"Browser",
|
||||||
|
|||||||
@@ -142,6 +142,12 @@ public sealed class ComponentSettingsSnapshot
|
|||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
#region Sticky Note Component Settings (便签组件设置)
|
||||||
|
|
||||||
|
public string StickyNoteContent { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
public ComponentSettingsSnapshot Clone()
|
public ComponentSettingsSnapshot Clone()
|
||||||
{
|
{
|
||||||
var clone = (ComponentSettingsSnapshot)MemberwiseClone();
|
var clone = (ComponentSettingsSnapshot)MemberwiseClone();
|
||||||
|
|||||||
@@ -452,6 +452,10 @@ public sealed class DesktopComponentRuntimeRegistry
|
|||||||
BuiltInComponentIds.DesktopBlackboardLandscape,
|
BuiltInComponentIds.DesktopBlackboardLandscape,
|
||||||
"component.blackboard_landscape",
|
"component.blackboard_landscape",
|
||||||
() => new WhiteboardWidget(baseWidthCells: 4)),
|
() => new WhiteboardWidget(baseWidthCells: 4)),
|
||||||
|
new DesktopComponentRuntimeRegistration(
|
||||||
|
BuiltInComponentIds.DesktopStickyNote,
|
||||||
|
"component.sticky_note",
|
||||||
|
() => new StickyNoteWidget()),
|
||||||
new DesktopComponentRuntimeRegistration(
|
new DesktopComponentRuntimeRegistration(
|
||||||
BuiltInComponentIds.DesktopBrowser,
|
BuiltInComponentIds.DesktopBrowser,
|
||||||
"component.browser",
|
"component.browser",
|
||||||
|
|||||||
51
LanMountainDesktop/Views/Components/StickyNoteWidget.axaml
Normal file
51
LanMountainDesktop/Views/Components/StickyNoteWidget.axaml
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
<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:md="clr-namespace:Markdown.Avalonia;assembly=Markdown.Avalonia"
|
||||||
|
mc:Ignorable="d"
|
||||||
|
d:DesignWidth="200"
|
||||||
|
d:DesignHeight="200"
|
||||||
|
x:Class="LanMountainDesktop.Views.Components.StickyNoteWidget">
|
||||||
|
|
||||||
|
<Border x:Name="RootBorder"
|
||||||
|
CornerRadius="{DynamicResource DesignCornerRadiusComponent}"
|
||||||
|
BorderBrush="#E0C878"
|
||||||
|
BorderThickness="1">
|
||||||
|
<Grid>
|
||||||
|
<md:MarkdownScrollViewer x:Name="MarkdownViewer"
|
||||||
|
Margin="14,14,14,10"
|
||||||
|
IsVisible="True" />
|
||||||
|
|
||||||
|
<TextBox x:Name="NoteTextBox"
|
||||||
|
AcceptsReturn="True"
|
||||||
|
TextWrapping="Wrap"
|
||||||
|
IsVisible="False"
|
||||||
|
Background="Transparent"
|
||||||
|
BorderThickness="0"
|
||||||
|
Margin="14,14,14,10"
|
||||||
|
FontFamily="Consolas,Cascadia Code,Courier New,monospace"
|
||||||
|
Foreground="#5D4E37" />
|
||||||
|
|
||||||
|
<Button x:Name="ToggleButton"
|
||||||
|
Width="28"
|
||||||
|
Height="28"
|
||||||
|
CornerRadius="14"
|
||||||
|
HorizontalAlignment="Right"
|
||||||
|
VerticalAlignment="Top"
|
||||||
|
Margin="4,4,4,0"
|
||||||
|
Padding="0"
|
||||||
|
Background="#00000010"
|
||||||
|
BorderThickness="0"
|
||||||
|
Click="OnToggleButtonClick">
|
||||||
|
<fi:SymbolIcon x:Name="ToggleIcon"
|
||||||
|
Symbol="Edit"
|
||||||
|
IconVariant="Regular"
|
||||||
|
FontSize="13"
|
||||||
|
Foreground="#8B7D5A" />
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
</UserControl>
|
||||||
371
LanMountainDesktop/Views/Components/StickyNoteWidget.axaml.cs
Normal file
371
LanMountainDesktop/Views/Components/StickyNoteWidget.axaml.cs
Normal file
@@ -0,0 +1,371 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Interactivity;
|
||||||
|
using Avalonia.Media;
|
||||||
|
using Avalonia.Styling;
|
||||||
|
using Avalonia.Threading;
|
||||||
|
using FluentIcons.Common;
|
||||||
|
using LanMountainDesktop.ComponentSystem;
|
||||||
|
using LanMountainDesktop.Host.Abstractions;
|
||||||
|
using LanMountainDesktop.Models;
|
||||||
|
using LanMountainDesktop.PluginSdk;
|
||||||
|
using LanMountainDesktop.Services.Settings;
|
||||||
|
|
||||||
|
namespace LanMountainDesktop.Views.Components;
|
||||||
|
|
||||||
|
public partial class StickyNoteWidget : UserControl,
|
||||||
|
IDesktopComponentWidget,
|
||||||
|
IComponentPlacementContextAware,
|
||||||
|
IComponentSettingsContextAware,
|
||||||
|
IDesktopPageVisibilityAwareComponentWidget,
|
||||||
|
IDisposable
|
||||||
|
{
|
||||||
|
private static readonly Color LightNoteYellow = Color.FromRgb(0xFF, 0xF9, 0xC4);
|
||||||
|
private static readonly Color LightNoteBorder = Color.FromRgb(0xE0, 0xC8, 0x78);
|
||||||
|
private static readonly Color LightNoteForeground = Color.FromRgb(0x5D, 0x4E, 0x37);
|
||||||
|
private static readonly Color LightNoteHint = Color.FromRgb(0x8B, 0x7D, 0x5A);
|
||||||
|
|
||||||
|
private static readonly Color DarkNoteYellow = Color.FromRgb(0x5D, 0x52, 0x29);
|
||||||
|
private static readonly Color DarkNoteBorder = Color.FromRgb(0x7A, 0x6D, 0x3A);
|
||||||
|
private static readonly Color DarkNoteForeground = Color.FromRgb(0xE8, 0xE0, 0xC8);
|
||||||
|
private static readonly Color DarkNoteHint = Color.FromRgb(0xA0, 0x96, 0x70);
|
||||||
|
|
||||||
|
private string _componentId = BuiltInComponentIds.DesktopStickyNote;
|
||||||
|
private string _placementId = string.Empty;
|
||||||
|
private IComponentSettingsAccessor? _settingsAccessor;
|
||||||
|
private string _markdownContent = string.Empty;
|
||||||
|
private bool _isEditing;
|
||||||
|
private bool _isDirty;
|
||||||
|
private bool _isOnActivePage = true;
|
||||||
|
private bool _isEditMode;
|
||||||
|
private bool _disposed;
|
||||||
|
private bool _isApplyingPersistedContent;
|
||||||
|
|
||||||
|
private readonly DispatcherTimer _autoSaveTimer = new()
|
||||||
|
{
|
||||||
|
Interval = TimeSpan.FromSeconds(30)
|
||||||
|
};
|
||||||
|
|
||||||
|
private CancellationTokenSource? _renderDebounceCts;
|
||||||
|
|
||||||
|
public StickyNoteWidget()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
|
||||||
|
_autoSaveTimer.Tick += OnAutoSaveTimerTick;
|
||||||
|
NoteTextBox.TextChanged += OnNoteTextBoxTextChanged;
|
||||||
|
AttachedToVisualTree += OnAttachedToVisualTree;
|
||||||
|
DetachedFromVisualTree += OnDetachedFromVisualTree;
|
||||||
|
|
||||||
|
ApplyNoteColors();
|
||||||
|
UpdateDisplay();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ApplyCellSize(double cellSize)
|
||||||
|
{
|
||||||
|
var scale = Math.Clamp(cellSize / 48d, 0.82, 2.2);
|
||||||
|
|
||||||
|
RootBorder.CornerRadius = new CornerRadius(
|
||||||
|
ComponentChromeCornerRadiusHelper.ResolveMainRectangleRadiusValue(
|
||||||
|
new ComponentChromeContext(
|
||||||
|
_componentId,
|
||||||
|
_placementId,
|
||||||
|
Math.Max(1, cellSize),
|
||||||
|
Appearance.AppearanceCornerRadiusTokenFactory.Create(
|
||||||
|
Settings.Core.GlobalAppearanceSettings.DefaultCornerRadiusStyle))));
|
||||||
|
|
||||||
|
RootBorder.Padding = new Thickness(
|
||||||
|
Math.Clamp(2 * scale, 1, 4),
|
||||||
|
Math.Clamp(2 * scale, 1, 4));
|
||||||
|
|
||||||
|
var contentMargin = Math.Clamp(12 * scale, 6, 20);
|
||||||
|
MarkdownViewer.Margin = new Thickness(contentMargin, contentMargin, contentMargin, contentMargin - 2);
|
||||||
|
NoteTextBox.Margin = new Thickness(contentMargin, contentMargin, contentMargin, contentMargin - 2);
|
||||||
|
NoteTextBox.FontSize = Math.Clamp(13 * scale, 10, 22);
|
||||||
|
|
||||||
|
var buttonSize = Math.Clamp(28 * scale, 22, 40);
|
||||||
|
ToggleButton.Width = buttonSize;
|
||||||
|
ToggleButton.Height = buttonSize;
|
||||||
|
ToggleButton.CornerRadius = new CornerRadius(buttonSize / 2d);
|
||||||
|
ToggleButton.Margin = new Thickness(Math.Clamp(4 * scale, 2, 8), Math.Clamp(4 * scale, 2, 8), Math.Clamp(4 * scale, 2, 8), 0);
|
||||||
|
ToggleIcon.FontSize = Math.Clamp(13 * scale, 10, 18);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetComponentPlacementContext(string componentId, string? placementId)
|
||||||
|
{
|
||||||
|
if (_isDirty && !string.IsNullOrWhiteSpace(_placementId))
|
||||||
|
{
|
||||||
|
PersistNoteImmediately();
|
||||||
|
}
|
||||||
|
|
||||||
|
_componentId = string.IsNullOrWhiteSpace(componentId)
|
||||||
|
? BuiltInComponentIds.DesktopStickyNote
|
||||||
|
: componentId.Trim();
|
||||||
|
_placementId = placementId?.Trim() ?? string.Empty;
|
||||||
|
|
||||||
|
if (_isEditing)
|
||||||
|
{
|
||||||
|
ExitEditMode();
|
||||||
|
}
|
||||||
|
|
||||||
|
LoadPersistedContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetComponentSettingsContext(DesktopComponentSettingsContext context)
|
||||||
|
{
|
||||||
|
_settingsAccessor = context.ComponentSettingsAccessor;
|
||||||
|
LoadPersistedContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetDesktopPageContext(bool isOnActivePage, bool isEditMode)
|
||||||
|
{
|
||||||
|
_isOnActivePage = isOnActivePage;
|
||||||
|
_isEditMode = isEditMode;
|
||||||
|
|
||||||
|
ToggleButton.IsHitTestVisible = !isEditMode;
|
||||||
|
NoteTextBox.IsReadOnly = isEditMode;
|
||||||
|
|
||||||
|
if (isEditMode && _isEditing)
|
||||||
|
{
|
||||||
|
ExitEditMode();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnToggleButtonClick(object? sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (_isEditing)
|
||||||
|
{
|
||||||
|
ExitEditMode();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
EnterEditMode();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void EnterEditMode()
|
||||||
|
{
|
||||||
|
_isEditing = true;
|
||||||
|
|
||||||
|
NoteTextBox.Text = _markdownContent;
|
||||||
|
MarkdownViewer.IsVisible = false;
|
||||||
|
NoteTextBox.IsVisible = true;
|
||||||
|
ToggleIcon.Symbol = Symbol.Checkmark;
|
||||||
|
|
||||||
|
Dispatcher.UIThread.Post(() => NoteTextBox.Focus(), DispatcherPriority.Input);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ExitEditMode()
|
||||||
|
{
|
||||||
|
_isEditing = false;
|
||||||
|
|
||||||
|
var editedContent = NoteTextBox.Text ?? string.Empty;
|
||||||
|
if (editedContent != _markdownContent)
|
||||||
|
{
|
||||||
|
_markdownContent = editedContent;
|
||||||
|
_isDirty = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
NoteTextBox.IsVisible = false;
|
||||||
|
MarkdownViewer.IsVisible = true;
|
||||||
|
ToggleIcon.Symbol = Symbol.Edit;
|
||||||
|
|
||||||
|
UpdateDisplay();
|
||||||
|
|
||||||
|
if (_isDirty)
|
||||||
|
{
|
||||||
|
PersistNoteImmediately();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnNoteTextBoxTextChanged(object? sender, TextChangedEventArgs e)
|
||||||
|
{
|
||||||
|
if (_isApplyingPersistedContent || !_isEditing)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_isDirty = true;
|
||||||
|
|
||||||
|
if (!_autoSaveTimer.IsEnabled)
|
||||||
|
{
|
||||||
|
_autoSaveTimer.Start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnAutoSaveTimerTick(object? sender, EventArgs e)
|
||||||
|
{
|
||||||
|
_autoSaveTimer.Stop();
|
||||||
|
|
||||||
|
if (_isDirty && _isEditing)
|
||||||
|
{
|
||||||
|
_markdownContent = NoteTextBox.Text ?? string.Empty;
|
||||||
|
PersistNoteImmediately();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateDisplay()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(_markdownContent))
|
||||||
|
{
|
||||||
|
MarkdownViewer.Markdown = "*Click ✏️ to write a note...*";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_renderDebounceCts?.Cancel();
|
||||||
|
_renderDebounceCts?.Dispose();
|
||||||
|
_renderDebounceCts = new CancellationTokenSource();
|
||||||
|
var token = _renderDebounceCts.Token;
|
||||||
|
|
||||||
|
Dispatcher.UIThread.Post(async () =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await Task.Delay(150, token);
|
||||||
|
if (!token.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
MarkdownViewer.Markdown = _markdownContent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException) { }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
MarkdownViewer.Markdown = $"*Error: {ex.Message}*";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LoadPersistedContent()
|
||||||
|
{
|
||||||
|
if (_settingsAccessor is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var snapshot = _settingsAccessor.LoadSnapshot<ComponentSettingsSnapshot>();
|
||||||
|
_isApplyingPersistedContent = true;
|
||||||
|
_markdownContent = snapshot.StickyNoteContent ?? string.Empty;
|
||||||
|
_isDirty = false;
|
||||||
|
UpdateDisplay();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
_markdownContent = string.Empty;
|
||||||
|
UpdateDisplay();
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_isApplyingPersistedContent = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void PersistNoteImmediately()
|
||||||
|
{
|
||||||
|
if (_settingsAccessor is null || _disposed)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var snapshot = _settingsAccessor.LoadSnapshot<ComponentSettingsSnapshot>();
|
||||||
|
snapshot.StickyNoteContent = _markdownContent;
|
||||||
|
_settingsAccessor.SaveSnapshot(snapshot,
|
||||||
|
[nameof(ComponentSettingsSnapshot.StickyNoteContent)]);
|
||||||
|
_isDirty = false;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ApplyNoteColors()
|
||||||
|
{
|
||||||
|
var isDark = Application.Current?.ActualThemeVariant == ThemeVariant.Dark;
|
||||||
|
|
||||||
|
if (isDark)
|
||||||
|
{
|
||||||
|
RootBorder.Background = new SolidColorBrush(DarkNoteYellow);
|
||||||
|
RootBorder.BorderBrush = new SolidColorBrush(DarkNoteBorder);
|
||||||
|
NoteTextBox.Foreground = new SolidColorBrush(DarkNoteForeground);
|
||||||
|
ToggleIcon.Foreground = new SolidColorBrush(DarkNoteHint);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
RootBorder.Background = new SolidColorBrush(LightNoteYellow);
|
||||||
|
RootBorder.BorderBrush = new SolidColorBrush(LightNoteBorder);
|
||||||
|
NoteTextBox.Foreground = new SolidColorBrush(LightNoteForeground);
|
||||||
|
ToggleIcon.Foreground = new SolidColorBrush(LightNoteHint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnAttachedToVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
|
||||||
|
{
|
||||||
|
Application.Current!.ActualThemeVariantChanged += OnThemeVariantChanged;
|
||||||
|
ApplyNoteColors();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnDetachedFromVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
|
||||||
|
{
|
||||||
|
Application.Current!.ActualThemeVariantChanged -= OnThemeVariantChanged;
|
||||||
|
|
||||||
|
if (_isDirty)
|
||||||
|
{
|
||||||
|
if (_isEditing)
|
||||||
|
{
|
||||||
|
_markdownContent = NoteTextBox.Text ?? string.Empty;
|
||||||
|
}
|
||||||
|
PersistNoteImmediately();
|
||||||
|
}
|
||||||
|
|
||||||
|
_autoSaveTimer.Stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnThemeVariantChanged(object? sender, EventArgs e)
|
||||||
|
{
|
||||||
|
Dispatcher.UIThread.Post(ApplyNoteColors);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ForceSave()
|
||||||
|
{
|
||||||
|
if (_isEditing)
|
||||||
|
{
|
||||||
|
_markdownContent = NoteTextBox.Text ?? string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_isDirty || _isEditing)
|
||||||
|
{
|
||||||
|
PersistNoteImmediately();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (_disposed)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_disposed = true;
|
||||||
|
_autoSaveTimer.Stop();
|
||||||
|
_renderDebounceCts?.Cancel();
|
||||||
|
_renderDebounceCts?.Dispose();
|
||||||
|
|
||||||
|
if (_isDirty)
|
||||||
|
{
|
||||||
|
if (_isEditing)
|
||||||
|
{
|
||||||
|
_markdownContent = NoteTextBox.Text ?? string.Empty;
|
||||||
|
}
|
||||||
|
PersistNoteImmediately();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4286,6 +4286,10 @@ public partial class MainWindow
|
|||||||
{
|
{
|
||||||
whiteboard.ForceSaveNote();
|
whiteboard.ForceSaveNote();
|
||||||
}
|
}
|
||||||
|
else if (contentHost?.Child is StickyNoteWidget stickyNote)
|
||||||
|
{
|
||||||
|
stickyNote.ForceSave();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user