Compare commits

...

1400 Commits

Author SHA1 Message Date
CJKmkp 8383002b5c improve:自动更新 2026-05-01 19:11:59 +08:00
CJKmkp 8c37b91b5b improve:自动更新 2026-05-01 18:58:10 +08:00
CJKmkp 5a387eef96 add:更新面板 2026-05-01 18:55:03 +08:00
CJKmkp c27759189d add:快捷键面板 2026-05-01 17:43:29 +08:00
CJKmkp a33a399989 add:快捷键面板 2026-05-01 17:39:27 +08:00
CJKmkp 6c7c76958f add:快捷键面板 2026-05-01 17:36:53 +08:00
CJKmkp 6980abe331 add:浮动栏自定义 2026-05-01 17:20:47 +08:00
CJKmkp 5fc92cdd10 improve:UI 2026-05-01 16:57:21 +08:00
CJKmkp 4bcc39a7ae improve:PPT控件 2026-05-01 16:36:17 +08:00
CJKmkp a044a8bc21 improve:PPT控件 2026-05-01 16:07:40 +08:00
CJKmkp f15a293a69 add:自定义文件名 2026-05-01 14:51:56 +08:00
CJKmkp 448a695503 improve:UI 2026-05-01 14:28:21 +08:00
CJKmkp 3915893ee6 Merge branch 'net6' of https://github.com/InkCanvasForClass/community into net6 2026-05-01 13:08:29 +08:00
CJKmkp 914e6eea2e improve:PPT控件 2026-05-01 13:08:26 +08:00
PrefacedCorg 3434ab6227 Merge branch 'net6' of https://github.com/InkCanvasForClass/community into net6 2026-05-01 12:34:26 +08:00
PrefacedCorg 90ba3f7fa6 refactor: 优化动画逻辑和UI组件结构
重构动画帮助类以支持自定义动画目标
简化颜色滑块更新逻辑
调整浮动工具栏显示逻辑
新增BoardMenuFrame自定义控件
2026-05-01 12:34:16 +08:00
CJKmkp f05bcf6cc8 improve:ROT模块 2026-05-01 10:51:28 +08:00
CJKmkp 21d5ee25ea improve:ROT模块 2026-05-01 10:39:04 +08:00
CJKmkp 13c73fbfe4 优化代码 2026-05-01 09:58:25 +08:00
CJKmkp 97dbedfed5 improve:UI 2026-05-01 09:31:10 +08:00
CJKmkp e5ef1c6472 improve:UI 2026-05-01 09:25:06 +08:00
CJKmkp 17945a9298 improve:OOBE 2026-05-01 09:10:28 +08:00
CJKmkp 17c0ecc0f5 improve:OOBE 2026-05-01 08:39:36 +08:00
CJKmkp 8c2fc15d81 improve:UI 2026-05-01 01:52:36 +08:00
CJKmkp ac399773d0 improve:OOBE 2026-05-01 01:49:56 +08:00
CJKmkp 199674feca improve:UI 2026-05-01 01:35:29 +08:00
CJKmkp cb6af3e21f add!:安全面板 2026-05-01 01:22:35 +08:00
CJKmkp efe0bb6ae2 add:存储管理 2026-05-01 01:00:12 +08:00
CJKmkp 4a1403eed3 Merge branch 'net6' of https://github.com/InkCanvasForClass/community into net6 2026-05-01 00:54:15 +08:00
CJKmkp 77a7628453 Merge branch 'net6' of https://github.com/InkCanvasForClass/community into net6 2026-05-01 00:50:23 +08:00
PrefacedCorg 21186d1edb Update DebugPage.xaml 2026-05-01 00:49:37 +08:00
PrefacedCorg 1cb77b4f48 refactor(设置页面): 移除独立的图标设置页面并合并到调试页面
将图标设置功能从独立的 IconographyPage 迁移到 DebugPage 中,简化页面结构
删除不再使用的 IconographyPage.xaml 和 IconographyPage.xaml.cs 文件
2026-05-01 00:47:47 +08:00
PrefacedCorg a6f92a883c Merge branch 'net6' of https://github.com/InkCanvasForClass/community into net6 2026-05-01 00:41:28 +08:00
PrefacedCorg 2c0b09a9ad feat(UI): 统一调整设置页面滑块控件布局和样式
调整多个设置页面的滑块控件布局,统一使用SimpleStackPanel水平排列滑块和数值显示文本,增加数值显示的Consolas字体样式,并统一滑块宽度为200像素
2026-05-01 00:36:41 +08:00
CJKmkp 003fffca2d improve:ROT模块 2026-05-01 00:33:35 +08:00
CJKmkp 28b719e9db Merge branch 'net6' of https://github.com/InkCanvasForClass/community into net6 2026-05-01 00:27:48 +08:00
CJKmkp 4d8fbef455 add:debug功能 2026-05-01 00:27:29 +08:00
PrefacedCorg e55f8e8ea1 Merge branch 'net6' of https://github.com/InkCanvasForClass/community into net6 2026-05-01 00:25:45 +08:00
PrefacedCorg 8ad987a64b refactor(设置): 将黑板缩放功能从开关改为滑块控制
将黑板缩放功能从布尔开关改为可调节的滑块控制,并统一所有滑块值的精度为两位小数
2026-05-01 00:25:35 +08:00
CJKmkp b0aff1e378 improve:UI 2026-05-01 00:07:18 +08:00
CJKmkp 6239ac0b88 improve:UI 2026-04-30 23:43:13 +08:00
CJKmkp 875fa9c6f0 improve:悬浮窗拦截 2026-04-30 23:33:05 +08:00
CJKmkp a7b020b0ff improve:UIA置顶 2026-04-30 22:59:19 +08:00
CJKmkp b614517728 improve:UIA置顶 2026-04-30 22:51:36 +08:00
CJKmkp 329e9bd933 improve:UIA置顶 2026-04-30 22:49:05 +08:00
CJKmkp 73f27a9423 优化代码 2026-04-30 20:31:05 +08:00
CJKmkp ffee3aa564 improve:设备ID 2026-04-30 20:27:25 +08:00
CJKmkp 9268ebb580 improve:UI 2026-04-30 20:09:51 +08:00
CJKmkp 40fc4e89e0 add:PPT增强预览视图 2026-04-30 19:09:55 +08:00
CJKmkp 6fd4a4f036 add:多屏支持 2026-04-30 18:35:37 +08:00
CJKmkp 483991b85c add:多屏支持 2026-04-30 18:28:39 +08:00
CJKmkp a0d24ea6cc improve:ROT模块 2026-04-30 17:58:28 +08:00
CJKmkp 9613285513 improve:ROT模块 2026-04-30 17:55:56 +08:00
CJKmkp 649a939a57 improve:实时笔锋 2026-04-30 17:51:43 +08:00
CJKmkp f20e360c0b improve:实时笔锋 2026-04-30 17:21:40 +08:00
CJKmkp c1e599971e fix:退出软件实现 2026-04-30 16:55:38 +08:00
CJKmkp 21c93d38c7 fix:issue #463 2026-04-30 16:44:52 +08:00
CJKmkp 4902559cfa improve:启动速度 2026-04-30 15:38:21 +08:00
CJKmkp 49b22dc184 improve:启动计时 2026-04-30 15:26:47 +08:00
CJKmkp f1f75ff015 improve:启动速度 2026-04-30 15:18:35 +08:00
CJKmkp 6e68fa9cfc improve:实时笔锋 2026-04-30 15:14:17 +08:00
CJKmkp c68596b91e improve:启动速度 2026-04-30 15:04:54 +08:00
CJKmkp 7d52573595 improve:启动速度 2026-04-30 14:50:57 +08:00
CJKmkp 6ac34ba8aa improve:启动速度 2026-04-30 14:44:49 +08:00
CJKmkp 9136f1dbe3 improve:启动速度 2026-04-30 14:42:24 +08:00
CJKmkp 98cfdb53c3 improve:UI 2026-04-30 14:38:23 +08:00
CJKmkp e17387c99e fix:版本修复 2026-04-30 14:34:58 +08:00
CJKmkp d30ed9e726 优化代码 2026-04-30 14:29:06 +08:00
CJKmkp 988af60a30 improve:实时笔锋 2026-04-30 14:25:32 +08:00
PrefacedCorg 8b53456b5d refactor(插件设置): 移除静态属性并优化页面导航逻辑
将 PluginSettingsPage 的 CurrentPlugin 从静态属性改为实例属性,避免状态共享问题
重构 SettingsWindow 的导航逻辑,支持直接传递插件信息并避免重复导航
2026-04-30 00:08:09 +08:00
PrefacedCorg d23193527e fix(UI): 调整笔划选择控件的位置
将笔划选择控件的垂直位置从900改为400,使其更符合用户操作习惯
2026-04-29 23:23:58 +08:00
PrefacedCorg b58376847e refactor: 移除按笔画擦除功能及相关UI元素
移除不再需要的按笔画擦除功能代码和对应的XAML界面元素,简化代码结构
2026-04-29 17:18:55 +08:00
PrefacedCorg d0551db70a style: 调整XAML布局格式和可见性属性
统一XAML元素的格式和对齐方式,并添加设计时可见性属性以便于开发调试
2026-04-29 17:15:16 +08:00
PrefacedCorg e416151a56 style: 调整主窗口尺寸为1920x1080标准分辨率 2026-04-29 17:05:49 +08:00
CJK_mkp b02b496b88 Merge pull request #461 from InkCanvasForClass/all-contributors/add-Hao3288
docs: add Hao3288 as a contributor for code
2026-04-28 17:27:27 +08:00
PrefacedCorg e31dbbcedc Update MW_FloatingBarIcons.cs 2026-04-27 18:13:44 +08:00
allcontributors[bot] 09ad1bac86 docs: update .all-contributorsrc 2026-04-27 04:56:04 +00:00
allcontributors[bot] a2ac761f70 docs: update README.md 2026-04-27 04:56:03 +00:00
PrefacedCorg 9e5ad7e2b4 refactor(HomePage): 将操作按钮布局改为网格排列
将操作区域的三个按钮从垂直排列改为水平网格布局,使用Grid控件实现三列等宽排列,提升界面空间利用率和美观性
2026-04-27 10:06:05 +08:00
PrefacedCorg 4f015fb155 refactor(设置): 重构设置窗口为独立窗口并移除旧设置面板
移除旧的内置设置面板及相关代码,将设置功能迁移至独立的设置窗口
优化设置窗口的主题同步功能,调整自动化页面布局
2026-04-27 09:40:24 +08:00
PrefacedCorg b4526473fa feat(设置界面): 重构自动化设置页面布局并优化悬浮窗拦截逻辑
将自动化设置页面中的白板应用分类整理,使用折叠面板分组显示
移除独立的悬浮窗拦截开关,改为根据子选项自动启用拦截功能
调整画板设置菜单项在导航中的位置
2026-04-26 21:01:11 +08:00
PrefacedCorg 3ab9fcc857 refactor(UI): 重构悬浮窗拦截设置界面布局
将悬浮窗拦截设置从LabeledSettingsCard改为SettingsCard和ToggleSwitch组合
优化界面结构,使开关控件更直观
添加Expander控制拦截规则的显示状态
2026-04-26 20:26:24 +08:00
PrefacedCorg 7ef231a4c7 Merge pull request #460 from Hao3288/net6
Use LabeledSettingsCard with i18n and IconSource
2026-04-26 18:59:20 +08:00
RedstonCoder da3a1f7dea Use LabeledSettingsCard with i18n and IconSource
Replace multiple ui:SettingsCard blocks with compact controls:LabeledSettingsCard entries. Each card now uses Header bindings to i18n keys and an IconSource attribute instead of nested HeaderIcon content, while preserving existing Toggled event handlers. This refactors and simplifies the XAML, standardizes icon usage, and enables localization for the floating app interceptor settings.
2026-04-26 18:56:21 +08:00
PrefacedCorg 81001542b8 Merge branch 'net6' of https://github.com/InkCanvasForClass/community into net6 2026-04-26 17:44:03 +08:00
PrefacedCorg aacf5b696c feat(主页): 添加重启、重置和退出应用程序功能
在主页设置页面新增操作区域,包含重启应用程序、重置为推荐设置和退出应用程序三个功能按钮。同时添加了对应的多语言资源字符串。
2026-04-26 17:40:21 +08:00
CJK_mkp 062c0feb65 Merge pull request #457 from InkCanvasForClass/trae/solo-agent-kMx5Zx
fix: 增强主窗口操作的稳定性
2026-04-26 17:17:30 +08:00
CJKmkp 92f97eab78 feat: 检查主窗口可见性时出错
Co-authored-by: traeagent <traeagent@users.noreply.github.com>
2026-04-26 09:05:58 +00:00
CJK_mkp 31120eab09 Merge pull request #456 from InkCanvasForClass/trae/solo-agent-hICuRf
Fix: 增强时光机功能处理笔划更改时的稳定性
2026-04-26 16:45:22 +08:00
CJKmkp 9b7d718f78 feat: WinRT Ink Analysis 性能优化重构方案
Co-authored-by: traeagent <traeagent@users.noreply.github.com>
2026-04-26 08:39:01 +00:00
PrefacedCorg 9132c9eb14 1 2026-04-26 16:38:54 +08:00
PrefacedCorg adebf8ec38 Merge branch 'net6' of https://github.com/InkCanvasForClass/community into net6 2026-04-26 12:25:43 +08:00
CJK_mkp 854e14f9b0 Merge pull request #455 from InkCanvasForClass/trae/solo-agent-hICuRf
refactor: 优化墨迹分析功能,引入 ModernInkAnalyzer
2026-04-26 12:23:48 +08:00
CJKmkp d7faac257c feat: WinRT Ink Analysis 性能优化重构方案
Co-authored-by: traeagent <traeagent@users.noreply.github.com>
2026-04-26 04:19:30 +00:00
CJKmkp 1b3c5294d9 feat: WinRT Ink Analysis 性能优化重构方案
Co-authored-by: traeagent <traeagent@users.noreply.github.com>
2026-04-26 04:18:06 +00:00
CJKmkp f7802ba4d0 feat: WinRT Ink Analysis 性能优化重构方案
Co-authored-by: traeagent <traeagent@users.noreply.github.com>
2026-04-26 03:01:17 +00:00
PrefacedCorg 8e84dce73f Merge branch 'net6' of https://github.com/InkCanvasForClass/community into net6 2026-04-26 10:33:02 +08:00
doudou0720 620da0e645 build: 为项目添加 Windows 目标平台支持
在所有相关项目中启用 EnableWindowsTargeting 以支持 交叉编译

Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>
2026-04-26 10:19:16 +08:00
PrefacedCorg c4657ebb86 1 2026-04-26 09:12:26 +08:00
PrefacedCorg 24cbaf69ea refactor(UI): 使用 LabeledSettingsCard 替换 SettingsCard 和 ToggleSwitch 组合
统一设置页面中的开关控件样式,将原有的 SettingsCard 和 ToggleSwitch 组合替换为自定义的 LabeledSettingsCard 控件,简化 XAML 结构并保持功能不变
2026-04-25 20:54:26 +08:00
PrefacedCorg f3ef2f7aec feat(设置): 新增高级设置和随机点名设置页面
refactor: 将高级和随机点名设置从主窗口迁移到独立页面
style: 优化墨迹识别页面的高精度直线拉直开关样式
fix: 修复URI处理中未刷新配置列表的问题
2026-04-25 20:26:56 +08:00
CJKmkp 77dff81217 improve:实时笔锋 2026-04-25 17:27:28 +08:00
CJKmkp 004364c3a9 improve:WinRT墨迹识别 2026-04-25 17:25:55 +08:00
CJKmkp 877d702978 improve:手掌擦 2026-04-25 17:23:48 +08:00
CJKmkp a8bc9f442b improve:快捷键 2026-04-25 17:22:52 +08:00
PrefacedCorg f09f05ab3c Merge branch 'net6' of https://github.com/InkCanvasForClass/community into net6 2026-04-25 17:00:42 +08:00
PrefacedCorg ef5377f85c feat(设置): 新增个性化设置页面并重构主题相关功能
重构主题和语言设置功能,将相关代码从主窗口迁移至新增的个性化设置页面
优化浮动工具栏图标选择逻辑,移除冗余代码
统一设置页面中开关控件的样式和行为
修复设置页面导航项的选择状态问题
2026-04-25 17:00:02 +08:00
CJKmkp 5dbdd7bf12 improve:UI 2026-04-25 16:48:07 +08:00
CJKmkp d73e3eb0ea improve:墨迹纠正 2026-04-25 16:40:39 +08:00
CJKmkp b7d03d694c fix:墨迹纠正 2026-04-25 16:40:39 +08:00
CJKmkp b7d4983af5 improve:墨迹纠正 2026-04-25 16:40:39 +08:00
CJKmkp 919cf8a736 更新版本号 2026-04-25 16:40:39 +08:00
CJKmkp 8a7d2ddf30 add:禁用硬件加速 & improve:UI 2026-04-25 16:40:35 +08:00
PrefacedCorg b1640f44c2 feat(设置): 新增墨迹纠正设置页面并重构设置界面
重构设置界面,将墨迹纠正相关功能从画板设置中分离出来,新增独立的墨迹纠正设置页面。主要变更包括:
- 新增 InkRecognitionPage 用于集中管理墨迹纠正功能
- 调整设置窗口导航结构,将墨迹纠正设为画板设置的子页面
- 优化设置项布局,使用折叠面板组织复杂选项
- 移除主窗口中冗余的崩溃处理和手势设置
- 修复多指手势时橡皮擦状态保存问题
- 新增笔尖模式相关字符串资源
2026-04-25 13:56:16 +08:00
PrefacedCorg 6e61538dec feat(设置界面): 更新首页导航文本和分组标题
修改首页导航项的提示文本为"快速导航",并将设置项分组标题调整为"基本"、"主界面"和"其他",使界面分类更清晰
2026-04-25 11:45:30 +08:00
PrefacedCorg f789a919e1 refactor(设置): 清理并重构设置页面代码
删除未使用的设置页面文件,包括外观、设计、字体、主题等页面
简化主页导航逻辑,移除动态生成导航项代码
更新导航菜单项图标和跳转逻辑
优化页面布局和代码结构
2026-04-25 11:32:50 +08:00
PrefacedCorg b9651240df feat(设置): 添加实验性选项页面并优化图标显示
添加新的实验性选项页面,将高级设置中的实验性功能移至该页面
优化设置页面中图标的显示,支持自定义字体
为外部协议调用和避免全屏助手添加设置项
默认启用避免全屏助手功能
2026-04-24 19:16:48 +08:00
PrefacedCorg 5266368f79 1 2026-04-24 10:24:20 +08:00
PrefacedCorg 42854ff924 style(SettingsViews): 为XAML文件添加设计时属性和修复格式
为多个设置页面添加d:IsExpanded和d:Visibility设计时属性
修复AboutPage.xaml的BOM头问题
统一SettingsExpander的格式
2026-04-24 07:39:31 +08:00
PrefacedCorg 48b0e09278 优化 2026-04-24 07:13:27 +08:00
PrefacedCorg f05062f902 优化 2026-04-23 23:53:52 +08:00
PrefacedCorg 532aa03c56 Revert "delete:视频展台"
This reverts commit 0683779e09.
2026-04-23 22:17:55 +08:00
PrefacedCorg b891cb6fe3 Revert "add:新设置"
This reverts commit b949b1651a.
2026-04-23 22:09:27 +08:00
PrefacedCorg 0683779e09 delete:视频展台
去你的视频展台
2026-04-23 22:07:09 +08:00
PrefacedCorg b949b1651a add:新设置 2026-04-23 06:48:52 +08:00
PrefacedCorg 259ce3dd11 feat(插件): 添加应用重启服务接口及实现
添加 IAppRestartService 接口及其实现 AppRestartService,用于插件系统调用应用重启功能
将原 StartupPage 中的重启逻辑提取到 AppRestartHelper 工具类中
在 App.xaml.cs 中注册 AppRestartService 供插件使用
2026-04-21 02:16:58 +08:00
PrefacedCorg cab703b98e fix(SettingsWindow): 修复导航逻辑中的重复导航问题
添加_isNavigating标志位防止在导航过程中重复触发导航事件
2026-04-21 01:02:22 +08:00
PrefacedCorg a57e0496ee add:i18n 2026-04-19 16:00:20 +08:00
PrefacedCorg df66a49ba5 Update CanvasPage.xaml 2026-04-19 15:17:13 +08:00
PrefacedCorg 245a29f797 refactor:迁移设置 2026-04-19 15:13:13 +08:00
PrefacedCorg 4e8f574fdb Update AboutPage.xaml 2026-04-19 14:50:44 +08:00
PrefacedCorg a5d6135f0c refactor:迁移设置 2026-04-19 14:42:11 +08:00
PrefacedCorg 12c7fb1713 refactor:迁移设置 2026-04-19 08:35:22 +08:00
doudou0720 4dd56a4e5d ci: 将项目剩余工作流从 .NET Framework 迁移至 .NET 6
更新 README.md 中的运行环境要求,修改贡献指南中的目标分支
调整 CI/CD 工作流以适配 .NET 6 构建路径
移除安装程序中对 .NET Framework 的依赖检查

Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>
2026-04-18 22:38:10 +08:00
PrefacedCorg abe5992d21 add:新新设置 2026-04-18 22:32:08 +08:00
PrefacedCorg 8d74b6ee30 优化 2026-04-17 21:10:26 +08:00
PrefacedCorg 8b5797ac66 fix:背景设置菜单无法显示 2026-04-17 13:41:18 +08:00
PrefacedCorg 231b850f74 fix:窗口置顶异常 2026-04-17 13:14:23 +08:00
PrefacedCorg 7003bb8426 优化 2026-04-17 09:26:29 +08:00
PrefacedCorg c4b5be783e 优化 2026-04-17 06:39:27 +08:00
PrefacedCorg c38829f7d8 优化 2026-04-17 06:23:49 +08:00
PrefacedCorg fdf3633d8c 优化 2026-04-17 02:05:20 +08:00
PrefacedCorg 3916476af7 优化 2026-04-17 01:43:18 +08:00
PrefacedCorg edc94547ab 优化 2026-04-17 01:20:06 +08:00
PrefacedCorg 8a8cf9a679 更新 MainWindow.xaml 2026-04-17 00:30:45 +08:00
PrefacedCorg 21f3b99838 fix:修复浮动工具栏批注菜单位置错误
这才是真正的精准定位 都给我学好了
2026-04-17 00:14:08 +08:00
PrefacedCorg 3bfa0b1d85 fix:修复浮动工具栏批注菜单位置错误
被恶心到了🤢
2026-04-17 00:03:41 +08:00
PrefacedCorg de55fd43a0 更新 MainWindow.xaml 2026-04-16 00:27:56 +08:00
PrefacedCorg 6e68e53b1e 更新 BoardToolbarButton.xaml.cs 2026-04-15 14:21:20 +08:00
PrefacedCorg 4b8d89854e 优化 2026-04-15 14:15:05 +08:00
PrefacedCorg 8298f7d5bb 优化 2026-04-15 13:02:16 +08:00
PrefacedCorg 816153a1db 优化( 2026-04-14 13:57:13 +08:00
PrefacedCorg 1e8ddf4754 优化 2026-04-14 12:47:54 +08:00
PrefacedCorg d73e87b980 优化 2026-04-14 12:19:36 +08:00
PrefacedCorg b64b0a3618 add:新控件 2026-04-13 23:53:59 +08:00
PrefacedCorg d52ae90b56 优化 2026-04-13 22:54:13 +08:00
PrefacedCorg 0045f97569 优化 2026-04-13 13:33:08 +08:00
PrefacedCorg 41be1e901d 将自定义控件单开一个项目 2026-04-13 13:01:14 +08:00
PrefacedCorg 0a14d96e10 更新 MW_Icons.cs 2026-04-13 01:44:58 +08:00
PrefacedCorg 57c3fe358b 2 2026-04-13 01:40:39 +08:00
PrefacedCorg c255db6ff3 1 2026-04-13 00:51:17 +08:00
CJK_mkp f6484f759e Merge pull request #443 from InkCanvasForClass/beta
合并net6
2026-04-12 17:23:23 +08:00
PrefacedCorg b16636d06b add:gong-wpf-dragdrop 2026-04-12 15:00:31 +08:00
CJKmkp b50049c822 improve:UI面板弹出 2026-04-12 09:08:33 +08:00
PrefacedCorg 220a316d70 浓缩才是精华(?) 2026-04-11 22:46:38 +08:00
PrefacedCorg dbac80509a 将浮动工具栏使用新控件 2026-04-11 17:18:51 +08:00
PrefacedCorg bb82eb4891 Update MW_AutoTheme.cs 2026-04-11 17:03:20 +08:00
CJK_mkp c1d584e3e7 Merge pull request #438 from InkCanvasForClass/All-Contributers/chore
docs: 更新贡献者配置和排版
2026-04-11 16:44:50 +08:00
CJK_mkp f776dac04d Merge pull request #436 from Tayasui-rainnya/beta
feat: add '包含墨迹' toggle and ink-overlay support for area screenshots
2026-04-11 16:44:34 +08:00
PrefacedCorg b56f99fff9 Update MW_Icons.cs 2026-04-11 15:08:25 +08:00
PrefacedCorg 4151dfaf1f 将浮动工具栏使用新控件 2026-04-11 14:29:44 +08:00
PrefacedCorg 836e0b0fde 将浮动工具栏使用新控件 2026-04-11 13:42:33 +08:00
doudou0720 6e7a0e36f4 docs: 更新贡献者配置和README排版
- 添加JSON schema验证
- 调整每行贡献者显示数量为5
- 更新贡献者贡献类型
- 重构README表格布局为每行5列

Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>
2026-04-11 13:06:26 +08:00
PrefacedCorg a7fe7937a5 将浮动工具栏使用新控件 2026-04-11 12:44:40 +08:00
PrefacedCorg dba59b92ac add:自定义控件
屎山给我吃饱了 把浮动工具栏用了自定义控件 不然重复代码太多了
2026-04-11 12:06:53 +08:00
PrefacedCorg 0d6ffa6de6 Update MainWindow.xaml 2026-04-11 08:22:33 +08:00
PrefacedCorg 9edf5ee36c Update MainWindow.xaml 2026-04-11 06:57:12 +08:00
tayasui rainnya! 585b712c4c Enhance XML comments for ShowScreenshotSelector method
Updated XML documentation for ShowScreenshotSelector method to provide clearer details on parameters and return values.
2026-04-11 06:27:19 +08:00
PrefacedCorg d6f20a365e 优化主窗口设置选项 2026-04-11 00:57:22 +08:00
PrefacedCorg f7befe387c add:自定义控件 2026-04-10 22:59:22 +08:00
PrefacedCorg b1a34b7a64 冗余得没边了 2026-04-10 21:02:17 +08:00
PrefacedCorg d556cf7741 移除冗余字体设置 2026-04-10 20:22:09 +08:00
PrefacedCorg 76a046cca6 重构?
去你妈的屎山(ai说的)
2026-04-10 19:49:22 +08:00
PrefacedCorg d6385aea1c 更新 Ink Canvas.sln 2026-04-10 13:55:57 +08:00
PrefacedCorg 31bf2b5af1 更新 InkCanvasService.cs 2026-04-10 13:05:15 +08:00
PrefacedCorg de613c8a4b 更新 PluginPage.xaml 2026-04-10 12:14:23 +08:00
PrefacedCorg 84b626d344 add:插件
屎山
2026-04-10 01:24:57 +08:00
tayasui rainnya! 2ca7d2a337 Merge pull request #1 from Tayasui-rainnya/codex/add-toggle-for-ink-inclusion-in-screenshot
feat: add '包含墨迹' toggle and ink-overlay support for area screenshots
2026-04-09 13:44:34 +08:00
tayasui rainnya! ea03e8e7c7 fix: keep include-ink available for image-only canvas and tighten bitmap disposal 2026-04-09 13:36:08 +08:00
tayasui rainnya! 778894482f fix: disambiguate WPF Matrix type in ink overlay render path 2026-04-09 13:22:19 +08:00
tayasui rainnya! 1035f7ef92 fix: render ink overlay from strokes to remove capture offset 2026-04-09 13:19:47 +08:00
tayasui rainnya! 6e4f8bc982 fix: correct ink overlay alignment in screenshot capture 2026-04-09 13:16:43 +08:00
tayasui rainnya! 36e47cec43 fix: align ink overlay DPI and ignore include-ink toolbar clicks 2026-04-09 13:07:28 +08:00
tayasui rainnya! 8b2bc352a6 feat: add include-ink toggle for area screenshot selector 2026-04-09 12:54:31 +08:00
PrefacedCorg 256b3c0887 Revert " add:基础插件框架"
This reverts commit 302ef307fe.
2026-04-08 12:44:00 +08:00
PrefacedCorg 77cf91e802 Revert "更新 PluginManager.cs"
This reverts commit ea55eb1738.
2026-04-08 12:43:55 +08:00
PrefacedCorg ea55eb1738 更新 PluginManager.cs 2026-04-08 12:41:56 +08:00
PrefacedCorg 302ef307fe add:基础插件框架 2026-04-08 00:51:28 +08:00
PrefacedCorg 3f3a10de7d 代码清理 2026-04-08 00:00:32 +08:00
PrefacedCorg c73123cd23 fix:触摸滑动(左右两边) 2026-04-07 23:58:17 +08:00
PrefacedCorg 73935e0f22 fix:开关显示英文 2026-04-07 23:46:26 +08:00
PrefacedCorg 0dfe835eef fix:触摸滑动 2026-04-07 23:32:20 +08:00
PrefacedCorg 1ec07421b5 Revert "啊"
This reverts commit 441e600b5d.
2026-04-07 00:33:19 +08:00
PrefacedCorg 441e600b5d 2026-04-06 18:48:31 +08:00
PrefacedCorg bd4b4bd233 Update MainWindow.xaml 2026-04-06 15:02:04 +08:00
CJK_mkp f0343c98b5 合并net6 (#432)
* 文件更名

* improve:UI

快捷键设置UI改进
2026-04-06 09:44:21 +08:00
doudou0720 36ff945384 fix:修复编译警告
Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>
2026-04-05 22:54:55 +08:00
CJKmkp 057fb35d00 improve:UI
快捷键设置UI改进
2026-04-05 22:03:16 +08:00
CJKmkp bcbece5bd6 文件更名 2026-04-05 21:50:53 +08:00
PrefacedCorg 1b01e4a5c5 Merge branch 'beta' into net6 2026-04-05 21:42:11 +08:00
PrefacedCorg 5aad206e0d Merge branch 'beta' of https://github.com/InkCanvasForClass/community into beta 2026-04-05 21:37:05 +08:00
CJKmkp 74344c4782 improve:issue #423 2026-04-05 21:35:43 +08:00
PrefacedCorg f280358f56 删除新的旧插件 2026-04-05 21:35:40 +08:00
CJKmkp 243201502b add:issue #402 2026-04-05 21:32:01 +08:00
CJKmkp 8c090218d1 improve:新设置 2026-04-05 21:19:19 +08:00
CJKmkp 6ca1c598d4 improve:UI 2026-04-05 21:08:38 +08:00
CJK_mkp 5eff424b2d 同步net6分支 (#430)
* improve:pdf插入

* fix:手掌擦 (#419)
2026-04-05 20:49:41 +08:00
CJK_mkp 0c078ef863 fix:手掌擦 (#419) 2026-04-05 20:43:40 +08:00
CJKmkp fea6576dfb improve:pdf插入 2026-04-05 20:37:48 +08:00
CJKmkp 998d829783 解决nuget问题 2026-04-05 20:26:07 +08:00
CJKmkp 226a6942dc improve:pdf插入 2026-04-05 20:21:59 +08:00
PrefacedCorg 52a3d184c7 Merge pull request #427 from InkCanvasForClass/bata-test
主线合并
2026-04-05 20:18:35 +08:00
PrefacedCorg 44cdab063c Merge branch 'net6' into bata-test 2026-04-05 20:14:45 +08:00
PrefacedCorg 24bc2cf138 Merge branch 'beta' of https://github.com/InkCanvasForClass/community into beta 2026-04-05 19:36:35 +08:00
PrefacedCorg e6b707ea51 Merge branch 'New-New-Settings' into beta 2026-04-05 19:35:17 +08:00
CJKmkp 6183011952 improve:UI 2026-04-05 19:23:00 +08:00
CJKmkp d80a59556e Revert "improve:pdf插入"
This reverts commit bad05f77b5.
2026-04-05 19:07:31 +08:00
CJKmkp ebbe018bae 代码优化 2026-04-05 18:52:19 +08:00
CJKmkp bad05f77b5 improve:pdf插入 2026-04-05 18:42:06 +08:00
CJKmkp ea375a9ce6 add:pdf插入 2026-04-05 18:36:47 +08:00
CJKmkp d3382d7856 add:pdf插入 2026-04-05 18:30:05 +08:00
CJKmkp acf0c17d7a add:pdf插入 2026-04-05 18:22:37 +08:00
CJKmkp 83568c8b33 improve:UI 2026-04-05 18:18:18 +08:00
CJKmkp cc80529498 add:pdf插入 2026-04-05 18:15:23 +08:00
PrefacedCorg 002970921d Update MainWindow.xaml 2026-04-05 18:08:48 +08:00
CJKmkp ea74592e89 add:pdf插入 2026-04-05 18:06:21 +08:00
PrefacedCorg fa38d3d664 remove:旧的新设置 2026-04-05 17:58:57 +08:00
CJKmkp cffedb8cb7 add:pdf插入 2026-04-05 17:49:05 +08:00
CJKmkp c77beb662e add:pdf插入 2026-04-05 17:31:35 +08:00
PrefacedCorg 77dd83c2d6 新设置
删除插件管理
2026-04-05 17:31:15 +08:00
PrefacedCorg ceb99cea2b 重命名和补回i18n 2026-04-05 17:27:00 +08:00
PrefacedCorg 04a8224484 Merge branch 'beta' into New-New-Settings 2026-04-05 17:08:03 +08:00
PrefacedCorg 2f54e9d7b3 add:复制按钮 2026-04-05 17:01:20 +08:00
CJKmkp 1165e5bbf2 add:临时窗口显示 2026-04-05 15:49:06 +08:00
doudou0720 24c6ca60a3 refactor: 替换任务栏图标库为 H.NotifyIcon (#425)
更新所有相关文件和引用,从 Hardcodet.Wpf.TaskbarNotification 迁移到 H.NotifyIcon
调整通知显示方式并添加 ForceCreate 调用确保图标正确显示

Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>
2026-04-05 15:04:37 +08:00
CJKmkp 8f3b65c6d4 improve:仅PPT模式 2026-04-05 14:58:32 +08:00
CJKmkp 4835e3db50 优化代码 2026-04-05 14:18:33 +08:00
CJKmkp 7a30c93ff5 delete:插件系统 2026-04-05 14:12:35 +08:00
CJKmkp 1fca17d557 add:插件系统 2026-04-05 14:06:49 +08:00
doudou0720 67a70fd6e4 chore: 更改工作流触发条件
Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>
2026-04-05 12:55:35 +08:00
doudou0720 2e8660de26 ci: 更新工作流以支持 .NET 6 和 Windows 10 19041
更新 GitHub Actions 工作流以构建针对 .NET 6 和 Windows 10 19041 平台的应用程序,并修改了程序集信息以声明支持的 OS 平台

Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>
2026-04-05 12:54:55 +08:00
CJKmkp 069a478559 improve:手写体识别 2026-04-05 12:17:02 +08:00
CJKmkp c70d8e1c4e improve:手写体识别 2026-04-05 11:35:14 +08:00
CJKmkp 379d514bb5 update:net6 2026-04-05 11:15:11 +08:00
CJKmkp 25dc6a00d3 Revert "feat:Downgrade to .NET Framework 4.6.2 project (#415)"
This reverts commit eb2f65e6e5.
2026-04-05 10:52:45 +08:00
doudou0720 eb2f65e6e5 feat:Downgrade to .NET Framework 4.6.2 project (#415)
* chore:Init net 462

* feat: 将 .NET Framework 依赖从 4.7.2 降级至 4.6.2

更新应用配置、安装程序和文档以支持 .NET Framework 4.6.2

Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>

---------

Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>
2026-04-05 10:47:00 +08:00
PANDAJSR 16c86cd02d feat:为托盘右键菜单添加设置入口 (#405)
* 软件设置内的贡献者列表添加了 PANDA-JSR

* feat(tray): add separate entries for new/legacy settings

* feat(tray): merge settings menu entries

* fix: use ikw namespace for tray menu stack panel
2026-04-05 09:53:14 +08:00
CJKmkp f5a657d5c3 improve:展台
新增将展台替换为希沃展台快捷启动功能
2026-04-05 09:38:16 +08:00
CJKmkp dfc23b4428 fix:一言API设置重复写入 2026-04-05 09:25:53 +08:00
CJKmkp b62055e705 Revert "fix:一言API设置重复写入"
This reverts commit 3cf1ea438b.
2026-04-05 09:12:31 +08:00
CJKmkp 3190211f9a fix:一言API设置重复写入 2026-04-05 09:06:20 +08:00
CJKmkp e138f600a1 improve:自动更新 2026-04-05 07:56:47 +08:00
CJKmkp 2173b82fb4 Merge branch 'beta' of https://github.com/InkCanvasForClass/community into beta 2026-04-05 07:42:27 +08:00
CJKmkp ceeb9bffba 更新版本号 2026-04-05 07:42:26 +08:00
doudou0720 ba70d67c89 ci:尝试修复softprops/action-gh-release@v2令牌疑似使用Action Token而非Octo-sts Token的未说明问题
神秘Action

Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>
2026-04-05 00:34:56 +08:00
CJKmkp 74341cc162 优化注释 2026-04-04 23:34:56 +08:00
CJKmkp fc4a3a1194 improve:UI 2026-04-04 23:34:26 +08:00
CJKmkp c33ac03255 improve:墨迹渲染 2026-04-04 23:06:16 +08:00
CJKmkp 66afe271c5 improve:实时笔锋 2026-04-04 22:56:34 +08:00
PrefacedCorg 9279783fc3 Revert "新设置"
This reverts commit 140e92eeda.
2026-04-04 22:26:32 +08:00
CJKmkp 3cf1ea438b fix:一言API设置重复写入 2026-04-04 22:21:34 +08:00
CJKmkp 277e46030d improve:WinRT墨迹识别及笔锋 2026-04-04 22:16:37 +08:00
CJKmkp af19ffb736 fix:一言API设置重复写入 2026-04-04 22:16:32 +08:00
CJKmkp 1f00962c47 Merge branch 'beta' of https://github.com/InkCanvasForClass/community into beta 2026-04-04 21:08:56 +08:00
PrefacedCorg 742a62b6ff Revert "新设置"
This reverts commit 140e92eeda.
2026-04-04 19:43:06 +08:00
PrefacedCorg 140e92eeda 新设置 2026-04-04 18:45:59 +08:00
PrefacedCorg 34c2dab82a i1145141919810n 2026-04-04 16:37:08 +08:00
PrefacedCorg fa2366c373 i1145141919810n 2026-04-04 15:40:36 +08:00
PrefacedCorg e2d898df14 Update SettingsWindow2.xaml 2026-04-04 12:45:47 +08:00
PrefacedCorg cacc67b11d 新设置 2026-04-04 11:33:47 +08:00
doudou0720 aef860a8cc ci: 修复发布工作流中的变量引用格式,避免changelog带有双引号而被Shell解析
神秘问题

Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>
2026-04-04 11:22:54 +08:00
PrefacedCorg c890f95092 i18n
添加注释
2026-04-04 07:50:30 +08:00
PrefacedCorg 7c9a5f8265 Merge branch 'beta' into New-New-Settings 2026-04-04 00:56:43 +08:00
PrefacedCorg 7eafcb02cb 新设置 2026-04-04 00:54:22 +08:00
PrefacedCorg d4517d3c53 Update SettingsWindow2.xaml.cs 2026-04-04 00:54:04 +08:00
PrefacedCorg 62a9a097aa i18n 2026-04-04 00:00:52 +08:00
PrefacedCorg e7fa1caf6c Update SettingsWindow2.xaml 2026-04-03 22:11:57 +08:00
PrefacedCorg e347443a0a Update SettingsWindow2.xaml 2026-04-03 19:46:30 +08:00
PrefacedCorg b77e540863 Reapply "更新 SettingsWindow2.xaml.cs"
This reverts commit a6fdb07e8b.
2026-04-03 19:30:44 +08:00
PrefacedCorg df4168e8fa Reapply "更新 SettingsWindow2.xaml"
This reverts commit 11cb1815ad.
2026-04-03 19:30:40 +08:00
PrefacedCorg 576f86ce48 Reapply "add:高dpi适配 插件接口 设置窗口多显示器优化 fix:触摸后鼠标指针显示异常"
This reverts commit 26f79da2f9.
2026-04-03 19:30:29 +08:00
PrefacedCorg c3335c0c24 Update SettingsWindow.xaml.cs 2026-04-03 19:30:06 +08:00
PrefacedCorg 26f79da2f9 Revert "add:高dpi适配 插件接口 设置窗口多显示器优化 fix:触摸后鼠标指针显示异常"
This reverts commit ca53624149.
2026-04-03 00:14:28 +08:00
PrefacedCorg 11cb1815ad Revert "更新 SettingsWindow2.xaml"
This reverts commit 5f9b01ef04.
2026-04-03 00:14:23 +08:00
PrefacedCorg a6fdb07e8b Revert "更新 SettingsWindow2.xaml.cs"
This reverts commit ffdb3cd431.
2026-04-03 00:14:10 +08:00
PrefacedCorg ffdb3cd431 更新 SettingsWindow2.xaml.cs 2026-04-02 00:17:25 +08:00
PrefacedCorg 5f9b01ef04 更新 SettingsWindow2.xaml 2026-04-02 00:01:02 +08:00
PrefacedCorg ca53624149 add:高dpi适配 插件接口 设置窗口多显示器优化 fix:触摸后鼠标指针显示异常 2026-04-01 23:57:50 +08:00
PrefacedCorg 6484450ad3 更新 SettingsWindow2.xaml.cs 2026-04-01 13:56:46 +08:00
PrefacedCorg 7c97b683ea 新新设置的第一个开关设置选项 开机时启动 2026-04-01 00:46:40 +08:00
PrefacedCorg 378b3e73f2 1 2026-04-01 00:16:36 +08:00
PrefacedCorg a2f21357b6 我服了 2026-03-31 00:25:08 +08:00
doudou0720 79e03dc0d5 fix(ci):尝试使用通用指令获取文件大小
Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>
2026-03-29 22:31:16 +08:00
PrefacedCorg e0f35450e1 add:NewNewSettings 2026-03-29 17:39:52 +08:00
PrefacedCorg cc9f58fb6a add:NewNewSettings 2026-03-29 14:21:27 +08:00
PrefacedCorg e8be85141b add:NewNewSettings 2026-03-29 13:47:19 +08:00
PrefacedCorg 1bf57ea2f5 1 2026-03-29 12:38:46 +08:00
CJKmkp 18b737b22b add:手写体识别 2026-03-29 12:24:13 +08:00
CJKmkp 4ec3332808 fix:自动收纳 2026-03-29 12:23:26 +08:00
doudou0720 b2290ecf65 feat(ci):支持多架构编译
Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>
2026-03-28 23:56:31 +08:00
CJKmkp 55c336daab 更新项目文件 2026-03-28 23:19:14 +08:00
CJKmkp f2ed3f619c 更新项目文件 2026-03-28 22:32:35 +08:00
CJKmkp 114f400bd9 更新项目文件 2026-03-28 22:29:38 +08:00
CJKmkp 506d1118b2 更新项目文件 2026-03-28 22:25:50 +08:00
PrefacedCorg 20441543f0 Merge pull request #420 from InkCanvasForClass/beta
...
2026-03-28 22:05:28 +08:00
doudou0720 2dbc47ac3c chore:调整构建配置
Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>
2026-03-28 21:53:10 +08:00
doudou0720 bbe99649cd chore:更新Nightly下载链接
Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>
2026-03-28 21:41:11 +08:00
doudou0720 04806d2004 refactor(ci): 重构构建配置和CI工作流
统一项目构建输出路径结构,移除冗余的x86 Debug配置
简化CI工作流中的artifact上传逻辑,支持多架构构建
更新PR检查工作流名称以更准确反映其用途

Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>
2026-03-28 21:35:24 +08:00
doudou0720 759a635026 chore:修复工作流语法错误
Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>
2026-03-28 21:04:20 +08:00
doudou0720 e68bd9286f feat(ci): 分离PR检查工作流并添加多架构构建支持
将PR检查从主工作流中分离为独立文件,并添加对x86架构的构建支持

Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>
2026-03-28 21:00:38 +08:00
CJKmkp a6b31e82c0 优化代码 2026-03-28 20:29:12 +08:00
CJKmkp bc3e37e541 improve:自动更新 2026-03-28 20:28:42 +08:00
CJKmkp 1124bb6bfa improve:自动更新 2026-03-28 20:20:44 +08:00
CJKmkp b19c19d73c 优化日志 2026-03-28 19:21:47 +08:00
CJKmkp feb3fad4da add:WinRT墨迹识别 2026-03-28 19:05:54 +08:00
CJKmkp 3db745d684 add:WinRT墨迹识别 2026-03-28 18:45:42 +08:00
CJKmkp b4089ae62e add:WinRT墨迹识别 2026-03-28 18:43:29 +08:00
CJKmkp 97bdf78b08 add:WinRT墨迹识别 2026-03-28 18:40:18 +08:00
CJKmkp dc9fb26260 add:WinRT墨迹识别 2026-03-28 18:30:40 +08:00
CJKmkp 97b0972fdf add:WinRT墨迹识别 2026-03-28 18:10:28 +08:00
CJKmkp ea23145349 add:WinRT墨迹识别 2026-03-28 18:01:14 +08:00
CJKmkp f7013196f7 delete:墨迹预测 2026-03-28 17:52:30 +08:00
CJKmkp 9d0baa0799 add:WinRT墨迹识别 2026-03-28 17:40:14 +08:00
CJK_mkp 91c2fa4eee Revert "feat: 选区截图时保留屏幕笔迹 (#406)" (#418)
This reverts commit f7aa107a62.
2026-03-28 17:23:27 +08:00
CJKmkp 837311b2b7 add:实时笔锋及墨迹预测 2026-03-28 17:16:47 +08:00
CJKmkp bc53eab669 Merge branch 'beta' of https://github.com/InkCanvasForClass/community into beta 2026-03-28 17:12:23 +08:00
CJKmkp 55eb811193 add:实时笔锋及墨迹预测 2026-03-28 17:11:30 +08:00
tayasui rainnya! f7aa107a62 feat: 选区截图时保留屏幕笔迹 (#406)
* feat: 使用选区截图时,不清除 Strokes(Keep it on screen)

* fix: 浮动栏选区截图前强制保持墨迹可见

* fix: 避免选区截图回滚 inkCanvas 运行时状态

* fix: 截图前退出并在结束后恢复批注状态

* fix: 截图流程改用轻量批注暂停避免副作用

* feat: 选区截图添加包含墨迹开关

* fix: 避免选区截图墨迹重复渲染

* fix: 全屏基础截图排除主窗口后再叠加墨迹

* fix: 隐藏浮动栏后再进入选区截图

* fix: 添加到白板时不强制恢复浮动栏可见性

* fix: 防止重复启动选区截图实例

* fix: 仅在白板接管成功后跳过浮动栏恢复

* feat: 选区截图时实时预览包含墨迹开关

* fix: 合并截图选择器OnClosed逻辑避免重复定义
2026-03-28 17:11:18 +08:00
CJKmkp 56a65af9a7 add:实时笔锋及墨迹预测 2026-03-28 17:06:16 +08:00
CJKmkp de3f5d16a2 add:实时笔锋及墨迹预测 2026-03-28 17:04:50 +08:00
CJKmkp d325a58f17 add:实时笔锋及墨迹预测 2026-03-28 16:59:02 +08:00
CJKmkp fd137ae787 improve:安全面板 2026-03-28 16:46:35 +08:00
CJKmkp bb1b893961 improve:自动收纳 2026-03-28 16:46:21 +08:00
CJKmkp 01c247ac29 add:unget for 现代化墨迹识别 2026-03-27 17:45:21 +08:00
CJKmkp 1fa75640ee 更新nuget 2026-03-27 17:36:10 +08:00
CJKmkp 6e0191af6b 更新nuget 2026-03-27 17:30:01 +08:00
PrefacedCorg 9ac4070e4e fix:窗口标题栏最小化隐藏 oobe 上下一步无法触摸 2026-03-24 01:21:57 +08:00
CJK_mkp 2590ea5bdb Fix PPT disconnect null check crash (#413) 2026-03-23 16:46:28 +08:00
CJK_mkp 290e031f77 Handle RPC failures during PPT disconnect (#412) 2026-03-23 16:41:50 +08:00
PrefacedCorg 9bc9af5eec 改了下标题栏
不会约定式提交
2026-03-22 14:35:19 +08:00
PrefacedCorg 8faffe9d4e Revert "新设置右侧下部添加触摸支持"
This reverts commit c9dd98fa8d.
2026-03-21 23:09:45 +08:00
PrefacedCorg 95dfad64ce 空值检测?
去你的空值
炸了不关我事
2026-03-21 23:06:16 +08:00
PrefacedCorg c9dd98fa8d 新设置右侧下部添加触摸支持
右侧顶部依旧无法触摸 不会修 等重写标题栏
2026-03-21 23:04:52 +08:00
doudou0720 3ae99c82cc fix(SymbolIcon):修复SymbolIcon问题并修改打包过程以正确构建单文件程序 (#407)
* refactor(Icon): 将SymbolIcon替换为FontIcon以使用Segoe Fluent Icons

Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>

* chore: 移除 Costura.Fody 的 IncludeAssets 配置

Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>

---------

Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>
2026-03-21 22:15:36 +08:00
doudou0720 016abafee4 fix(UI): 为新设置、云储存管理添加基本触控支持 (#409)
实测部分按钮(如新设置的云储存按钮)仍然有问题

统一设置所有ScrollViewer控件的PanningMode为VerticalOnly,防止水平滚动干扰用户体验

Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>
2026-03-21 17:46:19 +08:00
doudou0720 dca606cf5f chore(deps):Bump microsoft/setup-msbuild v2 to v3
Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>
2026-03-21 17:43:33 +08:00
CJKmkp e7c77b173d improve:PPT模块 2026-03-21 16:57:14 +08:00
CJKmkp 68b588ee7b improve:外部点名 2026-03-21 16:49:37 +08:00
CJKmkp b4c753dfb4 improve:PPT模块 2026-03-21 16:41:39 +08:00
CJKmkp 037bcf6007 Merge branch 'beta' of https://github.com/InkCanvasForClass/community into beta 2026-03-21 16:40:32 +08:00
CJKmkp b2e0e7b9e2 improve:安全面板 2026-03-21 16:30:55 +08:00
PrefacedCorg 03c1399768 看得不够长 调长点 2026-03-21 16:29:19 +08:00
PrefacedCorg 9b3a59f089 改漏了再改一下 2026-03-21 16:28:27 +08:00
PrefacedCorg e8f824a046 Merge branch 'beta' of https://github.com/InkCanvasForClass/community into beta 2026-03-21 16:19:38 +08:00
PrefacedCorg 4772b9cce7 改了两ui 适配了触摸 2026-03-21 16:18:36 +08:00
CJKmkp 74b64bd21f 代码优化 2026-03-21 16:13:42 +08:00
doudou0720 3934270ed2 refactor: 移除未使用的VBIDE COM引用
Github Action都没有这个lib怎么构建.jpg

Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>
2026-03-14 21:20:40 +08:00
CJKmkp 664ec925e9 add:i18n 2026-03-14 17:58:01 +08:00
CJKmkp 3806454b18 improve:遥测 2026-03-14 17:35:55 +08:00
PANDAJSR bea92f3483 新设置窗口:新增可拖动区域 (#401)
* 软件设置内的贡献者列表添加了 PANDA-JSR

* 设置窗口:新增可拖动区域
2026-03-14 17:24:54 +08:00
CJKmkp 133542d7fa Merge branch 'beta' of https://github.com/InkCanvasForClass/community into beta 2026-03-14 17:19:38 +08:00
doudou0720 7a363f7f79 feat:支持交叉编译并完成iNKORE.UI.WPF.Modern升级 (#398)
* fix(deps):仅更新SimpleStackPanel

* feat:支持交叉编译并完成iNKORE.UI升级

* chore:fix com

* fix(Build/logic):在非.Net Framework MSBuild下使用预生成的互操作dll而非依据系统判断

Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>

* feat:添加devcontainer.json

* chore(devcontainer.json):精简Dev Container

---------

Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>
2026-03-14 17:19:05 +08:00
doudou0720 2322efcc00 chore(Workflow):对prerelease添加警告
Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>
2026-03-14 17:10:54 +08:00
CJKmkp 3cc830b37b improve:展台 2026-03-14 17:08:26 +08:00
CJKmkp 7a1a9be3ab improve:墨迹渐隐 2026-03-14 16:51:26 +08:00
CJKmkp 082c9ed005 improve:计时器 2026-03-14 16:45:19 +08:00
CJKmkp dfd327d7fc improve:PPT模块 2026-03-14 16:19:12 +08:00
CJKmkp 3354850216 fix:心跳机制 2026-03-14 16:04:01 +08:00
CJK_mkp d330d0bf26 回滚134c905
Removed unnecessary comments related to heartbeat reset during startup.
2026-03-09 19:55:17 +08:00
CJK_mkp 13b9b597ce Fix heartbeat false positive right after startup (#400) 2026-03-09 17:08:43 +08:00
doudou0720 96491f3bf5 chore(deps):更新工作流action版本
Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>
2026-03-07 20:36:19 +08:00
CJK_mkp 4f99f11a33 撤销至c760211 2026-03-07 12:15:14 +08:00
CJK_mkp 134c90508d 撤销至4c874f 2026-03-07 12:13:29 +08:00
doudou0720 863829bb41 feat(ISSUE_TEMPLATE):为议题模板(.yml)添加上传文件功能
由于.md模板可以直接上传,故未添加

Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>
2026-03-07 11:21:53 +08:00
CJK_mkp 9802ab92e0 fix: stabilize PPT slideshow state sync on transient COM states (#394) 2026-03-05 11:55:55 +08:00
CJK_mkp b7eebeecb4 refactor: 移除启动完成后的额外保护窗口逻辑 (#393) 2026-03-05 11:47:24 +08:00
CJK_mkp a406778d35 Revert "improve:PPT模块" (#390)
This reverts commit 7d0de8b5a3.
2026-03-05 11:30:07 +08:00
CJKmkp c9cef8ca86 add:i18n 2026-03-04 14:06:21 +08:00
CJKmkp c099870352 add:i18n 2026-03-04 11:48:43 +08:00
CJKmkp ad921dcfe7 add:i18n 2026-03-04 11:42:22 +08:00
CJKmkp 0bdf8ea81b add:i18n 2026-03-04 11:17:05 +08:00
CJKmkp 079c3cf1a6 add:i18n 2026-03-04 10:50:59 +08:00
CJKmkp e29b4f3ff3 add:i18n 2026-03-04 10:25:25 +08:00
CJKmkp b67476ae19 add:i18n 2026-03-03 17:22:48 +08:00
CJKmkp 2e4b841e7e 更新版本号 2026-03-03 17:22:33 +08:00
CJKmkp 1dd49c6d93 fix:issue #387 2026-03-03 16:07:58 +08:00
CJKmkp a948c0d7fb 优化代码 2026-03-03 16:04:20 +08:00
CJKmkp 62e79ff5b3 add:i18n 2026-03-03 15:58:26 +08:00
CJKmkp 6300a06a44 add:i18n 2026-03-03 15:18:31 +08:00
CJKmkp a80c4e709f add:i18n 2026-03-03 14:39:47 +08:00
CJKmkp 08f7afc3f1 add:i18n 2026-03-03 14:33:41 +08:00
CJKmkp 1e65cee4ad add:i18n 2026-03-03 14:08:01 +08:00
CJKmkp bbe262df7e improve:崩溃重启 2026-03-03 12:20:26 +08:00
CJKmkp 7d0de8b5a3 improve:PPT模块 2026-03-03 12:13:49 +08:00
doudou0720 244da46ac2 fix(iss):修复Inno Setup文件格式错误
Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>
2026-02-24 18:04:35 +08:00
doudou0720 bd6a4bf298 feat(Inno Setup) 添加.NET Framework 4.7.2安装检查
在安装脚本中添加初始化检查,确保系统已安装.NET Framework 4.7.2或兼容版本。如果未安装,则显示错误提示信息。

Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>
2026-02-24 17:54:47 +08:00
CJK_mkp ef6f3adc3f Update README.md 2026-02-24 16:49:53 +08:00
allcontributors[bot] 2047fb3a3a docs: add Super-Yyt as a contributor for blog (#384)
* docs: update README.md

* docs: update .all-contributorsrc

* docs: update README.md

* docs: update .all-contributorsrc

---------

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
Co-authored-by: CJK_mkp <113243675+CJKmkp@users.noreply.github.com>
2026-02-24 16:48:16 +08:00
allcontributors[bot] 57594d7e4d docs: add doudou0720 as a contributor for blog (#385)
* docs: update README.md

* docs: update .all-contributorsrc

---------

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2026-02-24 16:46:25 +08:00
allcontributors[bot] ba60aa6eb0 docs: add Super-Yyt as a contributor for infra (#383)
* docs: update README.md

* docs: update .all-contributorsrc

---------

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2026-02-24 16:34:34 +08:00
allcontributors[bot] 11c7c0f83f docs: add LiuYan-xwx as a contributor for code (#382)
* docs: update README.md

* docs: update .all-contributorsrc

---------

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2026-02-24 16:28:15 +08:00
CJKmkp 1108433b99 文件更名 2026-02-24 15:09:38 +08:00
CJKmkp 3ac4802d44 improve:云存储管理UI 2026-02-24 14:57:31 +08:00
CJKmkp 884d015b88 Merge branch 'beta' of https://github.com/InkCanvasForClass/community into beta 2026-02-24 14:56:09 +08:00
CJKmkp 69ed0fc1ec improve:云存储管理UI 2026-02-24 14:56:04 +08:00
doudou0720 4a9d492571 fix(UI/Dlass): 修改Dlass设置界面文本和样式
Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>
2026-02-24 14:51:53 +08:00
doudou0720 8a9a4c8b0f fix(云储存设置窗口): 优化自动上传Dlass笔记开关状态的同步逻辑
Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>
2026-02-24 14:45:17 +08:00
CJKmkp 2ce6088e74 优化代码 2026-02-24 14:13:19 +08:00
doudou0720 0ad74d9f7f feat(Upload/WebDav):迁移Dlass并添加WebDav管理 (#381)
* feat(Upload/Common): 重构上传功能以添加通用设置管理

- 新增UploadSettings类用于管理上传通用设置
- 重构上传逻辑,将延迟上传功能移至UploadHelper
- 在Dlass设置窗口添加通用设置标签页
- 支持多上传提供者管理及取消操作
- 增强文件上传前的验证和错误处理

Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>

* feat(upload): 添加WebDav文件上传支持

- 新增WebDavUploader工具类实现文件上传功能
- 添加WebDavUploadProvider作为上传提供者
- 在设置界面增加WebDav配置选项
- 添加WebDav.Client NuGet包依赖

Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>

* feat(WebDAV): 实现WebDAV上传队列管理

Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>

* feat(Upload): 重命名Dlass设置项为云存储以支持WebDav保存

Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>

* feat(Dlass):迁移Dlass注册位置

Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>

* refactor(Upload): 优化上传逻辑和界面交互

- 修改Dlass标签页检测逻辑,使用Tag属性替代Header
- 限制WebDav上传队列的批量处理大小
- 移除多处上传延迟逻辑,统一在通用设置中配置
- 更新Dlass设置界面提示文本

Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>

* chore:修改窗口命名

Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>

* refactor(upload): 重构上传队列为统一管理架构

重构上传队列系统,引入BaseUploadQueue基类实现通用队列管理逻辑,创建UploadQueueHelper统一管理所有上传队列。将DlassUploadQueue和WebDavUploadQueue重构为继承自BaseUploadQueue的具体实现,简化代码并提高可维护性。修改MainWindow初始化代码以使用新的统一初始化方法。

Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>

* refactor(Upload): 重构上传队列系统,改进错误处理和资源管理

- 将上传队列改为可释放资源,实现IDisposable接口
- 移除硬编码的文件验证逻辑,改为可重写方法
- 改进API客户端,支持取消操作和更好的资源管理
- 优化队列初始化流程,增加错误处理
- 统一上传提供者的队列注册方式
- 改进日志记录和错误信息

Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>

* Update Settings.cs

* refactor(UpLoad/Queue): 移除冗余的上传成功/失败日志记录

优化WebDav上传逻辑,增加目录创建重试机制

Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>

* refactor(MW_Settings): 重构全选复选框状态更新逻辑

将直接设置全选复选框状态的逻辑拆分为两步,先计算所有分类复选框状态,再更新全选复选框,提高代码可读性

Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>

---------

Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>
Co-authored-by: CJK_mkp <113243675+CJKmkp@users.noreply.github.com>
2026-02-24 14:08:57 +08:00
CJKmkp c76021194a improve:ROT模块 2026-02-24 12:21:46 +08:00
CJKmkp 5c3056aea0 Merge branch 'beta' of https://github.com/InkCanvasForClass/community into beta 2026-02-23 23:13:33 +08:00
doudou0720 7deb5b1aae fix: 延迟显示通知以避免截图包含通知
Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>
2026-02-23 23:08:52 +08:00
CJKmkp a7bd58a6bd improve:OOBE 2026-02-23 19:48:03 +08:00
CJKmkp f690612fae improve:展台 2026-02-23 19:32:39 +08:00
CJKmkp 59d1c4b4ab Merge branch 'beta' of https://github.com/InkCanvasForClass/community into beta 2026-02-23 17:36:28 +08:00
doudou0720 ccd9398a53 feat(build): 更新 DSN 文件的嵌入逻辑
添加条件判断和生成逻辑,当 DLASS_SENTRY_DSN 环境变量存在时,自动生成 telemetry_dsn.generated.txt 文件并嵌入。同时添加清理目标确保构建后清理生成的文件。
2026-02-23 17:35:12 +08:00
CJKmkp 1db9ed8169 Merge branch 'beta' of https://github.com/InkCanvasForClass/community into beta 2026-02-23 17:08:02 +08:00
doudou0720 7fbef51237 fix(ci):为构建流程上传Sentry DSN
Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>
2026-02-23 17:06:17 +08:00
CJKmkp efe3308325 add:i18n 2026-02-23 15:45:49 +08:00
CJKmkp 4c874fa50b add:i18n 2026-02-23 14:31:48 +08:00
CJKmkp 1704ad37d2 add:i18n
i18n准备
2026-02-23 14:14:35 +08:00
CJKmkp a019c27c8b improve:配置切换
支持URL联动
2026-02-23 13:46:13 +08:00
CJKmkp 5119652ca5 improve:展台 2026-02-23 13:40:19 +08:00
CJKmkp 1fb0ea29a3 improve:展台 2026-02-23 13:29:12 +08:00
CJKmkp 1ca9fbd023 improve:展台UI 2026-02-23 13:24:32 +08:00
CJKmkp 9eb70de1bc improve:展台UI 2026-02-23 13:20:52 +08:00
CJKmkp fccfca8890 improve:展台 2026-02-23 13:17:20 +08:00
CJKmkp 06e12e2899 add:配置切换 2026-02-23 12:31:53 +08:00
CJKmkp 559822686c add:配置切换 2026-02-23 12:18:59 +08:00
CJKmkp c46ac64a9c add:配置切换 2026-02-23 12:11:03 +08:00
CJKmkp ea7233bc1b add:配置切换 2026-02-23 12:04:54 +08:00
doudou0720 a6bc7552f4 feat(Upload):解耦Dlass上传并使用UploadHelper接管 (#380)
* feat(Upload):解耦Dlass笔记上传

Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>

* fix(上传): 修复多线程环境下的上传提供者管理问题

添加线程同步锁确保上传提供者列表的线程安全
修改AutoUploadDelayMinutes属性确保最小值为0
优化提供者注册逻辑避免重复注册

Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>

---------

Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>
2026-02-22 23:53:12 +08:00
CJKmkp 7b04f18d4e improve:Dlass服务 2026-02-22 23:39:36 +08:00
CJKmkp 99f0e25acf improve:ROT联动 2026-02-22 22:04:58 +08:00
CJKmkp 7b7ce4320b Merge branch 'beta' of https://github.com/InkCanvasForClass/community into beta 2026-02-22 20:54:09 +08:00
CJKmkp 1023ef87f7 add:issue #317 2026-02-22 20:52:14 +08:00
doudou0720 84365130df fix(Workflows/post-release):尝试修改流程以上传大文件
Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>
2026-02-22 20:24:33 +08:00
CJKmkp 4097e6d20c add:issue #296 2026-02-22 20:03:26 +08:00
CJKmkp 940a8024ac add:PPT侧边栏 2026-02-22 19:58:47 +08:00
CJKmkp 15196b39c3 更新版本号 2026-02-22 19:34:52 +08:00
CJKmkp 81864d4947 improve:点名算法 2026-02-22 19:19:44 +08:00
CJKmkp 73a4a044ee improve:保存截屏 2026-02-22 19:12:59 +08:00
CJKmkp 8155aac25d improve:Dlass设置窗口 2026-02-22 18:47:33 +08:00
CJKmkp 6e8d1f5d9e improve:白板 2026-02-22 18:41:53 +08:00
CJKmkp 30852376dc fix:PPT记忆上次页码 2026-02-22 18:04:56 +08:00
CJKmkp ad986043aa Merge branch 'beta' of https://github.com/InkCanvasForClass/community into beta 2026-02-22 17:55:39 +08:00
CJKmkp 17c020284d improve:PPT模块 2026-02-22 17:52:33 +08:00
doudou0720 1d280a3a35 chore(MW_PPT): 修正XML注释的闭合标签和格式 2026-02-22 17:34:52 +08:00
doudou0720 8394c6d6d6 fix: 修复应用退出时未及时关闭PPT监控的问题
Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>
2026-02-22 16:29:30 +08:00
CJKmkp e2da207965 improve:UI 2026-02-22 14:47:25 +08:00
CJKmkp 6f00a332c1 improve:PPT模块 2026-02-22 14:30:23 +08:00
tayasui rainnya! 1c8bdcf352 fix: 全选后点击添加到白板显示已取消 (#379)
* 调整截图按钮默认行为为选区/白板全屏

* 截图选择器新增添加到白板按钮并支持新建白板页插入

* 修复添加到白板不生效:改为剪贴板复制粘贴插入

* 修复添加到白板截图时序:先截到内存再进白板

* Fix screenshot insert flow honoring add-to-whiteboard option

* Refine screenshot selector freehand confirm and blank double-click full select

* Fix freehand whiteboard insert alpha transparency

* Fix screenshot insert notification text for whiteboard flow

* Adjust screenshot selection: right-click/double-click full-select and keep selector on single click

* Show unmasked freehand selection area like rectangle mode

* Remove freehand cutout mask rendering from screenshot selector

* Restore freehand selected-area unmask behavior in selector

* fix: always restore window visibility after area screenshot

* fix: allow add-to-whiteboard after full-screen selection
2026-02-22 14:14:10 +08:00
CJKmkp 31fe3c858e improve:白板翻页性能 2026-02-22 14:12:26 +08:00
tayasui rainnya! c06be8f502 feat: 浮动栏截图修改为选区截图,添加“添加到白板”按钮 (#377)
* 调整截图按钮默认行为为选区/白板全屏

* 截图选择器新增添加到白板按钮并支持新建白板页插入

* 修复添加到白板不生效:改为剪贴板复制粘贴插入

* 修复添加到白板截图时序:先截到内存再进白板

* Fix screenshot insert flow honoring add-to-whiteboard option

* Refine screenshot selector freehand confirm and blank double-click full select

* Fix freehand whiteboard insert alpha transparency

* Fix screenshot insert notification text for whiteboard flow

* Adjust screenshot selection: right-click/double-click full-select and keep selector on single click

* Show unmasked freehand selection area like rectangle mode

* Remove freehand cutout mask rendering from screenshot selector

* Restore freehand selected-area unmask behavior in selector
2026-02-22 14:04:11 +08:00
CJKmkp 2d51eb73b5 improve:白板翻页性能 2026-02-22 13:57:56 +08:00
CJKmkp 30bc0b03e5 improve:UI 2026-02-22 13:28:35 +08:00
CJKmkp dd9cb1825e improve:白板翻页性能 2026-02-22 13:02:38 +08:00
CJKmkp 6d5c4b4e08 improve:白板翻页性能 2026-02-22 12:55:00 +08:00
CJKmkp 3d0a960337 improve:白板翻页性能 2026-02-22 12:42:47 +08:00
CJKmkp 705f4fd155 improve:白板翻页性能 2026-02-22 12:26:43 +08:00
CJKmkp ac98a76797 add:白板页面删除 2026-02-22 12:23:13 +08:00
CJKmkp 13396f418b improve:白板翻页性能 2026-02-22 12:20:37 +08:00
CJKmkp e70696cd6f improve:直线拉直 2026-02-22 11:42:03 +08:00
CJKmkp d8bbee8c76 add:使用橡皮后自动切回批注 2026-02-22 11:25:30 +08:00
CJKmkp 3e701718d3 add:PPT动画跳过 2026-02-22 10:50:11 +08:00
doudou0720 656863a7d0 feat(docstring):添加部分docstring (#376)
* feat(docstring):添加docstring

Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>

* fix(docstring):修复部分docstring格式错误

Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>

* fix(docstring):修复部分docstring

Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>

* chore(Docstring):MW_* 前14

* chore(Docstring):MW_* part 2

* chore(Docstring):MW_* part 3

* chore:优化缩进

* fix: 修复数学计算中的潜在除零错误和数值稳定性问题

Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>

* chore:删除Rebase时多余的OOBE函数

* chore: 更新代码注释和文档格式

修复XML文档注释中的格式问题,统一使用<c>和<see>标签
更新ConfigHelper类的预留说明,明确未来扩展用途
优化TimerDisplayDate_Elapsed方法的注释,说明UI异步更新机制
合并重复的注释摘要行,提高文档可读性
添加形状识别功能的64位进程限制说明
修正视频呈现器设备选择逻辑的文档说明

* chore(IPPTLinkManager): 更新TryEndSlideShow方法的XML注释格式

* chore: 修正代码注释中的术语和格式问题

更新多个文件中的XML注释,统一使用<see langword="..."/>标记代替<c>...</c>标记
规范术语使用(如"延迟初始化"代替"懒惰初始化")
修正注释中的格式错误和补充说明
调整代码区域的注释对齐格式

---------

Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>
2026-02-22 10:14:12 +08:00
CJKmkp 3e3db27296 add:一言名言种类自定义 2026-02-21 18:47:30 +08:00
CJKmkp 86cdb231d0 improve:OOBE 2026-02-21 17:54:22 +08:00
CJKmkp 5d6c9bb76b 代码优化 2026-02-21 17:49:10 +08:00
CJKmkp d02f1e99c8 代码优化 2026-02-21 17:46:22 +08:00
CJKmkp 26b6de9149 improve:安全功能 2026-02-21 17:44:35 +08:00
CJKmkp 2b3d1c11de improve:PPT模块 2026-02-21 17:44:30 +08:00
CJKmkp 51e3377b38 improve:展台 2026-02-21 17:44:20 +08:00
CJKmkp 99ec2d7609 代码优化 2026-02-21 16:51:34 +08:00
CJKmkp 469dbd1497 improve:PPT模块 2026-02-21 15:29:21 +08:00
CJKmkp b1513ca587 improve:PPT墨迹管理 2026-02-21 15:23:42 +08:00
CJKmkp b1e384e52d improve:PPT墨迹管理 2026-02-21 15:06:46 +08:00
CJKmkp 14cb2e836b improve:PPT墨迹管理 2026-02-20 15:19:27 +08:00
CJKmkp 1c50edd8be improve:PPT墨迹管理 2026-02-20 14:59:41 +08:00
CJKmkp 87570e16ce improve:展台UI 2026-02-20 14:27:23 +08:00
doudou0720 abf4bf0254 fix(MW_AutoTheme): 异步加载资源字典以优化启动性能 (#375)
* fix(MW_AutoTheme): 异步加载资源字典以优化启动性能、

Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>

* fix(MW_AutoTheme): 异步加载dark主题

Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>

---------

Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>
2026-02-20 14:22:47 +08:00
CJKmkp 092fd1c3ee improve:OOBE 2026-02-20 14:20:25 +08:00
CJKmkp c1105df271 improve:展台功能 2026-02-20 14:01:53 +08:00
CJKmkp 8292b2ef25 improve:PPT墨迹管理 2026-02-20 13:23:20 +08:00
CJKmkp d032293a0d improve:PPT墨迹管理 2026-02-20 12:51:20 +08:00
CJKmkp d038beb07a improve:PPT墨迹管理 2026-02-20 12:29:40 +08:00
CJKmkp edac024ad9 improve:展台 2026-02-20 12:06:11 +08:00
CJKmkp dc422a2ccc add:展台 2026-02-20 12:00:22 +08:00
CJKmkp bcb7002b22 add:展台 2026-02-19 19:28:21 +08:00
CJKmkp 1a585d353f add:展台 2026-02-19 18:34:30 +08:00
CJKmkp b16ec37df3 improve:安全面板 2026-02-19 18:28:01 +08:00
CJKmkp d2abadd69b add:展台 2026-02-19 18:24:55 +08:00
CJKmkp 432cc3825e add:展台 2026-02-19 18:18:47 +08:00
CJKmkp 9ef764ffa1 add:展台 2026-02-19 18:12:19 +08:00
doudou0720 c2ca1c9702 fix(Selection/Touch): 修复触摸拖动选择时的坐标计算问题
Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>
2026-02-18 22:37:54 +08:00
CJKmkp 07de74b775 improve:安全面板 2026-02-18 22:22:58 +08:00
CJKmkp a7d0f022dc improve:联动模块 2026-02-18 22:21:44 +08:00
CJKmkp 2e03cb1db1 improve:白板列表 2026-02-18 22:19:15 +08:00
CJKmkp 7f82312b96 improve:墨迹颜色切换 2026-02-18 22:13:55 +08:00
CJKmkp 10ba2db0f1 improve:进程保护 2026-02-18 22:10:38 +08:00
CJKmkp 8c99492518 improve:联动模块 2026-02-18 22:10:14 +08:00
CJKmkp b5b2d97786 improve:进程保护 2026-02-18 22:09:40 +08:00
CJKmkp 3d601189f1 improve:进程保护 2026-02-18 22:04:46 +08:00
CJKmkp 681b674eb2 improve:自动更新 2026-02-18 22:03:30 +08:00
CJKmkp c838b9d077 improve:联动模块 2026-02-18 22:00:44 +08:00
CJKmkp 703e710006 improve:OOBE 2026-02-18 22:00:39 +08:00
CJKmkp 3ab1c59a05 更新nuget 2026-02-18 20:32:00 +08:00
CJKmkp 45dc3cb537 add:安全中心 2026-02-16 20:22:41 +08:00
doudou0720 2f8c368eef fix(Touch):添加控制点触控支持以及移动墨迹问题 (#374)
* feat(MW_SelectionGestures.cs): 为缩放选择框添加触摸事件

Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>

* refactor(MW_SelectionGestures.cs): 清理PreviewTouch

Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>

* fix(MW_SelectionGestures.cs): 单指触摸时阻止缩放处理以允许拖动操作

当检测到单指触摸且存在选中笔迹时,直接返回以允许TouchMove处理拖动操作,避免与缩放手势冲突

Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>

---------

Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>
2026-02-16 17:15:33 +08:00
doudou0720 78302ca426 feat(cliff.toml):更新Changelog格式
Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>
2026-02-16 14:43:14 +08:00
doudou0720 0675a369c3 chore(ci):为日常构建使用预安装SDK
Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>
2026-02-16 12:58:44 +08:00
doudou0720 85f92ca9a5 feat(ci):使用Runner提供的.Net SDK版本以加快构建速度
Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>
2026-02-16 12:55:25 +08:00
doudou0720 f252eb068a feat(ci): 在构建解决方案时禁用分析器
Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>
2026-02-16 12:18:59 +08:00
doudou0720 8c65075823 fix(MW_SelectionGestures.cs): 调整选择框和手柄的扩展偏移量 (#373)
Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>
2026-02-15 08:07:35 +08:00
PANDAJSR 7d78f677b8 chore:软件设置内的贡献者列表添加了 PANDA-JSR (#372) 2026-02-15 08:07:17 +08:00
CJKmkp 41e9e9cc04 fix:issue #348 2026-02-15 02:33:42 +08:00
CJKmkp fefd8424b7 fix:issue #339 2026-02-15 02:05:15 +08:00
CJKmkp 60bdc64730 fix:issue #339 2026-02-15 02:04:23 +08:00
CJKmkp 7b6b2a30e6 fix:issue #358 2026-02-15 01:57:12 +08:00
CJKmkp 9b55eff11d improve:板刷模式 2026-02-15 01:43:46 +08:00
CJKmkp 07a03d3e60 Merge branch 'beta' of https://github.com/InkCanvasForClass/community into beta 2026-02-15 01:36:18 +08:00
doudou0720 cf3063f3e4 fix(StartupTimer):分离启动时间和启动画面时间
Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>
2026-02-14 21:40:01 +08:00
CJKmkp 1d46818eec add:OOBE 2026-02-14 16:09:39 +08:00
CJKmkp ec3a826e01 add:OOBE 2026-02-14 15:59:10 +08:00
CJKmkp 97d5b12732 improve:UI布局 2026-02-14 15:33:21 +08:00
CJKmkp e0cb877162 improve:ROT联动 2026-02-14 15:10:00 +08:00
CJKmkp 698478d826 improve:ROT联动 2026-02-14 15:01:39 +08:00
CJKmkp f722f3516b improve:ROT联动 2026-02-14 14:39:15 +08:00
CJKmkp 27a9f965a9 add:板刷模式 2026-02-14 13:47:44 +08:00
CJKmkp a95effabdd add:板刷模式 2026-02-14 13:12:39 +08:00
CJKmkp 08eb9d892d add:定时恢复墨迹设置 2026-02-14 12:49:39 +08:00
CJKmkp d7530d0c4c add:定时恢复墨迹设置 2026-02-14 12:35:08 +08:00
CJKmkp 967243e705 improve:快捷调色盘 2026-02-14 09:39:03 +08:00
CJKmkp 47eac7e70e add:定时恢复墨迹设置 2026-02-14 09:35:16 +08:00
CJKmkp f35075de9d add:定时恢复墨迹设置 2026-02-14 09:13:24 +08:00
CJKmkp b983cbe094 add:定时恢复墨迹设置 2026-02-14 00:23:05 +08:00
CJKmkp 3ea6da4790 improve:快抽 2026-02-13 18:25:57 +08:00
CJKmkp b658bf4fd0 improve:图片插入 2026-02-13 13:20:50 +08:00
CJKmkp 4aa12638f1 improve:图片插入 2026-02-13 12:35:27 +08:00
CJKmkp 54e9e3b10d Merge branch 'beta' of https://github.com/InkCanvasForClass/community into beta 2026-02-13 12:33:27 +08:00
CJKmkp 12a13bb97b improve:图片插入 2026-02-13 12:32:33 +08:00
doudou0720 8b35926b2d chore(Sentry):移除无效Sentry.Profiling
Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>
2026-02-13 10:33:08 +08:00
CJKmkp 1d71f809cc improve:优化timer 2026-02-13 09:29:24 +08:00
CJKmkp a9fefadec9 优化代码结构 2026-02-13 09:17:47 +08:00
CJKmkp bdf4b7e495 improve:Dlass服务 2026-02-13 09:17:18 +08:00
CJKmkp 12c8384347 Merge branch 'beta' of https://github.com/InkCanvasForClass/community into beta 2026-02-13 01:12:32 +08:00
doudou0720 272782ba4c fix(AutoSync CI):修改AutomaticUpdateVersionControl体积位置为beta
Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>
2026-02-13 01:11:45 +08:00
doudou0720 a75d7876e2 fix(AutoSync CI):为正式版补充额外逻辑
Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>
2026-02-13 00:52:08 +08:00
doudou0720 890f3e890c feat(ci):添加多仓库同步功能以及Draft Release支持
Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>
2026-02-13 00:44:09 +08:00
CJKmkp 1c2e513c34 improve:PPT联动 2026-02-13 00:35:32 +08:00
CJKmkp ed239929f3 improve:Dlass遥测 2026-02-13 00:09:38 +08:00
CJKmkp 2d65224389 improve:PPT联动 2026-02-12 23:19:55 +08:00
CJKmkp 418810d542 improve:PPT联动 2026-02-12 22:53:05 +08:00
CJKmkp 4a2d015b97 improve:PPT联动 2026-02-12 22:39:28 +08:00
CJKmkp 15cb1aa5f2 improve:PPT联动 2026-02-12 22:33:04 +08:00
CJKmkp 7226187ac6 improve:PPT联动 2026-02-12 22:18:05 +08:00
CJKmkp 6de2e49047 Revert "improve:PPT联动"
This reverts commit f0b31c15a6.
2026-02-12 21:57:02 +08:00
CJKmkp f0b31c15a6 improve:PPT联动 2026-02-12 21:53:37 +08:00
CJKmkp 80decf5656 improve:Dlass笔记上传服务 2026-02-12 21:52:28 +08:00
CJKmkp 376790330d improve:自动更新 2026-02-12 17:22:47 +08:00
2,2,3-三甲基戊烷 200317a0f5 Merge pull request #371 from InkCanvasForClass/all-contributors/add-PANDAJSR 2026-02-10 23:25:46 +08:00
allcontributors[bot] 92a405f833 docs: update .all-contributorsrc 2026-02-10 15:25:29 +00:00
allcontributors[bot] 2a3e759890 docs: update README.md 2026-02-10 15:25:28 +00:00
doudou0720 46e0b04dd1 improve(cliff.toml):更新Release模板
Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>
2026-02-07 23:35:29 +08:00
doudou0720 88a8d8fa4b 更新版本号
Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>
2026-02-07 17:22:38 +08:00
CJKmkp bf5c4ab188 improve:遥测服务 2026-02-07 16:42:45 +08:00
CJKmkp 14d314e0a2 add:新设置 2026-02-07 15:29:15 +08:00
CJKmkp 324d514f10 add:新设置 2026-02-07 15:21:42 +08:00
CJKmkp e11e1989ad add:新设置 2026-02-07 15:08:00 +08:00
CJKmkp a5b99c25ed add:新设置 2026-02-07 14:58:35 +08:00
CJKmkp 3c908feb95 add:新设置 2026-02-07 14:53:37 +08:00
CJKmkp 840eca88c8 add:新设置 2026-02-07 14:49:52 +08:00
CJKmkp fa7caf3592 add:新设置 2026-02-07 14:02:26 +08:00
CJKmkp 654e15f845 add:Dlass遥测 2026-02-07 12:43:03 +08:00
CJKmkp 7a81344aea add:Dlass遥测 2026-02-07 12:23:46 +08:00
CJKmkp 803d267687 add:Dlass遥测 2026-02-07 12:16:37 +08:00
CJKmkp c8e4d18364 improve:自动更新 2026-02-07 11:54:40 +08:00
CJKmkp aeecca1260 add:Dlass遥测 2026-02-07 11:37:33 +08:00
CJKmkp 49aaa2d58b add:Dlass遥测 2026-02-07 11:23:59 +08:00
CJKmkp dd22b119b7 add:Dlass遥测 2026-02-07 11:15:39 +08:00
CJKmkp 42984ea5fa 更新文件 2026-02-07 10:56:13 +08:00
CJKmkp ce0147091a improve:PPT联动 2026-02-07 10:44:17 +08:00
CJKmkp 884abf5b0e add:Dlass遥测 2026-02-07 10:43:25 +08:00
CJKmkp 5cc1c7093a add:新设置 2026-02-07 10:37:51 +08:00
CJKmkp 87aae93f4b add:Dlass遥测 2026-02-07 07:39:05 +08:00
CJKmkp 77002b59b2 add:Dlass遥测 2026-02-07 00:10:13 +08:00
CJKmkp 5601f72d59 add:Dlass遥测 2026-02-06 23:54:33 +08:00
CJKmkp afcc8050ce add:Dlass遥测 2026-02-06 23:46:05 +08:00
CJKmkp 17c043668c Merge branch 'beta' of https://github.com/InkCanvasForClass/community into beta 2026-02-06 23:37:07 +08:00
CJKmkp 2495fc7b26 add:Dlass遥测 2026-02-06 23:36:57 +08:00
doudou0720 61404ff852 feat(Workflow):改进工作流流程 (#370)
* feat 常态构建 : 使用官方action附带缓存选项

* feat(dotnet-build):Remove comment part

Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>

* fix(BuildDotnet):use "/" instead of "\\"

Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>

* fix:fall back to msbuild

Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>

* fix:Change to restore

Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>

* fix:Change to bash

Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>

* feat(prerelease):修改发版流程

Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>

* fix(PreRelease):尝试修复神秘问题

Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>

* feat(Release):添加统计信息

Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>

* chore:Delete Temp Workflow

Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>

---------

Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>
2026-02-06 23:24:45 +08:00
CJKmkp a62ae3c6e0 add:Dlass遥测 2026-02-06 23:14:41 +08:00
CJKmkp 28f65b3790 add:Dlass遥测 2026-02-06 23:12:54 +08:00
CJKmkp ce4b83dbe0 add:Dlass遥测 2026-02-06 23:00:57 +08:00
CJKmkp 7a24faece1 add:Dlass遥测 2026-02-06 22:47:41 +08:00
CJKmkp 22555b835b add:新设置 2026-02-06 22:06:50 +08:00
CJKmkp b2648fecac add:新设置 2026-02-06 22:01:01 +08:00
CJKmkp 15a6799d7b improve:PPT联动模块 2026-02-06 21:49:11 +08:00
CJKmkp 13b24aeb7c add:新设置 2026-02-06 21:45:48 +08:00
CJKmkp 674ce28420 improve:设备ID 2026-02-06 17:21:31 +08:00
CJKmkp 60c07c3738 add:双联动架构 2026-02-06 16:47:02 +08:00
CJKmkp 2b7f3c1f73 add:双联动架构 2026-02-06 16:38:33 +08:00
CJKmkp 04ff617e3c 优化日志 2026-02-06 16:28:54 +08:00
CJKmkp 5ee247d423 add:双联动架构 2026-02-06 16:26:18 +08:00
CJKmkp 0bd2c4eff7 文件更名 2026-02-06 16:12:58 +08:00
2,2,3-三甲基戊烷 0408729a1d Merge pull request #369 from PANDAJSR/beta
根据 https://github.com/doudou0720/ICC-CE/pull/20 中的修改建议修改了代码
2026-02-04 14:52:44 +08:00
PANDA-JSR 61e9a456af 根据 https://github.com/doudou0720/ICC-CE/pull/20 中的修改建议修改了代码 2026-02-04 14:49:56 +08:00
2,2,3-三甲基戊烷 93f89e7a41 Merge pull request #368 from PANDAJSR/feat-uri
根据 https://github.com/doudou0720/ICC-CE/pull/21 修改了代码
2026-02-04 14:42:03 +08:00
PANDA-JSR bb26c9ce55 fixed the XAML compilation error 2026-02-04 14:37:48 +08:00
PANDAJSR bd2107378b Merge branch 'InkCanvasForClass:beta' into feat-uri 2026-02-04 14:32:34 +08:00
PANDA-JSR 0b0714c166 根据 https://github.com/doudou0720/ICC-CE/pull/21 中的建议修改代码 2026-02-04 14:30:27 +08:00
2,2,3-三甲基戊烷 696ed84f4c Merge pull request #366 from PANDAJSR/feat--允许第三方程序通过系统URI来控制部分功能 2026-02-04 12:54:27 +08:00
2,2,3-三甲基戊烷 c8a467af9d Merge pull request #365 from PANDAJSR/beta 2026-02-04 12:54:06 +08:00
PANDA-JSR 8f6383cbe6 添加注册uri的功能 2026-02-04 11:09:49 +08:00
PANDA-JSR 25e64dd6f8 fix(自动折叠): 优化浮动栏自动折叠逻辑和日志记录
调整自动折叠检查间隔从200ms增加到500ms以提高性能
重构自动折叠处理逻辑,移除冗余代码并优化异常处理
增强日志记录信息,包含更多上下文用于调试
2026-02-03 22:34:17 +08:00
PANDA-JSR 745bc65379 improve: 自动收纳逻辑 2026-02-03 22:23:45 +08:00
CJKmkp a4868215f2 Merge branch 'beta' of https://github.com/InkCanvasForClass/community into beta 2026-01-31 17:20:08 +08:00
2,2,3-三甲基戊烷 8e2d8045c0 Merge pull request #359 from PANDAJSR/beta
feat(墨迹渐隐): 添加在笔工具菜单中隐藏墨迹渐隐控制开关的功能
2026-01-31 17:19:47 +08:00
doudou0720 5b290ef5c0 chore (MainWindow.xaml):Delete unuseful string
Signed-off-by: doudou0720 <98651603+doudou0720@users.noreply.github.com>
2026-01-31 10:42:16 +08:00
PANDA-JSR b68a50431f feat(墨迹渐隐): 添加在笔工具菜单中隐藏墨迹渐隐控制开关的功能
添加新设置选项,允许用户在笔工具菜单中隐藏墨迹渐隐控制开关。该功能包括:
- 新增设置属性 HideInkFadeControlInPenMenu
- 在设置界面添加对应的开关控件
- 实现开关状态同步和可见性更新逻辑
- 更新主界面笔工具菜单中的控制面板可见性

终于不怕课间有人玩墨迹渐隐了!!!
2026-01-23 12:21:28 +08:00
CJKmkp 4f8c7d66e1 improve:PPT联动 2026-01-19 21:02:24 +08:00
CJKmkp 48bf15457d Merge branch 'beta' of https://github.com/InkCanvasForClass/community into beta 2026-01-19 18:26:36 +08:00
CJKmkp d85bea7872 improve:PPT联动 2026-01-19 18:17:07 +08:00
CJKmkp 02fe33da5a improve:卡死检测 2026-01-19 18:16:03 +08:00
CJKmkp 3f17cc705b improve:PPT联动 2026-01-19 18:13:27 +08:00
doudou0720 8078d0f137 fix (Issue Template):修正格式问题 2026-01-18 22:35:58 +08:00
doudou0720 51ed642c35 fix (Issue Template):更改模板部分笔误
Close #353
2026-01-18 22:29:16 +08:00
doudou0720 842e1c0d6c feat (OpenSourceManagement):Issue模板 (#354) 2026-01-18 21:26:09 +08:00
CJKmkp f76cfe648f improve:PPT联动 2026-01-18 08:35:45 +08:00
CJKmkp 1b66473ae0 improve:PPT联动 2026-01-18 08:30:02 +08:00
CJKmkp f0bae76b78 imporve:PPT联动 2026-01-18 08:24:33 +08:00
CJKmkp ff58675069 improve:PPT联动 2026-01-18 08:18:38 +08:00
CJKmkp b3cb53b482 improve:PPT联动 2026-01-18 08:06:03 +08:00
CJKmkp 4eb9773398 improve:PPT联动 2026-01-18 07:58:41 +08:00
CJKmkp 748ab0fff2 improve:PPT联动 2026-01-18 07:51:50 +08:00
CJKmkp ae22162020 Merge branch 'beta' of https://github.com/InkCanvasForClass/community into beta 2026-01-18 07:42:30 +08:00
PrefacedCorg 7390e59ab0 优化自动收纳
在开启了 退出白板时自动收纳 时,当处于PPT放映模式时打开了白板退出后改为不自动收纳 不在PPT放映模式时则仍然自动收纳
2026-01-18 05:35:28 +08:00
PrefacedCorg 57e0331c58 fix:选择或套索选下扔处于橡皮擦模式 2026-01-18 04:23:51 +08:00
PrefacedCorg 862374bec4 修复ui错误 2026-01-18 04:05:55 +08:00
CJKmkp 266c5c0dc8 improve:PPT联动 2026-01-18 02:27:48 +08:00
CJKmkp 9fee9a1d6a improve:PPT联动 2026-01-18 02:07:39 +08:00
CJKmkp f65341d906 improve:PPT联动 2026-01-18 02:03:39 +08:00
CJKmkp c11ca7a3f7 improve:PPT联动 2026-01-18 01:30:24 +08:00
CJKmkp b7f3a38826 improve:PPT联动 2026-01-18 01:26:36 +08:00
CJKmkp 6ebe50e71b impsove:PPT联动 2026-01-18 01:20:30 +08:00
CJKmkp 6ce2aea000 improve:PPT联动 2026-01-18 01:05:54 +08:00
CJKmkp 60c341927f improve:PPT联动 2026-01-18 00:58:58 +08:00
CJKmkp ed03d40ff3 improve:PPT联动 2026-01-18 00:28:47 +08:00
CJKmkp 6c616f2b6b Merge branch 'beta' of https://github.com/InkCanvasForClass/community into beta 2026-01-18 00:13:20 +08:00
CJKmkp 84f026f1dd improve:PPT联动 2026-01-18 00:10:47 +08:00
CJKmkp 89c9c0d9ef improve:PPT联动 2026-01-17 23:02:06 +08:00
CJKmkp 9d631cc980 improve:PPT联动 2026-01-17 22:49:03 +08:00
CJKmkp 786d4fc719 improve:PPT联动 2026-01-17 22:38:40 +08:00
CJKmkp 9c68a5350b improve:PPT联动 2026-01-17 22:29:31 +08:00
CJKmkp e26bf83bb0 improve:PPT联动 2026-01-17 22:11:28 +08:00
CJKmkp 52583a6092 fix:浮动栏定位异常 2026-01-17 22:11:20 +08:00
CJKmkp de9056739b improve:PPT翻页 2026-01-17 20:23:33 +08:00
CJKmkp 9c611698c7 fix:issue #346 2026-01-17 20:05:33 +08:00
CJKmkp a6357abc15 improve:PPT联动 2026-01-17 19:46:18 +08:00
CJKmkp b83facb14e improve:PPT联动 2026-01-17 18:55:44 +08:00
PrefacedCorg 2dc16c13bc 更改注释 2026-01-17 17:52:12 +08:00
CJKmkp a46f8b36a0 improve:PPT联动模块 2026-01-17 17:44:46 +08:00
CJKmkp c0453ea1fc improve:PPT联动 2026-01-17 17:31:22 +08:00
CJKmkp 41b8f9c962 add:新设置 2026-01-17 17:24:27 +08:00
CJKmkp e69175e5c4 improve:主题适配 2026-01-17 17:22:46 +08:00
CJKmkp 76babf4dd3 improve:PPT联动 2026-01-17 17:11:11 +08:00
CJKmkp 5d6f53dc58 更新版本号 2026-01-17 17:10:56 +08:00
CJKmkp 72a49b7bf2 improve:自动更新 2026-01-17 17:05:55 +08:00
CJKmkp c4230a15c9 improve:信息保存 2026-01-17 16:51:28 +08:00
CJKmkp 84167bbcfc improve:自动收纳 2026-01-17 16:50:37 +08:00
CJKmkp 7f154ba1db improve:自动收纳 2026-01-17 16:46:37 +08:00
CJKmkp 7abb7e2ef1 improve:卡死检测 2026-01-17 16:32:57 +08:00
PrefacedCorg 04d21ac890 fix:颜色错误 2026-01-16 21:17:14 +08:00
CJKmkp a6992874f8 add:新设置 2026-01-11 08:23:09 +08:00
CJKmkp 1b2db81dde add:新设置 2026-01-11 00:16:22 +08:00
CJKmkp 17e6d23650 improve:PPT时间胶囊 2026-01-11 00:14:44 +08:00
CJKmkp 2aa1ba537f Merge branch 'beta' of https://github.com/InkCanvasForClass/community into beta 2026-01-10 23:32:06 +08:00
CJKmkp 981ca7629e add:新设置 2026-01-10 23:20:31 +08:00
PrefacedCorg 1aaef3d554 Merge branch 'beta' of https://github.com/InkCanvasForClass/community into beta 2026-01-10 23:16:16 +08:00
PrefacedCorg 2fd83a4a80 更新注释 2026-01-10 23:15:13 +08:00
CJKmkp fc89dce7c2 add:新设置 2026-01-10 23:06:27 +08:00
CJKmkp 62b85a4bbd add:新设置 2026-01-10 23:00:52 +08:00
CJKmkp 927d85ea68 improve:PPT模块 2026-01-10 22:13:00 +08:00
CJKmkp 89ccf700c3 更新注释 2026-01-10 21:40:22 +08:00
CJKmkp b3c29f2e27 更新信息 2026-01-10 21:39:08 +08:00
CJKmkp 06af63a10a add:PPT联动备用方法 2026-01-10 21:16:12 +08:00
CJKmkp 50742e5e4d add:PPT联动备用方法 2026-01-10 21:14:35 +08:00
CJKmkp 570c701b93 add:PPT联动备用方法 2026-01-10 21:10:13 +08:00
CJKmkp a1c4d53d7c add:PPT联动备用方法 2026-01-10 21:00:45 +08:00
CJKmkp 3250b81a23 add:PPT联动备用方法 2026-01-10 20:47:15 +08:00
CJKmkp fb914734c8 add:PPT联动备用方法 2026-01-10 20:38:55 +08:00
CJKmkp be6eb73671 improve:PPT侧边面板 2026-01-10 20:09:20 +08:00
CJKmkp 221a0f8e85 add:PPT联动备用方法 2026-01-10 19:51:00 +08:00
CJKmkp d5e5ec8c46 improve:卡死检测 2026-01-10 18:25:02 +08:00
CJKmkp b10215aec9 improve:开关逻辑 2026-01-10 18:22:58 +08:00
CJKmkp ea20f84d91 improve:自动更新 2026-01-10 18:14:55 +08:00
CJKmkp 008e843b39 Revert "fix:issue #339"
This reverts commit 172dd4f81b.
2026-01-10 18:06:58 +08:00
CJKmkp 172dd4f81b fix:issue #339 2026-01-10 18:02:07 +08:00
CJKmkp 4697fb4664 撤销新设置部分修改 2026-01-10 17:46:39 +08:00
CJKmkp c9e6ba972b 优化代码 2026-01-10 17:39:40 +08:00
CJKmkp 758f414302 Revert "add:新设置"
This reverts commit 0776071454.
2026-01-10 17:32:14 +08:00
CJKmkp 0fb5c04deb Revert "add:新设置"
This reverts commit fbfac18ca0.
2026-01-10 17:31:55 +08:00
CJKmkp 47885685fe Revert "add:新设置"
This reverts commit 89eb93aa67.
2026-01-10 17:31:50 +08:00
CJKmkp 8e87dddcd2 Revert "add:新设置"
This reverts commit 9d14f16fe2.
2026-01-10 17:31:46 +08:00
CJKmkp 4649649cf3 Revert "add:新设置"
This reverts commit acafdcc991.
2026-01-10 17:31:43 +08:00
CJKmkp 68a74be279 Revert "add:新设置"
This reverts commit 63585911a7.
2026-01-10 17:31:39 +08:00
CJKmkp 483e0757b7 撤销commit3cd2632 2026-01-10 17:31:10 +08:00
CJKmkp 9abce33257 Revert "improve:PPT墨迹加载"
This reverts commit e792f2637d.
2026-01-10 17:28:14 +08:00
CJKmkp 337d4d7288 撤销commit4fb73c1 2026-01-10 17:27:13 +08:00
CJKmkp bde0680f81 撤销commitf3ddd5a 2026-01-10 17:26:23 +08:00
CJKmkp ec579288a8 Revert "improve:PPT翻页打断"
This reverts commit ddfa9c2676.
2026-01-10 17:23:40 +08:00
CJK_mkp e2c222a156 Update copyright year in AssemblyInfo.cs 2026-01-08 11:32:46 +08:00
CJK_mkp 23de7e3575 更新信息 2026-01-08 11:32:16 +08:00
doudou0720 18a8f41bf3 fix(ci):扩大pr review范围到main,beta (#344)
* fix(ci):扩大pr review范围到main,beta

* fix(ci):将comment action v4 bump到 v5

* fix(ci):临时解决Action权限问题

* feat(ci):为nightly.link添加镜像

我估计应该没有外国人会用...
2026-01-08 11:31:43 +08:00
CJKmkp 63585911a7 add:新设置 2026-01-02 14:17:40 +08:00
CJKmkp acafdcc991 add:新设置 2026-01-02 13:38:04 +08:00
CJKmkp 9d14f16fe2 add:新设置 2026-01-02 13:14:10 +08:00
CJKmkp 89eb93aa67 add:新设置
主题优化
2026-01-02 12:59:04 +08:00
CJKmkp fbfac18ca0 add:新设置 2026-01-02 12:22:50 +08:00
CJKmkp 78b2f94bae 更新版本号 2026-01-02 11:18:19 +08:00
CJKmkp 89f0a401ef 代码优化 2026-01-02 10:05:57 +08:00
CJKmkp 0776071454 add:新设置 2026-01-02 09:47:34 +08:00
CJKmkp 56fb29fe15 improve:自动收纳 2026-01-02 09:28:00 +08:00
CJKmkp d7ed3884d6 add:新设置 2026-01-02 01:35:22 +08:00
CJKmkp ba673ccf41 add:新设置 2026-01-01 19:39:20 +08:00
CJKmkp 44c1071d49 add:新设置 2026-01-01 19:30:55 +08:00
CJKmkp 7789240f64 add:新设置 2026-01-01 18:39:24 +08:00
CJKmkp 7eee02dc94 improve:显示画笔光标 2026-01-01 18:21:28 +08:00
CJKmkp 965957aa1b add:新设置 2026-01-01 18:18:48 +08:00
CJKmkp 65a3917f62 improve:xml上传Dlass 2026-01-01 18:15:33 +08:00
CJKmkp 6ba4a57bbc add:新设置 2026-01-01 17:57:42 +08:00
CJKmkp 4e13817509 improve:PPT翻页按钮透明度设置 2026-01-01 17:42:02 +08:00
CJKmkp a8330fa2e9 fix:issue #339 2026-01-01 17:41:48 +08:00
doudou0720 be0d444db9 docs(README):Add DeepWiki badge to README (#340)
用于触发deepwiki的每周重新更新索引,方便相对快速地找到问题

Copied from Classisland
2026-01-01 12:30:04 +08:00
CJKmkp ecf3c1ad04 improve:PPT时间胶囊 2026-01-01 12:17:27 +08:00
CJKmkp 6bf439f493 improve:窗口检测模型 2026-01-01 12:09:00 +08:00
CJKmkp 94b52941af add:PPT侧边栏 2025-12-31 19:24:10 +08:00
CJKmkp a80fb33880 improve:图片选中 2025-12-31 18:51:57 +08:00
CJKmkp 30e4c35165 add:issue #314 2025-12-31 18:42:26 +08:00
CJKmkp 9bd1214567 improve:直线拉直
水平直线算法改进
2025-12-31 16:57:03 +08:00
CJKmkp c28a2bd792 improve:PPT时间胶囊
动画改进
2025-12-31 16:49:54 +08:00
CJKmkp 45a7e586c7 improve:Dlass服务
支持xml上传
2025-12-31 16:40:47 +08:00
CJKmkp 5903bab81f fix:issue #339 2025-12-31 16:38:02 +08:00
CJKmkp 295f8f56e3 fix:光标显示功能
修复开启后不显示的问题
2025-12-31 16:36:21 +08:00
CJKmkp 4cc8af7ff0 improve:PPT时间胶囊
改进主题
2025-12-31 16:28:04 +08:00
CJKmkp bca2dde497 Merge branch 'beta' of https://github.com/InkCanvasForClass/community into beta 2025-12-31 16:01:59 +08:00
CJKmkp a8a72159f0 improve:颜色选择后行为 2025-12-31 16:01:37 +08:00
CreeperAWA 8ea7cb6ac2 Remove redundant SHA256 hash calculations from release workflow (#338)
Removed hash calculations for ZIP and installer files in the prerelease workflow.
2025-12-31 15:55:07 +08:00
2,2,3-三甲基戊烷 2b721b111b Merge pull request #337 from InkCanvasForClass/all-contributors/add-2-2-3-trimethylpentane
docs: add 2-2-3-trimethylpentane as a contributor for test, tutorial, and video
2025-12-28 15:24:02 +08:00
allcontributors[bot] d7e080a579 docs: update .all-contributorsrc 2025-12-28 07:23:37 +00:00
allcontributors[bot] 6429c242e4 docs: update README.md 2025-12-28 07:23:36 +00:00
CJKmkp aa7b593b65 更新版本号 2025-12-28 15:11:55 +08:00
CJKmkp fabac7e2bb improve:点名UI
优化显示范围
2025-12-28 14:32:22 +08:00
CJKmkp 9b5ee56a09 improve:issue #175 2025-12-28 14:30:02 +08:00
CJKmkp c78d3d74c2 improve:更新提示 2025-12-28 13:59:25 +08:00
CJKmkp b1459901d0 fix:issue #329 2025-12-28 12:35:05 +08:00
CJKmkp aba56ac340 improve:直线拉直 2025-12-28 11:00:24 +08:00
CJKmkp 3d4e1872f1 add:新设置
增加搜索功能
2025-12-28 10:40:13 +08:00
doudou0720 7a4d33b7da fix(Workflow):删除obj缓存 (#336)
Removed caching of obj/ folders to prevent cross-version incremental build issues while retaining NuGet package caching.
2025-12-28 10:22:18 +08:00
CJKmkp afb65eb908 fix:触摸墨迹选中问题 2025-12-28 10:01:02 +08:00
CJKmkp df5daeeb75 add:新设置 2025-12-28 08:48:09 +08:00
CJKmkp 792779e5b2 Revert "fix:触摸墨迹选中"
This reverts commit 8172b7c776.
2025-12-28 08:37:05 +08:00
CJKmkp 3313a0a182 add:xml格式墨迹保存 2025-12-28 00:08:08 +08:00
CJKmkp 88dd53302f improve:PPT时间胶囊 2025-12-27 23:00:50 +08:00
CJKmkp e972adfbcb fix:issue #332 2025-12-27 22:57:22 +08:00
CJKmkp ab6425e0d9 fix:浮动栏收纳 2025-12-27 22:53:34 +08:00
CJKmkp a81ee5b3db add:PPT时间胶囊(没做完不好看) 2025-12-27 22:41:24 +08:00
CJKmkp fae08a5285 Revert "improve:issue #332"
This reverts commit ca4d2ac4a2.
2025-12-27 21:16:55 +08:00
CJKmkp eeb4a25d7a fix:issue #327 2025-12-27 19:46:08 +08:00
CJK_mkp fdf07180dd Merge branches 'beta' and 'beta' of https://github.com/InkCanvasForClass/community into beta 2025-12-27 18:40:55 +08:00
CJK_mkp 6387a6fcda fix:issue #334 2025-12-27 18:27:30 +08:00
CJK_mkp a6e400629a improve:墨迹纠正
纠正后补点
2025-12-27 18:22:00 +08:00
CJK_mkp 8172b7c776 fix:触摸墨迹选中 2025-12-27 18:13:54 +08:00
CJK_mkp ca4d2ac4a2 improve:issue #332 2025-12-27 17:58:53 +08:00
allcontributors[bot] abd9f850eb docs: add CJKmkp as a contributor for design (#333)
* docs: update README.md

* docs: update .all-contributorsrc

---------

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2025-12-27 17:31:08 +08:00
CJK_mkp 3aef6f5e05 fix:issue #327 2025-12-27 16:55:59 +08:00
CJK_mkp 2f957374e3 fix:issue #330 2025-12-27 16:51:46 +08:00
CJK_mkp 3f8cabc7e0 improve:PPT放映时的浮动栏定位 2025-12-27 16:41:30 +08:00
CJK_mkp 73d1dc8f48 improve:注释 2025-12-27 16:31:50 +08:00
CJK_mkp 2bfb78a257 撤销commitc99bd2b 2025-12-27 16:29:47 +08:00
PrefacedCorg 783e5c43a5 代码清理 2025-12-27 12:05:34 +08:00
PrefacedCorg d7e8330016 优化代码可读性
其实单纯就是强迫症
2025-12-27 10:51:42 +08:00
CJK_mkp 80d3836e9e fix:墨迹质量问题 2025-12-21 17:31:06 +08:00
CJK_mkp c26d1c348e fix:墨迹质量问题 2025-12-21 17:30:29 +08:00
PrefacedCorg 95c307cc0b 修改任务栏高度计算方式
应该可以解决 dock 栏偏移的问题 可能会炸 炸了再说
2025-12-21 16:32:23 +08:00
allcontributors[bot] 6e7299e445 docs: add doudou0720 as a contributor for code (#328)
* docs: update README.md

* docs: update .all-contributorsrc

---------

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2025-12-21 00:44:06 +08:00
CJKmkp 01fa047591 improve:pre-release
优化注释
2025-12-21 00:38:27 +08:00
CJKmkp 8c741c1fb7 improve:pre-release 2025-12-21 00:36:17 +08:00
CJK_mkp b0bfc8d5ed Update prerelease.yml 2025-12-21 00:00:52 +08:00
doudou0720 ad8d8f94ff Merge pull request #320 from doudou0720/beta-image
!refactor(CI/CD):构建工作流重构
2025-12-20 23:46:37 +08:00
PrefacedCorg c99bd2bb63 Update MainWindow.xaml 2025-12-20 23:00:04 +08:00
CJKmkp 468df7dad7 add:PPT按钮透明度设置 2025-12-20 22:56:13 +08:00
CJKmkp 3c2e4a0990 delete:新设置内选项 2025-12-20 21:26:03 +08:00
CJKmkp 87b717f6a9 improve:倒计时音效 2025-12-20 21:04:48 +08:00
PrefacedCorg 51dc45988e Revert "撤回commit8042b91"
This reverts commit 81621cb9d0.
2025-12-20 20:46:18 +08:00
CJKmkp 2f8b986f1f 更新版本号 2025-12-20 20:23:38 +08:00
CJKmkp 7f01e7acb6 improve:点名算法
改进不放回随机
2025-12-20 19:57:11 +08:00
CJKmkp d011d2ba8a improve:多指书写
优化性能
2025-12-20 19:55:37 +08:00
CJKmkp a922654c17 improve:多指书写 2025-12-20 19:36:16 +08:00
CJKmkp e70a486362 add:一言API
新增名言种类
2025-12-20 19:28:47 +08:00
CJKmkp 1683bc8418 improve:自动更新
优化线路自动测速
2025-12-20 19:18:58 +08:00
CJKmkp b6368fb0e4 add:有更新时系统级弹窗 2025-12-20 19:16:39 +08:00
CJKmkp ddfa9c2676 improve:PPT翻页打断
改进打断逻辑
2025-12-20 18:42:25 +08:00
CJKmkp f3ddd5a11a improve:PPT翻页打断
新增设置项
2025-12-20 18:37:57 +08:00
CJKmkp 39e8b2359e improve:PPT自动播放及记忆上次播放页码
改进弹窗出现时机
2025-12-20 18:25:07 +08:00
CJKmkp 4fb73c155b improve:PPT翻页打断 2025-12-20 18:11:27 +08:00
CJKmkp 3287603d0a fix:快捷键问题 2025-12-20 18:04:08 +08:00
CJKmkp 1abb317054 add;自动清理过期配置
避免老版本配置保留
2025-12-20 17:52:56 +08:00
CJKmkp 719e37c26b improve:清空点名名单
改进清空历史记录
2025-12-20 17:50:00 +08:00
CJKmkp 45d2f99fd7 improve:历史版本回滚
支持最近不再接收更新
2025-12-20 17:45:35 +08:00
CJKmkp f75fdded98 fix:开关显示异常 2025-12-20 17:38:36 +08:00
CJKmkp 22a6d87771 fix:几何绘制中断
解决绘制过程中增加触摸点导致后续失效
2025-12-20 17:33:50 +08:00
CJKmkp 2479da4bbc fix:错误注释 2025-12-20 17:31:57 +08:00
CJKmkp c22424d798 Merge branch 'beta' of https://github.com/InkCanvasForClass/community into beta 2025-12-20 17:31:03 +08:00
CJKmkp bbb30b7c25 improve:几何长按状态显示 2025-12-20 17:30:18 +08:00
CJKmkp 71cbe12cee improve:浮动栏按钮UI
优化按钮显示
2025-12-20 17:28:11 +08:00
CJKmkp 011effa047 优化代码 2025-12-20 17:22:17 +08:00
PrefacedCorg b1648dd702 更改避免全屏的解释说明 2025-12-20 17:07:52 +08:00
CJKmkp 81621cb9d0 撤回commit8042b91 2025-12-20 16:05:37 +08:00
PrefacedCorg 3f460d7a5c 代码清理 2025-12-20 13:56:46 +08:00
PrefacedCorg 6fe34c1250 feat: 白板和批注模式自动全屏 2025-12-20 13:28:47 +08:00
PrefacedCorg d5cac938c2 feat: 启用避免全屏助手时自动全屏处理白板和批注模式
- 进入白板/批注模式时自动全屏
- 退出时恢复工作区域大小
- 添加状态跟踪避免重复处理
- PPT放映模式和白板模式切换时正确管理全屏状态
2025-12-20 13:03:31 +08:00
CJKmkp 6222fabdd4 add:新下载新路 2025-12-13 22:01:21 +08:00
CJKmkp 8ed4f25499 improve:GitHub工作流 2025-12-13 21:12:18 +08:00
CJKmkp 3afd4641cd 更新版本号 2025-12-13 21:06:31 +08:00
CJKmkp e9fde97453 improve:GitHub工作流 2025-12-13 21:02:11 +08:00
CJKmkp 441f8b6e26 优化按钮名称 2025-12-13 20:47:30 +08:00
CJKmkp 8042b917a0 improve:浮动栏按钮UI
优化按钮显示
2025-12-13 20:45:31 +08:00
CJKmkp 343e7281fe improve:端点吸附
避免点线和虚线吸附
2025-12-13 20:32:43 +08:00
CJKmkp c64e6a4554 improve:issue #252
改进双指拖动
2025-12-13 20:22:23 +08:00
CJKmkp 8190bf275c Merge branch 'beta' of https://github.com/InkCanvasForClass/community into beta 2025-12-13 20:13:14 +08:00
CJKmkp e792f2637d improve:PPT墨迹加载 2025-12-13 20:13:08 +08:00
CJKmkp 3cd26323dc improve:翻页墨迹加载及浮动栏定位 2025-12-13 20:07:25 +08:00
PrefacedCorg 40e1c4d467 Update MW_FloatingBarIcons.cs 2025-12-13 19:54:10 +08:00
PrefacedCorg 86c22d373a Revert "feat: 添加白板模式自动全屏功能" 2025-12-13 19:48:21 +08:00
PrefacedCorg cb7a76efc5 Delete .vscode directory 2025-12-13 19:39:08 +08:00
PrefacedCorg 545425c4d3 Delete .kiro/steering directory 2025-12-13 19:38:34 +08:00
PrefacedCorg eb1aaa10e4 feat: 添加白板模式自动全屏功能
- 新增"白板模式自动全屏"设置选项
- 进入白板模式时自动全屏,退出时恢复工作区域大小
- 需配合"避免全屏助手"功能使用
- 用户可独立控制白板模式的全屏行为
2025-12-13 19:27:47 +08:00
CJKmkp daf0db312b improve:仅PPT模式
优化浮动栏显示
2025-12-13 18:02:05 +08:00
CJKmkp 8b2bc2f064 improve:退出放映时收纳及PPT记忆上次播放页数 2025-12-13 17:38:35 +08:00
CJKmkp 2e343cbbf9 improve:计时器
优化计时器行为
2025-12-13 17:20:18 +08:00
CJKmkp a75f0470bc improve:点名算法
缩小极差
2025-12-13 17:12:00 +08:00
CJKmkp 287d31a3a9 improve:窗口模式 2025-12-13 17:03:58 +08:00
CJKmkp 430fff0515 优化注释 2025-12-06 23:04:27 +08:00
CJKmkp fbbb7b8ad7 improve:issue #252
改进几何绘制
2025-12-06 23:01:41 +08:00
CJKmkp 54b74d7411 improve:issue #252
改进手掌擦及手势
2025-12-06 22:55:55 +08:00
CJKmkp 82dba31b2a improve:直线拉直
改进高精度拉直
2025-12-06 22:06:28 +08:00
CJKmkp 38d7e782e0 improve:issue #252
修复了几何绘制和多指书写状态问题
2025-11-30 11:51:19 +08:00
CJKmkp 05e5ceeb43 improve:计时器
改进主题加载
2025-11-29 23:11:01 +08:00
CJKmkp fcbbad71d2 更新版本号 2025-11-29 22:23:14 +08:00
CJKmkp 6f9161439f improve:直线拉直
改进直线拉直算法
2025-11-29 22:20:13 +08:00
CJKmkp aa0c4fb841 improve:Dlass服务
优化上传体验
2025-11-29 17:26:47 +08:00
CJKmkp cff50d1f81 fix:快捷键管理 2025-11-29 17:26:41 +08:00
CJKmkp b8581b6368 improve:点名算法
优化概率模型
2025-11-29 16:58:40 +08:00
CJKmkp 094f1223d1 improve:崩溃日志位置 2025-11-29 16:46:33 +08:00
CJKmkp 6802476afa improve:计时器UI
将计时器窗口整合至主窗口,优化全屏计时逻辑
2025-11-29 16:27:35 +08:00
CJKmkp a0539dce9b 更新版本号 2025-11-15 21:36:49 +08:00
CJKmkp bf2b8fec35 improve:计时器UI与点名UI
改进视觉反馈和按钮逻辑及窗口置顶
2025-11-15 21:14:08 +08:00
CJKmkp 082c9a03ec fix:issue #287 2025-11-15 20:48:04 +08:00
CJKmkp 4ccdd862ba improve:PPT模块
修复无焦点状态下键盘翻页无效
2025-11-15 20:24:28 +08:00
CJKmkp e5a20ed0fc improve:点名历史查看 2025-11-15 20:20:56 +08:00
CJKmkp a72022704e improve:点名算法 2025-11-15 19:34:52 +08:00
CJKmkp 2acc7ada30 improve:错误检测 2025-11-15 19:31:57 +08:00
CJKmkp e61882c331 improve:点名
点名算法优化
2025-11-15 19:20:35 +08:00
CJKmkp 61c145689a fix:issue #281 2025-11-15 18:41:42 +08:00
CJKmkp 40ea9664a7 improve:快抽置顶 2025-11-15 18:37:23 +08:00
CJKmkp bccd2d0f3e improve:PPT墨迹保存 2025-11-15 18:19:53 +08:00
CJKmkp b918809dca improve:线擦擦除 2025-11-15 18:06:36 +08:00
CJKmkp e7c2e92879 improve:PPT联动及墨迹管理
改回原处理模式,优化翻页操作
2025-11-15 18:04:22 +08:00
CJK_mkp cf03c921a7 更新版本号 2025-11-08 23:16:36 +08:00
CJK_mkp 2c45c839b1 Revert "improve:无焦点模式"
This reverts commit 83f5fc58d1.
2025-11-08 23:09:04 +08:00
CJK_mkp 83f5fc58d1 improve:无焦点模式 2025-11-08 22:55:18 +08:00
CJK_mkp 1a267f1e5a improve:点名 2025-11-08 22:23:44 +08:00
CJK_mkp c3fd5551d8 improve:快抽按钮 2025-11-08 22:17:52 +08:00
CJK_mkp 1baa74bb69 improve:快抽按钮 2025-11-08 22:07:38 +08:00
CJK_mkp 261ecefb17 improve:注释 2025-11-08 21:42:33 +08:00
CJK_mkp d81d8f7c5d improve:Dlass联动 2025-11-08 21:01:50 +08:00
CJK_mkp de1af12157 更新版本号 2025-11-08 20:41:43 +08:00
CJK_mkp 803cbbdee9 improve:默认设置 2025-11-08 20:29:18 +08:00
CJK_mkp 008477d5fa improve:默认设置 2025-11-08 20:17:51 +08:00
CJK_mkp 7f0d29ebd2 improve:快抽窗口 2025-11-08 20:07:00 +08:00
CJK_mkp 11bf8cffb2 improve:PPT墨迹加载 2025-11-08 19:55:10 +08:00
CJK_mkp 87b9ebc7e1 improve:PPT墨迹显示 2025-11-08 19:36:05 +08:00
CJK_mkp b89d27411b improve:计时器逻辑 2025-11-08 19:20:26 +08:00
CJK_mkp 24c37f1d3e fix:计时器时间不一致 2025-11-08 19:17:58 +08:00
CJK_mkp 58b0a0a3be fix:负数导致的计时器崩溃 2025-11-08 19:15:16 +08:00
CJK_mkp ed58873a82 fix:issue #272 2025-11-08 19:02:37 +08:00
CJK_mkp a8dcbd4af0 add:点名历史查看 2025-11-08 18:01:56 +08:00
CJK_mkp 4b2f29442a improve:点名 2025-11-07 10:36:24 +08:00
CJK_mkp dfab0d7ddf improvve:点名快抽 2025-11-07 10:35:27 +08:00
CJK_mkp ce1998b701 improve:Dlass 界面UI 2025-11-07 09:46:21 +08:00
CJK_mkp 92c631d6ce improve:操作逻辑 2025-11-02 12:34:50 +08:00
CJK_mkp 01009f9e35 更新版本号 2025-11-02 11:47:52 +08:00
CJK_mkp 74eca093da improve:悬浮快抽按钮 2025-11-02 11:40:27 +08:00
CJK_mkp e7d89e65b2 improve:AutoUpdate 2025-11-02 11:27:46 +08:00
CJK_mkp 24b2bffe8e improve:计时器窗口 2025-11-02 11:12:13 +08:00
CJK_mkp d2906476c8 add:Dlass联动 2025-11-02 10:46:16 +08:00
CJK_mkp 2b31a355ae add:Dlass联动 2025-11-02 10:30:36 +08:00
CJK_mkp b602048186 add:Dlass联动 2025-11-02 10:16:31 +08:00
CJK_mkp 4fb7031060 add:Dlass联动 2025-11-02 10:11:15 +08:00
CJK_mkp 4ef77c2e72 add:Dlass联动 2025-11-02 09:41:53 +08:00
CJK_mkp 72ba1a9f58 add:Dlass联动 2025-11-02 09:29:06 +08:00
CJK_mkp 0c3938b652 Merge pull request #274 from MKStoler1024/patch-1
Readme
2025-11-01 22:18:46 +08:00
CJK_mkp 16f80adb0d fix:issue #210 2025-11-01 22:12:47 +08:00
CJK_mkp 637b6bb4f9 fix:issue #210 2025-11-01 22:05:19 +08:00
CJK_mkp 12e91927a5 fix:issue #210 2025-11-01 21:56:31 +08:00
CJK_mkp 8f6f22ba7f 更新版本号 2025-11-01 21:37:55 +08:00
CJK_mkp b520d6a334 fix:issue #210 2025-11-01 21:00:23 +08:00
CJK_mkp b7bff30445 improve:UI 2025-11-01 20:49:37 +08:00
CJK_mkp 0d790bbd80 add:墨迹自动保存 2025-11-01 20:42:18 +08:00
CJK_mkp 8394e99127 improve:主题切换 2025-11-01 20:26:52 +08:00
CJK_mkp 16ae32bfd7 improve:点名UI 2025-11-01 19:17:52 +08:00
CJK_mkp cc6423e384 improve:点名UI 2025-11-01 18:56:38 +08:00
CJK_mkp 06cc587599 improve:启动动画 2025-11-01 18:46:04 +08:00
CJK_mkp 9d36088f1d fix:issue #210 2025-11-01 18:39:52 +08:00
CJK_mkp f9f73b015c improve:启动动画 2025-11-01 18:31:27 +08:00
CJK_mkp 77ffd696bb fix:手势面板不显示 2025-11-01 18:29:58 +08:00
CJK_mkp bdb8bed053 fix:issue #210 2025-11-01 18:25:35 +08:00
MKStoler1024 4b17c8e96e chore: remove sth in readme 2025-10-31 17:56:02 +08:00
MKStoler1024 8109711f4e chore: readme 2025-10-31 16:07:03 +08:00
CJK_mkp 327eba3fa7 Update MW_FloatingBarIcons.cs 2025-10-31 15:35:27 +08:00
CJK_mkp f3dccb2e99 Update MW_FloatingBarIcons.cs 2025-10-31 14:46:20 +08:00
CJK_mkp 7112d58e7c Update MW_ShapeDrawing.cs 2025-10-31 14:44:59 +08:00
CJK_mkp 6e0aad853c Update MW_TouchEvents.cs 2025-10-31 14:44:05 +08:00
CJK_mkp c64b1d0846 Update MW_BoardIcons.cs 2025-10-31 14:43:15 +08:00
CJK_mkp 6eba16ce99 Update MW_BoardIcons.cs 2025-10-31 12:19:30 +08:00
CJK_mkp 2f6f719843 Update MW_ShapeDrawing.cs 2025-10-31 12:18:45 +08:00
CJK_mkp f34bac49e4 Update MW_TouchEvents.cs 2025-10-31 12:16:52 +08:00
CJK_mkp 39cdc6231f Update MW_FloatingBarIcons.cs 2025-10-31 12:16:05 +08:00
CJK_mkp 745e24da70 Update MW_FloatingBarIcons.cs 2025-10-31 12:15:26 +08:00
CJK_mkp a4f4f4fb15 Update MW_TouchEvents.cs 2025-10-31 12:12:51 +08:00
CJK_mkp 3833c229c6 Update MW_FloatingBarIcons.cs 2025-10-31 12:11:02 +08:00
CJK_mkp 3dc3e9b5a8 Update MW_FloatingBarIcons.cs 2025-10-31 10:49:10 +08:00
CJK_mkp 12eeb79e9f 回滚一下 2025-10-30 12:02:29 +08:00
CJK_mkp e08c84f70d improve:点名快抽 2025-10-27 17:54:58 +08:00
CJK_mkp 10f55d5b65 fix:抽选结果截断 2025-10-27 17:45:41 +08:00
CJKmkp 761992d089 Delete PositionConverters.cs 2025-10-26 00:31:32 +08:00
CJKmkp 991a823700 更新版本号 2025-10-26 00:24:28 +08:00
CJKmkp 61dbcf762c add:点名快抽 2025-10-26 00:21:10 +08:00
CJKmkp 60b0149a9c add:新点名UI 2025-10-26 00:00:13 +08:00
CJKmkp 501c034cfa fix:点名UI按钮主题显示 2025-10-25 21:14:59 +08:00
CJKmkp 11593db23c 更新版本号 2025-10-25 20:14:07 +08:00
CJKmkp a9ae2a004f improve:注释 2025-10-25 20:09:04 +08:00
CJKmkp 7b2a31781c 优化代码 2025-10-25 19:57:56 +08:00
CJKmkp ab73eb9632 优化代码 2025-10-25 19:44:32 +08:00
CJKmkp 8ade170b4e improve:计时器数字 2025-10-25 17:27:19 +08:00
CJKmkp 91c3d1161d fix:issue #210 2025-10-25 17:03:44 +08:00
CJK_mkp bb63805e87 fix:issue #210
Removed the SimulateMultiTouchToggle method and its related comments.
2025-10-23 16:27:39 +08:00
CJK_mkp 8caf04990c 更新信息 2025-10-22 17:30:38 +08:00
CJK_mkp f28dd0a965 Update AssemblyInfo.cs 2025-10-22 17:29:12 +08:00
CJK_mkp b5d83268e5 更新信息 2025-10-22 17:28:56 +08:00
CJK_mkp 19adc42122 Change PPTNavigationBtn_MouseUp to async method 2025-10-22 12:18:03 +08:00
CJK_mkp 8afee913be Update MW_PPT.cs 2025-10-22 12:16:42 +08:00
CJK_mkp 82d101365f Update MW_PPT.cs 2025-10-22 12:12:43 +08:00
CJK_mkp 73e679f268 Make PPTNavigationBtn_MouseDown async
Changed PPTNavigationBtn_MouseDown to be asynchronous and added a delay for animation.
2025-10-22 12:08:29 +08:00
CJK_mkp 372a8a1de1 fix:PPT状态浮动栏位置异常 2025-10-20 17:45:14 +08:00
CJK_mkp 171cc34c91 撤销修改 2025-10-20 12:10:47 +08:00
CJK_mkp fbd217d674 撤销修改8d778a
Removed quick color palette visibility logic from ink editing mode.
2025-10-20 10:49:04 +08:00
CJKmkp f24960ab26 improve:UIA窗口置顶 2025-10-19 01:34:03 +08:00
CJKmkp d1a871c8f6 Reapply "fix:issue # 262"
This reverts commit 086b97906b.
2025-10-18 21:20:54 +08:00
CJKmkp 086b97906b Revert "fix:issue # 262"
This reverts commit 5bfd0c7b2f.
2025-10-18 21:16:24 +08:00
CJKmkp db76a11347 improve:窗口置顶 2025-10-18 20:45:23 +08:00
CJKmkp 729b10cce8 Reapply "improve:UIA窗口置顶"
This reverts commit 4cbd25ccb3.
2025-10-18 20:41:09 +08:00
CJKmkp 4cbd25ccb3 Revert "improve:UIA窗口置顶"
This reverts commit 0ef7b738a9.
2025-10-18 20:38:08 +08:00
CJKmkp 0ef7b738a9 improve:UIA窗口置顶 2025-10-18 20:15:40 +08:00
CJKmkp 33b5563601 更新版本号 2025-10-18 19:05:16 +08:00
CJKmkp f6f799f534 improve:墨迹清空 2025-10-18 19:03:30 +08:00
CJKmkp 8c3f1360e1 improve:注释 2025-10-18 18:45:08 +08:00
CJKmkp fafbabd603 improve:清空墨迹 2025-10-18 18:42:29 +08:00
CJKmkp 254e38895c fix:无焦点功能丢失 2025-10-18 18:29:52 +08:00
CJKmkp aa0bb22cdd add:UIA窗口置顶 2025-10-18 18:26:13 +08:00
CJKmkp 5bfd0c7b2f fix:issue # 262 2025-10-18 17:36:47 +08:00
CJKmkp c3885c170c improve:PPT墨迹管理 2025-10-18 17:24:47 +08:00
CJKmkp 88a7cce269 fix:issue #133 2025-10-18 16:46:09 +08:00
CJKmkp a1fccc2905 fix:PPT上次页数记忆跳转 2025-10-18 16:41:55 +08:00
CJKmkp a889041896 fix:issue #260 2025-10-18 16:31:07 +08:00
CJKmkp 8d778aba2c improve:PPT模块 2025-10-18 16:27:26 +08:00
CJKmkp ba5db63e0b improve:计时器UI 2025-10-18 16:15:19 +08:00
CJKmkp 0259d83429 improve:计时器 2025-10-12 18:06:44 +08:00
CJKmkp b6999e57ae improve:计时器 2025-10-12 18:05:40 +08:00
CJKmkp 04184cf731 improve:计时器 2025-10-12 17:44:44 +08:00
CJKmkp 04f98eb9e7 improve:计时器 2025-10-12 17:17:51 +08:00
CJKmkp 869dd045af improve:计时器 2025-10-12 16:08:36 +08:00
CJK_mkp 339ebb862e improve:计时器UI 2025-10-08 17:34:12 +08:00
CJKmkp 7b8598bf9f Revert "Update MainWindow.xaml.cs"
This reverts commit 298164d6ed.
2025-10-06 22:41:53 +08:00
CJKmkp ab1c460225 Reapply "add:手动更新"
This reverts commit 703a8a4e0d.
2025-10-06 22:41:42 +08:00
CJKmkp 298164d6ed Update MainWindow.xaml.cs 2025-10-06 22:39:18 +08:00
CJKmkp 703a8a4e0d Revert "add:手动更新"
This reverts commit 1fd95a2f2e.
2025-10-06 22:36:59 +08:00
CJKmkp deaf5fcbf6 更新版本号 2025-10-06 22:05:27 +08:00
CJKmkp 18b46689f1 improve:计时器 2025-10-06 21:49:19 +08:00
CJKmkp edbe3f8311 improve:计时器 2025-10-06 21:17:14 +08:00
CJKmkp 94c4c3e2d4 improve:计时器UI 2025-10-06 21:08:47 +08:00
CJKmkp 077f72737d improve:计时器UI 2025-10-06 21:05:22 +08:00
CJKmkp ae089a9390 improve:计时器UI 2025-10-06 20:57:33 +08:00
CJKmkp 0f087c2aa6 improve:计时器UI 2025-10-06 20:46:19 +08:00
CJKmkp 19d2ffb48e improve:计时器UI 2025-10-06 20:44:09 +08:00
CJKmkp a9bb6f73de improve:计时器UI 2025-10-06 20:33:50 +08:00
CJKmkp ca124732fa improve:计时器 2025-10-06 20:23:55 +08:00
CJKmkp 11da14aeab improve:计时器 2025-10-06 20:11:49 +08:00
CJKmkp 7d5f037c85 improve:计时器 2025-10-06 19:59:01 +08:00
CJKmkp f62b415227 improve:计时器UI 2025-10-06 19:56:00 +08:00
CJKmkp f2b8d4014e improve:计时器UI 2025-10-06 19:46:14 +08:00
CJKmkp 3ef047fb41 improve:计时器UI 2025-10-06 19:43:04 +08:00
CJKmkp aa3be4ab0d improve:计时器UI 2025-10-06 19:05:22 +08:00
CJKmkp 7500049ea2 improve:计时器UI 2025-10-06 19:03:10 +08:00
CJKmkp 44600df75c improve:计时器UI 2025-10-06 18:44:00 +08:00
CJKmkp dec2a15773 improve:计时器UI 2025-10-06 18:43:14 +08:00
PrefacedCorg 4da78a04cb 代码清理 2025-10-06 18:29:12 +08:00
CJKmkp 98ec204bab improve:计时器UI 2025-10-06 18:03:15 +08:00
CJKmkp cd9499b064 improve:计时器UI 2025-10-06 17:55:05 +08:00
CJKmkp f5e824be86 improve:计时器 2025-10-06 17:49:43 +08:00
CJKmkp c76254f4f9 improve:计时器UI 2025-10-06 17:28:52 +08:00
CJKmkp 77ac6f88ca improve:计时器UI 2025-10-06 17:28:32 +08:00
CJKmkp 7e10911991 improve:计时器UI 2025-10-06 17:23:16 +08:00
CJKmkp 161b67b09d improve:计时器UI 2025-10-06 17:17:56 +08:00
CJKmkp 9614536a29 improve:计时器UI 2025-10-06 17:14:40 +08:00
CJKmkp 8ba7aab468 improve:计时器UI 2025-10-06 17:08:38 +08:00
CJKmkp 4ea9f79de1 improve:计时器UI 2025-10-06 17:06:02 +08:00
CJKmkp b7f7025d97 improve:计时器UI 2025-10-06 17:00:27 +08:00
CJKmkp 20d5dd2668 improve:计时器UI 2025-10-06 16:53:00 +08:00
CJKmkp adc2d02fbb Merge branch 'beta' of https://github.com/InkCanvasForClass/community into beta 2025-10-06 16:39:08 +08:00
CJK_mkp ef2dbdc93b Merge pull request #258 from LiuYan-xwx/beta
feat: 优化滑动动画与设置面板逻辑
2025-10-06 16:38:11 +08:00
CJKmkp 9721ec1f0b improve:计时器UI 2025-10-06 16:35:21 +08:00
LiuYan-xwx 4d069d87d7 feat: 优化滑动动画与设置面板逻辑
- 调整滑动动画的目标位置,更新 From 和 To 属性值。
- 移动并优化 `BorderSettings.Visibility` 的设置逻辑。
- 提升代码可读性,减少重复代码,改进动画效果。
2025-10-06 16:32:33 +08:00
CJKmkp 0308f9ce65 improve:计时器UI 2025-10-06 16:30:55 +08:00
CJKmkp 6bcd3cb217 improve:计时器UI 2025-10-06 16:29:03 +08:00
CJKmkp 7f83c490db improve:计时器UI 2025-10-06 16:14:52 +08:00
CJKmkp bbf9c895b8 Merge branch 'beta' of https://github.com/InkCanvasForClass/community into beta 2025-10-06 16:06:15 +08:00
CJKmkp ffa2063c52 improve:计时器UI 2025-10-06 16:04:03 +08:00
CJKmkp d464b1f78e improve:计时器UI 2025-10-06 15:59:48 +08:00
CJKmkp 92cb071408 improve:计时器UI 2025-10-06 15:52:38 +08:00
PrefacedCorg 9141b60d03 Merge pull request #257 from LiuYan-xwx/beta
Update .gitignore
2025-10-06 15:44:52 +08:00
LiuYan-xwx df0a196931 Update .gitignore 2025-10-06 15:41:52 +08:00
CJKmkp a8cb1dd495 improve:计时器 2025-10-06 15:26:46 +08:00
CJKmkp adc4966d49 improve:计时器 2025-10-06 15:00:42 +08:00
CJKmkp 1b2ea8c522 improve:计时器UI 2025-10-06 14:55:52 +08:00
CJKmkp 7c8bdb489b improve:计时器 2025-10-06 14:50:56 +08:00
CJKmkp 16458fbb42 improve:计时器 2025-10-06 14:31:54 +08:00
CJKmkp c72839cdcb improve:计时器 2025-10-06 14:31:04 +08:00
CJKmkp a31ad5803c improve:计时器 2025-10-06 14:28:53 +08:00
CJKmkp cf800cbd36 improve:自动更新 2025-10-06 14:19:24 +08:00
CJKmkp 3c06ef0b1a 优化注释 2025-10-06 14:11:32 +08:00
CJKmkp 2c3b921f09 improve:计时器逻辑 2025-10-06 13:39:31 +08:00
CJKmkp 402ecc66ae improve:计时器逻辑 2025-10-06 13:38:12 +08:00
CJKmkp e25f56a9b5 improve:计时器逻辑 2025-10-06 13:34:52 +08:00
CJKmkp b28fa887a2 improve:计时器逻辑 2025-10-06 13:31:54 +08:00
CJKmkp f83a02e619 improve:UI 2025-10-06 13:15:03 +08:00
CJKmkp eaad089d68 更新版本号 2025-10-06 09:56:07 +08:00
CJKmkp a2fda16df9 improve:主题切换 2025-10-05 23:58:39 +08:00
CJKmkp e9e8ff57ae improve:主题切换 2025-10-05 23:22:02 +08:00
CJKmkp 1fd95a2f2e add:手动更新 2025-10-05 23:09:05 +08:00
CJK_mkp 228584ee48 Update README.md 2025-10-05 21:31:30 +08:00
CJK_mkp c651df0f6e Update README.md 2025-10-05 21:27:56 +08:00
CJKmkp 5cced9baf2 fix:图标数组错误 2025-10-05 21:13:37 +08:00
CJKmkp 5dd26e554b 更新版本号 2025-10-05 12:20:20 +08:00
CJK_mkp 696fd3e8cd Update privacy.txt 2025-10-05 12:07:42 +08:00
CJKmkp 1d19b705d3 fix:issue #202 2025-10-05 11:49:22 +08:00
CJKmkp d54074cb57 improve:主题切换 2025-10-05 09:44:54 +08:00
CJKmkp cc054aeb75 improve:主题切换 2025-10-05 09:33:25 +08:00
CJKmkp c32eaed534 improve:UI 2025-10-05 09:29:31 +08:00
CJKmkp fa23f73ec4 improve:UI 2025-10-05 09:26:14 +08:00
CJKmkp fbf6a13f92 improve:主题切换 2025-10-05 09:16:36 +08:00
CJKmkp 3eba662772 improve:主题切换 2025-10-05 09:14:29 +08:00
CJKmkp e007ee271f improve:主题切换 2025-10-05 08:58:16 +08:00
CJKmkp 59a8d65a89 improve:主题切换 2025-10-05 08:57:04 +08:00
CJKmkp 793519ae1b improve:主题切换 2025-10-05 08:55:18 +08:00
CJKmkp 5c44062aa2 improve:主题切换 2025-10-05 00:26:18 +08:00
CJKmkp fe03a2c2c0 improve:主题切换 2025-10-05 00:24:23 +08:00
CJKmkp d1d74a3770 improve:主题切换 2025-10-05 00:18:58 +08:00
CJKmkp 16108bf779 add:图标 2025-10-04 23:49:12 +08:00
CJKmkp 5bebf077e4 improve:主题切换 2025-10-04 22:59:11 +08:00
CJKmkp efcc01ad6b improve:主题切换 2025-10-04 22:44:47 +08:00
CJKmkp e8a4b45446 improve:主题切换 2025-10-04 22:39:32 +08:00
CJKmkp 6cba297d77 improve:主题切换 2025-10-04 22:37:46 +08:00
CJKmkp b149dc3cb9 improve:主题切换 2025-10-04 22:31:42 +08:00
CJKmkp 1ee4f934b9 improve:主题切换 2025-10-04 22:30:40 +08:00
CJKmkp f7b4adb85d Revert "improve:主题"
This reverts commit da3c41567d.
2025-10-04 22:12:47 +08:00
CJKmkp da3c41567d improve:主题 2025-10-04 22:08:30 +08:00
CJKmkp 5f73795220 improve:主题切换 2025-10-04 22:05:58 +08:00
CJKmkp 36bf1122c6 improve:主题切换 2025-10-04 22:04:24 +08:00
CJKmkp 402f8bb9f9 improve:主题切换 2025-10-04 21:55:55 +08:00
CJKmkp eef2a915fa improve:主题 2025-10-04 21:48:44 +08:00
CJKmkp 26ee4d172f improve:主题切换 2025-10-04 21:44:15 +08:00
CJKmkp 08eb450446 improve:主题切换 2025-10-04 21:40:55 +08:00
CJKmkp ecae7d818c improve:主题切换 2025-10-04 21:00:51 +08:00
CJKmkp 47ffccff68 improve:主题切换 2025-10-04 19:59:42 +08:00
CJKmkp 0ba5286c94 improve:主题切换 2025-10-04 18:42:55 +08:00
CJKmkp db8e1e3589 improve:主题切换 2025-10-04 18:29:42 +08:00
CJKmkp d0764a6a77 improve:主题切换 2025-10-04 18:19:43 +08:00
CJKmkp 7b6c347d6b improve:主题切换 2025-10-04 18:17:11 +08:00
CJKmkp d58fec180c improve:主题切换 2025-10-04 18:10:43 +08:00
CJKmkp ac4e4877a1 improve:主题切换 2025-10-04 17:49:44 +08:00
CJKmkp ddea61245d improve:主题切换 2025-10-04 17:47:52 +08:00
CJKmkp 98915bcff2 improve:主题切换 2025-10-04 17:32:17 +08:00
CJKmkp 8e0f0450df improve:主题切换 2025-10-04 17:30:26 +08:00
CJKmkp 1d9a669829 improve:主题切换 2025-10-04 17:26:11 +08:00
CJKmkp 97bcf168b7 improve:主题 2025-10-04 17:15:58 +08:00
CJKmkp e81198165b improve:主题 2025-10-04 17:14:14 +08:00
CJKmkp b03b8da586 improve:主题 2025-10-04 17:12:14 +08:00
CJKmkp d497e07187 improve:主题 2025-10-04 17:10:11 +08:00
CJKmkp f284a99194 improve:UI 2025-10-04 17:03:03 +08:00
CJKmkp 7d6dd6f805 add:仿希沃计时器 2025-10-04 17:00:15 +08:00
CJKmkp d70e28d198 improve:图标 2025-10-04 16:56:15 +08:00
CJKmkp 4c6f138e5c add:图标 2025-10-04 16:49:03 +08:00
CJKmkp 5974841a7b add:仿希沃计时器 2025-10-04 16:21:14 +08:00
CJKmkp 5df54439ba add:仿希沃计时器 2025-10-04 16:14:56 +08:00
CJKmkp 8f627c6b7f add:图标 2025-10-04 14:55:49 +08:00
CJKmkp 3f28bc5c6c improve:自动更新 2025-10-04 14:47:53 +08:00
CJKmkp 903fa699ca improve:侧边栏图标 2025-10-04 09:49:37 +08:00
PrefacedCorg d9e8f64699 1145141919810
代码清理
2025-10-03 17:08:46 +08:00
CJKmkp 8ad6ca8d41 优化代码 2025-10-03 12:26:15 +08:00
CJK_mkp 4017efc65e Merge pull request #248 from InkCanvasForClass/main
合并一下
2025-10-03 11:51:13 +08:00
CJK_mkp 6ed15d52de Merge pull request #247 from InkCanvasForClass/beta
test action
2025-10-03 11:50:15 +08:00
CJKmkp 5246a2f79b test action 2025-10-03 11:48:09 +08:00
CJK_mkp b670932dd7 Merge pull request #246 from InkCanvasForClass/beta
test action
2025-10-03 11:32:51 +08:00
CJKmkp ddf30bd96b test action 2025-10-03 11:31:54 +08:00
CJKmkp 7e92428485 test action 2025-10-03 11:30:28 +08:00
CJKmkp 4c16050c85 test action 2025-10-03 11:29:46 +08:00
CJK_mkp 2ed035525a Merge pull request #245 from InkCanvasForClass/beta
test action
2025-10-03 11:24:14 +08:00
CJKmkp 1b89b0d7b6 test action 2025-10-03 11:23:46 +08:00
CJK_mkp 0364821f0b Merge pull request #244 from InkCanvasForClass/beta
test action
2025-10-03 11:18:13 +08:00
CJKmkp fa9d6f37ae test action 2025-10-03 11:17:42 +08:00
CJK_mkp a4777904b9 Merge pull request #243 from InkCanvasForClass/beta
test action
2025-10-03 11:14:40 +08:00
CJKmkp 0b3c2f95c5 test action 2025-10-03 11:14:16 +08:00
CJK_mkp 8f54955432 Merge pull request #242 from InkCanvasForClass/beta
test action
2025-10-03 11:11:21 +08:00
CJKmkp 034db2fc27 test action 2025-10-03 11:07:58 +08:00
CJKmkp 59141b0241 test action 2025-10-03 11:04:00 +08:00
CJK_mkp 2e43b96a2c Merge pull request #241 from InkCanvasForClass/beta
合并pre-release action
2025-10-03 11:01:21 +08:00
CJKmkp edff96299c test action 2025-10-03 10:59:36 +08:00
CJKmkp 8d9587b790 test action 2025-10-03 10:57:58 +08:00
CJKmkp ad0cf2a849 test action 2025-10-03 10:56:55 +08:00
CJKmkp f928946c61 improve:白板模式切换 2025-10-03 10:47:28 +08:00
CJKmkp 6cc9d0bee2 fix:issue #219 2025-10-03 09:19:07 +08:00
CJKmkp 287e6bb91f fix:issue #219 2025-10-03 09:12:39 +08:00
CJKmkp 3509036d85 fix:issue #219 2025-10-03 09:04:40 +08:00
CJKmkp 752901dbb9 fix:issue #219 2025-10-03 08:55:28 +08:00
CJKmkp e3add94546 fix:issue #219 2025-10-03 08:52:47 +08:00
CJKmkp 8d31c74b39 add:图标 2025-10-02 22:51:44 +08:00
CJKmkp e6f608d206 add:图标 2025-10-02 22:49:29 +08:00
CJKmkp d80b9e29a7 Merge branch 'beta' of https://github.com/InkCanvasForClass/community into beta 2025-10-02 22:30:00 +08:00
CJK_mkp 092674465d Merge pull request #239 from InkCanvasForClass/all-contributors/add-Tayasui-rainnya
docs: add Tayasui-rainnya as a contributor for design
2025-10-02 22:28:55 +08:00
allcontributors[bot] b2e3a5bb18 docs: update .all-contributorsrc 2025-10-02 14:28:32 +00:00
allcontributors[bot] 147a2f957e docs: update README.md 2025-10-02 14:28:31 +00:00
CJKmkp ce56f2919c add:图标 2025-10-02 22:24:20 +08:00
CJKmkp 18059102a3 add:图标 2025-10-02 22:15:07 +08:00
CJKmkp 4542fcccbb 默认线路组改为inkeys 2025-10-02 21:52:55 +08:00
CJKmkp d12009d30c 修改 2025-10-02 20:31:08 +08:00
CJK_mkp d85e0fbd13 Merge pull request #238 from Tayasui-rainnya/beta
添加dark♂icc-ce图标
2025-10-02 20:29:50 +08:00
CJKmkp f7a5f9ae6b improve:启动动画 2025-10-02 19:51:31 +08:00
tayasui rainnya! 3d4c3d0acc dark♂icc-ce 图标 2025-10-02 19:42:24 +08:00
tayasui rainnya! c106c41048 dark♂图标 2025-10-02 19:41:18 +08:00
CJKmkp cea777e8b2 improve:启动动画 2025-10-02 19:25:57 +08:00
CJKmkp 6f069b73da improve:启动动画 2025-10-02 19:20:03 +08:00
CJKmkp c80af8c984 improve:aovidfullsreen 2025-10-02 19:13:20 +08:00
CJKmkp d51cbd0682 improve:aovidfullsreen 2025-10-02 19:08:33 +08:00
CJKmkp 7e94c945ff improve:aovidfullsreen 2025-10-02 19:05:24 +08:00
CJKmkp bd9095b4c2 improve:aovidfullsreen 2025-10-02 19:04:28 +08:00
CJKmkp c135bf8fb8 improve:aovidfullsreen 2025-10-02 18:57:49 +08:00
CJKmkp f6aebb15b4 improve:aovidfullsreen 2025-10-02 18:54:09 +08:00
CJKmkp 2b4f88becd fix:issue #237 2025-10-02 18:48:11 +08:00
CJKmkp 7fbd3639b6 fix:退出白板后收纳 2025-10-02 18:15:39 +08:00
CJKmkp 9e52c2c31d fix:退出白板后收纳 2025-10-02 18:06:08 +08:00
CJKmkp 46a1e5ff14 improve:主题切换 2025-10-02 17:39:56 +08:00
CJKmkp fa7c1fd646 fix:屏蔽压感 2025-10-02 17:04:12 +08:00
CJKmkp c347809eea improve:启动动画 2025-10-02 16:38:40 +08:00
CJKmkp dd09a42f02 improve:启动动画 2025-10-02 16:33:54 +08:00
CJKmkp db8ffd05ea improve:启动动画 2025-10-02 16:22:50 +08:00
CJK_mkp 045c29ca20 Merge pull request #236 from InkCanvasForClass/beta
ICC CE 1.7.12.0
2025-10-02 16:03:39 +08:00
CJKmkp 4ea6d19602 更新版本号 2025-10-02 15:58:33 +08:00
CJKmkp df00447c41 improve:启动动画 2025-10-02 15:36:52 +08:00
CJKmkp db5b1caea7 improve:启动动画 2025-10-02 15:35:53 +08:00
CJKmkp 8432954b8d add:issue #235 2025-10-02 15:30:51 +08:00
CJK_mkp 4a000e23e9 Update README.md 2025-10-02 15:09:27 +08:00
CJKmkp b42db51346 fix:忽略希沃白板五桌面批注窗口 2025-10-02 15:05:55 +08:00
CJKmkp 1ef968ea93 improve:悬浮窗拦截 2025-10-02 14:57:55 +08:00
CJKmkp 5b50537333 improve:开机动画 2025-10-02 14:53:57 +08:00
CJKmkp 8c624b48fb 删除无用信息 2025-10-02 14:08:22 +08:00
CJKmkp ad329fc2c8 improve:悬浮窗拦截 2025-10-02 11:50:00 +08:00
CJKmkp f045b8f659 fix:悬浮窗拦截 2025-10-02 11:45:55 +08:00
CJKmkp 00a9da45bb improve:启动动画 2025-10-02 02:59:49 +08:00
CJKmkp b7ecd12f8c improve:启动动画 2025-10-02 02:55:53 +08:00
CJKmkp a8a3164ee7 improve:启动动画 2025-10-02 02:44:45 +08:00
CJKmkp b1aec69e98 fix:issue #234 2025-10-02 02:32:29 +08:00
CJKmkp d713a8c5a4 优化注释 2025-10-02 02:30:24 +08:00
CJKmkp 3895faf941 fix:issue #133 2025-10-02 02:16:02 +08:00
CJKmkp bf336fdb10 add:issue #214 2025-10-02 02:11:54 +08:00
CJKmkp c96c26288c add:软件启动动画 2025-10-02 01:51:10 +08:00
CJKmkp 83558c089d add:软件启动动画 2025-10-02 00:17:11 +08:00
CJKmkp 26acb72e17 add:软件启动动画 2025-10-02 00:05:09 +08:00
CJKmkp 4724a432ab add:软件启动动画 2025-10-01 23:26:38 +08:00
CJKmkp 5b9c1627c0 add:软件启动动画 2025-10-01 22:38:40 +08:00
CJKmkp d06c2585cf add:软件启动动画 2025-10-01 22:24:49 +08:00
CJKmkp 3e96e51efd delete:office注册表修改 2025-10-01 22:06:23 +08:00
CJKmkp 9afa7191c4 fix:issue #232 2025-10-01 18:55:03 +08:00
CJKmkp dfddec5586 add:软件启动动画 2025-10-01 18:45:26 +08:00
CJKmkp 93022424b3 add:软件启动动画 2025-10-01 18:38:01 +08:00
CJKmkp aed77a187f add:软件启动动画 2025-10-01 18:17:39 +08:00
CJKmkp 583f47c98b 更新版本号 2025-10-01 15:20:14 +08:00
CJK_mkp ff528b3f9d Merge pull request #233 from InkCanvasForClass/beta
ICC CE 1.7.11.5
2025-10-01 15:17:17 +08:00
CJKmkp 6d98d96ccb fix:issue #217 2025-10-01 14:13:09 +08:00
CJKmkp 6f8e08c21a fix:issue #217 2025-10-01 13:03:11 +08:00
CJKmkp 3a7417f493 优化代码 2025-10-01 12:57:16 +08:00
CJKmkp 66164a0c33 improve:配置文件损坏自动恢复 2025-10-01 11:34:37 +08:00
CJKmkp 0d7a478e16 improve:配置文件损坏自动恢复 2025-10-01 11:29:59 +08:00
CJKmkp 9caef310df 优化代码 2025-10-01 10:04:40 +08:00
CJKmkp 688f742c16 add:老版UI切换 2025-10-01 09:52:15 +08:00
CJKmkp 38dd083cdc add:老版UI切换 2025-10-01 09:48:44 +08:00
CJKmkp 851bd1c3c6 improve:主题切换 2025-10-01 09:13:48 +08:00
CJKmkp c91b8a1a7a add:issue #224 2025-10-01 00:54:03 +08:00
CJKmkp 69c45764b2 Revert "add:issue #224"
This reverts commit 07a62d2f78.
2025-10-01 00:53:22 +08:00
CJKmkp 07a62d2f78 add:issue #224 2025-10-01 00:49:12 +08:00
CJKmkp 46b064b0a8 add:配置文件损坏自动恢复 2025-10-01 00:01:35 +08:00
CJKmkp 1d3b96bb65 更新Action 2025-09-30 23:45:40 +08:00
CJKmkp 916425b7f5 更新Action 2025-09-30 23:38:58 +08:00
CJKmkp 1c570ca242 fix:issue #210 2025-09-30 23:18:16 +08:00
CJKmkp ae41108971 优化代码 2025-09-30 22:56:32 +08:00
CJKmkp 395e7609a4 fix:issue #210 2025-09-30 22:55:06 +08:00
CJKmkp 185c9dc284 Reapply "fix:issue #201"
This reverts commit f40ef15a24.
2025-09-30 19:16:56 +08:00
CJKmkp a9b0ac0595 Revert "优化代码"
This reverts commit 92bb458345.
2025-09-30 19:15:03 +08:00
CJKmkp d9e3524211 fix:issue #201 2025-09-30 19:10:34 +08:00
CJKmkp f40ef15a24 Revert "fix:issue #201"
This reverts commit 9b5d157333.
2025-09-30 19:02:45 +08:00
CJKmkp 4d7544eefb deletefix:issue #210 2025-09-30 18:45:57 +08:00
CJKmkp 92bb458345 优化代码 2025-09-30 17:46:35 +08:00
CJKmkp 92e695ef7c fix:issue #213 2025-09-30 17:22:32 +08:00
CJKmkp 45ff3fbb9b fix:issue #213 2025-09-30 17:18:00 +08:00
CJKmkp ba252e92d3 fix:issue #229 2025-09-30 16:55:44 +08:00
CJK_mkp 4778f459e3 优化日志 2025-09-29 18:19:36 +08:00
CJK_mkp c997854ae0 优化日志 2025-09-29 18:14:39 +08:00
CJK_mkp 65b5322ad5 优化日志 2025-09-29 18:13:11 +08:00
CJK_mkp dd43bf0476 优化日志 2025-09-29 18:11:21 +08:00
CJKmkp 15c808eebd 更新版本号 2025-09-27 18:00:12 +08:00
CJKmkp 28748a99ca improve:手掌擦 2025-09-27 17:58:50 +08:00
CJKmkp fb37d3b9e6 improve:更新弹窗 2025-09-27 17:52:25 +08:00
CJKmkp e1f10e054c improve:issue #223 2025-09-27 17:09:06 +08:00
CJKmkp c670357c01 improve:橡皮擦 2025-09-27 17:01:33 +08:00
CJK_mkp 92dce9b36e Update MainWindow.xaml 2025-09-26 22:14:12 +08:00
PrefacedCorg aa2b62e8da 修复退出放映按钮字体显示不完全
把字体调小了()
2025-09-22 20:23:36 +08:00
CJKmkp a34a65f354 优化代码 2025-09-22 13:26:53 +08:00
CJKmkp eea5f8496c fix:高光显示 2025-09-22 13:11:22 +08:00
CJKmkp 091a256bcc Revert "Replace SymbolIcon with FontIcon throughout UI"
This reverts commit 37a69032f6.
2025-09-22 13:09:43 +08:00
CJKmkp 14c9ce3ce1 fix:issue #219 2025-09-22 12:32:53 +08:00
CJKmkp fd1e5e13fe fix:进入PPT自动收纳 2025-09-22 12:11:03 +08:00
CJKmkp 19685b8f95 fix:进入PPT自动收纳 2025-09-22 11:58:54 +08:00
CJKmkp a9da8dc10c fix:进入PPT自动收纳 2025-09-22 11:48:05 +08:00
CJKmkp fcfba7a978 fix:进入PPT自动收纳 2025-09-22 11:36:55 +08:00
PrefacedCorg 2c8c4351dd Update WenXiang.png 2025-09-25 10:26:38 +08:00
PrefacedCorg 79264b187d Remove unused exception variable in catch block
The exception variable 'ex' in the catch block was unused and has been removed for cleaner code.
2025-09-22 09:44:38 +08:00
PrefacedCorg 52eba9117b Merge branch 'beta' of https://github.com/InkCanvasForClass/community into beta 2025-09-22 09:34:45 +08:00
PrefacedCorg 37a69032f6 Replace SymbolIcon with FontIcon throughout UI
Updated all XAML and code-behind references from SymbolIcon/Symbol to FontIcon/Glyph for consistency and compatibility with the UI framework. This affects button icons, window controls, and shape drawing logic across multiple windows and components.
2025-09-22 09:34:39 +08:00
CJKmkp dd16b4f5a1 更新版本号 2025-09-21 08:28:32 +08:00
CJKmkp e9c20255a6 imporve:PPT墨迹保存 2025-09-21 08:26:55 +08:00
CJKmkp 630b4edf91 improve:PPT墨迹保存 2025-09-21 07:52:11 +08:00
CJKmkp 04ef638e17 fix:issue #190 2025-09-21 07:43:29 +08:00
CJKmkp 35bad240c2 更新版本号 2025-09-21 02:08:29 +08:00
CJKmkp 0cb4749cf3 add:主题切换 2025-09-21 02:05:49 +08:00
CJKmkp 7c8281eb00 add:主题切换 2025-09-21 01:35:13 +08:00
CJKmkp 1957288219 add:主题切换 2025-09-21 01:15:01 +08:00
CJKmkp c4d2b15c48 add:主题切换 2025-09-21 01:06:42 +08:00
CJKmkp beebfb0dae add:主题切换 2025-09-21 00:39:30 +08:00
CJKmkp d67172b795 优化代码 2025-09-21 00:28:30 +08:00
CJKmkp 2b012bc042 add:主题切换 2025-09-21 00:25:09 +08:00
CJKmkp 84da68f950 improve:历史版本回滚窗口 2025-09-20 22:27:26 +08:00
CJKmkp ad97fd13bf improve:更新弹窗 2025-09-20 22:21:05 +08:00
CJKmkp ebf6d0d5f7 improve:历史版本回滚窗口 2025-09-20 22:20:13 +08:00
CJKmkp 0f3b4b4384 Merge branch 'beta' of https://github.com/InkCanvasForClass/community into beta 2025-09-20 22:02:43 +08:00
PrefacedCorg 226cf46d6b Merge pull request #212 from Hao3288/beta
readme文件中的部分url地址填写错误
2025-09-20 21:55:56 +08:00
CJKmkp 5689e33541 improve:更新弹窗 2025-09-20 21:54:02 +08:00
NoobHao 15e59e7117 Update README.md 2025-09-20 21:46:43 +08:00
NoobHao 6b538f6662 Update README.md 2025-09-20 21:45:38 +08:00
CJKmkp ad6808b696 improve:图片拖动 2025-09-20 21:33:21 +08:00
CJKmkp 8347b18efc 优化代码 2025-09-20 21:25:30 +08:00
CJKmkp f92b132f0a 优化代码 2025-09-20 19:49:16 +08:00
CJKmkp 79057d6757 fix:issue #160 2025-09-20 19:36:00 +08:00
CJKmkp a9b5ee8f62 fix:issue #205 2025-09-20 19:03:00 +08:00
CJKmkp 65da2c449e 更新版本号 2025-09-20 17:13:04 +08:00
CJKmkp 0ef8c12650 improve:issue #180 2025-09-20 17:06:11 +08:00
PrefacedCorg 1dfa48aecb Update MainWindow.xaml 2025-09-20 16:42:35 +08:00
PrefacedCorg b5d1713b43 Merge branch 'beta' of https://github.com/InkCanvasForClass/community into beta 2025-09-20 16:36:02 +08:00
PrefacedCorg 102421997d add DeveloperAvatars 2025-09-20 16:34:55 +08:00
CJKmkp 43370bb8d1 fix:issue #202 2025-09-20 16:13:35 +08:00
CJKmkp 310f50fcde fix:issue #202 2025-09-20 16:10:52 +08:00
CJKmkp 9b5d157333 fix:issue #201 2025-09-20 15:36:45 +08:00
CJKmkp 53fbb7a0bd improve:更新弹窗 2025-09-20 15:36:24 +08:00
CJKmkp fe92117c4e improve:更新弹窗 2025-09-20 15:25:41 +08:00
CJKmkp 4a0d13457a improve:更新弹窗 2025-09-20 15:25:08 +08:00
CJKmkp 1ad6405003 improve:issue #197 2025-09-20 14:49:40 +08:00
CJKmkp 75e7e36011 improve:issue #133 2025-09-20 14:31:38 +08:00
CJKmkp 933c695b8c fix:issue #133 2025-09-20 14:02:29 +08:00
CJKmkp b34f7142f6 improve:墨迹平滑 2025-09-20 13:44:01 +08:00
CJKmkp 7392fa8165 fix:issue #204 2025-09-20 12:38:22 +08:00
CJKmkp 349d417869 fix:issue #205 2025-09-20 12:35:30 +08:00
CJKmkp b8e94cac9c fix:issue #210 2025-09-20 12:22:21 +08:00
CJKmkp cd90490b8d fix:issue #210 2025-09-20 12:15:21 +08:00
CJKmkp a9cc94ccb6 fix:issue #210 2025-09-20 12:03:30 +08:00
CJKmkp 17f137af09 fix:issue #210 2025-09-20 11:55:50 +08:00
CJKmkp ea7d0bbf71 fix:issue #206 2025-09-20 11:49:25 +08:00
CJKmkp 176f1cf405 fix:issue #208 2025-09-20 11:45:35 +08:00
CJKmkp 2ee93bbcc1 fix:issue #209 2025-09-20 11:42:33 +08:00
CJKmkp ba0629000e add:退出白板收纳 2025-09-20 11:22:09 +08:00
CJKmkp 313049f873 fix:issue #203 2025-09-20 11:08:52 +08:00
CJK_mkp d1a1b17d29 fix:白板参数 2025-09-18 18:07:45 +08:00
CJK_mkp eb91d2ce0a fix:白板参数 2025-09-18 18:06:53 +08:00
CJK_mkp 71c77da898 fix:issue #203 2025-09-18 17:58:51 +08:00
CJK_mkp 1b96c70386 fix:issue #160 2025-09-18 17:50:47 +08:00
CJK_mkp 703cecbd4f Merge pull request #196 from InkCanvasForClass/main
没事干合并一下commit
2025-09-13 23:15:56 +08:00
CJK_mkp e20bf41cc7 Merge pull request #195 from InkCanvasForClass/beta
ICC CE 1.7.11.0
2025-09-13 23:05:18 +08:00
CJKmkp 0f7b9524f9 fix:收纳模式下进入PPT自动进入批注异常 2025-09-13 22:53:31 +08:00
CJKmkp 53a9e901ec fix:退出PPT时错误的快捷键状态 2025-09-13 22:44:29 +08:00
CJKmkp 9a79fc71ed improve:PPT墨迹 2025-09-13 22:20:41 +08:00
CJKmkp e7f7a8038f 优化代码 2025-09-13 21:41:34 +08:00
CJKmkp 5361f8ae6f improve:墨迹延迟 2025-09-13 21:38:03 +08:00
CJKmkp ec3bcddc9d 更新版本号 2025-09-13 21:15:58 +08:00
CJKmkp 326a9f1d75 优化代码 2025-09-13 21:10:36 +08:00
CJKmkp 8c07e9b8a3 优化代码 2025-09-13 19:54:21 +08:00
CJKmkp 406d01febf 优化代码 2025-09-13 19:35:36 +08:00
CJKmkp 98663422b1 优化代码 2025-09-13 19:29:17 +08:00
CJKmkp 879dfcea28 add:issue #190 2025-09-13 19:10:40 +08:00
CJKmkp 40edb4c8de Merge branch 'beta' of https://github.com/InkCanvasForClass/community into beta 2025-09-13 18:53:49 +08:00
CJKmkp 07b6d142ad add:issue #190 2025-09-13 18:52:23 +08:00
2,2,3-三甲基戊烷 5716b3e3cf docs&fix: 移除ssh还存在于readme贡献者名单的bug 2025-09-13 18:45:59 +08:00
CJKmkp 1b242a07e1 delete:ssh 2025-09-13 18:29:13 +08:00
CJK_mkp 1516a73228 Merge pull request #192 from ShihaoShen2025/patch-1
删除all contributors src中有关hydro的信息
2025-09-13 18:22:46 +08:00
CJKmkp eef00eb28f add:issue #190 2025-09-13 18:20:56 +08:00
CJKmkp a5ac282ab6 add:issue #190 2025-09-13 18:13:31 +08:00
CJKmkp dbb88d4999 add:issue #190 2025-09-13 18:07:42 +08:00
电教沈同学 39addad3a1 Delete myself from ac-src
是时候给一切画上一个句号了。
再见,ICC-CE。
2025-09-13 17:58:06 +08:00
电教沈同学 87fd9a4470 Update .all-contributorsrc 2025-09-13 17:52:52 +08:00
电教沈同学 7a9156449f Update .all-contributorsrc 2025-09-13 17:50:36 +08:00
CJKmkp 6e2938e9c1 add:issue #190 2025-09-13 17:37:11 +08:00
CJKmkp 39e1498b26 add:issue #180 2025-09-13 16:36:41 +08:00
CJKmkp cc2dc9c1a7 add:issue #180 2025-09-13 16:28:31 +08:00
CJKmkp fa87198131 add:issue #180 2025-09-13 16:26:23 +08:00
CJKmkp 9b70b952f6 add:issue #180 2025-09-13 16:13:22 +08:00
CJKmkp b29fca1cdb add:issue #180 2025-09-13 16:09:51 +08:00
CJKmkp 501af800ce add:issue #180 2025-09-13 16:06:19 +08:00
CJKmkp 979be117c6 add:issue #180 2025-09-13 16:02:39 +08:00
CJKmkp 13594371bb add:issue #180 2025-09-13 15:34:50 +08:00
CJKmkp 9628d1b27f add:issue #181 2025-09-13 14:59:47 +08:00
CJKmkp fd0bd0b343 fix:托盘功能不可用 2025-09-13 14:33:49 +08:00
CJKmkp 9ea58bfdad 优化代码 2025-09-13 14:21:07 +08:00
CJKmkp c54d140107 improve:截图 2025-09-13 14:16:11 +08:00
CJKmkp 98d4a4213c improve:截图 2025-09-13 13:59:54 +08:00
CJKmkp a7d1de5ee3 improve:截图 2025-09-13 13:50:41 +08:00
CJKmkp fab9e4b265 improve:截图 2025-09-13 13:37:51 +08:00
CJKmkp d5fa46033e 优化日志 2025-09-13 13:03:04 +08:00
CJKmkp 32c179bbf9 improve:弹窗 2025-09-13 12:22:08 +08:00
CJKmkp 99f9886876 improve:直接调用外部点名 2025-09-13 12:08:45 +08:00
CJKmkp c1fcaff28a fix:issue #175 简易实现(后续会改) 2025-09-13 12:05:13 +08:00
CJKmkp 2da5e8d71b fix:动画导致的收纳问题 2025-09-13 11:56:26 +08:00
CJKmkp e80e64a287 fix:手势开关状态显示错误 2025-09-13 11:46:49 +08:00
CJKmkp 0344e51ef7 fix:issue #185 2025-09-13 11:38:46 +08:00
CJKmkp 9fd31b0584 improve:描述性文字 2025-09-13 11:32:08 +08:00
CJKmkp c6a48f79da fix:issue #184 2025-09-13 11:29:48 +08:00
CJKmkp 110d050cc6 fix:issue #188 2025-09-13 11:08:56 +08:00
CJKmkp 18188ef235 improve:弹窗 2025-09-13 11:04:04 +08:00
CJKmkp 3e1c397132 fix:issue #160 2025-09-13 10:58:43 +08:00
CJKmkp cda1f0b77d fix:无焦点功能状态丢失 2025-09-13 10:55:27 +08:00
CJKmkp afd49f154c improve:模式切换 issue #179 2025-09-13 10:51:08 +08:00
CJKmkp 505c620103 improve:PowerPoint联动增强 2025-09-13 10:48:33 +08:00
CJKmkp 5d9e340d6d improve:模式切换 issue #179 2025-09-13 10:46:02 +08:00
CJKmkp f705c4575c Merge branch 'beta' of https://github.com/InkCanvasForClass/community into beta 2025-09-13 10:43:50 +08:00
CJKmkp 1508bd806f 优化日志 2025-09-13 10:43:22 +08:00
CJK_mkp 1c542e0615 Update bug_report.yml 2025-09-12 15:59:11 +08:00
CJK_mkp ed5bcbde18 Update feature_request.yml 2025-09-12 15:58:36 +08:00
PrefacedCorg 084cbcd362 代码清理 2025-09-07 13:30:46 +08:00
CJKmkp ad8369cfe9 更新版本号 2025-09-07 13:25:20 +08:00
CJKmkp 7d36b3993e 优化代码 2025-09-07 09:41:42 +08:00
CJKmkp ead0854c8e improve:墨迹选择 2025-09-07 07:59:21 +08:00
CJKmkp 60b5655574 improve:墨迹选择 2025-09-07 07:53:05 +08:00
CJKmkp 9037630990 fix:issue #175 2025-09-07 01:50:44 +08:00
CJKmkp ba23fc9506 add:issue #171 2025-09-07 01:45:50 +08:00
CJKmkp 938ca7c0ea improve:墨迹选择 2025-09-07 01:22:46 +08:00
CJKmkp a034f7a9a0 improve:墨迹选择 2025-09-07 01:16:27 +08:00
PrefacedCorg d4c4fcfd74 Update PluginSettingsWindow.xaml 2025-09-07 01:02:34 +08:00
PrefacedCorg 5b457f2a8a 将插件管理器的页面改为无边框 2025-09-07 00:59:24 +08:00
CJKmkp adc22441fc fix:issue #173 2025-09-07 00:57:48 +08:00
CJKmkp 1ba59136c6 Merge branch 'beta' of https://github.com/InkCanvasForClass/community into beta 2025-09-07 00:49:58 +08:00
CJKmkp 77702b2ac9 fix:issue #170 2025-09-07 00:49:10 +08:00
PrefacedCorg 0d6c814908 好像改错了() 2025-09-07 00:49:01 +08:00
PrefacedCorg 42ce58db60 贡献名单add PrefacedCorg(2) 2025-09-07 00:47:30 +08:00
PrefacedCorg 1ef56033fb 贡献名单add PrefacedCorg(1) 2025-09-07 00:31:22 +08:00
CJKmkp 8c1d3c0248 fix:issue #168 2025-09-07 00:20:48 +08:00
CJKmkp a725a12d25 fix:issue #169 2025-09-07 00:14:59 +08:00
CJKmkp f67db9beed fix:issue #178 #177 2025-09-07 00:11:22 +08:00
CJKmkp f22fd7b5d1 fix:issue #172 2025-09-07 00:08:11 +08:00
CJKmkp 060664260b fix:issue #172 2025-09-07 00:03:15 +08:00
CJKmkp 9da5ec7413 撤销操作 2025-09-06 23:58:00 +08:00
CJKmkp 076240f7cd 测试构建 2025-09-06 23:55:54 +08:00
CJKmkp d97a4ad243 fix:墨迹识别 2025-09-06 23:21:05 +08:00
CJKmkp 56209d8491 fix:issue #159 2025-09-06 22:22:41 +08:00
411 changed files with 83297 additions and 34036 deletions
+68 -13
View File
@@ -1,4 +1,5 @@
{
"$schema": "https://www.schemastore.org/all-contributors.json",
"projectName": "community",
"projectOwner": "InkCanvasForClass",
"files": [
@@ -6,7 +7,7 @@
],
"commitType": "docs",
"commitConvention": "angular",
"contributorsPerLine": 7,
"contributorsPerLine": 5,
"contributors": [
{
"login": "CJKmkp",
@@ -16,16 +17,8 @@
"contributions": [
"maintenance",
"doc",
"code"
]
},
{
"login": "Hydro11451",
"name": "Hydrogen",
"avatar_url": "https://avatars.githubusercontent.com/u/214308559?v=4",
"profile": "http://hydro11451.qzz.io",
"contributions": [
"code"
"code",
"design"
]
},
{
@@ -45,7 +38,10 @@
"contributions": [
"blog",
"doc",
"design"
"design",
"test",
"tutorial",
"video"
]
},
{
@@ -98,6 +94,65 @@
"contributions": [
"design"
]
},
{
"login": "Tayasui-rainnya",
"name": "tayasui rainnya!",
"avatar_url": "https://avatars.githubusercontent.com/u/156585442?v=4",
"profile": "https://github.com/Tayasui-rainnya",
"contributions": [
"design",
"code"
]
},
{
"login": "doudou0720",
"name": "doudou0720",
"avatar_url": "https://avatars.githubusercontent.com/u/98651603?v=4",
"profile": "https://github.com/doudou0720",
"contributions": [
"code",
"blog",
"infra"
]
},
{
"login": "PANDAJSR",
"name": "PANDAJSR",
"avatar_url": "https://avatars.githubusercontent.com/u/170189561?v=4",
"profile": "https://github.com/PANDAJSR",
"contributions": [
"code"
]
},
{
"login": "LiuYan-xwx",
"name": "流焰xwx",
"avatar_url": "https://avatars.githubusercontent.com/u/66517348?v=4",
"profile": "http://lyxwx.top",
"contributions": [
"code"
]
},
{
"login": "Super-Yyt",
"name": "Super-Yyt",
"avatar_url": "https://avatars.githubusercontent.com/u/206630707?v=4",
"profile": "https://github.com/Super-Yyt",
"contributions": [
"infra",
"blog"
]
},
{
"login": "Hao3288",
"name": "NoobHao",
"avatar_url": "https://avatars.githubusercontent.com/u/119276078?v=4",
"profile": "https://github.com/Hao3288",
"contributions": [
"code"
]
}
]
],
"repoType": "github"
}
+12
View File
@@ -0,0 +1,12 @@
{
"image": "mcr.microsoft.com/devcontainers/dotnet",
"postCreateCommand": "dotnet restore",
"customizations": {
"vscode": {
"extensions": [
"ms-dotnettools.csdevkit",
"ms-dotnettools.csharp"
]
}
}
}
@@ -1,12 +1,15 @@
name: Bug 报告 | Bug Report
description: 反馈软件缺陷或异常 | Report a bug to help us improve
labels: [bug]
type: Bug
body:
- type: markdown
attributes:
value: |
感谢你的反馈!请详细填写以下内容,便于我们定位问题。
Thank you for your feedback! Please fill out the following information to help us locate the issue.
在报告问题之前,请确保你的软件已经更新到最新Beta版本,否则我们可能会无条件直接关闭该Issue,感谢配合!
Before reporting the issue, please make sure your software has been updated to the latest Beta version. Otherwise, we may unconditionally close this Issue without any further notice. Thank you for your cooperation!
- type: input
id: version
attributes:
@@ -33,24 +36,31 @@ body:
id: steps
attributes:
label: 复现步骤 | Steps to Reproduce
description: 如何复现该问题?如有必要可附截图/录屏 | How to reproduce this bug? Screenshots/recordings if needed
description: 如何复现该问题?如有必要可附截图/录屏/触发该问题的文件 | How to reproduce this bug? Screenshots/recordings/specific files if needed
placeholder: |
1.
2.
3.
validations:
required: false
required: true
- type: textarea
id: expected
attributes:
label: 期望结果 | Expected Behavior
description: 你期望的正确行为或结果 | What did you expect to happen?
validations:
required: false
required: true
- type: textarea
id: extra
attributes:
label: 其他补充信息 | Additional Info
description: 其他相关信息(如日志、配置、特殊环境等)| Any other context, logs, configs, special environment, etc.
description: 其他相关信息(如日志文件、崩溃日志文件、配置文件、特殊环境等)| Any other context, logs, crash logs, configs, special environment, etc.
validations:
required: false
- type: upload
id: files
attributes:
label: 上传有关文件 | Upload relevant files
description: "你可以在此处上传相关文件 | You can upload relevant files here."
validations:
required: false
@@ -1,6 +1,6 @@
name: 功能请求 | Feature Request
description: 提出你对本项目的功能建议 | Suggest an idea for this project
labels: [enhancement]
type: Feature
body:
- type: markdown
attributes:
@@ -20,7 +20,7 @@ body:
label: 需求动机 | Motivation
description: 为什么需要这个功能?| Why do you need this feature?
validations:
required: false
required: true
- type: textarea
id: design
attributes:
@@ -35,3 +35,10 @@ body:
description: 其他补充说明或建议 | Any other context or suggestions
validations:
required: false
- type: upload
id: files
attributes:
label: 上传有关文件 | Upload relevant files
description: "你可以在此处上传相关文件 | You can upload relevant files here."
validations:
required: false
+36
View File
@@ -0,0 +1,36 @@
---
name: (Markdown Template) Bug 报告 | Bug Report
about: 反馈软件缺陷或异常 | Report a bug to help us improve
title: "[Version x.x.x] <your title>"
type: Bug
---
<!---请注意,你正在使用Markdown格式的Issue模板,如果你删除该模板的框架、更改问题的tag/类型/受理人或者不按照规范填写,你的Issue可能被直接关闭,如果你对Markdown不熟悉,请使用位于该选项下方的反馈入口继续反馈,感谢配合!-->
<!---感谢你的反馈!请详细填写以下内容,便于我们定位问题。-->
<!---Thank you for your feedback! Please fill out the following information to help us locate the issue.-->
<!---在报告问题之前,请确保你的软件已经更新到最新Beta版本,否则我们可能会无条件直接关闭该Issue,感谢配合!-->
<!---Before reporting the issue, please make sure your software has been updated to the latest Beta version. Otherwise, we may unconditionally close this Issue without any further notice. Thank you for your cooperation!-->
### 软件版本 | App Version (必填 | Required)
<!---可在设置中的"关于"界面查看 | You can find it on the "About" interface in the settings-->
<!---例如 v1.2.3 | e.g. v1.2.3-->
### 操作系统及版本 | OS & Version (必填 | Required)
<!---例如 Windows 10 22H2 64位 | e.g. Windows 10 22H2 64bit-->
### 问题描述 | Description (必填 | Required)
<!---简要描述遇到的问题 | Briefly describe the problem-->
### 复现步骤 | Steps to Reproduce (必填 | Required)
<!---如何复现该问题?如有必要可附截图/录屏/触发该问题的文件 | How to reproduce this bug? Screenshots/recordings/specific files if needed-->
1.
2.
3.
### 期望结果 | Expected Behavior (必填 | Required)
<!---你期望的正确行为或结果 | What did you expect to happen?-->
### 其他补充信息 | Additional Info (可选 | Optional)
<!---其他相关信息(如日志文件、崩溃日志文件、配置文件、特殊环境等)| Any other context, logs, crash logs, configs, special environment, etc.-->
@@ -0,0 +1,23 @@
---
name: (Markdown Template) 功能请求 | Feature Request
about: 提出你对本项目的功能建议 | Suggest an idea for this project
title: "[Feature Request] "
type: Feature
---
<!---请注意,你正在使用Markdown格式的Issue模板,如果你删除该模板的框架、更改问题的tag/类型/受理人或者不按照规范填写,你的Issue可能被直接关闭,如果你对Markdown不熟悉,请使用位于该选项下方的反馈入口继续反馈,感谢配合!-->
<!---感谢你的建议!请详细描述你的需求。-->
<!---Thank you for your suggestion! Please describe your needs in detail.-->
### 功能描述 | Description (必填 | Required)
<!---请描述你希望添加的功能 | Describe the feature you want-->
### 需求动机 | Motivation (必填 | Required)
<!---为什么需要这个功能?| Why do you need this feature?-->
### 期望设计 | Expected Design (可选 | Optional)
<!---描述或画出你期望的界面或交互 | Describe or sketch the expected UI/UX-->
### 其他补充信息 | Additional Info (可选 | Optional)
<!---其他补充说明或建议 | Any other context or suggestions-->
+96 -25
View File
@@ -1,35 +1,106 @@
name: .NET Build
name: .NET Build & Package
on:
push:
branches: [ main,beta ]
pull_request:
branches: [ main ]
branches: [ net6 ]
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}-${{ github.head_ref || github.sha }}
cancel-in-progress: true
permissions:
contents: read
jobs:
build:
build-and-package:
name: Build & Package
runs-on: windows-latest
strategy:
fail-fast: false
matrix:
architecture: [AnyCPU, x86]
steps:
- uses: actions/checkout@v4.2.2
- name: Setup MSbuild
uses: microsoft/setup-msbuild@v2
- name: Setup NuGet
uses: NuGet/setup-nuget@v2.0.1
- name: Checkout code
uses: actions/checkout@v6
with:
fetch-depth: 1
- name: Restore NuGet Packages
run: nuget restore "Ink Canvas.sln"
- name: Setup MSBuild
uses: microsoft/setup-msbuild@v3
- name: Setup dotnet
uses: actions/setup-dotnet@v5
with:
cache: true
cache-dependency-path: '**/packages.lock.json'
- name: Restore Package
run: dotnet restore "Ink Canvas.sln" --locked-mode
- name: Build the Solution
env:
DLASS_SENTRY_DSN: ${{ secrets.DLASS_SENTRY_DSN }}
run: msbuild /p:platform="${{ matrix.architecture }}" /p:configuration="Debug" /p:GitFlow="$GITFLOW" "Ink Canvas/InkCanvasForClass.csproj" /m /p:UseMultiToolTask=true /p:EnforceProcessCountAcrossBuilds=true /verbosity:minimal -maxcpucount /p:RunAnalyzers=false
- name: Build the Solution
run: |
msbuild -t:restore /p:GitFlow="Github Action"
msbuild /p:platform="Any CPU" /p:configuration="Release" /p:GitFlow="Github Action" "Ink Canvas/InkCanvasForClass.csproj"
- name: Check if exe file is generated
id: check-exe
run: |
$exePath = "Ink Canvas\bin\Debug\${{ matrix.architecture }}\net6.0-windows10.0.19041.0\InkCanvasForClass.exe"
if (Test-Path $exePath) {
echo "build_success=true" >> $env:GITHUB_OUTPUT
} else {
echo "build_success=false" >> $env:GITHUB_OUTPUT
if ("${{ github.event_name }}" -eq "workflow_dispatch") {
exit 1
}
}
- name: Upload to artifact
uses: actions/upload-artifact@v4.5.0
with:
name: InkCanvasForClass
path: "Ink Canvas/bin/Any CPU/Release/net472/"
- name: Create Package (if build succeeded)
id: create-archive
if: steps.check-exe.outputs.build_success == 'true'
env:
GITHUB_SHA: ${{ github.sha }}
GITHUB_RUN_NUMBER: ${{ github.run_number }}
run: |
$shortSha = $env:GITHUB_SHA.Substring(0, 7)
$version = "debug-$shortSha-$env:GITHUB_RUN_NUMBER"
echo "archive_name=$version" >> $env:GITHUB_OUTPUT
- name: Upload Artifact (if build succeeded)
if: steps.check-exe.outputs.build_success == 'true'
uses: actions/upload-artifact@v7
with:
name: InkCanvasForClass.CE.debug.${{ matrix.architecture }}
path: "Ink Canvas/bin/Debug/${{ matrix.architecture }}/net6.0-windows10.0.19041.0/*"
- name: Create Summary
if: always()
shell: bash
run: |
echo "# Build Summary" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
if [ "${{ steps.check-exe.outputs.build_success }}" = "true" ]; then
echo "## ✅ Build Successful" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Commit:** \`${{ github.sha }}\`" >> $GITHUB_STEP_SUMMARY
echo "**Run:** #${{ github.run_number }}" >> $GITHUB_STEP_SUMMARY
echo "**Version:** ${{ steps.create-archive.outputs.archive_name }}" >> $GITHUB_STEP_SUMMARY
echo "**Architecture:** ${{ matrix.architecture }}" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "[Download Artifact](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "[Nightly.link Download](https://nightly.link/${{ github.repository }}/actions/runs/${{ github.run_id }}/InkCanvasForClass.CE.debug.${{ matrix.architecture }}.zip) \([GhProxy Fastly Mirror](https://cdn.gh-proxy.com/nightly.link/${{ github.repository }}/actions/runs/${{ github.run_id }}/InkCanvasForClass.CE.debug.${{ matrix.architecture }}.zip) / [GhProxy Mirror](https://gh-proxy.com/nightly.link/${{ github.repository }}/actions/runs/${{ github.run_id }}/InkCanvasForClass.CE.debug.${{ matrix.architecture }}.zip)\)" >> $GITHUB_STEP_SUMMARY
else
echo "## ❌ Build Failed" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Event:** ${{ github.event_name }} (${{ github.event.action || 'N/A' }})" >> $GITHUB_STEP_SUMMARY
echo "**Commit:** \`${{ github.sha }}\`" >> $GITHUB_STEP_SUMMARY
echo "**Architecture:** ${{ matrix.architecture }}" >> $GITHUB_STEP_SUMMARY
echo "**Run:** #${{ github.run_number }}" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "Check build logs for details." >> $GITHUB_STEP_SUMMARY
fi
+104
View File
@@ -0,0 +1,104 @@
name: PR Check
on:
pull_request:
types: [opened, synchronize]
branches: [ main, net6 ]
paths-ignore:
- '**/*.md'
concurrency:
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}-${{ github.head_ref || github.sha }}
cancel-in-progress: true
permissions:
contents: read
jobs:
build-and-package:
name: Build & Package
runs-on: windows-latest
strategy:
fail-fast: false
matrix:
architecture: [AnyCPU, x86]
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
fetch-depth: 1
- name: Setup MSBuild
uses: microsoft/setup-msbuild@v3
- name: Setup dotnet
uses: actions/setup-dotnet@v5
- name: Restore Package
run: dotnet restore "Ink Canvas.sln" --locked-mode
- name: Build the Solution
run: msbuild /p:platform="${{ matrix.architecture }}" /p:configuration="Debug" /p:GitFlow="$GITFLOW" "Ink Canvas/InkCanvasForClass.csproj" /m /p:UseMultiToolTask=true /p:EnforceProcessCountAcrossBuilds=true /verbosity:minimal -maxcpucount /p:RunAnalyzers=false
- name: Check if exe file is generated
id: check-exe
run: |
$exePath = "Ink Canvas\bin\Debug\${{ matrix.architecture }}\net6.0-windows10.0.19041.0\InkCanvasForClass.exe"
if (Test-Path $exePath) {
echo "build_success=true" >> $env:GITHUB_OUTPUT
} else {
echo "build_success=false" >> $env:GITHUB_OUTPUT
if ("${{ github.event_name }}" -eq "workflow_dispatch") {
exit 1
}
}
- name: Create Package (if build succeeded)
id: create-archive
if: steps.check-exe.outputs.build_success == 'true'
env:
GITHUB_SHA: ${{ github.sha }}
GITHUB_RUN_NUMBER: ${{ github.run_number }}
run: |
$shortSha = $env:GITHUB_SHA.Substring(0, 7)
$version = "debug-$shortSha-$env:GITHUB_RUN_NUMBER"
echo "archive_name=$version" >> $env:GITHUB_OUTPUT
- name: Upload Artifact (if build succeeded)
if: steps.check-exe.outputs.build_success == 'true'
uses: actions/upload-artifact@v7
with:
name: InkCanvasForClass.CE.debug.${{ matrix.architecture }}
path: "Ink Canvas/bin/Debug/${{ matrix.architecture }}/net6.0-windows10.0.19041.0/*"
- name: Create Summary
if: always()
shell: bash
run: |
echo "# Build Summary" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
if [ "${{ steps.check-exe.outputs.build_success }}" = "true" ]; then
echo "## ✅ Build Successful" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Commit:** \`${{ github.sha }}\`" >> $GITHUB_STEP_SUMMARY
echo "**Run:** #${{ github.run_number }}" >> $GITHUB_STEP_SUMMARY
echo "**Version:** ${{ steps.create-archive.outputs.archive_name }}" >> $GITHUB_STEP_SUMMARY
echo "**Architecture:** ${{ matrix.architecture }}" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "[Download Artifact](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "[Nightly.link Download](https://nightly.link/${{ github.repository }}/actions/runs/${{ github.run_id }}/InkCanvasForClass.CE.debug.${{ matrix.architecture }}.zip) \([GhProxy Fastly Mirror](https://cdn.gh-proxy.com/nightly.link/${{ github.repository }}/actions/runs/${{ github.run_id }}/InkCanvasForClass.CE.debug.${{ matrix.architecture }}.zip) / [GhProxy Mirror](https://gh-proxy.com/nightly.link/${{ github.repository }}/actions/runs/${{ github.run_id }}/InkCanvasForClass.CE.debug.${{ matrix.architecture }}.zip)\)" >> $GITHUB_STEP_SUMMARY
else
echo "## ❌ Build Failed" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Event:** ${{ github.event_name }} (${{ github.event.action || 'N/A' }})" >> $GITHUB_STEP_SUMMARY
echo "**Commit:** \`${{ github.sha }}\`" >> $GITHUB_STEP_SUMMARY
echo "**Architecture:** ${{ matrix.architecture }}" >> $GITHUB_STEP_SUMMARY
echo "**Run:** #${{ github.run_number }}" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "Check build logs for details." >> $GITHUB_STEP_SUMMARY
fi
+754
View File
@@ -0,0 +1,754 @@
name: Pre-release and Changelog
on:
push:
tags:
- '*'
workflow_dispatch:
inputs:
version_type:
description: 'Version bump type'
required: true
default: 'patch'
type: choice
options:
- patch
- minor
- major
- build
prerelease:
description: 'Create as pre-release'
required: true
default: true
type: boolean
draft:
description: 'Create as draft release'
required: true
default: false
type: boolean
concurrency:
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}-${{ github.sha }}
cancel-in-progress: true
jobs:
prepare:
runs-on: windows-latest
outputs:
tag_name: ${{ steps.get_tag.outputs.tag_name }}
version: ${{ steps.get_tag.outputs.version }}
is_prerelease: ${{ steps.release_type.outputs.is_prerelease }}
changelog: ${{ steps.read_changelog.outputs.changelog }}
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
fetch-depth: 0
fetch-tags: true
# ========== 获取当前版本 ==========
- name: Get current version from Git tag
id: get_version
run: |
# 获取最新的tag
$latestTag = git describe --tags --abbrev=0 2>$null
if ($latestTag) {
$version = $latestTag
echo "Found latest tag: $latestTag"
} else {
# 如果没有tag,使用默认值
$version = "1.0.0.0"
echo "No tag found, using default version: $version"
}
echo "current_version=$version" >> $env:GITHUB_OUTPUT
echo "Current version: $version"
# ========== 处理版本号和标签名 ==========
- name: Get tag name and version
id: get_tag
run: |
if ("${{ github.event_name }}" -eq "push") {
# 从 push tag 事件获取原始标签名
$tagName = "${{ github.ref }}".Replace("refs/tags/", "")
$cleanVersion = $tagName
echo "tag_name=$tagName" >> $env:GITHUB_OUTPUT
echo "version=$cleanVersion" >> $env:GITHUB_OUTPUT
echo "Using pushed tag: $tagName, version: $cleanVersion"
} else {
# 从 workflow_dispatch 计算新版本(4位格式)
$currentVersion = "${{ steps.get_version.outputs.current_version }}"
$versionParts = $currentVersion.Split('.')
# 确保版本号格式正确(至少4部分)
if ($versionParts.Length -ge 4) {
$major = [int]$versionParts[0]
$minor = [int]$versionParts[1]
$patch = [int]$versionParts[2]
$build = [int]$versionParts[3]
} else {
# 如果版本号格式不正确,补充为4位
if ($versionParts.Length -ge 3) {
$major = [int]$versionParts[0]
$minor = [int]$versionParts[1]
$patch = [int]$versionParts[2]
$build = 0
} else {
# 如果版本号格式不正确,抛出错误
echo "Error: Invalid version format. Expected format: x.y.z.w (e.g., 1.7.18.0)"
exit 1
}
}
$versionType = "${{ github.event.inputs.version_type }}"
$isPrerelease = "${{ github.event.inputs.prerelease }}" -eq "true"
switch ($versionType) {
"major" {
$major++
$minor = 0
$patch = 0
$build = 0
}
"minor" {
$minor++
$patch = 0
$build = 0
}
"patch" {
$patch++
$build = 0
}
"build" {
$build++
}
}
# 生成新版本号(4位格式,如1.7.18.0)
$newVersion = "$major.$minor.$patch.$build"
# 根据是否为预发布决定版本号最后一位
# 如果是预发布,确保最后一位不为0(使用1)
if ($isPrerelease -and $build -eq 0) {
$build = 1
$newVersion = "$major.$minor.$patch.$build"
}
$tagName = $newVersion
echo "tag_name=$tagName" >> $env:GITHUB_OUTPUT
echo "version=$newVersion" >> $env:GITHUB_OUTPUT
echo "New tag: $tagName, version: $newVersion"
}
- name: Determine release type
id: release_type
run: |
if ("${{ github.event_name }}" -eq "push") {
# 根据版本号最后一位确定是否为预发布版本
# 最后一位为0表示正式版本,非0表示预发布版本
$version = "${{ steps.get_tag.outputs.version }}"
$versionParts = $version.Split('.')
if ($versionParts.Length -ge 4) {
$build = [int]$versionParts[3]
if ($build -eq 0) {
echo "is_prerelease=false" >> $env:GITHUB_OUTPUT
echo "This is a release"
} else {
echo "is_prerelease=true" >> $env:GITHUB_OUTPUT
echo "This is a pre-release (beta)"
}
} else {
echo "is_prerelease=false" >> $env:GITHUB_OUTPUT
echo "This is a release (invalid version format)"
}
} else {
# workflow_dispatch 方式
echo "is_prerelease=${{ github.event.inputs.prerelease }}" >> $env:GITHUB_OUTPUT
}
# ========== 使用 git-cliff 生成变更日志 ==========
- name: Generate changelog with git-cliff (for pushed tag)
if: github.event_name == 'push'
id: git_cliff_tag
uses: orhun/git-cliff-action@v4
with:
config: build/cliff.toml # 使用项目build目录的 cliff.toml 配置
args: --latest --tag ${{ steps.get_tag.outputs.tag_name }} --output CHANGELOG.md
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Generate changelog with git-cliff (for workflow_dispatch)
if: github.event_name == 'workflow_dispatch'
id: git_cliff_unreleased
uses: orhun/git-cliff-action@v4
with:
config: build/cliff.toml
args: --unreleased --tag ${{ steps.get_tag.outputs.tag_name }} --output CHANGELOG.md
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Read changelog content
id: read_changelog
run: |
$changelogContent = Get-Content -Path CHANGELOG.md -Raw
echo "changelog<<EOF" >> $env:GITHUB_OUTPUT
echo $changelogContent >> $env:GITHUB_OUTPUT
echo "EOF" >> $env:GITHUB_OUTPUT
build:
needs: prepare
if: success()
runs-on: windows-latest
strategy:
fail-fast: false
matrix:
architecture: [AnyCPU, x86]
outputs:
archive_name: ${{ steps.create_archive.outputs.archive_name }}
zip_size: ${{ steps.calculate_size.outputs.zip_size }}
installer_size: ${{ steps.calculate_installer_size.outputs.installer_size }}
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
fetch-depth: 1
- name: Setup MSBuild
uses: microsoft/setup-msbuild@v3
- name: Setup dotnet
uses: actions/setup-dotnet@v5
with:
cache: true
cache-dependency-path: '**/packages.lock.json'
- name: Restore Package
run: dotnet restore "Ink Canvas.sln" --locked-mode
- name: Build the Solution (Release)
env:
DLASS_SENTRY_DSN: ${{ secrets.DLASS_SENTRY_DSN }}
run: |
msbuild /p:platform="${{ matrix.architecture }}" /p:configuration="Release" /p:GitFlow="Github Action" "Ink Canvas/InkCanvasForClass.csproj" /m /p:UseMultiToolTask=true /p:EnforceProcessCountAcrossBuilds=true /verbosity:minimal -maxcpucount /p:RunAnalyzers=false
- name: Check if exe file is generated
id: check-exe
run: |
$exePath = "Ink Canvas\bin\Release\${{ matrix.architecture }}\net6.0-windows10.0.19041.0\InkCanvasForClass.exe"
if (Test-Path $exePath) {
echo "build_success=true" >> $env:GITHUB_OUTPUT
} else {
echo "build_success=false" >> $env:GITHUB_OUTPUT
exit 1
}
- name: Install Inno Setup Unofficial Language Files
if: steps.check-exe.outputs.build_success == 'true'
run: |
# 创建临时目录用于下载文件
New-Item -ItemType Directory -Path "temp_lang" -Force
# 下载英语英国版语言文件
Invoke-WebRequest -Uri "https://github.com/jrsoftware/issrc/raw/refs/heads/main/Files/Languages/Unofficial/EnglishBritish.isl" -OutFile "temp_lang/EnglishBritish.isl"
# 下载简体中文版语言文件
Invoke-WebRequest -Uri "https://github.com/jrsoftware/issrc/raw/refs/heads/main/Files/Languages/Unofficial/ChineseSimplified.isl" -OutFile "temp_lang/ChineseSimplified.isl"
# 将文件移动到 Inno Setup 的语言目录
Move-Item -Path "temp_lang/EnglishBritish.isl" -Destination "C:/Program Files (x86)/Inno Setup 6/Languages/EnglishBritish.isl" -Force
Move-Item -Path "temp_lang/ChineseSimplified.isl" -Destination "C:/Program Files (x86)/Inno Setup 6/Languages/ChineseSimplified.isl" -Force
# 清理临时目录
Remove-Item -Path "temp_lang" -Recurse -Force
- name: Create Release Archive
id: create_archive
if: steps.check-exe.outputs.build_success == 'true'
run: |
$version = "${{ needs.prepare.outputs.version }}"
$architecture = "${{ matrix.architecture }}"
# 根据架构生成文件名后缀
if ($architecture -eq "AnyCPU") {
$suffix = "-x64"
} else {
$suffix = ""
}
$archiveName = "InkCanvasForClass.CE.$version$suffix.zip"
# 创建发布目录
New-Item -ItemType Directory -Path "release" -Force
# 复制发布文件(使用架构特定的路径)
Copy-Item "Ink Canvas\bin\Release\$architecture\net6.0-windows10.0.19041.0\*" "release/" -Recurse -Force
# 创建压缩包
Compress-Archive -Path "release/*" -DestinationPath $archiveName -Force
echo "archive_name=$archiveName" >> $env:GITHUB_OUTPUT
- name: Prepare Inno Setup script
if: steps.check-exe.outputs.build_success == 'true'
run: |
$version = "${{ needs.prepare.outputs.version }}"
$architecture = "${{ matrix.architecture }}"
# 更新 ISS 文件中的版本信息
$issPath = "build/InkCanvasForClass CE.iss"
$issContent = Get-Content -Path $issPath -Raw
# 替换版本信息
$issContent = $issContent -replace '#define MyAppVersion ".*"', "#define MyAppVersion `"$version`""
# 替换源文件路径为相对路径(考虑到ISS文件在build目录下,需要返回上级目录)
$issContent = $issContent -replace 'Source: ".*\\{#MyAppExeName}";', 'Source: "../release/{#MyAppExeName}";'
$issContent = $issContent -replace 'Source: ".*\\InkCanvasForClass.exe.config";', 'Source: "../release/InkCanvasForClass.exe.config";'
# 更新输出目录为当前目录
$issContent = $issContent -replace 'OutputDir=.*', 'OutputDir=.'
# 更新默认安装目录
$issContent = $issContent -replace 'DefaultDirName=.*', 'DefaultDirName={autopf}/{#MyAppName}'
# 更新许可证文件路径为相对路径(考虑到ISS文件在build目录下,需要返回上级目录)
$issContent = $issContent -replace 'LicenseFile=.*', 'LicenseFile=../LICENSE'
# 保存修改后的 ISS 文件
$issContent | Set-Content -Path $issPath -Encoding UTF8
- name: Build MSI installer with Inno Setup
if: steps.check-exe.outputs.build_success == 'true'
uses: Minionguyjpro/Inno-Setup-Action@v1.2.7
with:
path: build/InkCanvasForClass CE.iss
options: /O.
- name: Rename installer file
if: steps.check-exe.outputs.build_success == 'true'
run: |
$version = "${{ needs.prepare.outputs.version }}"
$architecture = "${{ matrix.architecture }}"
# 根据架构生成文件名后缀
if ($architecture -eq "AnyCPU") {
$suffix = "-x64"
} else {
$suffix = ""
}
$setupFile = "InkCanvasForClass CE Setup.exe"
$newSetupName = "InkCanvasForClass.CE.$version$suffix.Setup.exe"
if (Test-Path $setupFile) {
Rename-Item -Path $setupFile -NewName $newSetupName
} else {
Write-Error "Setup file not found: $setupFile"
}
- name: Calculate archive size
id: calculate_size
if: steps.check-exe.outputs.build_success == 'true'
run: |
$version = "${{ needs.prepare.outputs.version }}"
$architecture = "${{ matrix.architecture }}"
# 根据架构生成文件名后缀
if ($architecture -eq "AnyCPU") {
$suffix = "-x64"
} else {
$suffix = ""
}
$archiveName = "InkCanvasForClass.CE.$version$suffix.zip"
# 获取文件大小(字节)
$fileSize = (Get-Item $archiveName).Length
echo "zip_size=$fileSize" >> $env:GITHUB_OUTPUT
- name: Calculate installer size
id: calculate_installer_size
if: steps.check-exe.outputs.build_success == 'true'
run: |
$version = "${{ needs.prepare.outputs.version }}"
$architecture = "${{ matrix.architecture }}"
# 根据架构生成文件名后缀
if ($architecture -eq "AnyCPU") {
$suffix = "-x64"
} else {
$suffix = ""
}
$installerName = "InkCanvasForClass.CE.$version$suffix.Setup.exe"
if (Test-Path $installerName) {
# 获取文件大小(字节)
$fileSize = (Get-Item $installerName).Length
echo "installer_size=$fileSize" >> $env:GITHUB_OUTPUT
} else {
Write-Error "Installer file not found: $installerName"
}
- name: Upload Build Artifacts
if: steps.check-exe.outputs.build_success == 'true'
run: |
$version = "${{ needs.prepare.outputs.version }}"
$architecture = "${{ matrix.architecture }}"
# 根据架构生成文件名后缀
if ($architecture -eq "AnyCPU") {
$suffix = "-x64"
} else {
$suffix = ""
}
$zipFile = "InkCanvasForClass.CE.$version$suffix.zip"
$setupFile = "InkCanvasForClass.CE.$version$suffix.Setup.exe"
echo "zip_file=$zipFile" >> $env:GITHUB_OUTPUT
echo "setup_file=$setupFile" >> $env:GITHUB_OUTPUT
id: get_file_names
- name: Upload Build Artifacts
if: steps.check-exe.outputs.build_success == 'true'
uses: actions/upload-artifact@v7
with:
name: build-files-${{ needs.prepare.outputs.version }}-${{ matrix.architecture }}
path: |
${{ steps.get_file_names.outputs.zip_file }}
${{ steps.get_file_names.outputs.setup_file }}
- name: Create Build Summary
if: always()
shell: bash
run: |
echo "# Release Build Summary" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
if [ "${{ steps.check-exe.outputs.build_success }}" = "true" ]; then
echo "## ✅ Release Build Successful" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Version:** ${{ needs.prepare.outputs.version }}" >> $GITHUB_STEP_SUMMARY
echo "**Tag:** \`${{ needs.prepare.outputs.tag_name }}\`" >> $GITHUB_STEP_SUMMARY
echo "**Release Type:** ${{ needs.prepare.outputs.is_prerelease == 'true' && 'Pre-release' || 'Release' }}" >> $GITHUB_STEP_SUMMARY
echo "**Architecture:** ${{ matrix.architecture }}" >> $GITHUB_STEP_SUMMARY
echo "**Commit:** \`${{ github.sha }}\`" >> $GITHUB_STEP_SUMMARY
echo "**Run:** #${{ github.run_number }}" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
if [ -n "${{ steps.calculate_size.outputs.zip_size }}" ]; then
echo "**Archive Size:** ${{ steps.calculate_size.outputs.zip_size }} bytes" >> $GITHUB_STEP_SUMMARY
fi
if [ -n "${{ steps.calculate_installer_size.outputs.installer_size }}" ]; then
echo "**Installer Size:** ${{ steps.calculate_installer_size.outputs.installer_size }} bytes" >> $GITHUB_STEP_SUMMARY
fi
else
echo "## ❌ Release Build Failed" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Version:** ${{ needs.prepare.outputs.version }}" >> $GITHUB_STEP_SUMMARY
echo "**Tag:** \`${{ needs.prepare.outputs.tag_name }}\`" >> $GITHUB_STEP_SUMMARY
echo "**Architecture:** ${{ matrix.architecture }}" >> $GITHUB_STEP_SUMMARY
echo "**Event:** ${{ github.event_name }} (${{ github.event.action || 'N/A' }})" >> $GITHUB_STEP_SUMMARY
echo "**Commit:** \`${{ github.sha }}\`" >> $GITHUB_STEP_SUMMARY
echo "**Run:** #${{ github.run_number }}" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "Check build logs for details." >> $GITHUB_STEP_SUMMARY
fi
sign:
needs: [prepare, build]
if: success()
runs-on: ubuntu-latest
permissions:
contents: write
id-token: write
steps:
- name: Download Build Artifacts
uses: actions/download-artifact@v8
with:
pattern: build-files-${{ needs.prepare.outputs.version }}-*
merge-multiple: false
- name: Setup Python
uses: actions/setup-python@v6
with:
python-version: '3.14'
- name: Sign release artifacts with sigstore-python
uses: sigstore/gh-action-sigstore-python@v3.3.0
with:
inputs: |
build-files-${{ needs.prepare.outputs.version }}-AnyCPU/*.zip
build-files-${{ needs.prepare.outputs.version }}-AnyCPU/*.exe
build-files-${{ needs.prepare.outputs.version }}-x86/*.zip
build-files-${{ needs.prepare.outputs.version }}-x86/*.exe
release-signing-artifacts: true
upload-signing-artifacts: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload Signed Artifacts
uses: actions/upload-artifact@v7
with:
name: signed-files-${{ needs.prepare.outputs.version }}
path: |
build-files-${{ needs.prepare.outputs.version }}-AnyCPU/*.sigstore.json
build-files-${{ needs.prepare.outputs.version }}-x86/*.sigstore.json
release:
needs: [prepare, build, sign]
if: success()
runs-on: ubuntu-slim
permissions:
contents: write
outputs:
enhanced_changelog: ${{steps.enhanced_changelog.outputs.enhanced_changelog}}
steps:
- name: Download Build Artifacts
uses: actions/download-artifact@v8
with:
pattern: build-files-${{ needs.prepare.outputs.version }}-*
merge-multiple: true
- name: Download Signed Artifacts (if exists)
uses: actions/download-artifact@v8
with:
name: signed-files-${{ needs.prepare.outputs.version }}
continue-on-error: true
- name: Create enhanced changelog with file table
id: enhanced_changelog
run: |
version='${{ needs.prepare.outputs.version }}'
# 读取git-cliff生成的changelog内容
originalChangelog='${{ needs.prepare.outputs.changelog }}'
# 检查是否为预发布版本,如果是则添加警告提示
if [ '${{ needs.prepare.outputs.is_prerelease }}' = "true" ]; then
warningText=$'\n> [!CAUTION]\n'
warningText+=$'> **注意:此版本为预览或测试版**\n'
warningText+=$'> \n'
warningText+=$'> 请注意,这是一个预览/测试版本,使用时可能出现BUG,常规用户建议使用预览版或正式版\n\n'
originalChangelog="${warningText}${originalChangelog}"
fi
# 构建文件信息表格
fileTable=$'\n## 文件信息 (File Information)\n'
fileTable+=$'| 文件名 | 大小 |\n'
fileTable+=$'|--------|------|\n'
# AnyCPU (x64) 架构文件
if [ -f "InkCanvasForClass.CE.$version-x64.zip" ]; then
zipSize=$(wc -c < "InkCanvasForClass.CE.$version-x64.zip")
fileTable+=$'| InkCanvasForClass.CE.'"$version"'-x64.zip | '"$zipSize"' bytes |\n'
fi
if [ -f "InkCanvasForClass.CE.$version-x64.Setup.exe" ]; then
installerSize=$(wc -c < "InkCanvasForClass.CE.$version-x64.Setup.exe")
fileTable+=$'| InkCanvasForClass.CE.'"$version"'-x64.Setup.exe | '"$installerSize"' bytes |\n'
fi
if [ -f "InkCanvasForClass.CE.$version-x64.zip.sigstore.json" ]; then
sigstoreSize=$(wc -c < "InkCanvasForClass.CE.$version-x64.zip.sigstore.json")
fileTable+=$'| InkCanvasForClass.CE.'"$version"'-x64.zip.sigstore.json | '"$sigstoreSize"' bytes |\n'
fi
if [ -f "InkCanvasForClass.CE.$version-x64.Setup.exe.sigstore.json" ]; then
sigstoreSize=$(wc -c < "InkCanvasForClass.CE.$version-x64.Setup.exe.sigstore.json")
fileTable+=$'| InkCanvasForClass.CE.'"$version"'-x64.Setup.exe.sigstore.json | '"$sigstoreSize"' bytes |\n'
fi
# x86 架构文件
if [ -f "InkCanvasForClass.CE.$version.zip" ]; then
zipSize=$(wc -c < "InkCanvasForClass.CE.$version.zip")
fileTable+=$'| InkCanvasForClass.CE.'"$version"'.zip | '"$zipSize"' bytes |\n'
fi
if [ -f "InkCanvasForClass.CE.$version.Setup.exe" ]; then
installerSize=$(wc -c < "InkCanvasForClass.CE.$version.Setup.exe")
fileTable+=$'| InkCanvasForClass.CE.'"$version"'.Setup.exe | '"$installerSize"' bytes |\n'
fi
if [ -f "InkCanvasForClass.CE.$version.zip.sigstore.json" ]; then
sigstoreSize=$(wc -c < "InkCanvasForClass.CE.$version.zip.sigstore.json")
fileTable+=$'| InkCanvasForClass.CE.'"$version"'.zip.sigstore.json | '"$sigstoreSize"' bytes |\n'
fi
if [ -f "InkCanvasForClass.CE.$version.Setup.exe.sigstore.json" ]; then
sigstoreSize=$(wc -c < "InkCanvasForClass.CE.$version.Setup.exe.sigstore.json")
fileTable+=$'| InkCanvasForClass.CE.'"$version"'.Setup.exe.sigstore.json | '"$sigstoreSize"' bytes |\n'
fi
fileTable+=$'\n*文件大小信息由GitHub Actions自动生成*\n'
# 将表格附加到原始changelog
enhancedChangelog="${originalChangelog}${fileTable}"
# 输出增强版changelog内容
echo "enhanced_changelog<<EOF" >> $GITHUB_OUTPUT
echo "$enhancedChangelog" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
echo "Enhanced changelog created with file information table"
- name: Display Release Info
run: |
echo "=== Creating Release ==="
echo "Version: ${{ needs.prepare.outputs.version }}"
echo "Tag: ${{ needs.prepare.outputs.tag_name }}"
echo "Pre-release: ${{ needs.prepare.outputs.is_prerelease }}"
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ needs.prepare.outputs.tag_name }}
name: ICC CE ${{ needs.prepare.outputs.version }}
body: |
${{ steps.enhanced_changelog.outputs.enhanced_changelog }}
draft: ${{ github.event.inputs.draft || false }}
prerelease: ${{ needs.prepare.outputs.is_prerelease == 'true' }}
files: |
InkCanvasForClass.CE.${{ needs.prepare.outputs.version }}-x64.zip
InkCanvasForClass.CE.${{ needs.prepare.outputs.version }}-x64.Setup.exe
InkCanvasForClass.CE.${{ needs.prepare.outputs.version }}-x64.zip.sigstore.json
InkCanvasForClass.CE.${{ needs.prepare.outputs.version }}-x64.Setup.exe.sigstore.json
InkCanvasForClass.CE.${{ needs.prepare.outputs.version }}.zip
InkCanvasForClass.CE.${{ needs.prepare.outputs.version }}.Setup.exe
InkCanvasForClass.CE.${{ needs.prepare.outputs.version }}.zip.sigstore.json
InkCanvasForClass.CE.${{ needs.prepare.outputs.version }}.Setup.exe.sigstore.json
fail_on_unmatched_files: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
post_release:
needs: [prepare, release]
if: success() && github.event.inputs.draft != 'true'
runs-on: ubuntu-slim
permissions:
id-token: write
contents: write
steps:
- name: Download Build Artifacts
uses: actions/download-artifact@v8
with:
pattern: build-files-${{ needs.prepare.outputs.version }}-*
merge-multiple: true
- name: Get beta token
uses: octo-sts/action@main
id: octo-sts-beta
with:
scope: InkCanvasForClass/community-beta
identity: repo-sync
- name: Get download token
uses: octo-sts/action@main
id: octo-sts-downloads
with:
scope: InkCanvasForClass/downloads
identity: repo-sync
- name: Sync downloads repos(Universal)
env:
GITHUB_TOKEN: ${{ steps.octo-sts-downloads.outputs.token }}
run: |
set -e
git config --global user.name "github-actions[bot]"
git config --global user.email "github-actions[bot]@users.noreply.github.com"
REPO_DIR=$(mktemp -d)
git clone --depth 1 --filter=blob:none --branch main https://x-access-token:${{ steps.octo-sts-downloads.outputs.token }}@github.com/InkCanvasForClass/downloads.git $REPO_DIR
cd $REPO_DIR
IS_PRERELEASE="${{ needs.prepare.outputs.is_prerelease }}"
VERSION="${{ needs.prepare.outputs.version }}"
X64_ZIP_FILE="$GITHUB_WORKSPACE/InkCanvasForClass.CE.$VERSION-x64.zip"
X86_ZIP_FILE="$GITHUB_WORKSPACE/InkCanvasForClass.CE.$VERSION.zip"
if [ "$IS_PRERELEASE" == "true" ]; then
mkdir -p Beta
if [ -f "$X64_ZIP_FILE" ]; then
cp "$X64_ZIP_FILE" Beta/
git add Beta/InkCanvasForClass.CE.$VERSION-x64.zip
fi
if [ -f "$X86_ZIP_FILE" ]; then
cp "$X86_ZIP_FILE" Beta/
git add Beta/InkCanvasForClass.CE.$VERSION.zip
fi
git commit -m "Add $VERSION PreRelease"
else
mkdir -p Release Beta
if [ -f "$X64_ZIP_FILE" ]; then
cp "$X64_ZIP_FILE" Release/
cp "$X64_ZIP_FILE" Beta/
git add Release/InkCanvasForClass.CE.$VERSION-x64.zip Beta/InkCanvasForClass.CE.$VERSION-x64.zip
fi
if [ -f "$X86_ZIP_FILE" ]; then
cp "$X86_ZIP_FILE" Release/
cp "$X86_ZIP_FILE" Beta/
git add Release/InkCanvasForClass.CE.$VERSION.zip Beta/InkCanvasForClass.CE.$VERSION.zip
fi
git commit -m "Add $VERSION Release"
fi
git push origin main
- name: Update AutomaticUpdateVersionControl in beta repo
env:
GITHUB_TOKEN: ${{ steps.octo-sts-beta.outputs.token }}
run: |
CONTENT=$(echo -n "${{ needs.prepare.outputs.version }}" | base64 -w0)
SHA=$(gh api /repos/InkCanvasForClass/community-beta/contents/AutomaticUpdateVersionControl.txt --jq '.sha' 2>/dev/null || echo "")
gh api \
--method PUT \
/repos/InkCanvasForClass/community-beta/contents/AutomaticUpdateVersionControl.txt \
-f message="Update AutomaticUpdateVersionControl.txt" \
-f content="$CONTENT" \
-f branch="main" \
${SHA:+-f sha="$SHA"}
- name: Create GitHub Release on beta repo
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ needs.prepare.outputs.tag_name }}
name: ICC CE ${{ needs.prepare.outputs.version }}
body: |
${{ needs.release.outputs.enhanced_changelog }}
draft: false
prerelease: ${{ needs.prepare.outputs.is_prerelease == 'true' }}
files: |
InkCanvasForClass.CE.${{ needs.prepare.outputs.version }}-x64.zip
InkCanvasForClass.CE.${{ needs.prepare.outputs.version }}.zip
fail_on_unmatched_files: false
repository: "InkCanvasForClass/community-beta"
token: ${{ steps.octo-sts-beta.outputs.token }}
env:
GITHUB_TOKEN: ${{ steps.octo-sts-beta.outputs.token }}
- name: Update community repo AutomaticUpdateVersionControl
if: ${{needs.prepare.outputs.is_prerelease == 'false'}}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
CONTENT=$(echo -n "${{ needs.prepare.outputs.version }}" | base64 -w0)
SHA=$(gh api /repos/InkCanvasForClass/community/contents/AutomaticUpdateVersionControl.txt --jq '.sha' 2>/dev/null || echo "")
gh api \
--method PUT \
/repos/InkCanvasForClass/community/contents/AutomaticUpdateVersionControl.txt \
-f message="Update AutomaticUpdateVersionControl.txt" \
-f content="$CONTENT" \
-f branch="net6" \
${SHA:+-f sha="$SHA"}
+432 -3
View File
@@ -1,4 +1,433 @@
obj/
bin/
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##
## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore
# User-specific files
*.rsuser
*.suo
*.user
*.userosscache
*.sln.docstates
*.env
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Mono auto generated files
mono_crash.*
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
[Dd]ebug/x64/
[Dd]ebugPublic/x64/
[Rr]elease/x64/
[Rr]eleases/x64/
bin/x64/
obj/x64/
[Dd]ebug/x86/
[Dd]ebugPublic/x86/
[Rr]elease/x86/
[Rr]eleases/x86/
bin/x86/
obj/x86/
[Ww][Ii][Nn]32/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
[Aa][Rr][Mm]64[Ee][Cc]/
bld/
[Oo]bj/
[Oo]ut/
[Ll]og/
[Ll]ogs/
# Build results on 'Bin' directories
**/[Bb]in/*
# Uncomment if you have tasks that rely on *.refresh files to move binaries
# (https://github.com/github/gitignore/pull/3736)
#!**/[Bb]in/*.refresh
# Visual Studio 2015/2017 cache/options directory
.vs/
/Ink Canvas/obj
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
# Visual Studio 2017 auto generated files
Generated\ Files/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
*.trx
# NUnit
*.VisualState.xml
TestResult.xml
nunit-*.xml
# Approval Tests result files
*.received.*
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# Benchmark Results
BenchmarkDotNet.Artifacts/
# .NET Core
project.lock.json
project.fragment.lock.json
artifacts/
# ASP.NET Scaffolding
ScaffoldingReadMe.txt
# StyleCop
StyleCopReport.xml
# Files built by Visual Studio
*_i.c
*_p.c
*_h.h
*.ilk
*.meta
*.obj
*.idb
*.iobj
*.pch
*.pdb
*.ipdb
*.pgc
*.pgd
*.rsp
# but not Directory.Build.rsp, as it configures directory-level build defaults
!Directory.Build.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*_wpftmp.csproj
*.log
*.tlog
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb
# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap
# Visual Studio Trace Files
*.e2e
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# AxoCover is a Code Coverage Tool
.axoCover/*
!.axoCover/settings.json
# Coverlet is a free, cross platform Code Coverage Tool
coverage*.json
coverage*.xml
coverage*.info
# Visual Studio code coverage results
*.coverage
*.coveragexml
# NCrunch
_NCrunch_*
.NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# Note: Comment the next line if you want to checkin your web deploy settings,
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj
# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted
PublishScripts/
# NuGet Packages
*.nupkg
# NuGet Symbol Packages
*.snupkg
# The packages folder can be ignored because of Package Restore
**/[Pp]ackages/*
# except build/, which is used as an MSBuild target.
!**/[Pp]ackages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/[Pp]ackages/repositories.config
# NuGet v3's project.json files produces more ignorable files
*.nuget.props
*.nuget.targets
# Microsoft Azure Build Output
csx/
*.build.csdef
# Microsoft Azure Emulator
ecf/
rcf/
# Windows Store app package directories and files
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
*.appx
*.appxbundle
*.appxupload
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!?*.[Cc]ache/
# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
orleans.codegen.cs
# Including strong name files can present a security risk
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
#*.snk
# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
ServiceFabricBackup/
*.rptproj.bak
# SQL Server files
*.mdf
*.ldf
*.ndf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
*.rptproj.rsuser
*- [Bb]ackup.rdl
*- [Bb]ackup ([0-9]).rdl
*- [Bb]ackup ([0-9][0-9]).rdl
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
node_modules/
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw
# Visual Studio 6 workspace and project file (working project files containing files to include in project)
*.dsw
*.dsp
# Visual Studio 6 technical files
*.ncb
*.aps
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions
# Paket dependency manager
**/.paket/paket.exe
paket-files/
# FAKE - F# Make
**/.fake/
# CodeRush personal settings
**/.cr/personal
# Python Tools for Visual Studio (PTVS)
**/__pycache__/
*.pyc
# Cake - Uncomment if you are using it
#tools/**
#!tools/packages.config
# Tabs Studio
*.tss
# Telerik's JustMock configuration file
*.jmconfig
# BizTalk build output
*.btp.cs
*.btm.cs
*.odx.cs
*.xsd.cs
# OpenCover UI analysis results
OpenCover/
# Azure Stream Analytics local run output
ASALocalRun/
# MSBuild Binary and Structured Log
*.binlog
MSBuild_Logs/
# AWS SAM Build and Temporary Artifacts folder
.aws-sam
# NVidia Nsight GPU debugger configuration file
*.nvuser
# MFractors (Xamarin productivity tool) working folder
**/.mfractor/
# Local History for Visual Studio
**/.localhistory/
# Visual Studio History (VSHistory) files
.vshistory/
# BeatPulse healthcheck temp database
healthchecksdb
# Backup folder for Package Reference Convert tool in Visual Studio 2017
MigrationBackup/
# Ionide (cross platform F# VS Code tools) working folder
**/.ionide/
# Fody - auto-generated XML schema
FodyWeavers.xsd
# VS Code files for those working on multiple tools
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
!.vscode/*.code-snippets
# Local History for Visual Studio Code
.history/
# Built Visual Studio Code Extensions
*.vsix
# Windows Installer files from build outputs
*.cab
*.msi
*.msix
*.msm
*.msp
# Telemetry DSN configuration file (contains sensitive information)
telemetry_dsn.txt
**/telemetry_dsn.txt
.trae/skills/migrate-toggle-switch/SKILL.md
+1 -1
View File
@@ -1 +1 @@
1.7.10.0
1.7.18.0
+57 -13
View File
@@ -1,10 +1,14 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.5.33530.505
# Visual Studio Version 18
VisualStudioVersion = 18.4.11626.88 stable
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "InkCanvasForClass", "Ink Canvas\InkCanvasForClass.csproj", "{8D0EDFC7-F974-4571-BC49-6F3A6653FE81}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InkCanvas.PluginSdk", "InkCanvas.PluginSdk\InkCanvas.PluginSdk.csproj", "{6A0B1FE5-5D4A-EB5D-8C4F-A1F107FD7556}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InkCanvas.Controls", "InkCanvas.Controls\InkCanvas.Controls.csproj", "{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -23,22 +27,62 @@ Global
{8D0EDFC7-F974-4571-BC49-6F3A6653FE81}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8D0EDFC7-F974-4571-BC49-6F3A6653FE81}.Debug|ARM.ActiveCfg = Debug|Any CPU
{8D0EDFC7-F974-4571-BC49-6F3A6653FE81}.Debug|ARM.Build.0 = Debug|Any CPU
{8D0EDFC7-F974-4571-BC49-6F3A6653FE81}.Debug|ARM64.ActiveCfg = Debug|ARM64
{8D0EDFC7-F974-4571-BC49-6F3A6653FE81}.Debug|ARM64.Build.0 = Debug|ARM64
{8D0EDFC7-F974-4571-BC49-6F3A6653FE81}.Debug|ARM64.ActiveCfg = Debug|Any CPU
{8D0EDFC7-F974-4571-BC49-6F3A6653FE81}.Debug|ARM64.Build.0 = Debug|Any CPU
{8D0EDFC7-F974-4571-BC49-6F3A6653FE81}.Debug|x64.ActiveCfg = Debug|x64
{8D0EDFC7-F974-4571-BC49-6F3A6653FE81}.Debug|x64.Build.0 = Debug|x64
{8D0EDFC7-F974-4571-BC49-6F3A6653FE81}.Debug|x86.ActiveCfg = Debug|Any CPU
{8D0EDFC7-F974-4571-BC49-6F3A6653FE81}.Debug|x86.Build.0 = Debug|Any CPU
{8D0EDFC7-F974-4571-BC49-6F3A6653FE81}.Debug|x86.ActiveCfg = Debug|x86
{8D0EDFC7-F974-4571-BC49-6F3A6653FE81}.Debug|x86.Build.0 = Debug|x86
{8D0EDFC7-F974-4571-BC49-6F3A6653FE81}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8D0EDFC7-F974-4571-BC49-6F3A6653FE81}.Release|Any CPU.Build.0 = Release|Any CPU
{8D0EDFC7-F974-4571-BC49-6F3A6653FE81}.Release|ARM.ActiveCfg = Release|Any CPU
{8D0EDFC7-F974-4571-BC49-6F3A6653FE81}.Release|ARM.Build.0 = Release|Any CPU
{8D0EDFC7-F974-4571-BC49-6F3A6653FE81}.Release|ARM64.ActiveCfg = Release|ARM64
{8D0EDFC7-F974-4571-BC49-6F3A6653FE81}.Release|ARM64.Build.0 = Release|ARM64
{8D0EDFC7-F974-4571-BC49-6F3A6653FE81}.Release|x64.ActiveCfg = Release|x64
{8D0EDFC7-F974-4571-BC49-6F3A6653FE81}.Release|x64.Build.0 = Release|x64
{8D0EDFC7-F974-4571-BC49-6F3A6653FE81}.Release|x86.ActiveCfg = Release|x86
{8D0EDFC7-F974-4571-BC49-6F3A6653FE81}.Release|x86.Build.0 = Release|x86
{8D0EDFC7-F974-4571-BC49-6F3A6653FE81}.Release|ARM64.ActiveCfg = Release|Any CPU
{8D0EDFC7-F974-4571-BC49-6F3A6653FE81}.Release|ARM64.Build.0 = Release|Any CPU
{8D0EDFC7-F974-4571-BC49-6F3A6653FE81}.Release|x64.ActiveCfg = Release|Any CPU
{8D0EDFC7-F974-4571-BC49-6F3A6653FE81}.Release|x64.Build.0 = Release|Any CPU
{8D0EDFC7-F974-4571-BC49-6F3A6653FE81}.Release|x86.ActiveCfg = Release|Any CPU
{8D0EDFC7-F974-4571-BC49-6F3A6653FE81}.Release|x86.Build.0 = Release|Any CPU
{6A0B1FE5-5D4A-EB5D-8C4F-A1F107FD7556}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6A0B1FE5-5D4A-EB5D-8C4F-A1F107FD7556}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6A0B1FE5-5D4A-EB5D-8C4F-A1F107FD7556}.Debug|ARM.ActiveCfg = Debug|Any CPU
{6A0B1FE5-5D4A-EB5D-8C4F-A1F107FD7556}.Debug|ARM.Build.0 = Debug|Any CPU
{6A0B1FE5-5D4A-EB5D-8C4F-A1F107FD7556}.Debug|ARM64.ActiveCfg = Debug|Any CPU
{6A0B1FE5-5D4A-EB5D-8C4F-A1F107FD7556}.Debug|ARM64.Build.0 = Debug|Any CPU
{6A0B1FE5-5D4A-EB5D-8C4F-A1F107FD7556}.Debug|x64.ActiveCfg = Debug|Any CPU
{6A0B1FE5-5D4A-EB5D-8C4F-A1F107FD7556}.Debug|x64.Build.0 = Debug|Any CPU
{6A0B1FE5-5D4A-EB5D-8C4F-A1F107FD7556}.Debug|x86.ActiveCfg = Debug|Any CPU
{6A0B1FE5-5D4A-EB5D-8C4F-A1F107FD7556}.Debug|x86.Build.0 = Debug|Any CPU
{6A0B1FE5-5D4A-EB5D-8C4F-A1F107FD7556}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6A0B1FE5-5D4A-EB5D-8C4F-A1F107FD7556}.Release|Any CPU.Build.0 = Release|Any CPU
{6A0B1FE5-5D4A-EB5D-8C4F-A1F107FD7556}.Release|ARM.ActiveCfg = Release|Any CPU
{6A0B1FE5-5D4A-EB5D-8C4F-A1F107FD7556}.Release|ARM.Build.0 = Release|Any CPU
{6A0B1FE5-5D4A-EB5D-8C4F-A1F107FD7556}.Release|ARM64.ActiveCfg = Release|Any CPU
{6A0B1FE5-5D4A-EB5D-8C4F-A1F107FD7556}.Release|ARM64.Build.0 = Release|Any CPU
{6A0B1FE5-5D4A-EB5D-8C4F-A1F107FD7556}.Release|x64.ActiveCfg = Release|Any CPU
{6A0B1FE5-5D4A-EB5D-8C4F-A1F107FD7556}.Release|x64.Build.0 = Release|Any CPU
{6A0B1FE5-5D4A-EB5D-8C4F-A1F107FD7556}.Release|x86.ActiveCfg = Release|Any CPU
{6A0B1FE5-5D4A-EB5D-8C4F-A1F107FD7556}.Release|x86.Build.0 = Release|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|ARM.ActiveCfg = Debug|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|ARM.Build.0 = Debug|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|ARM64.ActiveCfg = Debug|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|ARM64.Build.0 = Debug|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|x64.ActiveCfg = Debug|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|x64.Build.0 = Debug|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|x86.ActiveCfg = Debug|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|x86.Build.0 = Debug|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.Build.0 = Release|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|ARM.ActiveCfg = Release|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|ARM.Build.0 = Release|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|ARM64.ActiveCfg = Release|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|ARM64.Build.0 = Release|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|x64.ActiveCfg = Release|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|x64.Build.0 = Release|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|x86.ActiveCfg = Release|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
-4
View File
@@ -1,4 +0,0 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:String x:Key="/Default/CodeInspection/PencilsConfiguration/ActualSeverity/@EntryValue">WARNING</s:String>
<s:String x:Key="/Default/Environment/Hierarchy/Build/BuildTool/CustomBuildToolPath/@EntryValue">C:\Program Files\Microsoft Visual Studio\2022\Community\MSBuild\Current\Bin\amd64\MSBuild.exe</s:String>
<s:Int64 x:Key="/Default/Environment/Hierarchy/Build/BuildTool/MsbuildVersion/@EntryValue">1114112</s:Int64></wpf:ResourceDictionary>
+62 -31
View File
@@ -1,9 +1,11 @@
<Application x:Class="Ink_Canvas.App"
<Application x:Class="Ink_Canvas.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:Ink_Canvas"
xmlns:tb="http://www.hardcodet.net/taskbar"
xmlns:props="clr-namespace:Ink_Canvas.Properties"
xmlns:tb="clr-namespace:H.NotifyIcon;assembly=H.NotifyIcon.Wpf"
xmlns:ui="http://schemas.inkore.net/lib/ui/wpf/modern"
xmlns:ikw="http://schemas.inkore.net/lib/ui/wpf"
>
<Application.Resources>
<ResourceDictionary>
@@ -13,9 +15,9 @@
<ContextMenu Opened="SysTrayMenu_Opened" Closed="SysTrayMenu_Closed" x:Shared="false" x:Key="SysTrayMenu" Padding="6" ui:ThemeManager.RequestedTheme="Light">
<MenuItem IsCheckable="True" IsChecked="False" Checked="HideICCMainWindowTrayIconMenuItem_Checked" Unchecked="HideICCMainWindowTrayIconMenuItem_UnChecked" Name="HideICCMainWindowTrayIconMenuItem">
<MenuItem.Header>
<ui:SimpleStackPanel Orientation="Horizontal" Margin="-4,0,0,0">
<ikw:SimpleStackPanel Orientation="Horizontal" Margin="-4,0,0,0">
<TextBlock Name="HideICCMainWindowTrayIconMenuItemHeaderText" FontSize="14" VerticalAlignment="Center" Foreground="#18181b" Text="隐藏ICC主窗口" />
</ui:SimpleStackPanel>
</ikw:SimpleStackPanel>
</MenuItem.Header>
<MenuItem.Icon>
<Image Width="28" Height="28" Margin="-2">
@@ -31,12 +33,42 @@
</Image>
</MenuItem.Icon>
</MenuItem>
<Separator Margin="0,3" />
<MenuItem>
<MenuItem Name="TempShowMainWindowTrayIconMenuItem" Click="TempShowMainWindowTrayIconMenuItem_Clicked">
<MenuItem.Header>
<ui:SimpleStackPanel Orientation="Horizontal" Margin="-4,0,0,0">
<ikw:SimpleStackPanel Orientation="Horizontal" Margin="-4,0,0,0">
<TextBlock FontSize="14" VerticalAlignment="Center" Foreground="#18181b" Text="{x:Static props:Strings.Tray_TempShowMainWindow}" />
</ikw:SimpleStackPanel>
</MenuItem.Header>
<MenuItem.Icon>
<Image Width="28" Height="28" Margin="-2">
<Image.Source>
<DrawingImage>
<DrawingImage.Drawing>
<DrawingGroup ClipGeometry="M0,0 V24 H24 V0 H0 Z">
<GeometryDrawing Brush="#27272a" Geometry="F0 M24,24z M0,0z M5,6C4.73478,6 4.48043,6.10536 4.29289,6.29289 4.10536,6.48043 4,6.73478 4,7L4,17C4,17.2652 4.10536,17.5196 4.29289,17.7071 4.48043,17.8946 4.73478,18 5,18L19,18C19.2652,18 19.5196,17.8946 19.7071,17.7071 19.8946,17.5196 20,17.2652 20,17L20,7C20,6.73478 19.8946,6.48043 19.7071,6.29289 19.5196,6.10536 19.2652,6 19,6L5,6z M2.87868,4.87868C3.44129,4.31607,4.20435,4,5,4L19,4C19.7957,4 20.5587,4.31607 21.1213,4.87868 21.6839,5.44129 22,6.20435 22,7L22,17C22,17.7957 21.6839,18.5587 21.1213,19.1213 20.5587,19.6839 19.7957,20 19,20L5,20C4.20435,20 3.44129,19.6839 2.87868,19.1213 2.31607,18.5587 2,17.7956 2,17L2,7C2,6.20435,2.31607,5.44129,2.87868,4.87868z M5,8C5,7.44772,5.44772,7,6,7L6.01,7C6.56228,7 7.01,7.44772 7.01,8 7.01,8.55228 6.56228,9 6.01,9L6,9C5.44772,9,5,8.55228,5,8z M9,7C8.44772,7 8,7.44772 8,8 8,8.55228 8.44772,9 9,9L9.01,9C9.56228,9 10.01,8.55228 10.01,8 10.01,7.44772 9.56228,7 9.01,7L9,7z" />
</DrawingGroup>
</DrawingImage.Drawing>
</DrawingImage>
</Image.Source>
</Image>
</MenuItem.Icon>
</MenuItem>
<MenuItem Name="OpenSettingsTrayIconMenuItem" Click="OpenSettingsTrayIconMenuItem_Clicked">
<MenuItem.Header>
<ikw:SimpleStackPanel Orientation="Horizontal" Margin="-4,0,0,0">
<TextBlock FontSize="14" VerticalAlignment="Center" Foreground="#18181b" Text="{x:Static props:Strings.Tray_OpenSettings}" />
</ikw:SimpleStackPanel>
</MenuItem.Header>
<MenuItem.Icon>
<Image Width="28" Height="28" Margin="-2" Source="/Resources/Icons-Fluent/ic_fluent_settings_24_regular.png" />
</MenuItem.Icon>
</MenuItem>
<Separator Margin="0,3" />
<MenuItem Name="DisableAllHotkeysMenuItem" Click="DisableAllHotkeysMenuItem_Clicked">
<MenuItem.Header>
<ikw:SimpleStackPanel Orientation="Horizontal" Margin="-4,0,0,0">
<TextBlock FontSize="14" VerticalAlignment="Center" Foreground="#18181b" Text="禁用所有快捷键" />
</ui:SimpleStackPanel>
</ikw:SimpleStackPanel>
</MenuItem.Header>
<MenuItem.Icon>
<Image Width="28" Height="28" Margin="-2">
@@ -54,9 +86,9 @@
</MenuItem>
<MenuItem Name="ForceFullScreenTrayIconMenuItem" Click="ForceFullScreenTrayIconMenuItem_Clicked">
<MenuItem.Header>
<ui:SimpleStackPanel Orientation="Horizontal" Margin="-4,0,0,0">
<ikw:SimpleStackPanel Orientation="Horizontal" Margin="-4,0,0,0">
<TextBlock FontSize="14" VerticalAlignment="Center" Foreground="#18181b" Text="强制全屏化" />
<ui:SimpleStackPanel Orientation="Horizontal" VerticalAlignment="Center" Spacing="4" Margin="16,0,0,0">
<ikw:SimpleStackPanel Orientation="Horizontal" VerticalAlignment="Center" Spacing="4" Margin="16,0,0,0">
<Border Padding="4,2,4,1" Background="#e4e4e7" BorderBrush="#a1a1aa" BorderThickness="1" CornerRadius="2.5">
<TextBlock FontSize="8" Foreground="#3f3f46" FontWeight="Bold" Text="CTRL" />
</Border>
@@ -64,8 +96,8 @@
<Border Padding="4,2,4,1" Background="#e4e4e7" BorderBrush="#a1a1aa" BorderThickness="1" CornerRadius="2.5">
<TextBlock FontSize="8" Foreground="#3f3f46" FontWeight="Bold" Text="F" />
</Border>
</ui:SimpleStackPanel>
</ui:SimpleStackPanel>
</ikw:SimpleStackPanel>
</ikw:SimpleStackPanel>
</MenuItem.Header>
<MenuItem.Icon>
<Image Width="28" Height="28" Margin="-2">
@@ -87,9 +119,9 @@
</MenuItem>
<MenuItem Name="FoldFloatingBarTrayIconMenuItem" Click="FoldFloatingBarTrayIconMenuItem_Clicked">
<MenuItem.Header>
<ui:SimpleStackPanel Orientation="Horizontal" Margin="-4,0,0,0">
<ikw:SimpleStackPanel Orientation="Horizontal" Margin="-4,0,0,0">
<TextBlock Name="FoldFloatingBarTrayIconMenuItemHeaderText" FontSize="14" VerticalAlignment="Center" Foreground="#18181b" Text="切换为收纳模式" />
<ui:SimpleStackPanel Orientation="Horizontal" VerticalAlignment="Center" Spacing="4" Margin="16,0,0,0">
<ikw:SimpleStackPanel Orientation="Horizontal" VerticalAlignment="Center" Spacing="4" Margin="16,0,0,0">
<Border Padding="4,2,4,1" Background="#e4e4e7" BorderBrush="#a1a1aa" BorderThickness="1" CornerRadius="2.5">
<TextBlock FontSize="8" Foreground="#3f3f46" FontWeight="Bold" Text="CTRL" />
</Border>
@@ -97,8 +129,8 @@
<Border Padding="4,2,4,1" Background="#e4e4e7" BorderBrush="#a1a1aa" BorderThickness="1" CornerRadius="2.5">
<TextBlock FontSize="8" Foreground="#3f3f46" FontWeight="Bold" Text="S" />
</Border>
</ui:SimpleStackPanel>
</ui:SimpleStackPanel>
</ikw:SimpleStackPanel>
</ikw:SimpleStackPanel>
</MenuItem.Header>
<MenuItem.Icon>
<Grid>
@@ -132,9 +164,9 @@
</MenuItem>
<MenuItem Name="ResetFloatingBarPositionTrayIconMenuItem" Click="ResetFloatingBarPositionTrayIconMenuItem_Clicked">
<MenuItem.Header>
<ui:SimpleStackPanel Orientation="Horizontal" Margin="-4,0,0,0">
<ikw:SimpleStackPanel Orientation="Horizontal" Margin="-4,0,0,0">
<TextBlock FontSize="14" VerticalAlignment="Center" Foreground="#18181b" Text="重置工具栏位置" />
<ui:SimpleStackPanel Orientation="Horizontal" VerticalAlignment="Center" Spacing="4" Margin="16,0,0,0">
<ikw:SimpleStackPanel Orientation="Horizontal" VerticalAlignment="Center" Spacing="4" Margin="16,0,0,0">
<Border Padding="4,2,4,1" Background="#e4e4e7" BorderBrush="#a1a1aa" BorderThickness="1" CornerRadius="2.5">
<TextBlock FontSize="8" Foreground="#3f3f46" FontWeight="Bold" Text="CTRL" />
</Border>
@@ -142,8 +174,8 @@
<Border Padding="4,2,4,1" Background="#e4e4e7" BorderBrush="#a1a1aa" BorderThickness="1" CornerRadius="2.5">
<TextBlock FontSize="8" Foreground="#3f3f46" FontWeight="Bold" Text="T" />
</Border>
</ui:SimpleStackPanel>
</ui:SimpleStackPanel>
</ikw:SimpleStackPanel>
</ikw:SimpleStackPanel>
</MenuItem.Header>
<MenuItem.Icon>
<Image Width="28" Height="28" Margin="-2">
@@ -163,9 +195,9 @@
<Separator Margin="0,3" />
<MenuItem Name="RestartAppTrayIconMenuItem" Click="RestartAppTrayIconMenuItem_Clicked">
<MenuItem.Header>
<ui:SimpleStackPanel Orientation="Horizontal" Margin="-4,0,0,0">
<ikw:SimpleStackPanel Orientation="Horizontal" Margin="-4,0,0,0">
<TextBlock FontSize="14" FontWeight="Bold" VerticalAlignment="Center" Foreground="#2563eb" Text="重启软件" />
<ui:SimpleStackPanel Orientation="Horizontal" VerticalAlignment="Center" Spacing="4" Margin="16,0,0,0">
<ikw:SimpleStackPanel Orientation="Horizontal" VerticalAlignment="Center" Spacing="4" Margin="16,0,0,0">
<Border Padding="4,2,4,1" Background="#e4e4e7" BorderBrush="#a1a1aa" BorderThickness="1" CornerRadius="2.5">
<TextBlock FontSize="8" Foreground="#3f3f46" FontWeight="Bold" Text="CTRL" />
</Border>
@@ -173,8 +205,8 @@
<Border Padding="4,2,4,1" Background="#e4e4e7" BorderBrush="#a1a1aa" BorderThickness="1" CornerRadius="2.5">
<TextBlock FontSize="8" Foreground="#3f3f46" FontWeight="Bold" Text="R" />
</Border>
</ui:SimpleStackPanel>
</ui:SimpleStackPanel>
</ikw:SimpleStackPanel>
</ikw:SimpleStackPanel>
</MenuItem.Header>
<MenuItem.Icon>
<Image Width="24" Height="24">
@@ -195,9 +227,9 @@
</MenuItem>
<MenuItem Name="CloseAppTrayIconMenuItem" Click="CloseAppTrayIconMenuItem_Clicked">
<MenuItem.Header>
<ui:SimpleStackPanel Orientation="Horizontal" Margin="-4,0,0,0">
<ikw:SimpleStackPanel Orientation="Horizontal" Margin="-4,0,0,0">
<TextBlock FontSize="14" FontWeight="Bold" VerticalAlignment="Center" Foreground="#dc2626" Text="退出软件" />
<ui:SimpleStackPanel Orientation="Horizontal" VerticalAlignment="Center" Spacing="4" Margin="16,0,0,0">
<ikw:SimpleStackPanel Orientation="Horizontal" VerticalAlignment="Center" Spacing="4" Margin="16,0,0,0">
<Border Padding="4,2,4,1" Background="#e4e4e7" BorderBrush="#a1a1aa" BorderThickness="1" CornerRadius="2.5">
<TextBlock FontSize="8" Foreground="#3f3f46" FontWeight="Bold" Text="CTRL" />
</Border>
@@ -205,8 +237,8 @@
<Border Padding="4,2,4,1" Background="#e4e4e7" BorderBrush="#a1a1aa" BorderThickness="1" CornerRadius="2.5">
<TextBlock FontSize="8" Foreground="#3f3f46" FontWeight="Bold" Text="Q" />
</Border>
</ui:SimpleStackPanel>
</ui:SimpleStackPanel>
</ikw:SimpleStackPanel>
</ikw:SimpleStackPanel>
</MenuItem.Header>
<MenuItem.Icon>
<Image Width="24" Height="24">
@@ -231,13 +263,12 @@
ToolTipText="InkCanvasForClass"
ContextMenu="{StaticResource SysTrayMenu}"
IconSource="/Resources/icc.ico"/>
<ResourceDictionary.MergedDictionaries>
<ui:ThemeResources RequestedTheme="Light"/>
<ResourceDictionary.MergedDictionaries>
<ui:ThemeResources/>
<ui:XamlControlsResources />
<ResourceDictionary Source="Resources/SeewoImageDictionary.xaml"/>
<ResourceDictionary Source="Resources/DrawShapeImageDictionary.xaml"/>
<ResourceDictionary Source="Resources/IconImageDictionary.xaml"/>
<ResourceDictionary Source="Resources/Styles/Light.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
+889 -706
View File
File diff suppressed because it is too large Load Diff
+6 -11
View File
@@ -2,6 +2,7 @@ using System.Reflection;
using System.Runtime.InteropServices;
using System.Windows;
[assembly: System.Runtime.Versioning.SupportedOSPlatform("windows")]
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
@@ -10,7 +11,7 @@ using System.Windows;
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("CJK_mkp")]
[assembly: AssemblyProduct("InkCanvasForClass")]
[assembly: AssemblyCopyright("Copyright © HARKOTEK Studio 2024")]
[assembly: AssemblyCopyright("Copyright © CJK_mkp 2025-2026")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
@@ -19,14 +20,8 @@ using System.Windows;
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)]
//In order to begin building localizable applications, set
//<UICulture>CultureYouAreCodingWith</UICulture> in your .csproj file
//inside a <PropertyGroup>. For example, if you are using US english
//in your source files, set the <UICulture> to en-US. Then uncomment
//the NeutralResourceLanguage attribute below. Update the "en-US" in
//the line below to match the UICulture setting in the project file.
//[assembly: NeutralResourcesLanguage("en-US", UltimateResourceFallbackLocation.Satellite)]
// i18n: 默认/回退语言为简体中文,与 Strings.resx 默认文案一致。
[assembly: System.Resources.NeutralResourcesLanguage("zh-CN", System.Resources.UltimateResourceFallbackLocation.MainAssembly)]
[assembly: ThemeInfo(
@@ -49,5 +44,5 @@ using System.Windows;
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.7.10.0")]
[assembly: AssemblyFileVersion("1.7.10.0")]
[assembly: AssemblyVersion("1.7.18.10")]
[assembly: AssemblyFileVersion("1.7.18.10")]
+141
View File
@@ -0,0 +1,141 @@
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Input;
using System.Windows.Media;
namespace Ink_Canvas.Controls
{
[TemplatePart(Name = PartCloseImage, Type = typeof(UIElement))]
[TemplatePart(Name = PartAnimationRoot, Type = typeof(UIElement))]
public class BoardMenuFrame : ContentControl
{
private const string PartCloseImage = "PART_CloseImage";
private const string PartAnimationRoot = "PART_AnimationRoot";
public static readonly DependencyProperty TitleProperty =
DependencyProperty.Register(nameof(Title), typeof(object), typeof(BoardMenuFrame), new PropertyMetadata(null));
public static readonly DependencyProperty TitleFontSizeProperty =
DependencyProperty.Register(nameof(TitleFontSize), typeof(double), typeof(BoardMenuFrame), new PropertyMetadata(11d));
public static readonly DependencyProperty HeaderHeightProperty =
DependencyProperty.Register(nameof(HeaderHeight), typeof(double), typeof(BoardMenuFrame), new PropertyMetadata(48d));
public static readonly DependencyProperty PanelCornerRadiusProperty =
DependencyProperty.Register(nameof(PanelCornerRadius), typeof(CornerRadius), typeof(BoardMenuFrame), new PropertyMetadata(new CornerRadius(5)));
public static readonly DependencyProperty HeaderCornerRadiusProperty =
DependencyProperty.Register(nameof(HeaderCornerRadius), typeof(CornerRadius), typeof(BoardMenuFrame), new PropertyMetadata(new CornerRadius(6, 6, 0, 0)));
public static readonly DependencyProperty PanelBackgroundProperty =
DependencyProperty.Register(nameof(PanelBackground), typeof(Brush), typeof(BoardMenuFrame), new PropertyMetadata(null));
public static readonly DependencyProperty HeaderBackgroundProperty =
DependencyProperty.Register(nameof(HeaderBackground), typeof(Brush), typeof(BoardMenuFrame),
new PropertyMetadata(new SolidColorBrush((Color)ColorConverter.ConvertFromString("#2563eb"))));
public static readonly DependencyProperty HeaderBorderBrushProperty =
DependencyProperty.Register(nameof(HeaderBorderBrush), typeof(Brush), typeof(BoardMenuFrame),
new PropertyMetadata(new SolidColorBrush((Color)ColorConverter.ConvertFromString("#1e3a8a"))));
public static readonly DependencyProperty IsOpenProperty =
DependencyProperty.Register(nameof(IsOpen), typeof(bool), typeof(BoardMenuFrame), new PropertyMetadata(false));
public static readonly DependencyProperty PlacementTargetProperty =
DependencyProperty.Register(nameof(PlacementTarget), typeof(UIElement), typeof(BoardMenuFrame), new PropertyMetadata(null));
public static readonly DependencyProperty PlacementProperty =
DependencyProperty.Register(nameof(Placement), typeof(PlacementMode), typeof(BoardMenuFrame), new PropertyMetadata(PlacementMode.Custom));
public static readonly DependencyProperty CustomPopupPlacementCallbackProperty =
DependencyProperty.Register(nameof(CustomPopupPlacementCallback), typeof(CustomPopupPlacementCallback), typeof(BoardMenuFrame),
new PropertyMetadata((CustomPopupPlacementCallback)PlaceCenteredAbove));
private static CustomPopupPlacement[] PlaceCenteredAbove(Size popupSize, Size targetSize, Point offset)
{
return new[]
{
new CustomPopupPlacement(
new Point((targetSize.Width - popupSize.Width) / 2 + offset.X,
-popupSize.Height + offset.Y),
PopupPrimaryAxis.Horizontal),
new CustomPopupPlacement(
new Point((targetSize.Width - popupSize.Width) / 2 + offset.X,
targetSize.Height - offset.Y),
PopupPrimaryAxis.Horizontal)
};
}
public static readonly DependencyProperty PopupHorizontalOffsetProperty =
DependencyProperty.Register(nameof(PopupHorizontalOffset), typeof(double), typeof(BoardMenuFrame), new PropertyMetadata(0d));
public static readonly DependencyProperty PopupVerticalOffsetProperty =
DependencyProperty.Register(nameof(PopupVerticalOffset), typeof(double), typeof(BoardMenuFrame), new PropertyMetadata(-4d));
public object Title { get => GetValue(TitleProperty); set => SetValue(TitleProperty, value); }
public double TitleFontSize { get => (double)GetValue(TitleFontSizeProperty); set => SetValue(TitleFontSizeProperty, value); }
public double HeaderHeight { get => (double)GetValue(HeaderHeightProperty); set => SetValue(HeaderHeightProperty, value); }
public CornerRadius PanelCornerRadius { get => (CornerRadius)GetValue(PanelCornerRadiusProperty); set => SetValue(PanelCornerRadiusProperty, value); }
public CornerRadius HeaderCornerRadius { get => (CornerRadius)GetValue(HeaderCornerRadiusProperty); set => SetValue(HeaderCornerRadiusProperty, value); }
public Brush PanelBackground { get => (Brush)GetValue(PanelBackgroundProperty); set => SetValue(PanelBackgroundProperty, value); }
public Brush HeaderBackground { get => (Brush)GetValue(HeaderBackgroundProperty); set => SetValue(HeaderBackgroundProperty, value); }
public Brush HeaderBorderBrush { get => (Brush)GetValue(HeaderBorderBrushProperty); set => SetValue(HeaderBorderBrushProperty, value); }
public bool IsOpen { get => (bool)GetValue(IsOpenProperty); set => SetValue(IsOpenProperty, value); }
public UIElement PlacementTarget { get => (UIElement)GetValue(PlacementTargetProperty); set => SetValue(PlacementTargetProperty, value); }
public PlacementMode Placement { get => (PlacementMode)GetValue(PlacementProperty); set => SetValue(PlacementProperty, value); }
public CustomPopupPlacementCallback CustomPopupPlacementCallback
{
get => (CustomPopupPlacementCallback)GetValue(CustomPopupPlacementCallbackProperty);
set => SetValue(CustomPopupPlacementCallbackProperty, value);
}
public double PopupHorizontalOffset { get => (double)GetValue(PopupHorizontalOffsetProperty); set => SetValue(PopupHorizontalOffsetProperty, value); }
public double PopupVerticalOffset { get => (double)GetValue(PopupVerticalOffsetProperty); set => SetValue(PopupVerticalOffsetProperty, value); }
public event MouseButtonEventHandler CloseMouseDown;
public event MouseButtonEventHandler CloseMouseUp;
public UIElement AnimationTarget { get; private set; }
private UIElement _closeImage;
static BoardMenuFrame()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(BoardMenuFrame), new FrameworkPropertyMetadata(typeof(BoardMenuFrame)));
VisibilityProperty.OverrideMetadata(typeof(BoardMenuFrame),
new FrameworkPropertyMetadata(Visibility.Collapsed, OnVisibilityChanged));
}
private static void OnVisibilityChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
((BoardMenuFrame)d).IsOpen = (Visibility)e.NewValue == Visibility.Visible;
}
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
if (_closeImage != null)
{
_closeImage.MouseDown -= CloseImage_MouseDown;
_closeImage.MouseUp -= CloseImage_MouseUp;
}
_closeImage = GetTemplateChild(PartCloseImage) as UIElement;
if (_closeImage != null)
{
_closeImage.MouseDown += CloseImage_MouseDown;
_closeImage.MouseUp += CloseImage_MouseUp;
}
AnimationTarget = GetTemplateChild(PartAnimationRoot) as UIElement;
}
private void CloseImage_MouseDown(object sender, MouseButtonEventArgs e)
{
CloseMouseDown?.Invoke(sender, e);
}
private void CloseImage_MouseUp(object sender, MouseButtonEventArgs e)
{
CloseMouseUp?.Invoke(sender, e);
}
}
}
+145
View File
@@ -0,0 +1,145 @@
using Ink_Canvas.Helpers;
using System;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Imaging;
namespace Ink_Canvas.Controls
{
/// <summary>
/// 画布上的多页 PDF:仅显示当前页;翻页与页码由主窗口 PDF 侧栏控制(无 XAML 文件)。
/// </summary>
public class PdfEmbeddedView : UserControl
{
private readonly Image _pageImage;
private string _pdfPath;
private uint _pageCount;
private uint _currentIndex;
private bool _compressLargePictures;
private bool _isPagingBusy;
private bool _layoutSizeCommitted;
/// <summary>页码或可翻页状态变化(用于更新侧栏)。</summary>
public event EventHandler PageNavigationStateChanged;
public PdfEmbeddedView()
{
MinWidth = 80;
MinHeight = 60;
var grid = new Grid { ClipToBounds = true };
_pageImage = new Image
{
Stretch = Stretch.Uniform,
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center
};
grid.Children.Add(_pageImage);
Content = grid;
}
/// <summary>
/// 初始化并显示指定页;由 MainWindow 在 UI 线程创建后调用。
/// </summary>
/// <param name="initialPageIndex">从 0 开始的页码,超出范围时夹紧到合法区间。</param>
public async Task InitializeAsync(string pdfFilePath, uint pageCount, bool compressLargePictures, uint initialPageIndex = 0)
{
_pdfPath = pdfFilePath ?? throw new ArgumentNullException(nameof(pdfFilePath));
_pageCount = pageCount;
_compressLargePictures = compressLargePictures;
if (_pageCount == 0)
_currentIndex = 0;
else
_currentIndex = initialPageIndex >= _pageCount ? _pageCount - 1 : initialPageIndex;
await ShowPageAsync(_currentIndex);
}
public string PdfPath => _pdfPath;
public uint PageCount => _pageCount;
public uint CurrentPageIndex => _currentIndex;
public string PageLabelText => _pageCount == 0 ? "" : $"{_currentIndex + 1} / {_pageCount}";
public bool CanGoPrevious => !_isPagingBusy && _pageCount > 1 && _currentIndex > 0;
public bool CanGoNext => !_isPagingBusy && _pageCount > 1 && _currentIndex < _pageCount - 1;
public async Task GoToPreviousPageAsync()
{
await GoRelativeAsync(-1);
}
public async Task GoToNextPageAsync()
{
await GoRelativeAsync(1);
}
private void NotifyPageNavigationStateChanged()
{
PageNavigationStateChanged?.Invoke(this, EventArgs.Empty);
}
private async Task GoRelativeAsync(int delta)
{
if (_isPagingBusy || _pageCount <= 1)
return;
int next = (int)_currentIndex + delta;
if (next < 0 || next >= _pageCount)
return;
_currentIndex = (uint)next;
await ShowPageAsync(_currentIndex);
}
private async Task ShowPageAsync(uint pageIndex)
{
_isPagingBusy = true;
NotifyPageNavigationStateChanged();
try
{
BitmapSource raw = await PdfWinRtHelper.RenderPageToBitmapSourceAsync(_pdfPath, pageIndex);
if (raw == null)
return;
BitmapSource display = ApplyCompressionIfNeeded(raw);
_pageImage.Source = display;
if (!_layoutSizeCommitted)
{
bool callerSized = !double.IsNaN(Width) && Width > 0 && !double.IsNaN(Height) && Height > 0;
if (!callerSized)
{
Width = display.PixelWidth;
Height = display.PixelHeight;
}
_layoutSizeCommitted = true;
}
}
finally
{
_isPagingBusy = false;
NotifyPageNavigationStateChanged();
}
}
private BitmapSource ApplyCompressionIfNeeded(BitmapSource rendered)
{
int width = rendered.PixelWidth;
int height = rendered.PixelHeight;
if (_compressLargePictures && (width > 1920 || height > 1080))
{
double scaleX = 1920.0 / width;
double scaleY = 1080.0 / height;
double scale = Math.Min(scaleX, scaleY);
return new TransformedBitmap(rendered, new ScaleTransform(scale, scale));
}
return rendered;
}
}
}
+136
View File
@@ -0,0 +1,136 @@
<UserControl x:Class="Ink_Canvas.Controls.PptNavBar"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
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:ikw="http://schemas.inkore.net/lib/ui/wpf"
xmlns:local="clr-namespace:Ink_Canvas.Controls"
mc:Ignorable="d"
d:DesignHeight="50" d:DesignWidth="200">
<UserControl.Resources>
<SolidColorBrush x:Key="PptNavBarItemForeground" Color="#71717a"/>
<DataTemplate x:Key="PptPreviewItemTemplate">
<Grid Margin="2,4">
<Border CornerRadius="8" Background="Transparent" Padding="6">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="28"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" VerticalAlignment="Center" HorizontalAlignment="Center"
FontSize="12" FontWeight="SemiBold"
Foreground="{DynamicResource PptNavBarItemForeground}"
Text="{Binding SlideNumber}"/>
<Image Grid.Column="1" Source="{Binding Thumbnail}" Stretch="Uniform"/>
</Grid>
</Border>
</Grid>
</DataTemplate>
<Style x:Key="PptPreviewItemContainerStyle" TargetType="ListBoxItem">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Padding" Value="0"/>
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
<Setter Property="FocusVisualStyle" Value="{x:Null}"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ListBoxItem">
<Border x:Name="Bd" CornerRadius="8" Background="Transparent"
BorderBrush="Transparent" BorderThickness="2">
<ContentPresenter/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Bd" Property="Background" Value="#1affffff"/>
</Trigger>
<Trigger Property="IsSelected" Value="True">
<Setter TargetName="Bd" Property="BorderBrush" Value="#66CCFF"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</UserControl.Resources>
<Border x:Name="RootBorder" BorderThickness="1" BorderBrush="#a1a1aa"
Background="#f4f4f5" Opacity="1" CornerRadius="6">
<Border.Effect>
<DropShadowEffect Color="#000000" BlurRadius="14" ShadowDepth="2" Opacity="0.16"/>
</Border.Effect>
<DockPanel x:Name="LayoutRoot" LastChildFill="True">
<ListBox x:Name="PreviewList"
Visibility="Collapsed"
Background="Transparent" BorderThickness="0"
Padding="6,8,6,4"
HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
ScrollViewer.VerticalScrollBarVisibility="Auto"
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
ItemTemplate="{StaticResource PptPreviewItemTemplate}"
ItemContainerStyle="{StaticResource PptPreviewItemContainerStyle}"
MouseUp="PreviewList_MouseUp"/>
<ikw:SimpleStackPanel x:Name="ButtonRow" Orientation="Horizontal"
HorizontalAlignment="Center" VerticalAlignment="Center">
<Border x:Name="PreviousButtonBorder" Width="50" Height="50"
MouseDown="PreviousButton_MouseDown"
MouseUp="PreviousButton_MouseUp"
MouseLeave="PreviousButton_MouseLeave"
CornerRadius="5 5 0 0" Background="Transparent">
<Grid>
<Border x:Name="PreviousButtonFeedbackBorder" Margin="3" CornerRadius="5"
Background="#18181b" Opacity="0"/>
<Image Height="28" Width="28">
<Image.Source>
<DrawingImage>
<DrawingImage.Drawing>
<DrawingGroup ClipGeometry="M0,0 V24 H24 V0 H0 Z">
<GeometryDrawing x:Name="PreviousButtonGeometry" Brush="#27272a"/>
</DrawingGroup>
</DrawingImage.Drawing>
</DrawingImage>
</Image.Source>
</Image>
</Grid>
</Border>
<Border x:Name="PageButtonBorder" Visibility="Visible" TextBlock.Foreground="#171717"
MouseDown="PageButton_MouseDown"
MouseUp="PageButton_MouseUp"
MouseLeave="PageButton_MouseLeave"
MinWidth="50" MinHeight="50" Background="Transparent">
<Grid>
<Border x:Name="PageButtonFeedbackBorder" Margin="0,3" CornerRadius="5"
Background="#18181b" Opacity="0"/>
<ikw:SimpleStackPanel VerticalAlignment="Center" Orientation="Vertical"
Spacing="0.5" Margin="12,0">
<TextBlock x:Name="PageNowText" HorizontalAlignment="Center" VerticalAlignment="Center"
FontSize="17" FontWeight="Bold" Text="?"/>
<TextBlock x:Name="PageTotalText" HorizontalAlignment="Center" VerticalAlignment="Center"
FontSize="10" Text="/ ?"/>
</ikw:SimpleStackPanel>
</Grid>
</Border>
<Border x:Name="NextButtonBorder" Width="50" Height="50"
MouseDown="NextButton_MouseDown"
MouseUp="NextButton_MouseUp"
MouseLeave="NextButton_MouseLeave"
CornerRadius="0 0 5 5" Background="Transparent">
<Grid>
<Border x:Name="NextButtonFeedbackBorder" Margin="3" CornerRadius="5"
Background="#18181b" Opacity="0"/>
<Image VerticalAlignment="Center" HorizontalAlignment="Center" Height="28" Width="28">
<Image.Source>
<DrawingImage>
<DrawingImage.Drawing>
<DrawingGroup ClipGeometry="M0,0 V24 H24 V0 H0 Z">
<GeometryDrawing x:Name="NextButtonGeometry" Brush="#27272a"/>
</DrawingGroup>
</DrawingImage.Drawing>
</DrawingImage>
</Image.Source>
</Image>
</Grid>
</Border>
</ikw:SimpleStackPanel>
</DockPanel>
</Border>
</UserControl>
+393
View File
@@ -0,0 +1,393 @@
using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Threading;
namespace Ink_Canvas.Controls
{
/// <summary>
/// PPT 翻页 + 增强预览一体化控件。
/// 通过 <see cref="Direction"/> 切换底部条 (LB/RB) 与侧边条 (LS/RS) 布局,
/// 预览列表内嵌于同一个 Border,展开时占据按钮组之外的剩余空间。
/// </summary>
public partial class PptNavBar : UserControl
{
public sealed class PreviewItem
{
public int SlideNumber { get; set; }
public BitmapImage Thumbnail { get; set; }
}
public enum NavDirection
{
LeftBottom,
RightBottom,
LeftSide,
RightSide
}
public static readonly DependencyProperty DirectionProperty = DependencyProperty.Register(
nameof(Direction), typeof(NavDirection), typeof(PptNavBar),
new PropertyMetadata(NavDirection.LeftBottom, OnDirectionChanged));
public static readonly DependencyProperty CurrentSlideProperty = DependencyProperty.Register(
nameof(CurrentSlide), typeof(int), typeof(PptNavBar),
new PropertyMetadata(0, OnPageChanged));
public static readonly DependencyProperty TotalSlidesProperty = DependencyProperty.Register(
nameof(TotalSlides), typeof(int), typeof(PptNavBar),
new PropertyMetadata(0, OnPageChanged));
public static readonly DependencyProperty PreviewItemsProperty = DependencyProperty.Register(
nameof(PreviewItems), typeof(IList<PreviewItem>), typeof(PptNavBar),
new PropertyMetadata(null, OnPreviewItemsChanged));
public static readonly DependencyProperty IsPreviewExpandedProperty = DependencyProperty.Register(
nameof(IsPreviewExpanded), typeof(bool), typeof(PptNavBar),
new PropertyMetadata(false, OnIsPreviewExpandedChanged));
public NavDirection Direction
{
get => (NavDirection)GetValue(DirectionProperty);
set => SetValue(DirectionProperty, value);
}
public int CurrentSlide
{
get => (int)GetValue(CurrentSlideProperty);
set => SetValue(CurrentSlideProperty, value);
}
public int TotalSlides
{
get => (int)GetValue(TotalSlidesProperty);
set => SetValue(TotalSlidesProperty, value);
}
public IList<PreviewItem> PreviewItems
{
get => (IList<PreviewItem>)GetValue(PreviewItemsProperty);
set => SetValue(PreviewItemsProperty, value);
}
public bool IsPreviewExpanded
{
get => (bool)GetValue(IsPreviewExpandedProperty);
set => SetValue(IsPreviewExpandedProperty, value);
}
public event EventHandler PreviousClick;
public event EventHandler NextClick;
public event EventHandler PageClick;
public event EventHandler<int> SlideSelected;
public event EventHandler PreviousPressedDown;
public event EventHandler NextPressedDown;
public event EventHandler PressEnded;
public event EventHandler<bool> PreviewExpandedChanged;
// 静态几何(左下/右下:水平箭头;左侧/右侧:垂直箭头)
private static readonly Geometry HArrowLeft = Geometry.Parse("F0 M24,24z M0,0z M3.3994,12.9642C2.86687,12.4317,2.86687,11.5683,3.3994,11.0358L9.94485,4.49031C10.4774,3.95777 11.3408,3.95777 11.8733,4.49031 12.4059,5.02284 12.4059,5.88625 11.8733,6.41878L7.65575,10.6364 19.6364,10.6364C20.3895,10.6364 21,11.2469 21,12 21,12.7531 20.3895,13.3636 19.6364,13.3636L7.65575,13.3636 11.8733,17.5812C12.4059,18.1137 12.4059,18.9772 11.8733,19.5097 11.3408,20.0422 10.4774,20.0422 9.94485,19.5097L3.3994,12.9642z");
private static readonly Geometry HArrowRight = Geometry.Parse("F0 M24,24z M0,0z M20.6006,12.9642C21.1331,12.4317,21.1331,11.5683,20.6006,11.0358L14.0551,4.49031C13.5226,3.95777 12.6592,3.95777 12.1267,4.49031 11.5941,5.02284 11.5941,5.88625 12.1267,6.41878L16.3443,10.6364 4.36364,10.6364C3.61052,10.6364 3,11.2469 3,12 3,12.7531 3.61052,13.3636 4.36364,13.3636L16.3443,13.3636 12.1267,17.5812C11.5941,18.1137 11.5941,18.9772 12.1267,19.5097 12.6592,20.0422 13.5226,20.0422 14.0551,19.5097L20.6006,12.9642z");
private static readonly Geometry VArrowUp = Geometry.Parse("F0 M24,24z M0,0z M11.0357,3.3994C11.5682,2.86687,12.4316,2.86687,12.9641,3.3994L19.5096,9.94485C20.0421,10.4774 20.0421,11.3408 19.5096,11.8733 18.9771,12.4059 18.1137,12.4059 17.5811,11.8733L13.3635,7.65575 13.3635,19.6364C13.3635,20.3895 12.753,21 11.9999,21 11.2468,21 10.6363,20.3895 10.6363,19.6364L10.6363,7.65575 6.41869,11.8733C5.88616,12.4059 5.02275,12.4059 4.49022,11.8733 3.95769,11.3408 3.95769,10.4774 4.49022,9.94485L11.0357,3.3994z");
private static readonly Geometry VArrowDown = Geometry.Parse("F0 M24,24z M0,0z M11.0357,20.6006C11.5682,21.1331,12.4316,21.1331,12.9641,20.6006L19.5096,14.0551C20.0421,13.5226 20.0421,12.6592 19.5096,12.1267 18.9771,11.5941 18.1137,11.5941 17.5811,12.1267L13.3635,16.3443 13.3635,4.36364C13.3635,3.61052 12.753,3 11.9999,3 11.2468,3 10.6363,3.61052 10.6363,4.36364L10.6363,16.3443 6.41869,12.1267C5.88616,11.5941 5.02275,11.5941 4.49022,12.1267 3.95769,12.6592 3.95769,13.5226 4.49022,14.0551L11.0357,20.6006z");
public PptNavBar()
{
InitializeComponent();
ApplyDirection(Direction);
}
private static void OnDirectionChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is PptNavBar bar) bar.ApplyDirection((NavDirection)e.NewValue);
}
private static void OnPageChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is PptNavBar bar) bar.RefreshPageText();
}
private static void OnPreviewItemsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is PptNavBar bar)
{
bar.PreviewList.ItemsSource = e.NewValue as IList<PreviewItem>;
bar.SyncPreviewSelection();
}
}
private static void OnIsPreviewExpandedChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is PptNavBar bar)
{
bool expanded = (bool)e.NewValue;
bar.PreviewList.Visibility = expanded ? Visibility.Visible : Visibility.Collapsed;
bar.ApplyLayout();
if (expanded)
{
bar.SyncPreviewSelection();
bar.HookOutsideClick();
}
else
{
bar.UnhookOutsideClick();
}
bar.PreviewExpandedChanged?.Invoke(bar, expanded);
}
}
private Window _hookedWindow;
private void HookOutsideClick()
{
if (_hookedWindow != null) return;
_hookedWindow = Window.GetWindow(this);
if (_hookedWindow != null)
{
_hookedWindow.PreviewMouseDown += OnWindowPreviewMouseDown;
}
}
private void UnhookOutsideClick()
{
if (_hookedWindow != null)
{
_hookedWindow.PreviewMouseDown -= OnWindowPreviewMouseDown;
_hookedWindow = null;
}
}
private void OnWindowPreviewMouseDown(object sender, MouseButtonEventArgs e)
{
if (e.OriginalSource is DependencyObject d && !IsDescendantOf(d, this))
{
IsPreviewExpanded = false;
}
}
private static bool IsDescendantOf(DependencyObject child, DependencyObject ancestor)
{
while (child != null)
{
if (ReferenceEquals(child, ancestor)) return true;
child = System.Windows.Media.VisualTreeHelper.GetParent(child)
?? System.Windows.LogicalTreeHelper.GetParent(child);
}
return false;
}
private void ApplyDirection(NavDirection dir) => ApplyLayout();
private void ApplyLayout()
{
var dir = Direction;
bool expanded = IsPreviewExpanded;
// 重置可能在不同状态下被设置的属性
ButtonRow.ClearValue(WidthProperty);
ButtonRow.ClearValue(HeightProperty);
ButtonRow.ClearValue(HorizontalAlignmentProperty);
PreviewList.ClearValue(WidthProperty);
PreviewList.ClearValue(HeightProperty);
PreviewList.ClearValue(MaxHeightProperty);
PreviewList.ClearValue(MaxWidthProperty);
PreviewList.ClearValue(HorizontalAlignmentProperty);
ClearValue(HeightProperty);
ClearValue(MaxHeightProperty);
double availableHeight = ComputeAvailableHeight();
switch (dir)
{
case NavDirection.LeftBottom:
case NavDirection.RightBottom:
DockPanel.SetDock(PreviewList, Dock.Top);
DockPanel.SetDock(ButtonRow, Dock.Bottom);
ButtonRow.Orientation = Orientation.Horizontal;
ButtonRow.Height = 50;
if (expanded)
{
// 预览面板拉宽到 280,贴向同侧角落
PreviewList.Width = 280;
PreviewList.MaxHeight = Math.Max(200, availableHeight - 50);
PreviewList.HorizontalAlignment = dir == NavDirection.LeftBottom
? HorizontalAlignment.Left
: HorizontalAlignment.Right;
// 按钮组宽度限制为原始内容宽度,并贴向同侧,保持按钮位置不变
ButtonRow.HorizontalAlignment = dir == NavDirection.LeftBottom
? HorizontalAlignment.Left
: HorizontalAlignment.Right;
}
else
{
PreviewList.SetBinding(WidthProperty, new System.Windows.Data.Binding(nameof(ButtonRow.ActualWidth)) { Source = ButtonRow });
PreviewList.MaxHeight = 380;
}
PreviousButtonGeometry.Geometry = HArrowLeft;
NextButtonGeometry.Geometry = HArrowRight;
break;
case NavDirection.LeftSide:
DockPanel.SetDock(PreviewList, Dock.Right);
DockPanel.SetDock(ButtonRow, Dock.Left);
ButtonRow.Orientation = Orientation.Vertical;
ButtonRow.Width = 50;
PreviewList.Width = 240;
PreviewList.MaxHeight = 480;
PreviousButtonGeometry.Geometry = VArrowUp;
NextButtonGeometry.Geometry = VArrowDown;
break;
case NavDirection.RightSide:
DockPanel.SetDock(PreviewList, Dock.Left);
DockPanel.SetDock(ButtonRow, Dock.Right);
ButtonRow.Orientation = Orientation.Vertical;
ButtonRow.Width = 50;
PreviewList.Width = 240;
PreviewList.MaxHeight = 480;
PreviousButtonGeometry.Geometry = VArrowUp;
NextButtonGeometry.Geometry = VArrowDown;
break;
}
}
private double ComputeAvailableHeight()
{
var window = Window.GetWindow(this);
double h = window != null ? window.ActualHeight : SystemParameters.PrimaryScreenHeight;
return Math.Max(240, h - 12);
}
private void RefreshPageText()
{
if (CurrentSlide > 0 && TotalSlides > 0)
{
PageNowText.Text = CurrentSlide.ToString();
PageTotalText.Text = $"/ {TotalSlides}";
}
else
{
PageNowText.Text = "?";
PageTotalText.Text = "/ ?";
}
SyncPreviewSelection();
}
private void SyncPreviewSelection()
{
if (PreviewItems == null || CurrentSlide <= 0) return;
foreach (var item in PreviewItems)
{
if (item.SlideNumber == CurrentSlide)
{
PreviewList.SelectedItem = item;
PreviewList.ScrollIntoView(item);
return;
}
}
}
private void SetFeedback(Border feedback, double opacity) => feedback.Opacity = opacity;
private object _lastDown;
private void PreviousButton_MouseDown(object sender, MouseButtonEventArgs e)
{
_lastDown = sender;
SetFeedback(PreviousButtonFeedbackBorder, 0.15);
PreviousPressedDown?.Invoke(this, EventArgs.Empty);
}
private void PreviousButton_MouseUp(object sender, MouseButtonEventArgs e)
{
SetFeedback(PreviousButtonFeedbackBorder, 0);
PressEnded?.Invoke(this, EventArgs.Empty);
if (_lastDown != sender) return;
_lastDown = null;
PreviousClick?.Invoke(this, EventArgs.Empty);
}
private void PreviousButton_MouseLeave(object sender, MouseEventArgs e)
{
SetFeedback(PreviousButtonFeedbackBorder, 0);
_lastDown = null;
PressEnded?.Invoke(this, EventArgs.Empty);
}
private void NextButton_MouseDown(object sender, MouseButtonEventArgs e)
{
_lastDown = sender;
SetFeedback(NextButtonFeedbackBorder, 0.15);
NextPressedDown?.Invoke(this, EventArgs.Empty);
}
private void NextButton_MouseUp(object sender, MouseButtonEventArgs e)
{
SetFeedback(NextButtonFeedbackBorder, 0);
PressEnded?.Invoke(this, EventArgs.Empty);
if (_lastDown != sender) return;
_lastDown = null;
NextClick?.Invoke(this, EventArgs.Empty);
}
private void NextButton_MouseLeave(object sender, MouseEventArgs e)
{
SetFeedback(NextButtonFeedbackBorder, 0);
_lastDown = null;
PressEnded?.Invoke(this, EventArgs.Empty);
}
private void PageButton_MouseDown(object sender, MouseButtonEventArgs e)
{
_lastDown = sender;
SetFeedback(PageButtonFeedbackBorder, 0.15);
}
private void PageButton_MouseUp(object sender, MouseButtonEventArgs e)
{
SetFeedback(PageButtonFeedbackBorder, 0);
if (_lastDown != sender) return;
_lastDown = null;
PageClick?.Invoke(this, EventArgs.Empty);
}
private void PageButton_MouseLeave(object sender, MouseEventArgs e)
{
SetFeedback(PageButtonFeedbackBorder, 0);
_lastDown = null;
}
private void PreviewList_MouseUp(object sender, MouseButtonEventArgs e)
{
if (PreviewList.SelectedItem is PreviewItem item)
{
SlideSelected?.Invoke(this, item.SlideNumber);
}
}
public void ApplyTheme(bool isDark)
{
var fgBrush = isDark ? Brushes.White : new SolidColorBrush(Color.FromRgb(39, 39, 42));
var feedbackBrush = isDark ? Brushes.White : new SolidColorBrush(Color.FromRgb(24, 24, 27));
var bgBrush = isDark
? new SolidColorBrush(Color.FromRgb(39, 39, 42))
: new SolidColorBrush(Color.FromRgb(244, 244, 245));
var borderBrush = isDark
? new SolidColorBrush(Color.FromRgb(82, 82, 91))
: new SolidColorBrush(Color.FromRgb(161, 161, 170));
PreviousButtonGeometry.Brush = fgBrush;
NextButtonGeometry.Brush = fgBrush;
PreviousButtonFeedbackBorder.Background = feedbackBrush;
NextButtonFeedbackBorder.Background = feedbackBrush;
PageButtonFeedbackBorder.Background = feedbackBrush;
PageNowText.Foreground = fgBrush;
PageTotalText.Foreground = fgBrush;
RootBorder.Background = bgBrush;
RootBorder.BorderBrush = borderBrush;
Resources["PptNavBarItemForeground"] = fgBrush;
}
public void SetPageButtonVisibility(Visibility v) => PageButtonBorder.Visibility = v;
public void SetBarOpacity(double opacity) => RootBorder.Opacity = opacity;
}
}
@@ -0,0 +1,77 @@
<UserControl x:Class="Ink_Canvas.Controls.QuickDrawFloatingButtonControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
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"
mc:Ignorable="d"
Width="65" Height="45">
<Border Background="{DynamicResource QuickDrawFloatingButtonBackground}"
CornerRadius="8"
BorderThickness="1"
BorderBrush="{DynamicResource QuickDrawFloatingButtonBorderBrush}">
<Border.Effect>
<DropShadowEffect Color="Black" Direction="315" ShadowDepth="3" Opacity="0.3" BlurRadius="5"/>
</Border.Effect>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="22"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<!-- 拖动区域 -->
<Border Grid.Column="0"
MouseLeftButtonDown="DragArea_MouseLeftButtonDown"
MouseMove="DragArea_MouseMove"
MouseLeftButtonUp="DragArea_MouseLeftButtonUp"
Cursor="SizeAll"
Background="Transparent">
<Grid VerticalAlignment="Center" Height="14" IsHitTestVisible="False">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="4"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="4"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- 三个白色横线 -->
<Border Grid.Row="0" Background="{DynamicResource QuickDrawFloatingButtonIconForeground}" Height="2" Width="10"
HorizontalAlignment="Center"
CornerRadius="1" Opacity="0.8" IsHitTestVisible="False"/>
<Border Grid.Row="2" Background="{DynamicResource QuickDrawFloatingButtonIconForeground}" Height="2" Width="10"
HorizontalAlignment="Center"
CornerRadius="1" Opacity="0.8" IsHitTestVisible="False"/>
<Border Grid.Row="4" Background="{DynamicResource QuickDrawFloatingButtonIconForeground}" Height="2" Width="10"
HorizontalAlignment="Center"
CornerRadius="1" Opacity="0.8" IsHitTestVisible="False"/>
</Grid>
</Border>
<!-- 半透明分割线 -->
<Rectangle Grid.Column="1" Width="1" Fill="#20FFFFFF" Margin="0,8,0,8"/>
<!-- 按钮区域 -->
<Border Grid.Column="2"
MouseLeftButtonDown="FloatingButton_Click"
Cursor="Hand"
Background="Transparent">
<Grid IsHitTestVisible="False">
<Path Data="M5 7C5 8.06087 5.42143 9.07828 6.17157 9.82843C6.92172 10.5786 7.93913 11 9 11C10.0609 11 11.0783 10.5786 11.8284 9.82843C12.5786 9.07828 13 8.06087 13 7C13 5.93913 12.5786 4.92172 11.8284 4.17157C11.0783 3.42143 10.0609 3 9 3C7.93913 3 6.92172 3.42143 6.17157 4.17157C5.42143 4.92172 5 5.93913 5 7Z M3 21V19C3 17.9391 3.42143 16.9217 4.17157 16.1716C4.92172 15.4214 5.93913 15 7 15H11C12.0609 15 13.0783 15.4214 13.8284 16.1716C14.5786 16.9217 15 17.9391 15 19V21 M16 3.13C16.8604 3.35031 17.623 3.85071 18.1676 4.55232C18.7122 5.25392 19.0078 6.11683 19.0078 7.005C19.0078 7.89318 18.7122 8.75608 18.1676 9.45769C17.623 10.1593 16.8604 10.6597 16 10.88 M21 21V19C20.9949 18.1172 20.6979 17.2608 20.1553 16.5644C19.6126 15.868 18.8548 15.3707 18 15.15"
Stroke="{DynamicResource QuickDrawFloatingButtonIconForeground}"
StrokeThickness="2"
StrokeLineJoin="Round"
Fill="Transparent"
Width="20" Height="20"
Stretch="Uniform"
HorizontalAlignment="Center"
VerticalAlignment="Center"
IsHitTestVisible="False"/>
</Grid>
</Border>
</Grid>
</Border>
</UserControl>
@@ -0,0 +1,148 @@
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Threading;
using HorizontalAlignment = System.Windows.HorizontalAlignment;
using MouseEventArgs = System.Windows.Input.MouseEventArgs;
using VerticalAlignment = System.Windows.VerticalAlignment;
namespace Ink_Canvas.Controls
{
/// <summary>
/// 快抽悬浮按钮控件
/// </summary>
public partial class QuickDrawFloatingButtonControl : UserControl
{
private bool _isDragging = false;
private Point _dragStartPoint;
private Point _controlStartPoint;
public QuickDrawFloatingButtonControl()
{
InitializeComponent();
}
/// <summary>
/// 快抽按钮点击事件
/// </summary>
private void FloatingButton_Click(object sender, MouseButtonEventArgs e)
{
try
{
// 如果正在拖动,不触发点击事件
if (_isDragging) return;
// 打开快抽窗口
var quickDrawWindow = new QuickDrawWindow();
quickDrawWindow.ShowDialog();
}
catch (Exception ex)
{
Helpers.LogHelper.WriteLogToFile($"打开快抽窗口失败: {ex.Message}", Helpers.LogHelper.LogType.Error);
}
}
/// <summary>
/// 拖动区域鼠标按下事件
/// </summary>
private void DragArea_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
_isDragging = false;
// 记录鼠标在屏幕上的初始位置
_dragStartPoint = this.PointToScreen(e.GetPosition(this));
// 记录控件的初始位置
var parent = this.Parent as FrameworkElement;
if (parent != null)
{
var transform = this.TransformToVisual(parent);
var currentPos = transform.Transform(new Point(0, 0));
_controlStartPoint = currentPos;
}
else
{
var currentMargin = this.Margin;
_controlStartPoint = new Point(
double.IsNaN(currentMargin.Left) ? 0 : currentMargin.Left,
double.IsNaN(currentMargin.Top) ? 0 : currentMargin.Top);
}
((UIElement)sender).CaptureMouse();
e.Handled = true;
}
/// <summary>
/// 拖动区域鼠标移动事件
/// </summary>
private void DragArea_MouseMove(object sender, MouseEventArgs e)
{
if (e.LeftButton == MouseButtonState.Pressed && ((UIElement)sender).IsMouseCaptured)
{
// 获取鼠标在屏幕上的当前位置
Point currentScreenPoint = this.PointToScreen(e.GetPosition(this));
Vector diff = currentScreenPoint - _dragStartPoint;
if (!_isDragging && (Math.Abs(diff.X) > 3 || Math.Abs(diff.Y) > 3))
{
_isDragging = true;
// 切换到绝对定位模式
this.HorizontalAlignment = HorizontalAlignment.Left;
this.VerticalAlignment = VerticalAlignment.Top;
}
if (_isDragging)
{
// 计算新位置
var parent = this.Parent as FrameworkElement;
if (parent != null)
{
// 计算屏幕坐标相对于父容器的位置
var parentPoint = parent.PointFromScreen(currentScreenPoint);
var startParentPoint = parent.PointFromScreen(_dragStartPoint);
// 计算相对于初始位置的偏移
double offsetX = parentPoint.X - startParentPoint.X;
double offsetY = parentPoint.Y - startParentPoint.Y;
// 新位置 = 初始位置 + 偏移
double newLeft = _controlStartPoint.X + offsetX;
double newTop = _controlStartPoint.Y + offsetY;
// 限制在父容器范围内
newLeft = Math.Max(0, Math.Min(newLeft, parent.ActualWidth - this.ActualWidth));
newTop = Math.Max(0, Math.Min(newTop, parent.ActualHeight - this.ActualHeight));
// 更新Margin
this.Margin = new Thickness(newLeft, newTop, 0, 0);
}
}
}
}
/// <summary>
/// 拖动区域鼠标释放事件
/// </summary>
private void DragArea_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
if (((UIElement)sender).IsMouseCaptured)
{
((UIElement)sender).ReleaseMouseCapture();
}
if (_isDragging)
{
Dispatcher.BeginInvoke(new Action(() => { _isDragging = false; }),
DispatcherPriority.Background);
}
else
{
_isDragging = false;
}
e.Handled = true;
}
}
}
@@ -0,0 +1,18 @@
using System.Windows;
namespace Ink_Canvas.Controls.Toolbar
{
/// <summary>
/// 工具栏按钮插件与宿主之间的桥梁。Phase 1 粗粒度暴露 MainWindow,后续收窄。
/// </summary>
public interface IToolbarHost
{
MainWindow Window { get; }
/// <summary>按 id 登记按钮的 view 实例(供 MainWindow 字段回填和互相查找)。</summary>
void RegisterView(string id, FrameworkElement view);
/// <summary>按 id 获取之前注册的 view。不存在返回 null。</summary>
FrameworkElement FindView(string id);
}
}
@@ -0,0 +1,29 @@
using System.Windows;
namespace Ink_Canvas.Controls.Toolbar
{
/// <summary>
/// 一个工具栏按钮(或任意浮动栏/白板栏条目)的插件化契约。
/// 实现类必须有无参构造函数,启动时会被 ToolbarRegistry 反射实例化。
/// </summary>
public interface IToolbarItem
{
/// <summary>稳定、唯一的 id,用于持久化用户配置。不要随便改。</summary>
string Id { get; }
ToolbarSlot DefaultSlot { get; }
/// <summary>同一 slot 内的默认顺序,小的在前。</summary>
int DefaultOrder { get; }
bool DefaultVisible { get; }
ToolbarInsertPosition DefaultPosition { get; }
/// <summary>仅当 Position 为 BeforeAnchor/AfterAnchor 时有意义,对应 XAML 里 x:Name。</summary>
string DefaultAnchorName { get; }
/// <summary>构造 UI 元素并接线所有行为。</summary>
FrameworkElement BuildView(IToolbarHost host);
}
}
@@ -0,0 +1,26 @@
using System.Windows.Input;
namespace Ink_Canvas.Controls.Toolbar.Items
{
/// <summary>
/// 清空按钮。位置:夹在颜色面板与 StackPanelCanvasControls 之间,
/// 所以用 BeforeAnchor 锚到 StackPanelCanvasControls。
/// </summary>
internal sealed class ClearToolItem : ToolbarImageButtonItemBase
{
public override string Id => "builtin.clear";
public override string LocalizationKey => "FloatingBar_Clear";
public override ToolbarSlot DefaultSlot => ToolbarSlot.FloatingBarMain;
public override int DefaultOrder => 0;
public override ToolbarInsertPosition DefaultPosition => ToolbarInsertPosition.BeforeAnchor;
public override string DefaultAnchorName => "StackPanelCanvasControls";
protected override string IconBrushResourceKey => "RedBrush";
protected override void OnClick(IToolbarHost host, object sender, MouseButtonEventArgs e)
=> host.Window.SymbolIconDelete_MouseUp(sender, e);
protected override void AfterBuild(IToolbarHost host, ToolbarImageButton view)
=> host.Window.AttachSymbolIconDelete(view);
}
}
@@ -0,0 +1,18 @@
using System.Windows.Input;
namespace Ink_Canvas.Controls.Toolbar.Items
{
internal sealed class CursorToolItem : ToolbarImageButtonItemBase
{
public override string Id => "builtin.cursor";
public override string LocalizationKey => "FloatingBar_Mouse";
public override ToolbarSlot DefaultSlot => ToolbarSlot.FloatingBarMain;
public override int DefaultOrder => 100;
protected override void OnClick(IToolbarHost host, object sender, MouseButtonEventArgs e)
=> host.Window.CursorIcon_Click(sender, e);
protected override void AfterBuild(IToolbarHost host, ToolbarImageButton view)
=> host.Window.AttachCursorIconView(view);
}
}
@@ -0,0 +1,19 @@
using System.Windows.Input;
namespace Ink_Canvas.Controls.Toolbar.Items
{
internal sealed class CursorWithDelToolItem : ToolbarImageButtonItemBase
{
public override string Id => "builtin.cursorWithDel";
public override string LocalizationKey => "FloatingBar_ClearAndMouse";
public override ToolbarSlot DefaultSlot => ToolbarSlot.FloatingBarCanvasControls;
public override int DefaultOrder => 320;
public override ToolbarInsertPosition DefaultPosition => ToolbarInsertPosition.Append;
protected override void OnClick(IToolbarHost host, object sender, MouseButtonEventArgs e)
=> host.Window.CursorWithDelIcon_Click(sender, e);
protected override void AfterBuild(IToolbarHost host, ToolbarImageButton view)
=> host.Window.AttachCursorWithDelBtn(view);
}
}
@@ -0,0 +1,18 @@
using System.Windows.Input;
namespace Ink_Canvas.Controls.Toolbar.Items
{
internal sealed class EraserByStrokesToolItem : ToolbarImageButtonItemBase
{
public override string Id => "builtin.eraserByStrokes";
public override string LocalizationKey => "FloatingBar_StrokeEraser";
public override ToolbarSlot DefaultSlot => ToolbarSlot.FloatingBarCanvasControls;
public override int DefaultOrder => 110;
protected override void OnClick(IToolbarHost host, object sender, MouseButtonEventArgs e)
=> host.Window.EraserIconByStrokes_Click(sender, e);
protected override void AfterBuild(IToolbarHost host, ToolbarImageButton view)
=> host.Window.AttachEraserByStrokesIcon(view);
}
}
@@ -0,0 +1,18 @@
using System.Windows.Input;
namespace Ink_Canvas.Controls.Toolbar.Items
{
internal sealed class EraserToolItem : ToolbarImageButtonItemBase
{
public override string Id => "builtin.eraser";
public override string LocalizationKey => "FloatingBar_AreaEraser";
public override ToolbarSlot DefaultSlot => ToolbarSlot.FloatingBarCanvasControls;
public override int DefaultOrder => 100;
protected override void OnClick(IToolbarHost host, object sender, MouseButtonEventArgs e)
=> host.Window.EraserIcon_Click(sender, e);
protected override void AfterBuild(IToolbarHost host, ToolbarImageButton view)
=> host.Window.AttachEraserIcon(view);
}
}
@@ -0,0 +1,20 @@
using System.Windows.Input;
namespace Ink_Canvas.Controls.Toolbar.Items
{
internal sealed class FoldToolItem : ToolbarImageButtonItemBase
{
public override string Id => "builtin.fold";
public override string LocalizationKey => "FloatingBar_Hide";
public override ToolbarSlot DefaultSlot => ToolbarSlot.FloatingBarEnd;
public override int DefaultOrder => 120;
public override ToolbarInsertPosition DefaultPosition => ToolbarInsertPosition.AfterAnchor;
public override string DefaultAnchorName => "FloatingBarEndSeparator";
protected override void OnClick(IToolbarHost host, object sender, MouseButtonEventArgs e)
=> host.Window.FoldFloatingBar_MouseUp(sender, e);
protected override void AfterBuild(IToolbarHost host, ToolbarImageButton view)
=> host.Window.AttachFoldIcon(view);
}
}
@@ -0,0 +1,18 @@
using System.Windows.Input;
namespace Ink_Canvas.Controls.Toolbar.Items
{
internal sealed class PenToolItem : ToolbarImageButtonItemBase
{
public override string Id => "builtin.pen";
public override string LocalizationKey => "FloatingBar_Annotate";
public override ToolbarSlot DefaultSlot => ToolbarSlot.FloatingBarMain;
public override int DefaultOrder => 110;
protected override void OnClick(IToolbarHost host, object sender, MouseButtonEventArgs e)
=> host.Window.PenIcon_Click(sender, e);
protected override void AfterBuild(IToolbarHost host, ToolbarImageButton view)
=> host.Window.AttachPenIconView(view);
}
}
@@ -0,0 +1,23 @@
using System.Windows.Input;
namespace Ink_Canvas.Controls.Toolbar.Items
{
internal sealed class RedoToolItem : ToolbarImageButtonItemBase
{
public override string Id => "builtin.redo";
public override string LocalizationKey => "Board_Redo";
public override ToolbarSlot DefaultSlot => ToolbarSlot.FloatingBarCanvasControls;
public override int DefaultOrder => 310;
public override ToolbarInsertPosition DefaultPosition => ToolbarInsertPosition.Append;
protected override void OnClick(IToolbarHost host, object sender, MouseButtonEventArgs e)
=> host.Window.SymbolIconRedo_MouseUp(sender, e);
protected override void AfterBuild(IToolbarHost host, ToolbarImageButton view)
{
host.Window.AttachSymbolIconRedo(view);
view.SetBinding(System.Windows.UIElement.IsEnabledProperty,
new System.Windows.Data.Binding("IsEnabled") { ElementName = "BtnRedo" });
}
}
}
@@ -0,0 +1,18 @@
using System.Windows.Input;
namespace Ink_Canvas.Controls.Toolbar.Items
{
internal sealed class SelectToolItem : ToolbarImageButtonItemBase
{
public override string Id => "builtin.select";
public override string LocalizationKey => "FloatingBar_LassoSelect";
public override ToolbarSlot DefaultSlot => ToolbarSlot.FloatingBarCanvasControls;
public override int DefaultOrder => 120;
protected override void OnClick(IToolbarHost host, object sender, MouseButtonEventArgs e)
=> host.Window.SymbolIconSelect_MouseUp(sender, e);
protected override void AfterBuild(IToolbarHost host, ToolbarImageButton view)
=> host.Window.AttachSymbolIconSelect(view);
}
}
@@ -0,0 +1,18 @@
using System.Windows.Input;
namespace Ink_Canvas.Controls.Toolbar.Items
{
internal sealed class ShapeDrawToolItem : ToolbarImageButtonItemBase
{
public override string Id => "builtin.shapeDraw";
public override string LocalizationKey => "FloatingBar_Geometry";
public override ToolbarSlot DefaultSlot => ToolbarSlot.FloatingBarCanvasControls;
public override int DefaultOrder => 130;
protected override void OnClick(IToolbarHost host, object sender, MouseButtonEventArgs e)
=> host.Window.ImageDrawShape_MouseUp(sender, e);
protected override void AfterBuild(IToolbarHost host, ToolbarImageButton view)
=> host.Window.AttachShapeDrawBtn(view);
}
}
@@ -0,0 +1,47 @@
using Ink_Canvas.Properties;
using System;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media;
namespace Ink_Canvas.Controls.Toolbar.Items
{
/// <summary>
/// 通用 ToolbarImageButton 工具栏条目基类——大幅减少每个按钮的样板代码。
/// 派生类通常只需给 Id / 本地化键 / Slot / Order / 点击处理 / Attach 回填。
/// </summary>
internal abstract class ToolbarImageButtonItemBase : IToolbarItem
{
public abstract string Id { get; }
public abstract string LocalizationKey { get; }
public abstract ToolbarSlot DefaultSlot { get; }
public abstract int DefaultOrder { get; }
public virtual bool DefaultVisible => true;
public virtual ToolbarInsertPosition DefaultPosition => ToolbarInsertPosition.Prepend;
public virtual string DefaultAnchorName => null;
/// <summary>DynamicResource 名称,用于 IconBrush。默认为 null(使用控件自带前景色)。</summary>
protected virtual string IconBrushResourceKey => null;
protected abstract void OnClick(IToolbarHost host, object sender, MouseButtonEventArgs e);
/// <summary>构建后调用,用于回填 MainWindow 的原命名属性(partial 扩展里的 Attach*)。可选。</summary>
protected virtual void AfterBuild(IToolbarHost host, ToolbarImageButton view) { }
public FrameworkElement BuildView(IToolbarHost host)
{
var btn = new ToolbarImageButton
{
Label = Strings.GetString(LocalizationKey) ?? LocalizationKey
};
if (!string.IsNullOrEmpty(IconBrushResourceKey))
{
if (btn.TryFindResource(IconBrushResourceKey) is Brush brush) btn.IconBrush = brush;
else btn.SetResourceReference(ToolbarImageButton.IconBrushProperty, IconBrushResourceKey);
}
btn.ButtonMouseUp += (s, e) => OnClick(host, s, e);
AfterBuild(host, btn);
return btn;
}
}
}
@@ -0,0 +1,20 @@
using System.Windows.Input;
namespace Ink_Canvas.Controls.Toolbar.Items
{
internal sealed class ToolsToolItem : ToolbarImageButtonItemBase
{
public override string Id => "builtin.tools";
public override string LocalizationKey => "Board_Tools";
public override ToolbarSlot DefaultSlot => ToolbarSlot.FloatingBarEnd;
public override int DefaultOrder => 110;
public override ToolbarInsertPosition DefaultPosition => ToolbarInsertPosition.AfterAnchor;
public override string DefaultAnchorName => "FloatingBarEndSeparator";
protected override void OnClick(IToolbarHost host, object sender, MouseButtonEventArgs e)
=> host.Window.SymbolIconTools_MouseUp(sender, e);
protected override void AfterBuild(IToolbarHost host, ToolbarImageButton view)
=> host.Window.AttachToolsBtn(view);
}
}
@@ -0,0 +1,23 @@
using System.Windows.Input;
namespace Ink_Canvas.Controls.Toolbar.Items
{
internal sealed class UndoToolItem : ToolbarImageButtonItemBase
{
public override string Id => "builtin.undo";
public override string LocalizationKey => "Board_Undo";
public override ToolbarSlot DefaultSlot => ToolbarSlot.FloatingBarCanvasControls;
public override int DefaultOrder => 300;
public override ToolbarInsertPosition DefaultPosition => ToolbarInsertPosition.Append;
protected override void OnClick(IToolbarHost host, object sender, MouseButtonEventArgs e)
=> host.Window.SymbolIconUndo_MouseUp(sender, e);
protected override void AfterBuild(IToolbarHost host, ToolbarImageButton view)
{
host.Window.AttachSymbolIconUndo(view);
view.SetBinding(System.Windows.UIElement.IsEnabledProperty,
new System.Windows.Data.Binding("IsEnabled") { ElementName = "BtnUndo" });
}
}
}
@@ -0,0 +1,20 @@
using System.Windows.Input;
namespace Ink_Canvas.Controls.Toolbar.Items
{
internal sealed class WhiteboardToolItem : ToolbarImageButtonItemBase
{
public override string Id => "builtin.whiteboard";
public override string LocalizationKey => "FloatingBar_Whiteboard";
public override ToolbarSlot DefaultSlot => ToolbarSlot.FloatingBarEnd;
public override int DefaultOrder => 100;
public override ToolbarInsertPosition DefaultPosition => ToolbarInsertPosition.AfterAnchor;
public override string DefaultAnchorName => "FloatingBarEndSeparator";
protected override void OnClick(IToolbarHost host, object sender, MouseButtonEventArgs e)
=> host.Window.ImageBlackboard_MouseUp(sender, e);
protected override void AfterBuild(IToolbarHost host, ToolbarImageButton view)
=> host.Window.AttachWhiteboardBtn(view);
}
}
@@ -0,0 +1,34 @@
using System.Collections.Generic;
using System.Windows;
namespace Ink_Canvas.Controls.Toolbar
{
/// <summary>
/// MainWindow 版的 IToolbarHost 实现。Phase 1 直接把 MainWindow 引用暴露给插件,
/// 插件可通过 host.Window 访问私有/内部成员(partial class 扩展或 internal 字段)。
/// 后续阶段逐步把具体行为抽成 Host 上的方法/事件,收窄这个接口。
/// </summary>
public sealed class ToolbarHost : IToolbarHost
{
private readonly Dictionary<string, FrameworkElement> _views = new Dictionary<string, FrameworkElement>();
public ToolbarHost(MainWindow window)
{
Window = window;
}
public MainWindow Window { get; }
public void RegisterView(string id, FrameworkElement view)
{
if (string.IsNullOrEmpty(id) || view == null) return;
_views[id] = view;
}
public FrameworkElement FindView(string id)
{
if (string.IsNullOrEmpty(id)) return null;
return _views.TryGetValue(id, out var v) ? v : null;
}
}
}
@@ -0,0 +1,14 @@
namespace Ink_Canvas.Controls.Toolbar
{
public enum ToolbarInsertPosition
{
/// <summary>从容器头部依次插入;Order 小的在前。</summary>
Prepend,
/// <summary>追加到容器末尾。</summary>
Append,
/// <summary>插入到由 AnchorName 指定的已有元素之前。</summary>
BeforeAnchor,
/// <summary>插入到由 AnchorName 指定的已有元素之后(同一锚点多项按 Order 依次排列)。</summary>
AfterAnchor
}
}
@@ -0,0 +1,33 @@
using Newtonsoft.Json;
using System.Collections.Generic;
namespace Ink_Canvas.Controls.Toolbar
{
/// <summary>
/// 单个工具栏按钮的用户配置(可见性、顺序、所属 slot、插入位置)。
/// 由 Settings.Toolbar 持久化。
/// </summary>
public class ToolbarItemConfig
{
[JsonProperty("visible")]
public bool Visible { get; set; } = true;
[JsonProperty("order")]
public int Order { get; set; }
[JsonProperty("slot")]
public ToolbarSlot Slot { get; set; } = ToolbarSlot.FloatingBarMain;
[JsonProperty("position")]
public ToolbarInsertPosition Position { get; set; } = ToolbarInsertPosition.Prepend;
[JsonProperty("anchorName")]
public string AnchorName { get; set; }
}
public class ToolbarLayoutSettings
{
[JsonProperty("items")]
public Dictionary<string, ToolbarItemConfig> Items { get; set; } = new Dictionary<string, ToolbarItemConfig>();
}
}
@@ -0,0 +1,161 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Windows;
using System.Windows.Controls;
using Ink_Canvas.Helpers;
namespace Ink_Canvas.Controls.Toolbar
{
/// <summary>
/// 扫描当前程序集里的 IToolbarItem 实现,按用户配置(Settings.Toolbar)排序/过滤后注入到目标容器。
/// </summary>
public static class ToolbarRegistry
{
private static List<IToolbarItem> _items;
public static IReadOnlyList<IToolbarItem> Discover()
{
if (_items != null) return _items;
var itemType = typeof(IToolbarItem);
_items = Assembly.GetExecutingAssembly()
.GetTypes()
.Where(t => !t.IsAbstract && !t.IsInterface && itemType.IsAssignableFrom(t))
.Select(t =>
{
try { return (IToolbarItem)Activator.CreateInstance(t); }
catch (Exception ex)
{
LogHelper.WriteLogToFile($"ToolbarRegistry: 实例化 {t.FullName} 失败: {ex.Message}", LogHelper.LogType.Warning);
return null;
}
})
.Where(i => i != null)
.ToList();
return _items;
}
/// <summary>按 slot 分配工具栏条目到对应容器。调用者负责清空目标容器里要被接管的旧内容。</summary>
public static void Populate(IToolbarHost host, IDictionary<ToolbarSlot, Panel> slots, ToolbarLayoutSettings layout)
{
if (host == null || slots == null) return;
layout = layout ?? new ToolbarLayoutSettings();
var grouped = new Dictionary<ToolbarSlot, List<(IToolbarItem item, ToolbarItemConfig cfg)>>();
foreach (var item in Discover())
{
if (!layout.Items.TryGetValue(item.Id, out var cfg))
{
cfg = new ToolbarItemConfig
{
Visible = item.DefaultVisible,
Order = item.DefaultOrder,
Slot = item.DefaultSlot,
Position = item.DefaultPosition,
AnchorName = item.DefaultAnchorName
};
}
if (!cfg.Visible) continue;
if (!grouped.TryGetValue(cfg.Slot, out var list))
{
list = new List<(IToolbarItem, ToolbarItemConfig)>();
grouped[cfg.Slot] = list;
}
list.Add((item, cfg));
}
foreach (var kv in grouped)
{
if (!slots.TryGetValue(kv.Key, out var container) || container == null) continue;
InjectIntoContainer(host, container, kv.Value);
}
}
private static void InjectIntoContainer(IToolbarHost host, Panel container,
List<(IToolbarItem item, ToolbarItemConfig cfg)> entries)
{
// 按 Position 分桶,每桶内按 Order 升序。
var prepend = entries.Where(e => e.cfg.Position == ToolbarInsertPosition.Prepend).OrderBy(e => e.cfg.Order).ToList();
var append = entries.Where(e => e.cfg.Position == ToolbarInsertPosition.Append).OrderBy(e => e.cfg.Order).ToList();
var before = entries.Where(e => e.cfg.Position == ToolbarInsertPosition.BeforeAnchor).ToList();
var after = entries.Where(e => e.cfg.Position == ToolbarInsertPosition.AfterAnchor).ToList();
var prependIndex = 0;
foreach (var entry in prepend)
{
var view = BuildAndRegister(host, entry.item);
if (view == null) continue;
container.Children.Insert(prependIndex++, view);
}
foreach (var entry in append)
{
var view = BuildAndRegister(host, entry.item);
if (view == null) continue;
container.Children.Add(view);
}
foreach (var group in before.GroupBy(e => e.cfg.AnchorName))
{
var anchor = FindNamedChild(container, group.Key);
if (anchor == null)
{
LogHelper.WriteLogToFile($"ToolbarRegistry: 未找到锚点 '{group.Key}' (BeforeAnchor)", LogHelper.LogType.Warning);
continue;
}
var idx = container.Children.IndexOf(anchor);
foreach (var entry in group.OrderBy(e => e.cfg.Order))
{
var view = BuildAndRegister(host, entry.item);
if (view == null) continue;
container.Children.Insert(idx++, view);
}
}
foreach (var group in after.GroupBy(e => e.cfg.AnchorName))
{
var anchor = FindNamedChild(container, group.Key);
if (anchor == null)
{
LogHelper.WriteLogToFile($"ToolbarRegistry: 未找到锚点 '{group.Key}' (AfterAnchor)", LogHelper.LogType.Warning);
continue;
}
var idx = container.Children.IndexOf(anchor) + 1;
foreach (var entry in group.OrderBy(e => e.cfg.Order))
{
var view = BuildAndRegister(host, entry.item);
if (view == null) continue;
container.Children.Insert(idx++, view);
}
}
}
private static UIElement FindNamedChild(Panel container, string name)
{
if (string.IsNullOrEmpty(name)) return null;
foreach (UIElement child in container.Children)
{
if (child is FrameworkElement fe && fe.Name == name) return child;
}
return null;
}
private static FrameworkElement BuildAndRegister(IToolbarHost host, IToolbarItem item)
{
try
{
var view = item.BuildView(host);
if (view == null) return null;
host.RegisterView(item.Id, view);
return view;
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"ToolbarRegistry: 构建 {item.Id} 失败: {ex.Message}", LogHelper.LogType.Warning);
return null;
}
}
}
}
@@ -0,0 +1,11 @@
namespace Ink_Canvas.Controls.Toolbar
{
public enum ToolbarSlot
{
FloatingBarMain,
FloatingBarCanvasControls,
FloatingBarEnd,
BlackboardLeft,
BlackboardRight
}
}
-176
View File
@@ -1,176 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. -->
<xs:element name="Weavers">
<xs:complexType>
<xs:all>
<xs:element name="Costura" minOccurs="0" maxOccurs="1">
<xs:complexType>
<xs:all>
<xs:element minOccurs="0" maxOccurs="1" name="ExcludeAssemblies" type="xs:string">
<xs:annotation>
<xs:documentation>A list of assembly names to exclude from the default action of "embed all Copy Local references", delimited with line breaks</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element minOccurs="0" maxOccurs="1" name="IncludeAssemblies" type="xs:string">
<xs:annotation>
<xs:documentation>A list of assembly names to include from the default action of "embed all Copy Local references", delimited with line breaks.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element minOccurs="0" maxOccurs="1" name="ExcludeRuntimeAssemblies" type="xs:string">
<xs:annotation>
<xs:documentation>A list of runtime assembly names to exclude from the default action of "embed all Copy Local references", delimited with line breaks</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element minOccurs="0" maxOccurs="1" name="IncludeRuntimeAssemblies" type="xs:string">
<xs:annotation>
<xs:documentation>A list of runtime assembly names to include from the default action of "embed all Copy Local references", delimited with line breaks.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element minOccurs="0" maxOccurs="1" name="Unmanaged32Assemblies" type="xs:string">
<xs:annotation>
<xs:documentation>Obsolete, use UnmanagedWinX86Assemblies instead</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element minOccurs="0" maxOccurs="1" name="UnmanagedWinX86Assemblies" type="xs:string">
<xs:annotation>
<xs:documentation>A list of unmanaged X86 (32 bit) assembly names to include, delimited with line breaks.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element minOccurs="0" maxOccurs="1" name="Unmanaged64Assemblies" type="xs:string">
<xs:annotation>
<xs:documentation>Obsolete, use UnmanagedWinX64Assemblies instead.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element minOccurs="0" maxOccurs="1" name="UnmanagedWinX64Assemblies" type="xs:string">
<xs:annotation>
<xs:documentation>A list of unmanaged X64 (64 bit) assembly names to include, delimited with line breaks.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element minOccurs="0" maxOccurs="1" name="UnmanagedWinArm64Assemblies" type="xs:string">
<xs:annotation>
<xs:documentation>A list of unmanaged Arm64 (64 bit) assembly names to include, delimited with line breaks.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element minOccurs="0" maxOccurs="1" name="PreloadOrder" type="xs:string">
<xs:annotation>
<xs:documentation>The order of preloaded assemblies, delimited with line breaks.</xs:documentation>
</xs:annotation>
</xs:element>
</xs:all>
<xs:attribute name="CreateTemporaryAssemblies" type="xs:boolean">
<xs:annotation>
<xs:documentation>This will copy embedded files to disk before loading them into memory. This is helpful for some scenarios that expected an assembly to be loaded from a physical file.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="IncludeDebugSymbols" type="xs:boolean">
<xs:annotation>
<xs:documentation>Controls if .pdbs for reference assemblies are also embedded.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="IncludeRuntimeReferences" type="xs:boolean">
<xs:annotation>
<xs:documentation>Controls if runtime assemblies are also embedded.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="UseRuntimeReferencePaths" type="xs:boolean">
<xs:annotation>
<xs:documentation>Controls whether the runtime assemblies are embedded with their full path or only with their assembly name.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="DisableCompression" type="xs:boolean">
<xs:annotation>
<xs:documentation>Embedded assemblies are compressed by default, and uncompressed when they are loaded. You can turn compression off with this option.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="DisableCleanup" type="xs:boolean">
<xs:annotation>
<xs:documentation>As part of Costura, embedded assemblies are no longer included as part of the build. This cleanup can be turned off.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="DisableEventSubscription" type="xs:boolean">
<xs:annotation>
<xs:documentation>The attach method no longer subscribes to the `AppDomain.AssemblyResolve` (.NET 4.x) and `AssemblyLoadContext.Resolving` (.NET 6.0+) events.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="LoadAtModuleInit" type="xs:boolean">
<xs:annotation>
<xs:documentation>Costura by default will load as part of the module initialization. This flag disables that behavior. Make sure you call CosturaUtility.Initialize() somewhere in your code.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="IgnoreSatelliteAssemblies" type="xs:boolean">
<xs:annotation>
<xs:documentation>Costura will by default use assemblies with a name like 'resources.dll' as a satellite resource and prepend the output path. This flag disables that behavior.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="ExcludeAssemblies" type="xs:string">
<xs:annotation>
<xs:documentation>A list of assembly names to exclude from the default action of "embed all Copy Local references", delimited with |</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="IncludeAssemblies" type="xs:string">
<xs:annotation>
<xs:documentation>A list of assembly names to include from the default action of "embed all Copy Local references", delimited with |.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="ExcludeRuntimeAssemblies" type="xs:string">
<xs:annotation>
<xs:documentation>A list of runtime assembly names to exclude from the default action of "embed all Copy Local references", delimited with |</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="IncludeRuntimeAssemblies" type="xs:string">
<xs:annotation>
<xs:documentation>A list of runtime assembly names to include from the default action of "embed all Copy Local references", delimited with |.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="Unmanaged32Assemblies" type="xs:string">
<xs:annotation>
<xs:documentation>Obsolete, use UnmanagedWinX86Assemblies instead</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="UnmanagedWinX86Assemblies" type="xs:string">
<xs:annotation>
<xs:documentation>A list of unmanaged X86 (32 bit) assembly names to include, delimited with |.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="Unmanaged64Assemblies" type="xs:string">
<xs:annotation>
<xs:documentation>Obsolete, use UnmanagedWinX64Assemblies instead</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="UnmanagedWinX64Assemblies" type="xs:string">
<xs:annotation>
<xs:documentation>A list of unmanaged X64 (64 bit) assembly names to include, delimited with |.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="UnmanagedWinArm64Assemblies" type="xs:string">
<xs:annotation>
<xs:documentation>A list of unmanaged Arm64 (64 bit) assembly names to include, delimited with |.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="PreloadOrder" type="xs:string">
<xs:annotation>
<xs:documentation>The order of preloaded assemblies, delimited with |.</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
</xs:element>
</xs:all>
<xs:attribute name="VerifyAssembly" type="xs:boolean">
<xs:annotation>
<xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="VerifyIgnoreCodes" type="xs:string">
<xs:annotation>
<xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="GenerateXsd" type="xs:boolean">
<xs:annotation>
<xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
</xs:element>
</xs:schema>
+385 -30
View File
@@ -95,18 +95,22 @@ namespace Ink_Canvas.Helpers
// 使用改进的贝塞尔曲线拟合
var smoothedPoints = ApplyImprovedBezierSmoothing(originalPoints);
System.Diagnostics.Debug.WriteLine($"AsyncAdvancedBezierSmoothing: 原始点数={originalPoints.Length}, 平滑后点数={smoothedPoints.Length}");
cancellationToken.ThrowIfCancellationRequested();
// 严格控制点数,避免产生过多点
if (smoothedPoints.Length > originalPoints.Length * 2)
// 放宽点数限制
if (smoothedPoints.Length > originalPoints.Length * 3.0)
{
System.Diagnostics.Debug.WriteLine($"AsyncAdvancedBezierSmoothing: 点数过多,进行重采样");
// 如果点数增加太多,进行重采样
smoothedPoints = ResampleEquidistantOptimized(smoothedPoints, ResampleInterval);
}
// 最终检查:确保点数不会过多
if (smoothedPoints.Length > originalPoints.Length * 1.5)
// 进一步放宽最终检查
if (smoothedPoints.Length > originalPoints.Length * 2.5)
{
System.Diagnostics.Debug.WriteLine($"AsyncAdvancedBezierSmoothing: 重采样后点数仍然过多,返回原始笔画");
// 如果仍然太多点,使用原始笔画
return stroke;
}
@@ -117,6 +121,7 @@ namespace Ink_Canvas.Helpers
DrawingAttributes = stroke.DrawingAttributes.Clone()
};
System.Diagnostics.Debug.WriteLine($"AsyncAdvancedBezierSmoothing: 成功创建平滑笔画");
return smoothedStroke;
}
@@ -125,33 +130,42 @@ namespace Ink_Canvas.Helpers
/// </summary>
private StylusPoint[] ApplyImprovedBezierSmoothing(StylusPoint[] points)
{
if (points.Length < 4) return points;
if (points.Length < 6) return points; // 5次贝塞尔需要6个点
var result = new List<StylusPoint>();
// 添加第一个点
result.Add(points[0]);
// 使用非重叠的窗口进行贝塞尔曲线拟合
for (int i = 0; i < points.Length - 3; i += 3) // 每次移动3个点,避免重叠
// 使用5次贝塞尔曲线,每次移动1个点确保连续性
for (int i = 0; i < points.Length - 5; i++)
{
var p0 = points[i];
var p1 = points[Math.Min(i + 1, points.Length - 1)];
var p2 = points[Math.Min(i + 2, points.Length - 1)];
var p3 = points[Math.Min(i + 3, points.Length - 1)];
var p1 = points[i + 1];
var p2 = points[i + 2];
var p3 = points[i + 3];
var p4 = points[i + 4];
var p5 = points[i + 5];
// 计算改进的控制点
var controlPoints = CalculateImprovedControlPoints(p0, p1, p2, p3);
// 计算5次贝塞尔的控制点
var controlPoints = CalculateQuinticControlPoints(p0, p1, p2, p3, p4, p5);
// 限制插值步数,避免点数爆炸
int steps = Math.Min(UseAdaptiveInterpolation ?
CalculateAdaptiveSteps(p0, p1, p2, p3) : InterpolationSteps, 16);
// 生成贝塞尔曲线点,但跳过第一个点避免重复
for (int j = 1; j <= steps; j++)
// 生成插值点
if (i == 0)
{
double t = (double)j / steps;
var bezierPoint = CubicBezierWithControlPoints(controlPoints, t, p0, p3);
// 第一个窗口:生成更多插值点
for (int j = 1; j <= 4; j++)
{
double t = (double)j / 5;
var bezierPoint = CalculateQuinticBezierPoint(p0, controlPoints, p5, t);
result.Add(bezierPoint);
}
}
else
{
// 后续窗口:只生成最后一个插值点,避免重复
double t = 4.0 / 5.0; // 只取最后一个插值点
var bezierPoint = CalculateQuinticBezierPoint(p0, controlPoints, p5, t);
result.Add(bezierPoint);
}
}
@@ -159,8 +173,209 @@ namespace Ink_Canvas.Helpers
// 添加最后一个点
result.Add(points[points.Length - 1]);
// 去重和优化点数
return RemoveDuplicatePoints(result.ToArray());
System.Diagnostics.Debug.WriteLine($"ApplyImprovedBezierSmoothing: 原始点数={points.Length}, 生成点数={result.Count}");
// 使用更宽松的去重
return RemoveDuplicatePointsLoose(result.ToArray());
}
/// <summary>
/// 5次贝塞尔曲线控制点计算
/// </summary>
private (Point cp1, Point cp2, Point cp3, Point cp4) CalculateQuinticControlPoints(
StylusPoint p0, StylusPoint p1, StylusPoint p2, StylusPoint p3, StylusPoint p4, StylusPoint p5)
{
// 计算控制点距离(基于相邻点距离)
double dist1 = Math.Sqrt((p1.X - p0.X) * (p1.X - p0.X) + (p1.Y - p0.Y) * (p1.Y - p0.Y));
double dist2 = Math.Sqrt((p2.X - p1.X) * (p2.X - p1.X) + (p2.Y - p1.Y) * (p2.Y - p1.Y));
double dist3 = Math.Sqrt((p4.X - p3.X) * (p4.X - p3.X) + (p4.Y - p3.Y) * (p4.Y - p3.Y));
double dist4 = Math.Sqrt((p5.X - p4.X) * (p5.X - p4.X) + (p5.Y - p4.Y) * (p5.Y - p4.Y));
// 使用更小的控制点距离,产生超平滑的曲线
double controlDist1 = dist1 * 0.15;
double controlDist2 = dist2 * 0.15;
double controlDist3 = dist3 * 0.15;
double controlDist4 = dist4 * 0.15;
// 计算控制点方向 - 使用更平滑的方向计算
double dir1X = p2.X - p0.X;
double dir1Y = p2.Y - p0.Y;
double dir2X = p3.X - p1.X;
double dir2Y = p3.Y - p1.Y;
double dir3X = p4.X - p2.X;
double dir3Y = p4.Y - p2.Y;
double dir4X = p5.X - p3.X;
double dir4Y = p5.Y - p3.Y;
// 归一化方向
NormalizeVector(ref dir1X, ref dir1Y);
NormalizeVector(ref dir2X, ref dir2Y);
NormalizeVector(ref dir3X, ref dir3Y);
NormalizeVector(ref dir4X, ref dir4Y);
// 计算控制点
var cp1 = new Point(p1.X + dir1X * controlDist1, p1.Y + dir1Y * controlDist1);
var cp2 = new Point(p2.X + dir2X * controlDist2, p2.Y + dir2Y * controlDist2);
var cp3 = new Point(p3.X - dir3X * controlDist3, p3.Y - dir3Y * controlDist3);
var cp4 = new Point(p4.X - dir4X * controlDist4, p4.Y - dir4Y * controlDist4);
return (cp1, cp2, cp3, cp4);
}
/// <summary>
/// 归一化向量
/// </summary>
private void NormalizeVector(ref double x, ref double y)
{
double length = Math.Sqrt(x * x + y * y);
if (length > 0)
{
x /= length;
y /= length;
}
}
/// <summary>
/// 5次贝塞尔曲线点计算
/// </summary>
private StylusPoint CalculateQuinticBezierPoint(StylusPoint p0, (Point cp1, Point cp2, Point cp3, Point cp4) controlPoints, StylusPoint p5, double t)
{
double oneMinusT = 1 - t;
double oneMinusT2 = oneMinusT * oneMinusT;
double oneMinusT3 = oneMinusT2 * oneMinusT;
double oneMinusT4 = oneMinusT3 * oneMinusT;
double oneMinusT5 = oneMinusT4 * oneMinusT;
double t2 = t * t;
double t3 = t2 * t;
double t4 = t3 * t;
double t5 = t4 * t;
// 5次贝塞尔曲线公式
double x = oneMinusT5 * p0.X +
5 * oneMinusT4 * t * controlPoints.cp1.X +
10 * oneMinusT3 * t2 * controlPoints.cp2.X +
10 * oneMinusT2 * t3 * controlPoints.cp3.X +
5 * oneMinusT * t4 * controlPoints.cp4.X +
t5 * p5.X;
double y = oneMinusT5 * p0.Y +
5 * oneMinusT4 * t * controlPoints.cp1.Y +
10 * oneMinusT3 * t2 * controlPoints.cp2.Y +
10 * oneMinusT2 * t3 * controlPoints.cp3.Y +
5 * oneMinusT * t4 * controlPoints.cp4.Y +
t5 * p5.Y;
// 压力插值 - 使用线性插值
float pressure = (float)((1 - t) * p0.PressureFactor + t * p5.PressureFactor);
return new StylusPoint(x, y, Math.Max(pressure, 0.1f));
}
/// <summary>
/// 简化的控制点计算
/// </summary>
private (Point cp1, Point cp2) CalculateSimpleControlPoints(StylusPoint p0, StylusPoint p1, StylusPoint p2, StylusPoint p3)
{
// 计算控制点距离(基于线段长度)
double dist1 = Math.Sqrt((p1.X - p0.X) * (p1.X - p0.X) + (p1.Y - p0.Y) * (p1.Y - p0.Y));
double dist2 = Math.Sqrt((p3.X - p2.X) * (p3.X - p2.X) + (p3.Y - p2.Y) * (p3.Y - p2.Y));
// 使用更小的控制点距离,产生更平滑的曲线
double controlDist1 = dist1 * 0.2; // 进一步减少控制点影响
double controlDist2 = dist2 * 0.2;
// 计算控制点方向 - 使用更平滑的方向计算
double dir1X = p2.X - p0.X; // 使用更远的点计算方向
double dir1Y = p2.Y - p0.Y;
double dir2X = p3.X - p1.X;
double dir2Y = p3.Y - p1.Y;
// 归一化方向
double len1 = Math.Sqrt(dir1X * dir1X + dir1Y * dir1Y);
double len2 = Math.Sqrt(dir2X * dir2X + dir2Y * dir2Y);
if (len1 > 0)
{
dir1X /= len1;
dir1Y /= len1;
}
if (len2 > 0)
{
dir2X /= len2;
dir2Y /= len2;
}
// 计算控制点
var cp1 = new Point(
p1.X + dir1X * controlDist1,
p1.Y + dir1Y * controlDist1
);
var cp2 = new Point(
p2.X - dir2X * controlDist2,
p2.Y - dir2Y * controlDist2
);
return (cp1, cp2);
}
/// <summary>
/// 宽松的去重算法
/// </summary>
private StylusPoint[] RemoveDuplicatePointsLoose(StylusPoint[] points)
{
if (points.Length < 2) return points;
var result = new List<StylusPoint>();
result.Add(points[0]);
double minDistance = 0.1; // 非常小的距离阈值,几乎不去重
for (int i = 1; i < points.Length; i++)
{
var lastPoint = result[result.Count - 1];
var currentPoint = points[i];
// 计算距离
double distance = Math.Sqrt(
(currentPoint.X - lastPoint.X) * (currentPoint.X - lastPoint.X) +
(currentPoint.Y - lastPoint.Y) * (currentPoint.Y - lastPoint.Y));
// 如果距离足够大,添加这个点
if (distance >= minDistance)
{
result.Add(currentPoint);
}
}
System.Diagnostics.Debug.WriteLine($"RemoveDuplicatePointsLoose: 输入点数={points.Length}, 输出点数={result.Count}");
return result.ToArray();
}
/// <summary>
/// 计算贝塞尔曲线上的点
/// </summary>
private StylusPoint CalculateBezierPoint(StylusPoint p0, Point cp1, Point cp2, StylusPoint p3, double t)
{
double x = Math.Pow(1 - t, 3) * p0.X +
3 * Math.Pow(1 - t, 2) * t * cp1.X +
3 * (1 - t) * Math.Pow(t, 2) * cp2.X +
Math.Pow(t, 3) * p3.X;
double y = Math.Pow(1 - t, 3) * p0.Y +
3 * Math.Pow(1 - t, 2) * t * cp1.Y +
3 * (1 - t) * Math.Pow(t, 2) * cp2.Y +
Math.Pow(t, 3) * p3.Y;
// 压力插值
float pressure = (float)(Math.Pow(1 - t, 3) * p0.PressureFactor +
3 * Math.Pow(1 - t, 2) * t * p0.PressureFactor +
3 * (1 - t) * Math.Pow(t, 2) * p3.PressureFactor +
Math.Pow(t, 3) * p3.PressureFactor);
return new StylusPoint(x, y, Math.Max(pressure, 0.1f));
}
/// <summary>
@@ -251,7 +466,7 @@ namespace Ink_Canvas.Helpers
var result = new List<StylusPoint>();
result.Add(points[0]);
double minDistance = ResampleInterval * 0.5; // 最小距离阈值
double minDistance = 0.3; // 进一步减少最小距离阈值,保留更多平滑点
for (int i = 1; i < points.Length; i++)
{
@@ -270,6 +485,7 @@ namespace Ink_Canvas.Helpers
}
}
System.Diagnostics.Debug.WriteLine($"RemoveDuplicatePoints: 输入点数={points.Length}, 输出点数={result.Count}");
return result.ToArray();
}
@@ -516,23 +732,31 @@ namespace Ink_Canvas.Helpers
/// </summary>
public class AdvancedBezierSmoothing
{
public double SmoothingStrength { get; set; } = 0.3;
public double ResampleInterval { get; set; } = 3.0;
public int InterpolationSteps { get; set; } = 8;
public double SmoothingStrength { get; set; } = 0.6;
public double ResampleInterval { get; set; } = 2.0;
public int InterpolationSteps { get; set; } = 12;
public Stroke SmoothStroke(Stroke stroke)
{
if (stroke == null || stroke.StylusPoints.Count < 3)
{
System.Diagnostics.Debug.WriteLine($"AdvancedBezierSmoothing: 笔画点数不足,跳过平滑 (点数: {stroke?.StylusPoints.Count ?? 0})");
return stroke;
}
var originalPoints = stroke.StylusPoints.ToList();
System.Diagnostics.Debug.WriteLine($"AdvancedBezierSmoothing: 开始平滑处理,原始点数: {stroke.StylusPoints.Count}");
// 简化处理:只进行轻度平滑
var smoothedPoints = ApplyLightExponentialSmoothing(originalPoints, 0.2); // 很轻的平滑
var originalPoints = stroke.StylusPoints.ToArray();
// 使用真正的贝塞尔曲线平滑
var smoothedPoints = ApplyCubicBezierSmoothing(originalPoints);
System.Diagnostics.Debug.WriteLine($"AdvancedBezierSmoothing: 平滑完成,平滑后点数: {smoothedPoints.Length}");
// 检查点数是否合理
if (smoothedPoints.Count > originalPoints.Count * 1.5)
if (smoothedPoints.Length > originalPoints.Length * 10.0)
{
System.Diagnostics.Debug.WriteLine($"AdvancedBezierSmoothing: 点数增加过多,返回原始笔画 (原始:{originalPoints.Length}, 平滑后:{smoothedPoints.Length})");
return stroke; // 如果点数增加太多,返回原始笔画
}
@@ -540,9 +764,140 @@ namespace Ink_Canvas.Helpers
{
DrawingAttributes = stroke.DrawingAttributes.Clone()
};
System.Diagnostics.Debug.WriteLine($"AdvancedBezierSmoothing: 创建平滑笔画成功");
return smoothedStroke;
}
/// <summary>
/// 三次贝塞尔曲线平滑
/// </summary>
private StylusPoint[] ApplyCubicBezierSmoothing(StylusPoint[] points)
{
if (points.Length < 4) return points;
var result = new List<StylusPoint>();
result.Add(points[0]);
// 使用更保守的窗口大小和插值
int windowSize = Math.Min(4, points.Length);
int stepSize = Math.Max(1, points.Length / 10); // 根据点数动态调整步长
for (int i = 0; i <= points.Length - windowSize; i += stepSize)
{
if (i + windowSize - 1 >= points.Length) break;
var p0 = points[i];
var p1 = points[Math.Min(i + 1, points.Length - 1)];
var p2 = points[Math.Min(i + 2, points.Length - 1)];
var p3 = points[Math.Min(i + windowSize - 1, points.Length - 1)];
// 计算控制点
var controlPoints = CalculateControlPoints(p0, p1, p2, p3);
// 只生成2-3个插值点
int steps = 2;
// 生成贝塞尔曲线点
for (int j = 1; j <= steps; j++)
{
double t = (double)j / steps;
var bezierPoint = CalculateBezierPoint(p0, controlPoints.cp1, controlPoints.cp2, p3, t);
result.Add(bezierPoint);
}
}
result.Add(points[points.Length - 1]);
// 去重处理
return RemoveDuplicatePoints(result.ToArray());
}
/// <summary>
/// 去除重复点
/// </summary>
private StylusPoint[] RemoveDuplicatePoints(StylusPoint[] points)
{
if (points.Length <= 1) return points;
var result = new List<StylusPoint> { points[0] };
double minDistance = 1.0; // 最小距离阈值
for (int i = 1; i < points.Length; i++)
{
var lastPoint = result[result.Count - 1];
var currentPoint = points[i];
double distance = Math.Sqrt(Math.Pow(currentPoint.X - lastPoint.X, 2) +
Math.Pow(currentPoint.Y - lastPoint.Y, 2));
if (distance > minDistance)
{
result.Add(currentPoint);
}
}
return result.ToArray();
}
/// <summary>
/// 计算控制点
/// </summary>
private (Point cp1, Point cp2) CalculateControlPoints(StylusPoint p0, StylusPoint p1, StylusPoint p2, StylusPoint p3)
{
// 计算切线方向
var tangent1 = new Vector(p1.X - p0.X, p1.Y - p0.Y);
var tangent2 = new Vector(p3.X - p2.X, p3.Y - p2.Y);
// 归一化切线
if (tangent1.Length > 0) tangent1.Normalize();
if (tangent2.Length > 0) tangent2.Normalize();
// 计算控制点距离
double dist1 = Math.Sqrt((p1.X - p0.X) * (p1.X - p0.X) + (p1.Y - p0.Y) * (p1.Y - p0.Y));
double dist2 = Math.Sqrt((p3.X - p2.X) * (p3.X - p2.X) + (p3.Y - p2.Y) * (p3.Y - p2.Y));
double controlDist1 = dist1 * SmoothingStrength;
double controlDist2 = dist2 * SmoothingStrength;
// 计算控制点
var cp1 = new Point(
p1.X + tangent1.X * controlDist1,
p1.Y + tangent1.Y * controlDist1
);
var cp2 = new Point(
p2.X - tangent2.X * controlDist2,
p2.Y - tangent2.Y * controlDist2
);
return (cp1, cp2);
}
/// <summary>
/// 计算贝塞尔曲线上的点
/// </summary>
private StylusPoint CalculateBezierPoint(StylusPoint p0, Point cp1, Point cp2, StylusPoint p3, double t)
{
double x = Math.Pow(1 - t, 3) * p0.X +
3 * Math.Pow(1 - t, 2) * t * cp1.X +
3 * (1 - t) * Math.Pow(t, 2) * cp2.X +
Math.Pow(t, 3) * p3.X;
double y = Math.Pow(1 - t, 3) * p0.Y +
3 * Math.Pow(1 - t, 2) * t * cp1.Y +
3 * (1 - t) * Math.Pow(t, 2) * cp2.Y +
Math.Pow(t, 3) * p3.Y;
// 压力插值
float pressure = (float)(Math.Pow(1 - t, 3) * p0.PressureFactor +
3 * Math.Pow(1 - t, 2) * t * p0.PressureFactor +
3 * (1 - t) * Math.Pow(t, 2) * p3.PressureFactor +
Math.Pow(t, 3) * p3.PressureFactor);
return new StylusPoint(x, y, Math.Max(pressure, 0.1f));
}
/// <summary>
/// 轻度指数平滑
/// </summary>
+33 -21
View File
@@ -1,12 +1,23 @@
using System;
using System;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Animation;
using Ink_Canvas.Controls;
namespace Ink_Canvas.Helpers
{
internal class AnimationsHelper
{
private static UIElement ResolveAnimationTarget(UIElement element)
{
if (element is BoardMenuFrame frame)
{
frame.ApplyTemplate();
return frame.AnimationTarget ?? element;
}
return element;
}
public static void ShowWithFadeIn(UIElement element, double duration = 0.15)
{
if (element.Visibility == Visibility.Visible) return;
@@ -36,14 +47,17 @@ namespace Ink_Canvas.Helpers
{
try
{
if (element.Visibility == Visibility.Visible) return;
if (element == null)
throw new ArgumentNullException(nameof(element));
if (element.Visibility == Visibility.Visible) return;
element.Visibility = Visibility.Visible;
var target = ResolveAnimationTarget(element);
var sb = new Storyboard();
// 渐变动画
var fadeInAnimation = new DoubleAnimation
{
From = 0.5,
@@ -54,10 +68,9 @@ namespace Ink_Canvas.Helpers
Storyboard.SetTargetProperty(fadeInAnimation, new PropertyPath(UIElement.OpacityProperty));
// 滑动动画
var slideAnimation = new DoubleAnimation
{
From = element.RenderTransform.Value.OffsetY + 10, // 滑动距离
From = 10,
To = 0,
Duration = TimeSpan.FromSeconds(duration)
};
@@ -68,12 +81,11 @@ namespace Ink_Canvas.Helpers
sb.Children.Add(fadeInAnimation);
sb.Children.Add(slideAnimation);
element.Visibility = Visibility.Visible;
element.RenderTransform = new TranslateTransform();
target.RenderTransform = new TranslateTransform();
sb.Begin((FrameworkElement)element);
sb.Begin((FrameworkElement)target);
}
catch { }
catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); }
}
public static void ShowWithSlideFromLeftAndFade(UIElement element, double duration = 0.25)
@@ -113,7 +125,7 @@ namespace Ink_Canvas.Helpers
sb.Begin((FrameworkElement)element);
}
catch { }
catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); }
}
public static void ShowWithScaleFromLeft(UIElement element, double duration = 0.2)
@@ -156,7 +168,7 @@ namespace Ink_Canvas.Helpers
sb.Begin((FrameworkElement)element);
}
catch { }
catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); }
}
public static void ShowWithScaleFromRight(UIElement element, double duration = 0.2)
@@ -200,21 +212,22 @@ namespace Ink_Canvas.Helpers
sb.Begin((FrameworkElement)element);
}
catch { }
catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); }
}
public static void HideWithSlideAndFade(UIElement element, double duration = 0.15)
{
try
{
if (element.Visibility == Visibility.Collapsed) return;
if (element == null)
throw new ArgumentNullException(nameof(element));
if (element.Visibility == Visibility.Collapsed) return;
var target = ResolveAnimationTarget(element);
var sb = new Storyboard();
// 渐变动画
var fadeOutAnimation = new DoubleAnimation
{
From = 1,
@@ -224,11 +237,10 @@ namespace Ink_Canvas.Helpers
fadeOutAnimation.EasingFunction = new CubicEase();
Storyboard.SetTargetProperty(fadeOutAnimation, new PropertyPath(UIElement.OpacityProperty));
// 滑动动画
var slideAnimation = new DoubleAnimation
{
From = 0,
To = element.RenderTransform.Value.OffsetY + 10, // 滑动距离
To = 10,
Duration = TimeSpan.FromSeconds(duration)
};
slideAnimation.EasingFunction = new CubicEase();
@@ -243,10 +255,10 @@ namespace Ink_Canvas.Helpers
element.Visibility = Visibility.Collapsed;
};
element.RenderTransform = new TranslateTransform();
sb.Begin((FrameworkElement)element);
target.RenderTransform = new TranslateTransform();
sb.Begin((FrameworkElement)target);
}
catch { }
catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); }
}
public static void HideWithFadeOut(UIElement element, double duration = 0.15)
+113
View File
@@ -0,0 +1,113 @@
using Ink_Canvas.Windows.SettingsViews.Helpers;
using System;
using System.Diagnostics;
using System.Security.Principal;
using System.Windows;
namespace Ink_Canvas.Helpers
{
public static class AppRestartHelper
{
public static bool IsRunningAsAdmin()
{
try
{
var identity = WindowsIdentity.GetCurrent();
var principal = new WindowsPrincipal(identity);
return principal.IsInRole(WindowsBuiltInRole.Administrator);
}
catch
{
return false;
}
}
public static void RestartApp(bool asAdmin)
{
try
{
App.IsAppExitByUser = true;
(Application.Current as App)?.ReleaseMutexForRestart();
string exePath = Process.GetCurrentProcess().MainModule.FileName;
if (asAdmin)
{
var psi = new ProcessStartInfo(exePath) { UseShellExecute = true, Verb = "runas" };
Process.Start(psi);
}
else
{
// 当前已是管理员时,直接通过用户令牌降权启动,避免经由 explorer 中转的延迟
if (IsRunningAsAdmin() && UIAccessHelper.RestartAsNormalUser())
{
Application.Current.Shutdown();
return;
}
Process.Start("explorer.exe", "\"" + exePath + "\"");
}
Application.Current.Shutdown();
}
catch (Exception ex)
{
Debug.WriteLine($"重启应用时出错: {ex.Message}");
}
}
public static void RestartWithCurrentPrivileges()
{
RestartApp(IsRunningAsAdmin());
}
public static void RestartAsAdmin()
{
RestartApp(true);
}
public static void RestartAsNormal()
{
RestartApp(false);
}
public static void SwitchToUIATopMostAndRestart()
{
try
{
SettingsManager.Settings.Advanced.EnableUIAccessTopMost = true;
if (!SettingsManager.Settings.Advanced.IsAlwaysOnTop)
{
SettingsManager.Settings.Advanced.IsAlwaysOnTop = true;
}
SettingsManager.SaveSettingsToFile();
App.IsUIAccessTopMostEnabled = true;
RestartApp(true);
}
catch (Exception ex)
{
Debug.WriteLine($"切换到UIA置顶模式时出错: {ex.Message}");
}
}
public static void SwitchToNormalTopMostAndRestart()
{
try
{
SettingsManager.Settings.Advanced.EnableUIAccessTopMost = false;
SettingsManager.SaveSettingsToFile();
App.IsUIAccessTopMostEnabled = false;
RestartApp(IsRunningAsAdmin());
}
catch (Exception ex)
{
Debug.WriteLine($"切换到普通置顶模式时出错: {ex.Message}");
}
}
}
}
+225
View File
@@ -0,0 +1,225 @@
using Newtonsoft.Json;
using System;
using System.IO;
using System.Linq;
namespace Ink_Canvas.Helpers
{
/// <summary>
/// 自动备份管理器
/// 负责管理配置文件的自动备份功能
/// </summary>
public static class AutoBackupManager
{
private static readonly string BackupDir = Path.Combine(App.RootPath, "Backups");
private static readonly string SettingsFile = Path.Combine(App.RootPath, "Configs", "Settings.json");
private static readonly string BackupPrefix = "Settings_AutoBackup_";
/// <summary>
/// 检查是否需要执行自动备份
/// </summary>
/// <param name="settings">设置对象</param>
/// <returns>如果需要备份返回<see langword="true"/>,否则返回<see langword="false"/></returns>
public static bool ShouldPerformAutoBackup(Settings settings)
{
try
{
// 如果自动备份功能未启用,不执行备份
if (!settings.Advanced.IsAutoBackupEnabled)
{
return false;
}
// 如果从未备份过,需要创建首次备份
if (settings.Advanced.LastAutoBackupTime == DateTime.MinValue)
{
return true;
}
// 检查是否已超过备份间隔
var daysSinceLastBackup = (DateTime.Now - settings.Advanced.LastAutoBackupTime).TotalDays;
return daysSinceLastBackup >= settings.Advanced.AutoBackupIntervalDays;
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"检查自动备份条件时出错: {ex.Message}", LogHelper.LogType.Error);
return false;
}
}
/// <summary>
/// 执行自动备份
/// </summary>
/// <remarks>
/// 为主配置文件创建一次自动备份并在成功后更新并保存设置中的最后备份时间。
/// </remarks>
/// <param name="settings">应用的设置对象;在成功备份后会更新 settings.Advanced.LastAutoBackupTime 并调用保存操作。</param>
/// <returns><see langword="true"/> 表示备份成功,<see langword="false"/> 表示备份失败或被跳过。</returns>
public static bool PerformAutoBackup(Settings settings)
{
try
{
// 确保备份目录存在
if (!Directory.Exists(BackupDir))
{
ProcessProtectionManager.WithWriteAccess(BackupDir, () => Directory.CreateDirectory(BackupDir));
}
// 检查主配置文件是否存在
if (!File.Exists(SettingsFile))
{
LogHelper.WriteLogToFile("主配置文件不存在,跳过自动备份", LogHelper.LogType.Warning);
return false;
}
// 创建备份文件名(使用当前日期时间)
string backupFileName = $"{BackupPrefix}{DateTime.Now:yyyyMMdd_HHmmss}.json";
string backupPath = Path.Combine(BackupDir, backupFileName);
// 复制主配置文件到备份位置
ProcessProtectionManager.WithWriteAccess(backupPath, () => File.Copy(SettingsFile, backupPath, true));
// 更新最后备份时间
settings.Advanced.LastAutoBackupTime = DateTime.Now;
MainWindow.SaveSettingsToFile();
return true;
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"执行自动备份时出错: {ex.Message}", LogHelper.LogType.Error);
return false;
}
}
/// <summary>
/// 尝试从备份恢复配置文件
/// </summary>
/// <remarks>
/// 从最新可用的自动备份恢复主设置文件(Settings.json)。如果当前设置文件存在,会先将其复制到备份目录并加上时间戳作为“损坏”的备份副本,然后用最新备份覆盖原文件。
/// </remarks>
/// <returns><see langword="true"/> 如果恢复成功,<see langword="false"/> 否则。</returns>
public static bool TryRestoreFromBackup()
{
try
{
// 确保备份目录存在
if (!Directory.Exists(BackupDir))
{
LogHelper.WriteLogToFile("备份目录不存在,无法从备份恢复", LogHelper.LogType.Warning);
return false;
}
// 查找最新的备份文件
var backupFiles = Directory.GetFiles(BackupDir, $"{BackupPrefix}*.json")
.OrderByDescending(f => File.GetCreationTime(f))
.ToArray();
if (backupFiles.Length == 0)
{
LogHelper.WriteLogToFile("没有找到可用的备份文件", LogHelper.LogType.Warning);
return false;
}
// 尝试使用最新的备份文件
string latestBackup = backupFiles[0];
// 验证备份文件是否有效
try
{
string backupJson = File.ReadAllText(latestBackup);
var testSettings = JsonConvert.DeserializeObject<Settings>(backupJson);
if (testSettings == null)
{
LogHelper.WriteLogToFile("备份文件内容无效,无法恢复", LogHelper.LogType.Error);
return false;
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"备份文件验证失败: {ex.Message}", LogHelper.LogType.Error);
return false;
}
// 备份当前损坏的配置文件(如果存在)
if (File.Exists(SettingsFile))
{
string corruptedBackup = Path.Combine(BackupDir, $"Settings_Corrupted_{DateTime.Now:yyyyMMdd_HHmmss}.json");
ProcessProtectionManager.WithWriteAccess(corruptedBackup, () => File.Copy(SettingsFile, corruptedBackup, true));
}
// 从备份恢复配置文件
ProcessProtectionManager.WithWriteAccess(SettingsFile, () => File.Copy(latestBackup, SettingsFile, true));
return true;
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"从备份恢复配置文件时出错: {ex.Message}", LogHelper.LogType.Error);
return false;
}
}
/// <summary>
/// 清理过期的备份文件
/// 保留最近30天的备份文件
/// </summary>
/// <remarks>
/// 删除备份目录中按“备份前缀”匹配且创建时间早于 30 天的自动备份文件(即自动备份文件的命名前缀),不会删除诸如 Settings_Corrupted_*.json 之类的其他备份或错误状态文件。
/// 如果备份目录不存在则不执行任何操作;删除操作在受写入保护的上下文中执行,任何错误会被记录但不会抛出异常。
/// </remarks>
public static void CleanupOldBackups()
{
try
{
if (!Directory.Exists(BackupDir))
{
return;
}
var cutoffDate = DateTime.Now.AddDays(-30);
var backupFiles = Directory.GetFiles(BackupDir, $"{BackupPrefix}*.json");
int deletedCount = 0;
foreach (var file in backupFiles)
{
if (File.GetCreationTime(file) < cutoffDate)
{
ProcessProtectionManager.WithWriteAccess(file, () => File.Delete(file));
deletedCount++;
}
}
if (deletedCount > 0)
{
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"清理过期备份文件时出错: {ex.Message}", LogHelper.LogType.Error);
}
}
/// <summary>
/// 初始化自动备份功能
/// 在应用程序启动时调用
/// </summary>
/// <param name="settings">设置对象</param>
public static void Initialize(Settings settings)
{
try
{
// 检查是否需要执行自动备份
if (ShouldPerformAutoBackup(settings))
{
PerformAutoBackup(settings);
}
// 清理过期备份
CleanupOldBackups();
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"初始化自动备份功能时出错: {ex.Message}", LogHelper.LogType.Error);
}
}
}
}
+444
View File
@@ -0,0 +1,444 @@
using System;
using System.ComponentModel;
using System.Globalization;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Media;
using System.Windows.Threading;
namespace Ink_Canvas.Helpers
{
/// <summary>
/// Automatically shrinks text to fit available width.
/// Supports TextBlock and Label.
/// Only shrinks, never enlarges above MaxFontSize.
/// </summary>
public static class AutoFontSizeHelper
{
public static readonly DependencyProperty IsEnabledProperty =
DependencyProperty.RegisterAttached(
"IsEnabled",
typeof(bool),
typeof(AutoFontSizeHelper),
new PropertyMetadata(false, OnIsEnabledChanged));
public static void SetIsEnabled(DependencyObject element, bool value) => element.SetValue(IsEnabledProperty, value);
public static bool GetIsEnabled(DependencyObject element) => (bool)element.GetValue(IsEnabledProperty);
public static readonly DependencyProperty MinFontSizeProperty =
DependencyProperty.RegisterAttached(
"MinFontSize",
typeof(double),
typeof(AutoFontSizeHelper),
new PropertyMetadata(6d, OnSizingPropertyChanged));
public static void SetMinFontSize(DependencyObject element, double value) => element.SetValue(MinFontSizeProperty, value);
public static double GetMinFontSize(DependencyObject element) => (double)element.GetValue(MinFontSizeProperty);
public static readonly DependencyProperty MaxFontSizeProperty =
DependencyProperty.RegisterAttached(
"MaxFontSize",
typeof(double),
typeof(AutoFontSizeHelper),
new PropertyMetadata(double.NaN, OnSizingPropertyChanged));
public static void SetMaxFontSize(DependencyObject element, double value) => element.SetValue(MaxFontSizeProperty, value);
public static double GetMaxFontSize(DependencyObject element) => (double)element.GetValue(MaxFontSizeProperty);
public static readonly DependencyProperty StepProperty =
DependencyProperty.RegisterAttached(
"Step",
typeof(double),
typeof(AutoFontSizeHelper),
new PropertyMetadata(0.5d, OnSizingPropertyChanged));
public static void SetStep(DependencyObject element, double value) => element.SetValue(StepProperty, value);
public static double GetStep(DependencyObject element) => (double)element.GetValue(StepProperty);
private static readonly DependencyProperty IsAdjustingProperty =
DependencyProperty.RegisterAttached(
"IsAdjusting",
typeof(bool),
typeof(AutoFontSizeHelper),
new PropertyMetadata(false));
private static void SetIsAdjusting(DependencyObject element, bool value) => element.SetValue(IsAdjustingProperty, value);
private static bool GetIsAdjusting(DependencyObject element) => (bool)element.GetValue(IsAdjustingProperty);
private static readonly DependencyProperty OriginalFontSizeProperty =
DependencyProperty.RegisterAttached(
"OriginalFontSize",
typeof(double),
typeof(AutoFontSizeHelper),
new PropertyMetadata(double.NaN));
private static void SetOriginalFontSize(DependencyObject element, double value) => element.SetValue(OriginalFontSizeProperty, value);
private static double GetOriginalFontSize(DependencyObject element) => (double)element.GetValue(OriginalFontSizeProperty);
private static void OnIsEnabledChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (!(d is FrameworkElement fe)) return;
if (!(fe is TextBlock) && !(fe is Label)) return;
if ((bool)e.NewValue)
{
var originalFontSize = GetElementFontSize(fe);
if (!double.IsNaN(originalFontSize) && originalFontSize > 0)
{
SetOriginalFontSize(fe, originalFontSize);
}
fe.SizeChanged += Element_OnSizeChanged;
fe.Loaded += Element_OnLoaded;
fe.Unloaded += Element_OnUnloaded;
TryHookContentChanged(fe, true);
fe.Dispatcher.BeginInvoke(new Action(() => TryAdjust(fe)), DispatcherPriority.Loaded);
}
else
{
fe.SizeChanged -= Element_OnSizeChanged;
fe.Loaded -= Element_OnLoaded;
fe.Unloaded -= Element_OnUnloaded;
TryHookContentChanged(fe, false);
}
}
private static void OnSizingPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is FrameworkElement fe && GetIsEnabled(fe))
{
fe.Dispatcher.BeginInvoke(new Action(() => TryAdjust(fe)), DispatcherPriority.Loaded);
}
}
private static void Element_OnLoaded(object sender, RoutedEventArgs e)
{
if (sender is FrameworkElement fe)
{
fe.Dispatcher.BeginInvoke(new Action(() => TryAdjust(fe)), DispatcherPriority.Loaded);
}
}
private static void Element_OnUnloaded(object sender, RoutedEventArgs e)
{
// No extra cleanup required here.
}
private static void Element_OnSizeChanged(object sender, SizeChangedEventArgs e)
{
if (sender is FrameworkElement fe) TryAdjust(fe);
}
private static void Element_OnTextChanged(object sender, EventArgs e)
{
if (sender is FrameworkElement fe)
{
fe.Dispatcher.BeginInvoke(new Action(() => TryAdjust(fe)), DispatcherPriority.Loaded);
}
}
private static void TryHookContentChanged(FrameworkElement fe, bool add)
{
try
{
DependencyPropertyDescriptor dpd = null;
if (fe is TextBlock)
{
dpd = DependencyPropertyDescriptor.FromProperty(TextBlock.TextProperty, typeof(TextBlock));
}
else if (fe is Label)
{
dpd = DependencyPropertyDescriptor.FromProperty(ContentControl.ContentProperty, typeof(ContentControl));
}
if (dpd == null) return;
if (add) dpd.AddValueChanged(fe, Element_OnTextChanged);
else dpd.RemoveValueChanged(fe, Element_OnTextChanged);
}
catch
{
// Ignore descriptor issues in rare runtime cases.
}
}
private static void TryAdjust(FrameworkElement fe)
{
if (fe == null) return;
if (!GetIsEnabled(fe)) return;
if (GetIsAdjusting(fe)) return;
var text = GetElementText(fe);
if (string.IsNullOrEmpty(text)) return;
if (!ShouldAutoScaleForCurrentCulture(text))
{
RestoreOriginalFontSize(fe);
return;
}
var availableWidth = GetAvailableWidth(fe);
if (double.IsNaN(availableWidth) || availableWidth <= 1) return;
var min = GetMinFontSize(fe);
if (double.IsNaN(min) || min <= 0) min = 6d;
var step = GetStep(fe);
if (double.IsNaN(step) || step < 0.1) step = 0.5d;
var current = GetElementFontSize(fe);
if (double.IsNaN(current) || current <= 0) return;
var max = GetMaxFontSize(fe);
if (double.IsNaN(max) || max <= 0) max = current;
// Never enlarge: auto-fit should only reduce font size when needed.
if (max > current) max = current;
var startFont = Math.Min(current, max);
if (startFont < min) startFont = min;
SetIsAdjusting(fe, true);
try
{
var font = startFont;
var desired = MeasureTextWidth(fe, text, font);
if (desired <= 0) return;
while (font > min && desired > availableWidth + 0.5)
{
font = Math.Max(min, font - step);
desired = MeasureTextWidth(fe, text, font);
if (desired <= 0) break;
}
// Hard-fit fallback: when very narrow slots (e.g., 28px) still overflow at MinFontSize,
// keep shrinking proportionally so text always fits in the available width.
if (desired > availableWidth + 0.5)
{
var hardFont = font;
for (var i = 0; i < 6 && desired > availableWidth + 0.5; i++)
{
var ratio = availableWidth / Math.Max(1.0, desired);
hardFont = Math.Max(1.0, hardFont * ratio);
desired = MeasureTextWidth(fe, text, hardFont);
if (desired <= 0) break;
}
font = hardFont;
}
if (!double.IsNaN(font) && font > 0 && Math.Abs(current - font) > 0.01)
{
SetElementFontSize(fe, font);
}
}
finally
{
SetIsAdjusting(fe, false);
}
}
private static string GetElementText(FrameworkElement fe)
{
if (fe is TextBlock tb) return tb.Text;
if (fe is Label label) return label.Content as string ?? label.Content?.ToString();
return null;
}
private static bool ShouldAutoScaleForCurrentCulture(string text)
{
// Requirement: auto-scale for English UI only, keep Chinese font size unchanged.
var culture = CultureInfo.CurrentUICulture;
var name = culture?.Name ?? string.Empty;
if (name.StartsWith("en", StringComparison.OrdinalIgnoreCase))
{
return true;
}
// Fallback: if actual rendered text is Latin-heavy, still auto-scale.
// This avoids clipping when culture detection is out of sync.
if (string.IsNullOrWhiteSpace(text)) return false;
foreach (var ch in text)
{
if ((ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z'))
{
return true;
}
}
return false;
}
private static void RestoreOriginalFontSize(FrameworkElement fe)
{
var original = GetOriginalFontSize(fe);
if (double.IsNaN(original) || original <= 0) return;
var current = GetElementFontSize(fe);
if (double.IsNaN(current) || current <= 0) return;
if (Math.Abs(current - original) > 0.01)
{
SetElementFontSize(fe, original);
}
}
private static double GetElementFontSize(FrameworkElement fe)
{
if (fe is TextBlock tb) return tb.FontSize;
if (fe is Label label) return label.FontSize;
return double.NaN;
}
private static void SetElementFontSize(FrameworkElement fe, double value)
{
if (fe is TextBlock tb) tb.FontSize = value;
else if (fe is Label label) label.FontSize = value;
}
private static double GetAvailableWidth(FrameworkElement fe)
{
double width = double.PositiveInfinity;
// Explicit width on the element itself should be a hard cap.
if (!double.IsNaN(fe.Width) && !double.IsInfinity(fe.Width) && fe.Width > 1)
{
width = Math.Min(width, fe.Width - fe.Margin.Left - fe.Margin.Right);
}
if (!double.IsNaN(fe.MaxWidth) && !double.IsInfinity(fe.MaxWidth) && fe.MaxWidth > 1)
{
width = Math.Min(width, fe.MaxWidth - fe.Margin.Left - fe.Margin.Right);
}
// Prefer the real layout slot first. This is usually the most accurate
// "space actually assigned by layout" for the element.
var slot = LayoutInformation.GetLayoutSlot(fe);
if (!double.IsNaN(slot.Width) && !double.IsInfinity(slot.Width))
{
var slotWidth = slot.Width - fe.Margin.Left - fe.Margin.Right;
if (slotWidth > 1) width = Math.Min(width, slotWidth);
}
if (fe.ActualWidth > 1) width = Math.Min(width, fe.ActualWidth);
// Immediate parent may be a StackPanel that does not constrain width.
// Walk a few ancestors and take the tightest finite width as fallback.
DependencyObject ancestor = fe.Parent ?? VisualTreeHelper.GetParent(fe);
var depth = 0;
while (ancestor != null && depth < 8)
{
if (ancestor is FrameworkElement af && af.ActualWidth > 1)
{
var candidate = af.ActualWidth;
// If ancestor sets explicit width, treat it as a stronger cap.
if (!double.IsNaN(af.Width) && !double.IsInfinity(af.Width) && af.Width > 1)
{
candidate = Math.Min(candidate, af.Width);
}
if (!double.IsNaN(af.MaxWidth) && !double.IsInfinity(af.MaxWidth) && af.MaxWidth > 1)
{
candidate = Math.Min(candidate, af.MaxWidth);
}
if (ancestor is Control ac)
{
candidate -= ac.Padding.Left + ac.Padding.Right;
candidate -= ac.BorderThickness.Left + ac.BorderThickness.Right;
}
else if (ancestor is Border ab)
{
candidate -= ab.Padding.Left + ab.Padding.Right;
candidate -= ab.BorderThickness.Left + ab.BorderThickness.Right;
}
if (candidate > 1) width = Math.Min(width, candidate);
}
ancestor = (ancestor as FrameworkElement)?.Parent ?? VisualTreeHelper.GetParent(ancestor);
depth++;
}
if (double.IsInfinity(width) || double.IsNaN(width) || width <= 1) return -1;
// Keep width as inner text area.
if (fe is Control control)
{
width -= control.Padding.Left + control.Padding.Right;
width -= control.BorderThickness.Left + control.BorderThickness.Right;
}
else if (fe is Border border)
{
width -= border.Padding.Left + border.Padding.Right;
width -= border.BorderThickness.Left + border.BorderThickness.Right;
}
return width;
}
private static double MeasureTextWidth(FrameworkElement fe, string text, double fontSize)
{
try
{
var dpi = VisualTreeHelper.GetDpi(fe);
var culture = CultureInfo.CurrentUICulture;
if (fe.Language != null)
{
try
{
culture = fe.Language.GetEquivalentCulture();
}
catch
{
}
}
var fontFamily = SystemFonts.MessageFontFamily;
var fontStyle = FontStyles.Normal;
var fontWeight = FontWeights.Normal;
var fontStretch = FontStretches.Normal;
Brush foreground = Brushes.Black;
var flowDirection = FlowDirection.LeftToRight;
if (fe is TextBlock tb)
{
fontFamily = tb.FontFamily;
fontStyle = tb.FontStyle;
fontWeight = tb.FontWeight;
fontStretch = tb.FontStretch;
foreground = tb.Foreground;
flowDirection = tb.FlowDirection;
}
else if (fe is Label label)
{
fontFamily = label.FontFamily;
fontStyle = label.FontStyle;
fontWeight = label.FontWeight;
fontStretch = label.FontStretch;
foreground = label.Foreground;
flowDirection = label.FlowDirection;
}
var typeface = new Typeface(fontFamily, fontStyle, fontWeight, fontStretch);
var formatted = new FormattedText(
text,
culture,
flowDirection,
typeface,
fontSize,
foreground,
dpi.PixelsPerDip);
return formatted.WidthIncludingTrailingWhitespace;
}
catch
{
return -1;
}
}
}
}
+551 -78
View File
@@ -27,6 +27,118 @@ namespace Ink_Canvas.Helpers
private static readonly string updatesFolderPath = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "AutoUpdate");
private static string statusFilePath;
// 全局下载取消令牌;UI 通过 RequestCancelDownload 取消当前下载
private static CancellationTokenSource _activeDownloadCts;
private static readonly object _activeDownloadLock = new object();
public static void RequestCancelDownload()
{
lock (_activeDownloadLock)
{
try { _activeDownloadCts?.Cancel(); } catch { }
}
}
private static CancellationTokenSource BeginDownloadSession()
{
lock (_activeDownloadLock)
{
try { _activeDownloadCts?.Cancel(); } catch { }
_activeDownloadCts = new CancellationTokenSource();
return _activeDownloadCts;
}
}
private static void EndDownloadSession(CancellationTokenSource cts)
{
lock (_activeDownloadLock)
{
if (ReferenceEquals(_activeDownloadCts, cts)) _activeDownloadCts = null;
}
try { cts?.Dispose(); } catch { }
}
public static bool IsX64UpdatePackageSelected()
{
try
{
return MainWindow.Settings?.Startup?.UpdatePackageArchitecture == UpdatePackageArchitecture.X64;
}
catch
{
return false;
}
}
private static string NormalizeVersionForUpdate(string version)
{
if (string.IsNullOrWhiteSpace(version)) return version;
return version.Trim().TrimStart('v', 'V');
}
public static string AppendX64SuffixBeforeZipExtension(string url)
{
if (string.IsNullOrEmpty(url) || !IsX64UpdatePackageSelected()) return url;
int query = url.IndexOf('?');
string pathPart = query >= 0 ? url.Substring(0, query) : url;
string qs = query >= 0 ? url.Substring(query) : "";
const string ext = ".zip";
int idx = pathPart.LastIndexOf(ext, StringComparison.OrdinalIgnoreCase);
if (idx < 0) return url;
var basePart = pathPart.Substring(0, idx);
if (basePart.EndsWith("-x64", StringComparison.OrdinalIgnoreCase)) return url;
return basePart + "-x64" + ext + qs;
}
public static string GetUpdateZipFileName(string version)
{
var v = NormalizeVersionForUpdate(version);
return IsX64UpdatePackageSelected()
? $"InkCanvasForClass.CE.{v}-x64.zip"
: $"InkCanvasForClass.CE.{v}.zip";
}
public static string GetUpdateDownloadStatusFilePath(string version)
{
var v = NormalizeVersionForUpdate(version);
string name = IsX64UpdatePackageSelected()
? $"DownloadV{v}_x64Status.txt"
: $"DownloadV{v}Status.txt";
return Path.Combine(updatesFolderPath, name);
}
public static string GetLocalUpdateZipFilePath(string version)
{
return Path.Combine(updatesFolderPath, GetUpdateZipFileName(version));
}
private static string PickBrowserDownloadUrlFromAssets(JToken assets)
{
if (assets == null || !assets.Any()) return null;
bool wantX64 = IsX64UpdatePackageSelected();
string anyZip = null;
string x64Zip = null;
string nonX64Zip = null;
foreach (JToken a in assets)
{
string name = a["name"]?.ToString();
string url = a["browser_download_url"]?.ToString();
if (string.IsNullOrEmpty(name) || string.IsNullOrEmpty(url)) continue;
if (!name.EndsWith(".zip", StringComparison.OrdinalIgnoreCase)) continue;
if (anyZip == null) anyZip = url;
if (name.EndsWith("-x64.zip", StringComparison.OrdinalIgnoreCase))
{
if (x64Zip == null) x64Zip = url;
}
else
{
if (nonX64Zip == null) nonX64Zip = url;
}
}
if (wantX64)
return x64Zip ?? anyZip;
return nonX64Zip ?? anyZip;
}
// 线路组结构体(包含版本、下载、日志地址)
public class UpdateLineGroup
@@ -74,6 +186,99 @@ namespace Ink_Canvas.Helpers
GroupName = "inkeys",
DownloadUrlFormat = "https://iccce.inkeys.top/Release/InkCanvasForClass.CE.{0}.zip",
LogUrl = "https://bgithub.xyz/InkCanvasForClass/community/raw/refs/heads/main/UpdateLog.md"
},
new UpdateLineGroup
{
GroupName = "gh-proxy",
VersionUrl = "https://gh-proxy.org/https://raw.githubusercontent.com/InkCanvasForClass/community/refs/heads/main/AutomaticUpdateVersionControl.txt",
DownloadUrlFormat = "https://gh-proxy.org/https://github.com/InkCanvasForClass/community/releases/download/{0}/InkCanvasForClass.CE.{0}.zip",
LogUrl = "https://gh-proxy.org/https://raw.githubusercontent.com/InkCanvasForClass/community/refs/heads/main/UpdateLog.md"
},
new UpdateLineGroup
{
GroupName = "hk.gh-proxy",
VersionUrl = "https://hk.gh-proxy.org/https://raw.githubusercontent.com/InkCanvasForClass/community/refs/heads/main/AutomaticUpdateVersionControl.txt",
DownloadUrlFormat = "https://hk.gh-proxy.org/https://github.com/InkCanvasForClass/community/releases/download/{0}/InkCanvasForClass.CE.{0}.zip",
LogUrl = "https://hk.gh-proxy.org/https://raw.githubusercontent.com/InkCanvasForClass/community/refs/heads/main/UpdateLog.md"
},
new UpdateLineGroup
{
GroupName = "cdn.gh-proxy",
VersionUrl = "https://cdn.gh-proxy.org/https://raw.githubusercontent.com/InkCanvasForClass/community/refs/heads/main/AutomaticUpdateVersionControl.txt",
DownloadUrlFormat = "https://cdn.gh-proxy.org/https://github.com/InkCanvasForClass/community/releases/download/{0}/InkCanvasForClass.CE.{0}.zip",
LogUrl = "https://cdn.gh-proxy.org/https://raw.githubusercontent.com/InkCanvasForClass/community/refs/heads/main/UpdateLog.md"
},
new UpdateLineGroup
{
GroupName = "edgeone.gh-proxy",
VersionUrl = "https://edgeone.gh-proxy.org/https://raw.githubusercontent.com/InkCanvasForClass/community/refs/heads/main/AutomaticUpdateVersionControl.txt",
DownloadUrlFormat = "https://edgeone.gh-proxy.org/https://github.com/InkCanvasForClass/community/releases/download/{0}/InkCanvasForClass.CE.{0}.zip",
LogUrl = "https://edgeone.gh-proxy.org/https://raw.githubusercontent.com/InkCanvasForClass/community/refs/heads/main/UpdateLog.md"
}
}
},
{ UpdateChannel.Preview, new List<UpdateLineGroup>
{
new UpdateLineGroup
{
GroupName = "GitHub主线",
VersionUrl = "https://github.com/InkCanvasForClass/community-beta/raw/refs/heads/main/AutomaticUpdateVersionControl.txt",
DownloadUrlFormat = "https://github.com/InkCanvasForClass/community-beta/releases/download/{0}/InkCanvasForClass.CE.{0}.zip",
LogUrl = "https://github.com/InkCanvasForClass/community-beta/raw/refs/heads/main/UpdateLog.md"
},
new UpdateLineGroup
{
GroupName = "bgithub备用",
VersionUrl = "https://bgithub.xyz/InkCanvasForClass/community-beta/raw/refs/heads/main/AutomaticUpdateVersionControl.txt",
DownloadUrlFormat = "https://bgithub.xyz/InkCanvasForClass/community-beta/releases/download/{0}/InkCanvasForClass.CE.{0}.zip",
LogUrl = "https://bgithub.xyz/InkCanvasForClass/community-beta/raw/refs/heads/main/UpdateLog.md"
},
new UpdateLineGroup
{
GroupName = "kkgithub线路",
VersionUrl = "https://kkgithub.com/InkCanvasForClass/community-beta/raw/refs/heads/main/AutomaticUpdateVersionControl.txt",
DownloadUrlFormat = "https://kkgithub.com/InkCanvasForClass/community-beta/releases/download/{0}/InkCanvasForClass.CE.{0}.zip",
LogUrl = "https://kkgithub.com/InkCanvasForClass/community-beta/raw/refs/heads/main/UpdateLog.md"
},
new UpdateLineGroup
{
GroupName = "智教联盟",
DownloadUrlFormat = "https://get.smart-teach.cn/d/Ningbo-S3/shared/jiangling/community-beta/InkCanvasForClass.CE.{0}.zip",
LogUrl = "https://bgithub.xyz/InkCanvasForClass/community-beta/raw/refs/heads/main/UpdateLog.md"
},
new UpdateLineGroup
{
GroupName = "inkeys",
DownloadUrlFormat = "https://iccce.inkeys.top/Beta/InkCanvasForClass.CE.{0}.zip",
LogUrl = "https://bgithub.xyz/InkCanvasForClass/community-beta/raw/refs/heads/main/UpdateLog.md"
},
new UpdateLineGroup
{
GroupName = "gh-proxy",
VersionUrl = "https://gh-proxy.org/https://raw.githubusercontent.com/InkCanvasForClass/community-beta/refs/heads/main/AutomaticUpdateVersionControl.txt",
DownloadUrlFormat = "https://gh-proxy.org/https://github.com/InkCanvasForClass/community-beta/releases/download/{0}/InkCanvasForClass.CE.{0}.zip",
LogUrl = "https://gh-proxy.org/https://raw.githubusercontent.com/InkCanvasForClass/community-beta/refs/heads/main/UpdateLog.md"
},
new UpdateLineGroup
{
GroupName = "hk.gh-proxy",
VersionUrl = "https://hk.gh-proxy.org/https://raw.githubusercontent.com/InkCanvasForClass/community-beta/refs/heads/main/AutomaticUpdateVersionControl.txt",
DownloadUrlFormat = "https://hk.gh-proxy.org/https://github.com/InkCanvasForClass/community-beta/releases/download/{0}/InkCanvasForClass.CE.{0}.zip",
LogUrl = "https://hk.gh-proxy.org/https://raw.githubusercontent.com/InkCanvasForClass/community-beta/refs/heads/main/UpdateLog.md"
},
new UpdateLineGroup
{
GroupName = "cdn.gh-proxy",
VersionUrl = "https://cdn.gh-proxy.org/https://raw.githubusercontent.com/InkCanvasForClass/community-beta/refs/heads/main/AutomaticUpdateVersionControl.txt",
DownloadUrlFormat = "https://cdn.gh-proxy.org/https://github.com/InkCanvasForClass/community-beta/releases/download/{0}/InkCanvasForClass.CE.{0}.zip",
LogUrl = "https://cdn.gh-proxy.org/https://raw.githubusercontent.com/InkCanvasForClass/community-beta/refs/heads/main/UpdateLog.md"
},
new UpdateLineGroup
{
GroupName = "edgeone.gh-proxy",
VersionUrl = "https://edgeone.gh-proxy.org/https://raw.githubusercontent.com/InkCanvasForClass/community-beta/refs/heads/main/AutomaticUpdateVersionControl.txt",
DownloadUrlFormat = "https://edgeone.gh-proxy.org/https://github.com/InkCanvasForClass/community-beta/releases/download/{0}/InkCanvasForClass.CE.{0}.zip",
LogUrl = "https://edgeone.gh-proxy.org/https://raw.githubusercontent.com/InkCanvasForClass/community-beta/refs/heads/main/UpdateLog.md"
}
}
},
@@ -111,6 +316,34 @@ namespace Ink_Canvas.Helpers
GroupName = "inkeys",
DownloadUrlFormat = "https://iccce.inkeys.top/Beta/InkCanvasForClass.CE.{0}.zip",
LogUrl = "https://bgithub.xyz/InkCanvasForClass/community-beta/raw/refs/heads/main/UpdateLog.md"
},
new UpdateLineGroup
{
GroupName = "gh-proxy",
VersionUrl = "https://gh-proxy.org/https://raw.githubusercontent.com/InkCanvasForClass/community-beta/refs/heads/main/AutomaticUpdateVersionControl.txt",
DownloadUrlFormat = "https://gh-proxy.org/https://github.com/InkCanvasForClass/community-beta/releases/download/{0}/InkCanvasForClass.CE.{0}.zip",
LogUrl = "https://gh-proxy.org/https://raw.githubusercontent.com/InkCanvasForClass/community-beta/refs/heads/main/UpdateLog.md"
},
new UpdateLineGroup
{
GroupName = "hk.gh-proxy",
VersionUrl = "https://hk.gh-proxy.org/https://raw.githubusercontent.com/InkCanvasForClass/community-beta/refs/heads/main/AutomaticUpdateVersionControl.txt",
DownloadUrlFormat = "https://hk.gh-proxy.org/https://github.com/InkCanvasForClass/community-beta/releases/download/{0}/InkCanvasForClass.CE.{0}.zip",
LogUrl = "https://hk.gh-proxy.org/https://raw.githubusercontent.com/InkCanvasForClass/community-beta/refs/heads/main/UpdateLog.md"
},
new UpdateLineGroup
{
GroupName = "cdn.gh-proxy",
VersionUrl = "https://cdn.gh-proxy.org/https://raw.githubusercontent.com/InkCanvasForClass/community-beta/refs/heads/main/AutomaticUpdateVersionControl.txt",
DownloadUrlFormat = "https://cdn.gh-proxy.org/https://github.com/InkCanvasForClass/community-beta/releases/download/{0}/InkCanvasForClass.CE.{0}.zip",
LogUrl = "https://cdn.gh-proxy.org/https://raw.githubusercontent.com/InkCanvasForClass/community-beta/refs/heads/main/UpdateLog.md"
},
new UpdateLineGroup
{
GroupName = "edgeone.gh-proxy",
VersionUrl = "https://edgeone.gh-proxy.org/https://raw.githubusercontent.com/InkCanvasForClass/community-beta/refs/heads/main/AutomaticUpdateVersionControl.txt",
DownloadUrlFormat = "https://edgeone.gh-proxy.org/https://github.com/InkCanvasForClass/community-beta/releases/download/{0}/InkCanvasForClass.CE.{0}.zip",
LogUrl = "https://edgeone.gh-proxy.org/https://raw.githubusercontent.com/InkCanvasForClass/community-beta/refs/heads/main/UpdateLog.md"
}
}
}
@@ -167,7 +400,7 @@ namespace Ink_Canvas.Helpers
}
}
}
catch { }
catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); }
return -1;
}
@@ -181,6 +414,8 @@ namespace Ink_Canvas.Helpers
// 获取所有可用线路组,按延迟排序
public static async Task<List<UpdateLineGroup>> GetAvailableLineGroupsOrdered(UpdateChannel channel)
{
var cached = TryGetCachedOrderedGroups(channel);
if (cached != null) return cached;
var groups = ChannelLineGroups[channel];
var availableGroups = new List<(UpdateLineGroup group, long delay)>();
@@ -188,14 +423,46 @@ namespace Ink_Canvas.Helpers
foreach (var group in groups)
{
// 跳过"智教联盟"和"inkeys"线路组,不参与延迟检测和排序
string testUrl = null;
if (group.GroupName == "智教联盟" || group.GroupName == "inkeys")
{
LogHelper.WriteLogToFile($"AutoUpdate | 跳过{group.GroupName}线路组延迟检测");
try
{
if (!string.IsNullOrEmpty(group.DownloadUrlFormat))
{
testUrl = group.DownloadUrlFormat.Replace("{0}", "test");
testUrl = AppendX64SuffixBeforeZipExtension(testUrl);
}
}
catch
{
testUrl = null;
}
}
else
{
testUrl = group.VersionUrl;
}
if (string.IsNullOrEmpty(testUrl))
{
LogHelper.WriteLogToFile($"AutoUpdate | 线路组 {group.GroupName} 缺少可用测速地址,跳过", LogHelper.LogType.Warning);
continue;
}
LogHelper.WriteLogToFile($"AutoUpdate | 检测线路组: {group.GroupName} ({group.VersionUrl})");
var delay = await GetUrlDelay(group.VersionUrl);
LogHelper.WriteLogToFile($"AutoUpdate | 检测线路组: {group.GroupName} ({testUrl})");
long delay;
if (group.GroupName == "智教联盟" || group.GroupName == "inkeys")
{
delay = await GetDownloadUrlDelay(testUrl);
}
else
{
delay = await GetUrlDelay(testUrl);
}
if (delay >= 0)
{
LogHelper.WriteLogToFile($"AutoUpdate | 线路组 {group.GroupName} 延迟: {delay}ms");
@@ -213,20 +480,12 @@ namespace Ink_Canvas.Helpers
.Select(x => x.group)
.ToList();
// 将"智教联盟"线路组插入到最前面(如果存在)
var zhiJiaoGroup = groups.FirstOrDefault(g => g.GroupName == "智教联盟");
if (zhiJiaoGroup != null)
{
orderedGroups.Insert(0, zhiJiaoGroup);
LogHelper.WriteLogToFile("AutoUpdate | 智教联盟线路组已插入到首位");
}
// 将"inkeys"线路组插入到第二位(如果存在)
var inkeysGroup = groups.FirstOrDefault(g => g.GroupName == "inkeys");
var inkeysGroup = orderedGroups.FirstOrDefault(g => g.GroupName == "inkeys");
if (inkeysGroup != null)
{
orderedGroups.Insert(1, inkeysGroup);
LogHelper.WriteLogToFile("AutoUpdate | inkeys线路组已插入到第二位");
orderedGroups.Remove(inkeysGroup);
orderedGroups.Insert(0, inkeysGroup);
LogHelper.WriteLogToFile("AutoUpdate | inkeys线路组已默认优先");
}
if (orderedGroups.Count > 0)
@@ -242,9 +501,87 @@ namespace Ink_Canvas.Helpers
LogHelper.WriteLogToFile("AutoUpdate | 所有线路组均不可用", LogHelper.LogType.Error);
}
CacheOrderedGroups(channel, orderedGroups);
return orderedGroups;
}
// 缓存按延迟排序后的线路组,避免短时间内重复测速
private static readonly Dictionary<UpdateChannel, (List<UpdateLineGroup> groups, DateTime cachedAt)> _orderedGroupsCache
= new Dictionary<UpdateChannel, (List<UpdateLineGroup>, DateTime)>();
private static readonly TimeSpan _orderedGroupsCacheTtl = TimeSpan.FromMinutes(15);
private static List<UpdateLineGroup> TryGetCachedOrderedGroups(UpdateChannel channel)
{
lock (_orderedGroupsCache)
{
if (_orderedGroupsCache.TryGetValue(channel, out var entry) &&
entry.groups != null && entry.groups.Count > 0 &&
DateTime.UtcNow - entry.cachedAt < _orderedGroupsCacheTtl)
{
LogHelper.WriteLogToFile($"AutoUpdate | 复用线路组延迟检测缓存({entry.groups.Count} 个)");
return new List<UpdateLineGroup>(entry.groups);
}
return null;
}
}
private static void CacheOrderedGroups(UpdateChannel channel, List<UpdateLineGroup> groups)
{
lock (_orderedGroupsCache)
{
_orderedGroupsCache[channel] = (new List<UpdateLineGroup>(groups), DateTime.UtcNow);
}
}
public static void InvalidateOrderedGroupsCache()
{
lock (_orderedGroupsCache)
{
_orderedGroupsCache.Clear();
}
}
private static async Task<long> GetDownloadUrlDelay(string url)
{
try
{
var osVersion = Environment.OSVersion;
bool isWindows7 = osVersion.Version.Major == 6 && osVersion.Version.Minor == 1;
if (isWindows7)
{
using (var handler = new HttpClientHandler())
{
handler.ServerCertificateCustomValidationCallback = (sender, cert, chain, sslPolicyErrors) => true;
using (var client = new HttpClient(handler))
{
client.Timeout = TimeSpan.FromSeconds(5);
var sw = Stopwatch.StartNew();
var resp = await client.SendAsync(new HttpRequestMessage(HttpMethod.Head, url));
sw.Stop();
return sw.ElapsedMilliseconds;
}
}
}
else
{
using (var client = new HttpClient())
{
client.Timeout = TimeSpan.FromSeconds(5);
var sw = Stopwatch.StartNew();
var resp = await client.SendAsync(new HttpRequestMessage(HttpMethod.Head, url));
sw.Stop();
return sw.ElapsedMilliseconds;
}
}
}
catch
{
return -1;
}
}
// 获取远程版本号
private static async Task<string> GetRemoteVersion(string fileUrl)
{
@@ -407,7 +744,7 @@ namespace Ink_Canvas.Helpers
{
try
{
string apiUrl = channel == UpdateChannel.Beta
string apiUrl = (channel == UpdateChannel.Beta || channel == UpdateChannel.Preview)
? "https://api.github.com/repos/InkCanvasForClass/community-beta/releases"
: "https://api.github.com/repos/InkCanvasForClass/community/releases";
using (var client = new HttpClient())
@@ -423,7 +760,7 @@ namespace Ink_Canvas.Helpers
if (version == targetVersion || version == $"v{targetVersion}" || version == $"V{targetVersion}")
{
string releaseNotes = release["body"]?.ToString();
string downloadUrl = release["assets"]?.First?["browser_download_url"]?.ToString();
string downloadUrl = PickBrowserDownloadUrlFromAssets(release["assets"]);
// 解析发布时间
DateTime? releaseTime = null;
@@ -449,28 +786,58 @@ namespace Ink_Canvas.Helpers
{
try
{
string apiUrl = channel == UpdateChannel.Beta
? "https://api.github.com/repos/InkCanvasForClass/community-beta/releases/latest"
: "https://api.github.com/repos/InkCanvasForClass/community/releases/latest";
using (var client = new HttpClient())
if (channel == UpdateChannel.Beta)
{
client.DefaultRequestHeaders.Add("User-Agent", "ICC-CE Auto Updater");
LogHelper.WriteLogToFile("AutoUpdate | 使用GitHub API调用");
var response = await client.GetStringAsync(apiUrl);
var json = JObject.Parse(response);
string version = json["tag_name"]?.ToString();
string releaseNotes = json["body"]?.ToString();
string downloadUrl = json["assets"]?.First?["browser_download_url"]?.ToString();
// 解析发布时间
DateTime? releaseTime = null;
if (json["published_at"] != null && DateTime.TryParse(json["published_at"].ToString(), out DateTime parsedTime))
string apiUrl = "https://api.github.com/repos/InkCanvasForClass/community-beta/releases";
using (var client = new HttpClient())
{
releaseTime = parsedTime;
}
client.DefaultRequestHeaders.Add("User-Agent", "ICC-CE Auto Updater");
LogHelper.WriteLogToFile("AutoUpdate | 使用GitHub API调用");
var response = await client.GetStringAsync(apiUrl);
var releases = JArray.Parse(response);
if (!string.IsNullOrEmpty(version) && !string.IsNullOrEmpty(downloadUrl))
return (version, downloadUrl, releaseNotes, releaseTime);
if (releases.Count > 0)
{
var latestRelease = releases[0];
string version = latestRelease["tag_name"]?.ToString();
string releaseNotes = latestRelease["body"]?.ToString();
string downloadUrl = PickBrowserDownloadUrlFromAssets(latestRelease["assets"]);
DateTime? releaseTime = null;
if (latestRelease["published_at"] != null && DateTime.TryParse(latestRelease["published_at"].ToString(), out DateTime parsedTime))
{
releaseTime = parsedTime;
}
if (!string.IsNullOrEmpty(version) && !string.IsNullOrEmpty(downloadUrl))
return (version, downloadUrl, releaseNotes, releaseTime);
}
}
}
else
{
string apiUrl = channel == UpdateChannel.Preview
? "https://api.github.com/repos/InkCanvasForClass/community-beta/releases/latest"
: "https://api.github.com/repos/InkCanvasForClass/community/releases/latest";
using (var client = new HttpClient())
{
client.DefaultRequestHeaders.Add("User-Agent", "ICC-CE Auto Updater");
LogHelper.WriteLogToFile("AutoUpdate | 使用GitHub API调用");
var response = await client.GetStringAsync(apiUrl);
var json = JObject.Parse(response);
string version = json["tag_name"]?.ToString();
string releaseNotes = json["body"]?.ToString();
string downloadUrl = PickBrowserDownloadUrlFromAssets(json["assets"]);
DateTime? releaseTime = null;
if (json["published_at"] != null && DateTime.TryParse(json["published_at"].ToString(), out DateTime parsedTime))
{
releaseTime = parsedTime;
}
if (!string.IsNullOrEmpty(version) && !string.IsNullOrEmpty(downloadUrl))
return (version, downloadUrl, releaseNotes, releaseTime);
}
}
}
catch (Exception ex)
@@ -648,9 +1015,11 @@ namespace Ink_Canvas.Helpers
// 使用多线路组下载新版(支持自动切换)
public static async Task<bool> DownloadSetupFileWithFallback(string version, List<UpdateLineGroup> groups, Action<double, string> progressCallback = null)
{
var session = BeginDownloadSession();
try
{
statusFilePath = Path.Combine(updatesFolderPath, $"DownloadV{version}Status.txt");
version = NormalizeVersionForUpdate(version);
statusFilePath = GetUpdateDownloadStatusFilePath(version);
if (File.Exists(statusFilePath) && File.ReadAllText(statusFilePath).Trim().ToLower() == "true")
{
@@ -666,34 +1035,36 @@ namespace Ink_Canvas.Helpers
LogHelper.WriteLogToFile($"AutoUpdate | 创建更新目录: {updatesFolderPath}");
}
string zipFilePath = Path.Combine(updatesFolderPath, $"InkCanvasForClass.CE.{version}.zip");
string zipFilePath = GetLocalUpdateZipFilePath(version);
LogHelper.WriteLogToFile($"AutoUpdate | 目标文件路径: {zipFilePath}");
SaveDownloadStatus(false);
// 优先尝试“智教联盟”线路组
var zhiJiaoGroup = groups.FirstOrDefault(g => g.GroupName == "智教联盟");
// 优先尝试"inkeys"线路组
var inkeysGroup = groups.FirstOrDefault(g => g.GroupName == "inkeys");
if (zhiJiaoGroup != null || inkeysGroup != null)
if (inkeysGroup != null)
{
var priorityGroups = new List<UpdateLineGroup>();
if (zhiJiaoGroup != null)
{
priorityGroups.Add(zhiJiaoGroup);
LogHelper.WriteLogToFile("AutoUpdate | 下载时优先尝试智教联盟线路组");
}
if (inkeysGroup != null)
{
priorityGroups.Add(inkeysGroup);
LogHelper.WriteLogToFile("AutoUpdate | 下载时优先尝试inkeys线路组");
}
groups = priorityGroups.Concat(groups.Where(g => g.GroupName != "智教联盟" && g.GroupName != "inkeys")).ToList();
groups.Remove(inkeysGroup);
groups.Insert(0, inkeysGroup);
LogHelper.WriteLogToFile("AutoUpdate | 下载时优先尝试inkeys线路组");
}
// 依次尝试每个线路组
CancellationToken groupLoopToken;
lock (_activeDownloadLock)
{
groupLoopToken = _activeDownloadCts?.Token ?? CancellationToken.None;
}
foreach (var group in groups)
{
if (groupLoopToken.IsCancellationRequested)
{
LogHelper.WriteLogToFile("AutoUpdate | 用户已取消,停止尝试后续线路组");
break;
}
string url = string.Format(group.DownloadUrlFormat, version);
url = AppendX64SuffixBeforeZipExtension(url);
// 智教联盟需要先获取真实下载地址
if (group.GroupName == "智教联盟")
{
@@ -717,6 +1088,12 @@ namespace Ink_Canvas.Helpers
bool downloadSuccess = await DownloadFile(url, zipFilePath, progressCallback);
if (groupLoopToken.IsCancellationRequested)
{
LogHelper.WriteLogToFile("AutoUpdate | 用户已取消,停止尝试后续线路组");
break;
}
if (downloadSuccess)
{
SaveDownloadStatus(true);
@@ -732,6 +1109,13 @@ namespace Ink_Canvas.Helpers
progressCallback?.Invoke(0, "所有线路组下载均失败");
return false;
}
catch (OperationCanceledException)
{
LogHelper.WriteLogToFile("AutoUpdate | 下载已被用户取消", LogHelper.LogType.Warning);
SaveDownloadStatus(false);
progressCallback?.Invoke(0, "下载已取消");
return false;
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"AutoUpdate | 下载更新时出错: {ex.Message}", LogHelper.LogType.Error);
@@ -744,6 +1128,10 @@ namespace Ink_Canvas.Helpers
progressCallback?.Invoke(0, $"下载异常: {ex.Message}");
return false;
}
finally
{
EndDownloadSession(session);
}
}
// 下载文件的具体实现
@@ -754,6 +1142,12 @@ namespace Ink_Canvas.Helpers
// 降低并发数,减少网络压力
int[] threadOptions = { 32, 16, 8, 4, 1 };
CancellationToken externalToken;
lock (_activeDownloadLock)
{
externalToken = _activeDownloadCts?.Token ?? CancellationToken.None;
}
// 检查服务器是否支持Range分块下载
bool supportRange = false;
long totalSize = -1;
@@ -857,7 +1251,7 @@ namespace Ink_Canvas.Helpers
// 增加连接超时设置
client.Timeout = TimeSpan.FromSeconds(30);
var downloadCts = CancellationTokenSource.CreateLinkedTokenSource(cts.Token);
var downloadCts = CancellationTokenSource.CreateLinkedTokenSource(cts.Token, externalToken);
var lastReadTime = DateTime.UtcNow;
bool dataReceived = false;
@@ -917,19 +1311,32 @@ namespace Ink_Canvas.Helpers
success = true;
LogHelper.WriteLogToFile($"AutoUpdate | 分块{block.Index}下载成功");
}
catch (Exception ex) when (ex is HttpRequestException || ex is IOException || ex is TaskCanceledException)
catch (Exception ex) when (ex is HttpRequestException || ex is IOException || ex is TaskCanceledException || ex is OperationCanceledException)
{
// 用户主动取消:不再重试
if (externalToken.IsCancellationRequested)
{
LogHelper.WriteLogToFile($"AutoUpdate | 分块{block.Index}下载已被用户取消", LogHelper.LogType.Warning);
if (File.Exists(tempPath))
{
try { File.Delete(tempPath); } catch { }
}
cts.Cancel();
return;
}
LogHelper.WriteLogToFile($"AutoUpdate | 分块{block.Index}下载失败,第{retry + 1}次: {ex.Message}", LogHelper.LogType.Warning);
progressCallback?.Invoke(0, $"分块{block.Index}下载失败,第{retry + 1}次: {ex.Message}");
// 清理可能损坏的分块文件
if (File.Exists(tempPath))
{
try { File.Delete(tempPath); } catch { }
try { File.Delete(tempPath); } catch (Exception innerEx) { System.Diagnostics.Debug.WriteLine(innerEx); }
}
// 增加重试间隔,避免频繁重试
await Task.Delay(2000 * (retry + 1));
try { await Task.Delay(2000 * (retry + 1), externalToken); }
catch (OperationCanceledException) { cts.Cancel(); return; }
}
}
if (success)
@@ -1050,12 +1457,18 @@ namespace Ink_Canvas.Helpers
LogHelper.WriteLogToFile($"AutoUpdate | 开始单线程下载: {fileUrl}");
progressCallback?.Invoke(0, "开始单线程下载");
CancellationToken token;
lock (_activeDownloadLock)
{
token = _activeDownloadCts?.Token ?? CancellationToken.None;
}
using (var client = new HttpClient())
{
client.DefaultRequestHeaders.UserAgent.ParseAdd("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36");
client.Timeout = TimeSpan.FromMinutes(10); // 单线程下载设置更长的超时时间
using (var resp = await client.GetAsync(fileUrl, HttpCompletionOption.ResponseHeadersRead))
using (var resp = await client.GetAsync(fileUrl, HttpCompletionOption.ResponseHeadersRead, token))
{
resp.EnsureSuccessStatusCode();
using (var fs = new FileStream(destinationPath, FileMode.Create, FileAccess.Write, FileShare.None))
@@ -1066,9 +1479,9 @@ namespace Ink_Canvas.Helpers
long downloaded = 0;
var lastProgressUpdate = DateTime.UtcNow;
while ((read = await stream.ReadAsync(buffer, 0, buffer.Length)) > 0)
while ((read = await stream.ReadAsync(buffer, 0, buffer.Length, token)) > 0)
{
await fs.WriteAsync(buffer, 0, read);
await fs.WriteAsync(buffer, 0, read, token);
downloaded += read;
// 限制进度更新频率,避免UI卡顿
@@ -1090,6 +1503,13 @@ namespace Ink_Canvas.Helpers
LogHelper.WriteLogToFile("AutoUpdate | 单线程下载完成");
return true;
}
catch (OperationCanceledException)
{
LogHelper.WriteLogToFile("AutoUpdate | 单线程下载已被取消", LogHelper.LogType.Warning);
progressCallback?.Invoke(0, "下载已取消");
try { if (File.Exists(destinationPath)) File.Delete(destinationPath); } catch { }
return false;
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"AutoUpdate | 单线程下载失败: {ex.Message}", LogHelper.LogType.Error);
@@ -1111,11 +1531,17 @@ namespace Ink_Canvas.Helpers
return resp.Content.Headers.ContentLength.Value;
}
}
catch { }
catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); }
return -1;
}
// 保存下载状态
/// <summary>
/// 将下载完成状态写入预定义的状态文件以供后续检查。
/// </summary>
/// <param name="isSuccess">指示下载是否成功;将以字符串形式写入状态文件("True" 或 "False")。</param>
/// <remarks>
/// 如果状态文件路径为空则不执行任何操作;方法内部捕获异常并记录日志,不会向调用方抛出异常。
/// </remarks>
private static void SaveDownloadStatus(bool isSuccess)
{
try
@@ -1136,11 +1562,32 @@ namespace Ink_Canvas.Helpers
}
}
// 安装新版本应用 - 优化版本,不使用命令行
/// <summary>
/// 安装指定版本的更新包并启动新版本进程以完成替换,然后退出当前应用程序。
/// </summary>
/// <remarks>
/// 该方法会临时将 App.IsUpdateInstalling 置为 true、尝试关闭进程保护(并在结束时还原)、在必要时备份当前设置、解压更新 ZIP、启动解压后的新可执行文件(以更新模式传递旧进程 ID、解压路径和目标路径等参数),并在新进程启动后关闭当前进程。方法会记录日志并在遇到错误时安全退出相应步骤,但不会抛出异常给调用方以外的上下文。</remarks>
/// <param name="version">要安装的版本号,用于定位更新包文件名(与 <see cref="GetLocalUpdateZipFilePath"/> 一致;选择 x64 包时为 InkCanvasForClass.CE.{version}-x64.zip)。</param>
/// <param name="isInSilence">指示是否以静默模式启动新版本(影响传递给新进程的参数和可能的用户提示)。</param>
public static void InstallNewVersionApp(string version, bool isInSilence)
{
bool wasProcessProtectionEnabled = false;
try
{
wasProcessProtectionEnabled = ProcessProtectionManager.Enabled;
}
catch
{
}
try
{
App.IsUpdateInstalling = true;
if (wasProcessProtectionEnabled)
{
try { ProcessProtectionManager.SetEnabled(false); } catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); }
}
// 在更新前备份设置文件
try
{
@@ -1171,7 +1618,7 @@ namespace Ink_Canvas.Helpers
LogHelper.WriteLogToFile($"更新前自动备份设置时出错: {ex.Message}", LogHelper.LogType.Error);
}
string zipFilePath = Path.Combine(updatesFolderPath, $"InkCanvasForClass.CE.{version}.zip");
string zipFilePath = GetLocalUpdateZipFilePath(version);
LogHelper.WriteLogToFile($"AutoUpdate | 检查ZIP文件: {zipFilePath}");
if (!File.Exists(zipFilePath))
@@ -1226,7 +1673,7 @@ namespace Ink_Canvas.Helpers
try
{
LogHelper.WriteLogToFile($"AutoUpdate | 开始解压ZIP文件到: {extractPath}");
ZipFile.ExtractToDirectory(zipFilePath, extractPath);
SafeZipExtractor.ExtractZipSafely(zipFilePath, extractPath, overwrite: true);
LogHelper.WriteLogToFile("AutoUpdate | ZIP文件解压完成");
}
catch (Exception ex)
@@ -1301,6 +1748,18 @@ namespace Ink_Canvas.Helpers
LogHelper.WriteLogToFile($"AutoUpdate | 内部异常: {ex.InnerException.Message}", LogHelper.LogType.Error);
}
}
finally
{
// 确保无论更新成功还是失败,都恢复标志位和进程保护状态
App.IsUpdateInstalling = false;
try
{
ProcessProtectionManager.SetEnabled(wasProcessProtectionEnabled);
}
catch
{
}
}
}
// 处理更新模式的启动参数
@@ -1854,16 +2313,16 @@ namespace Ink_Canvas.Helpers
{
foreach (string file in Directory.GetFiles(updatesFolderPath, "*", SearchOption.AllDirectories))
{
try { File.Delete(file); } catch { }
try { File.Delete(file); } catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); }
}
foreach (string dir in Directory.GetDirectories(updatesFolderPath))
{
try { Directory.Delete(dir, true); } catch { }
try { Directory.Delete(dir, true); } catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); }
}
try { Directory.Delete(updatesFolderPath, true); } catch { }
try { Directory.Delete(updatesFolderPath, true); } catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); }
}
}
catch { }
catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); }
}
// 版本修复方法,强制下载并安装指定通道的最新版本
@@ -1873,9 +2332,9 @@ namespace Ink_Canvas.Helpers
{
LogHelper.WriteLogToFile($"AutoUpdate | 开始修复版本,通道: {channel}");
// 获取远程版本号(自动选择最快线路组,始终下载远程版本,版本修复模式)
var (remoteVersion, group, _) = await CheckForUpdates(channel, true, true);
if (string.IsNullOrEmpty(remoteVersion) || group == null)
// 获取远程版本号(始终下载远程版本,版本修复模式)
var (remoteVersion, preferredGroup, _) = await CheckForUpdates(channel, true, true);
if (string.IsNullOrEmpty(remoteVersion))
{
LogHelper.WriteLogToFile("AutoUpdate | 修复版本时获取远程版本失败", LogHelper.LogType.Error);
return false;
@@ -1883,8 +2342,22 @@ namespace Ink_Canvas.Helpers
LogHelper.WriteLogToFile($"AutoUpdate | 修复版本远程版本: {remoteVersion}");
var availableGroups = await GetAvailableLineGroupsOrdered(channel);
if (availableGroups.Count == 0)
{
LogHelper.WriteLogToFile("AutoUpdate | 修复版本时无可用线路组", LogHelper.LogType.Error);
return false;
}
if (preferredGroup != null)
{
availableGroups.RemoveAll(g => g.GroupName == preferredGroup.GroupName);
availableGroups.Insert(0, preferredGroup);
LogHelper.WriteLogToFile($"AutoUpdate | 修复版本下载优先使用线路组: {preferredGroup.GroupName}");
}
// 无论版本是否为最新,都下载远程版本
bool downloadResult = await DownloadSetupFile(remoteVersion, group);
bool downloadResult = await DownloadSetupFileWithFallback(remoteVersion, availableGroups);
if (!downloadResult)
{
LogHelper.WriteLogToFile("AutoUpdate | 修复版本时下载更新失败", LogHelper.LogType.Error);
@@ -1913,7 +2386,7 @@ namespace Ink_Canvas.Helpers
var result = new List<(string, string, string)>();
try
{
string apiUrl = channel == UpdateChannel.Beta
string apiUrl = (channel == UpdateChannel.Beta || channel == UpdateChannel.Preview)
? "https://api.github.com/repos/InkCanvasForClass/community-beta/releases"
: "https://api.github.com/repos/InkCanvasForClass/community/releases";
using (var client = new HttpClient())
@@ -1926,7 +2399,7 @@ namespace Ink_Canvas.Helpers
{
string version = item["tag_name"]?.ToString();
string releaseNotes = item["body"]?.ToString();
string downloadUrl = item["assets"]?.First?["browser_download_url"]?.ToString();
string downloadUrl = PickBrowserDownloadUrlFromAssets(item["assets"]);
if (!string.IsNullOrEmpty(version) && !string.IsNullOrEmpty(downloadUrl))
result.Add((version, downloadUrl, releaseNotes));
}
@@ -2072,4 +2545,4 @@ namespace Ink_Canvas.Helpers
return currentTime >= StartTime || currentTime <= EndTime;
}
}
}
}
+603
View File
@@ -0,0 +1,603 @@
using Newtonsoft.Json;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace Ink_Canvas.Helpers
{
/// <summary>
/// 上传队列项数据(用于序列化)
/// </summary>
public class UploadQueueItemData
{
[JsonProperty("file_path")]
public string FilePath { get; set; }
[JsonProperty("retry_count")]
public int RetryCount { get; set; }
[JsonProperty("added_time")]
public DateTime AddedTime { get; set; }
}
/// <summary>
/// 上传队列项
/// </summary>
public class UploadQueueItem
{
public string FilePath { get; set; }
public int RetryCount { get; set; }
}
/// <summary>
/// 通用上传队列基类
/// </summary>
public abstract class BaseUploadQueue : IDisposable
{
protected const int BATCH_SIZE = 10; // 批量上传大小
protected const int MAX_RETRY_COUNT = 3; // 最大重试次数
/// <summary>
/// 上传队列
/// </summary>
protected readonly ConcurrentQueue<UploadQueueItem> _uploadQueue = new ConcurrentQueue<UploadQueueItem>();
/// <summary>
/// 队列处理锁,防止并发处理
/// </summary>
protected readonly SemaphoreSlim _queueProcessingLock = new SemaphoreSlim(1, 1);
/// <summary>
/// 队列保存锁,防止并发保存
/// </summary>
protected readonly SemaphoreSlim _queueSaveLock = new SemaphoreSlim(1, 1);
/// <summary>
/// 是否已初始化队列
/// </summary>
protected bool _isQueueInitialized = false;
/// <summary>
/// 是否已释放资源
/// </summary>
private bool _disposed = false;
/// <summary>
/// 队列文件名
/// </summary>
protected abstract string QueueFileName { get; }
/// <summary>
/// 允许的文件扩展名
/// </summary>
protected virtual HashSet<string> AllowedExtensions => new HashSet<string> { ".png", ".icstk", ".xml", ".zip" };
/// <summary>
/// 获取队列文件路径
/// </summary>
protected string GetQueueFilePath()
{
var configsDir = Path.Combine(App.RootPath, "Configs");
if (!Directory.Exists(configsDir))
{
Directory.CreateDirectory(configsDir);
}
return Path.Combine(configsDir, QueueFileName);
}
/// <summary>
/// 获取最大文件大小
/// </summary>
/// <param name="extension">文件扩展名</param>
/// <returns>最大文件大小(字节)</returns>
protected virtual long GetMaxFileSize(string extension)
{
return extension == ".zip" ? 50 * 1024 * 1024 : 10 * 1024 * 1024;
}
/// <summary>
/// 初始化上传队列
/// </summary>
public void InitializeQueue()
{
if (_isQueueInitialized)
{
return;
}
try
{
var queueFilePath = GetQueueFilePath();
if (!File.Exists(queueFilePath))
{
_isQueueInitialized = true;
return;
}
var jsonContent = File.ReadAllText(queueFilePath);
if (string.IsNullOrWhiteSpace(jsonContent))
{
_isQueueInitialized = true;
return;
}
var queueData = JsonConvert.DeserializeObject<List<UploadQueueItemData>>(jsonContent);
if (queueData == null || queueData.Count == 0)
{
_isQueueInitialized = true;
return;
}
int restoredCount = 0;
int skippedCount = 0;
foreach (var item in queueData)
{
// 验证文件是否存在
if (!File.Exists(item.FilePath))
{
skippedCount++;
continue;
}
// 验证文件格式和大小
if (!IsValidFile(item.FilePath))
{
skippedCount++;
continue;
}
// 恢复队列项
_uploadQueue.Enqueue(new UploadQueueItem
{
FilePath = item.FilePath,
RetryCount = item.RetryCount
});
restoredCount++;
}
_isQueueInitialized = true;
if (restoredCount > 0)
{
LogHelper.WriteLogToFile($"[{GetType().Name}] 已恢复上传队列:{restoredCount}个文件,跳过{skippedCount}个无效文件", LogHelper.LogType.Event);
// 如果恢复了队列,触发处理
_ = Task.Run(async () =>
{
try
{
await ProcessUploadQueueAsync().ConfigureAwait(false);
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"[{GetType().Name}] 恢复上传队列后处理时出错: {ex}", LogHelper.LogType.Error);
}
});
}
else if (skippedCount > 0)
{
LogHelper.WriteLogToFile($"[{GetType().Name}] 队列恢复完成:跳过{skippedCount}个无效文件", LogHelper.LogType.Event);
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"[{GetType().Name}] 恢复上传队列时出错: {ex.Message}", LogHelper.LogType.Error);
_isQueueInitialized = true; // 即使出错也标记为已初始化,避免重复尝试
}
}
/// <summary>
/// 保存队列到文件
/// </summary>
protected async Task SaveQueueToFileAsync(CancellationToken cancellationToken = default)
{
if (!await _queueSaveLock.WaitAsync(1000, cancellationToken)) // 最多等待1秒
{
return; // 如果无法获取锁,跳过保存(避免阻塞)
}
try
{
cancellationToken.ThrowIfCancellationRequested();
var queueData = new List<UploadQueueItemData>();
// 将队列转换为可序列化的格式
foreach (var item in _uploadQueue)
{
cancellationToken.ThrowIfCancellationRequested();
queueData.Add(new UploadQueueItemData
{
FilePath = item.FilePath,
RetryCount = item.RetryCount,
AddedTime = DateTime.Now
});
}
var queueFilePath = GetQueueFilePath();
// 如果队列为空,清空文件
if (queueData.Count == 0)
{
ClearQueueFile();
return;
}
var jsonContent = JsonConvert.SerializeObject(queueData, Formatting.Indented);
// 使用进程保护的写入门控,避免安全面板中"进程文件保护"占用导致无法写入
var tempFilePath = queueFilePath + ".tmp";
ProcessProtectionManager.WithWriteAccess(queueFilePath, () =>
{
File.WriteAllText(tempFilePath, jsonContent);
if (File.Exists(queueFilePath))
File.Delete(queueFilePath);
File.Move(tempFilePath, queueFilePath);
});
}
catch (OperationCanceledException)
{
// 取消操作,静默处理
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"[{GetType().Name}] 保存上传队列时出错: {ex.Message}", LogHelper.LogType.Error);
}
finally
{
_queueSaveLock.Release();
}
}
/// <summary>
/// 清空队列文件
/// </summary>
protected void ClearQueueFile()
{
try
{
var queueFilePath = GetQueueFilePath();
ProcessProtectionManager.WithWriteAccess(queueFilePath, () =>
{
if (File.Exists(queueFilePath))
File.WriteAllText(queueFilePath, "[]");
});
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"[{GetType().Name}] 清空队列文件时出错: {ex.Message}", LogHelper.LogType.Error);
}
}
/// <summary>
/// 将文件加入上传队列
/// </summary>
protected void EnqueueFile(string filePath, int retryCount = 0, CancellationToken cancellationToken = default)
{
_uploadQueue.Enqueue(new UploadQueueItem
{
FilePath = filePath,
RetryCount = retryCount
});
// 异步保存队列到文件
_ = Task.Run(async () =>
{
try
{
cancellationToken.ThrowIfCancellationRequested();
await SaveQueueToFileAsync(cancellationToken).ConfigureAwait(false);
}
catch (OperationCanceledException)
{
// 取消操作,静默处理
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"[{GetType().Name}] 保存上传队列时出错(后台任务): {ex}", LogHelper.LogType.Error);
}
}, cancellationToken);
// 触发队列处理
_ = ProcessUploadQueueAsync(cancellationToken);
}
/// <summary>
/// 处理上传队列,批量上传文件
/// </summary>
protected async Task ProcessUploadQueueAsync(CancellationToken cancellationToken = default)
{
// 使用信号量防止并发处理
if (!await _queueProcessingLock.WaitAsync(0, cancellationToken))
{
return; // 已有处理任务在运行
}
try
{
cancellationToken.ThrowIfCancellationRequested();
var filesToUpload = new List<UploadQueueItem>();
// 从队列中取出最多BATCH_SIZE个文件
int count = 0;
while (count < BATCH_SIZE && _uploadQueue.TryDequeue(out UploadQueueItem item))
{
cancellationToken.ThrowIfCancellationRequested();
// 再次检查文件是否存在
if (File.Exists(item.FilePath) && IsValidFile(item.FilePath))
{
filesToUpload.Add(item);
count++;
}
}
if (filesToUpload.Count == 0)
{
return;
}
// 检查是否启用
if (!IsUploadEnabled())
{
LogHelper.WriteLogToFile($"[{GetType().Name}] 上传失败:上传未启用", LogHelper.LogType.Error);
// 将文件重新加入队列
foreach (var item in filesToUpload)
{
EnqueueFile(item.FilePath, item.RetryCount, cancellationToken);
}
return;
}
// 并发上传所有文件,并处理失败重试
var uploadTasks = filesToUpload.Select(async item =>
{
try
{
cancellationToken.ThrowIfCancellationRequested();
var success = await UploadFileInternalAsync(item.FilePath, cancellationToken);
if (!success)
{
// 检查是否是可重试的错误
if (IsRetryableError(item.FilePath))
{
// 检查重试次数
if (item.RetryCount < MAX_RETRY_COUNT)
{
LogHelper.WriteLogToFile($"[{GetType().Name}] 上传失败,将重试 ({item.RetryCount + 1}/{MAX_RETRY_COUNT}): {Path.GetFileName(item.FilePath)}", LogHelper.LogType.Event);
EnqueueFile(item.FilePath, item.RetryCount + 1, cancellationToken);
}
else
{
LogHelper.WriteLogToFile($"[{GetType().Name}] 上传失败,已达到最大重试次数: {Path.GetFileName(item.FilePath)}", LogHelper.LogType.Error);
}
}
}
return success;
}
catch (OperationCanceledException)
{
// 取消操作,将文件重新加入队列
EnqueueFile(item.FilePath, item.RetryCount, cancellationToken);
throw;
}
catch (Exception ex)
{
// 检查是否是可重试的错误(超时、网络错误等)
var errorMessage = ex.Message.ToLower();
bool isRetryable = errorMessage.Contains("超时") ||
errorMessage.Contains("timeout") ||
errorMessage.Contains("网络错误") ||
errorMessage.Contains("network") ||
errorMessage.Contains("408") || // 请求超时
errorMessage.Contains("423") || // 资源锁定
errorMessage.Contains("429") || // 请求过多
errorMessage.Contains("500") || // 服务器错误
errorMessage.Contains("502") || // 网关错误
errorMessage.Contains("503") || // 服务不可用
errorMessage.Contains("504"); // 网关超时
if (isRetryable && IsRetryableError(item.FilePath))
{
// 检查重试次数
if (item.RetryCount < MAX_RETRY_COUNT)
{
LogHelper.WriteLogToFile($"[{GetType().Name}] 上传失败({ex.Message}),将重试 ({item.RetryCount + 1}/{MAX_RETRY_COUNT}): {Path.GetFileName(item.FilePath)}", LogHelper.LogType.Error);
EnqueueFile(item.FilePath, item.RetryCount + 1, cancellationToken);
}
else
{
LogHelper.WriteLogToFile($"[{GetType().Name}] 上传失败,已达到最大重试次数: {Path.GetFileName(item.FilePath)}", LogHelper.LogType.Error);
}
}
else
{
LogHelper.WriteLogToFile($"[{GetType().Name}] 上传失败(不可重试): {Path.GetFileName(item.FilePath)} - {ex.Message}", LogHelper.LogType.Error);
}
return false;
}
});
await Task.WhenAll(uploadTasks).ConfigureAwait(false);
// 上传完成后保存队列状态
await SaveQueueToFileAsync(cancellationToken).ConfigureAwait(false);
// 检查队列中是否还有文件,如果有就继续处理
if (_uploadQueue.Count > 0)
{
_ = Task.Run(async () =>
{
try
{
await ProcessUploadQueueAsync(cancellationToken).ConfigureAwait(false);
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"[{GetType().Name}] 继续处理上传队列时出错: {ex}", LogHelper.LogType.Error);
}
}, cancellationToken);
}
}
finally
{
_queueProcessingLock.Release();
}
}
/// <summary>
/// 验证文件是否有效
/// </summary>
protected virtual bool IsValidFile(string filePath)
{
try
{
var fileExtension = Path.GetExtension(filePath).ToLower();
if (!AllowedExtensions.Contains(fileExtension))
{
return false;
}
var fileInfo = new FileInfo(filePath);
long maxSize = GetMaxFileSize(fileExtension);
if (fileInfo.Length > maxSize)
{
LogHelper.WriteLogToFile($"[{GetType().Name}] 上传失败:文件过大({fileInfo.Length / 1024 / 1024:F2}MB),超过{maxSize / 1024 / 1024}MB限制", LogHelper.LogType.Error);
return false;
}
return true;
}
catch
{
return false;
}
}
/// <summary>
/// 判断错误是否可重试
/// </summary>
protected bool IsRetryableError(string filePath)
{
// 检查文件是否存在
if (!File.Exists(filePath))
{
return false; // 文件不存在,不可重试
}
// 检查文件是否有效
if (!IsValidFile(filePath))
{
return false; // 文件无效,不可重试
}
// 检查是否启用
if (!IsUploadEnabled())
{
return false; // 上传未启用,不可重试
}
// 其他错误(超时、网络错误等)可以重试
return true;
}
/// <summary>
/// 检查上传是否启用
/// </summary>
protected abstract bool IsUploadEnabled();
/// <summary>
/// 内部上传方法,执行实际上传操作
/// </summary>
protected abstract Task<bool> UploadFileInternalAsync(string filePath, CancellationToken cancellationToken);
/// <summary>
/// 异步上传文件
/// </summary>
public async Task<bool> UploadFileAsync(string filePath, CancellationToken cancellationToken = default)
{
try
{
cancellationToken.ThrowIfCancellationRequested();
// 检查是否启用
if (!IsUploadEnabled())
{
return false;
}
// 基本验证
if (!File.Exists(filePath))
{
LogHelper.WriteLogToFile($"[{GetType().Name}] 上传失败:文件不存在 - {filePath}", LogHelper.LogType.Error);
return false;
}
if (!IsValidFile(filePath))
{
return false;
}
// 确保队列已初始化
if (!_isQueueInitialized)
{
InitializeQueue();
}
// 加入队列
EnqueueFile(filePath, 0, cancellationToken);
return true;
}
catch (OperationCanceledException)
{
LogHelper.WriteLogToFile($"[{GetType().Name}] 上传被取消: {Path.GetFileName(filePath)}", LogHelper.LogType.Event);
throw;
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"[{GetType().Name}] 加入上传队列时出错: {ex.Message}", LogHelper.LogType.Error);
return false;
}
}
/// <summary>
/// 释放资源
/// </summary>
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
/// <summary>
/// 释放资源
/// </summary>
/// <param name="disposing">是否手动释放</param>
protected virtual void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
{
_queueProcessingLock?.Dispose();
_queueSaveLock?.Dispose();
}
_disposed = true;
}
}
/// <summary>
/// 析构函数
/// </summary>
~BaseUploadQueue()
{
Dispose(false);
}
}
}
+424
View File
@@ -0,0 +1,424 @@
using AForge.Video;
using AForge.Video.DirectShow;
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Imaging;
using System.Linq;
using System.Windows.Media.Imaging;
using System.Windows.Threading;
namespace Ink_Canvas.Helpers
{
public class CameraService : IDisposable
{
private VideoCaptureDevice _videoSource;
private bool _isCapturing;
private Bitmap _currentFrame;
private readonly object _frameLock = new object();
private Dispatcher _dispatcher;
// 新增属性
private int _rotationAngle = 0; // 0=0度,1=90度,2=180度,3=270度
private int _resolutionWidth = 640;
private int _resolutionHeight = 480;
public event EventHandler<Bitmap> FrameReceived;
public event EventHandler<string> ErrorOccurred;
public bool IsCapturing => _isCapturing;
public List<FilterInfo> AvailableCameras { get; private set; }
public FilterInfo CurrentCamera { get; private set; }
// 新增属性
public int RotationAngle
{
get => _rotationAngle;
set => _rotationAngle = Math.Max(0, Math.Min(3, value));
}
public int ResolutionWidth
{
get => _resolutionWidth;
set => _resolutionWidth = Math.Max(320, Math.Min(3840, value));
}
public int ResolutionHeight
{
get => _resolutionHeight;
set => _resolutionHeight = Math.Max(240, Math.Min(2160, value));
}
public CameraService()
{
_dispatcher = Dispatcher.CurrentDispatcher;
AvailableCameras = new List<FilterInfo>();
RefreshCameraList();
}
public CameraService(int rotationAngle, int resolutionWidth, int resolutionHeight)
{
_dispatcher = Dispatcher.CurrentDispatcher;
AvailableCameras = new List<FilterInfo>();
_rotationAngle = rotationAngle;
_resolutionWidth = resolutionWidth;
_resolutionHeight = resolutionHeight;
RefreshCameraList();
}
/// <summary>
/// 刷新可用摄像头列表
/// </summary>
public void RefreshCameraList()
{
try
{
AvailableCameras.Clear();
var videoDevices = new FilterInfoCollection(FilterCategory.VideoInputDevice);
foreach (FilterInfo device in videoDevices)
{
AvailableCameras.Add(device);
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"刷新摄像头列表失败: {ex.Message}", LogHelper.LogType.Error);
ErrorOccurred?.Invoke(this, $"刷新摄像头列表失败: {ex.Message}");
}
}
/// <summary>
/// 开始摄像头预览
/// </summary>
/// <param name="cameraIndex">摄像头索引</param>
public bool StartPreview(int cameraIndex = 0)
{
try
{
if (AvailableCameras.Count == 0)
{
RefreshCameraList();
if (AvailableCameras.Count == 0)
{
ErrorOccurred?.Invoke(this, "未找到可用的摄像头设备");
return false;
}
}
if (cameraIndex < 0 || cameraIndex >= AvailableCameras.Count)
{
ErrorOccurred?.Invoke(this, "摄像头索引超出范围");
return false;
}
// 停止当前预览
StopPreview();
CurrentCamera = AvailableCameras[cameraIndex];
_videoSource = new VideoCaptureDevice(CurrentCamera.MonikerString);
// 设置视频源事件处理
_videoSource.NewFrame += VideoSource_NewFrame;
// 启动视频源
_videoSource.Start();
_isCapturing = true;
LogHelper.WriteLogToFile($"开始摄像头预览: {CurrentCamera.Name}");
return true;
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"启动摄像头预览失败: {ex.Message}", LogHelper.LogType.Error);
ErrorOccurred?.Invoke(this, $"启动摄像头预览失败: {ex.Message}");
return false;
}
}
/// <summary>
/// 停止摄像头预览
/// </summary>
public void StopPreview()
{
try
{
if (_videoSource != null && _videoSource.IsRunning)
{
_videoSource.SignalToStop();
_videoSource.WaitForStop();
_videoSource.NewFrame -= VideoSource_NewFrame;
_videoSource = null;
}
_isCapturing = false;
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"停止摄像头预览失败: {ex.Message}", LogHelper.LogType.Error);
}
}
/// <summary>
/// 切换到指定摄像头
/// </summary>
/// <param name="cameraIndex">摄像头索引</param>
public bool SwitchCamera(int cameraIndex)
{
try
{
if (cameraIndex < 0 || cameraIndex >= AvailableCameras.Count)
{
ErrorOccurred?.Invoke(this, "摄像头索引超出范围");
return false;
}
return StartPreview(cameraIndex);
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"切换摄像头失败: {ex.Message}", LogHelper.LogType.Error);
ErrorOccurred?.Invoke(this, $"切换摄像头失败: {ex.Message}");
return false;
}
}
/// <summary>
/// 获取当前帧的BitmapSource(WPF格式),直接返回可用的WPF位图
/// </summary>
public BitmapSource GetCurrentFrameAsBitmapSource()
{
lock (_frameLock)
{
if (_currentFrame == null)
return null;
try
{
// 验证位图有效性
if (_currentFrame.Width <= 0 || _currentFrame.Height <= 0)
return null;
// 使用更安全的方法转换位图
var bitmapData = _currentFrame.LockBits(
new Rectangle(0, 0, _currentFrame.Width, _currentFrame.Height),
ImageLockMode.ReadOnly,
_currentFrame.PixelFormat);
try
{
// 根据像素格式选择合适的WPF像素格式
System.Windows.Media.PixelFormat wpfPixelFormat;
switch (_currentFrame.PixelFormat)
{
case PixelFormat.Format24bppRgb:
wpfPixelFormat = System.Windows.Media.PixelFormats.Bgr24;
break;
case PixelFormat.Format32bppArgb:
wpfPixelFormat = System.Windows.Media.PixelFormats.Bgra32;
break;
case PixelFormat.Format32bppRgb:
wpfPixelFormat = System.Windows.Media.PixelFormats.Bgr32;
break;
default:
wpfPixelFormat = System.Windows.Media.PixelFormats.Bgr24;
break;
}
var bitmapSource = BitmapSource.Create(
bitmapData.Width,
bitmapData.Height,
_currentFrame.HorizontalResolution,
_currentFrame.VerticalResolution,
wpfPixelFormat,
null,
bitmapData.Scan0,
bitmapData.Stride * bitmapData.Height,
bitmapData.Stride);
bitmapSource.Freeze();
return bitmapSource;
}
finally
{
_currentFrame.UnlockBits(bitmapData);
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"转换帧为BitmapSource失败: {ex.Message}", LogHelper.LogType.Error);
return null;
}
}
}
/// <summary>
/// 视频源新帧事件处理
/// </summary>
private void VideoSource_NewFrame(object sender, NewFrameEventArgs eventArgs)
{
try
{
lock (_frameLock)
{
// 释放之前的帧
_currentFrame?.Dispose();
// 创建新的位图,避免Clone的问题
var sourceFrame = eventArgs.Frame;
if (sourceFrame != null)
{
try
{
var width = sourceFrame.Width;
var height = sourceFrame.Height;
if (width > 0 && height > 0)
{
// 应用旋转
Bitmap rotatedFrame = ApplyRotation(sourceFrame);
int targetWidth = _resolutionWidth;
int targetHeight = _resolutionHeight;
if (_rotationAngle == 1 || _rotationAngle == 3)
{
targetWidth = _resolutionHeight;
targetHeight = _resolutionWidth;
}
_currentFrame = ResizeImageWithAspectRatio(rotatedFrame, targetWidth, targetHeight);
rotatedFrame?.Dispose();
}
else
{
_currentFrame = null;
}
}
catch (Exception frameEx)
{
LogHelper.WriteLogToFile($"处理源帧失败: {frameEx.Message}", LogHelper.LogType.Error);
_currentFrame = null;
}
}
else
{
_currentFrame = null;
}
}
// 在UI线程中触发事件
_dispatcher.BeginInvoke(new Action(() =>
{
FrameReceived?.Invoke(this, _currentFrame);
}));
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"处理新帧失败: {ex.Message}", LogHelper.LogType.Error);
ErrorOccurred?.Invoke(this, $"处理新帧失败: {ex.Message}");
}
}
/// <summary>
/// 获取摄像头名称列表
/// </summary>
public List<string> GetCameraNames()
{
return AvailableCameras.Select(camera => camera.Name).ToList();
}
/// <summary>
/// 检查是否有可用摄像头
/// </summary>
public bool HasAvailableCameras()
{
if (AvailableCameras.Count == 0)
{
RefreshCameraList();
}
return AvailableCameras.Count > 0;
}
/// <summary>
/// 应用旋转到图像
/// </summary>
private Bitmap ApplyRotation(Bitmap source)
{
if (_rotationAngle == 0)
return new Bitmap(source);
var rotationType = RotateFlipType.RotateNoneFlipNone;
switch (_rotationAngle)
{
case 1: rotationType = RotateFlipType.Rotate90FlipNone; break;
case 2: rotationType = RotateFlipType.Rotate180FlipNone; break;
case 3: rotationType = RotateFlipType.Rotate270FlipNone; break;
}
var rotated = new Bitmap(source);
rotated.RotateFlip(rotationType);
return rotated;
}
/// <summary>
/// 调整图像大小
/// </summary>
private Bitmap ResizeImageWithAspectRatio(Bitmap source, int targetWidth, int targetHeight)
{
if (source.Width == targetWidth && source.Height == targetHeight)
return new Bitmap(source);
double scaleX = (double)targetWidth / source.Width;
double scaleY = (double)targetHeight / source.Height;
double scale = Math.Min(scaleX, scaleY);
// 计算实际尺寸
int actualWidth = (int)(source.Width * scale);
int actualHeight = (int)(source.Height * scale);
var resized = new Bitmap(actualWidth, actualHeight, PixelFormat.Format24bppRgb);
using (var graphics = Graphics.FromImage(resized))
{
graphics.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic;
graphics.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality;
graphics.PixelOffsetMode = System.Drawing.Drawing2D.PixelOffsetMode.HighQuality;
graphics.DrawImage(source, 0, 0, actualWidth, actualHeight);
}
return resized;
}
/// <summary>
/// 调整图像大小
/// </summary>
private Bitmap ResizeImage(Bitmap source, int width, int height)
{
if (source.Width == width && source.Height == height)
return new Bitmap(source);
var resized = new Bitmap(width, height, PixelFormat.Format24bppRgb);
using (var graphics = Graphics.FromImage(resized))
{
graphics.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic;
graphics.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality;
graphics.PixelOffsetMode = System.Drawing.Drawing2D.PixelOffsetMode.HighQuality;
graphics.DrawImage(source, 0, 0, width, height);
}
return resized;
}
public void Dispose()
{
StopPreview();
lock (_frameLock)
{
_currentFrame?.Dispose();
}
}
}
}
+109
View File
@@ -0,0 +1,109 @@
using System;
namespace Ink_Canvas.Helpers
{
public class ComPPTLinkManager : IPPTLinkManager
{
private readonly PPTManager _inner;
public ComPPTLinkManager()
{
_inner = new PPTManager();
_inner.SlideShowBegin += wn => SlideShowBegin?.Invoke(wn);
_inner.SlideShowNextSlide += wn => SlideShowNextSlide?.Invoke(wn);
_inner.SlideShowEnd += pres => SlideShowEnd?.Invoke(pres);
_inner.PresentationOpen += pres => PresentationOpen?.Invoke(pres);
_inner.PresentationClose += pres => PresentationClose?.Invoke(pres);
_inner.PPTConnectionChanged += connected => PPTConnectionChanged?.Invoke(connected);
_inner.SlideShowStateChanged += inSlideShow => SlideShowStateChanged?.Invoke(inSlideShow);
}
#region IPPTLinkManager
public event Action<object> SlideShowBegin;
public event Action<object> SlideShowNextSlide;
public event Action<object> SlideShowEnd;
public event Action<object> PresentationOpen;
public event Action<object> PresentationClose;
public event Action<bool> PPTConnectionChanged;
public event Action<bool> SlideShowStateChanged;
#endregion
#region IPPTLinkManager
public bool IsConnected => _inner.IsConnected;
public bool IsInSlideShow => _inner.IsInSlideShow;
public bool IsSupportWPS
{
get => _inner.IsSupportWPS;
set => _inner.IsSupportWPS = value;
}
public bool SkipAnimationsWhenNavigating
{
get => _inner.SkipAnimationsWhenNavigating;
set => _inner.SkipAnimationsWhenNavigating = value;
}
public int SlidesCount => _inner.SlidesCount;
public object PPTApplication => _inner.PPTApplication;
#endregion
#region
/// <summary>
/// 开始监控本地 PowerPoint 的连接与运行状态,并在状态变化时触发相应事件。
/// </summary>
public void StartMonitoring() => _inner.StartMonitoring();
/// <summary>
/// 停止对 PowerPoint 的监控,断开当前连接并停止触发相关事件。
/// </summary>
public void StopMonitoring() => _inner.StopMonitoring();
/// <summary>
/// 强制断开当前 COM PPT 连接并停止对其监控,同时写入事件日志。
/// </summary>
/// <remarks>
/// 会向日志记录一条事件信息并调用内部管理器停止监控;该方法不会重新启动监控或重新初始化内部管理器实例。
/// </remarks>
public void ReloadConnection()
{
LogHelper.WriteLogToFile("COM PPT 执行热重载:强制断开并重新连接", LogHelper.LogType.Event);
_inner.StopMonitoring();
}
#endregion
#region
public bool TryStartSlideShow() => _inner.TryStartSlideShow();
public bool TryEndSlideShow() => _inner.TryEndSlideShow();
#endregion
#region
public bool TryNavigateToSlide(int slideNumber) => _inner.TryNavigateToSlide(slideNumber);
public bool TryNavigateNext() => _inner.TryNavigateNext();
public bool TryNavigatePrevious() => _inner.TryNavigatePrevious();
#endregion
#region
public int GetCurrentSlideNumber() => _inner.GetCurrentSlideNumber();
public string GetPresentationName() => _inner.GetPresentationName();
public bool TryShowSlideNavigation() => _inner.TryShowSlideNavigation();
public object GetCurrentActivePresentation() => _inner.GetCurrentActivePresentation();
#endregion
#region IDisposable
public void Dispose()
{
_inner?.Dispose();
}
#endregion
}
}
+161
View File
@@ -0,0 +1,161 @@
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
namespace Ink_Canvas.Helpers
{
/// <summary>
/// 提供多配置文件保存、切换与热重载支持。
/// 方案保存在 Configs/Profiles 目录下,当前生效的配置仍为 Configs/Settings.json。
/// </summary>
public static class ConfigProfileManager
{
private static readonly string ProfilesDir = Path.Combine(App.RootPath, "Configs", "Profiles");
private static readonly string SettingsFilePath = Path.Combine(App.RootPath, "Configs", "Settings.json");
private const string ProfileExtension = ".json";
/// <summary>将配置文件名称转为安全文件名(去掉非法字符)。</summary>
private static string ToSafeFileName(string profileName)
{
if (string.IsNullOrWhiteSpace(profileName)) return "未命名";
var invalid = Path.GetInvalidFileNameChars();
var name = string.Join("_", profileName.Trim().Split(invalid, StringSplitOptions.RemoveEmptyEntries));
return string.IsNullOrEmpty(name) ? "未命名" : name;
}
/// <summary>确保配置文件目录存在。</summary>
public static void EnsureProfilesDirectory()
{
try
{
if (!Directory.Exists(ProfilesDir))
{
ProcessProtectionManager.WithWriteAccess(ProfilesDir, () => Directory.CreateDirectory(ProfilesDir));
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"创建配置文件目录失败: {ex.Message}", LogHelper.LogType.Error);
}
}
/// <summary>获取所有配置文件名称(不含扩展名),按名称排序。</summary>
public static IReadOnlyList<string> ListProfileNames()
{
try
{
EnsureProfilesDirectory();
if (!Directory.Exists(ProfilesDir)) return Array.Empty<string>();
var files = Directory.GetFiles(ProfilesDir, "*" + ProfileExtension);
return files
.Select(f => Path.GetFileNameWithoutExtension(f))
.Where(n => !string.IsNullOrEmpty(n))
.OrderBy(n => n, StringComparer.OrdinalIgnoreCase)
.ToList();
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"列举配置文件失败: {ex.Message}", LogHelper.LogType.Error);
return Array.Empty<string>();
}
}
/// <summary>获取某配置文件对应的文件路径。</summary>
public static string GetProfilePath(string profileName)
{
var safe = ToSafeFileName(profileName);
return Path.Combine(ProfilesDir, safe + ProfileExtension);
}
/// <summary>将当前配置的 JSON 内容保存为指定名称的配置文件。</summary>
/// <param name="profileName">配置文件显示名称(会转为安全文件名)。</param>
/// <param name="settingsJson">已序列化好的 Settings JSON 字符串。</param>
/// <returns>成功返回 true。</returns>
public static bool SaveAsProfile(string profileName, string settingsJson)
{
try
{
if (string.IsNullOrWhiteSpace(settingsJson))
{
LogHelper.WriteLogToFile("配置文件保存失败:内容为空", LogHelper.LogType.Warning);
return false;
}
EnsureProfilesDirectory();
var path = GetProfilePath(profileName);
ProcessProtectionManager.WithWriteAccess(path, () => File.WriteAllText(path, settingsJson));
LogHelper.WriteLogToFile($"配置文件已保存: {ToSafeFileName(profileName)}", LogHelper.LogType.Event);
return true;
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"保存配置文件失败: {ex.Message}", LogHelper.LogType.Error);
return false;
}
}
/// <summary>将指定配置文件应用到当前配置(覆盖 Configs/Settings.json),供主窗口随后热重载。</summary>
/// <param name="profileName">配置文件名称(与 ListProfileNames 中一致,或与保存时使用的显示名一致)。</param>
/// <returns>成功返回 true;文件不存在或复制失败返回 false。</returns>
public static bool ApplyProfile(string profileName)
{
try
{
var path = GetProfilePath(profileName);
if (!File.Exists(path))
{
LogHelper.WriteLogToFile($"配置文件文件不存在: {path}", LogHelper.LogType.Warning);
return false;
}
var json = File.ReadAllText(path);
if (string.IsNullOrWhiteSpace(json))
{
LogHelper.WriteLogToFile("配置文件内容为空", LogHelper.LogType.Warning);
return false;
}
// 可选:校验是否为合法 Settings JSON
try
{
JsonConvert.DeserializeObject<Settings>(json);
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"配置文件格式无效: {ex.Message}", LogHelper.LogType.Error);
return false;
}
var configsDir = Path.GetDirectoryName(SettingsFilePath);
if (!string.IsNullOrEmpty(configsDir) && !Directory.Exists(configsDir))
{
ProcessProtectionManager.WithWriteAccess(configsDir, () => Directory.CreateDirectory(configsDir));
}
ProcessProtectionManager.WithWriteAccess(SettingsFilePath, () => File.WriteAllText(SettingsFilePath, json));
LogHelper.WriteLogToFile($"已应用配置文件: {profileName}(请热重载以生效)", LogHelper.LogType.Event);
return true;
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"应用配置文件失败: {ex.Message}", LogHelper.LogType.Error);
return false;
}
}
/// <summary>删除指定名称的配置文件。</summary>
public static bool DeleteProfile(string profileName)
{
try
{
var path = GetProfilePath(profileName);
if (!File.Exists(path)) return true;
ProcessProtectionManager.WithWriteAccess(path, () => File.Delete(path));
LogHelper.WriteLogToFile($"已删除配置文件: {profileName}", LogHelper.LogType.Event);
return true;
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"删除配置文件失败: {ex.Message}", LogHelper.LogType.Error);
return false;
}
}
}
}
+64
View File
@@ -2,6 +2,7 @@
using System.Globalization;
using System.Windows;
using System.Windows.Data;
using System.Windows.Media;
namespace Ink_Canvas.Converter
{
@@ -112,4 +113,67 @@ namespace Ink_Canvas.Converter
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { throw new NotImplementedException(); }
}
public class InverseBooleanToVisibilityConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if ((bool)value)
{
return Visibility.Collapsed;
}
return Visibility.Visible;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
if ((bool)value)
{
return Visibility.Collapsed;
}
return Visibility.Visible;
}
}
public class RippleEffectTranslationConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is double d)
{
return -d / 2;
}
return 0.0;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
return null;
}
}
public class StringToGeometryConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
try
{
if (value is string geometryString && !string.IsNullOrEmpty(geometryString))
{
return Geometry.Parse(geometryString);
}
}
catch (Exception)
{
}
return null;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}
+96
View File
@@ -0,0 +1,96 @@
using System;
using System.Runtime.InteropServices;
using System.Text;
namespace Ink_Canvas.Helpers
{
public static class DebugConsoleManager
{
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool AllocConsole();
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool FreeConsole();
[DllImport("kernel32.dll", SetLastError = true)]
private static extern IntPtr GetConsoleWindow();
[DllImport("user32.dll")]
private static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
[DllImport("user32.dll")]
private static extern IntPtr GetSystemMenu(IntPtr hWnd, bool bRevert);
[DllImport("user32.dll")]
private static extern bool DeleteMenu(IntPtr hMenu, uint uPosition, uint uFlags);
[DllImport("kernel32.dll")]
private static extern bool SetConsoleTitle(string lpConsoleTitle);
private const int SW_HIDE = 0;
private const int SW_SHOW = 5;
private const uint SC_CLOSE = 0xF060;
private const uint MF_BYCOMMAND = 0x00000000;
private static bool _allocated;
public static bool IsVisible { get; private set; }
public static void Show()
{
try
{
if (!_allocated)
{
if (GetConsoleWindow() == IntPtr.Zero)
{
if (!AllocConsole()) return;
}
_allocated = true;
Console.OutputEncoding = Encoding.UTF8;
SetConsoleTitle("InkCanvasForClass - Debug Console");
// 移除关闭菜单,避免用户点 X 时直接结束进程
var hWnd = GetConsoleWindow();
if (hWnd != IntPtr.Zero)
{
var hMenu = GetSystemMenu(hWnd, false);
if (hMenu != IntPtr.Zero) DeleteMenu(hMenu, SC_CLOSE, MF_BYCOMMAND);
}
}
else
{
var hWnd = GetConsoleWindow();
if (hWnd != IntPtr.Zero) ShowWindow(hWnd, SW_SHOW);
}
IsVisible = true;
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"[DebugConsoleManager] Show failed: {ex.Message}");
}
}
public static void Hide()
{
try
{
var hWnd = GetConsoleWindow();
if (hWnd != IntPtr.Zero) ShowWindow(hWnd, SW_HIDE);
IsVisible = false;
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"[DebugConsoleManager] Hide failed: {ex.Message}");
}
}
public static void WriteLine(string line)
{
if (!IsVisible) return;
try { Console.WriteLine(line); }
catch { }
}
}
}
+360 -171
View File
@@ -1,9 +1,11 @@
using Newtonsoft.Json;
using OSVersionExtension;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Text;
@@ -17,10 +19,13 @@ namespace Ink_Canvas.Helpers
// 文件路径策略
private static readonly string DeviceIdFilePath = Path.Combine(App.RootPath, "device_id.dat");
private static readonly string UsageStatsFilePath = Path.Combine(App.RootPath, "usage_stats.enc");
private static readonly string UsageStatsBackupPath = Path.Combine(App.RootPath, "saves", "usage_stats_backup.enc");
private static readonly string UsageStatsBackupPath = Path.Combine(App.RootPath, "Saves", "usage_stats_backup.enc");
private static readonly string DeviceId;
private static readonly object fileLock = new object();
private static UsageStats usageStatsCache;
private static DateTime usageStatsCacheTime;
private static readonly TimeSpan UsageStatsCacheDuration = TimeSpan.FromMinutes(2);
static DeviceIdentifier()
{
@@ -46,20 +51,40 @@ namespace Ink_Canvas.Helpers
{
try
{
// 1. 尝试从主文件读取设备ID
string deviceId = LoadDeviceIdFromFile(DeviceIdFilePath);
if (!string.IsNullOrEmpty(deviceId))
// 计算当前设备的硬件指纹
string currentHardwareFingerprint = GenerateHardwareFingerprint();
// 1. 尝试从主文件读取设备ID及其硬件指纹
var storedInfo = LoadDeviceIdFromFile(DeviceIdFilePath);
if (storedInfo != null && !string.IsNullOrEmpty(storedInfo.DeviceId) && IsValidDeviceId(storedInfo.DeviceId))
{
LogHelper.WriteLogToFile($"DeviceIdentifier | 从主文件读取设备ID: {deviceId}");
return deviceId;
if (!string.IsNullOrEmpty(storedInfo.HardwareFingerprint))
{
if (storedInfo.HardwareFingerprint == currentHardwareFingerprint)
{
LogHelper.WriteLogToFile($"DeviceIdentifier | 从主文件读取设备ID且硬件信息一致: {storedInfo.DeviceId}");
return storedInfo.DeviceId;
}
LogHelper.WriteLogToFile("DeviceIdentifier | 检测到当前硬件信息与保存的设备ID不一致,将重新生成设备ID");
}
else
{
LogHelper.WriteLogToFile("DeviceIdentifier | 检测到旧格式设备ID文件(无硬件信息),将基于当前硬件重新生成设备ID并升级文件格式");
}
}
// 2. 生成新的设备ID
string newDeviceId = GenerateDeviceId();
// 2. 基于当前硬件指纹生成新的设备ID
string newDeviceId = GenerateDeviceIdFromFingerprint(currentHardwareFingerprint);
LogHelper.WriteLogToFile($"DeviceIdentifier | 生成新设备ID: {newDeviceId}");
// 3. 保存到主文件
SaveDeviceIdToFile(DeviceIdFilePath, newDeviceId);
// 3. 保存到主文件(包含硬件指纹)
var newInfo = new DeviceIdInfo
{
DeviceId = newDeviceId,
HardwareFingerprint = currentHardwareFingerprint
};
SaveDeviceIdToFile(DeviceIdFilePath, newInfo);
return newDeviceId;
}
@@ -79,143 +104,9 @@ namespace Ink_Canvas.Helpers
{
try
{
// 收集硬件信息
var hardwareInfo = new StringBuilder();
// 使用反射获取硬件信息,避免直接引用System.Management
try
{
// 尝试加载System.Management程序集
var assembly = Assembly.Load("System.Management");
if (assembly != null)
{
// CPU信息
try
{
var searcherType = assembly.GetType("System.Management.ManagementObjectSearcher");
var searcher = Activator.CreateInstance(searcherType, "SELECT ProcessorId FROM Win32_Processor");
var getMethod = searcherType.GetMethod("Get");
var enumerator = getMethod.Invoke(searcher, null);
var moveNextMethod = enumerator.GetType().GetMethod("MoveNext");
var currentProperty = enumerator.GetType().GetProperty("Current");
if ((bool)moveNextMethod.Invoke(enumerator, null))
{
var obj = currentProperty.GetValue(enumerator);
var indexer = obj.GetType().GetProperty("Item", new[] { typeof(string) });
var processorId = indexer.GetValue(obj, new object[] { "ProcessorId" });
hardwareInfo.Append(processorId?.ToString() ?? "");
}
var disposeMethod = searcher.GetType().GetMethod("Dispose");
disposeMethod?.Invoke(searcher, null);
}
catch { }
// 主板序列号
try
{
var searcherType = assembly.GetType("System.Management.ManagementObjectSearcher");
var searcher = Activator.CreateInstance(searcherType, "SELECT SerialNumber FROM Win32_BaseBoard");
var getMethod = searcherType.GetMethod("Get");
var enumerator = getMethod.Invoke(searcher, null);
var moveNextMethod = enumerator.GetType().GetMethod("MoveNext");
var currentProperty = enumerator.GetType().GetProperty("Current");
if ((bool)moveNextMethod.Invoke(enumerator, null))
{
var obj = currentProperty.GetValue(enumerator);
var indexer = obj.GetType().GetProperty("Item", new[] { typeof(string) });
var serialNumber = indexer.GetValue(obj, new object[] { "SerialNumber" });
hardwareInfo.Append(serialNumber?.ToString() ?? "");
}
var disposeMethod = searcher.GetType().GetMethod("Dispose");
disposeMethod?.Invoke(searcher, null);
}
catch { }
// BIOS序列号
try
{
var searcherType = assembly.GetType("System.Management.ManagementObjectSearcher");
var searcher = Activator.CreateInstance(searcherType, "SELECT SerialNumber FROM Win32_BIOS");
var getMethod = searcherType.GetMethod("Get");
var enumerator = getMethod.Invoke(searcher, null);
var moveNextMethod = enumerator.GetType().GetMethod("MoveNext");
var currentProperty = enumerator.GetType().GetProperty("Current");
if ((bool)moveNextMethod.Invoke(enumerator, null))
{
var obj = currentProperty.GetValue(enumerator);
var indexer = obj.GetType().GetProperty("Item", new[] { typeof(string) });
var serialNumber = indexer.GetValue(obj, new object[] { "SerialNumber" });
hardwareInfo.Append(serialNumber?.ToString() ?? "");
}
var disposeMethod = searcher.GetType().GetMethod("Dispose");
disposeMethod?.Invoke(searcher, null);
}
catch { }
// 主硬盘序列号
try
{
var searcherType = assembly.GetType("System.Management.ManagementObjectSearcher");
var searcher = Activator.CreateInstance(searcherType, "SELECT SerialNumber FROM Win32_DiskDrive WHERE MediaType='Fixed hard disk media'");
var getMethod = searcherType.GetMethod("Get");
var enumerator = getMethod.Invoke(searcher, null);
var moveNextMethod = enumerator.GetType().GetMethod("MoveNext");
var currentProperty = enumerator.GetType().GetProperty("Current");
if ((bool)moveNextMethod.Invoke(enumerator, null))
{
var obj = currentProperty.GetValue(enumerator);
var indexer = obj.GetType().GetProperty("Item", new[] { typeof(string) });
var serialNumber = indexer.GetValue(obj, new object[] { "SerialNumber" });
hardwareInfo.Append(serialNumber?.ToString() ?? "");
}
var disposeMethod = searcher.GetType().GetMethod("Dispose");
disposeMethod?.Invoke(searcher, null);
}
catch { }
}
}
catch { }
// 如果硬件信息不足,添加系统信息
if (hardwareInfo.Length < 10)
{
hardwareInfo.Append(Environment.MachineName);
hardwareInfo.Append(Environment.UserName);
hardwareInfo.Append(Environment.OSVersion);
}
// 生成哈希
using (var sha256 = SHA256.Create())
{
byte[] hashBytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(hardwareInfo.ToString()));
string hashString = BitConverter.ToString(hashBytes).Replace("-", "");
// 取前25个字符,确保唯一性
string deviceId = hashString.Substring(0, 25);
// 添加校验位(第25位)
int checksum = 0;
for (int i = 0; i < 24; i++)
{
checksum += Convert.ToInt32(deviceId[i]);
}
checksum %= 36; // 0-9, A-Z
char checksumChar = checksum < 10 ? (char)(checksum + '0') : (char)(checksum - 10 + 'A');
return deviceId.Substring(0, 24) + checksumChar;
}
// 基于当前硬件指纹生成设备ID
string hardwareFingerprint = GenerateHardwareFingerprint();
return GenerateDeviceIdFromFingerprint(hardwareFingerprint);
}
catch (Exception ex)
{
@@ -224,6 +115,171 @@ namespace Ink_Canvas.Helpers
}
}
/// <summary>
/// 生成当前设备的硬件指纹字符串(用于生成和校验设备ID)
/// </summary>
private static string GenerateHardwareFingerprint()
{
var hardwareInfo = new StringBuilder();
AppendFingerprintPart(hardwareInfo, "CPU",
GetWmiProperty("SELECT ProcessorId FROM Win32_Processor", "ProcessorId"),
GetRegistryValue(@"HARDWARE\DESCRIPTION\System\CentralProcessor\0", "ProcessorNameString"),
GetRegistryValue(@"HARDWARE\DESCRIPTION\System\CentralProcessor\0", "Identifier"));
AppendFingerprintPart(hardwareInfo, "BOARD",
GetWmiProperty("SELECT SerialNumber FROM Win32_BaseBoard", "SerialNumber"),
GetRegistryValue(@"HARDWARE\DESCRIPTION\System\BIOS", "BaseBoardSerialNumber"),
GetRegistryValue(@"HARDWARE\DESCRIPTION\System\BIOS", "BaseBoardProduct"));
AppendFingerprintPart(hardwareInfo, "BIOS",
GetWmiProperty("SELECT SerialNumber FROM Win32_BIOS", "SerialNumber"),
GetRegistryValue(@"HARDWARE\DESCRIPTION\System\BIOS", "BIOSVersion"),
GetRegistryValue(@"HARDWARE\DESCRIPTION\System\BIOS", "BIOSVendor"));
AppendFingerprintPart(hardwareInfo, "DISK",
GetWmiProperty("SELECT SerialNumber FROM Win32_DiskDrive WHERE MediaType='Fixed hard disk media'", "SerialNumber"),
GetSystemDriveVolumeSerial(),
GetRegistryValue(@"SOFTWARE\Microsoft\Cryptography", "MachineGuid"));
if (hardwareInfo.Length < 10)
{
hardwareInfo.Append(Environment.MachineName);
hardwareInfo.Append(Environment.UserName);
hardwareInfo.Append(Environment.OSVersion);
}
return hardwareInfo.ToString();
}
private static void AppendFingerprintPart(StringBuilder hardwareInfo, string key, params string[] candidates)
{
foreach (var candidate in candidates)
{
if (!string.IsNullOrWhiteSpace(candidate))
{
hardwareInfo.Append(key).Append(':').Append(candidate.Trim()).Append(';');
return;
}
}
}
private static string GetWmiProperty(string query, string propertyName)
{
try
{
var assembly = Assembly.Load("System.Management");
var searcherType = assembly?.GetType("System.Management.ManagementObjectSearcher");
if (searcherType == null)
{
return null;
}
var searcher = Activator.CreateInstance(searcherType, query);
var getMethod = searcherType.GetMethod("Get");
var resultCollection = getMethod?.Invoke(searcher, null);
if (resultCollection == null)
{
return null;
}
var enumerator = resultCollection.GetType().GetMethod("GetEnumerator")?.Invoke(resultCollection, null);
var moveNextMethod = enumerator?.GetType().GetMethod("MoveNext");
var currentProperty = enumerator?.GetType().GetProperty("Current");
if (enumerator == null || moveNextMethod == null || currentProperty == null)
{
return null;
}
if (!(bool)moveNextMethod.Invoke(enumerator, null))
{
return null;
}
var currentObject = currentProperty.GetValue(enumerator);
var indexer = currentObject?.GetType().GetProperty("Item", new[] { typeof(string) });
var result = indexer?.GetValue(currentObject, new object[] { propertyName })?.ToString();
searcher?.GetType().GetMethod("Dispose")?.Invoke(searcher, null);
return result;
}
catch
{
return null;
}
}
private static string GetRegistryValue(string subKey, string valueName)
{
try
{
return Microsoft.Win32.Registry.GetValue($@"HKEY_LOCAL_MACHINE\{subKey}", valueName, null)?.ToString();
}
catch
{
return null;
}
}
private static string GetSystemDriveVolumeSerial()
{
try
{
var rootPath = Path.GetPathRoot(Environment.SystemDirectory);
if (string.IsNullOrWhiteSpace(rootPath))
{
return null;
}
if (GetVolumeInformation(rootPath, null, 0, out uint serialNumber, out _, out _, null, 0))
{
return serialNumber.ToString("X8");
}
}
catch
{
}
return null;
}
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
private static extern bool GetVolumeInformation(
string rootPathName,
StringBuilder volumeNameBuffer,
uint volumeNameSize,
out uint volumeSerialNumber,
out uint maximumComponentLength,
out uint fileSystemFlags,
StringBuilder fileSystemNameBuffer,
uint nFileSystemNameSize);
/// <summary>
/// 基于硬件指纹生成25字符的设备ID
/// </summary>
private static string GenerateDeviceIdFromFingerprint(string hardwareFingerprint)
{
// 生成哈希
using (var sha256 = SHA256.Create())
{
byte[] hashBytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(hardwareFingerprint ?? string.Empty));
string hashString = BitConverter.ToString(hashBytes).Replace("-", "");
// 取前25个字符,确保唯一性
string deviceId = hashString.Substring(0, 25);
// 添加校验位(第25位)
int checksum = 0;
for (int i = 0; i < 24; i++)
{
checksum += Convert.ToInt32(deviceId[i]);
}
checksum %= 36; // 0-9, A-Z
char checksumChar = checksum < 10 ? (char)(checksum + '0') : (char)(checksum - 10 + 'A');
return deviceId.Substring(0, 24) + checksumChar;
}
}
/// <summary>
/// 生成备用设备ID(基于时间戳)
/// </summary>
@@ -282,16 +338,33 @@ namespace Ink_Canvas.Helpers
/// <summary>
/// 从文件加载设备ID
/// </summary>
private static string LoadDeviceIdFromFile(string filePath)
private static DeviceIdInfo LoadDeviceIdFromFile(string filePath)
{
try
{
if (File.Exists(filePath))
{
string content = File.ReadAllText(filePath).Trim();
try
{
var info = JsonConvert.DeserializeObject<DeviceIdInfo>(content);
if (info != null && !string.IsNullOrEmpty(info.DeviceId) && IsValidDeviceId(info.DeviceId))
{
return info;
}
}
catch
{
}
if (IsValidDeviceId(content))
{
return content;
return new DeviceIdInfo
{
DeviceId = content,
HardwareFingerprint = null
};
}
}
}
@@ -305,7 +378,12 @@ namespace Ink_Canvas.Helpers
/// <summary>
/// 保存设备ID到文件
/// </summary>
private static void SaveDeviceIdToFile(string filePath, string deviceId)
/// <remarks>
/// 将设备标识信息以格式化的 JSON 写入指定文件,并确保目标目录存在;在失败时记录错误但不抛出异常。
/// </remarks>
/// <param name="filePath">目标文件的完整路径,用于保存设备标识信息。</param>
/// <param name="info">要保存的设备标识信息对象(包含 DeviceId 和 可选的硬件指纹)。</param>
private static void SaveDeviceIdToFile(string filePath, DeviceIdInfo info)
{
try
{
@@ -313,10 +391,11 @@ namespace Ink_Canvas.Helpers
var directory = Path.GetDirectoryName(filePath);
if (!Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
ProcessProtectionManager.WithWriteAccess(directory, () => Directory.CreateDirectory(directory));
}
File.WriteAllText(filePath, deviceId);
string json = JsonConvert.SerializeObject(info, Formatting.Indented);
ProcessProtectionManager.WithWriteAccess(filePath, () => File.WriteAllText(filePath, json));
LogHelper.WriteLogToFile($"DeviceIdentifier | 设备ID已保存到: {filePath}");
}
@@ -326,6 +405,14 @@ namespace Ink_Canvas.Helpers
}
}
private class DeviceIdInfo
{
[JsonProperty("deviceId")]
public string DeviceId { get; set; }
[JsonProperty("hardwareFingerprint")]
public string HardwareFingerprint { get; set; }
}
/// <summary>
@@ -342,7 +429,9 @@ namespace Ink_Canvas.Helpers
[JsonProperty("launchCount")]
public int LaunchCount { get; set; }
// 新的秒级精度字段
[JsonProperty("systemVersion")]
public string SystemVersion { get; set; }
[JsonProperty("totalUsageSeconds")]
public long TotalUsageSeconds { get; set; }
@@ -363,6 +452,9 @@ namespace Ink_Canvas.Helpers
[JsonProperty("lastModified")]
public DateTime LastModified { get; set; }
[JsonProperty("updateChannel")]
public Ink_Canvas.UpdateChannel UpdateChannel { get; set; } = Ink_Canvas.UpdateChannel.Release;
// 每周统计数据(秒级精度)
[JsonProperty("weeklyLaunchCount")]
public int WeeklyLaunchCount { get; set; }
@@ -501,6 +593,20 @@ namespace Ink_Canvas.Helpers
// 记录每周启动次数
stats.RecordWeeklyLaunch();
try
{
var osName = OSVersion.GetOperatingSystem();
var osVersion = OSVersion.GetOSVersion();
string versionText = osVersion != null
? $"{osName} {osVersion.Version}"
: osName.ToString();
stats.SystemVersion = versionText;
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"DeviceIdentifier | 刷新系统版本信息失败: {ex.Message}", LogHelper.LogType.Warning);
}
// 计算使用频率
CalculateUsageFrequency(stats);
@@ -536,8 +642,6 @@ namespace Ink_Canvas.Helpers
// 更新秒级精度数据
stats.TotalUsageSeconds += sessionSeconds;
// 记录每周使用时长(秒级精度)
stats.RecordWeeklyUsage(sessionSeconds);
@@ -545,7 +649,6 @@ namespace Ink_Canvas.Helpers
if (stats.LaunchCount > 0)
{
stats.AverageSessionSeconds = (double)stats.TotalUsageSeconds / stats.LaunchCount;
}
}
@@ -565,6 +668,20 @@ namespace Ink_Canvas.Helpers
}
}
public static string GetSystemVersion()
{
try
{
var stats = GetUsageStatsCached();
return stats.SystemVersion;
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"DeviceIdentifier | 获取系统版本失败: {ex.Message}", LogHelper.LogType.Error);
return null;
}
}
/// <summary>
/// 计算使用频率和更新优先级(基于真实的每周统计数据)
/// 通过多维度评分系统确定用户类型:高频(≥80分)、中频(40-79分)、低频(<40分)
@@ -674,7 +791,7 @@ namespace Ink_Canvas.Helpers
{
try
{
var stats = LoadUsageStats();
var stats = GetUsageStatsCached();
return stats.UpdatePriority;
}
catch (Exception ex)
@@ -691,7 +808,7 @@ namespace Ink_Canvas.Helpers
{
try
{
var stats = LoadUsageStats();
var stats = GetUsageStatsCached();
return stats.UsageFrequency;
}
catch (Exception ex)
@@ -793,6 +910,23 @@ namespace Ink_Canvas.Helpers
}
}
private static UsageStats GetUsageStatsCached(bool forceRefresh = false)
{
lock (fileLock)
{
if (!forceRefresh
&& usageStatsCache != null
&& (DateTime.Now - usageStatsCacheTime) < UsageStatsCacheDuration)
{
return usageStatsCache;
}
usageStatsCache = LoadUsageStats();
usageStatsCacheTime = DateTime.Now;
return usageStatsCache;
}
}
/// <summary>
/// 保存使用统计
/// </summary>
@@ -803,6 +937,9 @@ namespace Ink_Canvas.Helpers
// 保存到备份文件
SaveUsageStatsToFile(UsageStatsBackupPath, stats);
usageStatsCache = stats;
usageStatsCacheTime = DateTime.Now;
}
@@ -905,7 +1042,7 @@ namespace Ink_Canvas.Helpers
var directory = Path.GetDirectoryName(filePath);
if (!Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
ProcessProtectionManager.WithWriteAccess(directory, () => Directory.CreateDirectory(directory));
}
string json = JsonConvert.SerializeObject(stats, Formatting.Indented);
@@ -929,7 +1066,7 @@ namespace Ink_Canvas.Helpers
checksum.CopyTo(finalData, 0);
encryptedData.CopyTo(finalData, checksum.Length);
File.WriteAllBytes(filePath, finalData);
ProcessProtectionManager.WithWriteAccess(filePath, () => File.WriteAllBytes(filePath, finalData));
LogHelper.WriteLogToFile($"DeviceIdentifier | 加密使用统计已保存到: {filePath}");
}
@@ -940,6 +1077,40 @@ namespace Ink_Canvas.Helpers
}
}
public static void UpdateUsageChannel(Ink_Canvas.UpdateChannel channel)
{
try
{
lock (fileLock)
{
var stats = LoadUsageStats();
if (stats == null)
{
stats = new UsageStats
{
DeviceId = DeviceId,
LastLaunchTime = DateTime.Now,
LaunchCount = 0,
TotalUsageSeconds = 0,
AverageSessionSeconds = 0,
LastUpdateCheck = DateTime.MinValue,
UpdatePriority = UpdatePriority.Medium,
UsageFrequency = UsageFrequency.Medium
};
}
stats.UpdateChannel = channel;
SaveUsageStats(stats);
LogHelper.WriteLogToFile($"DeviceIdentifier | 更新使用统计中的通道信息: {channel}");
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"DeviceIdentifier | 更新通道信息到使用统计失败: {ex.Message}", LogHelper.LogType.Error);
}
}
/// <summary>
/// 记录更新检查时间
/// </summary>
@@ -960,6 +1131,23 @@ namespace Ink_Canvas.Helpers
}
}
/// <summary>
/// 获取上次更新检查时间
/// </summary>
public static DateTime GetLastUpdateCheck()
{
try
{
var stats = LoadUsageStats();
return stats.LastUpdateCheck;
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"DeviceIdentifier | 获取上次更新检查时间失败: {ex.Message}", LogHelper.LogType.Error);
return DateTime.MinValue;
}
}
/// <summary>
/// 从备份文件恢复使用统计数据
@@ -1065,7 +1253,6 @@ namespace Ink_Canvas.Helpers
// 如果不是自动更新(即版本修复),则应用不同的策略
if (!isAutoUpdate)
{
// 版本修复:立即允许,不受分级策略影响
LogHelper.WriteLogToFile($"DeviceIdentifier | 版本修复 - 版本: {updateVersion}, 类型: {updateType}, 结果: 允许");
return true;
}
@@ -1092,18 +1279,21 @@ namespace Ink_Canvas.Helpers
{
int versionDiff = CalculateVersionGenerationDifference(localVersion, updateVersion);
LogHelper.WriteLogToFile($"DeviceIdentifier | 无法获取版本发布时间,使用版本号差异判断 - 本地版本: {localVersion}, 远程版本: {updateVersion}, 代数差异: {versionDiff}");
// 当版本号代数差异大于3时自动更新
if (versionDiff > 3)
if (versionDiff <= 0)
{
LogHelper.WriteLogToFile($"DeviceIdentifier | 版本号代数差异({versionDiff})大于3,自动更新");
return true;
}
else
{
LogHelper.WriteLogToFile($"DeviceIdentifier | 版本号代数差异({versionDiff})不大于3,暂不更新");
LogHelper.WriteLogToFile($"DeviceIdentifier | 版本号代数差异({versionDiff})<=0,可能是相同版本或降级,暂不更新");
return false;
}
// 当代数差异较大(>=3)时直接放行,避免被分级策略卡住
if (versionDiff >= 3)
{
LogHelper.WriteLogToFile($"DeviceIdentifier | 版本号代数差异({versionDiff})>=3,跳过分级策略直接推送");
return true;
}
LogHelper.WriteLogToFile($"DeviceIdentifier | 版本号代数差异({versionDiff})>=1,进入分级策略判断");
}
// 计算最近活跃度(最后一次使用距今的天数)
@@ -1477,4 +1667,3 @@ namespace Ink_Canvas.Helpers
}
}
}
+484
View File
@@ -0,0 +1,484 @@
using Newtonsoft.Json;
using System;
using System.IO;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace Ink_Canvas.Helpers
{
/// <summary>
/// Dlass API 客户端,用于与服务端通信
/// </summary>
public class DlassApiClient : IDisposable
{
private const string DEFAULT_BASE_URL = "https://dlass.tech";
private readonly string _appId;
private readonly string _appSecret;
private readonly string _baseUrl;
private HttpClient _httpClient;
private string _accessToken;
private DateTime _tokenExpiresAt;
private string _userToken;
/// <summary>
/// 初始化 Dlass API 客户端
/// </summary>
/// <param name="appId">应用ID</param>
/// <param name="appSecret">应用密钥</param>
/// <param name="baseUrl">API基础URL,如果为空则使用默认URL</param>
/// <param name="userToken">用户Token,如果提供则优先使用用户token而不是App Secret</param>
public DlassApiClient(string appId, string appSecret, string baseUrl = null, string userToken = null)
{
_appId = appId ?? throw new ArgumentNullException(nameof(appId));
_appSecret = appSecret ?? throw new ArgumentNullException(nameof(appSecret));
_userToken = userToken;
_baseUrl = baseUrl ?? DEFAULT_BASE_URL;
_baseUrl = _baseUrl.TrimEnd('/');
if (!_baseUrl.StartsWith("http://") && !_baseUrl.StartsWith("https://"))
{
_baseUrl = "https://" + _baseUrl;
}
_httpClient = new HttpClient
{
BaseAddress = new Uri(_baseUrl),
Timeout = TimeSpan.FromSeconds(30)
};
_httpClient.DefaultRequestHeaders.Add("User-Agent", "InkCanvas/1.0");
}
/// <summary>
/// 获取访问令牌(Access Token
/// </summary>
/// <param name="cancellationToken">取消令牌</param>
public async Task<string> GetAccessTokenAsync(CancellationToken cancellationToken = default)
{
if (!string.IsNullOrEmpty(_userToken))
{
return _userToken;
}
if (!string.IsNullOrEmpty(_accessToken) && DateTime.Now < _tokenExpiresAt.AddMinutes(-5))
{
return _accessToken;
}
try
{
cancellationToken.ThrowIfCancellationRequested();
var requestData = new
{
app_id = _appId,
app_secret = _appSecret,
grant_type = "client_credentials"
};
var json = JsonConvert.SerializeObject(requestData);
var content = new StringContent(json, Encoding.UTF8, "application/json");
var response = await _httpClient.PostAsync("/oauth/token", content, cancellationToken);
var responseContent = await response.Content.ReadAsStringAsync();
if (response.IsSuccessStatusCode)
{
var tokenResponse = JsonConvert.DeserializeObject<TokenResponse>(responseContent);
_accessToken = tokenResponse.AccessToken;
_tokenExpiresAt = DateTime.Now.AddSeconds(tokenResponse.ExpiresIn ?? 3600);
return _accessToken;
}
else
{
throw new Exception($"获取Access Token失败: {response.StatusCode}");
}
}
catch (OperationCanceledException)
{
throw;
}
catch (HttpRequestException httpEx)
{
throw new Exception($"获取Access Token时网络错误: {httpEx.Message}", httpEx);
}
catch (Exception ex)
{
throw new Exception($"获取Access Token时出错: {ex.Message}", ex);
}
}
/// <summary>
/// 发送GET请求
/// </summary>
/// <param name="endpoint">API端点</param>
/// <param name="requireAuth">是否需要认证</param>
/// <param name="cancellationToken">取消令牌</param>
public async Task<T> GetAsync<T>(string endpoint, bool requireAuth = true, CancellationToken cancellationToken = default)
{
try
{
cancellationToken.ThrowIfCancellationRequested();
string token = null;
if (requireAuth)
{
token = await GetAccessTokenAsync(cancellationToken);
}
var request = new HttpRequestMessage(HttpMethod.Get, endpoint);
if (requireAuth && !string.IsNullOrEmpty(token))
{
if (!string.IsNullOrEmpty(_userToken))
{
request.Headers.Add("X-User-Token", token);
}
else
{
request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
}
}
var response = await _httpClient.SendAsync(request, cancellationToken);
var content = await response.Content.ReadAsStringAsync();
if (response.IsSuccessStatusCode)
{
if (string.IsNullOrEmpty(content))
{
return default(T);
}
return JsonConvert.DeserializeObject<T>(content);
}
else
{
throw new Exception($"API请求失败: {response.StatusCode} - {content}");
}
}
catch (OperationCanceledException)
{
throw;
}
catch (HttpRequestException httpEx)
{
throw new Exception($"发送请求时出错: {httpEx.Message}", httpEx);
}
catch (Exception ex)
{
throw new Exception($"发送请求时出错: {ex.Message}", ex);
}
}
/// <summary>
/// 发送POST请求
/// </summary>
/// <param name="endpoint">API端点</param>
/// <param name="data">请求数据</param>
/// <param name="requireAuth">是否需要认证</param>
/// <param name="cancellationToken">取消令牌</param>
public async Task<T> PostAsync<T>(string endpoint, object data = null, bool requireAuth = true, CancellationToken cancellationToken = default)
{
try
{
cancellationToken.ThrowIfCancellationRequested();
string token = null;
if (requireAuth)
{
token = await GetAccessTokenAsync(cancellationToken);
}
var request = new HttpRequestMessage(HttpMethod.Post, endpoint);
if (requireAuth && !string.IsNullOrEmpty(token))
{
if (!string.IsNullOrEmpty(_userToken))
{
request.Headers.Add("X-User-Token", token);
}
else
{
request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
}
}
if (data != null)
{
var json = JsonConvert.SerializeObject(data);
request.Content = new StringContent(json, Encoding.UTF8, "application/json");
}
var response = await _httpClient.SendAsync(request, cancellationToken);
var content = await response.Content.ReadAsStringAsync();
if (response.IsSuccessStatusCode)
{
if (string.IsNullOrEmpty(content))
{
return default(T);
}
return JsonConvert.DeserializeObject<T>(content);
}
else
{
throw new Exception($"API请求失败: {response.StatusCode} - {content}");
}
}
catch (OperationCanceledException)
{
throw;
}
catch (HttpRequestException httpEx)
{
throw new Exception($"发送请求时出错: {httpEx.Message}", httpEx);
}
catch (Exception ex)
{
throw new Exception($"发送请求时出错: {ex.Message}", ex);
}
}
/// <summary>
/// 发送PUT请求
/// </summary>
/// <param name="endpoint">API端点</param>
/// <param name="data">请求数据</param>
/// <param name="requireAuth">是否需要认证</param>
/// <param name="cancellationToken">取消令牌</param>
public async Task<T> PutAsync<T>(string endpoint, object data = null, bool requireAuth = true, CancellationToken cancellationToken = default)
{
try
{
cancellationToken.ThrowIfCancellationRequested();
string token = null;
if (requireAuth)
{
token = await GetAccessTokenAsync(cancellationToken);
}
var request = new HttpRequestMessage(HttpMethod.Put, endpoint);
if (requireAuth && !string.IsNullOrEmpty(token))
{
// 如果是用户token,使用X-User-Token header
if (!string.IsNullOrEmpty(_userToken))
{
request.Headers.Add("X-User-Token", token);
}
else
{
request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
}
}
if (data != null)
{
var json = JsonConvert.SerializeObject(data);
request.Content = new StringContent(json, Encoding.UTF8, "application/json");
}
var response = await _httpClient.SendAsync(request, cancellationToken);
var content = await response.Content.ReadAsStringAsync();
if (response.IsSuccessStatusCode)
{
if (string.IsNullOrEmpty(content))
{
return default(T);
}
return JsonConvert.DeserializeObject<T>(content);
}
else
{
throw new Exception($"API请求失败: {response.StatusCode} - {content}");
}
}
catch (OperationCanceledException)
{
throw;
}
catch (HttpRequestException httpEx)
{
throw new Exception($"发送请求时出错: {httpEx.Message}", httpEx);
}
catch (Exception ex)
{
throw new Exception($"发送请求时出错: {ex.Message}", ex);
}
}
/// <summary>
/// 发送DELETE请求
/// </summary>
/// <param name="endpoint">API端点</param>
/// <param name="requireAuth">是否需要认证</param>
/// <param name="cancellationToken">取消令牌</param>
public async Task<bool> DeleteAsync(string endpoint, bool requireAuth = true, CancellationToken cancellationToken = default)
{
try
{
cancellationToken.ThrowIfCancellationRequested();
string token = null;
if (requireAuth)
{
token = await GetAccessTokenAsync(cancellationToken);
}
var request = new HttpRequestMessage(HttpMethod.Delete, endpoint);
if (requireAuth && !string.IsNullOrEmpty(token))
{
// 如果是用户token,使用X-User-Token header
if (!string.IsNullOrEmpty(_userToken))
{
request.Headers.Add("X-User-Token", token);
}
else
{
request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
}
}
var response = await _httpClient.SendAsync(request, cancellationToken);
if (response.IsSuccessStatusCode)
{
return true;
}
else
{
return false;
}
}
catch (OperationCanceledException)
{
throw;
}
catch (HttpRequestException)
{
return false;
}
catch (Exception)
{
return false;
}
}
/// <summary>
/// 上传笔记文件
/// </summary>
/// <param name="endpoint">上传端点</param>
/// <param name="filePath">文件路径</param>
/// <param name="boardId">白板ID</param>
/// <param name="secretKey">白板密钥</param>
/// <param name="title">笔记标题(可选)</param>
/// <param name="description">笔记描述(可选)</param>
/// <param name="tags">笔记标签(可选)</param>
/// <param name="cancellationToken">取消令牌</param>
public async Task<T> UploadNoteAsync<T>(string endpoint, string filePath, string boardId, string secretKey, string title = null, string description = null, string tags = null, CancellationToken cancellationToken = default)
{
try
{
cancellationToken.ThrowIfCancellationRequested();
if (!File.Exists(filePath))
{
throw new FileNotFoundException($"文件不存在: {filePath}");
}
var request = new HttpRequestMessage(HttpMethod.Post, endpoint);
// 设置白板认证头
request.Headers.Add("X-Board-ID", boardId);
request.Headers.Add("X-Secret-Key", secretKey);
// 创建multipart/form-data内容
var content = new MultipartFormDataContent();
// 添加文件
var fileContent = new ByteArrayContent(File.ReadAllBytes(filePath));
var fileName = Path.GetFileName(filePath);
fileContent.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
content.Add(fileContent, "file", fileName);
// 添加可选参数
if (!string.IsNullOrEmpty(title))
{
content.Add(new StringContent(title), "title");
}
if (!string.IsNullOrEmpty(description))
{
content.Add(new StringContent(description), "description");
}
if (!string.IsNullOrEmpty(tags))
{
content.Add(new StringContent(tags), "tags");
}
request.Content = content;
var response = await _httpClient.SendAsync(request, cancellationToken);
var responseContent = await response.Content.ReadAsStringAsync();
if (response.IsSuccessStatusCode)
{
if (string.IsNullOrEmpty(responseContent))
{
return default(T);
}
return JsonConvert.DeserializeObject<T>(responseContent);
}
else
{
throw new Exception($"上传文件失败: {response.StatusCode} - {responseContent}");
}
}
catch (OperationCanceledException)
{
throw;
}
catch (HttpRequestException httpEx)
{
throw new Exception($"上传文件时网络错误: {httpEx.Message}", httpEx);
}
catch (Exception ex)
{
throw new Exception($"上传文件时出错: {ex.Message}", ex);
}
}
/// <summary>
/// 释放资源
/// </summary>
public void Dispose()
{
_httpClient?.Dispose();
}
#region
/// <summary>
/// Token响应模型
/// </summary>
private class TokenResponse
{
[JsonProperty("access_token")]
public string AccessToken { get; set; }
[JsonProperty("expires_in")]
public int? ExpiresIn { get; set; }
[JsonProperty("token_type")]
public string TokenType { get; set; }
}
#endregion
}
}
+257
View File
@@ -0,0 +1,257 @@
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace Ink_Canvas.Helpers
{
/// <summary>
/// Dlass上传队列
/// </summary>
public class DlassUploadQueue : BaseUploadQueue
{
private const string APP_ID = "app_WkjocWqsrVY7T6zQV2CfiA";
private const string APP_SECRET = "o7dx5b5ASGUMcM72PCpmRQYAhSijqaOVHoGyBK0IxbA";
/// <summary>
/// 队列文件名
/// </summary>
protected override string QueueFileName => "DlassUploadQueue.json";
/// <summary>
/// 上传笔记响应模型
/// </summary>
public class UploadNoteResponse
{
[JsonProperty("success")]
public bool Success { get; set; }
[JsonProperty("message")]
public string Message { get; set; }
[JsonProperty("note_id")]
public int? NoteId { get; set; }
[JsonProperty("filename")]
public string Filename { get; set; }
[JsonProperty("file_path")]
public string FilePath { get; set; }
[JsonProperty("file_url")]
public string FileUrl { get; set; }
}
/// <summary>
/// 白板信息模型(用于查找白板)
/// </summary>
private class WhiteboardInfo
{
[JsonProperty("id")]
public int Id { get; set; }
[JsonProperty("name")]
public string Name { get; set; }
[JsonProperty("board_id")]
public string BoardId { get; set; }
[JsonProperty("secret_key")]
public string SecretKey { get; set; }
[JsonProperty("class_name")]
public string ClassName { get; set; }
[JsonProperty("class_id")]
public int ClassId { get; set; }
}
/// <summary>
/// 认证响应模型
/// </summary>
private class AuthWithTokenResponse
{
[JsonProperty("success")]
public bool Success { get; set; }
[JsonProperty("whiteboards")]
public List<WhiteboardInfo> Whiteboards { get; set; }
}
/// <summary>
/// 检查上传是否启用
/// </summary>
protected override bool IsUploadEnabled()
{
return MainWindow.Settings?.Dlass?.IsAutoUploadNotes == true;
}
/// <summary>
/// 内部上传方法,执行实际上传操作
/// </summary>
protected override async Task<bool> UploadFileInternalAsync(string filePath, CancellationToken cancellationToken)
{
try
{
cancellationToken.ThrowIfCancellationRequested();
// 再次检查文件是否存在(可能在队列等待时被删除)
if (!File.Exists(filePath))
{
return false;
}
// 获取白板信息
var whiteboard = await GetWhiteboardInfo(cancellationToken);
if (whiteboard == null)
{
return false;
}
// 获取API基础URL和用户Token
var apiBaseUrl = MainWindow.Settings?.Dlass?.ApiBaseUrl ?? "https://dlass.tech";
var userToken = MainWindow.Settings?.Dlass?.UserToken;
// 准备上传参数
var fileName = Path.GetFileNameWithoutExtension(filePath);
var fileExtension = Path.GetExtension(filePath).ToLower();
var title = fileName;
string fileType;
string tags;
if (fileExtension == ".zip")
{
fileType = "多页面墨迹压缩包";
tags = "自动上传,多页面,zip,压缩包";
}
else if (fileExtension == ".icstk")
{
fileType = "墨迹文件";
tags = "自动上传,墨迹,icstk";
}
else if (fileExtension == ".xml")
{
fileType = "XML文件";
tags = "自动上传,xml";
}
else
{
fileType = "笔记";
tags = "自动上传,笔记,png";
}
var description = $"自动上传的{fileType} - {DateTime.Now:yyyy-MM-dd HH:mm:ss}";
// 创建API客户端并上传文件
var apiClient = new DlassApiClient(APP_ID, APP_SECRET, apiBaseUrl, userToken);
try
{
cancellationToken.ThrowIfCancellationRequested();
var uploadResult = await apiClient.UploadNoteAsync<UploadNoteResponse>(
"/api/whiteboard/upload_note",
filePath,
whiteboard.BoardId,
whiteboard.SecretKey,
title,
description,
tags,
cancellationToken);
if (uploadResult != null && uploadResult.Success)
{
return true;
}
else
{
return false;
}
}
finally
{
apiClient.Dispose();
}
}
catch (OperationCanceledException)
{
throw;
}
catch (Exception)
{
throw;
}
}
/// <summary>
/// 获取白板信息
/// </summary>
private async Task<WhiteboardInfo> GetWhiteboardInfo(CancellationToken cancellationToken)
{
try
{
cancellationToken.ThrowIfCancellationRequested();
var selectedClassName = MainWindow.Settings?.Dlass?.SelectedClassName;
if (string.IsNullOrEmpty(selectedClassName))
{
LogHelper.WriteLogToFile("[DlassUploadQueue] 上传失败:未选择班级", LogHelper.LogType.Error);
return null;
}
var userToken = MainWindow.Settings?.Dlass?.UserToken;
if (string.IsNullOrEmpty(userToken))
{
LogHelper.WriteLogToFile("[DlassUploadQueue] 上传失败:未设置用户Token", LogHelper.LogType.Error);
return null;
}
var apiBaseUrl = MainWindow.Settings?.Dlass?.ApiBaseUrl ?? "https://dlass.tech";
// 创建API客户端并获取白板信息
var apiClient = new DlassApiClient(APP_ID, APP_SECRET, apiBaseUrl, userToken);
try
{
var authData = new
{
app_id = APP_ID,
app_secret = APP_SECRET,
user_token = userToken
};
var authResult = await apiClient.PostAsync<AuthWithTokenResponse>("/api/whiteboard/framework/auth-with-token", authData, requireAuth: false, cancellationToken: cancellationToken);
if (authResult == null || !authResult.Success || authResult.Whiteboards == null)
{
LogHelper.WriteLogToFile("[DlassUploadQueue] 上传失败:无法获取白板信息", LogHelper.LogType.Error);
return null;
}
// 查找匹配班级的白板
var whiteboard = authResult.Whiteboards
.FirstOrDefault(w => !string.IsNullOrEmpty(w.ClassName) && w.ClassName == selectedClassName);
if (whiteboard == null || string.IsNullOrEmpty(whiteboard.BoardId) || string.IsNullOrEmpty(whiteboard.SecretKey))
{
LogHelper.WriteLogToFile($"[DlassUploadQueue] 上传失败:未找到班级'{selectedClassName}'对应的白板", LogHelper.LogType.Error);
return null;
}
return whiteboard;
}
finally
{
apiClient.Dispose();
}
}
catch (OperationCanceledException)
{
throw;
}
catch (Exception)
{
return null;
}
}
}
}
@@ -0,0 +1,81 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
namespace Ink_Canvas.Helpers
{
public static class ExternalCallerLauncher
{
private static readonly string[] ClassIslandProtocols =
{
"classisland://plugins/IslandCaller/Simple/1",
"classisland://plugins/IslandCaller/Simple",
"classisland://plugins/IslandCaller/Run"
};
public static string[] GetProtocolsByType(int externalCallerType)
{
switch (externalCallerType)
{
case 0:
return ClassIslandProtocols;
case 1:
return new[]
{
"secrandom://roll_call/quick_draw",
"secrandom://direct_extraction"
};
case 2:
return new[] { "namepicker://" };
default:
return ClassIslandProtocols;
}
}
public static string[] GetProtocolsByName(string externalCallerName)
{
switch (externalCallerName)
{
case "ClassIsland":
return ClassIslandProtocols;
case "SecRandom":
return new[]
{
"secrandom://roll_call/quick_draw",
"secrandom://direct_extraction"
};
case "NamePicker":
return new[] { "namepicker://" };
default:
return ClassIslandProtocols;
}
}
public static bool TryLaunch(IEnumerable<string> protocols, out Exception lastException)
{
lastException = null;
if (protocols == null) return false;
foreach (var protocol in protocols)
{
if (string.IsNullOrWhiteSpace(protocol)) continue;
try
{
Process.Start(new ProcessStartInfo
{
FileName = protocol,
UseShellExecute = true
});
return true;
}
catch (Exception ex)
{
lastException = ex;
}
}
return false;
}
}
}
+350 -39
View File
@@ -1,12 +1,12 @@
using Microsoft.Win32;
using System;
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
using System.Security;
using System.Windows;
using Microsoft.Win32;
using System.Threading;
using System.Text;
using System.Threading;
using System.Windows;
namespace Ink_Canvas.Helpers
{
@@ -19,11 +19,14 @@ namespace Ink_Canvas.Helpers
private const string FileTypeName = "InkCanvasStrokesFile";
private const string AppName = "Ink Canvas";
private const string AppDescription = "Ink Canvas Strokes File";
// IPC相关常量
private const string IpcMutexName = "InkCanvasFileAssociationIpc";
private const string IpcEventName = "InkCanvasFileAssociationEvent";
private const string IpcFilePrefix = "InkCanvasFileAssociation_";
private const string IpcBoardModePrefix = "InkCanvasBoardMode_";
private const string IpcShowModePrefix = "InkCanvasShowMode_";
private const string IpcUriCommandPrefix = "InkCanvasUriCommand_";
private const int IpcTimeout = 5000; // 5秒超时
/// <summary>
@@ -34,19 +37,19 @@ namespace Ink_Canvas.Helpers
try
{
string exePath = Process.GetCurrentProcess().MainModule.FileName;
// 注册文件类型
using (RegistryKey fileTypeKey = Registry.ClassesRoot.CreateSubKey(FileTypeName))
{
fileTypeKey.SetValue("", AppDescription);
fileTypeKey.SetValue("FriendlyTypeName", AppDescription);
// 设置默认图标
using (RegistryKey defaultIconKey = fileTypeKey.CreateSubKey("DefaultIcon"))
{
defaultIconKey.SetValue("", $"\"{exePath}\",0");
}
// 设置打开命令
using (RegistryKey shellKey = fileTypeKey.CreateSubKey("shell"))
using (RegistryKey openKey = shellKey.CreateSubKey("open"))
@@ -55,16 +58,16 @@ namespace Ink_Canvas.Helpers
commandKey.SetValue("", $"\"{exePath}\" \"%1\"");
}
}
// 注册文件扩展名
using (RegistryKey extensionKey = Registry.ClassesRoot.CreateSubKey(FileExtension))
{
extensionKey.SetValue("", FileTypeName);
}
// 刷新系统文件关联缓存
RefreshSystemFileAssociations();
LogHelper.WriteLogToFile($"成功注册{FileExtension}文件关联", LogHelper.LogType.Event);
return true;
}
@@ -94,13 +97,13 @@ namespace Ink_Canvas.Helpers
{
// 删除文件扩展名关联
Registry.ClassesRoot.DeleteSubKeyTree(FileExtension, false);
// 删除文件类型定义
Registry.ClassesRoot.DeleteSubKeyTree(FileTypeName, false);
// 刷新系统文件关联缓存
RefreshSystemFileAssociations();
LogHelper.WriteLogToFile($"成功注销{FileExtension}文件关联", LogHelper.LogType.Event);
return true;
}
@@ -121,21 +124,21 @@ namespace Ink_Canvas.Helpers
using (RegistryKey extensionKey = Registry.ClassesRoot.OpenSubKey(FileExtension))
{
if (extensionKey == null) return false;
string fileType = extensionKey.GetValue("") as string;
if (string.IsNullOrEmpty(fileType)) return false;
using (RegistryKey fileTypeKey = Registry.ClassesRoot.OpenSubKey(fileType))
{
if (fileTypeKey == null) return false;
using (RegistryKey shellKey = fileTypeKey.OpenSubKey("shell\\open\\command"))
{
if (shellKey == null) return false;
string command = shellKey.GetValue("") as string;
if (string.IsNullOrEmpty(command)) return false;
// 检查命令是否指向当前应用程序
string currentExePath = Process.GetCurrentProcess().MainModule.FileName;
return command.Contains(currentExePath);
@@ -147,7 +150,7 @@ namespace Ink_Canvas.Helpers
{
LogHelper.WriteLogToFile($"检查文件关联状态时出错: {ex.Message}", LogHelper.LogType.Error);
}
return false;
}
@@ -184,11 +187,11 @@ namespace Ink_Canvas.Helpers
public static string GetIcstkFileFromArgs(string[] args)
{
if (args == null || args.Length == 0) return null;
foreach (string arg in args)
{
if (string.IsNullOrEmpty(arg)) continue;
// 检查是否为.icstk文件
if (Path.GetExtension(arg).Equals(FileExtension, StringComparison.OrdinalIgnoreCase))
{
@@ -204,7 +207,7 @@ namespace Ink_Canvas.Helpers
}
}
}
return null;
}
@@ -218,24 +221,24 @@ namespace Ink_Canvas.Helpers
try
{
LogHelper.WriteLogToFile($"尝试通过IPC发送文件路径给已运行实例: {filePath}", LogHelper.LogType.Event);
// 创建IPC文件
string tempDir = Path.GetTempPath();
string ipcFileName = IpcFilePrefix + Guid.NewGuid().ToString("N") + ".tmp";
string ipcFilePath = Path.Combine(tempDir, ipcFileName);
// 写入文件路径到IPC文件
File.WriteAllText(ipcFilePath, filePath, Encoding.UTF8);
// 创建事件通知已运行实例
using (EventWaitHandle ipcEvent = new EventWaitHandle(false, EventResetMode.ManualReset, IpcEventName))
{
ipcEvent.Set();
}
// 等待一段时间让已运行实例处理文件
Thread.Sleep(1000);
// 清理IPC文件
try
{
@@ -248,7 +251,7 @@ namespace Ink_Canvas.Helpers
{
LogHelper.WriteLogToFile($"清理IPC文件失败: {ex.Message}", LogHelper.LogType.Warning);
}
LogHelper.WriteLogToFile("IPC文件路径发送完成", LogHelper.LogType.Event);
return true;
}
@@ -259,6 +262,157 @@ namespace Ink_Canvas.Helpers
}
}
/// <summary>
/// 尝试通过IPC将白板模式命令发送给已运行的实例
/// </summary>
/// <returns>是否成功发送</returns>
public static bool TrySendBoardModeCommandToExistingInstance()
{
try
{
LogHelper.WriteLogToFile("尝试通过IPC发送白板模式命令给已运行实例", LogHelper.LogType.Event);
// 创建IPC文件
string tempDir = Path.GetTempPath();
string ipcFileName = IpcBoardModePrefix + Guid.NewGuid().ToString("N") + ".tmp";
string ipcFilePath = Path.Combine(tempDir, ipcFileName);
// 写入白板模式命令到IPC文件
File.WriteAllText(ipcFilePath, "BOARD_MODE", Encoding.UTF8);
// 创建事件通知已运行实例
using (EventWaitHandle ipcEvent = new EventWaitHandle(false, EventResetMode.ManualReset, IpcEventName))
{
ipcEvent.Set();
}
// 等待一段时间让已运行实例处理命令
Thread.Sleep(1000);
// 清理IPC文件
try
{
if (File.Exists(ipcFilePath))
{
File.Delete(ipcFilePath);
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"清理IPC文件失败: {ex.Message}", LogHelper.LogType.Warning);
}
LogHelper.WriteLogToFile("IPC白板模式命令发送完成", LogHelper.LogType.Event);
return true;
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"通过IPC发送白板模式命令失败: {ex.Message}", LogHelper.LogType.Error);
return false;
}
}
/// <summary>
/// 尝试通过IPC将展开浮动栏命令发送给已运行的实例
/// </summary>
/// <returns>是否成功发送</returns>
public static bool TrySendShowModeCommandToExistingInstance()
{
try
{
LogHelper.WriteLogToFile("尝试通过IPC发送展开浮动栏命令给已运行实例", LogHelper.LogType.Event);
// 创建IPC文件
string tempDir = Path.GetTempPath();
string ipcFileName = IpcShowModePrefix + Guid.NewGuid().ToString("N") + ".tmp";
string ipcFilePath = Path.Combine(tempDir, ipcFileName);
// 写入展开浮动栏命令到IPC文件
File.WriteAllText(ipcFilePath, "SHOW_MODE", Encoding.UTF8);
// 创建事件通知已运行实例
using (EventWaitHandle ipcEvent = new EventWaitHandle(false, EventResetMode.ManualReset, IpcEventName))
{
ipcEvent.Set();
}
// 等待一段时间让已运行实例处理命令
Thread.Sleep(1000);
// 清理IPC文件
try
{
if (File.Exists(ipcFilePath))
{
File.Delete(ipcFilePath);
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"清理IPC文件失败: {ex.Message}", LogHelper.LogType.Warning);
}
LogHelper.WriteLogToFile("IPC展开浮动栏命令发送完成", LogHelper.LogType.Event);
return true;
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"通过IPC发送展开浮动栏命令失败: {ex.Message}", LogHelper.LogType.Error);
return false;
}
}
/// <summary>
/// 尝试通过IPC将URI命令发送给已运行的实例
/// </summary>
/// <param name="uri">URI命令</param>
/// <returns>是否成功发送</returns>
public static bool TrySendUriCommandToExistingInstance(string uri)
{
try
{
LogHelper.WriteLogToFile($"尝试通过IPC发送URI命令给已运行实例: {uri}", LogHelper.LogType.Event);
// 创建IPC文件
string tempDir = Path.GetTempPath();
string ipcFileName = IpcUriCommandPrefix + Guid.NewGuid().ToString("N") + ".tmp";
string ipcFilePath = Path.Combine(tempDir, ipcFileName);
// 写入URI命令到IPC文件
File.WriteAllText(ipcFilePath, uri, Encoding.UTF8);
// 创建事件通知已运行实例
using (EventWaitHandle ipcEvent = new EventWaitHandle(false, EventResetMode.ManualReset, IpcEventName))
{
ipcEvent.Set();
}
// 等待一段时间让已运行实例处理命令
Thread.Sleep(1000);
// 清理IPC文件
try
{
if (File.Exists(ipcFilePath))
{
File.Delete(ipcFilePath);
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"清理IPC文件失败: {ex.Message}", LogHelper.LogType.Warning);
}
LogHelper.WriteLogToFile("IPC URI命令发送完成", LogHelper.LogType.Event);
return true;
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"通过IPC发送URI命令失败: {ex.Message}", LogHelper.LogType.Error);
return false;
}
}
/// <summary>
/// 启动IPC监听器,等待其他实例发送文件路径
/// </summary>
@@ -271,7 +425,7 @@ namespace Ink_Canvas.Helpers
try
{
LogHelper.WriteLogToFile("启动IPC监听器", LogHelper.LogType.Event);
using (EventWaitHandle ipcEvent = new EventWaitHandle(false, EventResetMode.ManualReset, IpcEventName))
{
while (true)
@@ -281,11 +435,11 @@ namespace Ink_Canvas.Helpers
{
// 处理IPC文件
ProcessIpcFiles();
// 重置事件
ipcEvent.Reset();
}
// 检查应用是否还在运行
if (Application.Current == null || Application.Current.Dispatcher == null)
{
@@ -299,7 +453,7 @@ namespace Ink_Canvas.Helpers
LogHelper.WriteLogToFile($"IPC监听器出错: {ex.Message}", LogHelper.LogType.Error);
}
});
ipcThread.IsBackground = true;
ipcThread.Start();
}
@@ -317,19 +471,20 @@ namespace Ink_Canvas.Helpers
try
{
string tempDir = Path.GetTempPath();
// 处理文件路径IPC文件
string[] ipcFiles = Directory.GetFiles(tempDir, IpcFilePrefix + "*.tmp");
foreach (string ipcFile in ipcFiles)
{
try
{
// 读取文件路径
string filePath = File.ReadAllText(ipcFile, Encoding.UTF8);
if (!string.IsNullOrEmpty(filePath) && File.Exists(filePath))
{
LogHelper.WriteLogToFile($"IPC接收到文件路径: {filePath}", LogHelper.LogType.Event);
// 在UI线程中处理文件打开
Application.Current.Dispatcher.BeginInvoke(new Action(() =>
{
@@ -348,14 +503,14 @@ namespace Ink_Canvas.Helpers
}
}));
}
// 删除IPC文件
File.Delete(ipcFile);
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"处理IPC文件失败: {ex.Message}", LogHelper.LogType.Warning);
// 尝试删除损坏的IPC文件
try
{
@@ -364,7 +519,163 @@ namespace Ink_Canvas.Helpers
File.Delete(ipcFile);
}
}
catch { }
catch (Exception innerEx) { System.Diagnostics.Debug.WriteLine(innerEx); }
}
}
// 处理白板模式命令IPC文件
string[] boardModeFiles = Directory.GetFiles(tempDir, IpcBoardModePrefix + "*.tmp");
foreach (string ipcFile in boardModeFiles)
{
try
{
// 读取命令内容
string command = File.ReadAllText(ipcFile, Encoding.UTF8);
if (command == "BOARD_MODE")
{
LogHelper.WriteLogToFile("IPC接收到白板模式命令", LogHelper.LogType.Event);
// 在UI线程中处理白板模式切换
Application.Current.Dispatcher.BeginInvoke(new Action(() =>
{
try
{
// 获取主窗口并切换到白板模式
if (Application.Current.MainWindow is MainWindow mainWindow)
{
mainWindow.SwitchToBoardMode();
mainWindow.ShowNotification("已切换到白板模式");
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"IPC处理白板模式切换失败: {ex.Message}", LogHelper.LogType.Error);
}
}));
}
// 删除IPC文件
File.Delete(ipcFile);
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"处理白板模式IPC文件失败: {ex.Message}", LogHelper.LogType.Warning);
// 尝试删除损坏的IPC文件
try
{
if (File.Exists(ipcFile))
{
File.Delete(ipcFile);
}
}
catch (Exception innerEx) { System.Diagnostics.Debug.WriteLine(innerEx); }
}
}
// 处理展开浮动栏命令IPC文件
string[] showModeFiles = Directory.GetFiles(tempDir, IpcShowModePrefix + "*.tmp");
foreach (string ipcFile in showModeFiles)
{
try
{
// 读取命令内容
string command = File.ReadAllText(ipcFile, Encoding.UTF8);
if (command == "SHOW_MODE")
{
LogHelper.WriteLogToFile("IPC接收到展开浮动栏命令", LogHelper.LogType.Event);
// 在UI线程中处理展开浮动栏
Application.Current.Dispatcher.BeginInvoke(new Action(async () =>
{
try
{
// 获取主窗口并展开浮动栏
if (Application.Current.MainWindow is MainWindow mainWindow)
{
// 如果当前处于收纳模式,则展开浮动栏
if (mainWindow.isFloatingBarFolded)
{
await mainWindow.UnFoldFloatingBar(new object());
}
mainWindow.ShowNotification("已退出收纳模式并恢复浮动栏");
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"IPC处理展开浮动栏失败: {ex.Message}", LogHelper.LogType.Error);
}
}));
}
// 删除IPC文件
File.Delete(ipcFile);
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"处理展开浮动栏IPC文件失败: {ex.Message}", LogHelper.LogType.Warning);
// 尝试删除损坏的IPC文件
try
{
if (File.Exists(ipcFile))
{
File.Delete(ipcFile);
}
}
catch (Exception innerEx) { System.Diagnostics.Debug.WriteLine(innerEx); }
}
}
// 处理URI命令IPC文件
string[] uriCommandFiles = Directory.GetFiles(tempDir, IpcUriCommandPrefix + "*.tmp");
foreach (string ipcFile in uriCommandFiles)
{
try
{
// 读取命令内容
string uri = File.ReadAllText(ipcFile, Encoding.UTF8);
if (!string.IsNullOrEmpty(uri))
{
LogHelper.WriteLogToFile($"IPC接收到URI命令: {uri}", LogHelper.LogType.Event);
// 在UI线程中处理URI命令
Application.Current.Dispatcher.BeginInvoke(new Action(() =>
{
try
{
// 获取主窗口并处理URI命令
if (Application.Current.MainWindow is MainWindow mainWindow)
{
mainWindow.HandleUriCommand(uri);
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"IPC处理URI命令失败: {ex.Message}", LogHelper.LogType.Error);
}
}));
}
// 删除IPC文件
File.Delete(ipcFile);
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"处理URI命令IPC文件失败: {ex.Message}", LogHelper.LogType.Warning);
// 尝试删除损坏的IPC文件
try
{
if (File.Exists(ipcFile))
{
File.Delete(ipcFile);
}
}
catch (Exception innerEx) { System.Diagnostics.Debug.WriteLine(innerEx); }
}
}
}
@@ -377,4 +688,4 @@ namespace Ink_Canvas.Helpers
[DllImport("shell32.dll")]
private static extern void SHChangeNotify(uint wEventId, uint uFlags, IntPtr dwItem1, IntPtr dwItem2);
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,398 @@
using Ink_Canvas.Helpers;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Ink_Canvas
{
/// <summary>
/// 悬浮窗拦截管理器
/// </summary>
public class FloatingWindowInterceptorManager : IDisposable
{
#region
private FloatingWindowInterceptor _interceptor;
private bool _isInitialized;
private bool _disposed;
private FloatingWindowInterceptorSettings _settings;
#endregion
#region
public event EventHandler<FloatingWindowInterceptor.WindowInterceptedEventArgs> WindowIntercepted;
public event EventHandler<FloatingWindowInterceptor.WindowRestoredEventArgs> WindowRestored;
#endregion
#region
public bool IsEnabled => _interceptor != null && _settings != null && _settings.IsEnabled;
public bool IsRunning => _interceptor != null && _interceptor.IsRunning;
#endregion
#region
/// <summary>
/// 初始化拦截器
/// </summary>
public void Initialize(FloatingWindowInterceptorSettings settings)
{
if (_isInitialized) return;
try
{
_settings = settings ?? new FloatingWindowInterceptorSettings();
_interceptor = new FloatingWindowInterceptor();
// 订阅事件
_interceptor.WindowIntercepted += OnWindowIntercepted;
_interceptor.WindowRestored += OnWindowRestored;
// 应用配置
ApplySettings();
_isInitialized = true;
// 如果设置了自动启动,则启动拦截器
if (_settings.AutoStart && _settings.IsEnabled)
{
Start();
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"初始化悬浮窗拦截器失败: {ex.Message}", LogHelper.LogType.Error);
}
}
/// <summary>
/// 启动拦截器
/// </summary>
public void Start()
{
if (!_isInitialized || _settings == null) return;
if (_interceptor == null) return;
try
{
_interceptor.Start(_settings.ScanIntervalMs);
LogHelper.WriteLogToFile("悬浮窗拦截器已启动", LogHelper.LogType.Event);
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"启动悬浮窗拦截器失败: {ex.Message}", LogHelper.LogType.Error);
}
}
/// <summary>
/// 停止拦截器
/// </summary>
public void Stop()
{
if (_interceptor == null) return;
try
{
_interceptor.Stop();
LogHelper.WriteLogToFile("悬浮窗拦截器已停止", LogHelper.LogType.Event);
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"停止悬浮窗拦截器失败: {ex.Message}", LogHelper.LogType.Error);
}
}
/// <summary>
/// 设置拦截规则
/// </summary>
public void SetInterceptRule(FloatingWindowInterceptor.InterceptType type, bool enabled)
{
if (_interceptor == null || _settings == null) return;
try
{
_interceptor.SetInterceptRule(type, enabled);
// 更新设置
var ruleName = type.ToString();
if (_settings.InterceptRules.ContainsKey(ruleName))
{
_settings.InterceptRules[ruleName] = enabled;
}
// 获取规则信息以处理父子关系
var rule = _interceptor.GetInterceptRule(type);
if (rule != null)
{
// 如果是父规则,更新所有子规则的设置
if (rule.ChildTypes.Count > 0)
{
foreach (var childType in rule.ChildTypes)
{
var childRuleName = childType.ToString();
if (_settings.InterceptRules.ContainsKey(childRuleName))
{
_settings.InterceptRules[childRuleName] = enabled;
}
}
}
// 如果是子规则,更新父规则的设置
else if (rule.ParentType.HasValue)
{
var parentRule = _interceptor.GetInterceptRule(rule.ParentType.Value);
if (parentRule != null)
{
var parentRuleName = rule.ParentType.Value.ToString();
if (_settings.InterceptRules.ContainsKey(parentRuleName))
{
_settings.InterceptRules[parentRuleName] = parentRule.IsEnabled;
}
}
}
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"设置拦截规则失败: {ex.Message}", LogHelper.LogType.Error);
}
}
/// <summary>
/// 获取拦截规则
/// </summary>
public FloatingWindowInterceptor.InterceptRule GetInterceptRule(FloatingWindowInterceptor.InterceptType type)
{
return _interceptor?.GetInterceptRule(type);
}
/// <summary>
/// 获取所有拦截规则
/// </summary>
public Dictionary<FloatingWindowInterceptor.InterceptType, FloatingWindowInterceptor.InterceptRule> GetAllRules()
{
return _interceptor?.GetAllRules() ?? new Dictionary<FloatingWindowInterceptor.InterceptType, FloatingWindowInterceptor.InterceptRule>();
}
/// <summary>
/// 手动扫描一次
/// </summary>
public void ScanOnce()
{
if (_interceptor == null) return;
try
{
_interceptor.ScanOnce();
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"手动扫描失败: {ex.Message}", LogHelper.LogType.Error);
}
}
/// <summary>
/// 恢复所有被拦截的窗口
/// </summary>
public void RestoreAllWindows()
{
if (_interceptor == null) return;
try
{
_interceptor.RestoreAllWindows();
LogHelper.WriteLogToFile("已恢复所有被拦截的窗口", LogHelper.LogType.Event);
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"恢复窗口失败: {ex.Message}", LogHelper.LogType.Error);
}
}
/// <summary>
/// 应用设置
/// </summary>
public void ApplySettings()
{
if (_interceptor == null || _settings == null) return;
try
{
// 应用拦截规则设置
foreach (var kvp in _settings.InterceptRules)
{
if (Enum.TryParse<FloatingWindowInterceptor.InterceptType>(kvp.Key, out var type))
{
_interceptor.SetInterceptRule(type, kvp.Value);
}
}
// 如果启用了拦截器,则启动
if (_settings.IsEnabled && !IsRunning)
{
Start();
}
// 如果禁用了拦截器,则停止
else if (!_settings.IsEnabled && IsRunning)
{
Stop();
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"应用设置失败: {ex.Message}", LogHelper.LogType.Error);
}
}
/// <summary>
/// 更新扫描间隔
/// </summary>
public void UpdateScanInterval(int intervalMs)
{
if (_interceptor == null || _settings == null) return;
try
{
_settings.ScanIntervalMs = intervalMs;
// 如果正在运行,重启以应用新间隔
if (IsRunning)
{
Stop();
Start();
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"更新扫描间隔失败: {ex.Message}", LogHelper.LogType.Error);
}
}
/// <summary>
/// 获取拦截统计信息
/// </summary>
public InterceptStatistics GetStatistics()
{
if (_interceptor == null || _settings == null) return new InterceptStatistics();
try
{
var rules = GetAllRules();
var enabledRules = rules.Count(r => r.Value.IsEnabled);
var totalRules = rules.Count;
return new InterceptStatistics
{
TotalRules = totalRules,
EnabledRules = enabledRules,
IsRunning = IsRunning,
ScanIntervalMs = _settings.ScanIntervalMs
};
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"获取统计信息失败: {ex.Message}", LogHelper.LogType.Error);
return new InterceptStatistics();
}
}
#endregion
#region
private void OnWindowIntercepted(object sender, FloatingWindowInterceptor.WindowInterceptedEventArgs e)
{
try
{
// 记录日志
LogHelper.WriteLogToFile($"拦截窗口: {e.WindowTitle} ({e.InterceptType})", LogHelper.LogType.Event);
// 显示通知(如果启用)
if (_settings != null && _settings.ShowNotifications)
{
ShowNotification($"已拦截悬浮窗: {e.Rule.Description}");
}
// 触发事件
WindowIntercepted?.Invoke(this, e);
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"处理窗口拦截事件失败: {ex.Message}", LogHelper.LogType.Error);
}
}
private void OnWindowRestored(object sender, FloatingWindowInterceptor.WindowRestoredEventArgs e)
{
try
{
// 记录日志
LogHelper.WriteLogToFile($"恢复窗口: {e.InterceptType}", LogHelper.LogType.Event);
// 触发事件
WindowRestored?.Invoke(this, e);
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"处理窗口恢复事件失败: {ex.Message}", LogHelper.LogType.Error);
}
}
private void ShowNotification(string message)
{
try
{
// 这里可以集成系统通知或自定义通知
// 暂时使用调试输出
System.Diagnostics.Debug.WriteLine($"通知: {message}");
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"显示通知失败: {ex.Message}", LogHelper.LogType.Error);
}
}
#endregion
#region
public class InterceptStatistics
{
public int TotalRules { get; set; }
public int EnabledRules { get; set; }
public bool IsRunning { get; set; }
public int ScanIntervalMs { get; set; }
}
#endregion
#region IDisposable
public void Dispose()
{
if (_disposed) return;
try
{
Stop();
_interceptor?.Dispose();
_interceptor = null;
_isInitialized = false;
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"释放悬浮窗拦截器失败: {ex.Message}", LogHelper.LogType.Error);
}
finally
{
_disposed = true;
}
}
#endregion
}
}
+46 -5
View File
@@ -1,4 +1,4 @@
using System;
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Text;
@@ -36,6 +36,29 @@ namespace Ink_Canvas.Helpers
public int Height => Bottom - Top;
}
[StructLayout(LayoutKind.Sequential)]
private struct MONITORINFO
{
public uint cbSize;
public RECT rcMonitor;
public RECT rcWork;
public uint dwFlags;
}
[DllImport("user32.dll")]
private static extern bool GetMonitorInfo(IntPtr hMonitor, ref MONITORINFO lpmi);
[DllImport("user32.dll")]
private static extern IntPtr MonitorFromWindow(IntPtr hwnd, uint dwFlags);
[DllImport("user32.dll")]
private static extern IntPtr MonitorFromRect(ref RECT lprc, uint dwFlags);
public static IntPtr GetForegroundWindowHandle()
{
return GetForegroundWindow();
}
public static string WindowTitle()
{
IntPtr foregroundWindowHandle = GetForegroundWindow();
@@ -106,10 +129,28 @@ namespace Ink_Canvas.Helpers
public static double GetTaskbarHeight(Screen screen, double dpiScaleY)
{
// 获取工作区和屏幕高度的差值
var workingArea = screen.WorkingArea;
var bounds = screen.Bounds;
int taskbarHeight = bounds.Height - workingArea.Height;
// 创建RECT结构体表示屏幕边界
RECT screenRect = new RECT
{
Left = screen.Bounds.Left,
Top = screen.Bounds.Top,
Right = screen.Bounds.Right,
Bottom = screen.Bounds.Bottom
};
// 获取屏幕句柄
const uint MONITOR_DEFAULTTONEAREST = 0x00000002;
IntPtr hMonitor = MonitorFromRect(ref screenRect, MONITOR_DEFAULTTONEAREST);
// 初始化MONITORINFO结构体
MONITORINFO monitorInfo = new MONITORINFO();
monitorInfo.cbSize = (uint)Marshal.SizeOf(typeof(MONITORINFO));
// 获取监视器信息
GetMonitorInfo(hMonitor, ref monitorInfo);
// 计算任务栏高度:monitorInfo.rcMonitor.bottom减去monitorInfo.rcWork.bottom的值
int taskbarHeight = monitorInfo.rcMonitor.Bottom - monitorInfo.rcWork.Bottom;
// 考虑 DPI 缩放
return taskbarHeight / dpiScaleY;
}
-3
View File
@@ -1,5 +1,4 @@
using System;
using System.Runtime.ExceptionServices;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Interop;
@@ -189,9 +188,7 @@ namespace Ink_Canvas.Helpers
/// <summary>
/// 确保窗口全屏的Hook
/// 使用HandleProcessCorruptedStateExceptions,防止访问内存过程中因为一些致命异常导致程序崩溃
/// </summary>
[HandleProcessCorruptedStateExceptions]
private static IntPtr KeepFullScreenHook(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
{
//处理WM_WINDOWPOSCHANGING消息
+661 -36
View File
@@ -5,6 +5,8 @@ using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Text;
using System.Windows;
using System.Windows.Forms;
using System.Windows.Input;
namespace Ink_Canvas.Helpers
@@ -20,8 +22,18 @@ namespace Ink_Canvas.Helpers
private bool _isDisposed;
private bool _hotkeysShouldBeRegistered = true; // 启动时注册热键
// 多屏幕支持相关字段
private Screen _currentScreen;
private bool _isMultiScreenMode = false;
private bool _enableScreenSpecificHotkeys = true; // 是否启用基于屏幕的热键注册
// 智能热键管理相关字段
private bool _isWindowFocused = false;
private bool _isMouseOverWindow = false;
private System.Windows.Threading.DispatcherTimer _mousePositionTimer;
// 配置文件路径
private static readonly string HotkeyConfigFile = Path.Combine(App.RootPath, "HotkeyConfig.json");
private static readonly string HotkeyConfigFile = Path.Combine(App.RootPath, "Configs", "HotkeyConfig.json");
#endregion
#region Constructor
@@ -30,6 +42,12 @@ namespace Ink_Canvas.Helpers
_mainWindow = mainWindow ?? throw new ArgumentNullException(nameof(mainWindow));
_registeredHotkeys = new Dictionary<string, HotkeyInfo>();
_hotkeysShouldBeRegistered = true; // 启动时注册热键
// 初始化多屏幕支持
InitializeMultiScreenSupport();
// 启动时确保配置文件存在
EnsureConfigFileExists();
}
#endregion
@@ -49,11 +67,27 @@ namespace Ink_Canvas.Helpers
if (_isDisposed)
return false;
// 检查是否应该注册热键(基于屏幕和模式)
if (!ShouldRegisterHotkeys())
{
return false;
}
// 如果快捷键已存在,先注销
if (_registeredHotkeys.ContainsKey(hotkeyName))
{
UnregisterHotkey(hotkeyName);
}
else
{
try
{
HotkeyManager.Current.Remove(hotkeyName);
}
catch
{
}
}
// 创建快捷键信息
var hotkeyInfo = new HotkeyInfo
@@ -82,12 +116,14 @@ namespace Ink_Canvas.Helpers
});
_registeredHotkeys[hotkeyName] = hotkeyInfo;
// 成功注册全局快捷键
// 记录注册信息
var screenInfo = _isMultiScreenMode ? $" (屏幕: {_currentScreen?.DeviceName})" : "";
return true;
}
catch (Exception ex)
catch (Exception)
{
LogHelper.WriteLogToFile($"注册全局快捷键 {hotkeyName} 失败: {ex.Message}", LogHelper.LogType.Error);
return false;
}
}
@@ -176,7 +212,6 @@ namespace Ink_Canvas.Helpers
{
if (!File.Exists(HotkeyConfigFile))
{
LogHelper.WriteLogToFile("快捷键配置文件不存在");
return new List<HotkeyInfo>();
}
@@ -209,7 +244,6 @@ namespace Ink_Canvas.Helpers
});
}
LogHelper.WriteLogToFile($"从配置文件读取到 {hotkeyList.Count} 个快捷键信息");
return hotkeyList;
}
catch (Exception ex)
@@ -251,6 +285,7 @@ namespace Ink_Canvas.Helpers
// 功能快捷键
RegisterHotkey("DrawLine", Key.L, ModifierKeys.Alt, () => _mainWindow.BtnDrawLine_Click(null, null));
RegisterHotkey("Screenshot", Key.C, ModifierKeys.Alt, () => _mainWindow.SaveScreenShotToDesktop());
RegisterHotkey("QuickDraw", Key.K, ModifierKeys.Alt, () => _mainWindow.OpenQuickDrawFromHotkey());
RegisterHotkey("Hide", Key.V, ModifierKeys.Alt, () => _mainWindow.SymbolIconEmoji_MouseUp(null, null));
// 退出快捷键
@@ -280,28 +315,27 @@ namespace Ink_Canvas.Helpers
return;
}
// 如果配置文件不存在,先创建默认配置文件
if (!File.Exists(HotkeyConfigFile))
{
LogHelper.WriteLogToFile($"快捷键配置文件不存在: {HotkeyConfigFile}", LogHelper.LogType.Warning);
CreateDefaultConfigFile();
RegisterDefaultHotkeys();
_hotkeysShouldBeRegistered = true;
return;
}
// 尝试从配置文件加载
if (LoadHotkeysFromConfigFile())
{
// 成功从配置文件加载快捷键设置
_hotkeysShouldBeRegistered = true;
LogHelper.WriteLogToFile("成功从配置文件加载快捷键设置");
}
else
{
// 如果配置文件存在加载失败,使用默认快捷键
if (!File.Exists(HotkeyConfigFile))
{
LogHelper.WriteLogToFile("配置文件不存在,注册默认快捷键");
RegisterDefaultHotkeys();
_hotkeysShouldBeRegistered = true;
}
else
{
LogHelper.WriteLogToFile("配置文件存在但加载失败,回退到默认快捷键", LogHelper.LogType.Warning);
RegisterDefaultHotkeys();
_hotkeysShouldBeRegistered = true;
}
LogHelper.WriteLogToFile("配置文件存在加载失败,回退到默认快捷键", LogHelper.LogType.Warning);
RegisterDefaultHotkeys();
_hotkeysShouldBeRegistered = true;
}
}
catch (Exception ex)
@@ -318,11 +352,9 @@ namespace Ink_Canvas.Helpers
{
try
{
LogHelper.WriteLogToFile("开始保存快捷键配置到配置文件", LogHelper.LogType.Event);
if (SaveHotkeysToConfigFile())
{
LogHelper.WriteLogToFile("快捷键配置已成功保存到配置文件", LogHelper.LogType.Event);
}
else
{
@@ -346,16 +378,28 @@ namespace Ink_Canvas.Helpers
if (!_hotkeysShouldBeRegistered)
{
_hotkeysShouldBeRegistered = true;
LogHelper.WriteLogToFile("启用快捷键注册功能");
// 立即加载快捷键设置
LoadHotkeysFromSettings();
// 启动鼠标位置监控定时器
if (_isMultiScreenMode && _enableScreenSpecificHotkeys && _mousePositionTimer != null)
{
_mousePositionTimer.Start();
}
// 根据上下文决定是否立即加载快捷键
if (ShouldEnableHotkeysBasedOnContext())
{
LoadHotkeysFromSettings();
}
}
else
{
LogHelper.WriteLogToFile("快捷键注册功能已经启用,重新加载快捷键设置");
// 即使已经启用,也要重新加载快捷键设置以确保快捷键正常工作
LoadHotkeysFromSettings();
if (_registeredHotkeys.Count == 0)
{
if (ShouldEnableHotkeysBasedOnContext())
{
LoadHotkeysFromSettings();
}
}
}
}
catch (Exception ex)
@@ -375,14 +419,18 @@ namespace Ink_Canvas.Helpers
if (_hotkeysShouldBeRegistered)
{
_hotkeysShouldBeRegistered = false;
LogHelper.WriteLogToFile("禁用快捷键注册功能");
// 停止鼠标位置监控定时器
if (_mousePositionTimer != null && _mousePositionTimer.IsEnabled)
{
_mousePositionTimer.Stop();
}
// 注销所有快捷键
UnregisterAllHotkeys();
}
else
{
LogHelper.WriteLogToFile("快捷键注册功能已经禁用");
}
}
catch (Exception ex)
@@ -407,20 +455,27 @@ namespace Ink_Canvas.Helpers
{
// 如果设置允许,则在鼠标模式下也启用快捷键
EnableHotkeyRegistration();
LogHelper.WriteLogToFile("切换到鼠标模式,但根据设置保持快捷键启用");
if (_hotkeysShouldBeRegistered && _registeredHotkeys.Count == 0)
{
LoadHotkeysFromSettings();
}
}
else
{
// 鼠标模式下禁用快捷键,让键盘操作放行
DisableHotkeyRegistration();
LogHelper.WriteLogToFile("切换到鼠标模式,禁用快捷键以放行键盘操作");
}
}
else
{
// 非鼠标模式下启用快捷键
EnableHotkeyRegistration();
LogHelper.WriteLogToFile("切换到非鼠标模式,启用快捷键");
if (_hotkeysShouldBeRegistered && _registeredHotkeys.Count == 0)
{
LoadHotkeysFromSettings();
}
}
}
catch (Exception ex)
@@ -457,7 +512,6 @@ namespace Ink_Canvas.Helpers
if (success)
{
LogHelper.WriteLogToFile($"成功更新快捷键 {hotkeyName}: {modifiers}+{key}", LogHelper.LogType.Event);
// 自动保存配置
SaveHotkeysToSettings();
}
@@ -470,9 +524,466 @@ namespace Ink_Canvas.Helpers
return false;
}
}
/// <summary>
/// 启用基于屏幕的热键注册
/// </summary>
public void EnableScreenSpecificHotkeys()
{
try
{
_enableScreenSpecificHotkeys = true;
// 如果当前在多屏幕环境下,刷新热键注册
if (_isMultiScreenMode)
{
RefreshHotkeysForCurrentScreen();
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"启用基于屏幕的热键注册时出错: {ex.Message}", LogHelper.LogType.Error);
}
}
/// <summary>
/// 禁用基于屏幕的热键注册
/// </summary>
public void DisableScreenSpecificHotkeys()
{
try
{
_enableScreenSpecificHotkeys = false;
// 重新注册热键(全局模式)
if (_hotkeysShouldBeRegistered)
{
RefreshHotkeysForCurrentScreen();
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"禁用基于屏幕的热键注册时出错: {ex.Message}", LogHelper.LogType.Error);
}
}
/// <summary>
/// 刷新多屏相关设置(开关和跟随鼠标策略)。
/// </summary>
public void RefreshMultiScreenSettings()
{
try
{
var advanced = MainWindow.Settings.Advanced;
_isMultiScreenMode = advanced.EnableMultiScreenSupport && ScreenDetectionHelper.HasMultipleScreens();
_enableScreenSpecificHotkeys = _isMultiScreenMode;
if (_isMultiScreenMode)
{
_currentScreen = advanced.FollowMouseForScreenSelection
? Screen.FromPoint(Control.MousePosition)
: ScreenDetectionHelper.GetWindowScreen(_mainWindow);
}
else
{
_currentScreen = ScreenDetectionHelper.GetPrimaryScreen();
}
RefreshHotkeysForCurrentScreen();
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"刷新多屏设置时出错: {ex.Message}", LogHelper.LogType.Error);
}
}
/// <summary>
/// 获取当前屏幕信息
/// </summary>
/// <returns>当前屏幕信息</returns>
public string GetCurrentScreenInfo()
{
try
{
if (_isMultiScreenMode && _currentScreen != null)
{
return $"多屏幕环境 - 当前屏幕: {_currentScreen.DeviceName} ({_currentScreen.Bounds.Width}x{_currentScreen.Bounds.Height})";
}
else
{
return "单屏幕环境";
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"获取当前屏幕信息时出错: {ex.Message}", LogHelper.LogType.Error);
return "无法获取屏幕信息";
}
}
/// <summary>
/// 检查是否启用了基于屏幕的热键注册
/// </summary>
/// <returns>是否启用</returns>
public bool IsScreenSpecificHotkeysEnabled()
{
return _enableScreenSpecificHotkeys && _isMultiScreenMode;
}
/// <summary>
/// 手动刷新当前屏幕的热键注册
/// </summary>
public void RefreshCurrentScreenHotkeys()
{
try
{
RefreshHotkeysForCurrentScreen();
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"刷新当前屏幕热键时出错: {ex.Message}", LogHelper.LogType.Error);
}
}
#endregion
#region Private Helper Methods
/// <summary>
/// 初始化多屏幕支持
/// </summary>
private void InitializeMultiScreenSupport()
{
try
{
var advanced = MainWindow.Settings.Advanced;
_isMultiScreenMode = advanced.EnableMultiScreenSupport && ScreenDetectionHelper.HasMultipleScreens();
_enableScreenSpecificHotkeys = _isMultiScreenMode;
if (_isMultiScreenMode)
{
_currentScreen = advanced.FollowMouseForScreenSelection
? Screen.FromPoint(Control.MousePosition)
: ScreenDetectionHelper.GetWindowScreen(_mainWindow);
// 监听窗口位置变化事件
_mainWindow.LocationChanged += OnWindowLocationChanged;
// 初始化智能热键管理
InitializeSmartHotkeyManagement();
}
else
{
_currentScreen = ScreenDetectionHelper.GetPrimaryScreen();
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"初始化多屏幕支持时出错: {ex.Message}", LogHelper.LogType.Error);
_isMultiScreenMode = false;
_currentScreen = ScreenDetectionHelper.GetPrimaryScreen();
}
}
/// <summary>
/// 初始化智能热键管理
/// </summary>
private void InitializeSmartHotkeyManagement()
{
try
{
// 监听窗口焦点事件
_mainWindow.GotFocus += OnWindowGotFocus;
_mainWindow.LostFocus += OnWindowLostFocus;
// 监听鼠标进入/离开事件
_mainWindow.MouseEnter += OnMouseEnterWindow;
_mainWindow.MouseLeave += OnMouseLeaveWindow;
// 初始化鼠标位置监控定时器
_mousePositionTimer = new System.Windows.Threading.DispatcherTimer();
_mousePositionTimer.Interval = TimeSpan.FromMilliseconds(500); // 每500ms检查一次
_mousePositionTimer.Tick += OnMousePositionTimerTick;
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"初始化热键管理时出错: {ex.Message}", LogHelper.LogType.Error);
}
}
/// <summary>
/// 窗口位置变化事件处理
/// </summary>
private void OnWindowLocationChanged(object sender, EventArgs e)
{
try
{
if (!_isMultiScreenMode || !_enableScreenSpecificHotkeys)
return;
if (MainWindow.Settings.Advanced.FollowMouseForScreenSelection)
return;
var newScreen = ScreenDetectionHelper.GetWindowScreen(_mainWindow);
if (newScreen != null && newScreen != _currentScreen)
{
_currentScreen = newScreen;
// 重新注册热键以适应新屏幕
RefreshHotkeysForCurrentScreen();
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"处理窗口位置变化时出错: {ex.Message}", LogHelper.LogType.Error);
}
}
/// <summary>
/// 为当前屏幕刷新热键注册
/// </summary>
private void RefreshHotkeysForCurrentScreen()
{
try
{
if (!_hotkeysShouldBeRegistered)
return;
// 注销所有现有热键
UnregisterAllHotkeys();
// 重新注册热键
LoadHotkeysFromSettings();
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"刷新当前屏幕热键时出错: {ex.Message}", LogHelper.LogType.Error);
}
}
/// <summary>
/// 窗口获得焦点事件处理
/// </summary>
private void OnWindowGotFocus(object sender, RoutedEventArgs e)
{
try
{
_isWindowFocused = true;
UpdateHotkeyStateBasedOnContext();
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"处理窗口获得焦点事件时出错: {ex.Message}", LogHelper.LogType.Error);
}
}
/// <summary>
/// 窗口失去焦点事件处理
/// </summary>
private void OnWindowLostFocus(object sender, RoutedEventArgs e)
{
try
{
_isWindowFocused = false;
UpdateHotkeyStateBasedOnContext();
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"处理窗口失去焦点事件时出错: {ex.Message}", LogHelper.LogType.Error);
}
}
/// <summary>
/// 鼠标进入窗口事件处理
/// </summary>
private void OnMouseEnterWindow(object sender, System.Windows.Input.MouseEventArgs e)
{
try
{
_isMouseOverWindow = true;
UpdateHotkeyStateBasedOnContext();
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"处理鼠标进入窗口事件时出错: {ex.Message}", LogHelper.LogType.Error);
}
}
/// <summary>
/// 鼠标离开窗口事件处理
/// </summary>
private void OnMouseLeaveWindow(object sender, System.Windows.Input.MouseEventArgs e)
{
try
{
_isMouseOverWindow = false;
UpdateHotkeyStateBasedOnContext();
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"处理鼠标离开窗口事件时出错: {ex.Message}", LogHelper.LogType.Error);
}
}
/// <summary>
/// 鼠标位置定时器事件处理
/// </summary>
private void OnMousePositionTimerTick(object sender, EventArgs e)
{
try
{
if (!_isMultiScreenMode || !_enableScreenSpecificHotkeys)
return;
var mousePosition = Control.MousePosition;
var mouseScreen = Screen.FromPoint(mousePosition);
if (MainWindow.Settings.Advanced.FollowMouseForScreenSelection &&
mouseScreen != null &&
mouseScreen != _currentScreen)
{
_currentScreen = mouseScreen;
RefreshHotkeysForCurrentScreen();
}
// 无论屏幕是否变化,都检查热键状态
// 这样可以确保热键状态始终与当前上下文保持一致
bool shouldEnableHotkeys = ShouldEnableHotkeysBasedOnContext();
bool currentlyHasHotkeys = _registeredHotkeys.Count > 0;
if (shouldEnableHotkeys && !currentlyHasHotkeys)
{
UpdateHotkeyStateBasedOnContext();
}
else if (!shouldEnableHotkeys && currentlyHasHotkeys)
{
UpdateHotkeyStateBasedOnContext();
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"处理鼠标位置定时器事件时出错: {ex.Message}", LogHelper.LogType.Error);
}
}
/// <summary>
/// 根据上下文更新热键状态
/// </summary>
private void UpdateHotkeyStateBasedOnContext()
{
try
{
if (!_hotkeysShouldBeRegistered)
return;
bool shouldEnableHotkeys = ShouldEnableHotkeysBasedOnContext();
bool currentlyHasHotkeys = _registeredHotkeys.Count > 0;
if (shouldEnableHotkeys && !currentlyHasHotkeys)
{
// 需要注册快捷键
LoadHotkeysFromSettings();
}
else if (!shouldEnableHotkeys && currentlyHasHotkeys)
{
// 需要注销快捷键
UnregisterAllHotkeys();
}
// 如果状态没有变化,则不进行任何操作
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"根据上下文更新热键状态时出错: {ex.Message}", LogHelper.LogType.Error);
}
}
/// <summary>
/// 检查是否应该注册热键(基于屏幕和模式)
/// </summary>
/// <returns>是否应该注册热键</returns>
private bool ShouldRegisterHotkeys()
{
try
{
// 如果禁用热键注册,则不注册
if (!_hotkeysShouldBeRegistered)
return false;
// 如果启用基于屏幕的热键注册
if (_enableScreenSpecificHotkeys && _isMultiScreenMode)
{
return ShouldEnableHotkeysBasedOnContext();
}
return true;
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"检查是否应该注册热键时出错: {ex.Message}", LogHelper.LogType.Error);
return true; // 出错时默认注册
}
}
/// <summary>
/// 根据上下文检查是否应该启用热键
/// </summary>
/// <returns>是否应该启用热键</returns>
private bool ShouldEnableHotkeysBasedOnContext()
{
try
{
// 检查当前是否处于鼠标模式
bool isMouseMode = IsInSelectMode();
if (isMouseMode)
{
// 鼠标模式下,根据设置决定是否启用快捷键
return MainWindow.Settings.Appearance.EnableHotkeysInMouseMode;
}
else
{
// 非鼠标模式下,需要检查焦点和屏幕位置
// 策略1:鼠标在窗口上时启用热键(最高优先级)
if (_isMouseOverWindow)
{
return true;
}
// 策略2:在多屏幕环境下,检查鼠标是否在当前窗口所在的屏幕上
if (_isMultiScreenMode && _enableScreenSpecificHotkeys)
{
var mousePosition = Control.MousePosition;
var mouseScreen = Screen.FromPoint(mousePosition);
if (mouseScreen == _currentScreen)
{
return true;
}
else
{
return false;
}
}
// 策略3:单屏幕环境下,窗口有焦点时启用热键
if (_isWindowFocused)
{
return true;
}
// 策略4:如果以上都不满足,但在非鼠标模式下,仍然启用快捷键
// 这样可以确保在批注模式下快捷键始终可用
return true;
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"检查是否应该启用热键时出错: {ex.Message}", LogHelper.LogType.Error);
return true; // 出错时默认启用
}
}
/// <summary>
/// 切换到指定笔类型
/// </summary>
@@ -505,6 +1016,89 @@ namespace Ink_Canvas.Helpers
}
}
/// <summary>
/// 确保配置文件存在,如果不存在则创建
/// </summary>
private void EnsureConfigFileExists()
{
try
{
// 如果配置文件不存在,创建默认配置文件
if (!File.Exists(HotkeyConfigFile))
{
CreateDefaultConfigFile();
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"确保快捷键配置文件存在时出错: {ex.Message}", LogHelper.LogType.Error);
}
}
/// <summary>
/// 创建默认的快捷键配置文件
/// </summary>
private void CreateDefaultConfigFile()
{
try
{
// 确保配置目录存在
string configDir = Path.GetDirectoryName(HotkeyConfigFile);
if (!Directory.Exists(configDir))
{
Directory.CreateDirectory(configDir);
}
// 创建默认配置对象
var config = new HotkeyConfig
{
Version = "1.0",
LastModified = DateTime.Now,
Hotkeys = new List<HotkeyConfigItem>()
};
// 添加默认快捷键配置
config.Hotkeys.AddRange(new[]
{
new HotkeyConfigItem { Name = "Undo", Key = Key.Z, Modifiers = ModifierKeys.Control },
new HotkeyConfigItem { Name = "Redo", Key = Key.Y, Modifiers = ModifierKeys.Control },
new HotkeyConfigItem { Name = "Clear", Key = Key.E, Modifiers = ModifierKeys.Control },
new HotkeyConfigItem { Name = "Paste", Key = Key.V, Modifiers = ModifierKeys.Control },
new HotkeyConfigItem { Name = "SelectTool", Key = Key.S, Modifiers = ModifierKeys.Alt },
new HotkeyConfigItem { Name = "DrawTool", Key = Key.D, Modifiers = ModifierKeys.Alt },
new HotkeyConfigItem { Name = "EraserTool", Key = Key.E, Modifiers = ModifierKeys.Alt },
new HotkeyConfigItem { Name = "BlackboardTool", Key = Key.B, Modifiers = ModifierKeys.Alt },
new HotkeyConfigItem { Name = "QuitDrawTool", Key = Key.Q, Modifiers = ModifierKeys.Alt },
new HotkeyConfigItem { Name = "Pen1", Key = Key.D1, Modifiers = ModifierKeys.Alt },
new HotkeyConfigItem { Name = "Pen2", Key = Key.D2, Modifiers = ModifierKeys.Alt },
new HotkeyConfigItem { Name = "Pen3", Key = Key.D3, Modifiers = ModifierKeys.Alt },
new HotkeyConfigItem { Name = "Pen4", Key = Key.D4, Modifiers = ModifierKeys.Alt },
new HotkeyConfigItem { Name = "Pen5", Key = Key.D5, Modifiers = ModifierKeys.Alt },
new HotkeyConfigItem { Name = "DrawLine", Key = Key.L, Modifiers = ModifierKeys.Alt },
new HotkeyConfigItem { Name = "Screenshot", Key = Key.C, Modifiers = ModifierKeys.Alt },
new HotkeyConfigItem { Name = "QuickDraw", Key = Key.K, Modifiers = ModifierKeys.Alt },
new HotkeyConfigItem { Name = "Hide", Key = Key.V, Modifiers = ModifierKeys.Alt },
new HotkeyConfigItem { Name = "Exit", Key = Key.Escape, Modifiers = ModifierKeys.None }
});
// 序列化为JSON
var settings = new JsonSerializerSettings
{
Formatting = Formatting.Indented
};
string jsonContent = JsonConvert.SerializeObject(config, settings);
// 写入配置文件
File.WriteAllText(HotkeyConfigFile, jsonContent, Encoding.UTF8);
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"创建默认快捷键配置文件时出错: {ex.Message}", LogHelper.LogType.Error);
}
}
/// <summary>
/// 从配置文件加载快捷键设置
/// </summary>
@@ -561,7 +1155,14 @@ namespace Ink_Canvas.Helpers
}
}
LogHelper.WriteLogToFile($"成功加载 {successCount}/{config.Hotkeys.Count} 个快捷键配置", LogHelper.LogType.Event);
// 旧版 HotkeyConfig.json 无「快抽」项时补注册默认组合,避免升级后无快捷键
if (successCount > 0 && !IsHotkeyRegistered("QuickDraw"))
{
var quickDrawAction = GetActionByName("QuickDraw");
if (quickDrawAction != null && RegisterHotkey("QuickDraw", Key.K, ModifierKeys.Alt, quickDrawAction))
successCount++;
}
if (successCount > 0)
{
_hotkeysShouldBeRegistered = true;
@@ -620,7 +1221,6 @@ namespace Ink_Canvas.Helpers
// 直接写入原文件,覆盖原有内容
File.WriteAllText(HotkeyConfigFile, jsonContent, Encoding.UTF8);
LogHelper.WriteLogToFile($"快捷键配置已保存到: {HotkeyConfigFile}", LogHelper.LogType.Event);
return true;
}
catch (Exception ex)
@@ -673,6 +1273,8 @@ namespace Ink_Canvas.Helpers
return () => _mainWindow.BtnDrawLine_Click(null, null);
case "Screenshot":
return () => _mainWindow.SaveScreenShotToDesktop();
case "QuickDraw":
return () => _mainWindow.OpenQuickDrawFromHotkey();
case "Hide":
return () => _mainWindow.SymbolIconEmoji_MouseUp(null, null);
case "Exit":
@@ -826,6 +1428,29 @@ namespace Ink_Canvas.Helpers
{
if (!_isDisposed)
{
// 注销所有快捷键
UnregisterAllHotkeys();
// 停止定时器
if (_mousePositionTimer != null)
{
_mousePositionTimer.Stop();
_mousePositionTimer = null;
}
// 移除事件监听器
if (_mainWindow != null)
{
if (_isMultiScreenMode)
{
_mainWindow.LocationChanged -= OnWindowLocationChanged;
}
_mainWindow.GotFocus -= OnWindowGotFocus;
_mainWindow.LostFocus -= OnWindowLostFocus;
_mainWindow.MouseEnter -= OnMouseEnterWindow;
_mainWindow.MouseLeave -= OnMouseLeaveWindow;
}
_isDisposed = true;
}
@@ -186,11 +186,11 @@ namespace Ink_Canvas.Helpers
}
/// <summary>
/// 释放GPU资源
/// 释放GPU相关资源标记
/// </summary>
public void Dispose()
{
_renderTarget?.Clear();
_isInitialized = false;
}
}
+35
View File
@@ -0,0 +1,35 @@
using System;
using System.Security.Cryptography;
using System.Text;
namespace Ink_Canvas.Helpers
{
/// <summary>
/// 哈希计算辅助类,用于路径/标识等短字符串的 MD5 前缀哈希。
/// </summary>
internal static class HashHelper
{
/// <summary>
/// 对给定路径字符串计算 MD5 哈希,返回前 8 位十六进制字符串。
/// </summary>
/// <param name="filePath">文件路径或任意字符串</param>
/// <returns>8 位十六进制字符串;异常或空输入时返回 "error" 或 "unknown"</returns>
public static string GetFileHash(string filePath)
{
try
{
if (string.IsNullOrEmpty(filePath)) return "unknown";
using (var md5 = MD5.Create())
{
byte[] hash = md5.ComputeHash(Encoding.UTF8.GetBytes(filePath));
return BitConverter.ToString(hash).Replace("-", "").Substring(0, 8);
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"计算文件哈希失败: {ex}", LogHelper.LogType.Error);
return "error";
}
}
}
}
+63
View File
@@ -0,0 +1,63 @@
using System;
namespace Ink_Canvas.Helpers
{
public interface IPPTLinkManager : IDisposable
{
event Action<object> SlideShowBegin;
event Action<object> SlideShowNextSlide;
event Action<object> SlideShowEnd;
event Action<object> PresentationOpen;
event Action<object> PresentationClose;
event Action<bool> PPTConnectionChanged;
event Action<bool> SlideShowStateChanged;
bool IsConnected { get; }
bool IsInSlideShow { get; }
bool IsSupportWPS { get; set; }
bool SkipAnimationsWhenNavigating { get; set; }
int SlidesCount { get; }
object PPTApplication { get; }
/// <summary>
/// 开始监视与 PowerPoint 的连接以及幻灯片放映相关状态,并在状态变化时触发对应事件。
/// </summary>
void StartMonitoring();
/// <summary>
/// 停止监控 PowerPoint 的连接与事件,停止接收并处理与演示文稿和幻灯片放映相关的通知。
/// </summary>
void StopMonitoring();
/// <summary>
/// 重新加载或重建与 PowerPoint 的连接。
/// </summary>
/// <remarks>
/// 调用后实现应刷新内部连接与状态,必要时重建与 PowerPoint 的会话;此操作可能导致 IsConnected 变化并触发 PPTConnectionChanged 或其他相关事件(例如 SlideShowStateChanged)。
/// </remarks>
void ReloadConnection();
/// <summary>
/// 尝试启动当前演示文稿的放映模式。
/// </summary>
/// <returns><c>true</c> 如果放映已成功启动,<c>false</c> 否则。</returns>
bool TryStartSlideShow();
/// <summary>
/// 尝试结束当前正在进行的幻灯片放映。
/// </summary>
/// <returns><c>true</c> 如果放映已成功结束,<c>false</c> 否则。</returns>
bool TryEndSlideShow();
// 导航控制
bool TryNavigateToSlide(int slideNumber);
bool TryNavigateNext();
bool TryNavigatePrevious();
// 查询
int GetCurrentSlideNumber();
string GetPresentationName();
bool TryShowSlideNavigation();
object GetCurrentActivePresentation();
}
}
+6 -3
View File
@@ -71,6 +71,11 @@ namespace Ink_Canvas.Helpers
try
{
// 确保主窗口的InkCanvas保持Ink编辑模式,防止墨迹渐隐时切换到鼠标模式
if (_mainWindow.inkCanvas.EditingMode != InkCanvasEditingMode.Ink)
{
_mainWindow.inkCanvas.EditingMode = InkCanvasEditingMode.Ink;
}
// 记录墨迹的起点和终点
_strokeStartPoints[stroke] = startPoint;
@@ -259,7 +264,6 @@ namespace Ink_Canvas.Helpers
public void Enable()
{
IsEnabled = true;
LogHelper.WriteLogToFile("墨迹渐隐功能已启用");
}
/// <summary>
@@ -268,7 +272,6 @@ namespace Ink_Canvas.Helpers
public void Disable()
{
IsEnabled = false;
LogHelper.WriteLogToFile("墨迹渐隐功能已禁用");
}
#endregion
@@ -889,4 +892,4 @@ namespace Ink_Canvas.Helpers
}
#endregion
}
}
}
+246
View File
@@ -0,0 +1,246 @@
using System;
using System.Threading.Tasks;
using System.Windows.Ink;
namespace Ink_Canvas.Helpers
{
public sealed class InkRecognitionManager
{
private static InkRecognitionManager _instance;
private static readonly object _lock = new object();
private readonly object _initSync = new object();
private ModernInkProcessor _modernProcessor;
private bool _isModernSystemAvailable;
private bool _isInitialized;
public static InkRecognitionManager Instance
{
get
{
if (_instance == null)
{
lock (_lock)
{
if (_instance == null)
_instance = new InkRecognitionManager();
}
}
return _instance;
}
}
private InkRecognitionManager() { }
private void Initialize()
{
if (_isInitialized) return;
try
{
// 启动阶段只做能力探测,不做 WinRT 组件实例化(避免冷启动延迟)
_isModernSystemAvailable = WinRtInkShapeRecognizer.IsApiAvailable;
_isInitialized = true;
}
catch (Exception ex)
{
LogHelper.WriteLogToFile("墨迹识别管理器初始化失败: " + ex.Message, LogHelper.LogType.Error);
_isInitialized = false;
}
}
private void EnsureInitialized()
{
if (_isInitialized) return;
lock (_initSync)
{
if (_isInitialized) return;
Initialize();
}
}
private void EnsureModernAnalyzerInitialized()
{
if (_modernProcessor != null || !_isModernSystemAvailable) return;
lock (_initSync)
{
if (_modernProcessor != null || !_isModernSystemAvailable) return;
try
{
_modernProcessor ??= new ModernInkProcessor();
}
catch (Exception ex)
{
LogHelper.WriteLogToFile("WinRT 墨迹模块懒加载失败: " + ex.Message, LogHelper.LogType.Warning);
_isModernSystemAvailable = false;
_modernProcessor = null;
}
}
}
public Task<InkShapeRecognitionResult> RecognizeShapeAsync(
StrokeCollection strokes,
ShapeRecognitionEngineMode mode)
{
EnsureInitialized();
if (!_isInitialized || strokes == null || strokes.Count == 0)
return Task.FromResult(InkShapeRecognitionResult.Empty);
try
{
if (ShapeRecognitionRouter.ResolveUseWinRt(mode)
&& WinRtInkShapeRecognizer.IsApiAvailable)
{
return RecognizeShapeWinRtOnDispatcherContext(strokes);
}
var legacy = InkRecognizeHelper.RecognizeShapeIACore(strokes);
return Task.FromResult(InkRecognizeHelper.FromIACoreOrEmpty(legacy));
}
catch (Exception ex)
{
LogHelper.WriteLogToFile("墨迹形状识别失败: " + ex.Message, LogHelper.LogType.Error);
return Task.FromResult(InkShapeRecognitionResult.Empty);
}
}
private static async Task<InkShapeRecognitionResult> RecognizeShapeWinRtOnDispatcherContext(
StrokeCollection strokes)
{
return await WinRtInkShapeRecognizer.RecognizeShapeAsync(strokes).ConfigureAwait(true);
}
/// <param name="applyHandwritingBeautify">为 true 且走 WinRT 时,将识别成功的词替换为手写风格字体的轮廓墨迹(见设置中的字体列表)。</param>
/// <param name="handwritingFontFamilyList">逗号分隔的字体回退列表(WPF FontFamily);null 时使用内置默认。</param>
public Task<StrokeCollection> CorrectInkAsync(
StrokeCollection strokes,
ShapeRecognitionEngineMode mode,
bool applyHandwritingBeautify = false,
string handwritingFontFamilyList = null)
{
EnsureInitialized();
if (!_isInitialized)
{
LogHelper.WriteLogToFile("[手写体] CorrectInkAsync 跳过:InkRecognitionManager 未初始化。", LogHelper.LogType.Info);
return Task.FromResult(strokes);
}
if (strokes == null || strokes.Count == 0)
{
LogHelper.WriteLogToFile("[手写体] CorrectInkAsync 跳过:无笔画。", LogHelper.LogType.Info);
return Task.FromResult(strokes);
}
try
{
var useWinRt = ShapeRecognitionRouter.ResolveUseWinRt(mode);
if (!applyHandwritingBeautify)
{
LogHelper.WriteLogToFile(
"[手写体] CorrectInkAsync 跳过:未开启「识别转手写体字形」(applyHandwritingBeautify=false)。笔画数=" +
strokes.Count,
LogHelper.LogType.Info);
return Task.FromResult(strokes);
}
if (!useWinRt)
{
LogHelper.WriteLogToFile(
"[手写体] CorrectInkAsync 跳过:当前引擎非 WinRT(模式=" + mode + ")。笔画数=" + strokes.Count,
LogHelper.LogType.Info);
return Task.FromResult(strokes);
}
EnsureModernAnalyzerInitialized();
if (_modernProcessor == null)
{
LogHelper.WriteLogToFile(
"[手写体] CorrectInkAsync 跳过:ModernInkProcessor 未就绪(WinRT 初始化失败?)。笔画数=" +
strokes.Count,
LogHelper.LogType.Warning);
return Task.FromResult(strokes);
}
LogHelper.WriteLogToFile(
"[手写体] CorrectInkAsync 开始:笔画数=" + strokes.Count +
",字体=" + (string.IsNullOrWhiteSpace(handwritingFontFamilyList) ? "(默认)" : handwritingFontFamilyList.Trim()),
LogHelper.LogType.Info);
return WinRtHandwritingRecognizer.ConvertRecognizedTextToHandwritingInkAsync(strokes, handwritingFontFamilyList);
}
catch (Exception ex)
{
LogHelper.WriteLogToFile("墨迹纠正失败: " + ex.Message, LogHelper.LogType.Error);
return Task.FromResult(strokes);
}
}
/// <summary>
/// WinRT 手写体识别(需 Windows 10+ 及系统手写识别组件)。返回分词候选与包围框,供剪贴板或插件使用。
/// </summary>
public Task<HandwritingRecognitionResult> RecognizeHandwritingAsync(
StrokeCollection strokes,
ShapeRecognitionEngineMode mode)
{
EnsureInitialized();
if (!_isInitialized || strokes == null || strokes.Count == 0)
return Task.FromResult(HandwritingRecognitionResult.Empty);
try
{
if (!ShapeRecognitionRouter.ResolveUseWinRt(mode)
|| !WinRtHandwritingRecognizer.IsApiAvailable)
return Task.FromResult(HandwritingRecognitionResult.Empty);
return WinRtHandwritingRecognizer.RecognizeHandwritingAsync(strokes);
}
catch (Exception ex)
{
LogHelper.WriteLogToFile("手写识别失败: " + ex.Message, LogHelper.LogType.Error);
return Task.FromResult(HandwritingRecognitionResult.Empty);
}
}
public bool IsValidShapeType(string shapeName)
{
return !string.IsNullOrEmpty(shapeName)
&& (shapeName.Contains("Triangle") || shapeName.Contains("Circle")
|| shapeName.Contains("Rectangle") || shapeName.Contains("Diamond")
|| shapeName.Contains("Parallelogram") || shapeName.Contains("Square")
|| shapeName.Contains("Ellipse") || shapeName.Contains("Line")
|| shapeName.Contains("Arrow"));
}
public string GetSystemInfo()
{
return _isModernSystemAvailable
? $"现代化墨迹识别系统 (Windows Runtime API) - 进程架构: {Environment.Is64BitProcess}"
: $"传统墨迹识别系统 (IACore) - 进程架构: {Environment.Is64BitProcess}";
}
public void Dispose()
{
_modernProcessor?.Dispose();
_isInitialized = false;
}
}
internal sealed class ModernInkProcessor : IDisposable
{
public ModernInkProcessor()
{
if (!WinRtInkShapeRecognizer.IsApiAvailable)
throw new InvalidOperationException("WinRT 墨迹分析需要 Windows 10 及以上。");
}
public Task<InkShapeRecognitionResult> RecognizeShapeAsync(StrokeCollection strokes)
{
return WinRtInkShapeRecognizer.RecognizeShapeAsync(strokes);
}
public void Dispose()
{
}
}
}
+134 -10
View File
@@ -1,4 +1,5 @@
using System.Linq;
using System.Linq;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Ink;
using System.Windows.Media;
@@ -7,8 +8,8 @@ namespace Ink_Canvas.Helpers
{
public class InkRecognizeHelper
{
//识别形状
public static ShapeRecognizeResult RecognizeShape(StrokeCollection strokes)
/// <summary>IACore / IAWinFX 形状识别(典型用于 32 位进程)。</summary>
public static ShapeRecognizeResult RecognizeShapeIACore(StrokeCollection strokes)
{
if (strokes == null || strokes.Count == 0)
return default;
@@ -25,35 +26,158 @@ namespace Ink_Canvas.Helpers
var alternates = analyzer.GetAlternates();
if (alternates.Count > 0)
{
while ((!alternates[0].Strokes.Contains(strokes.Last()) ||
!IsContainShapeType(((InkDrawingNode)alternates[0].AlternateNodes[0]).GetShapeName()))
&& strokesCount >= 2)
while (strokesCount >= 2)
{
var alt0 = alternates[0];
if (alt0?.AlternateNodes == null || alt0.AlternateNodes.Count == 0)
break;
var drawNode = alt0.AlternateNodes[0] as InkDrawingNode;
if (drawNode == null)
break;
var shapeOk = IsContainShapeType(drawNode.GetShapeName());
if (alt0.Strokes.Contains(strokes.Last()) && shapeOk)
break;
analyzer.RemoveStroke(strokes[strokes.Count - strokesCount]);
strokesCount--;
sfsaf = analyzer.Analyze();
if (sfsaf.Successful)
{
alternates = analyzer.GetAlternates();
}
else
break;
if (alternates.Count == 0)
break;
}
if (alternates.Count > 0)
{
var altFinal = alternates[0];
if (altFinal?.AlternateNodes != null && altFinal.AlternateNodes.Count > 0)
analysisAlternate = altFinal;
}
analysisAlternate = alternates[0];
}
}
analyzer.Dispose();
if (analysisAlternate != null && analysisAlternate.AlternateNodes.Count > 0)
if (analysisAlternate != null && analysisAlternate.AlternateNodes != null && analysisAlternate.AlternateNodes.Count > 0)
{
var node = analysisAlternate.AlternateNodes[0] as InkDrawingNode;
if (node == null)
return default;
return new ShapeRecognizeResult(node.Centroid, node.HotPoints, analysisAlternate, node);
}
return default;
}
/// <summary>兼容旧调用:等价于 <see cref="RecognizeShapeIACore"/>。</summary>
public static ShapeRecognizeResult RecognizeShape(StrokeCollection strokes) =>
RecognizeShapeIACore(strokes);
/// <summary>按设置选择 WinRT<see cref="InkRecognitionManager"/>)或 IACoreWinRT 请用 <see cref="RecognizeShapeUnifiedAsync"/>。</summary>
public static InkShapeRecognitionResult RecognizeShapeUnified(
StrokeCollection strokes,
ShapeRecognitionEngineMode mode)
{
if (strokes == null || strokes.Count == 0)
return InkShapeRecognitionResult.Empty;
if (ShapeRecognitionRouter.ResolveUseWinRt(mode))
return InkShapeRecognitionResult.Empty;
var legacy = RecognizeShapeIACore(strokes);
return FromIACoreOrEmpty(legacy);
}
/// <summary>与 CE 反编译版 <c>InkRecognitionManager.RecognizeShapeAsync</c> 对齐的统一入口。</summary>
public static Task<InkShapeRecognitionResult> RecognizeShapeUnifiedAsync(
StrokeCollection strokes,
ShapeRecognitionEngineMode mode)
{
if (strokes == null || strokes.Count == 0)
return Task.FromResult(InkShapeRecognitionResult.Empty);
return InkRecognitionManager.Instance.RecognizeShapeAsync(strokes, mode);
}
public static void WarmupShapeRecognition(ShapeRecognitionEngineMode mode)
{
try
{
if (ShapeRecognitionRouter.ResolveUseWinRt(mode))
{
WinRtInkShapeRecognizer.Warmup();
WinRtHandwritingRecognizer.Warmup();
}
else
RecognizeShapeIACore(new StrokeCollection());
}
catch
{
// 预热失败不影响启动
}
}
/// <summary>WinRT 手写识别(Windows 10+)。</summary>
public static Task<HandwritingRecognitionResult> RecognizeHandwritingUnifiedAsync(
StrokeCollection strokes,
ShapeRecognitionEngineMode mode) =>
InkRecognitionManager.Instance.RecognizeHandwritingAsync(strokes, mode);
/// <summary>WinRT 下将识别成功的词替换为手写体字形墨迹;是否应用由设置「WinRT 识别转手写体字形」控制。</summary>
public static Task<StrokeCollection> CorrectHandwritingStrokesUnifiedAsync(
StrokeCollection strokes,
ShapeRecognitionEngineMode mode) =>
InkRecognitionManager.Instance.CorrectInkAsync(
strokes,
mode,
MainWindow.Settings?.InkToShape?.EnableWinRtHandwritingStrokeBeautify ?? false,
MainWindow.Settings?.InkToShape?.HandwritingCorrectionFontFamily);
/// <summary>显式指定是否应用手写体字形替换(忽略开关);字体仍从设置读取。</summary>
public static Task<StrokeCollection> CorrectHandwritingStrokesUnifiedAsync(
StrokeCollection strokes,
ShapeRecognitionEngineMode mode,
bool applyHandwritingBeautify) =>
InkRecognitionManager.Instance.CorrectInkAsync(
strokes,
mode,
applyHandwritingBeautify,
MainWindow.Settings?.InkToShape?.HandwritingCorrectionFontFamily);
internal static InkShapeRecognitionResult FromIACoreOrEmpty(ShapeRecognizeResult legacy)
{
if (legacy?.InkDrawingNode == null)
return InkShapeRecognitionResult.Empty;
var node = legacy.InkDrawingNode;
var shape = node.GetShape();
if (shape == null)
return InkShapeRecognitionResult.Empty;
var hot = ClonePointCollection(node.HotPoints);
return new InkShapeRecognitionResult(
node.GetShapeName(),
legacy.Centroid,
hot,
shape.Width,
shape.Height,
node.Strokes);
}
private static PointCollection ClonePointCollection(PointCollection src)
{
var dst = new PointCollection();
if (src == null) return dst;
foreach (System.Windows.Point p in src)
dst.Add(p);
return dst;
}
public static bool IsContainShapeType(string name)
{
if (string.IsNullOrEmpty(name))
return false;
if (name.Contains("Triangle") || name.Contains("Circle") ||
name.Contains("Rectangle") || name.Contains("Diamond") ||
name.Contains("Parallelogram") || name.Contains("Square")
+85
View File
@@ -0,0 +1,85 @@
using OSVersionExtension;
using System.Windows;
using System.Windows.Ink;
using System.Windows.Media;
namespace Ink_Canvas.Helpers
{
/// <summary>墨迹形状识别后端:自动 / IACore / WinRT。</summary>
public enum ShapeRecognitionEngineMode
{
Auto = 0,
IACore = 1,
WinRT = 2,
}
public static class ShapeRecognitionRouter
{
/// <summary>
/// 自动模式:在 Windows 10 及以上系统默认使用 WinRT,否则使用 IACore。
/// </summary>
public static bool ResolveUseWinRt(ShapeRecognitionEngineMode mode)
{
if (mode == ShapeRecognitionEngineMode.WinRT) return true;
if (mode == ShapeRecognitionEngineMode.IACore) return false;
return OSVersion.GetOperatingSystem() >= OSVersionExtension.OperatingSystem.Windows10;
}
public static bool ShouldRunShapeRecognition(bool inkToShapeEnabled, ShapeRecognitionEngineMode mode)
{
if (!inkToShapeEnabled) return false;
if (ResolveUseWinRt(mode))
return OSVersion.GetOperatingSystem() >= OSVersionExtension.OperatingSystem.Windows10;
return true;
}
public static ShapeRecognitionEngineMode FromSettingsInt(int value)
{
if (value == (int)ShapeRecognitionEngineMode.IACore) return ShapeRecognitionEngineMode.IACore;
if (value == (int)ShapeRecognitionEngineMode.WinRT) return ShapeRecognitionEngineMode.WinRT;
return ShapeRecognitionEngineMode.Auto;
}
}
/// <summary>与具体识别后端无关的形状识别结果,供统一纠正模块消费。</summary>
public sealed class InkShapeRecognitionResult
{
public static readonly InkShapeRecognitionResult Empty = new InkShapeRecognitionResult();
private InkShapeRecognitionResult()
{
IsSuccess = false;
ShapeName = string.Empty;
Centroid = new Point();
HotPoints = new PointCollection();
StrokesToRemove = new StrokeCollection();
}
public InkShapeRecognitionResult(
string shapeName,
Point centroid,
PointCollection hotPoints,
double shapeWidth,
double shapeHeight,
StrokeCollection strokesToRemove)
{
ShapeName = shapeName ?? string.Empty;
Centroid = centroid;
HotPoints = hotPoints ?? new PointCollection();
ShapeWidth = shapeWidth;
ShapeHeight = shapeHeight;
StrokesToRemove = strokesToRemove ?? new StrokeCollection();
IsSuccess = StrokesToRemove.Count > 0
&& !string.IsNullOrEmpty(ShapeName)
&& ShapeName != "Drawing";
}
public bool IsSuccess { get; }
public string ShapeName { get; }
public Point Centroid { get; set; }
public PointCollection HotPoints { get; }
public double ShapeWidth { get; }
public double ShapeHeight { get; }
public StrokeCollection StrokesToRemove { get; }
}
}
+22 -13
View File
@@ -71,37 +71,46 @@ namespace Ink_Canvas.Helpers
}
/// <summary>
/// 应用质量设置
/// 应用质量设置
/// </summary>
public void ApplyQualitySettings()
{
// 保存用户设置的异步处理偏好
bool userAsyncPreference = UseAsyncProcessing;
switch (Quality)
{
case SmoothingQuality.Performance:
SmoothingStrength = 0.2;
ResampleInterval = 4.0;
InterpolationSteps = 6;
SmoothingStrength = 0.15;
ResampleInterval = 5.0;
InterpolationSteps = 4;
UseAdaptiveInterpolation = false;
CurveTension = 0.2;
CurveTension = 0.15;
MaxConcurrentTasks = Math.Max(1, Environment.ProcessorCount / 2);
UseHardwareAcceleration = true;
UseAsyncProcessing = userAsyncPreference;
break;
case SmoothingQuality.Balanced:
SmoothingStrength = 0.4;
ResampleInterval = 2.5;
InterpolationSteps = 12;
SmoothingStrength = 0.3;
ResampleInterval = 3.0;
InterpolationSteps = 8;
UseAdaptiveInterpolation = true;
CurveTension = 0.3;
CurveTension = 0.25;
MaxConcurrentTasks = Environment.ProcessorCount;
UseHardwareAcceleration = true;
UseAsyncProcessing = userAsyncPreference;
break;
case SmoothingQuality.Quality:
SmoothingStrength = 0.6;
ResampleInterval = 1.5;
InterpolationSteps = 20;
SmoothingStrength = 0.5;
ResampleInterval = 2.0;
InterpolationSteps = 15;
UseAdaptiveInterpolation = true;
CurveTension = 0.4;
CurveTension = 0.35;
MaxConcurrentTasks = Environment.ProcessorCount;
UseHardwareAcceleration = true;
UseAsyncProcessing = userAsyncPreference;
break;
}
}
+57
View File
@@ -0,0 +1,57 @@
using Ink_Canvas.Properties;
using System.Globalization;
using System.Threading;
namespace Ink_Canvas.Helpers
{
/// <summary>
/// i18n 本地化辅助:设置/获取当前 UI 语言,便于后续从配置切换语言。
/// </summary>
public static class LocalizationHelper
{
/// <summary>
/// 当前 UI 语言(如 "zh-CN", "en-US")。未设置时使用系统当前 UI 语言。
/// </summary>
public static CultureInfo CurrentCulture
{
get => Thread.CurrentThread.CurrentUICulture;
set
{
if (value == null) return;
Thread.CurrentThread.CurrentUICulture = value;
Strings.Culture = value;
}
}
/// <summary>
/// 使用指定语言名称设置当前 UI 语言(如 "zh-CN", "en-US")。
/// 若名称无效则保持当前语言不变。
/// </summary>
public static bool TrySetCulture(string cultureName)
{
try
{
if (string.IsNullOrWhiteSpace(cultureName))
{
CurrentCulture = CultureInfo.InstalledUICulture;
return true;
}
var culture = CultureInfo.GetCultureInfo(cultureName);
CurrentCulture = culture;
return true;
}
catch
{
return false;
}
}
/// <summary>
/// 获取本地化字符串。优先使用强类型属性,未知键时用此方法。
/// </summary>
public static string GetString(string key)
{
return Strings.GetString(key);
}
}
}
+44 -12
View File
@@ -1,4 +1,4 @@
using System;
using System;
using System.Diagnostics;
using System.IO;
using System.Threading;
@@ -29,6 +29,11 @@ namespace Ink_Canvas.Helpers
WriteLogToFile(msg, LogType.Error);
}
/// <summary>
/// 将一条日志消息记录到应用的日志文件中(可能是单一日志文件或按启动时间存档的文件),同时在日志条目中包含时间戳、线程 ID 和调用者信息,并遵循应用的日志设置。
/// </summary>
/// <param name="str">要记录的日志文本消息。</param>
/// <param name="logType">日志的类型/等级,用于在日志条目中标识(例如 Info、Error、Warning 等)。</param>
public static void WriteLogToFile(string str, LogType logType = LogType.Info)
{
// 检查日志是否启用
@@ -62,7 +67,7 @@ namespace Ink_Canvas.Helpers
if (!Directory.Exists(App.RootPath))
{
Directory.CreateDirectory(App.RootPath);
ProcessProtectionManager.WithWriteAccess(App.RootPath, () => Directory.CreateDirectory(App.RootPath));
}
var threadId = Thread.CurrentThread.ManagedThreadId;
@@ -78,14 +83,31 @@ namespace Ink_Canvas.Helpers
}
}
string logLine = string.Format("{0} [T{1}] [{2}] [{3}] {4}", DateTime.Now.ToString("O"), threadId, strLogType, callerInfo, str);
using (StreamWriter sw = new StreamWriter(file, true))
DebugConsoleManager.WriteLine(logLine);
ProcessProtectionManager.WithWriteAccess(file, () =>
{
sw.WriteLine(logLine);
}
using (StreamWriter sw = new StreamWriter(file, true))
{
sw.WriteLine(logLine);
}
});
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"[LogHelper] WriteLogToFile failed: {ex.Message}");
}
catch { }
}
/// <summary>
/// 检查指定日志文件夹的总大小,并在超过 MaxLogsFolderSizeBytes 时删除该文件夹下的所有文件并记录清理日志。
/// </summary>
/// <param name="logsPath">要检查和清理的日志文件夹路径。</param>
/// <remarks>
/// - 如果目录不存在则直接返回。
/// - 当总大小超过 MaxLogsFolderSizeBytes 时,会尝试删除目录下的每个文件(单个删除失败将被忽略)。
/// - 清理完成后会向该目录下的 Log_{AppStartTime}.txt 写入一条带有时间戳和 [Cleanup] 标签的记录。
/// - 方法内部捕获并忽略所有异常以避免影响调用者流程。
/// </remarks>
private static void CheckAndCleanLogsFolder(string logsPath)
{
try
@@ -111,18 +133,28 @@ namespace Ink_Canvas.Helpers
{
file.Delete();
}
catch { }
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"[LogHelper] Delete log file failed: {ex.Message}");
}
}
// 记录清理操作
string cleanupMessage = $"Logs folder exceeded size limit ({totalSize / 1024.0 / 1024.0:F2} MB > {MaxLogsFolderSizeBytes / 1024.0 / 1024.0:F2} MB). Folder cleaned.";
using (StreamWriter sw = new StreamWriter(Path.Combine(logsPath, $"Log_{AppStartTime}.txt"), true))
var logFile = Path.Combine(logsPath, $"Log_{AppStartTime}.txt");
ProcessProtectionManager.WithWriteAccess(logFile, () =>
{
sw.WriteLine($"{DateTime.Now:O} [Cleanup] {cleanupMessage}");
}
using (StreamWriter sw = new StreamWriter(logFile, true))
{
sw.WriteLine($"{DateTime.Now:O} [Cleanup] {cleanupMessage}");
}
});
}
}
catch { }
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"[LogHelper] CheckAndCleanLogsFolder failed: {ex.Message}");
}
}
internal static void WriteLogToFile(string v, object warning)
@@ -139,4 +171,4 @@ namespace Ink_Canvas.Helpers
Warning
}
}
}
}
+149 -20
View File
@@ -1,4 +1,5 @@
using System;
using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Ink;
using System.Windows.Input;
@@ -8,27 +9,53 @@ namespace Ink_Canvas.Helpers
{
public class VisualCanvas : FrameworkElement
{
private readonly List<DrawingVisual> _visuals = new List<DrawingVisual>();
protected override Visual GetVisualChild(int index)
{
return Visual;
if (index < 0 || index >= _visuals.Count)
throw new ArgumentOutOfRangeException(nameof(index));
return _visuals[index];
}
protected override int VisualChildrenCount => 1;
protected override int VisualChildrenCount => _visuals.Count;
public VisualCanvas(DrawingVisual visual)
public VisualCanvas()
{
Visual = visual;
CacheMode = new BitmapCache();
RenderOptions.SetBitmapScalingMode(this, BitmapScalingMode.HighQuality);
RenderOptions.SetEdgeMode(this, EdgeMode.Aliased);
RenderOptions.SetCachingHint(this, CachingHint.Cache);
}
public void AddVisual(DrawingVisual visual)
{
if (visual == null) return;
_visuals.Add(visual);
AddVisualChild(visual);
}
public void Clear()
{
foreach (var visual in _visuals)
{
RemoveVisualChild(visual);
}
_visuals.Clear();
}
public DrawingVisual Visual { get; }
public IReadOnlyList<DrawingVisual> Visuals => _visuals;
}
/// <summary>
/// 用于显示笔迹的类
/// 用于显示笔迹的类
/// </summary>
public class StrokeVisual : DrawingVisual
public class StrokeVisual
{
private int _lastDrawnPointCount = 0;
private const int INCREMENTAL_DRAW_THRESHOLD = 2;
private VisualCanvas _visualCanvas;
/// <summary>
/// 创建显示笔迹的类
/// </summary>
@@ -43,7 +70,7 @@ namespace Ink_Canvas.Helpers
}
/// <summary>
/// 创建显示笔迹的类
/// 创建显示笔迹的类
/// </summary>
/// <param name="drawingAttributes"></param>
public StrokeVisual(DrawingAttributes drawingAttributes)
@@ -52,12 +79,20 @@ namespace Ink_Canvas.Helpers
}
/// <summary>
/// 设置或获取显示的笔迹
/// 设置或获取显示的笔迹
/// </summary>
public Stroke Stroke { set; get; }
/// <summary>
/// 在笔迹中添加点
/// 设置关联的VisualCanvas
/// </summary>
public void SetVisualCanvas(VisualCanvas visualCanvas)
{
_visualCanvas = visualCanvas;
}
/// <summary>
/// 在笔迹中添加点
/// </summary>
/// <param name="point"></param>
public void Add(StylusPoint point)
@@ -74,25 +109,119 @@ namespace Ink_Canvas.Helpers
}
/// <summary>
/// 重新画出笔迹
/// 绘制点段到新的DrawingVisual
/// </summary>
private static double PressureToVisualScale(float pressureFactor, bool ignorePressure)
{
if (ignorePressure)
return 1.0;
// 与 WPF 墨迹观感接近:0.5 为标称,压低变细、抬高变粗(预览此前固定 Pen 宽,等同忽略压感)
return Math.Max(0.22, Math.Min(2.1, 0.42 + 1.16 * pressureFactor));
}
private void DrawSegmentToNewVisual(int startIndex, int endIndex)
{
if (Stroke == null || Stroke.StylusPoints.Count == 0 || _visualCanvas == null) return;
if (startIndex >= endIndex || startIndex < 0 || endIndex > Stroke.StylusPoints.Count) return;
var points = Stroke.StylusPoints;
var drawingAttributes = Stroke.DrawingAttributes;
var ignorePressure = drawingAttributes.IgnorePressure;
// 创建新的DrawingVisual用于绘制这个点段
var segmentVisual = new DrawingVisual();
RenderOptions.SetBitmapScalingMode(segmentVisual, BitmapScalingMode.HighQuality);
RenderOptions.SetEdgeMode(segmentVisual, EdgeMode.Aliased);
RenderOptions.SetCachingHint(segmentVisual, CachingHint.Cache);
using (var dc = segmentVisual.RenderOpen())
{
// 绘制指定范围内的点段
if (endIndex - startIndex >= 2)
{
// 多个点,绘制线段
for (int i = startIndex; i < endIndex - 1 && i < points.Count - 1; i++)
{
var startPoint = new Point(points[i].X, points[i].Y);
var endPoint = new Point(points[i + 1].X, points[i + 1].Y);
var s0 = PressureToVisualScale(points[i].PressureFactor, ignorePressure);
var s1 = PressureToVisualScale(points[i + 1].PressureFactor, ignorePressure);
var thickness = Math.Max(0.35, (drawingAttributes.Width * s0 + drawingAttributes.Width * s1) / 2.0);
var pen = new Pen(new SolidColorBrush(drawingAttributes.Color), thickness)
{
StartLineCap = PenLineCap.Round,
EndLineCap = PenLineCap.Round,
LineJoin = PenLineJoin.Round
};
dc.DrawLine(pen, startPoint, endPoint);
}
}
else if (endIndex - startIndex == 1 && startIndex < points.Count)
{
// 只有一个点,绘制圆点
var brush = new SolidColorBrush(drawingAttributes.Color);
var point = points[startIndex];
var s = PressureToVisualScale(point.PressureFactor, ignorePressure);
dc.DrawEllipse(brush, null, new Point(point.X, point.Y),
drawingAttributes.Width * s / 2, drawingAttributes.Height * s / 2);
}
}
// 将新的DrawingVisual添加到VisualCanvas中
_visualCanvas.AddVisual(segmentVisual);
}
/// <summary>
/// 重新画出笔迹
/// </summary>
public void Redraw()
{
try
if (Stroke == null || _visualCanvas == null) return;
var currentPointCount = Stroke.StylusPoints.Count;
if (currentPointCount == 0) return;
// 计算新增的点数
int newPointCount = currentPointCount - _lastDrawnPointCount;
// 如果新增点数达到阈值,才进行增量绘制
if (newPointCount >= INCREMENTAL_DRAW_THRESHOLD || _lastDrawnPointCount == 0)
{
using (var dc = RenderOpen())
try
{
Stroke.Draw(dc);
if (_lastDrawnPointCount == 0)
{
// 首次绘制:绘制所有点
DrawSegmentToNewVisual(0, currentPointCount);
_lastDrawnPointCount = currentPointCount;
}
else
{
// 从上次绘制的最后一个点开始
int startIndex = Math.Max(0, _lastDrawnPointCount - 1);
DrawSegmentToNewVisual(startIndex, currentPointCount);
_lastDrawnPointCount = currentPointCount;
}
}
catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); }
}
catch { }
}
/// <summary>
/// 强制重绘
/// </summary>
public void ForceRedraw()
{
if (_visualCanvas != null)
{
_visualCanvas.Clear();
}
_lastDrawnPointCount = 0;
Redraw();
}
private readonly DrawingAttributes _drawingAttributes;
public static implicit operator Stroke(StrokeVisual v)
{
throw new NotImplementedException();
}
}
}
+26
View File
@@ -0,0 +1,26 @@
using System;
using System.Runtime.InteropServices;
namespace Ink_Canvas.Helpers
{
/// <summary>
/// .NET Core / 5+ 未提供 <see cref="Marshal.GetActiveObject"/>,通过 OLE 实现等效行为。
/// </summary>
internal static class OleActiveObject
{
[DllImport("ole32.dll", CharSet = CharSet.Unicode, ExactSpelling = true)]
private static extern int CLSIDFromProgID(string lpszProgId, out Guid lpclsid);
[DllImport("oleaut32.dll", PreserveSig = true)]
private static extern int GetActiveObject(ref Guid rclsid, IntPtr pvReserved, [MarshalAs(UnmanagedType.IUnknown)] out object ppunk);
public static object GetActiveObject(string progId)
{
int hr = CLSIDFromProgID(progId, out Guid clsid);
Marshal.ThrowExceptionForHR(hr);
hr = GetActiveObject(ref clsid, IntPtr.Zero, out object obj);
Marshal.ThrowExceptionForHR(hr);
return obj;
}
}
}
+354 -164
View File
@@ -1,14 +1,13 @@
using Microsoft.Office.Interop.PowerPoint;
using Microsoft.Office.Interop.PowerPoint;
using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;
using System.Runtime.InteropServices;
using System.Windows.Ink;
namespace Ink_Canvas.Helpers
{
/// <summary>
/// PPT墨迹管理器 - 负责PPT中墨迹的保存、加载和同步
/// PPT墨迹管理器 - 负责按幻灯片保存/加载墨迹、自动保存与内存管理。
/// </summary>
public class PPTInkManager : IDisposable
{
@@ -20,7 +19,8 @@ namespace Ink_Canvas.Helpers
#region Private Fields
private MemoryStream[] _memoryStreams;
private int _maxSlides = 100;
private const int DefaultMaxSlides = 100;
private int _maxSlides = DefaultMaxSlides;
private string _currentPresentationId = "";
private readonly object _lockObject = new object();
private bool _disposed;
@@ -29,53 +29,92 @@ namespace Ink_Canvas.Helpers
private DateTime _inkLockUntil = DateTime.MinValue;
private int _lockedSlideIndex = -1;
private const int InkLockMilliseconds = 500;
// 添加快速切换保护机制
private DateTime _lastSwitchTime = DateTime.MinValue;
private int _lastSwitchSlideIndex = -1;
private const int MinSwitchIntervalMs = 100;
private long _totalMemoryUsage;
private const long MaxMemoryUsageBytes = 100 * 1024 * 1024; // 100MB
private DateTime _lastMemoryCleanup = DateTime.MinValue;
private const int MemoryCleanupIntervalMinutes = 5;
private const string StrokeFileExtension = ".icstk";
#endregion
#region Constructor
/// <summary>
/// 初始化 PPTInkManager 实例并为内部内存流分配初始容量以跟踪默认最大幻灯片数加上备用槽位。
/// </summary>
public PPTInkManager()
{
InitializeMemoryStreams();
InitializeMemoryStreams(DefaultMaxSlides + 2);
}
private void InitializeMemoryStreams()
/// <summary>
/// 根据指定容量初始化用于存储每页墨迹的内存流数组。
/// </summary>
/// <param name="capacity">期望的数组容量;如果小于 2,则会使用 2 作为最小容量。</param>
private void InitializeMemoryStreams(int capacity)
{
_memoryStreams = new MemoryStream[_maxSlides + 2];
if (_memoryStreams != null)
{
for (int i = 0; i < _memoryStreams.Length; i++)
{
try { _memoryStreams[i]?.Dispose(); } catch (Exception ex) { LogHelper.WriteLogToFile($"InitializeMemoryStreams 释放内存流 {i} 失败: {ex}", LogHelper.LogType.Warning); }
}
}
_memoryStreams = new MemoryStream[Math.Max(2, capacity)];
}
#endregion
#region Public Methods
/// <summary>
/// 初始化新的演示文稿
/// </summary>
/// <remarks>
/// 为新的或当前的演示文稿初始化墨迹管理器的内部状态。
/// 方法会清除所有内存中的笔迹数据,重置墨迹写入锁与快速切换追踪,并根据演示文稿的幻灯片数量分配内部内存缓冲区。
/// 如果已启用自动保存且设置了 <see cref="AutoSaveLocation"/>,则会尝试加载磁盘上的已保存墨迹文件。
/// </remarks>
/// <param name="presentation">要初始化的 PowerPoint Presentation 实例;为 null 时方法不执行任何操作并直接返回。</param>
public void InitializePresentation(Presentation presentation)
{
ThrowIfDisposed();
if (presentation == null) return;
lock (_lockObject)
{
try
{
// 完全清理之前的墨迹状态
ClearAllStrokes();
// 重置墨迹锁定状态
ClearAllStrokesInternal();
_inkLockUntil = DateTime.MinValue;
_lockedSlideIndex = -1;
_lastSwitchSlideIndex = -1;
_lastSwitchTime = DateTime.MinValue;
// 生成演示文稿唯一标识符
_currentPresentationId = GeneratePresentationId(presentation);
// 重新初始化内存流数组
var slideCount = presentation.Slides.Count;
_memoryStreams = new MemoryStream[slideCount + 2];
// 如果启用自动保存,尝试加载已保存的墨迹
if (IsAutoSaveEnabled && !string.IsNullOrEmpty(AutoSaveLocation))
int slideCount = 0;
try
{
LoadSavedStrokes();
slideCount = presentation.Slides.Count;
}
catch (COMException comEx)
{
uint hr = (uint)comEx.HResult;
if (hr == 0x80048010) return;
throw;
}
LogHelper.WriteLogToFile($"已初始化演示文稿墨迹管理: {presentation.Name}, 幻灯片数量: {slideCount}", LogHelper.LogType.Trace);
int capacity = slideCount + 2;
_maxSlides = Math.Max(_maxSlides, slideCount);
_memoryStreams = new MemoryStream[capacity];
if (IsAutoSaveEnabled && !string.IsNullOrEmpty(AutoSaveLocation))
LoadSavedStrokes();
}
catch (Exception ex)
{
@@ -87,33 +126,25 @@ namespace Ink_Canvas.Helpers
/// <summary>
/// 保存当前页面的墨迹
/// </summary>
/// <remarks>
/// 将指定幻灯片的墨迹保存到内部内存缓存,并在必要时触发内存清理。
/// </remarks>
/// <param name="slideIndex">要保存的幻灯片索引(从 1 开始)。方法在索引小于或等于 0 时不执行任何操作。</param>
/// <param name="strokes">要保存的墨迹集合;为 null 时方法不执行任何操作。</param>
public void SaveCurrentSlideStrokes(int slideIndex, StrokeCollection strokes)
{
ThrowIfDisposed();
if (slideIndex <= 0 || strokes == null) return;
lock (_lockObject)
{
try
{
// 检查墨迹锁定
if (!CanWriteInk(slideIndex))
{
LogHelper.WriteLogToFile($"墨迹写入被锁定,当前页:{slideIndex},锁定页:{_lockedSlideIndex}", LogHelper.LogType.Warning);
return;
}
if (!CanWriteInk(slideIndex)) return;
if (slideIndex >= _memoryStreams.Length) return;
if (slideIndex < _memoryStreams.Length)
{
var ms = new MemoryStream();
strokes.Save(ms);
ms.Position = 0;
// 释放旧的内存流
_memoryStreams[slideIndex]?.Dispose();
_memoryStreams[slideIndex] = ms;
LogHelper.WriteLogToFile($"已保存第{slideIndex}页墨迹,大小: {ms.Length} bytes", LogHelper.LogType.Trace);
}
ReplaceSlideStream(slideIndex, strokes);
CheckAndPerformMemoryCleanup();
}
catch (Exception ex)
{
@@ -122,11 +153,45 @@ namespace Ink_Canvas.Helpers
}
}
/// <summary>
/// 强制保存指定页墨迹到内存(不受锁定限制)。用于放映结束前保存当前画布到当前页。
/// </summary>
/// <remarks>
/// 强制将指定幻灯片的墨迹保存到内部内存缓存,覆盖该幻灯片已有的墨迹数据。
/// </remarks>
/// <param name="slideIndex">要保存的幻灯片索引(从 1 开始)。</param>
/// <param name="strokes">要保存的墨迹集合,不能为空。</param>
public void ForceSaveSlideStrokes(int slideIndex, StrokeCollection strokes)
{
ThrowIfDisposed();
if (slideIndex <= 0 || strokes == null) return;
lock (_lockObject)
{
try
{
if (slideIndex >= _memoryStreams.Length) return;
ReplaceSlideStream(slideIndex, strokes);
LogHelper.WriteLogToFile($"已强制保存第{slideIndex}页墨迹", LogHelper.LogType.Trace);
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"强制保存第{slideIndex}页墨迹失败: {ex}", LogHelper.LogType.Error);
}
}
}
/// <summary>
/// 加载指定页面的墨迹
/// </summary>
/// <remarks>
/// 加载并返回指定幻灯片的墨迹集合。
/// </remarks>
/// <param name="slideIndex">要加载的幻灯片索引(从1开始)。</param>
/// <returns>包含指定幻灯片的墨迹的 StrokeCollection;如果该幻灯片没有已保存的墨迹或加载失败,则返回空的 StrokeCollection。</returns>
public StrokeCollection LoadSlideStrokes(int slideIndex)
{
ThrowIfDisposed();
if (slideIndex <= 0) return new StrokeCollection();
lock (_lockObject)
@@ -136,9 +201,7 @@ namespace Ink_Canvas.Helpers
if (slideIndex < _memoryStreams.Length && _memoryStreams[slideIndex] != null && _memoryStreams[slideIndex].Length > 0)
{
_memoryStreams[slideIndex].Position = 0;
var strokes = new StrokeCollection(_memoryStreams[slideIndex]);
LogHelper.WriteLogToFile($"已加载第{slideIndex}页墨迹,笔画数量: {strokes.Count}", LogHelper.LogType.Trace);
return strokes;
return new StrokeCollection(_memoryStreams[slideIndex]);
}
}
catch (Exception ex)
@@ -153,33 +216,30 @@ namespace Ink_Canvas.Helpers
/// <summary>
/// 切换到指定页面并加载墨迹
/// </summary>
/// <remarks>
/// 切换到指定幻灯片并返回该幻灯片的已加载笔迹集合。
/// </remarks>
/// <param name="slideIndex">要切换到的幻灯片索引(从 1 开始)。</param>
/// <param name="currentStrokes">可选的当前笔迹集合,用于在切换时提供当前画面状态。</param>
/// <returns>`StrokeCollection`:指定幻灯片已加载的笔迹集合;若加载失败则返回一个空的 `StrokeCollection`。</returns>
public StrokeCollection SwitchToSlide(int slideIndex, StrokeCollection currentStrokes = null)
{
ThrowIfDisposed();
lock (_lockObject)
{
try
{
// 如果有当前墨迹,先保存到正确的页面
if (currentStrokes != null && currentStrokes.Count > 0)
var now = DateTime.Now;
if (now - _lastSwitchTime < TimeSpan.FromMilliseconds(MinSwitchIntervalMs) && _lastSwitchSlideIndex == slideIndex)
{
// 确定要保存的页面索引
int saveToSlideIndex = _lockedSlideIndex > 0 ? _lockedSlideIndex : slideIndex;
// 确保页面索引有效
if (saveToSlideIndex > 0 && saveToSlideIndex < _memoryStreams.Length)
{
SaveCurrentSlideStrokes(saveToSlideIndex, currentStrokes);
LogHelper.WriteLogToFile($"已保存第{saveToSlideIndex}页墨迹,墨迹数量: {currentStrokes.Count}", LogHelper.LogType.Trace);
}
LogHelper.WriteLogToFile($"快速切换保护:忽略重复请求 页{slideIndex}", LogHelper.LogType.Trace);
return LoadSlideStrokes(slideIndex);
}
// 设置墨迹锁定
LockInkForSlide(slideIndex);
// 加载新页面的墨迹
var newStrokes = LoadSlideStrokes(slideIndex);
LogHelper.WriteLogToFile($"已切换到第{slideIndex}页,加载墨迹数量: {newStrokes.Count}", LogHelper.LogType.Trace);
StrokeCollection newStrokes = LoadSlideStrokes(slideIndex);
_lastSwitchTime = now;
_lastSwitchSlideIndex = slideIndex;
return newStrokes;
}
catch (Exception ex)
@@ -193,68 +253,66 @@ namespace Ink_Canvas.Helpers
/// <summary>
/// 保存所有墨迹到文件
/// </summary>
public void SaveAllStrokesToFile(Presentation presentation)
/// <remarks>
/// 将内存中当前演示文稿的每页墨迹保存到磁盘,并根据情况写入当前播放位置文件。
/// 仅在 IsAutoSaveEnabled 为真且 AutoSaveLocation 已设置时执行。会在演示文稿专属文件夹中写入按页编号的墨迹文件(带 `.icstk` 扩展名)和可选的 Position 文件。遇到特定 COM 错误(HRESULT 0x80048010)时会中止保存当前幻灯片计数读取而不抛出异常;单页保存失败会记录错误并继续处理其他页。
/// </remarks>
/// <param name="presentation">要保存墨迹的 PowerPoint 演示文稿对象。</param>
/// <param name="currentSlideIndex">当前播放的页码;如果大于 0 则以此值写入 Position 文件,否则使用当前被锁定的页码或最后切换的页码作为保存位置。</param>
public void SaveAllStrokesToFile(Presentation presentation, int currentSlideIndex = -1)
{
ThrowIfDisposed();
if (!IsAutoSaveEnabled || string.IsNullOrEmpty(AutoSaveLocation) || presentation == null) return;
lock (_lockObject)
{
try
{
var folderPath = GetPresentationFolderPath();
string folderPath = GetPresentationFolderPath();
if (!Directory.Exists(folderPath))
{
Directory.CreateDirectory(folderPath);
int positionToSave = currentSlideIndex > 0 ? currentSlideIndex : (_lockedSlideIndex > 0 ? _lockedSlideIndex : _lastSwitchSlideIndex);
if (positionToSave > 0)
{
try { File.WriteAllText(Path.Combine(folderPath, "Position"), positionToSave.ToString()); }
catch (Exception ex) { LogHelper.WriteLogToFile($"保存 Position 失败: {ex}", LogHelper.LogType.Warning); }
}
// 保存位置信息
try
int slideCount = 0;
try { slideCount = presentation.Slides.Count; }
catch (COMException comEx)
{
File.WriteAllText(Path.Combine(folderPath, "Position"), _lockedSlideIndex.ToString());
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"保存位置信息失败: {ex}", LogHelper.LogType.Error);
if ((uint)comEx.HResult == 0x80048010) return;
throw;
}
// 保存所有页面的墨迹
int savedCount = 0;
for (int i = 1; i <= presentation.Slides.Count && i < _memoryStreams.Length; i++)
for (int i = 1; i <= slideCount && i < _memoryStreams.Length; i++)
{
if (_memoryStreams[i] != null)
if (_memoryStreams[i] == null) continue;
try
{
try
if (_memoryStreams[i].Length > 8)
{
if (_memoryStreams[i].Length > 8)
_memoryStreams[i].Position = 0;
byte[] buf = new byte[_memoryStreams[i].Length];
int read = _memoryStreams[i].Read(buf, 0, buf.Length);
if (read > 0)
{
var srcBuf = new byte[_memoryStreams[i].Length];
_memoryStreams[i].Position = 0;
var byteLength = _memoryStreams[i].Read(srcBuf, 0, srcBuf.Length);
var filePath = Path.Combine(folderPath, i.ToString("0000") + ".icstk");
File.WriteAllBytes(filePath, srcBuf);
savedCount++;
LogHelper.WriteLogToFile($"已保存第{i}页墨迹,大小: {byteLength} bytes", LogHelper.LogType.Trace);
}
else
{
// 删除空的墨迹文件
var filePath = Path.Combine(folderPath, i.ToString("0000") + ".icstk");
if (File.Exists(filePath))
{
File.Delete(filePath);
}
string basePath = Path.Combine(folderPath, i.ToString("0000"));
File.WriteAllBytes(basePath + StrokeFileExtension, buf);
}
}
catch (Exception ex)
else
{
LogHelper.WriteLogToFile($"保存第{i}页墨迹失败: {ex}", LogHelper.LogType.Error);
TryDeleteStrokeFile(folderPath, i);
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"保存第{i}页墨迹到文件失败: {ex}", LogHelper.LogType.Error);
}
}
LogHelper.WriteLogToFile($"已保存{savedCount}页墨迹到文件", LogHelper.LogType.Event);
}
catch (Exception ex)
{
@@ -266,42 +324,50 @@ namespace Ink_Canvas.Helpers
/// <summary>
/// 从文件加载已保存的墨迹
/// </summary>
/// <remarks>
/// 从自动保存目录加载已保存的幻灯片墨迹数据到内存流中,供后续显示和编辑使用。
/// 仅在启用自动保存且已设置 AutoSaveLocation 时执行。函数获取当前演示文稿的自动保存文件夹,遍历以 <c>.icstk</c> 为扩展名的文件,
/// 将文件名(去除扩展名)解析为幻灯片索引并在合法且文件大小大于 8 字节时加载到对应的内存流槽位。对单个文件的读取失败会记录错误并继续处理其他文件;
/// 若成功加载则会记录已加载页数。方法在内部使用锁以保证线程安全。
/// </remarks>
public void LoadSavedStrokes()
{
ThrowIfDisposed();
if (!IsAutoSaveEnabled || string.IsNullOrEmpty(AutoSaveLocation)) return;
lock (_lockObject)
{
try
{
var folderPath = GetPresentationFolderPath();
string folderPath = GetPresentationFolderPath();
if (!Directory.Exists(folderPath)) return;
var files = new DirectoryInfo(folderPath).GetFiles("*.icstk");
var dir = new DirectoryInfo(folderPath);
int loadedCount = 0;
foreach (var file in files)
foreach (FileInfo file in dir.GetFiles("*" + StrokeFileExtension))
{
string nameWithoutExt = Path.GetFileNameWithoutExtension(file.Name);
if (!int.TryParse(nameWithoutExt, out int slideIndex) || slideIndex <= 0) continue;
if (slideIndex >= _memoryStreams.Length) continue;
try
{
if (int.TryParse(Path.GetFileNameWithoutExtension(file.Name), out int slideIndex))
byte[] bytes = File.ReadAllBytes(file.FullName);
if (bytes.Length > 8)
{
if (slideIndex > 0 && slideIndex < _memoryStreams.Length)
{
var fileBytes = File.ReadAllBytes(file.FullName);
_memoryStreams[slideIndex] = new MemoryStream(fileBytes);
_memoryStreams[slideIndex].Position = 0;
loadedCount++;
}
_memoryStreams[slideIndex] = new MemoryStream(bytes);
_memoryStreams[slideIndex].Position = 0;
loadedCount++;
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"加载墨迹文件{file.Name}失败: {ex}", LogHelper.LogType.Error);
LogHelper.WriteLogToFile($"加载墨迹文件 {file.Name} 失败: {ex}", LogHelper.LogType.Error);
}
}
LogHelper.WriteLogToFile($"已从文件加载{loadedCount}页墨迹", LogHelper.LogType.Event);
if (loadedCount > 0)
LogHelper.WriteLogToFile($"已从磁盘加载 {loadedCount} 页墨迹", LogHelper.LogType.Trace);
}
catch (Exception ex)
{
@@ -313,101 +379,225 @@ namespace Ink_Canvas.Helpers
/// <summary>
/// 清除所有墨迹
/// </summary>
/// <remarks>
/// 清除并释放当前演示文稿所有幻灯片的墨迹数据和相关内存资源。
/// 该方法在内部加锁以保证线程安全;会处置并清空所有内部存储的墨迹流、重建内部流数组并清空 CurrentStrokes。
/// </remarks>
public void ClearAllStrokes()
{
ThrowIfDisposed();
lock (_lockObject)
{
try
{
for (int i = 0; i < _memoryStreams.Length; i++)
{
_memoryStreams[i]?.Dispose();
_memoryStreams[i] = null;
}
CurrentStrokes.Clear();
LogHelper.WriteLogToFile("已清除所有墨迹", LogHelper.LogType.Trace);
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"清除墨迹失败: {ex}", LogHelper.LogType.Error);
}
ClearAllStrokesInternal();
}
}
/// <summary>
/// 翻页后锁定墨迹写入
/// 为指定幻灯片设置短时墨迹写入锁,防止在该时间窗口内对其他幻灯片进行写入操作。
/// </summary>
/// <param name="slideIndex">要上锁的幻灯片索引(大于 0)。锁从调用时刻开始,持续 InkLockMilliseconds 毫秒。</param>
public void LockInkForSlide(int slideIndex)
{
ThrowIfDisposed();
_inkLockUntil = DateTime.Now.AddMilliseconds(InkLockMilliseconds);
_lockedSlideIndex = slideIndex;
}
/// <summary>
/// 检查是否可以写入墨迹
/// 确定在当前滑页上下文中是否允许写入墨迹(基于短期的墨迹写入锁与容差窗口)。
/// </summary>
/// <param name="currentSlideIndex">当前尝试写入墨迹的幻灯片索引(从 1 开始)。</param>
/// <returns>`true` 如果允许写入墨迹(锁已过期、目标为被锁定的幻灯片,或处于短暂的容差窗口内),`false` 否则。</returns>
public bool CanWriteInk(int currentSlideIndex)
{
if (DateTime.Now < _inkLockUntil) return false;
if (currentSlideIndex != _lockedSlideIndex && _lockedSlideIndex > 0) return false;
return true;
ThrowIfDisposed();
if (DateTime.Now >= _inkLockUntil) return true;
if (currentSlideIndex == _lockedSlideIndex) return true;
if (DateTime.Now - (_inkLockUntil.AddMilliseconds(-InkLockMilliseconds)) < TimeSpan.FromMilliseconds(50)) return true;
return false;
}
/// <summary>
/// 重置与墨迹书写和幻灯片切换相关的锁与跟踪状态为初始(未锁定)值。
/// </summary>
/// <remarks>
/// 将内部的墨迹写入到期时间、当前被锁定的幻灯片索引、上次切换时间和上次切换的幻灯片索引均恢复为默认未设置状态。
/// </remarks>
public void ResetLockState()
{
ThrowIfDisposed();
lock (_lockObject)
{
_inkLockUntil = DateTime.MinValue;
_lockedSlideIndex = -1;
_lastSwitchTime = DateTime.MinValue;
_lastSwitchSlideIndex = -1;
}
}
#endregion
#region Private Methods
#region Private Helpers
/// <summary>
/// 释放并清除类内用于存储各页墨迹的所有内存流,清空当前画笔集合,并重置内部内存流数组容量为 _maxSlides + 2。
/// </summary>
/// <remarks>
/// - 会逐个释放已存在的 MemoryStream(忽略释放过程中的异常),并将对应槽位设为 null。
/// - 会清空 CurrentStrokes 集合。
/// - 会记录一条跟踪日志,指示已完成清除操作。
/// </remarks>
private void ClearAllStrokesInternal()
{
if (_memoryStreams != null)
{
for (int i = 0; i < _memoryStreams.Length; i++)
{
try { _memoryStreams[i]?.Dispose(); }
catch (Exception ex) { LogHelper.WriteLogToFile($"释放内存流 {i} 失败: {ex}", LogHelper.LogType.Warning); }
finally { _memoryStreams[i] = null; }
}
_memoryStreams = new MemoryStream[_maxSlides + 2];
}
CurrentStrokes?.Clear();
}
/// <summary>
/// 用指定的笔迹集合替换内部存储中对应幻灯片索引的内存流:释放(并忽略释放错误)旧流,将 <paramref name="strokes"/> 序列化到新的 <see cref="MemoryStream"/> 并保存回内部数组。
/// </summary>
/// <param name="slideIndex">要替换的幻灯片索引(内部内存流数组的索引)。</param>
/// <param name="strokes">要序列化并保存到内存流的笔迹集合。</param>
private void ReplaceSlideStream(int slideIndex, StrokeCollection strokes)
{
try { _memoryStreams[slideIndex]?.Dispose(); } catch (Exception ex) { LogHelper.WriteLogToFile($"释放旧内存流失败: {ex}", LogHelper.LogType.Warning); }
var ms = new MemoryStream();
strokes.Save(ms);
ms.Position = 0;
_memoryStreams[slideIndex] = ms;
}
/// <summary>
/// 从指定文件夹删除对应幻灯片的笔迹文件(按四位索引命名);如果文件不存在或删除失败则静默忽略错误。
/// </summary>
/// <param name="folderPath">存放笔迹文件的文件夹路径。</param>
/// <param name="slideIndex">用于生成文件名的幻灯片索引(格式化为四位,例如 1 -> "0001")。</param>
private void TryDeleteStrokeFile(string folderPath, int slideIndex)
{
try
{
string path = Path.Combine(folderPath, slideIndex.ToString("0000") + StrokeFileExtension);
if (File.Exists(path)) File.Delete(path);
}
catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); }
}
/// <summary>
/// 检查当前墨迹内存使用状况并在超过阈值时触发清理操作。
/// </summary>
/// <remarks>
/// 会更新内部的内存使用统计并刷新上次清理时间;当总占用超过 MaxMemoryUsageBytes 时,会记录警告并调用 CleanupInactiveSlideStrokes 清理不活跃幻灯页的墨迹流。若检查或清理过程中发生异常,会记录错误日志。
/// </remarks>
private void CheckAndPerformMemoryCleanup()
{
try
{
var now = DateTime.Now;
if (now - _lastMemoryCleanup < TimeSpan.FromMinutes(MemoryCleanupIntervalMinutes)) return;
long currentMemoryUsage = 0;
if (_memoryStreams != null)
{
for (int i = 0; i < _memoryStreams.Length; i++)
if (_memoryStreams[i] != null) currentMemoryUsage += _memoryStreams[i].Length;
}
_totalMemoryUsage = currentMemoryUsage;
if (currentMemoryUsage > MaxMemoryUsageBytes)
{
LogHelper.WriteLogToFile($"墨迹内存超限 ({currentMemoryUsage / (1024 * 1024)}MB),执行清理", LogHelper.LogType.Warning);
CleanupInactiveSlideStrokes();
}
_lastMemoryCleanup = now;
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"内存清理检查失败: {ex}", LogHelper.LogType.Error);
}
}
/// <summary>
/// 清理不活跃幻灯片的内存化墨迹数据以回收内存空间。
/// </summary>
/// <remarks>
/// 将释放除当前锁定幻灯片与最近切换幻灯片之外的每页内存流(若存在),并将对应数组项设为 null;完成后若有释放,会记录已清理页数与释放的总大小(KB)。
/// </remarks>
private void CleanupInactiveSlideStrokes()
{
if (_memoryStreams == null) return;
int cleaned = 0;
long freed = 0;
for (int i = 0; i < _memoryStreams.Length; i++)
{
if (i == _lockedSlideIndex || i == _lastSwitchSlideIndex) continue;
if (_memoryStreams[i] != null)
{
long len = _memoryStreams[i].Length;
try { _memoryStreams[i].Dispose(); freed += len; cleaned++; }
catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); }
finally { _memoryStreams[i] = null; }
}
}
if (cleaned > 0)
LogHelper.WriteLogToFile($"已清理 {cleaned} 页墨迹,释放 {freed / 1024}KB", LogHelper.LogType.Trace);
}
/// <summary>
/// 生成基于演示文稿名称、幻灯片数量和路径哈希的标识符字符串。
/// </summary>
/// <returns>由 `名称_幻灯片数_路径哈希` 组成的标识符;若生成失败则返回形如 `unknown_{ticks}` 的回退标识符。</returns>
private string GeneratePresentationId(Presentation presentation)
{
try
{
var presentationPath = presentation.FullName;
var fileHash = GetFileHash(presentationPath);
return $"{presentation.Name}_{presentation.Slides.Count}_{fileHash}";
string path = presentation.FullName;
string hash = HashHelper.GetFileHash(path);
return $"{presentation.Name}_{presentation.Slides.Count}_{hash}";
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"生成演示文稿ID失败: {ex}", LogHelper.LogType.Error);
LogHelper.WriteLogToFile($"生成演示文稿 ID 失败: {ex}", LogHelper.LogType.Error);
return $"unknown_{DateTime.Now.Ticks}";
}
}
private string GetFileHash(string filePath)
{
try
{
if (string.IsNullOrEmpty(filePath)) return "unknown";
using (var md5 = MD5.Create())
{
byte[] hashBytes = md5.ComputeHash(Encoding.UTF8.GetBytes(filePath));
return BitConverter.ToString(hashBytes).Replace("-", "").Substring(0, 8);
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"计算文件哈希值失败: {ex}", LogHelper.LogType.Error);
return "error";
}
}
private string GetPresentationFolderPath()
{
return Path.Combine(AutoSaveLocation, "Auto Saved - Presentations", _currentPresentationId);
}
#endregion
#region Dispose
/// <summary>
/// 释放 PPTInkManager 持有的资源并清除所有内存中的笔迹数据。
/// </summary>
/// <remarks>
/// 调用后该实例将进入已释放状态,不应再被使用。方法为幂等且线程安全:如果已释放则立即返回,否则在同步区内清理资源并标记为已释放。
/// </remarks>
public void Dispose()
{
if (!_disposed)
{
lock (_lockObject)
{
ClearAllStrokes();
}
_disposed = true;
}
if (_disposed) return;
lock (_lockObject) { ClearAllStrokesInternal(); }
_disposed = true;
GC.SuppressFinalize(this);
}
private void ThrowIfDisposed()
{
if (_disposed) throw new ObjectDisposedException(nameof(PPTInkManager));
}
#endregion
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,522 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.ComTypes;
using System.Text;
namespace Ink_Canvas.Helpers
{
public static class PPTROTConnectionHelper
{
#region Win32 API Declarations
[DllImport("ole32.dll")]
private static extern int GetRunningObjectTable(int reserved, out IRunningObjectTable prot);
[DllImport("ole32.dll")]
private static extern int CreateBindCtx(int reserved, out IBindCtx ppbc);
[DllImport("user32.dll")]
private static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);
[DllImport("user32.dll")]
private static extern IntPtr GetForegroundWindow();
[DllImport("user32.dll")]
private static extern int GetWindowText(IntPtr hWnd, StringBuilder lpString, int nMaxCount);
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool IsWindowVisible(IntPtr hWnd);
[DllImport("user32.dll")]
private static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect);
[StructLayout(LayoutKind.Sequential)]
private struct RECT
{
public int Left;
public int Top;
public int Right;
public int Bottom;
}
#endregion
#region Constants
private static readonly Guid PowerPointApplicationGuid = new Guid("91493441-5A91-11CF-8700-00AA0060263B");
private static readonly string[] PptLikeExtensions = new[]
{
".pptx", ".pptm", ".ppt",
".ppsx", ".ppsm", ".pps",
".potx", ".potm", ".pot",
".dps", ".dpt"
};
#endregion
#region Public Methods
public static Microsoft.Office.Interop.PowerPoint.Application TryConnectViaROT(bool isSupportWPS = false)
{
try
{
object bestApp = GetAnyActivePowerPoint(null, out int bestPriority, out _);
if (bestApp != null && bestPriority > 0)
{
try
{
Type appType = typeof(Microsoft.Office.Interop.PowerPoint.Application);
Microsoft.Office.Interop.PowerPoint.Application pptApp = null;
if (appType.IsInstanceOfType(bestApp))
{
pptApp = (Microsoft.Office.Interop.PowerPoint.Application)bestApp;
}
if (pptApp != null)
{
try
{
var nameObj = pptApp.GetType().InvokeMember("Name", BindingFlags.GetProperty, null, pptApp, null);
SafeReleaseComObject(nameObj);
return pptApp;
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"ROT 连接验证 Name 不可用(将依赖 SlideShowWindows: {ex.Message}", LogHelper.LogType.Warning);
return pptApp;
}
}
else
{
SafeReleaseComObject(bestApp);
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"ROT 连接验证失败: {ex.Message}", LogHelper.LogType.Warning);
SafeReleaseComObject(bestApp);
}
}
else if (bestApp != null)
{
SafeReleaseComObject(bestApp);
}
try
{
var pptApp = (Microsoft.Office.Interop.PowerPoint.Application)OleActiveObject.GetActiveObject("PowerPoint.Application");
if (pptApp != null && Marshal.IsComObject(pptApp))
{
try
{
var _ = pptApp.Name;
}
catch (COMException)
{
}
return pptApp;
}
}
catch (COMException) { }
catch (InvalidCastException) { }
if (isSupportWPS)
{
try
{
var wpsApp = (Microsoft.Office.Interop.PowerPoint.Application)OleActiveObject.GetActiveObject("kwpp.Application");
if (wpsApp != null && Marshal.IsComObject(wpsApp))
{
try
{
var _ = wpsApp.Name;
}
catch (COMException)
{
}
return wpsApp;
}
}
catch (COMException) { }
catch (InvalidCastException) { }
}
return null;
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"ROT 连接过程发生异常: {ex}", LogHelper.LogType.Error);
return null;
}
}
#endregion
#region Public Methods
/// <summary>
/// 在系统的运行对象表(ROT)中查找并返回最合适的正在运行的 PowerPoint 应用实例。
/// </summary>
/// <param name="targetApp">可选的目标 PowerPoint COM 对象,用于优先比较;传入 null 表示不指定目标。</param>
/// <param name="bestPriority">输出参数:返回找到的最佳实例的优先级(0 表示未找到或无活动演示)。</param>
/// <param name="targetPriority">输出参数:返回与 <paramref name="targetApp"/> 对应实例的优先级(如果未提供或未命中则为 0)。</param>
/// <returns>最合适的 PowerPoint 应用对象(通常为 COM Application 实例),若未找到则返回 null。</returns>
public static object GetAnyActivePowerPoint(object targetApp, out int bestPriority, out int targetPriority)
{
IRunningObjectTable rot = null;
IEnumMoniker enumMoniker = null;
object bestApp = null;
bestPriority = 0;
targetPriority = 0;
int highestPriority = 0;
List<object> foundAppObjects = new List<object>();
try
{
int hr = GetRunningObjectTable(0, out rot);
if (hr != 0 || rot == null)
{
LogHelper.WriteLogToFile("无法获取 Running Object Table", LogHelper.LogType.Warning);
return null;
}
rot.EnumRunning(out enumMoniker);
if (enumMoniker == null)
{
LogHelper.WriteLogToFile("无法枚举 ROT 中的对象", LogHelper.LogType.Warning);
return null;
}
IMoniker[] moniker = new IMoniker[1];
IntPtr fetched = IntPtr.Zero;
while (enumMoniker.Next(1, moniker, fetched) == 0)
{
IBindCtx bindCtx = null;
object comObject = null;
dynamic candidateApp = null;
string displayName = "Unknown";
dynamic activePres = null;
dynamic ssWindow = null;
bool keepAlive = false;
try
{
CreateBindCtx(0, out bindCtx);
moniker[0].GetDisplayName(bindCtx, null, out displayName);
if (LooksLikePresentationFile(displayName) || displayName == "!{91493441-5A91-11CF-8700-00AA0060263B}")
{
rot.GetObject(moniker[0], out comObject);
if (comObject != null)
{
try
{
object appObj = comObject.GetType().InvokeMember("Application", BindingFlags.GetProperty, null, comObject, null);
candidateApp = appObj;
}
catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); }
}
}
bool isDuplicate = false;
if (candidateApp != null)
{
foreach (var processedApp in foundAppObjects)
{
if (AreComObjectsEqual((object)candidateApp, processedApp))
{
isDuplicate = true;
break;
}
}
if (!isDuplicate)
{
foundAppObjects.Add(candidateApp);
keepAlive = true;
}
}
if (candidateApp != null && !isDuplicate)
{
int currentPriority = 0;
bool isTarget = false;
if (targetApp != null && AreComObjectsEqual((object)candidateApp, targetApp))
{
isTarget = true;
}
try
{
try
{
activePres = candidateApp.ActivePresentation;
}
catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); }
if (activePres != null)
{
currentPriority = 1;
try
{
ssWindow = activePres.SlideShowWindow;
}
catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); }
if (ssWindow != null)
{
currentPriority = 2;
try
{
bool isActive = false;
try
{
object val = ssWindow.Active;
if (val is int && (int)val == -1) isActive = true;
else if (val is bool && (bool)val == true) isActive = true;
}
catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); }
if (isActive)
{
currentPriority = 3;
}
else
{
if (IsSlideShowWindowActive(ssWindow))
{
currentPriority = 3;
}
}
}
catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); }
}
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"计算优先级时出错: {ex.Message}", LogHelper.LogType.Warning);
}
if (isTarget)
{
targetPriority = currentPriority;
}
if (currentPriority > 0)
{
if (currentPriority > highestPriority)
{
highestPriority = currentPriority;
SafeReleaseComObject(bestApp);
bestApp = candidateApp;
candidateApp = null;
}
}
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"ROT 枚举循环中出错: {ex.Message}", LogHelper.LogType.Warning);
}
finally
{
SafeReleaseComObject(ssWindow);
SafeReleaseComObject(activePres);
if (!keepAlive)
{
SafeReleaseComObject(candidateApp);
}
CleanUpLoopObjects(bindCtx, moniker[0], comObject);
}
}
bestPriority = highestPriority;
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"ROT 扫描关键错误: {ex}", LogHelper.LogType.Error);
}
finally
{
if (foundAppObjects != null)
{
foreach (var cachedApp in foundAppObjects)
{
if (bestApp != null && ReferenceEquals(cachedApp, bestApp))
continue;
SafeReleaseComObject(cachedApp);
}
foundAppObjects.Clear();
}
if (enumMoniker != null) Marshal.ReleaseComObject(enumMoniker);
if (rot != null) Marshal.ReleaseComObject(rot);
}
return bestApp;
}
public static bool AreComObjectsEqual(object o1, object o2)
{
if (o1 == null || o2 == null) return false;
if (ReferenceEquals(o1, o2)) return true;
IntPtr pUnk1 = IntPtr.Zero;
IntPtr pUnk2 = IntPtr.Zero;
try
{
pUnk1 = Marshal.GetIUnknownForObject(o1);
pUnk2 = Marshal.GetIUnknownForObject(o2);
return pUnk1 == pUnk2;
}
catch { return false; }
finally
{
if (pUnk1 != IntPtr.Zero) Marshal.Release(pUnk1);
if (pUnk2 != IntPtr.Zero) Marshal.Release(pUnk2);
}
}
private static bool LooksLikePresentationFile(string displayName)
{
if (string.IsNullOrEmpty(displayName))
return false;
string lower = displayName.ToLowerInvariant();
foreach (var ext in PptLikeExtensions)
{
if (lower.Contains(ext))
return true;
}
return false;
}
public static bool IsSlideShowWindowActive(object sswObj)
{
try
{
dynamic ssw = sswObj;
IntPtr foregroundHwnd = GetForegroundWindow();
if (foregroundHwnd == IntPtr.Zero) return false;
uint fgPid;
GetWindowThreadProcessId(foregroundHwnd, out fgPid);
IntPtr sswHwnd = IntPtr.Zero;
try
{
sswHwnd = GetPptHwndFromSlideShowWindow(sswObj);
}
catch { return false; }
if (sswHwnd == IntPtr.Zero) return false;
uint sswPid;
GetWindowThreadProcessId(sswHwnd, out sswPid);
if (fgPid == sswPid) return true;
try
{
using (Process fgProc = Process.GetProcessById((int)fgPid))
using (Process appProc = Process.GetProcessById((int)sswPid))
{
string fgName = fgProc.ProcessName.ToLower();
string appName = appProc.ProcessName.ToLower();
if (fgName.StartsWith("wps") && appName.StartsWith("wpp"))
{
return true;
}
}
}
catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); }
return false;
}
catch
{
return false;
}
}
private static IntPtr GetPptHwndFromSlideShowWindow(object pptSlideShowWindowObj)
{
IntPtr hwnd = IntPtr.Zero;
if (pptSlideShowWindowObj == null) return IntPtr.Zero;
try
{
Microsoft.Office.Interop.PowerPoint.SlideShowWindow slideWindow = (Microsoft.Office.Interop.PowerPoint.SlideShowWindow)pptSlideShowWindowObj;
int hwndVal = slideWindow.HWND;
hwnd = new IntPtr(hwndVal);
}
catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); }
return hwnd;
}
public static void SafeReleaseComObject(object comObj)
{
if (comObj == null) return;
if (Marshal.IsComObject(comObj))
{
try
{
Marshal.ReleaseComObject(comObj);
}
catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); }
}
}
private static void CleanUpLoopObjects(IBindCtx bindCtx, IMoniker moniker, object comObject)
{
if (comObject != null && Marshal.IsComObject(comObject))
Marshal.ReleaseComObject(comObject);
if (moniker != null)
Marshal.ReleaseComObject(moniker);
if (bindCtx != null)
Marshal.ReleaseComObject(bindCtx);
}
public static int GetSlideShowWindowsCount(Microsoft.Office.Interop.PowerPoint.Application pptApp)
{
try
{
if (pptApp == null) return 0;
return pptApp.SlideShowWindows.Count;
}
catch
{
return 0;
}
}
public static bool IsValidSlideShowWindow(object pptSlideShowWindow)
{
if (pptSlideShowWindow == null) return false;
try
{
dynamic ssw = pptSlideShowWindow;
var _ = ssw.Active;
return true;
}
catch
{
return false;
}
}
#endregion
}
}
+91 -139
View File
@@ -1,6 +1,7 @@
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Interop;
using System.Windows.Media;
using System.Windows.Threading;
@@ -22,6 +23,10 @@ namespace Ink_Canvas.Helpers
public int PPTRBButtonPosition { get; set; } = 0;
public bool EnablePPTButtonPageClickable { get; set; } = true;
public bool EnablePPTButtonLongPressPageTurn { get; set; } = true;
public double PPTLSButtonOpacity { get; set; } = 0.5;
public double PPTRSButtonOpacity { get; set; } = 0.5;
public double PPTLBButtonOpacity { get; set; } = 0.5;
public double PPTRBButtonOpacity { get; set; } = 0.5;
#endregion
#region Private Fields
@@ -81,29 +86,55 @@ namespace Ink_Canvas.Helpers
_mainWindow.BtnPPTSlideShow.Visibility = Visibility.Collapsed;
_mainWindow.BtnPPTSlideShowEnd.Visibility = Visibility.Visible;
// 只有在页数有效时才更新页码显示
if (currentSlide > 0 && totalSlides > 0)
{
_mainWindow.PPTBtnPageNow.Text = currentSlide.ToString();
_mainWindow.PPTBtnPageTotal.Text = $"/ {totalSlides}";
LogHelper.WriteLogToFile($"更新PPT页码显示: {currentSlide}/{totalSlides}", LogHelper.LogType.Trace);
}
else
{
// 页数无效时清空页码显示
_mainWindow.PPTBtnPageNow.Text = "?";
_mainWindow.PPTBtnPageTotal.Text = "/ ?";
LogHelper.WriteLogToFile($"PPT页数无效,清空页码显示: 当前页={currentSlide}, 总页数={totalSlides}", LogHelper.LogType.Warning);
}
// 同步页码到所有翻页条 + 兼容旧绑定的隐藏 placeholder
SetPageNumberOnAllBars(currentSlide, totalSlides);
UpdateNavigationPanelsVisibility();
UpdateNavigationButtonStyles();
_mainWindow.UpdatePPTTimeCapsuleVisibility();
_mainWindow.UpdatePPTQuickPanelVisibility();
if (MainWindow.Settings.Advanced.IsEnableAvoidFullScreenHelper)
{
// 设置为画板模式,允许全屏操作
AvoidFullScreenHelper.SetBoardMode(true);
_dispatcher.BeginInvoke(new Action(() =>
{
MainWindow.MoveWindow(new WindowInteropHelper(_mainWindow).Handle, 0, 0,
System.Windows.Forms.Screen.PrimaryScreen.Bounds.Width,
System.Windows.Forms.Screen.PrimaryScreen.Bounds.Height, true);
// MoveWindow 触发的 WM_WINDOWPOSCHANGING + 重绘会打断面板的 ShowWithFadeIn 动画,
// 在窗口尺寸最终确定后重新评估一次翻页面板的可见性。
UpdateNavigationPanelsVisibility();
UpdateNavigationButtonStyles();
}), DispatcherPriority.ApplicationIdle);
_mainWindow.isFullScreenApplied = true; // 标记已应用全屏处理
}
}
else
{
_mainWindow.BtnPPTSlideShow.Visibility = Visibility.Visible;
_mainWindow.BtnPPTSlideShowEnd.Visibility = Visibility.Collapsed;
HideAllNavigationPanels();
_mainWindow.UpdatePPTTimeCapsuleVisibility();
_mainWindow.UpdatePPTQuickPanelVisibility();
if (MainWindow.Settings.Advanced.IsEnableAvoidFullScreenHelper)
{
// 恢复为非画板模式,重新启用全屏限制
AvoidFullScreenHelper.SetBoardMode(false);
_dispatcher.BeginInvoke(new Action(() =>
{
// 退出PPT放映模式,恢复到工作区域大小
var workingArea = System.Windows.Forms.Screen.PrimaryScreen.WorkingArea;
MainWindow.MoveWindow(new WindowInteropHelper(_mainWindow).Handle,
workingArea.X, workingArea.Y,
workingArea.Width, workingArea.Height, true);
}), DispatcherPriority.ApplicationIdle);
_mainWindow.isFullScreenApplied = false; // 标记全屏处理已还原
}
}
}
catch (Exception ex)
@@ -122,20 +153,7 @@ namespace Ink_Canvas.Helpers
{
try
{
// 只有在页数有效时才更新页码显示
if (currentSlide > 0 && totalSlides > 0)
{
_mainWindow.PPTBtnPageNow.Text = currentSlide.ToString();
_mainWindow.PPTBtnPageTotal.Text = $"/ {totalSlides}";
LogHelper.WriteLogToFile($"更新PPT页码显示: {currentSlide}/{totalSlides}", LogHelper.LogType.Trace);
}
else
{
// 页数无效时清空页码显示
_mainWindow.PPTBtnPageNow.Text = "?";
_mainWindow.PPTBtnPageTotal.Text = "/ ?";
LogHelper.WriteLogToFile($"PPT页数无效,清空页码显示: 当前页={currentSlide}, 总页数={totalSlides}", LogHelper.LogType.Warning);
}
SetPageNumberOnAllBars(currentSlide, totalSlides);
}
catch (Exception ex)
{
@@ -144,6 +162,34 @@ namespace Ink_Canvas.Helpers
});
}
private void SetPageNumberOnAllBars(int currentSlide, int totalSlides)
{
var bars = new[]
{
_mainWindow.LeftBottomPanelForPPTNavigation,
_mainWindow.RightBottomPanelForPPTNavigation,
_mainWindow.LeftSidePanelForPPTNavigation,
_mainWindow.RightSidePanelForPPTNavigation,
};
foreach (var bar in bars)
{
if (bar == null) continue;
bar.CurrentSlide = currentSlide;
bar.TotalSlides = totalSlides;
}
// 兼容旧绑定(其它界面通过 ElementName 引用 PPTBtnPageNow / PPTBtnPageTotal
if (currentSlide > 0 && totalSlides > 0)
{
_mainWindow.PPTBtnPageNow.Text = currentSlide.ToString();
_mainWindow.PPTBtnPageTotal.Text = $"/ {totalSlides}";
}
else
{
_mainWindow.PPTBtnPageNow.Text = "?";
_mainWindow.PPTBtnPageTotal.Text = "/ ?";
}
}
/// <summary>
/// 处理PPT放映状态变化
/// </summary>
@@ -157,7 +203,6 @@ namespace Ink_Canvas.Helpers
{
// 如果不在放映模式,隐藏所有导航面板
HideAllNavigationPanels();
LogHelper.WriteLogToFile("PPT放映状态变化:隐藏导航面板", LogHelper.LogType.Trace);
}
}
catch (Exception ex)
@@ -181,21 +226,19 @@ namespace Ink_Canvas.Helpers
bool isInSlideShow = _mainWindow.PPTManager?.IsInSlideShow == true;
int slidesCount = _mainWindow.PPTManager?.SlidesCount ?? 0;
bool hasValidPageCount = slidesCount > 0;
bool shouldShowButtons = ShowPPTButton &&
_mainWindow.BtnPPTSlideShowEnd.Visibility == Visibility.Visible &&
isInSlideShow &&
hasValidPageCount;
hasValidPageCount &&
!MainWindow.Settings.Automation.IsAutoFoldInPPTSlideShow;
if (!shouldShowButtons)
{
HideAllNavigationPanels();
LogHelper.WriteLogToFile($"隐藏PPT导航面板 - 放映状态: {isInSlideShow}, 页数: {slidesCount}, 按钮设置: {ShowPPTButton}", LogHelper.LogType.Trace);
return;
}
LogHelper.WriteLogToFile($"显示PPT导航面板 - 放映状态: {isInSlideShow}, 页数: {slidesCount}", LogHelper.LogType.Trace);
// 设置侧边按钮位置
_mainWindow.LeftSidePanelForPPTNavigation.Margin = new Thickness(0, 0, 0, PPTLSButtonPosition * 2);
_mainWindow.RightSidePanelForPPTNavigation.Margin = new Thickness(0, 0, 0, PPTRSButtonPosition * 2);
@@ -355,17 +398,17 @@ namespace Ink_Canvas.Helpers
// 页码按钮显示
var pageButtonVisibility = options[0] == '2' ? Visibility.Visible : Visibility.Collapsed;
_mainWindow.PPTLSPageButton.Visibility = pageButtonVisibility;
_mainWindow.PPTRSPageButton.Visibility = pageButtonVisibility;
_mainWindow.LeftSidePanelForPPTNavigation.SetPageButtonVisibility(pageButtonVisibility);
_mainWindow.RightSidePanelForPPTNavigation.SetPageButtonVisibility(pageButtonVisibility);
// 透明度设置
var opacity = options[1] == '2' ? 0.5 : 1.0;
_mainWindow.PPTBtnLSBorder.Opacity = opacity;
_mainWindow.PPTBtnRSBorder.Opacity = opacity;
// 透明度
_mainWindow.LeftSidePanelForPPTNavigation.SetBarOpacity(PPTLSButtonOpacity);
_mainWindow.RightSidePanelForPPTNavigation.SetBarOpacity(PPTRSButtonOpacity);
// 颜色主题
bool isDarkTheme = options[2] == '2';
ApplyButtonTheme(_mainWindow.PPTBtnLSBorder, _mainWindow.PPTBtnRSBorder, isDarkTheme, true);
_mainWindow.LeftSidePanelForPPTNavigation.ApplyTheme(isDarkTheme);
_mainWindow.RightSidePanelForPPTNavigation.ApplyTheme(isDarkTheme);
}
catch (Exception ex)
{
@@ -384,114 +427,23 @@ namespace Ink_Canvas.Helpers
// 页码按钮显示
var pageButtonVisibility = options[0] == '2' ? Visibility.Visible : Visibility.Collapsed;
_mainWindow.PPTLBPageButton.Visibility = pageButtonVisibility;
_mainWindow.PPTRBPageButton.Visibility = pageButtonVisibility;
_mainWindow.LeftBottomPanelForPPTNavigation.SetPageButtonVisibility(pageButtonVisibility);
_mainWindow.RightBottomPanelForPPTNavigation.SetPageButtonVisibility(pageButtonVisibility);
// 透明度设置
var opacity = options[1] == '2' ? 0.5 : 1.0;
_mainWindow.PPTBtnLBBorder.Opacity = opacity;
_mainWindow.PPTBtnRBBorder.Opacity = opacity;
// 透明度
_mainWindow.LeftBottomPanelForPPTNavigation.SetBarOpacity(PPTLBButtonOpacity);
_mainWindow.RightBottomPanelForPPTNavigation.SetBarOpacity(PPTRBButtonOpacity);
// 颜色主题
bool isDarkTheme = options[2] == '2';
ApplyButtonTheme(_mainWindow.PPTBtnLBBorder, _mainWindow.PPTBtnRBBorder, isDarkTheme, false);
_mainWindow.LeftBottomPanelForPPTNavigation.ApplyTheme(isDarkTheme);
_mainWindow.RightBottomPanelForPPTNavigation.ApplyTheme(isDarkTheme);
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"更新底部按钮样式失败: {ex}", LogHelper.LogType.Error);
}
}
private void ApplyButtonTheme(Border leftBorder, Border rightBorder, bool isDarkTheme, bool isSideButton)
{
try
{
Color backgroundColor, borderColor, foregroundColor, feedbackColor;
if (isDarkTheme)
{
backgroundColor = Color.FromRgb(39, 39, 42);
borderColor = Color.FromRgb(82, 82, 91);
foregroundColor = Colors.White;
feedbackColor = Colors.White;
}
else
{
backgroundColor = Color.FromRgb(244, 244, 245);
borderColor = Color.FromRgb(161, 161, 170);
foregroundColor = Color.FromRgb(39, 39, 42);
feedbackColor = Color.FromRgb(24, 24, 27);
}
// 应用背景和边框颜色
var backgroundBrush = new SolidColorBrush(backgroundColor);
var borderBrush = new SolidColorBrush(borderColor);
leftBorder.Background = backgroundBrush;
leftBorder.BorderBrush = borderBrush;
rightBorder.Background = backgroundBrush;
rightBorder.BorderBrush = borderBrush;
// 应用图标和文字颜色
var foregroundBrush = new SolidColorBrush(foregroundColor);
var feedbackBrush = new SolidColorBrush(feedbackColor);
if (isSideButton)
{
ApplySideButtonColors(foregroundBrush, feedbackBrush);
}
else
{
ApplyBottomButtonColors(foregroundBrush, feedbackBrush);
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"应用按钮主题失败: {ex}", LogHelper.LogType.Error);
}
}
private void ApplySideButtonColors(SolidColorBrush foregroundBrush, SolidColorBrush feedbackBrush)
{
// 图标颜色
_mainWindow.PPTLSPreviousButtonGeometry.Brush = foregroundBrush;
_mainWindow.PPTRSPreviousButtonGeometry.Brush = foregroundBrush;
_mainWindow.PPTLSNextButtonGeometry.Brush = foregroundBrush;
_mainWindow.PPTRSNextButtonGeometry.Brush = foregroundBrush;
// 反馈背景颜色
_mainWindow.PPTLSPreviousButtonFeedbackBorder.Background = feedbackBrush;
_mainWindow.PPTRSPreviousButtonFeedbackBorder.Background = feedbackBrush;
_mainWindow.PPTLSPageButtonFeedbackBorder.Background = feedbackBrush;
_mainWindow.PPTRSPageButtonFeedbackBorder.Background = feedbackBrush;
_mainWindow.PPTLSNextButtonFeedbackBorder.Background = feedbackBrush;
_mainWindow.PPTRSNextButtonFeedbackBorder.Background = feedbackBrush;
// 文字颜色
TextBlock.SetForeground(_mainWindow.PPTLSPageButton, foregroundBrush);
TextBlock.SetForeground(_mainWindow.PPTRSPageButton, foregroundBrush);
}
private void ApplyBottomButtonColors(SolidColorBrush foregroundBrush, SolidColorBrush feedbackBrush)
{
// 图标颜色
_mainWindow.PPTLBPreviousButtonGeometry.Brush = foregroundBrush;
_mainWindow.PPTRBPreviousButtonGeometry.Brush = foregroundBrush;
_mainWindow.PPTLBNextButtonGeometry.Brush = foregroundBrush;
_mainWindow.PPTRBNextButtonGeometry.Brush = foregroundBrush;
// 反馈背景颜色
_mainWindow.PPTLBPreviousButtonFeedbackBorder.Background = feedbackBrush;
_mainWindow.PPTRBPreviousButtonFeedbackBorder.Background = feedbackBrush;
_mainWindow.PPTLBPageButtonFeedbackBorder.Background = feedbackBrush;
_mainWindow.PPTRBPageButtonFeedbackBorder.Background = feedbackBrush;
_mainWindow.PPTLBNextButtonFeedbackBorder.Background = feedbackBrush;
_mainWindow.PPTRBNextButtonFeedbackBorder.Background = feedbackBrush;
// 文字颜色
TextBlock.SetForeground(_mainWindow.PPTLBPageButton, foregroundBrush);
TextBlock.SetForeground(_mainWindow.PPTRBPageButton, foregroundBrush);
}
#endregion
}
}
+79
View File
@@ -0,0 +1,79 @@
using System;
using System.IO;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Media.Imaging;
using Windows.Data.Pdf;
using Windows.Storage;
using Windows.Storage.Streams;
namespace Ink_Canvas.Helpers
{
/// <summary>
/// 使用 Windows.Data.PdfWinRT)将 PDF 页渲染为 WPF 可用的位图。
/// </summary>
internal static class PdfWinRtHelper
{
public static async Task<uint> GetPageCountAsync(string pdfPath)
{
if (string.IsNullOrWhiteSpace(pdfPath) || !File.Exists(pdfPath))
return 0;
var file = await StorageFile.GetFileFromPathAsync(pdfPath).AsTask();
var doc = await PdfDocument.LoadFromFileAsync(file).AsTask();
if (doc.IsPasswordProtected)
return 0;
return doc.PageCount;
}
public static async Task<BitmapSource> RenderPageToBitmapSourceAsync(string pdfPath, uint pageIndex)
{
if (string.IsNullOrWhiteSpace(pdfPath) || !File.Exists(pdfPath))
return null;
var file = await StorageFile.GetFileFromPathAsync(pdfPath).AsTask();
var doc = await PdfDocument.LoadFromFileAsync(file).AsTask();
if (doc.IsPasswordProtected)
return null;
if (pageIndex >= doc.PageCount)
return null;
var page = doc.GetPage(pageIndex);
try
{
using (var ras = new InMemoryRandomAccessStream())
{
await page.RenderToStreamAsync(ras).AsTask();
ras.Seek(0);
var ms = new MemoryStream();
using (var netStream = ras.AsStreamForRead())
netStream.CopyTo(ms);
ms.Position = 0;
try
{
return await Application.Current.Dispatcher.InvokeAsync(() =>
{
var bi = new BitmapImage();
bi.BeginInit();
bi.StreamSource = ms;
bi.CacheOption = BitmapCacheOption.OnLoad;
bi.EndInit();
bi.Freeze();
return (BitmapSource)bi;
});
}
finally
{
ms.Dispose();
}
}
}
finally
{
(page as IDisposable)?.Dispose();
}
}
}
}
@@ -1,274 +0,0 @@
using iNKORE.UI.WPF.Modern.Controls;
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
namespace Ink_Canvas.Helpers.Plugins.BuiltIn.SuperLauncher
{
/// <summary>
/// 启动台按钮控件
/// </summary>
public class LauncherButton
{
/// <summary>
/// 父插件
/// </summary>
private readonly SuperLauncherPlugin _plugin;
/// <summary>
/// 实际按钮控件
/// </summary>
private readonly SimpleStackPanel _panel;
/// <summary>
/// 获取按钮UI元素
/// </summary>
public UIElement Element => _panel;
/// <summary>
/// 构造函数
/// </summary>
/// <param name="plugin">父插件</param>
public LauncherButton(SuperLauncherPlugin plugin)
{
try
{
_plugin = plugin;
LogHelper.WriteLogToFile("开始创建启动台按钮");
// 创建SimpleStackPanel
_panel = new SimpleStackPanel
{
Name = "Launcher_Icon",
Orientation = Orientation.Vertical,
HorizontalAlignment = HorizontalAlignment.Center,
Width = 28,
Margin = new Thickness(0, -2, 0, 0),
Background = Brushes.Transparent
};
LogHelper.WriteLogToFile("创建SimpleStackPanel完成");
// 添加图标
var image = CreateIconImage();
_panel.Children.Add(image);
// 添加文本
TextBlock textBlock = new TextBlock
{
Text = "启动台",
Foreground = Brushes.Black,
FontSize = 8,
Margin = new Thickness(0, 1, 0, 0),
TextAlignment = TextAlignment.Center
};
_panel.Children.Add(textBlock);
// 设置鼠标事件
_panel.MouseDown += Panel_MouseDown;
_panel.MouseUp += Panel_MouseUp;
_panel.MouseLeave += Panel_MouseLeave;
// 右键菜单支持
_panel.ContextMenu = CreateContextMenu();
// 设置工具提示
_panel.ToolTip = "启动台";
LogHelper.WriteLogToFile("启动台按钮创建完成");
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"创建启动台按钮时出错: {ex.Message}", LogHelper.LogType.Error);
LogHelper.NewLog(ex);
}
}
/// <summary>
/// 创建右键菜单
/// </summary>
private ContextMenu CreateContextMenu()
{
try
{
// 创建菜单
ContextMenu menu = new ContextMenu();
// 创建位置切换菜单项
MenuItem positionMenuItem = new MenuItem();
positionMenuItem.Header = _plugin.Config.ButtonPosition == LauncherButtonPosition.Left ?
"移至右侧" : "移至左侧";
positionMenuItem.Click += (s, e) =>
{
// 切换位置
_plugin.Config.ButtonPosition = _plugin.Config.ButtonPosition == LauncherButtonPosition.Left ?
LauncherButtonPosition.Right : LauncherButtonPosition.Left;
// 更新按钮位置
_plugin.UpdateButtonPosition();
// 保存配置
_plugin.SaveConfig();
LogHelper.WriteLogToFile($"通过右键菜单切换启动台按钮位置为: {_plugin.Config.ButtonPosition}");
};
menu.Items.Add(positionMenuItem);
// 添加设置菜单项
MenuItem settingsMenuItem = new MenuItem();
settingsMenuItem.Header = "打开设置";
settingsMenuItem.Click += (s, e) =>
{
// 打开插件设置窗口
var mainWindow = Application.Current.MainWindow;
if (mainWindow != null)
{
try
{
// 使用反射调用主窗口的ShowPluginSettings方法
var method = mainWindow.GetType().GetMethod("ShowPluginSettings");
if (method != null)
{
method.Invoke(mainWindow, null);
LogHelper.WriteLogToFile("已打开插件设置窗口");
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"打开插件设置窗口失败: {ex.Message}", LogHelper.LogType.Error);
}
}
};
menu.Items.Add(settingsMenuItem);
return menu;
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"创建右键菜单时出错: {ex.Message}", LogHelper.LogType.Error);
return null;
}
}
/// <summary>
/// 获取实际的UI元素
/// </summary>
[Obsolete("使用Element属性代替")]
public UIElement GetUIElement()
{
return _panel;
}
/// <summary>
/// 创建图标图像
/// </summary>
private Image CreateIconImage()
{
try
{
// 创建图像
Image image = new Image
{
Height = 17,
Margin = new Thickness(0, 3, 0, 0)
};
// 设置位图缩放模式
RenderOptions.SetBitmapScalingMode(image, BitmapScalingMode.HighQuality);
// 创建绘图图像
DrawingImage drawingImage = new DrawingImage();
DrawingGroup drawingGroup = new DrawingGroup();
drawingGroup.ClipGeometry = Geometry.Parse("M0,0 V24 H24 V0 H0 Z");
// 使用提供的应用网格图标
GeometryDrawing geometryDrawing = new GeometryDrawing
{
Brush = new SolidColorBrush(Color.FromRgb(0x1B, 0x1B, 0x1B)),
Geometry = Geometry.Parse("F0 M24,24z M0,0z M4.41721,4.29873C4.35178,4.29873,4.29873,4.35178,4.29873,4.41721L4.29873,9.15646C4.29873,9.22189,4.35178,9.27494,4.41721,9.27494L9.15646,9.27494C9.22189,9.27494,9.27494,9.22189,9.27494,9.15646L9.27494,4.41721C9.27494,4.35178,9.22189,4.29873,9.15646,4.29873L4.41721,4.29873z M2.64,4.41721C2.64,3.43569,3.43569,2.64,4.41721,2.64L9.15646,2.64C10.138,2.64,10.9337,3.43569,10.9337,4.41721L10.9337,9.15646C10.9337,10.138,10.138,10.9337,9.15646,10.9337L4.41721,10.9337C3.43569,10.9337,2.64,10.138,2.64,9.15646L2.64,4.41721z M14.8435,4.29873C14.7781,4.29873,14.7251,4.35178,14.7251,4.41721L14.7251,9.15646C14.7251,9.22189,14.7781,9.27494,14.8435,9.27494L19.5828,9.27494C19.6482,9.27494,19.7013,9.22189,19.7013,9.15646L19.7013,4.41721C19.7013,4.35178,19.6482,4.29873,19.5828,4.29873L14.8435,4.29873z M13.0663,4.41721C13.0663,3.43569,13.862,2.64,14.8435,2.64L19.5828,2.64C20.5643,2.64,21.36,3.43569,21.36,4.41721L21.36,9.15646C21.36,10.138,20.5643,10.9337,19.5828,10.9337L14.8435,10.9337C13.862,10.9337,13.0663,10.138,13.0663,9.15646L13.0663,4.41721z M14.8435,14.7251C14.7781,14.7251,14.7251,14.7781,14.7251,14.8435L14.7251,19.5828C14.7251,19.6482,14.7781,19.7013,14.8435,19.7013L19.5828,19.7013C19.6482,19.7013,19.7013,19.6482,19.7013,19.5828L19.7013,14.8435C19.7013,14.7781,19.6482,14.7251,19.5828,14.7251L14.8435,14.7251z M13.0663,14.8435C13.0663,13.862,13.862,13.0663,14.8435,13.0663L19.5828,13.0663C20.5643,13.0663,21.36,13.862,21.36,14.8435L21.36,19.5828C21.36,20.5643,20.5643,21.36,19.5828,21.36L14.8435,21.36C13.862,21.36,13.0663,20.5643,13.0663,19.5828L13.0663,14.8435z M4.41721,14.7251C4.35178,14.7251,4.29873,14.7781,4.29873,14.8435L4.29873,19.5828C4.29873,19.6482,4.35178,19.7013,4.41721,19.7013L9.15646,19.7013C9.22189,19.7013,9.27494,19.6482,9.27494,19.5828L9.27494,14.8435C9.27494,14.7781,9.22189,14.7251,9.15646,14.7251L4.41721,14.7251z M2.64,14.8435C2.64,13.862,3.43569,13.0663,4.41721,13.0663L9.15646,13.0663C10.138,13.0663,10.9337,13.862,10.9337,14.8435L10.9337,19.5828C10.9337,20.5643,10.138,21.36,9.15646,21.36L4.41721,21.36C3.43569,21.36,2.64,20.5643,2.64,19.5828L2.64,14.8435z")
};
drawingGroup.Children.Add(geometryDrawing);
// 设置图像源
drawingImage.Drawing = drawingGroup;
image.Source = drawingImage;
return image;
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"创建图标图像时出错: {ex.Message}", LogHelper.LogType.Error);
LogHelper.NewLog(ex);
// 返回一个空图像
return new Image();
}
}
/// <summary>
/// 鼠标按下事件
/// </summary>
private void Panel_MouseDown(object sender, MouseButtonEventArgs e)
{
try
{
// 提供反馈
_panel.Background = new SolidColorBrush(Color.FromArgb(40, 0, 0, 0));
LogHelper.WriteLogToFile("启动台按钮鼠标按下");
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"启动台按钮鼠标按下事件出错: {ex.Message}", LogHelper.LogType.Error);
}
}
/// <summary>
/// 鼠标抬起事件
/// </summary>
private void Panel_MouseUp(object sender, MouseButtonEventArgs e)
{
try
{
// 只有左键点击才显示启动台窗口
if (e.ChangedButton != MouseButton.Left)
{
return;
}
// 恢复背景
_panel.Background = Brushes.Transparent;
LogHelper.WriteLogToFile("启动台按钮鼠标抬起,准备显示启动台窗口");
// 获取按钮在屏幕上的位置
Point buttonPosition = _panel.PointToScreen(new Point(_panel.ActualWidth / 2, 0));
// 显示启动台窗口
_plugin.ShowLauncherWindow(buttonPosition);
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"启动台按钮鼠标抬起事件出错: {ex.Message}", LogHelper.LogType.Error);
LogHelper.NewLog(ex);
}
}
/// <summary>
/// 鼠标离开事件
/// </summary>
private void Panel_MouseLeave(object sender, MouseEventArgs e)
{
try
{
// 恢复背景
_panel.Background = Brushes.Transparent;
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"启动台按钮鼠标离开事件出错: {ex.Message}", LogHelper.LogType.Error);
}
}
}
}
@@ -1,332 +0,0 @@
using Microsoft.Win32;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.IO;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Interop;
using System.Windows.Media;
using System.Windows.Media.Imaging;
namespace Ink_Canvas.Helpers.Plugins.BuiltIn.SuperLauncher
{
/// <summary>
/// 启动台按钮位置
/// </summary>
public enum LauncherButtonPosition
{
/// <summary>
/// 左侧
/// </summary>
Left,
/// <summary>
/// 右侧
/// </summary>
Right
}
/// <summary>
/// 启动台配置
/// </summary>
public class LauncherConfig
{
/// <summary>
/// 启动台按钮位置
/// </summary>
public LauncherButtonPosition ButtonPosition { get; set; } = LauncherButtonPosition.Right;
/// <summary>
/// 启动台应用程序列表
/// </summary>
public List<LauncherItem> Items { get; set; } = new List<LauncherItem>();
}
/// <summary>
/// 启动台应用项
/// </summary>
public class LauncherItem
{
/// <summary>
/// 应用程序名称
/// </summary>
public string Name { get; set; }
/// <summary>
/// 应用程序路径
/// </summary>
public string Path { get; set; }
/// <summary>
/// 是否可见
/// </summary>
public bool IsVisible { get; set; } = true;
/// <summary>
/// 在启动台中的位置(0-39
/// </summary>
public int Position { get; set; } = -1;
/// <summary>
/// 是否已固定位置
/// </summary>
public bool IsPositionFixed { get; set; } = false;
/// <summary>
/// 图标缓存
/// </summary>
[JsonIgnore]
private ImageSource _iconCache;
/// <summary>
/// 获取应用程序图标
/// </summary>
[JsonIgnore]
public ImageSource Icon
{
get
{
if (_iconCache != null)
{
return _iconCache;
}
try
{
if (File.Exists(Path))
{
// 从文件中获取图标
Icon icon = System.Drawing.Icon.ExtractAssociatedIcon(Path);
if (icon != null)
{
_iconCache = Imaging.CreateBitmapSourceFromHIcon(
icon.Handle,
Int32Rect.Empty,
BitmapSizeOptions.FromEmptyOptions());
icon.Dispose();
return _iconCache;
}
}
else
{
// 从注册表中获取文件类型关联图标
string extension = System.IO.Path.GetExtension(Path);
if (!string.IsNullOrEmpty(extension))
{
string fileType = Registry.ClassesRoot.OpenSubKey(extension)?.GetValue(string.Empty) as string;
if (!string.IsNullOrEmpty(fileType))
{
string iconPath = Registry.ClassesRoot.OpenSubKey(fileType + "\\DefaultIcon")?.GetValue(string.Empty) as string;
if (!string.IsNullOrEmpty(iconPath))
{
string[] parts = iconPath.Split(',');
string iconFile = parts[0].Trim('"');
int iconIndex = parts.Length > 1 ? Convert.ToInt32(parts[1]) : 0;
if (File.Exists(iconFile))
{
Icon icon = IconExtractor.Extract(iconFile, iconIndex, true);
if (icon != null)
{
_iconCache = Imaging.CreateBitmapSourceFromHIcon(
icon.Handle,
Int32Rect.Empty,
BitmapSizeOptions.FromEmptyOptions());
icon.Dispose();
return _iconCache;
}
}
}
}
}
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"获取应用图标时出错: {ex.Message}", LogHelper.LogType.Error);
}
// 返回默认图标
return GetDefaultIcon();
}
}
/// <summary>
/// 获取默认图标
/// </summary>
private ImageSource GetDefaultIcon()
{
try
{
// 对于资源管理器,使用特定图标
if (Path.EndsWith("explorer.exe", StringComparison.OrdinalIgnoreCase))
{
try
{
// 直接从C:\Windows\explorer.exe获取图标
string explorerPath = @"C:\Windows\explorer.exe";
if (File.Exists(explorerPath))
{
Icon icon = System.Drawing.Icon.ExtractAssociatedIcon(explorerPath);
if (icon != null)
{
_iconCache = Imaging.CreateBitmapSourceFromHIcon(
icon.Handle,
Int32Rect.Empty,
BitmapSizeOptions.FromEmptyOptions());
icon.Dispose();
return _iconCache;
}
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"获取资源管理器图标时出错: {ex.Message}", LogHelper.LogType.Warning);
// 如果获取Windows图标失败,回退到默认图标
}
// 回退到备用图标
string explorerIconPath = System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Resources", "Icons-Fluent", "ic_fluent_folder_24_regular.png");
if (File.Exists(explorerIconPath))
{
Uri uri = new Uri(explorerIconPath);
BitmapImage image = new BitmapImage(uri);
_iconCache = image;
return _iconCache;
}
}
// 返回一个简单的默认图标
string iconPath = System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Resources", "Icons-png", "icc.png");
if (File.Exists(iconPath))
{
Uri uri = new Uri(iconPath);
BitmapImage image = new BitmapImage(uri);
_iconCache = image;
return _iconCache;
}
// 如果还是没有找到,尝试使用应用程序图标
string appIconPath = System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Resources", "Icons-Fluent", "ic_fluent_apps_24_regular.png");
if (File.Exists(appIconPath))
{
Uri uri = new Uri(appIconPath);
BitmapImage image = new BitmapImage(uri);
_iconCache = image;
return _iconCache;
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"获取默认图标时出错: {ex.Message}", LogHelper.LogType.Error);
}
return null;
}
/// <summary>
/// 启动应用程序
/// </summary>
public void Launch()
{
try
{
if (string.IsNullOrEmpty(Path))
{
LogHelper.WriteLogToFile("无法启动应用程序:路径为空", LogHelper.LogType.Error);
return;
}
// 检查文件是否存在
if (!File.Exists(Path) && !Path.Contains(":\\"))
{
// 可能是系统命令,如explorer.exe
ProcessStartInfo psi = new ProcessStartInfo
{
FileName = Path,
UseShellExecute = true
};
Process.Start(psi);
}
else
{
// 使用Process.Start启动应用程序
ProcessStartInfo psi = new ProcessStartInfo
{
FileName = Path,
UseShellExecute = true
};
Process.Start(psi);
}
LogHelper.WriteLogToFile($"已启动应用程序: {Path}");
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"启动应用程序时出错: {ex.Message}", LogHelper.LogType.Error);
MessageBox.Show($"启动应用程序时出错: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
}
/// <summary>
/// 图标提取工具类
/// </summary>
public static class IconExtractor
{
/// <summary>
/// 从文件中提取图标
/// </summary>
/// <param name="file">文件路径</param>
/// <param name="index">图标索引</param>
/// <param name="largeIcon">是否提取大图标</param>
/// <returns>提取的图标</returns>
public static Icon Extract(string file, int index, bool largeIcon)
{
try
{
IntPtr large;
IntPtr small;
ExtractIconEx(file, index, out large, out small, 1);
try
{
return Icon.FromHandle(largeIcon ? large : small);
}
catch
{
return null;
}
finally
{
if (large != IntPtr.Zero)
DestroyIcon(large);
if (small != IntPtr.Zero)
DestroyIcon(small);
}
}
catch
{
return null;
}
}
[DllImport("Shell32.dll", EntryPoint = "ExtractIconEx")]
private static extern int ExtractIconEx(
[MarshalAs(UnmanagedType.LPStr)] string lpszFile,
int nIconIndex,
out IntPtr phiconLarge,
out IntPtr phiconSmall,
int nIcons);
[DllImport("User32.dll")]
private static extern int DestroyIcon(IntPtr hIcon);
}
}
@@ -1,143 +0,0 @@
<UserControl x:Class="Ink_Canvas.Helpers.Plugins.BuiltIn.SuperLauncher.LauncherSettingsControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:Ink_Canvas.Helpers.Plugins.BuiltIn.SuperLauncher"
mc:Ignorable="d"
d:DesignHeight="500" d:DesignWidth="600">
<UserControl.Resources>
<!-- 自定义按钮样式 -->
<Style x:Key="DefaultButtonStyle" TargetType="Button">
<Setter Property="FontWeight" Value="SemiBold"/>
<Setter Property="FontSize" Value="12"/>
<Setter Property="Cursor" Value="Hand"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border x:Name="border"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="4"
Padding="{TemplateBinding Padding}">
<ContentPresenter HorizontalAlignment="Center"
VerticalAlignment="Center"
TextElement.Foreground="{TemplateBinding Foreground}"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="border" Property="Opacity" Value="0.8"/>
<Setter Property="Effect">
<Setter.Value>
<DropShadowEffect Color="Black" Direction="270" ShadowDepth="2" Opacity="0.3" BlurRadius="4"/>
</Setter.Value>
</Setter>
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter TargetName="border" Property="Opacity" Value="0.6"/>
<Setter Property="RenderTransform">
<Setter.Value>
<ScaleTransform ScaleX="0.95" ScaleY="0.95"/>
</Setter.Value>
</Setter>
<Setter Property="RenderTransformOrigin" Value="0.5,0.5"/>
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter TargetName="border" Property="Opacity" Value="0.4"/>
<Setter Property="Cursor" Value="Arrow"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</UserControl.Resources>
<Grid Margin="10">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- 标题 -->
<TextBlock Grid.Row="0" Text="超级启动台设置" FontSize="16" FontWeight="Bold" Margin="0,0,0,15" Foreground="Black"/>
<!-- 基本设置 -->
<StackPanel Grid.Row="1" Margin="0,0,0,15">
<TextBlock Text="基本设置" FontSize="14" FontWeight="SemiBold" Margin="0,0,0,10" Foreground="Black"/>
<Grid Margin="10,0,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="120"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- 按钮位置 -->
<TextBlock Grid.Row="0" Grid.Column="0" Text="按钮位置:" VerticalAlignment="Center" Foreground="Black"/>
<StackPanel Grid.Row="0" Grid.Column="1" Orientation="Horizontal" Margin="0,5">
<RadioButton x:Name="RbtnLeft" Content="浮动栏左侧" Margin="0,0,20,0" Checked="RbtnPosition_Checked" Foreground="Black"/>
<RadioButton x:Name="RbtnRight" Content="浮动栏右侧" IsChecked="True" Checked="RbtnPosition_Checked" Foreground="Black"/>
</StackPanel>
</Grid>
</StackPanel>
<!-- 应用管理 -->
<Grid Grid.Row="2">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Text="应用管理" FontSize="14" FontWeight="SemiBold" Margin="0,0,0,10" Foreground="Black"/>
<Border Grid.Row="1" BorderThickness="1" BorderBrush="#CCCCCC" CornerRadius="5">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- 应用列表 -->
<DataGrid Grid.Row="0" x:Name="DgApps" AutoGenerateColumns="False" Margin="5"
CanUserAddRows="False" CanUserDeleteRows="False"
HeadersVisibility="Column" SelectionMode="Single"
SelectionChanged="DgApps_SelectionChanged">
<DataGrid.Columns>
<DataGridCheckBoxColumn Header="显示" Binding="{Binding IsVisible}" Width="50"/>
<DataGridTextColumn Header="名称" Binding="{Binding Name}" Width="150"/>
<DataGridTextColumn Header="路径" Binding="{Binding Path}" Width="*"/>
<DataGridTextColumn Header="位置" Binding="{Binding Position}" Width="50"/>
</DataGrid.Columns>
</DataGrid>
<!-- 操作按钮 -->
<StackPanel Grid.Row="1" Orientation="Horizontal" Margin="5">
<Button x:Name="BtnAdd" Content="添加" Padding="10,5" Margin="0,5,5,5" Click="BtnAdd_Click"
Background="#FF007ACC" Foreground="White" BorderBrush="#FF005A9B" BorderThickness="1"
Style="{StaticResource DefaultButtonStyle}"/>
<Button x:Name="BtnEdit" Content="编辑" Padding="10,5" Margin="5" Click="BtnEdit_Click"
Background="#FF6C757D" Foreground="White" BorderBrush="#FF5A6268" BorderThickness="1"
Style="{StaticResource DefaultButtonStyle}"/>
<Button x:Name="BtnDelete" Content="删除" Padding="10,5" Margin="5" Click="BtnDelete_Click"
Background="#FFDC3545" Foreground="White" BorderBrush="#FFBD2130" BorderThickness="1"
Style="{StaticResource DefaultButtonStyle}"/>
</StackPanel>
</Grid>
</Border>
</Grid>
<!-- 底部按钮 -->
<StackPanel Grid.Row="3" Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,15,0,0">
<Button x:Name="BtnSave" Content="保存设置" Padding="15,5" Click="BtnSave_Click"
Background="#FF28A745" Foreground="White" BorderBrush="#FF1E7E34" BorderThickness="1"
Style="{StaticResource DefaultButtonStyle}"/>
</StackPanel>
</Grid>
</UserControl>
@@ -1,396 +0,0 @@
using Ink_Canvas.Windows;
using Microsoft.Win32;
using System;
using System.ComponentModel;
using System.IO;
using System.Windows;
using System.Windows.Controls;
namespace Ink_Canvas.Helpers.Plugins.BuiltIn.SuperLauncher
{
/// <summary>
/// LauncherSettingsControl.xaml 的交互逻辑
/// </summary>
public partial class LauncherSettingsControl : UserControl
{
/// <summary>
/// 父插件
/// </summary>
private readonly SuperLauncherPlugin _plugin;
/// <summary>
/// 构造函数
/// </summary>
/// <param name="plugin">父插件</param>
public LauncherSettingsControl(SuperLauncherPlugin plugin)
{
InitializeComponent();
_plugin = plugin;
// 设置按钮位置
RbtnLeft.IsChecked = _plugin.Config.ButtonPosition == LauncherButtonPosition.Left;
RbtnRight.IsChecked = _plugin.Config.ButtonPosition == LauncherButtonPosition.Right;
// 绑定应用列表
DgApps.ItemsSource = _plugin.LauncherItems;
// 初始化按钮状态
UpdateButtonStates();
}
/// <summary>
/// 更新按钮状态
/// </summary>
private void UpdateButtonStates()
{
bool hasSelection = DgApps.SelectedItem != null;
BtnEdit.IsEnabled = hasSelection;
BtnDelete.IsEnabled = hasSelection;
}
/// <summary>
/// 位置单选按钮选择事件
/// </summary>
private void RbtnPosition_Checked(object sender, RoutedEventArgs e)
{
if (!IsLoaded) return;
LauncherButtonPosition oldPosition = _plugin.Config.ButtonPosition;
if (sender == RbtnLeft)
{
_plugin.Config.ButtonPosition = LauncherButtonPosition.Left;
}
else if (sender == RbtnRight)
{
_plugin.Config.ButtonPosition = LauncherButtonPosition.Right;
}
// 如果位置发生变化,更新按钮位置
if (oldPosition != _plugin.Config.ButtonPosition)
{
try
{
// 更新按钮位置
_plugin.UpdateButtonPosition();
// 保存配置
_plugin.SaveConfig();
LogHelper.WriteLogToFile($"启动台按钮位置已更改为: {_plugin.Config.ButtonPosition}");
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"更新启动台按钮位置时出错: {ex.Message}", LogHelper.LogType.Error);
MessageBox.Show($"更新启动台按钮位置时出错: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
}
/// <summary>
/// 添加按钮点击事件
/// </summary>
private void BtnAdd_Click(object sender, RoutedEventArgs e)
{
try
{
// 创建新的启动项
LauncherItem item = new LauncherItem
{
Name = "",
Path = "",
IsVisible = true,
Position = -1 // 让插件管理器分配位置
};
// 直接显示编辑对话框
EditLauncherItem(item, true);
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"添加启动项时出错: {ex.Message}", LogHelper.LogType.Error);
MessageBox.Show($"添加启动项时出错: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
/// <summary>
/// 编辑应用按钮点击事件
/// </summary>
private void BtnEdit_Click(object sender, RoutedEventArgs e)
{
if (DgApps.SelectedItem is LauncherItem item)
{
EditLauncherItem(item, false);
}
}
/// <summary>
/// 删除应用按钮点击事件
/// </summary>
private void BtnDelete_Click(object sender, RoutedEventArgs e)
{
if (DgApps.SelectedItem is LauncherItem item)
{
// 确认删除
MessageBoxResult result = MessageBox.Show(
$"确定要删除 {item.Name} 吗?",
"删除确认",
MessageBoxButton.YesNo,
MessageBoxImage.Question);
if (result == MessageBoxResult.Yes)
{
// 从集合中移除
_plugin.LauncherItems.Remove(item);
// 保存配置
_plugin.SaveConfig();
}
}
}
/// <summary>
/// 保存设置按钮点击事件
/// </summary>
private void BtnSave_Click(object sender, RoutedEventArgs e)
{
try
{
// 保存配置
_plugin.SaveConfig();
// 如果插件已启用,重新加载启动台按钮
if (_plugin.IsEnabled)
{
_plugin.Disable();
_plugin.Enable();
}
else
{
// 如果插件未启用,则启用它
_plugin.Enable();
// 通知PluginSettingsWindow刷新插件列表
var window = Window.GetWindow(this);
if (window is PluginSettingsWindow pluginSettingsWindow)
{
// 触发刷新
pluginSettingsWindow.RefreshPluginList();
}
}
MessageBox.Show("设置已保存并应用!", "提示", MessageBoxButton.OK, MessageBoxImage.Information);
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"保存设置时出错: {ex.Message}", LogHelper.LogType.Error);
MessageBox.Show($"保存设置时出错: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
/// <summary>
/// 应用项选择变更事件
/// </summary>
private void DgApps_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
UpdateButtonStates();
}
/// <summary>
/// 编辑启动项
/// </summary>
/// <param name="item">启动项</param>
/// <param name="isNew">是否为新建</param>
private void EditLauncherItem(LauncherItem item, bool isNew)
{
// 创建简单的编辑窗口
Window editWindow = new Window
{
Title = isNew ? "添加" : "编辑应用",
Width = 400,
Height = 200,
WindowStartupLocation = WindowStartupLocation.CenterScreen,
ResizeMode = ResizeMode.NoResize
};
// 创建编辑表单
Grid grid = new Grid
{
Margin = new Thickness(20)
};
grid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
grid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
grid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Star) });
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(80) });
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
// 名称输入框
TextBlock nameLabel = new TextBlock
{
Text = "名称:",
VerticalAlignment = VerticalAlignment.Center
};
TextBox nameTextBox = new TextBox
{
Text = item.Name,
Margin = new Thickness(0, 5, 0, 5)
};
Grid.SetRow(nameLabel, 0);
Grid.SetColumn(nameLabel, 0);
Grid.SetRow(nameTextBox, 0);
Grid.SetColumn(nameTextBox, 1);
grid.Children.Add(nameLabel);
grid.Children.Add(nameTextBox);
// 路径输入框
TextBlock pathLabel = new TextBlock
{
Text = "路径:",
VerticalAlignment = VerticalAlignment.Center
};
Grid pathGrid = new Grid();
pathGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
pathGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength() });
TextBox pathTextBox = new TextBox
{
Text = item.Path,
Margin = new Thickness(0, 5, 5, 5)
};
Button browseButton = new Button
{
Content = "浏览",
Padding = new Thickness(5, 0, 5, 0),
Margin = new Thickness(0, 5, 0, 5)
};
browseButton.Click += (s, e) =>
{
OpenFileDialog dialog = new OpenFileDialog
{
Title = "选择应用程序",
Filter = "应用程序 (*.exe)|*.exe|所有文件 (*.*)|*.*",
Multiselect = false,
FileName = pathTextBox.Text
};
if (dialog.ShowDialog() == true)
{
pathTextBox.Text = dialog.FileName;
// 如果选择的是.exe文件,自动获取文件名填入名称字段
if (Path.GetExtension(dialog.FileName).ToLower() == ".exe")
{
string fileName = Path.GetFileNameWithoutExtension(dialog.FileName);
// 只有在名称字段为空或者是新建项目时才自动填入
if (string.IsNullOrWhiteSpace(nameTextBox.Text) || isNew)
{
nameTextBox.Text = fileName;
}
}
}
};
Grid.SetColumn(pathTextBox, 0);
Grid.SetColumn(browseButton, 1);
pathGrid.Children.Add(pathTextBox);
pathGrid.Children.Add(browseButton);
Grid.SetRow(pathLabel, 1);
Grid.SetColumn(pathLabel, 0);
Grid.SetRow(pathGrid, 1);
Grid.SetColumn(pathGrid, 1);
grid.Children.Add(pathLabel);
grid.Children.Add(pathGrid);
// 确认和取消按钮
StackPanel buttonPanel = new StackPanel
{
Orientation = Orientation.Horizontal,
HorizontalAlignment = HorizontalAlignment.Right,
Margin = new Thickness(0, 10, 0, 0)
};
Button okButton = new Button
{
Content = "确定",
Padding = new Thickness(15, 5, 15, 5),
Margin = new Thickness(0, 0, 10, 0),
IsDefault = true
};
Button cancelButton = new Button
{
Content = "取消",
Padding = new Thickness(15, 5, 15, 5),
IsCancel = true
};
okButton.Click += (s, e) =>
{
// 验证输入
if (string.IsNullOrWhiteSpace(nameTextBox.Text))
{
MessageBox.Show("请输入应用名称!", "提示", MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
if (string.IsNullOrWhiteSpace(pathTextBox.Text))
{
MessageBox.Show("请输入应用路径!", "提示", MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
// 更新项目
item.Name = nameTextBox.Text;
item.Path = pathTextBox.Text;
// 如果是新建,添加到集合
if (isNew)
{
_plugin.AddLauncherItem(item);
}
else
{
// 触发属性变更通知,刷新DataGrid
if (DgApps.ItemsSource is ICollectionView view)
{
view.Refresh();
}
// 保存配置
_plugin.SaveConfig();
}
editWindow.DialogResult = true;
editWindow.Close();
};
cancelButton.Click += (s, e) =>
{
editWindow.DialogResult = false;
editWindow.Close();
};
buttonPanel.Children.Add(okButton);
buttonPanel.Children.Add(cancelButton);
Grid.SetRow(buttonPanel, 2);
Grid.SetColumnSpan(buttonPanel, 2);
grid.Children.Add(buttonPanel);
// 设置窗口内容
editWindow.Content = grid;
// 显示窗口
editWindow.ShowDialog();
}
}
}
@@ -1,91 +0,0 @@
<Window x:Class="Ink_Canvas.Helpers.Plugins.BuiltIn.SuperLauncher.LauncherWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
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:local="clr-namespace:Ink_Canvas.Helpers.Plugins.BuiltIn.SuperLauncher"
mc:Ignorable="d"
Title="启动台"
Width="400"
Height="300"
WindowStyle="None"
AllowsTransparency="True"
Background="#80000000"
ResizeMode="NoResize"
Topmost="True"
Deactivated="Window_Deactivated"
ShowInTaskbar="False">
<Window.Resources>
<!-- 应用项样式 -->
<Style x:Key="LauncherItemStyle" TargetType="Button">
<Setter Property="Width" Value="80"/>
<Setter Property="Height" Value="80"/>
<Setter Property="Margin" Value="5"/>
<Setter Property="Background" Value="#40FFFFFF"/>
<Setter Property="Foreground" Value="White"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border x:Name="border" Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="8">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Image Grid.Row="0" Source="{Binding Icon}" Width="32" Height="32" Margin="0,10,0,5"/>
<TextBlock Grid.Row="1" Text="{Binding Name}" TextWrapping="Wrap" TextAlignment="Center"
Margin="2,0,2,8" FontSize="11" Foreground="White"/>
</Grid>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="#80FFFFFF"/>
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter Property="Background" Value="#C0FFFFFF"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Window.Resources>
<Border CornerRadius="15" Background="#80000000" BorderThickness="1" BorderBrush="#40FFFFFF">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<!-- 标题栏 -->
<Grid Grid.Row="0" Height="40">
<TextBlock Text="启动台" Foreground="White" FontSize="18" FontWeight="Bold"
VerticalAlignment="Center" Margin="15,0,0,0"/>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,0,10,0">
<Button x:Name="BtnFixMode" Click="BtnFixMode_Click" Width="30" Height="30"
Margin="5,0" Background="Transparent" BorderThickness="0"
ToolTip="切换固定模式">
<Path x:Name="FixModeIcon" Data="M7,2V13H10V22L17,10H13L17,2H7Z" Fill="White" Stretch="Uniform" Width="16" Height="16"/>
</Button>
<Button x:Name="BtnClose" Click="BtnClose_Click" Width="30" Height="30"
Background="Transparent" BorderThickness="0"
ToolTip="关闭">
<Path Data="M19,6.41L17.59,5L12,10.59L6.41,5L5,6.41L10.59,12L5,17.59L6.41,19L12,13.41L17.59,19L19,17.59L13.41,12L19,6.41Z"
Fill="White" Stretch="Uniform" Width="16" Height="16"/>
</Button>
</StackPanel>
</Grid>
<!-- 应用网格 -->
<ScrollViewer Grid.Row="1" Margin="10" VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled">
<WrapPanel x:Name="AppPanel" Orientation="Horizontal" HorizontalAlignment="Center"/>
</ScrollViewer>
</Grid>
</Border>
</Window>
@@ -1,466 +0,0 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Threading;
namespace Ink_Canvas.Helpers.Plugins.BuiltIn.SuperLauncher
{
/// <summary>
/// LauncherWindow.xaml 的交互逻辑
/// </summary>
public partial class LauncherWindow : Window
{
/// <summary>
/// 父插件
/// </summary>
private readonly SuperLauncherPlugin _plugin;
/// <summary>
/// 是否处于固定模式
/// </summary>
private bool _isFixMode;
/// <summary>
/// 应用项按钮列表
/// </summary>
private readonly Dictionary<Button, LauncherItem> _appButtons = new Dictionary<Button, LauncherItem>();
/// <summary>
/// 拖拽中的按钮
/// </summary>
private Button _draggingButton;
/// <summary>
/// 拖拽开始位置
/// </summary>
private Point _dragStartPoint;
/// <summary>
/// 构造函数
/// </summary>
public LauncherWindow(SuperLauncherPlugin plugin)
{
InitializeComponent();
_plugin = plugin;
// 加载应用项
LoadLauncherItems();
// 添加鼠标按下事件(用于拖动窗口)
MouseDown += (s, e) =>
{
if (e.ChangedButton == MouseButton.Left && e.ButtonState == MouseButtonState.Pressed)
{
DragMove();
}
};
// 根据应用数量调整窗口大小
AdjustWindowSize();
}
/// <summary>
/// 加载启动台应用项
/// </summary>
private void LoadLauncherItems()
{
// 清空现有应用项
AppPanel.Children.Clear();
_appButtons.Clear();
// 获取显示的应用项
var visibleItems = _plugin.LauncherItems
.Where(item => item.IsVisible)
.OrderBy(item => item.Position)
.ToList();
foreach (var item in visibleItems)
{
// 创建应用按钮
Button appButton = new Button
{
Style = (Style)FindResource("LauncherItemStyle"),
DataContext = item,
Tag = item.Position
};
// 添加点击事件
appButton.Click += AppButton_Click;
// 在固定模式下,添加拖拽事件
appButton.PreviewMouseDown += AppButton_PreviewMouseDown;
appButton.PreviewMouseMove += AppButton_PreviewMouseMove;
appButton.PreviewMouseUp += AppButton_PreviewMouseUp;
// 记录按钮和项目的对应关系
_appButtons.Add(appButton, item);
// 添加到面板
AppPanel.Children.Add(appButton);
}
}
/// <summary>
/// 根据应用数量调整窗口大小
/// </summary>
private void AdjustWindowSize()
{
try
{
// 每行最多显示4个应用
const int appsPerRow = 4;
// 计算行数
int visibleCount = _appButtons.Count;
int rowCount = (int)Math.Ceiling(visibleCount / (double)appsPerRow);
// 设置窗口宽度(每个应用90像素宽 = 80 + 5*2
Width = Math.Min(appsPerRow * 90 + 40, 400); // 最大宽度400
// 设置窗口高度(每个应用90像素高 = 80 + 5*2
Height = Math.Min(rowCount * 90 + 60, 600); // 最大高度600,标题栏40 + 边距20
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"调整启动台窗口大小时出错: {ex.Message}", LogHelper.LogType.Error);
}
}
/// <summary>
/// 应用按钮点击事件
/// </summary>
private void AppButton_Click(object sender, RoutedEventArgs e)
{
try
{
if (_isFixMode) return; // 在固定模式下,不响应点击事件
if (sender is Button button && _appButtons.TryGetValue(button, out LauncherItem item))
{
// 获取应用路径和名称,用于后续启动
string appPath = item.Path;
string appName = item.Name;
LogHelper.WriteLogToFile($"点击启动应用: {appName}, 路径: {appPath}");
// 首先标记窗口正在关闭
IsClosing = true;
// 创建一个应用启动任务
var launchTask = new Task(() =>
{
try
{
// 等待一段时间,确保窗口关闭流程已经开始
Thread.Sleep(200);
// 使用UI线程启动应用
Application.Current.Dispatcher.Invoke(() =>
{
try
{
// 检查应用路径是否存在
if (File.Exists(appPath) || !appPath.Contains(":\\"))
{
// 创建进程启动信息
var psi = new ProcessStartInfo
{
FileName = appPath,
UseShellExecute = true,
};
// 启动应用程序
var process = Process.Start(psi);
LogHelper.WriteLogToFile($"应用程序 {appName} 已启动");
}
else
{
LogHelper.WriteLogToFile($"应用路径不存在: {appPath}", LogHelper.LogType.Error);
MessageBox.Show($"找不到应用程序: {appPath}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"启动应用程序失败: {ex.Message}", LogHelper.LogType.Error);
MessageBox.Show($"启动应用程序失败: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
}
});
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"应用启动任务出错: {ex.Message}", LogHelper.LogType.Error);
}
});
// 关闭窗口
try
{
Dispatcher.BeginInvoke(new Action(() =>
{
try { Close(); } catch { }
// 启动应用程序任务
launchTask.Start();
}), DispatcherPriority.Background);
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"关闭窗口或启动任务时出错: {ex.Message}", LogHelper.LogType.Error);
// 如果无法通过UI关闭窗口,直接启动任务
launchTask.Start();
}
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"应用按钮点击事件出错: {ex.Message}", LogHelper.LogType.Error);
try { IsClosing = true; Close(); } catch { }
}
}
#region
/// <summary>
/// 应用按钮鼠标按下事件
/// </summary>
private void AppButton_PreviewMouseDown(object sender, MouseButtonEventArgs e)
{
if (!_isFixMode) return;
if (e.ChangedButton == MouseButton.Left && sender is Button button)
{
_draggingButton = button;
_dragStartPoint = e.GetPosition(AppPanel);
button.CaptureMouse();
button.Opacity = 0.7;
// 阻止事件冒泡,以避免触发按钮点击
e.Handled = true;
}
}
/// <summary>
/// 应用按钮鼠标移动事件
/// </summary>
private void AppButton_PreviewMouseMove(object sender, MouseEventArgs e)
{
if (!_isFixMode || _draggingButton == null) return;
if (e.LeftButton == MouseButtonState.Pressed)
{
Point currentPosition = e.GetPosition(AppPanel);
// 移动按钮
System.Windows.Controls.Canvas.SetLeft(_draggingButton, currentPosition.X - _draggingButton.ActualWidth / 2);
System.Windows.Controls.Canvas.SetTop(_draggingButton, currentPosition.Y - _draggingButton.ActualHeight / 2);
// 将按钮移到最上层
Panel.SetZIndex(_draggingButton, 100);
// 阻止事件冒泡
e.Handled = true;
}
}
/// <summary>
/// 应用按钮鼠标释放事件
/// </summary>
private void AppButton_PreviewMouseUp(object sender, MouseButtonEventArgs e)
{
if (!_isFixMode || _draggingButton == null) return;
// 释放鼠标捕获
_draggingButton.ReleaseMouseCapture();
// 计算新位置
Point releasePoint = e.GetPosition(AppPanel);
int newPosition = CalculateGridPosition(releasePoint);
// 获取当前项目
LauncherItem currentItem = _appButtons[_draggingButton];
// 重新排序
ReorderItems(currentItem, newPosition);
// 重新加载应用项
LoadLauncherItems();
// 保存配置
_plugin.SaveConfig();
// 清除拖拽状态
_draggingButton.Opacity = 1;
Panel.SetZIndex(_draggingButton, 0);
_draggingButton = null;
// 阻止事件冒泡
e.Handled = true;
}
/// <summary>
/// 计算网格位置
/// </summary>
private int CalculateGridPosition(Point point)
{
// 计算行和列
int columnCount = 4; // 每行最多4个应用
int columnWidth = 90; // 应用宽度(包括边距)
int rowHeight = 90; // 应用高度(包括边距)
int column = (int)(point.X / columnWidth);
int row = (int)(point.Y / rowHeight);
// 确保在有效范围内
column = Math.Max(0, Math.Min(column, columnCount - 1));
row = Math.Max(0, row);
// 计算位置索引
return row * columnCount + column;
}
/// <summary>
/// 重新排序应用项
/// </summary>
private void ReorderItems(LauncherItem item, int newPosition)
{
try
{
// 设置项目为固定位置
item.IsPositionFixed = true;
// 如果位置相同,无需调整
if (item.Position == newPosition)
{
return;
}
// 获取所有可见项目
var visibleItems = _plugin.LauncherItems
.Where(i => i.IsVisible)
.OrderBy(i => i.Position)
.ToList();
// 移除当前项目
visibleItems.Remove(item);
// 查找插入位置
int insertIndex = 0;
for (int i = 0; i < visibleItems.Count; i++)
{
if (visibleItems[i].Position >= newPosition)
{
insertIndex = i;
break;
}
insertIndex = i + 1;
}
// 插入项目
visibleItems.Insert(insertIndex, item);
// 重新分配位置
for (int i = 0; i < visibleItems.Count; i++)
{
visibleItems[i].Position = i;
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"重新排序应用项时出错: {ex.Message}", LogHelper.LogType.Error);
}
}
#endregion
#region
/// <summary>
/// 窗口失去焦点事件
/// </summary>
private void Window_Deactivated(object sender, EventArgs e)
{
try
{
// 只有在非固定模式、窗口已加载、未处于关闭状态且IsLoaded=true时关闭窗口
if (!_isFixMode && IsLoaded && !IsClosing)
{
// 标记为正在关闭
IsClosing = true;
// 使用Dispatcher.BeginInvoke而不是直接调用Close,避免冲突
Dispatcher.BeginInvoke(new Action(() =>
{
try
{
// 再次检查窗口状态
if (IsLoaded && !IsClosing)
{
Close();
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"延迟关闭窗口时出错: {ex.Message}", LogHelper.LogType.Error);
}
}), DispatcherPriority.Background);
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"窗口失去焦点关闭时出错: {ex.Message}", LogHelper.LogType.Error);
}
}
/// <summary>
/// 窗口是否正在关闭
/// </summary>
private bool IsClosing { get; set; }
/// <summary>
/// 重写OnClosing方法,标记窗口正在关闭
/// </summary>
protected override void OnClosing(CancelEventArgs e)
{
IsClosing = true;
base.OnClosing(e);
}
/// <summary>
/// 关闭按钮点击事件
/// </summary>
private void BtnClose_Click(object sender, RoutedEventArgs e)
{
Close();
}
/// <summary>
/// 固定模式按钮点击事件
/// </summary>
private void BtnFixMode_Click(object sender, RoutedEventArgs e)
{
// 切换固定模式
_isFixMode = !_isFixMode;
// 更新固定模式按钮图标颜色
FixModeIcon.Fill = _isFixMode ? Brushes.Yellow : Brushes.White;
// 显示提示
if (_isFixMode)
{
MessageBox.Show("已进入固定模式,您可以拖动应用图标调整位置。", "提示", MessageBoxButton.OK, MessageBoxImage.Information);
}
}
#endregion
}
}
@@ -1,589 +0,0 @@
using Ink_Canvas.Helpers.Plugins.BuiltIn.SuperLauncher;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
namespace Ink_Canvas.Helpers.Plugins.BuiltIn
{
/// <summary>
/// 超级启动台插件
/// </summary>
public class SuperLauncherPlugin : PluginBase
{
#region
public override string Name => "超级启动台";
public override string Description => "在浮动栏添加一个启动台按钮,可快速启动常用应用程序。";
public override Version Version => new Version(1, 0, 1);
public override string Author => "ICC CE 团队";
public override bool IsBuiltIn => true;
#endregion
#region
/// <summary>
/// 启动台配置
/// </summary>
public LauncherConfig Config { get; private set; }
/// <summary>
/// 启动台应用程序列表
/// </summary>
public ObservableCollection<LauncherItem> LauncherItems { get; private set; }
/// <summary>
/// 启动台按钮
/// </summary>
private LauncherButton _launcherButton;
/// <summary>
/// 启动台窗口
/// </summary>
private LauncherWindow _launcherWindow;
/// <summary>
/// 配置文件路径
/// </summary>
private readonly string _configPath = Path.Combine(App.RootPath, "PluginConfigs", "SuperLauncher.json");
/// <summary>
/// 标记是否已添加到浮动栏
/// </summary>
private bool _isAddedToFloatingBar;
#endregion
#region
public override void Initialize()
{
try
{
base.Initialize();
// 创建配置目录
string configDir = Path.Combine(App.RootPath, "PluginConfigs");
if (!Directory.Exists(configDir))
{
Directory.CreateDirectory(configDir);
}
// 加载配置
LoadConfig();
LogHelper.WriteLogToFile("超级启动台插件已初始化");
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"初始化超级启动台插件时出错: {ex.Message}", LogHelper.LogType.Error);
LogHelper.NewLog(ex);
}
}
public override void Enable()
{
try
{
if (IsEnabled) return; // 防止重复启用
// 创建启动台按钮
if (_launcherButton == null)
{
_launcherButton = new LauncherButton(this);
LogHelper.WriteLogToFile("超级启动台按钮已创建");
}
// 添加启动台按钮到浮动栏
AddLauncherButtonToFloatingBar();
// 设置启用状态
base.Enable();
// 保存插件配置
SavePluginSettings();
LogHelper.WriteLogToFile("超级启动台插件已启用");
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"启用超级启动台插件时出错: {ex.Message}", LogHelper.LogType.Error);
LogHelper.NewLog(ex);
}
}
public override void Disable()
{
try
{
if (!IsEnabled) return; // 防止重复禁用
// 从浮动栏移除启动台按钮
RemoveLauncherButtonFromFloatingBar();
// 如果启动台窗口打开,则关闭
if (_launcherWindow != null && _launcherWindow.IsVisible)
{
_launcherWindow.Close();
_launcherWindow = null;
}
// 设置禁用状态
base.Disable();
// 保存插件配置
SavePluginSettings();
LogHelper.WriteLogToFile("超级启动台插件已禁用");
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"禁用超级启动台插件时出错: {ex.Message}", LogHelper.LogType.Error);
LogHelper.NewLog(ex);
}
}
public override UserControl GetSettingsView()
{
return new LauncherSettingsControl(this);
}
public override void Cleanup()
{
// 保存配置
SaveConfig();
// 从浮动栏移除启动台按钮
RemoveLauncherButtonFromFloatingBar();
// 如果启动台窗口打开,则关闭
if (_launcherWindow != null && _launcherWindow.IsVisible)
{
_launcherWindow.Close();
_launcherWindow = null;
}
base.Cleanup();
}
/// <summary>
/// 保存插件设置
/// </summary>
public override void SavePluginSettings()
{
try
{
// 确保配置已加载
if (Config == null)
{
LoadConfig();
}
// 更新其他设置,但不更改插件启用状态
// 保存配置
SaveConfig();
LogHelper.WriteLogToFile("超级启动台插件设置已保存");
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"保存超级启动台插件设置时出错: {ex.Message}", LogHelper.LogType.Error);
}
}
#endregion
#region
/// <summary>
/// 加载配置
/// </summary>
private void LoadConfig()
{
try
{
if (File.Exists(_configPath))
{
string json = File.ReadAllText(_configPath);
Config = JsonConvert.DeserializeObject<LauncherConfig>(json) ?? CreateDefaultConfig();
LauncherItems = new ObservableCollection<LauncherItem>(Config.Items ?? new List<LauncherItem>());
// 注意:不再根据配置更改插件启用状态
// 插件状态由PluginManager统一管理
}
else
{
Config = CreateDefaultConfig();
LauncherItems = new ObservableCollection<LauncherItem>(Config.Items);
SaveConfig();
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"加载超级启动台配置时出错: {ex.Message}", LogHelper.LogType.Error);
Config = CreateDefaultConfig();
LauncherItems = new ObservableCollection<LauncherItem>(Config.Items);
}
}
/// <summary>
/// 保存配置
/// </summary>
public void SaveConfig()
{
try
{
// 同步LauncherItems到Config
Config.Items = new List<LauncherItem>(LauncherItems);
// 序列化并保存配置
string json = JsonConvert.SerializeObject(Config, Formatting.Indented);
File.WriteAllText(_configPath, json);
LogHelper.WriteLogToFile("超级启动台配置已保存");
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"保存超级启动台配置时出错: {ex.Message}", LogHelper.LogType.Error);
}
}
/// <summary>
/// 创建默认配置
/// </summary>
private LauncherConfig CreateDefaultConfig()
{
var config = new LauncherConfig
{
ButtonPosition = LauncherButtonPosition.Right,
// 不再使用IsEnabled,插件状态由PluginManager管理
Items = new List<LauncherItem>
{
new LauncherItem
{
Name = "资源管理器",
Path = @"C:\Windows\explorer.exe",
IsVisible = true,
Position = 0
}
}
};
return config;
}
#endregion
#region
/// <summary>
/// 将启动台按钮添加到浮动栏
/// </summary>
private void AddLauncherButtonToFloatingBar()
{
try
{
// 如果已经添加,先移除
if (_isAddedToFloatingBar)
{
RemoveLauncherButtonFromFloatingBar();
_isAddedToFloatingBar = false;
}
// 获取主窗口实例
var mainWindow = Application.Current.MainWindow;
if (mainWindow == null)
{
LogHelper.WriteLogToFile("未找到主窗口实例,无法添加启动台按钮", LogHelper.LogType.Error);
return;
}
// 创建启动台按钮
_launcherButton = new LauncherButton(this);
var buttonElement = _launcherButton.Element;
// 查找浮动栏
var floatingBar = mainWindow.FindName("StackPanelFloatingBar") as Panel;
if (floatingBar == null)
{
// 如果直接查找失败,则尝试遍历可视树查找
Panel floatingBarPanelFromTree = null;
FindStackPanelFloatingBar(mainWindow, ref floatingBarPanelFromTree);
floatingBar = floatingBarPanelFromTree;
}
if (floatingBar == null)
{
LogHelper.WriteLogToFile("未找到浮动栏,无法添加启动台按钮", LogHelper.LogType.Error);
return;
}
// 添加启动台按钮到浮动栏
if (Config.ButtonPosition == LauncherButtonPosition.Left)
{
floatingBar.Children.Insert(0, buttonElement);
LogHelper.WriteLogToFile("启动台按钮已添加到浮动栏左侧");
}
else
{
floatingBar.Children.Add(buttonElement);
LogHelper.WriteLogToFile("启动台按钮已添加到浮动栏右侧");
}
_isAddedToFloatingBar = true;
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"添加启动台按钮到浮动栏时出错: {ex.Message}", LogHelper.LogType.Error);
LogHelper.NewLog(ex);
}
}
/// <summary>
/// 递归查找StackPanelFloatingBar
/// </summary>
private void FindStackPanelFloatingBar(DependencyObject parent, ref Panel result)
{
if (parent == null || result != null) return;
try
{
// 检查当前对象是否为我们要找的面板
if (parent is Panel panel && panel.Name == "StackPanelFloatingBar")
{
result = panel;
return;
}
// 获取子元素数量
int childCount = VisualTreeHelper.GetChildrenCount(parent);
// 遍历所有子元素
for (int i = 0; i < childCount; i++)
{
DependencyObject child = VisualTreeHelper.GetChild(parent, i);
FindStackPanelFloatingBar(child, ref result);
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"查找StackPanelFloatingBar时出错: {ex.Message}", LogHelper.LogType.Error);
}
}
/// <summary>
/// 从浮动栏移除启动台按钮
/// </summary>
private void RemoveLauncherButtonFromFloatingBar()
{
try
{
if (!_isAddedToFloatingBar || _launcherButton == null)
{
return;
}
// 获取主窗口实例
var mainWindow = Application.Current.MainWindow;
if (mainWindow == null)
{
LogHelper.WriteLogToFile("未找到主窗口实例,无法移除启动台按钮", LogHelper.LogType.Error);
return;
}
// 获取按钮元素
var buttonElement = _launcherButton.Element;
// 查找浮动栏
var floatingBar = mainWindow.FindName("StackPanelFloatingBar") as Panel;
if (floatingBar == null)
{
// 如果直接查找失败,则尝试遍历可视树查找
Panel floatingBarPanelFromTree = null;
FindStackPanelFloatingBar(mainWindow, ref floatingBarPanelFromTree);
floatingBar = floatingBarPanelFromTree;
}
if (floatingBar == null)
{
LogHelper.WriteLogToFile("未找到浮动栏,无法移除启动台按钮", LogHelper.LogType.Error);
return;
}
// 从浮动栏移除启动台按钮
if (floatingBar.Children.Contains(buttonElement))
{
floatingBar.Children.Remove(buttonElement);
LogHelper.WriteLogToFile("启动台按钮已从浮动栏移除");
}
_isAddedToFloatingBar = false;
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"移除启动台按钮时出错: {ex.Message}", LogHelper.LogType.Error);
LogHelper.NewLog(ex);
}
}
/// <summary>
/// 更新启动台按钮位置
/// </summary>
public void UpdateButtonPosition()
{
try
{
// 如果按钮已添加到浮动栏,重新添加以更新位置
if (_isAddedToFloatingBar)
{
RemoveLauncherButtonFromFloatingBar();
AddLauncherButtonToFloatingBar();
LogHelper.WriteLogToFile($"启动台按钮位置已更新为: {Config.ButtonPosition}");
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"更新启动台按钮位置时出错: {ex.Message}", LogHelper.LogType.Error);
LogHelper.NewLog(ex);
}
}
#endregion
#region
/// <summary>
/// 显示启动台窗口
/// </summary>
/// <param name="buttonPosition">按钮在屏幕上的位置</param>
public void ShowLauncherWindow(Point buttonPosition)
{
try
{
// 如果窗口已存在,关闭它
if (_launcherWindow != null && _launcherWindow.IsVisible)
{
_launcherWindow.Close();
_launcherWindow = null;
return;
}
// 创建新的启动台窗口
_launcherWindow = new LauncherWindow(this);
// 计算窗口位置,使其位于按钮上方
PositionLauncherWindow(_launcherWindow, buttonPosition);
// 显示窗口
_launcherWindow.Show();
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"显示启动台窗口时出错: {ex.Message}", LogHelper.LogType.Error);
}
}
/// <summary>
/// 设置启动台窗口位置
/// </summary>
/// <param name="window">启动台窗口</param>
/// <param name="buttonPosition">按钮在屏幕上的位置</param>
private void PositionLauncherWindow(LauncherWindow window, Point buttonPosition)
{
// 确保窗口已加载
if (window.ActualWidth == 0 || window.ActualHeight == 0)
{
window.WindowStartupLocation = WindowStartupLocation.CenterScreen;
// 设置窗口加载完成后的位置
window.Loaded += (s, e) =>
{
// 窗口位于按钮上方居中
double left = buttonPosition.X - (window.ActualWidth / 2);
double top = buttonPosition.Y - window.ActualHeight - 10; // 在按钮上方留出一些间距
// 确保窗口在屏幕内
left = Math.Max(0, Math.Min(left, SystemParameters.WorkArea.Width - window.ActualWidth));
top = Math.Max(0, Math.Min(top, SystemParameters.WorkArea.Height - window.ActualHeight));
window.Left = left;
window.Top = top;
};
}
else
{
// 窗口位于按钮上方居中
double left = buttonPosition.X - (window.ActualWidth / 2);
double top = buttonPosition.Y - window.ActualHeight - 10; // 在按钮上方留出一些间距
// 确保窗口在屏幕内
left = Math.Max(0, Math.Min(left, SystemParameters.WorkArea.Width - window.ActualWidth));
top = Math.Max(0, Math.Min(top, SystemParameters.WorkArea.Height - window.ActualHeight));
window.Left = left;
window.Top = top;
}
}
/// <summary>
/// 添加应用到启动台
/// </summary>
/// <param name="item">启动台项</param>
public void AddLauncherItem(LauncherItem item)
{
// 如果项目数量已达上限,则不添加
if (LauncherItems.Count >= 40)
{
MessageBox.Show("启动台项目数量已达上限(40个)!", "提示", MessageBoxButton.OK, MessageBoxImage.Information);
return;
}
// 寻找合适的位置
if (item.Position < 0)
{
item.Position = FindNextAvailablePosition();
}
// 添加项目并保存配置
LauncherItems.Add(item);
SaveConfig();
}
/// <summary>
/// 查找下一个可用位置
/// </summary>
private int FindNextAvailablePosition()
{
// 获取已使用的位置列表
var usedPositions = new HashSet<int>();
foreach (var item in LauncherItems)
{
usedPositions.Add(item.Position);
}
// 查找第一个可用位置
for (int i = 0; i < 40; i++)
{
if (!usedPositions.Contains(i))
{
return i;
}
}
// 如果所有位置都已使用,则返回0
return 0;
}
#endregion
}
}
@@ -1,92 +0,0 @@
using System.Windows.Controls;
namespace Ink_Canvas.Helpers.Plugins
{
/// <summary>
/// 增强的插件基类,提供对插件服务的访问和基本实现
/// </summary>
public abstract class EnhancedPluginBase : PluginBase, IEnhancedPlugin
{
/// <summary>
/// 插件服务实例
/// </summary>
public IPluginService PluginService { get; private set; }
/// <summary>
/// 构造函数
/// </summary>
protected EnhancedPluginBase()
{
PluginService = PluginServiceManager.Instance;
}
/// <summary>
/// 插件启动时调用,在Initialize之后
/// </summary>
public virtual void OnStartup()
{
LogHelper.WriteLogToFile($"插件 {Name} 已启动");
}
/// <summary>
/// 插件关闭时调用,在Cleanup之前
/// </summary>
public virtual void OnShutdown()
{
LogHelper.WriteLogToFile($"插件 {Name} 正在关闭");
}
/// <summary>
/// 获取插件的菜单项
/// </summary>
/// <returns>菜单项集合</returns>
public virtual MenuItem[] GetMenuItems()
{
return new MenuItem[0];
}
/// <summary>
/// 获取插件的工具栏按钮
/// </summary>
/// <returns>工具栏按钮集合</returns>
public virtual Button[] GetToolbarButtons()
{
return new Button[0];
}
/// <summary>
/// 获取插件的状态栏信息
/// </summary>
/// <returns>状态栏信息</returns>
public virtual string GetStatusBarInfo()
{
return $"{Name} v{Version} - {(IsEnabled ? "" : "")}";
}
/// <summary>
/// 插件配置变更时调用
/// </summary>
public virtual void OnConfigurationChanged()
{
LogHelper.WriteLogToFile($"插件 {Name} 配置已变更");
}
/// <summary>
/// 重写初始化方法,调用OnStartup
/// </summary>
public override void Initialize()
{
base.Initialize();
OnStartup();
}
/// <summary>
/// 重写清理方法,调用OnShutdown
/// </summary>
public override void Cleanup()
{
OnShutdown();
base.Cleanup();
}
}
}
@@ -1,241 +0,0 @@
using System.Windows.Controls;
namespace Ink_Canvas.Helpers.Plugins
{
/// <summary>
/// 增强的插件基类 V2,提供对三个专门服务接口的访问
/// 插件开发者可以根据需要选择性地使用这些服务
/// </summary>
public abstract class EnhancedPluginBaseV2 : PluginBase, IEnhancedPlugin
{
/// <summary>
/// 获取服务实例
/// </summary>
public IGetService GetService { get; private set; }
/// <summary>
/// 窗口服务实例
/// </summary>
public IWindowService WindowService { get; private set; }
/// <summary>
/// 操作服务实例
/// </summary>
public IActionService ActionService { get; private set; }
/// <summary>
/// 插件服务实例(兼容性)
/// </summary>
public IPluginService PluginService { get; private set; }
/// <summary>
/// 构造函数
/// </summary>
protected EnhancedPluginBaseV2()
{
// 初始化所有服务实例
PluginService = PluginServiceManager.Instance;
GetService = PluginServiceManager.Instance;
WindowService = PluginServiceManager.Instance;
ActionService = PluginServiceManager.Instance;
}
/// <summary>
/// 插件启动时调用,在Initialize之后
/// </summary>
public virtual void OnStartup()
{
LogHelper.WriteLogToFile($"插件 {Name} 已启动");
}
/// <summary>
/// 插件关闭时调用,在Cleanup之前
/// </summary>
public virtual void OnShutdown()
{
LogHelper.WriteLogToFile($"插件 {Name} 正在关闭");
}
/// <summary>
/// 获取插件的菜单项
/// </summary>
/// <returns>菜单项集合</returns>
public virtual MenuItem[] GetMenuItems()
{
return new MenuItem[0];
}
/// <summary>
/// 获取插件的工具栏按钮
/// </summary>
/// <returns>工具栏按钮集合</returns>
public virtual Button[] GetToolbarButtons()
{
return new Button[0];
}
/// <summary>
/// 获取插件的状态栏信息
/// </summary>
/// <returns>状态栏信息</returns>
public virtual string GetStatusBarInfo()
{
return $"{Name} v{Version} - {(IsEnabled ? "" : "")}";
}
/// <summary>
/// 插件配置变更时调用
/// </summary>
public virtual void OnConfigurationChanged()
{
LogHelper.WriteLogToFile($"插件 {Name} 配置已变更");
}
#region 便
/// <summary>
/// 显示通知消息
/// </summary>
/// <param name="message">消息内容</param>
/// <param name="type">消息类型</param>
protected void ShowNotification(string message, NotificationType type = NotificationType.Info)
{
WindowService.ShowNotification(message, type);
}
/// <summary>
/// 显示确认对话框
/// </summary>
/// <param name="message">消息内容</param>
/// <param name="title">标题</param>
/// <returns>用户选择结果</returns>
protected bool ShowConfirmDialog(string message, string title = "确认")
{
return WindowService.ShowConfirmDialog(message, title);
}
/// <summary>
/// 显示输入对话框
/// </summary>
/// <param name="message">提示消息</param>
/// <param name="title">标题</param>
/// <param name="defaultValue">默认值</param>
/// <returns>用户输入内容</returns>
protected string ShowInputDialog(string message, string title = "输入", string defaultValue = "")
{
return WindowService.ShowInputDialog(message, title, defaultValue);
}
/// <summary>
/// 获取系统设置
/// </summary>
/// <typeparam name="T">设置类型</typeparam>
/// <param name="key">设置键</param>
/// <param name="defaultValue">默认值</param>
/// <returns>设置值</returns>
protected T GetSetting<T>(string key, T defaultValue = default(T))
{
return GetService.GetSetting(key, defaultValue);
}
/// <summary>
/// 设置系统设置
/// </summary>
/// <typeparam name="T">设置类型</typeparam>
/// <param name="key">设置键</param>
/// <param name="value">设置值</param>
protected void SetSetting<T>(string key, T value)
{
ActionService.SetSetting(key, value);
}
/// <summary>
/// 保存设置
/// </summary>
protected void SaveSettings()
{
ActionService.SaveSettings();
}
/// <summary>
/// 清除当前画布
/// </summary>
protected void ClearCanvas()
{
ActionService.ClearCanvas();
}
/// <summary>
/// 撤销操作
/// </summary>
protected void Undo()
{
ActionService.Undo();
}
/// <summary>
/// 重做操作
/// </summary>
protected void Redo()
{
ActionService.Redo();
}
/// <summary>
/// 检查是否可以撤销
/// </summary>
protected bool CanUndo => GetService.CanUndo;
/// <summary>
/// 检查是否可以重做
/// </summary>
protected bool CanRedo => GetService.CanRedo;
/// <summary>
/// 获取当前绘制模式
/// </summary>
protected int CurrentDrawingMode => GetService.CurrentDrawingMode;
/// <summary>
/// 设置绘制模式
/// </summary>
/// <param name="mode">绘制模式</param>
protected void SetDrawingMode(int mode)
{
ActionService.SetDrawingMode(mode);
}
/// <summary>
/// 注册事件处理器
/// </summary>
/// <param name="eventName">事件名称</param>
/// <param name="handler">事件处理器</param>
protected void RegisterEventHandler(string eventName, System.EventHandler handler)
{
ActionService.RegisterEventHandler(eventName, handler);
}
/// <summary>
/// 注销事件处理器
/// </summary>
/// <param name="eventName">事件名称</param>
/// <param name="handler">事件处理器</param>
protected void UnregisterEventHandler(string eventName, System.EventHandler handler)
{
ActionService.UnregisterEventHandler(eventName, handler);
}
/// <summary>
/// 触发事件
/// </summary>
/// <param name="eventName">事件名称</param>
/// <param name="sender">事件发送者</param>
/// <param name="args">事件参数</param>
protected void TriggerEvent(string eventName, object sender, System.EventArgs args)
{
ActionService.TriggerEvent(eventName, sender, args);
}
#endregion
}
}
@@ -1,296 +0,0 @@
using System;
using System.Windows.Media;
namespace Ink_Canvas.Helpers.Plugins
{
/// <summary>
/// 操作服务接口,统一所有执行操作相关的方法
/// </summary>
public interface IActionService
{
#region
/// <summary>
/// 清除当前画布
/// </summary>
void ClearCanvas();
/// <summary>
/// 清除所有画布
/// </summary>
void ClearAllCanvases();
/// <summary>
/// 添加新页面
/// </summary>
void AddNewPage();
/// <summary>
/// 删除当前页面
/// </summary>
void DeleteCurrentPage();
/// <summary>
/// 切换到指定页面
/// </summary>
/// <param name="pageIndex">页面索引</param>
void SwitchToPage(int pageIndex);
/// <summary>
/// 切换到下一页
/// </summary>
void NextPage();
/// <summary>
/// 切换到上一页
/// </summary>
void PreviousPage();
#endregion
#region
/// <summary>
/// 设置绘制模式
/// </summary>
/// <param name="mode">绘制模式</param>
void SetDrawingMode(int mode);
/// <summary>
/// 设置笔触宽度
/// </summary>
/// <param name="width">宽度</param>
void SetInkWidth(double width);
/// <summary>
/// 设置笔触颜色
/// </summary>
/// <param name="color">颜色</param>
void SetInkColor(Color color);
/// <summary>
/// 设置高亮笔宽度
/// </summary>
/// <param name="width">宽度</param>
void SetHighlighterWidth(double width);
/// <summary>
/// 设置橡皮擦大小
/// </summary>
/// <param name="size">大小</param>
void SetEraserSize(int size);
/// <summary>
/// 设置橡皮擦类型
/// </summary>
/// <param name="type">类型</param>
void SetEraserType(int type);
/// <summary>
/// 设置橡皮擦形状
/// </summary>
/// <param name="shape">形状</param>
void SetEraserShape(int shape);
/// <summary>
/// 设置笔触透明度
/// </summary>
/// <param name="alpha">透明度</param>
void SetInkAlpha(double alpha);
/// <summary>
/// 设置笔触样式
/// </summary>
/// <param name="style">样式</param>
void SetInkStyle(int style);
/// <summary>
/// 设置背景颜色
/// </summary>
/// <param name="color">颜色</param>
void SetBackgroundColor(string color);
#endregion
#region
/// <summary>
/// 保存画布内容
/// </summary>
/// <param name="filePath">文件路径</param>
void SaveCanvas(string filePath);
/// <summary>
/// 加载画布内容
/// </summary>
/// <param name="filePath">文件路径</param>
void LoadCanvas(string filePath);
/// <summary>
/// 导出为图片
/// </summary>
/// <param name="filePath">文件路径</param>
/// <param name="format">图片格式</param>
void ExportAsImage(string filePath, string format);
/// <summary>
/// 导出为PDF
/// </summary>
/// <param name="filePath">文件路径</param>
void ExportAsPDF(string filePath);
#endregion
#region
/// <summary>
/// 撤销操作
/// </summary>
void Undo();
/// <summary>
/// 重做操作
/// </summary>
void Redo();
#endregion
#region
/// <summary>
/// 全选
/// </summary>
void SelectAll();
/// <summary>
/// 取消选择
/// </summary>
void DeselectAll();
/// <summary>
/// 删除选中内容
/// </summary>
void DeleteSelected();
/// <summary>
/// 复制选中内容
/// </summary>
void CopySelected();
/// <summary>
/// 剪切选中内容
/// </summary>
void CutSelected();
/// <summary>
/// 粘贴内容
/// </summary>
void Paste();
#endregion
#region
/// <summary>
/// 设置系统设置
/// </summary>
/// <typeparam name="T">设置类型</typeparam>
/// <param name="key">设置键</param>
/// <param name="value">设置值</param>
void SetSetting<T>(string key, T value);
/// <summary>
/// 保存设置到文件
/// </summary>
void SaveSettings();
/// <summary>
/// 从文件加载设置
/// </summary>
void LoadSettings();
/// <summary>
/// 重置设置为默认值
/// </summary>
void ResetSettings();
#endregion
#region
/// <summary>
/// 启用插件
/// </summary>
/// <param name="pluginName">插件名称</param>
void EnablePlugin(string pluginName);
/// <summary>
/// 禁用插件
/// </summary>
/// <param name="pluginName">插件名称</param>
void DisablePlugin(string pluginName);
/// <summary>
/// 卸载插件
/// </summary>
/// <param name="pluginName">插件名称</param>
void UnloadPlugin(string pluginName);
#endregion
#region
/// <summary>
/// 注册事件处理器
/// </summary>
/// <param name="eventName">事件名称</param>
/// <param name="handler">事件处理器</param>
void RegisterEventHandler(string eventName, EventHandler handler);
/// <summary>
/// 注销事件处理器
/// </summary>
/// <param name="eventName">事件名称</param>
/// <param name="handler">事件处理器</param>
void UnregisterEventHandler(string eventName, EventHandler handler);
/// <summary>
/// 触发事件
/// </summary>
/// <param name="eventName">事件名称</param>
/// <param name="sender">事件发送者</param>
/// <param name="args">事件参数</param>
void TriggerEvent(string eventName, object sender, EventArgs args);
#endregion
#region
/// <summary>
/// 重启应用程序
/// </summary>
void RestartApplication();
/// <summary>
/// 退出应用程序
/// </summary>
void ExitApplication();
/// <summary>
/// 检查更新
/// </summary>
void CheckForUpdates();
/// <summary>
/// 打开帮助文档
/// </summary>
void OpenHelpDocument();
/// <summary>
/// 打开关于页面
/// </summary>
void OpenAboutPage();
#endregion
}
}
@@ -1,178 +0,0 @@
using System;
using System.IO;
namespace Ink_Canvas.Helpers.Plugins
{
/// <summary>
/// ICCPP 插件适配器,用于加载和管理 .iccpp 格式的插件
/// </summary>
public class ICCPPPluginAdapter : PluginBase
{
private readonly byte[] _pluginData;
private readonly string _pluginPath;
private readonly string _pluginName;
private readonly Version _pluginVersion;
private bool _isInitialized;
/// <summary>
/// 创建 ICCPP 插件适配器
/// </summary>
/// <param name="pluginPath">插件文件路径</param>
/// <param name="pluginData">插件文件数据</param>
public ICCPPPluginAdapter(string pluginPath, byte[] pluginData)
{
_pluginPath = pluginPath;
_pluginData = pluginData;
PluginPath = pluginPath;
// 从文件名获取插件名称
_pluginName = Path.GetFileNameWithoutExtension(pluginPath);
_pluginVersion = new Version(1, 0, 0); // 默认版本
// 尝试从插件数据中读取更多信息
TryReadPluginMetadata();
}
public ICCPPPluginAdapter()
{
_pluginPath = string.Empty;
_pluginData = new byte[0];
PluginPath = string.Empty;
_pluginName = "ICCPPPlugin";
_pluginVersion = new Version(1, 0, 0);
// 可选:初始化其他字段
}
/// <summary>
/// 尝试从插件数据中读取元数据
/// </summary>
private void TryReadPluginMetadata()
{
try
{
// 这里可以根据 .iccpp 文件的实际格式解析元数据
// 例如,如果文件有特定的头部结构,可以在这里解析
// 示例:如果前100字节包含元数据
if (_pluginData.Length > 100)
{
// 解析元数据的代码...
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"解析插件 {_pluginName} 元数据时出错: {ex.Message}", LogHelper.LogType.Error);
}
}
#region IPlugin
/// <summary>
/// 插件名称
/// </summary>
public override string Name => _pluginName;
/// <summary>
/// 插件描述
/// </summary>
public override string Description => $"{_pluginName} (ICCPP 格式插件)";
/// <summary>
/// 插件版本
/// </summary>
public override Version Version => _pluginVersion;
/// <summary>
/// 插件作者
/// </summary>
public override string Author => "未知";
/// <summary>
/// 是否为内置插件
/// </summary>
public override bool IsBuiltIn => false;
/// <summary>
/// 初始化插件
/// </summary>
public override void Initialize()
{
if (_isInitialized) return;
try
{
// 这里可以添加 .iccpp 插件的初始化逻辑
// 例如,根据文件格式加载特定资源
LogHelper.WriteLogToFile($"ICCPP 插件 {Name} 已初始化");
_isInitialized = true;
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"初始化 ICCPP 插件 {Name} 时出错: {ex.Message}", LogHelper.LogType.Error);
}
}
/// <summary>
/// 启用插件
/// </summary>
public override void Enable()
{
if (IsEnabled) return;
try
{
// 这里可以添加 .iccpp 插件的启用逻辑
// 例如,加载动态库、注册事件等
base.Enable(); // 设置启用状态并触发事件
LogHelper.WriteLogToFile($"ICCPP 插件 {Name} 已启用");
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"启用 ICCPP 插件 {Name} 时出错: {ex.Message}", LogHelper.LogType.Error);
}
}
/// <summary>
/// 禁用插件
/// </summary>
public override void Disable()
{
if (!IsEnabled) return;
try
{
// 这里可以添加 .iccpp 插件的禁用逻辑
// 例如,卸载动态库、注销事件等
base.Disable(); // 设置禁用状态并触发事件
LogHelper.WriteLogToFile($"ICCPP 插件 {Name} 已禁用");
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"禁用 ICCPP 插件 {Name} 时出错: {ex.Message}", LogHelper.LogType.Error);
}
}
/// <summary>
/// 清理插件资源
/// </summary>
public override void Cleanup()
{
try
{
// 这里可以添加 .iccpp 插件的清理逻辑
// 例如,释放资源等
LogHelper.WriteLogToFile($"ICCPP 插件 {Name} 已清理资源");
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"清理 ICCPP 插件 {Name} 资源时出错: {ex.Message}", LogHelper.LogType.Error);
}
}
#endregion
}
}
@@ -1,48 +0,0 @@
using System.Windows.Controls;
namespace Ink_Canvas.Helpers.Plugins
{
/// <summary>
/// 增强的插件接口,提供对插件服务的访问
/// </summary>
public interface IEnhancedPlugin : IPlugin
{
/// <summary>
/// 获取插件服务实例
/// </summary>
IPluginService PluginService { get; }
/// <summary>
/// 插件启动时调用,在Initialize之后
/// </summary>
void OnStartup();
/// <summary>
/// 插件关闭时调用,在Cleanup之前
/// </summary>
void OnShutdown();
/// <summary>
/// 获取插件的菜单项
/// </summary>
/// <returns>菜单项集合</returns>
MenuItem[] GetMenuItems();
/// <summary>
/// 获取插件的工具栏按钮
/// </summary>
/// <returns>工具栏按钮集合</returns>
Button[] GetToolbarButtons();
/// <summary>
/// 获取插件的状态栏信息
/// </summary>
/// <returns>状态栏信息</returns>
string GetStatusBarInfo();
/// <summary>
/// 插件配置变更时调用
/// </summary>
void OnConfigurationChanged();
}
}
-215
View File
@@ -1,215 +0,0 @@
using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
namespace Ink_Canvas.Helpers.Plugins
{
/// <summary>
/// 获取服务接口,统一所有获取类的方法
/// </summary>
public interface IGetService
{
#region UI获取
/// <summary>
/// 获取主窗口引用
/// </summary>
Window MainWindow { get; }
/// <summary>
/// 获取当前画布
/// </summary>
InkCanvas CurrentCanvas { get; }
/// <summary>
/// 获取所有画布页面
/// </summary>
List<Canvas> AllCanvasPages { get; }
/// <summary>
/// 获取当前页面索引
/// </summary>
int CurrentPageIndex { get; }
/// <summary>
/// 获取当前页面数量
/// </summary>
int TotalPageCount { get; }
/// <summary>
/// 获取浮动工具栏
/// </summary>
FrameworkElement FloatingToolBar { get; }
/// <summary>
/// 获取左侧面板
/// </summary>
FrameworkElement LeftPanel { get; }
/// <summary>
/// 获取右侧面板
/// </summary>
FrameworkElement RightPanel { get; }
/// <summary>
/// 获取顶部面板
/// </summary>
FrameworkElement TopPanel { get; }
/// <summary>
/// 获取底部面板
/// </summary>
FrameworkElement BottomPanel { get; }
#endregion
#region
/// <summary>
/// 获取当前绘制模式
/// </summary>
int CurrentDrawingMode { get; }
/// <summary>
/// 获取当前笔触宽度
/// </summary>
double CurrentInkWidth { get; }
/// <summary>
/// 获取当前笔触颜色
/// </summary>
Color CurrentInkColor { get; }
/// <summary>
/// 获取当前高亮笔宽度
/// </summary>
double CurrentHighlighterWidth { get; }
/// <summary>
/// 获取当前橡皮擦大小
/// </summary>
int CurrentEraserSize { get; }
/// <summary>
/// 获取当前橡皮擦类型
/// </summary>
int CurrentEraserType { get; }
/// <summary>
/// 获取当前橡皮擦形状
/// </summary>
int CurrentEraserShape { get; }
/// <summary>
/// 获取当前笔触透明度
/// </summary>
double CurrentInkAlpha { get; }
/// <summary>
/// 获取当前笔触样式
/// </summary>
int CurrentInkStyle { get; }
/// <summary>
/// 获取当前背景颜色
/// </summary>
string CurrentBackgroundColor { get; }
#endregion
#region
/// <summary>
/// 获取当前主题模式
/// </summary>
bool IsDarkTheme { get; }
/// <summary>
/// 获取当前是否为白板模式
/// </summary>
bool IsWhiteboardMode { get; }
/// <summary>
/// 获取当前是否为PPT模式
/// </summary>
bool IsPPTMode { get; }
/// <summary>
/// 获取当前是否为全屏模式
/// </summary>
bool IsFullScreenMode { get; }
/// <summary>
/// 获取当前是否为画板模式
/// </summary>
bool IsCanvasMode { get; }
/// <summary>
/// 获取当前是否为选择模式
/// </summary>
bool IsSelectionMode { get; }
/// <summary>
/// 获取当前是否为擦除模式
/// </summary>
bool IsEraserMode { get; }
/// <summary>
/// 获取当前是否为形状绘制模式
/// </summary>
bool IsShapeDrawingMode { get; }
/// <summary>
/// 获取当前是否为高亮模式
/// </summary>
bool IsHighlighterMode { get; }
#endregion
#region
/// <summary>
/// 获取是否可以撤销
/// </summary>
bool CanUndo { get; }
/// <summary>
/// 获取是否可以重做
/// </summary>
bool CanRedo { get; }
#endregion
#region
/// <summary>
/// 获取系统设置
/// </summary>
/// <typeparam name="T">设置类型</typeparam>
/// <param name="key">设置键</param>
/// <param name="defaultValue">默认值</param>
/// <returns>设置值</returns>
T GetSetting<T>(string key, T defaultValue = default(T));
#endregion
#region
/// <summary>
/// 获取所有已加载的插件
/// </summary>
/// <returns>插件列表</returns>
List<IPlugin> GetAllPlugins();
/// <summary>
/// 获取指定插件
/// </summary>
/// <param name="pluginName">插件名称</param>
/// <returns>插件实例</returns>
IPlugin GetPlugin(string pluginName);
#endregion
}
}
-67
View File
@@ -1,67 +0,0 @@
using System;
using System.Windows.Controls;
namespace Ink_Canvas.Helpers.Plugins
{
/// <summary>
/// 定义插件的基本接口
/// </summary>
public interface IPlugin
{
/// <summary>
/// 插件名称
/// </summary>
string Name { get; }
/// <summary>
/// 插件描述
/// </summary>
string Description { get; }
/// <summary>
/// 插件版本
/// </summary>
Version Version { get; }
/// <summary>
/// 插件作者
/// </summary>
string Author { get; }
/// <summary>
/// 是否为内置插件
/// </summary>
bool IsBuiltIn { get; }
/// <summary>
/// 初始化插件
/// 此方法在插件加载时被调用,用于执行一些初始化工作
/// </summary>
void Initialize();
/// <summary>
/// 启用插件
/// 此方法在插件被用户或系统启用时调用,激活插件功能
/// </summary>
void Enable();
/// <summary>
/// 禁用插件
/// 此方法在插件被用户或系统禁用时调用,停用插件功能
/// </summary>
void Disable();
/// <summary>
/// 获取插件设置界面
/// 此方法返回插件的设置界面控件,用于展示在设置窗口
/// </summary>
/// <returns>插件设置界面</returns>
UserControl GetSettingsView();
/// <summary>
/// 插件卸载时的清理工作
/// 此方法在插件被卸载前调用,用于释放资源和执行清理
/// </summary>
void Cleanup();
}
}
@@ -1,44 +0,0 @@
using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
namespace Ink_Canvas.Helpers.Plugins
{
/// <summary>
/// 插件服务接口,提供对软件内部功能的访问
/// 继承自三个专门的服务接口:获取服务、窗口服务、操作服务
/// </summary>
public interface IPluginService : IGetService, IWindowService, IActionService
{
// 这个接口现在继承自三个专门的服务接口
// 所有方法都在子接口中定义,这里不需要重复定义
}
/// <summary>
/// 通知类型枚举
/// </summary>
public enum NotificationType
{
/// <summary>
/// 信息
/// </summary>
Info,
/// <summary>
/// 成功
/// </summary>
Success,
/// <summary>
/// 警告
/// </summary>
Warning,
/// <summary>
/// 错误
/// </summary>
Error
}
}
@@ -1,154 +0,0 @@
using System;
namespace Ink_Canvas.Helpers.Plugins
{
/// <summary>
/// 窗口服务接口,统一所有窗口操作相关的方法
/// </summary>
public interface IWindowService
{
#region
/// <summary>
/// 显示设置窗口
/// </summary>
void ShowSettingsWindow();
/// <summary>
/// 隐藏设置窗口
/// </summary>
void HideSettingsWindow();
/// <summary>
/// 显示插件设置窗口
/// </summary>
void ShowPluginSettingsWindow();
/// <summary>
/// 隐藏插件设置窗口
/// </summary>
void HidePluginSettingsWindow();
/// <summary>
/// 显示帮助窗口
/// </summary>
void ShowHelpWindow();
/// <summary>
/// 隐藏帮助窗口
/// </summary>
void HideHelpWindow();
/// <summary>
/// 显示关于窗口
/// </summary>
void ShowAboutWindow();
/// <summary>
/// 隐藏关于窗口
/// </summary>
void HideAboutWindow();
#endregion
#region
/// <summary>
/// 显示通知消息
/// </summary>
/// <param name="message">消息内容</param>
/// <param name="type">消息类型</param>
void ShowNotification(string message, NotificationType type = NotificationType.Info);
/// <summary>
/// 显示确认对话框
/// </summary>
/// <param name="message">消息内容</param>
/// <param name="title">标题</param>
/// <returns>用户选择结果</returns>
bool ShowConfirmDialog(string message, string title = "确认");
/// <summary>
/// 显示输入对话框
/// </summary>
/// <param name="message">提示消息</param>
/// <param name="title">标题</param>
/// <param name="defaultValue">默认值</param>
/// <returns>用户输入内容</returns>
string ShowInputDialog(string message, string title = "输入", string defaultValue = "");
#endregion
#region
/// <summary>
/// 设置窗口全屏状态
/// </summary>
/// <param name="isFullScreen">是否全屏</param>
void SetFullScreen(bool isFullScreen);
/// <summary>
/// 设置窗口置顶状态
/// </summary>
/// <param name="isTopMost">是否置顶</param>
void SetTopMost(bool isTopMost);
/// <summary>
/// 设置窗口可见性
/// </summary>
/// <param name="isVisible">是否可见</param>
void SetWindowVisibility(bool isVisible);
/// <summary>
/// 最小化窗口
/// </summary>
void MinimizeWindow();
/// <summary>
/// 最大化窗口
/// </summary>
void MaximizeWindow();
/// <summary>
/// 恢复窗口
/// </summary>
void RestoreWindow();
/// <summary>
/// 关闭窗口
/// </summary>
void CloseWindow();
#endregion
#region
/// <summary>
/// 设置窗口位置
/// </summary>
/// <param name="x">X坐标</param>
/// <param name="y">Y坐标</param>
void SetWindowPosition(double x, double y);
/// <summary>
/// 设置窗口大小
/// </summary>
/// <param name="width">宽度</param>
/// <param name="height">高度</param>
void SetWindowSize(double width, double height);
/// <summary>
/// 获取窗口位置
/// </summary>
/// <returns>窗口位置</returns>
(double x, double y) GetWindowPosition();
/// <summary>
/// 获取窗口大小
/// </summary>
/// <returns>窗口大小</returns>
(double width, double height) GetWindowSize();
#endregion
}
}
-161
View File
@@ -1,161 +0,0 @@
using System;
using System.Windows.Controls;
namespace Ink_Canvas.Helpers.Plugins
{
/// <summary>
/// 插件基类,提供基本实现
/// </summary>
public abstract class PluginBase : IPlugin
{
/// <summary>
/// 插件状态(私有字段)
/// </summary>
private bool _isEnabled;
/// <summary>
/// 插件状态(公共属性)
/// </summary>
public bool IsEnabled
{
get => _isEnabled;
protected set
{
if (_isEnabled != value)
{
_isEnabled = value;
OnEnabledStateChanged(value);
}
}
}
/// <summary>
/// 插件ID
/// </summary>
public string Id { get; protected set; }
/// <summary>
/// 插件路径
/// </summary>
public string PluginPath { get; set; }
/// <summary>
/// 插件名称
/// </summary>
public abstract string Name { get; }
/// <summary>
/// 插件描述
/// </summary>
public abstract string Description { get; }
/// <summary>
/// 插件版本
/// </summary>
public abstract Version Version { get; }
/// <summary>
/// 插件作者
/// </summary>
public abstract string Author { get; }
/// <summary>
/// 是否为内置插件
/// </summary>
public virtual bool IsBuiltIn => false;
/// <summary>
/// 状态变更事件
/// </summary>
public event EventHandler<bool> EnabledStateChanged;
/// <summary>
/// 初始化插件
/// </summary>
public virtual void Initialize()
{
Id = GetType().FullName;
// 添加日志,记录插件名称
try
{
string name = Name;
LogHelper.WriteLogToFile($"初始化插件: ID={Id}, 名称={name ?? ""}");
if (string.IsNullOrEmpty(name))
{
LogHelper.WriteLogToFile($"警告: 插件 {Id} 的名称为空", LogHelper.LogType.Warning);
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"获取插件名称时出错: {ex.Message}", LogHelper.LogType.Error);
}
LogHelper.WriteLogToFile($"插件 {Name} 已初始化");
}
/// <summary>
/// 启用插件
/// </summary>
public virtual void Enable()
{
if (!IsEnabled)
{
IsEnabled = true;
LogHelper.WriteLogToFile($"插件 {Name} 已启用");
}
}
/// <summary>
/// 禁用插件
/// </summary>
public virtual void Disable()
{
if (IsEnabled)
{
IsEnabled = false;
LogHelper.WriteLogToFile($"插件 {Name} 已禁用");
}
}
/// <summary>
/// 获取插件设置界面
/// </summary>
/// <returns>插件设置界面</returns>
public virtual UserControl GetSettingsView()
{
// 默认返回空设置页面
return new UserControl();
}
/// <summary>
/// 插件卸载时的清理工作
/// </summary>
public virtual void Cleanup()
{
LogHelper.WriteLogToFile($"插件 {Name} 已卸载");
}
/// <summary>
/// 保存插件自身的设置
/// 注意:此方法仅用于保存插件的特定设置,不应影响插件启用/禁用状态
/// 插件启用状态由PluginManager统一管理
/// </summary>
public virtual void SavePluginSettings()
{
// 默认实现不做任何事情
// 子类可以重写此方法,将自身设置保存到配置文件中
LogHelper.WriteLogToFile($"插件 {Name} 设置已保存", LogHelper.LogType.Event);
}
/// <summary>
/// 触发状态变更事件
/// </summary>
/// <param name="isEnabled">是否启用</param>
protected virtual void OnEnabledStateChanged(bool isEnabled)
{
EnabledStateChanged?.Invoke(this, isEnabled);
}
}
}

Some files were not shown because too many files have changed in this diff Show More