From 4a89c2388bcc7722907642daece63c3d24080794 Mon Sep 17 00:00:00 2001 From: lincube Date: Sun, 12 Apr 2026 12:14:25 +0800 Subject: [PATCH] =?UTF-8?q?feat.=E4=BE=BF=E7=AD=BE=E7=BB=84=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .trae/skills/refactoring-insight/SKILL.md | 112 ++++++ CHANGELOG.md | 67 +++- .../ComponentSystem/BuiltInComponentIds.cs | 1 + .../ComponentSystem/ComponentRegistry.cs | 10 + .../Models/ComponentSettingsSnapshot.cs | 6 + .../DesktopComponentRuntimeRegistry.cs | 4 + .../Views/Components/StickyNoteWidget.axaml | 51 +++ .../Components/StickyNoteWidget.axaml.cs | 371 ++++++++++++++++++ .../Views/MainWindow.ComponentSystem.cs | 4 + 9 files changed, 613 insertions(+), 13 deletions(-) create mode 100644 .trae/skills/refactoring-insight/SKILL.md create mode 100644 LanMountainDesktop/Views/Components/StickyNoteWidget.axaml create mode 100644 LanMountainDesktop/Views/Components/StickyNoteWidget.axaml.cs diff --git a/.trae/skills/refactoring-insight/SKILL.md b/.trae/skills/refactoring-insight/SKILL.md new file mode 100644 index 0000000..a7110d7 --- /dev/null +++ b/.trae/skills/refactoring-insight/SKILL.md @@ -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(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` pattern) +- Common patterns that could be extracted into base classes: + - `SettingsPageViewModelBase` for shared ViewModel boilerplate + - `JsonFileSettingsService` for repeated settings persistence + - `SettingsDomainServiceBase` 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`. diff --git a/CHANGELOG.md b/CHANGELOG.md index 029742a..ed7b8ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,18 +1,53 @@ # 更新日志 / Changelog - -## [0.8.3.2] - 2026-04-09 +## [0.8.3.3](https://github.com/yourorg/LanMountainDesktop/releases/tag/v0.8.3.3) - 2026-04-12 ### 新增 (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) + - 无 ### 修复 (Fixed) + - 🐛 **应用启动台文件夹应用数量限制**: 修复了应用启动台文件夹无法查看超过12个应用的问题 - 问题原因: 文件夹弹窗未实现滚动功能,应用列表超出显示区域后被截断 - 修复方案: 为文件夹内容区域添加滚动支持,允许用户滚动查看所有应用 @@ -24,13 +59,15 @@ - 修复方案: 在 `IncrementalUpdateItems()` 方法中同步更新课程项的文字颜色 ### 移除 (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) + - ✨ **快捷方式组件**: 新增快捷方式组件,可在阑山桌面内便捷打开系统应用与文件 - 支持创建快捷方式,统一管理应用和文件 - 提供单击打开和双击打开两种交互模式 @@ -38,38 +75,46 @@ - 📝 初始化更新日志文档,为后续版本发布建立基础 ### 变更 (Changed) + - 无 ### 修复 (Fixed) + - 无 ### 移除 (Removed) + - 无 ---- +*** 所有重要的更改都将记录在此文件中。 格式基于 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.0.0/), 并且本项目遵循 [语义化版本](https://semver.org/lang/zh-CN/)。 ---- +*** -## [格式示例] +## \[格式示例] ### 新增 (Added) + - 待发布的新功能 ### 变更 (Changed) + - 待发布的变更 ### 修复 (Fixed) + - 待发布的修复 ### 移除 (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 diff --git a/LanMountainDesktop/ComponentSystem/BuiltInComponentIds.cs b/LanMountainDesktop/ComponentSystem/BuiltInComponentIds.cs index 6050563..b2bab9e 100644 --- a/LanMountainDesktop/ComponentSystem/BuiltInComponentIds.cs +++ b/LanMountainDesktop/ComponentSystem/BuiltInComponentIds.cs @@ -47,4 +47,5 @@ public static class BuiltInComponentIds public const string DesktopFileManager = "DesktopFileManager"; public const string DesktopNotificationBox = "DesktopNotificationBox"; public const string DesktopShortcut = "DesktopShortcut"; + public const string DesktopStickyNote = "DesktopStickyNote"; } diff --git a/LanMountainDesktop/ComponentSystem/ComponentRegistry.cs b/LanMountainDesktop/ComponentSystem/ComponentRegistry.cs index f5708da..9c9a4a6 100644 --- a/LanMountainDesktop/ComponentSystem/ComponentRegistry.cs +++ b/LanMountainDesktop/ComponentSystem/ComponentRegistry.cs @@ -327,6 +327,16 @@ public sealed class ComponentRegistry AllowStatusBarPlacement: false, AllowDesktopPlacement: true, ResizeMode: DesktopComponentResizeMode.Free), + new DesktopComponentDefinition( + BuiltInComponentIds.DesktopStickyNote, + "Sticky Note", + "Notepad", + "Board", + MinWidthCells: 2, + MinHeightCells: 2, + AllowStatusBarPlacement: false, + AllowDesktopPlacement: true, + ResizeMode: DesktopComponentResizeMode.Free), new DesktopComponentDefinition( BuiltInComponentIds.DesktopBrowser, "Browser", diff --git a/LanMountainDesktop/Models/ComponentSettingsSnapshot.cs b/LanMountainDesktop/Models/ComponentSettingsSnapshot.cs index f2b8292..7388e65 100644 --- a/LanMountainDesktop/Models/ComponentSettingsSnapshot.cs +++ b/LanMountainDesktop/Models/ComponentSettingsSnapshot.cs @@ -142,6 +142,12 @@ public sealed class ComponentSettingsSnapshot #endregion + #region Sticky Note Component Settings (便签组件设置) + + public string StickyNoteContent { get; set; } = string.Empty; + + #endregion + public ComponentSettingsSnapshot Clone() { var clone = (ComponentSettingsSnapshot)MemberwiseClone(); diff --git a/LanMountainDesktop/Views/Components/DesktopComponentRuntimeRegistry.cs b/LanMountainDesktop/Views/Components/DesktopComponentRuntimeRegistry.cs index 0c6fd53..1f4ac08 100644 --- a/LanMountainDesktop/Views/Components/DesktopComponentRuntimeRegistry.cs +++ b/LanMountainDesktop/Views/Components/DesktopComponentRuntimeRegistry.cs @@ -452,6 +452,10 @@ public sealed class DesktopComponentRuntimeRegistry BuiltInComponentIds.DesktopBlackboardLandscape, "component.blackboard_landscape", () => new WhiteboardWidget(baseWidthCells: 4)), + new DesktopComponentRuntimeRegistration( + BuiltInComponentIds.DesktopStickyNote, + "component.sticky_note", + () => new StickyNoteWidget()), new DesktopComponentRuntimeRegistration( BuiltInComponentIds.DesktopBrowser, "component.browser", diff --git a/LanMountainDesktop/Views/Components/StickyNoteWidget.axaml b/LanMountainDesktop/Views/Components/StickyNoteWidget.axaml new file mode 100644 index 0000000..158c1c1 --- /dev/null +++ b/LanMountainDesktop/Views/Components/StickyNoteWidget.axaml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + diff --git a/LanMountainDesktop/Views/Components/StickyNoteWidget.axaml.cs b/LanMountainDesktop/Views/Components/StickyNoteWidget.axaml.cs new file mode 100644 index 0000000..1b92997 --- /dev/null +++ b/LanMountainDesktop/Views/Components/StickyNoteWidget.axaml.cs @@ -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(); + _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(); + 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(); + } + } +} diff --git a/LanMountainDesktop/Views/MainWindow.ComponentSystem.cs b/LanMountainDesktop/Views/MainWindow.ComponentSystem.cs index 7c8cd72..eaf949f 100644 --- a/LanMountainDesktop/Views/MainWindow.ComponentSystem.cs +++ b/LanMountainDesktop/Views/MainWindow.ComponentSystem.cs @@ -4286,6 +4286,10 @@ public partial class MainWindow { whiteboard.ForceSaveNote(); } + else if (contentHost?.Child is StickyNoteWidget stickyNote) + { + stickyNote.ForceSave(); + } } } }