├── ioc.png ├── assets ├── demo.gif ├── image copy 4.png ├── feature_health.png ├── feature_media.png ├── feature_notify.png └── feature_blackhole.png ├── DrinkWaterMode.cs ├── AssemblyInfo.cs ├── .gitignore ├── Spring.cs ├── WinIsland.sln ├── WinIsland.csproj ├── AppSettings.cs ├── App.xaml.cs ├── README.md ├── App.xaml ├── SettingsWindow.xaml.cs ├── LICENSE ├── SettingsWindow.xaml ├── MainWindow.xaml └── MainWindow.xaml.cs /ioc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lb-li/WinIsland/HEAD/ioc.png -------------------------------------------------------------------------------- /assets/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lb-li/WinIsland/HEAD/assets/demo.gif -------------------------------------------------------------------------------- /assets/image copy 4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lb-li/WinIsland/HEAD/assets/image copy 4.png -------------------------------------------------------------------------------- /assets/feature_health.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lb-li/WinIsland/HEAD/assets/feature_health.png -------------------------------------------------------------------------------- /assets/feature_media.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lb-li/WinIsland/HEAD/assets/feature_media.png -------------------------------------------------------------------------------- /assets/feature_notify.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lb-li/WinIsland/HEAD/assets/feature_notify.png -------------------------------------------------------------------------------- /assets/feature_blackhole.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lb-li/WinIsland/HEAD/assets/feature_blackhole.png -------------------------------------------------------------------------------- /DrinkWaterMode.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace WinIsland 4 | { 5 | public enum DrinkWaterMode 6 | { 7 | Interval, 8 | Custom 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Windows; 2 | 3 | [assembly: ThemeInfo( 4 | ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located 5 | //(used if a resource is not found in the page, 6 | // or application resource dictionaries) 7 | ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located 8 | //(used if a resource is not found in the page, 9 | // app, or any theme specific resource dictionaries) 10 | )] 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Visual Studio & .NET default ignore list 2 | .vs/ 3 | bin/ 4 | obj/ 5 | [Bb]in/ 6 | [Oo]bj/ 7 | 8 | # User-specific files 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | *.suo 13 | 14 | # Build results 15 | [Dd]ebug/ 16 | [Dd]ebugPublic/ 17 | [Rr]elease/ 18 | [Rr]eleasePublic/ 19 | x64/ 20 | x86/ 21 | [Aa][Rr][Mm]/ 22 | [Aa][Rr][Mm]64/ 23 | bld/ 24 | [Bb]in/ 25 | [Oo]bj/ 26 | [Ll]og/ 27 | 28 | # Project specific 29 | debug_log.txt 30 | *.log 31 | 32 | # Windows image file caches 33 | Thumbs.db 34 | ehthumbs.db 35 | 36 | # Folder config file 37 | Desktop.ini 38 | 39 | # Recycle Bin used on file shares 40 | $RECYCLE.BIN/ 41 | 42 | # Mac junk 43 | .DS_Store 44 | 45 | # Publish folder (just in case) 46 | **/publish/ 47 | -------------------------------------------------------------------------------- /Spring.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace WinIsland 4 | { 5 | public class Spring 6 | { 7 | // 目标数值(比如:胶囊想要变成的宽度) 8 | public double Target { get; set; } 9 | 10 | // 当前数值(胶囊现在的宽度) 11 | public double Current { get; private set; } 12 | 13 | // 速度 14 | private double Velocity; 15 | 16 | // 物理参数 (你可以调整这两个数来改变手感!) 17 | private const double Tension = 200.0; // 张力:越大切换越快 18 | private const double Friction = 18.0; // 摩擦力:越小回弹越厉害(果冻感) 19 | 20 | public Spring(double startValue) 21 | { 22 | Current = startValue; 23 | Target = startValue; 24 | } 25 | 26 | // 每一帧调用一次这个方法来更新位置 27 | public double Update(double dt) 28 | { 29 | // 物理公式:F = -kx - dv 30 | var force = Tension * (Target - Current); 31 | var acceleration = force - Friction * Velocity; 32 | 33 | Velocity += acceleration * dt; 34 | Current += Velocity * dt; 35 | 36 | return Current; 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /WinIsland.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.10.35004.147 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WinIsland", "WinIsland.csproj", "{C9F5E3DE-12A6-49BE-B7E6-584E9EDBB07E}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {C9F5E3DE-12A6-49BE-B7E6-584E9EDBB07E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {C9F5E3DE-12A6-49BE-B7E6-584E9EDBB07E}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {C9F5E3DE-12A6-49BE-B7E6-584E9EDBB07E}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {C9F5E3DE-12A6-49BE-B7E6-584E9EDBB07E}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ExtensibilityGlobals) = postSolution 23 | SolutionGuid = {86FD865D-B1D0-4F25-A0FB-BB7189AA03D2} 24 | EndGlobalSection 25 | EndGlobal 26 | -------------------------------------------------------------------------------- /WinIsland.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | WinExe 5 | net8.0-windows10.0.19041.0 6 | enable 7 | enable 8 | true 9 | 10 | 11 | true 12 | true 13 | win-x64 14 | true 15 | true 16 | none 17 | false 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | ico.ico 28 | 29 | 30 | 31 | 32 | PreserveNewest 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /AppSettings.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Text.Json; 4 | 5 | namespace WinIsland 6 | { 7 | public class AppSettings 8 | { 9 | public bool DrinkWaterEnabled { get; set; } = false; 10 | public int DrinkWaterIntervalMinutes { get; set; } = 30; 11 | public bool TodoEnabled { get; set; } = false; 12 | public string DrinkWaterStartTime { get; set; } = "09:00"; 13 | public string DrinkWaterEndTime { get; set; } = "22:00"; 14 | public DrinkWaterMode DrinkWaterMode { get; set; } = DrinkWaterMode.Interval; 15 | public List CustomDrinkWaterTimes { get; set; } = new List(); 16 | public List TodoList { get; set; } = new List(); 17 | 18 | private static string ConfigPath => Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "settings.json"); 19 | 20 | public static AppSettings Load() 21 | { 22 | try 23 | { 24 | if (File.Exists(ConfigPath)) 25 | { 26 | var json = File.ReadAllText(ConfigPath); 27 | return JsonSerializer.Deserialize(json) ?? new AppSettings(); 28 | } 29 | } 30 | catch { } 31 | return new AppSettings(); 32 | } 33 | 34 | public void Save() 35 | { 36 | try 37 | { 38 | var json = JsonSerializer.Serialize(this, new JsonSerializerOptions { WriteIndented = true }); 39 | File.WriteAllText(ConfigPath, json); 40 | } 41 | catch { } 42 | } 43 | } 44 | 45 | public class TodoItem 46 | { 47 | public DateTime ReminderTime { get; set; } 48 | public string Content { get; set; } 49 | public bool IsCompleted { get; set; } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /App.xaml.cs: -------------------------------------------------------------------------------- 1 | using System.Configuration; 2 | using System.Data; 3 | using System.Windows; 4 | using System.Windows.Controls; 5 | using Hardcodet.Wpf.TaskbarNotification; 6 | 7 | namespace WinIsland 8 | { 9 | /// 10 | /// Interaction logic for App.xaml 11 | /// 12 | public partial class App : Application 13 | { 14 | private TaskbarIcon _trayIcon; 15 | 16 | protected override void OnStartup(StartupEventArgs e) 17 | { 18 | base.OnStartup(e); 19 | 20 | // 创建托盘图标 21 | _trayIcon = new TaskbarIcon 22 | { 23 | Icon = LoadIconFromResource("ico.ico"), 24 | ToolTipText = "WinIsland - 灵动岛", 25 | ContextMenu = (ContextMenu)FindResource("TrayMenu") 26 | }; 27 | _trayIcon.TrayLeftMouseDown += (s, args) => MainWindow?.Activate(); 28 | } 29 | 30 | private System.Drawing.Icon LoadIconFromResource(string resourceName) 31 | { 32 | try 33 | { 34 | Uri uri = new Uri($"pack://application:,,,/{resourceName}"); 35 | var streamInfo = GetResourceStream(uri); 36 | if (streamInfo != null) 37 | { 38 | return new System.Drawing.Icon(streamInfo.Stream); 39 | } 40 | } 41 | catch { } 42 | 43 | // 如果加载失败,使用系统默认图标 44 | return System.Drawing.SystemIcons.Application; 45 | } 46 | 47 | protected override void OnExit(ExitEventArgs e) 48 | { 49 | _trayIcon?.Dispose(); 50 | base.OnExit(e); 51 | } 52 | 53 | private void Settings_Click(object sender, RoutedEventArgs e) 54 | { 55 | // 打开设置窗口 56 | var settingsWindow = new SettingsWindow(); 57 | settingsWindow.Show(); 58 | } 59 | 60 | private void Exit_Click(object sender, RoutedEventArgs e) 61 | { 62 | Shutdown(); 63 | } 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🏝️ WinIsland (Win灵动岛) 2 | 3 | > **打破 Windows 沉闷交互的边界,单屏生产力的终极形态。** 4 | > 5 | > WinIsland 不仅仅是 iOS 灵动岛的复刻,它是为 Windows 桌面生态量身打造的**智能中枢**。它以极简的 "胶囊" 形态悬浮于顶,却在每一次需要时,以惊艳的物理弹性动画展开无限可能。 6 | 7 | ![WinIsland](ioc.png) 8 | 9 | ## ✨ 最新动态 (Update) 10 | 11 | - **🆕 独立便携版**:现已支持单文件运行,无需复杂的安装步骤,即点即用。 12 | - **🚀 性能飞跃**:重写物理动画引擎,CPU 占用率即使在动画时也微乎其微。 13 | - **🔧 交互升级**:支持任意位置拖拽停靠,双击快速进入专注模式。 14 | - **📈 监控组件**:新增 CPU/内存/网速实时监控,以及任务栏通用进度条投射。 15 | 16 | ## 🎮 快捷交互指南 (Quick Start) 17 | 18 | WinIsland 提倡 "直觉式" 的交互体验,没有复杂的菜单,一切尽在指尖: 19 | 20 | | 操作 | 动作 | 说明 | 21 | | :--- | :--- | :--- | 22 | | **✋ 移动位置** | **按住左键拖拽** | 觉得挡住了视线?按住灵动岛任意空白处,即可将其拖拽到屏幕任何位置,松手自动吸附。 | 23 | | **🧘 专注模式** | **双击左键** | 快速开启/关闭番茄钟专注模式,屏蔽外界打扰,专注于当下任务。 | 24 | | **📂 文件中转** | **拖拽文件至岛** | 将文件拖向灵动岛,它会变身 "黑洞" 吸入文件;再次拖出即可释放到其他软件。 | 25 | | **🖱️ 鼠标穿透** | **自动感应** | 当没有媒体播放或通知时,鼠标可以直接穿透灵动岛点击后方的窗口,互不干扰。 | 26 | 27 | ## 🚀 核心功能 28 | 29 | ### 1. 🌪️ 独创「文件引力黑洞」 30 | **即使只有一块屏幕,也能享受多屏般的高效文件流转。** 31 | ![File Gravity Hole](assets/feature_blackhole.png) 32 | * **Drag & Drop 2.0**:当你拖拽文件至屏幕顶部,灵动岛瞬间化身紫色引力黑洞。 33 | * **暂存任意文件**:松手即吸入。文件被安全托管在岛内,不再占用你的鼠标和剪贴板。 34 | * **跨应用传输**:切换到微信、PS 或邮件窗口,从岛上轻轻一拖,文件即刻释放。 35 | 36 | ### 2. 🎵 沉浸式媒体接管 37 | **让音乐不仅好听,而且好看。** 38 | ![Media Control](assets/feature_media.png) 39 | * **全局兼容**:完美支持网易云音乐、Spotify、Apple Music 等主流播放器。 40 | * **视觉律动**:内置实时音频频谱分析,灵动岛会随着重低音的节奏跳动呼吸。 41 | * **打扰更少**:切歌、暂停,一切操作均在顶部微型窗口完成,无需离开当前工作区。 42 | 43 | ### 3. ⚡ 硬件级感知与全场景通知接管 44 | **彻底取代 Windows 原生通知,更加优雅,更懂你。** 45 | ![Hardware Notification](assets/feature_notify.png) 46 | * **消息接管**:QQ、钉钉... 所有应用的消息通知都将被灵动岛统一接管。它会自动提取核心内容展示,并支持**自动清理系统原生通知**,还你一个干净的侧边栏。 47 | * **硬件感知**:AirPods 连接了?U盘拔出了?灵动岛会以优雅的 3D 翻转动画告知你,随后自动隐退。 48 | 49 | ### 4. 💻 极客组件 (Geek Tools) 50 | **专为极客打造的系统级感知能力。** 51 | * **系统仪表盘**:待机时可显示 CPU 使用率、内存负载以及实时上传/下载网速,时刻掌握电脑状态。 52 | * **通用进度投射**:自动捕获浏览器下载、文件解压等任务栏进度条,将其同步投射到灵动岛上,无需反复切换窗口查看进度。 53 | 54 | ### 5. 🧘 赛博养生与专注 55 | ![Health Assistant](assets/feature_health.png) 56 | * **全屏专注模式**:双击灵动岛开启番茄钟,屏幕边缘伴随金色呼吸光效,助你进入心流状态。 57 | * **智能喝水提醒**:可设置固定间隔或自定义时间点,细腻的水滴动画提醒你补充水分。 58 | * **Todo 速记**:不再遗忘闪念即逝的任务,时刻保持井井有条。 59 | 60 | ### 6. 🛡️ 智能隐身 61 | * **交互穿透**:智能识别状态,平时不妨碍你点击岛屿后方的窗口。 62 | * **自动避让**:鼠标靠近时可自动变透明(需在设置中开启),防止遮挡关键内容。 63 | 64 | --- 65 | 66 | ## 📥 使用方法 67 | 68 | 1. **下载**:获取最新的 `WinIsland.exe`。 69 | 2. **运行**:双击即可启动,无需安装。 70 | 3. **设置**:在灵动岛上右键(或通过托盘图标)可打开设置面板,自定义你的偏好。 71 | 72 | > **注意**:部分安全软件可能会对新发布的程序进行拦截,请允许运行以获得完整体验。 73 | 74 | ## 🎨 极致的工匠设计 75 | 76 | 我们拒绝 Windows Forms 的廉价感。WinIsland 的每一像素都经过精心打磨: 77 | * **Spring Physics 引擎**:自研弹簧阻尼算法,窗口的每一次变大变小,都像果冻一样Q弹真实。 78 | * **Glassmorphism**:深邃的背景模糊与弥散光影,完美融入 Windows 11 设计语言。 79 | * **60FPS 流畅度**:基于 GPU 加速的渲染管线,拒绝掉帧与卡顿。 80 | 81 | --- 82 | 83 | ## ❤️ 参与贡献 84 | 85 | WinIsland 是一个开源项目,我们需要你的创意! 86 | 如果你有好的想法或发现了 Bug,欢迎提交 Issue 或 PR。让我们一起重新定义 Windows 的桌面体验。 87 | 88 | *Created with ❤️ by Antigravity* 89 | -------------------------------------------------------------------------------- /App.xaml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | #000000 10 | #1C1C1E 11 | #2C2C2E 12 | #007AFF 13 | #FF3B30 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 41 | 42 | 43 | 76 | 77 | 89 | 90 | 91 | 92 | 93 | 95 | 96 | 97 | 98 | 99 | 100 | 102 | 103 | 104 | 105 | 106 | 107 | -------------------------------------------------------------------------------- /SettingsWindow.xaml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | using System.Windows; 5 | using System.Windows.Controls; 6 | using System.Windows.Input; 7 | using System.Windows.Media; 8 | using Microsoft.Win32; 9 | 10 | namespace WinIsland 11 | { 12 | public partial class SettingsWindow : Window 13 | { 14 | public SettingsWindow() 15 | { 16 | InitializeComponent(); 17 | LoadSettings(); 18 | } 19 | 20 | private void Window_MouseDown(object sender, MouseButtonEventArgs e) 21 | { 22 | if (e.ChangedButton == MouseButton.Left) 23 | this.DragMove(); 24 | } 25 | 26 | private void LoadSettings() 27 | { 28 | var settings = AppSettings.Load(); 29 | 30 | // 系统设置 31 | ChkStartWithWindows.IsChecked = IsStartupEnabled(); 32 | 33 | // 喝水提醒 34 | ChkDrinkWater.IsChecked = settings.DrinkWaterEnabled; 35 | TxtDrinkWaterInterval.Text = settings.DrinkWaterIntervalMinutes.ToString(); 36 | TxtDrinkStartTime.Text = settings.DrinkWaterStartTime; 37 | TxtDrinkEndTime.Text = settings.DrinkWaterEndTime; 38 | 39 | if (settings.DrinkWaterMode == DrinkWaterMode.Custom) 40 | RbModeCustom.IsChecked = true; 41 | else 42 | RbModeInterval.IsChecked = true; 43 | 44 | ListCustomDrinkTimes.ItemsSource = settings.CustomDrinkWaterTimes; 45 | 46 | // 待办事项 47 | ChkTodo.IsChecked = settings.TodoEnabled; 48 | ListTodo.ItemsSource = settings.TodoList; 49 | DpTodoDate.SelectedDate = DateTime.Today; 50 | 51 | UpdateDrinkWaterUI(); 52 | UpdateTodoUI(); 53 | } 54 | 55 | private void Save_Click(object sender, RoutedEventArgs e) 56 | { 57 | // 系统自启动 58 | if (ChkStartWithWindows.IsChecked == true) EnableStartup(); 59 | else DisableStartup(); 60 | 61 | var settings = AppSettings.Load(); 62 | 63 | // 喝水提醒 64 | settings.DrinkWaterEnabled = ChkDrinkWater.IsChecked == true; 65 | if (int.TryParse(TxtDrinkWaterInterval.Text, out int interval)) 66 | { 67 | settings.DrinkWaterIntervalMinutes = Math.Max(1, interval); 68 | } 69 | settings.DrinkWaterStartTime = TxtDrinkStartTime.Text; 70 | settings.DrinkWaterEndTime = TxtDrinkEndTime.Text; 71 | settings.DrinkWaterMode = RbModeCustom.IsChecked == true ? DrinkWaterMode.Custom : DrinkWaterMode.Interval; 72 | 73 | // 待办事项 74 | settings.TodoEnabled = ChkTodo.IsChecked == true; 75 | 76 | settings.Save(); 77 | 78 | // 通知主窗口重新加载设置 79 | if (Application.Current.MainWindow is MainWindow mw) 80 | { 81 | mw.ReloadSettings(); 82 | } 83 | 84 | Close(); 85 | } 86 | 87 | private void Cancel_Click(object sender, RoutedEventArgs e) 88 | { 89 | Close(); 90 | } 91 | 92 | private bool IsStartupEnabled() 93 | { 94 | try 95 | { 96 | using var key = Registry.CurrentUser.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Run", false); 97 | return key?.GetValue("WinIsland") != null; 98 | } 99 | catch { return false; } 100 | } 101 | 102 | private void EnableStartup() 103 | { 104 | try 105 | { 106 | using var key = Registry.CurrentUser.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Run", true); 107 | key?.SetValue("WinIsland", System.Reflection.Assembly.GetExecutingAssembly().Location); 108 | } 109 | catch (Exception ex) 110 | { 111 | MessageBox.Show($"无法启用开机自启: {ex.Message}"); 112 | } 113 | } 114 | 115 | private void DisableStartup() 116 | { 117 | try 118 | { 119 | using var key = Registry.CurrentUser.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Run", true); 120 | key?.DeleteValue("WinIsland", false); 121 | } 122 | catch { } 123 | } 124 | 125 | private void ChkDrinkWater_Checked(object sender, RoutedEventArgs e) => UpdateDrinkWaterUI(); 126 | private void ChkDrinkWater_Unchecked(object sender, RoutedEventArgs e) => UpdateDrinkWaterUI(); 127 | private void RbMode_Checked(object sender, RoutedEventArgs e) => UpdateDrinkWaterUI(); 128 | 129 | private void UpdateDrinkWaterUI() 130 | { 131 | if (PanelDrinkWaterSettings == null) return; 132 | 133 | if (ChkDrinkWater.IsChecked == true) 134 | { 135 | PanelDrinkWaterSettings.Visibility = Visibility.Visible; 136 | if (RbModeCustom.IsChecked == true) 137 | { 138 | PanelModeInterval.Visibility = Visibility.Collapsed; 139 | PanelModeCustom.Visibility = Visibility.Visible; 140 | } 141 | else 142 | { 143 | PanelModeInterval.Visibility = Visibility.Visible; 144 | PanelModeCustom.Visibility = Visibility.Collapsed; 145 | } 146 | } 147 | else 148 | { 149 | PanelDrinkWaterSettings.Visibility = Visibility.Collapsed; 150 | } 151 | } 152 | 153 | private void ChkTodo_Checked(object sender, RoutedEventArgs e) => UpdateTodoUI(); 154 | private void ChkTodo_Unchecked(object sender, RoutedEventArgs e) => UpdateTodoUI(); 155 | 156 | private void UpdateTodoUI() 157 | { 158 | if (PanelTodo == null) return; 159 | PanelTodo.Visibility = ChkTodo.IsChecked == true ? Visibility.Visible : Visibility.Collapsed; 160 | } 161 | 162 | // --- 自定义时间逻辑 --- 163 | 164 | private void BtnAddCustomDrinkTime_Click(object sender, RoutedEventArgs e) 165 | { 166 | string input = TxtCustomDrinkTime.Text.Trim(); 167 | if (string.IsNullOrEmpty(input)) return; 168 | 169 | if (!TryParseTime(input, out string formattedTime)) 170 | { 171 | MessageBox.Show("时间格式错误,请使用 HH:MM"); 172 | return; 173 | } 174 | 175 | var settings = AppSettings.Load(); 176 | 177 | // 重新加载以进行修改 178 | var currentList = (List)ListCustomDrinkTimes.ItemsSource ?? new List(); 179 | if (!currentList.Contains(formattedTime)) 180 | { 181 | currentList.Add(formattedTime); 182 | currentList.Sort(); 183 | 184 | // 刷新列表 185 | ListCustomDrinkTimes.ItemsSource = null; 186 | ListCustomDrinkTimes.ItemsSource = currentList; 187 | 188 | // 立即保存 189 | var s = AppSettings.Load(); 190 | s.CustomDrinkWaterTimes = currentList; 191 | s.Save(); 192 | } 193 | 194 | TxtCustomDrinkTime.Text = ""; 195 | } 196 | 197 | private void BtnDeleteCustomDrinkTime_Click(object sender, RoutedEventArgs e) 198 | { 199 | if (sender is Button btn && btn.Tag is string timeStr) 200 | { 201 | var currentList = (List)ListCustomDrinkTimes.ItemsSource; 202 | if (currentList != null && currentList.Remove(timeStr)) 203 | { 204 | ListCustomDrinkTimes.ItemsSource = null; 205 | ListCustomDrinkTimes.ItemsSource = currentList; 206 | 207 | var s = AppSettings.Load(); 208 | s.CustomDrinkWaterTimes = currentList; 209 | s.Save(); 210 | } 211 | } 212 | } 213 | 214 | // --- 待办事项逻辑 --- 215 | 216 | private void BtnAddTodo_Click(object sender, RoutedEventArgs e) 217 | { 218 | if (DpTodoDate.SelectedDate == null) return; 219 | if (!TimeSpan.TryParse(TxtTodoTime.Text, out TimeSpan time)) return; 220 | if (string.IsNullOrWhiteSpace(TxtTodoContent.Text)) return; 221 | 222 | var newItem = new TodoItem 223 | { 224 | ReminderTime = DpTodoDate.SelectedDate.Value.Date + time, 225 | Content = TxtTodoContent.Text, 226 | IsCompleted = false 227 | }; 228 | 229 | var currentList = (List)ListTodo.ItemsSource ?? new List(); 230 | currentList.Add(newItem); 231 | 232 | // 按时间排序 233 | currentList.Sort((a, b) => a.ReminderTime.CompareTo(b.ReminderTime)); 234 | 235 | ListTodo.ItemsSource = null; 236 | ListTodo.ItemsSource = currentList; 237 | 238 | // 立即保存 239 | var s = AppSettings.Load(); 240 | s.TodoList = currentList; 241 | s.Save(); 242 | 243 | TxtTodoContent.Text = ""; 244 | } 245 | 246 | private void BtnDeleteTodo_Click(object sender, RoutedEventArgs e) 247 | { 248 | if (sender is Button btn && btn.Tag is TodoItem item) 249 | { 250 | var currentList = (List)ListTodo.ItemsSource; 251 | if (currentList != null) 252 | { 253 | // 移除匹配的项目 254 | currentList.RemoveAll(x => x.Content == item.Content && x.ReminderTime == item.ReminderTime); 255 | 256 | ListTodo.ItemsSource = null; 257 | ListTodo.ItemsSource = currentList; 258 | 259 | var s = AppSettings.Load(); 260 | s.TodoList = currentList; 261 | s.Save(); 262 | } 263 | } 264 | } 265 | 266 | // --- 辅助方法 --- 267 | 268 | private bool TryParseTime(string input, out string formattedTime) 269 | { 270 | formattedTime = ""; 271 | input = input.Replace(" ", "").Replace(":", ""); 272 | 273 | // 支持 930 -> 09:30, 1400 -> 14:00 274 | if (input.Length == 3) input = "0" + input; 275 | 276 | if (input.Length == 4 && int.TryParse(input, out _)) 277 | { 278 | string h = input.Substring(0, 2); 279 | string m = input.Substring(2, 2); 280 | if (int.Parse(h) < 24 && int.Parse(m) < 60) 281 | { 282 | formattedTime = $"{h}:{m}"; 283 | return true; 284 | } 285 | } 286 | 287 | // 标准解析 288 | if (TimeSpan.TryParse(input, out TimeSpan ts)) 289 | { 290 | formattedTime = ts.ToString(@"hh\:mm"); 291 | return true; 292 | } 293 | 294 | return false; 295 | } 296 | } 297 | } 298 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /SettingsWindow.xaml: -------------------------------------------------------------------------------- 1 | 13 | 14 | 15 | 16 | #1E1E1E 17 | #252526 18 | #007ACC 19 | #E0E0E0 20 | #858585 21 | #333333 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 52 | 53 | 61 | 62 | 63 | 69 | 70 | 76 | 77 | 78 | 107 | 108 | 128 | 129 | 130 | 163 | 164 | 165 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 379 | 380 | 381 | 387 | 388 | 389 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | -------------------------------------------------------------------------------- /MainWindow.xaml.cs: -------------------------------------------------------------------------------- 1 | using NAudio.Wave; 2 | using System; 3 | using System.IO; 4 | using System.Management; // WMI 5 | using System.Threading.Tasks; 6 | using System.Windows; 7 | using System.Windows.Controls; 8 | using System.Windows.Media; 9 | using System.Windows.Media.Animation; 10 | using System.Windows.Media.Imaging; 11 | using System.Windows.Threading; 12 | using Windows.Media.Control; 13 | using Windows.Storage.Streams; 14 | using Windows.UI.Notifications.Management; // UserNotificationListener 15 | using Windows.UI.Notifications; // UserNotification 16 | using System.Windows.Forms; 17 | using System.Windows.Media.Effects; // Screen 18 | 19 | namespace WinIsland 20 | { 21 | public partial class MainWindow : Window 22 | { 23 | private bool _isFileStationActive = false; 24 | private List _storedFiles = new List(); 25 | private Spring _widthSpring; 26 | private Spring _heightSpring; 27 | private DateTime _lastFrameTime; 28 | 29 | private GlobalSystemMediaTransportControlsSessionManager _mediaManager; 30 | private GlobalSystemMediaTransportControlsSession _currentSession; 31 | private WasapiLoopbackCapture _capture; 32 | private float _currentVolume = 0; 33 | 34 | // 通知相关 35 | private DispatcherTimer _notificationTimer; 36 | private bool _isNotificationActive = false; 37 | private UserNotificationListener _listener; 38 | 39 | public MainWindow() 40 | { 41 | InitializeComponent(); 42 | 43 | // 隐藏在 Alt+Tab 切换器中 44 | this.Loaded += (s, e) => 45 | { 46 | var hwnd = new System.Windows.Interop.WindowInteropHelper(this).Handle; 47 | int exStyle = GetWindowLong(hwnd, GWL_EXSTYLE); 48 | SetWindowLong(hwnd, GWL_EXSTYLE, exStyle | WS_EX_TOOLWINDOW); 49 | 50 | Task.Delay(100).ContinueWith(_ => Dispatcher.Invoke(() => SetClickThrough(true))); 51 | }; 52 | 53 | // 窗口内容渲染完成后居中 54 | this.ContentRendered += (s, e) => 55 | { 56 | CenterWindowAtTop(); 57 | }; 58 | 59 | InitializePhysics(); 60 | InitializeMediaListener(); 61 | InitializeAudioCapture(); 62 | InitializeDeviceWatcher(); 63 | InitializeNotificationTimer(); 64 | InitializeNotificationListener(); 65 | InitializeDrinkWaterFeature(); 66 | InitializeTodoFeature(); 67 | } 68 | 69 | public void ReloadSettings() 70 | { 71 | InitializeDrinkWaterFeature(); 72 | InitializeTodoFeature(); 73 | } 74 | 75 | private void CenterWindowAtTop() 76 | { 77 | try 78 | { 79 | // 获取主屏幕的工作区域 80 | var screenWidth = SystemParameters.PrimaryScreenWidth; 81 | var screenHeight = SystemParameters.PrimaryScreenHeight; 82 | 83 | // 计算居中位置 84 | this.Left = (screenWidth - this.Width) / 2; 85 | this.Top = 10; // 距离顶部 10 像素 86 | 87 | LogDebug($"Window centered: Left={this.Left}, Top={this.Top}, Width={this.Width}, ScreenWidth={screenWidth}"); 88 | } 89 | catch (Exception ex) 90 | { 91 | LogDebug($"Center window error: {ex.Message}"); 92 | } 93 | } 94 | 95 | // Win32 API 声明 96 | private const int GWL_EXSTYLE = -20; 97 | private const int WS_EX_TOOLWINDOW = 0x00000080; 98 | private const int WS_EX_TRANSPARENT = 0x00000020; 99 | private const int WS_EX_LAYERED = 0x00080000; 100 | 101 | private void SetClickThrough(bool enable) 102 | { 103 | try 104 | { 105 | var hwnd = new System.Windows.Interop.WindowInteropHelper(this).Handle; 106 | int exStyle = GetWindowLong(hwnd, GWL_EXSTYLE); 107 | if (enable) 108 | { 109 | SetWindowLong(hwnd, GWL_EXSTYLE, exStyle | WS_EX_TRANSPARENT); 110 | LogDebug("Click-through enabled"); 111 | } 112 | else 113 | { 114 | SetWindowLong(hwnd, GWL_EXSTYLE, exStyle & ~WS_EX_TRANSPARENT); 115 | LogDebug("Click-through disabled"); 116 | } 117 | } 118 | catch { } 119 | } 120 | 121 | [System.Runtime.InteropServices.DllImport("user32.dll")] 122 | private static extern int GetWindowLong(IntPtr hWnd, int nIndex); 123 | 124 | [System.Runtime.InteropServices.DllImport("user32.dll")] 125 | private static extern int SetWindowLong(IntPtr hWnd, int nIndex, int dwNewLong); 126 | 127 | #region 0. 通知逻辑 (WMI & UserNotificationListener) 128 | 129 | private void InitializeNotificationTimer() 130 | { 131 | _notificationTimer = new DispatcherTimer(); 132 | _notificationTimer.Interval = TimeSpan.FromSeconds(3); // 通知显示3秒 133 | _notificationTimer.Tick += (s, e) => HideNotification(); 134 | } 135 | 136 | private async void InitializeNotificationListener() 137 | { 138 | try 139 | { 140 | if (!Windows.Foundation.Metadata.ApiInformation.IsTypePresent("Windows.UI.Notifications.Management.UserNotificationListener")) return; 141 | 142 | _listener = UserNotificationListener.Current; 143 | var accessStatus = await _listener.RequestAccessAsync(); 144 | 145 | if (accessStatus == UserNotificationListenerAccessStatus.Allowed) 146 | { 147 | _listener.NotificationChanged += Listener_NotificationChanged; 148 | } 149 | } 150 | catch (Exception ex) 151 | { 152 | System.Diagnostics.Debug.WriteLine($"Notification Listener Error: {ex.Message}"); 153 | } 154 | } 155 | 156 | private void Listener_NotificationChanged(UserNotificationListener sender, UserNotificationChangedEventArgs args) 157 | { 158 | try 159 | { 160 | // 暂时移除 ChangeType 检查 161 | var notifId = args.UserNotificationId; 162 | var notif = _listener.GetNotification(notifId); 163 | if (notif == null) return; 164 | 165 | var appName = notif.AppInfo.DisplayInfo.DisplayName; 166 | if (string.IsNullOrEmpty(appName)) return; 167 | 168 | // 简单的过滤逻辑: 微信 或 QQ 169 | bool isWeChat = appName.Contains("WeChat", StringComparison.OrdinalIgnoreCase) || appName.Contains("微信"); 170 | bool isQQ = appName.Contains("QQ", StringComparison.OrdinalIgnoreCase); 171 | 172 | if (isWeChat || isQQ) 173 | { 174 | string displayMsg = $"{appName}: New Message"; 175 | 176 | // 尝试获取详细内容 177 | try 178 | { 179 | var binding = notif.Notification.Visual.GetBinding(KnownNotificationBindings.ToastGeneric); 180 | if (binding != null) 181 | { 182 | var texts = binding.GetTextElements(); 183 | string title = texts.Count > 0 ? texts[0].Text : appName; 184 | string body = texts.Count > 1 ? texts[1].Text : "New Message"; 185 | displayMsg = $"{title}: {body}"; 186 | } 187 | } 188 | catch { } 189 | 190 | Dispatcher.Invoke(() => ShowMessageNotification(displayMsg)); 191 | } 192 | } 193 | catch { } 194 | } 195 | 196 | // 使用 Windows.Devices.Enumeration 替代 WMI,更加轻量且实时 197 | private Windows.Devices.Enumeration.DeviceWatcher _bluetoothWatcher; 198 | private Windows.Devices.Enumeration.DeviceWatcher _usbWatcher; 199 | private System.Collections.Concurrent.ConcurrentDictionary _deviceMap = new System.Collections.Concurrent.ConcurrentDictionary(); 200 | private System.Collections.Concurrent.ConcurrentDictionary _deviceStateCache = new System.Collections.Concurrent.ConcurrentDictionary(); 201 | private bool _isBluetoothEnumComplete = false; 202 | private bool _isUsbEnumComplete = false; 203 | 204 | private void InitializeDeviceWatcher() 205 | { 206 | try 207 | { 208 | LogDebug("Initializing Device Watchers..."); 209 | 210 | // 蓝牙设备监听 (配对的蓝牙设备) 211 | string bluetoothSelector = "System.Devices.Aep.ProtocolId:=\"{e0cbf06c-cd8b-4647-bb8a-263b43f0f974}\""; 212 | 213 | // 请求额外属性,特别是连接状态 214 | var requestedProperties = new string[] 215 | { 216 | "System.Devices.Aep.IsConnected", 217 | "System.Devices.Aep.SignalStrength", 218 | "System.Devices.Aep.Bluetooth.Le.IsConnectable" 219 | }; 220 | 221 | _bluetoothWatcher = Windows.Devices.Enumeration.DeviceInformation.CreateWatcher( 222 | bluetoothSelector, 223 | requestedProperties, 224 | Windows.Devices.Enumeration.DeviceInformationKind.AssociationEndpoint); 225 | 226 | _bluetoothWatcher.Added += BluetoothWatcher_Added; 227 | _bluetoothWatcher.Removed += BluetoothWatcher_Removed; 228 | _bluetoothWatcher.Updated += BluetoothWatcher_Updated; 229 | _bluetoothWatcher.EnumerationCompleted += (s, e) => 230 | { 231 | _isBluetoothEnumComplete = true; 232 | LogDebug("Bluetooth enumeration completed"); 233 | }; 234 | _bluetoothWatcher.Start(); 235 | LogDebug("Bluetooth watcher started"); 236 | 237 | // USB 设备监听 238 | string usbSelector = "System.Devices.InterfaceClassGuid:=\"{a5dcbf10-6530-11d2-901f-00c04fb951ed}\""; // USB 设备接口 239 | _usbWatcher = Windows.Devices.Enumeration.DeviceInformation.CreateWatcher( 240 | usbSelector, 241 | null, 242 | Windows.Devices.Enumeration.DeviceInformationKind.DeviceInterface); 243 | 244 | _usbWatcher.Added += UsbWatcher_Added; 245 | _usbWatcher.Removed += UsbWatcher_Removed; 246 | _usbWatcher.EnumerationCompleted += (s, e) => { _isUsbEnumComplete = true; }; 247 | _usbWatcher.Start(); 248 | } 249 | catch (Exception ex) 250 | { 251 | System.Diagnostics.Debug.WriteLine($"Device Watcher Error: {ex.Message}"); 252 | } 253 | } 254 | 255 | private void BluetoothWatcher_Added(Windows.Devices.Enumeration.DeviceWatcher sender, Windows.Devices.Enumeration.DeviceInformation args) 256 | { 257 | LogDebug($"BT Added: {args.Name} (ID: {args.Id.Substring(0, Math.Min(30, args.Id.Length))})"); 258 | 259 | // 过滤无效设备名 260 | if (string.IsNullOrEmpty(args.Name) || !IsValidDeviceName(args.Name)) 261 | { 262 | LogDebug($"BT Added: Filtered invalid name: {args.Name}"); 263 | return; 264 | } 265 | 266 | _deviceMap.TryAdd(args.Id, args.Name); 267 | 268 | // 初始化设备状态缓存(假设初始为断开) 269 | if (args.Properties.ContainsKey("System.Devices.Aep.IsConnected")) 270 | { 271 | bool isConnected = (bool)args.Properties["System.Devices.Aep.IsConnected"]; 272 | _deviceStateCache[args.Id] = (isConnected, DateTime.Now); 273 | LogDebug($"BT Added: Initial state = {isConnected}"); 274 | } 275 | 276 | // 只在枚举完成后且设备是连接状态才显示通知 277 | if (_isBluetoothEnumComplete && args.Properties.ContainsKey("System.Devices.Aep.IsConnected")) 278 | { 279 | bool isConnected = (bool)args.Properties["System.Devices.Aep.IsConnected"]; 280 | if (isConnected) 281 | { 282 | LogDebug($"BT Added Notification: {args.Name}"); 283 | Dispatcher.Invoke(() => ShowDeviceNotification($"蓝牙: {args.Name}", true)); 284 | } 285 | } 286 | } 287 | 288 | private void BluetoothWatcher_Removed(Windows.Devices.Enumeration.DeviceWatcher sender, Windows.Devices.Enumeration.DeviceInformationUpdate args) 289 | { 290 | LogDebug($"BT Removed: ID={args.Id.Substring(0, Math.Min(30, args.Id.Length))}"); 291 | 292 | // 检查该设备之前的连接状态 293 | bool wasConnected = false; 294 | if (_deviceStateCache.TryRemove(args.Id, out var lastState)) 295 | { 296 | wasConnected = lastState.isConnected; 297 | } 298 | 299 | if (_deviceMap.TryRemove(args.Id, out string deviceName)) 300 | { 301 | LogDebug($"BT Removed from map: {deviceName}"); 302 | // 只有当枚举完成,且设备之前确实是连接状态时,才显示断开通知 303 | // 这样避免了未连接的配对设备在系统后台刷新时触发误报 304 | if (_isBluetoothEnumComplete && !string.IsNullOrEmpty(deviceName) && wasConnected) 305 | { 306 | LogDebug($"BT Removed Notification: {deviceName}"); 307 | Dispatcher.Invoke(() => ShowDeviceNotification($"蓝牙: {deviceName}", false)); 308 | } 309 | } 310 | } 311 | 312 | private void BluetoothWatcher_Updated(Windows.Devices.Enumeration.DeviceWatcher sender, Windows.Devices.Enumeration.DeviceInformationUpdate args) 313 | { 314 | // 蓝牙设备状态更新(连接/断开) 315 | LogDebug($"BT Updated: ID={args.Id.Substring(0, Math.Min(30, args.Id.Length))}, Props={args.Properties.Count}"); 316 | 317 | if (args.Properties.ContainsKey("System.Devices.Aep.IsConnected")) 318 | { 319 | bool isConnected = (bool)args.Properties["System.Devices.Aep.IsConnected"]; 320 | LogDebug($"BT IsConnected: {isConnected}"); 321 | 322 | if (_deviceMap.TryGetValue(args.Id, out string deviceName) && !string.IsNullOrEmpty(deviceName)) 323 | { 324 | // 过滤无效设备名 325 | if (!IsValidDeviceName(deviceName)) 326 | { 327 | LogDebug($"BT Updated: Filtered invalid name: {deviceName}"); 328 | return; 329 | } 330 | 331 | // 防抖:检查状态是否真的改变 332 | var now = DateTime.Now; 333 | bool shouldNotify = false; 334 | 335 | if (_deviceStateCache.TryGetValue(args.Id, out var cachedState)) 336 | { 337 | // 状态没变,忽略 338 | if (cachedState.isConnected == isConnected) 339 | { 340 | LogDebug($"BT Updated: State unchanged, ignored"); 341 | return; 342 | } 343 | 344 | // 距离上次更新太近(2秒内),忽略 345 | if ((now - cachedState.lastUpdate).TotalSeconds < 2) 346 | { 347 | LogDebug($"BT Updated: Too frequent, ignored (last: {(now - cachedState.lastUpdate).TotalSeconds:F1}s ago)"); 348 | return; 349 | } 350 | 351 | // 状态真的改变了,且时间间隔足够 352 | shouldNotify = true; 353 | } 354 | else 355 | { 356 | // 第一次收到这个设备的状态,只在连接时通知 357 | shouldNotify = isConnected; 358 | } 359 | 360 | // 更新缓存 361 | _deviceStateCache[args.Id] = (isConnected, now); 362 | 363 | if (shouldNotify) 364 | { 365 | LogDebug($"BT Updated Notification: {deviceName} -> {(isConnected ? "Connected" : "Disconnected")}"); 366 | Dispatcher.Invoke(() => ShowDeviceNotification($"蓝牙: {deviceName}", isConnected)); 367 | } 368 | } 369 | else 370 | { 371 | LogDebug($"BT device not in map or empty name"); 372 | } 373 | } 374 | } 375 | 376 | private bool IsValidDeviceName(string name) 377 | { 378 | if (string.IsNullOrWhiteSpace(name)) return false; 379 | 380 | // 过滤太短的名字(可能是 MAC 地址片段) 381 | if (name.Length < 4) return false; 382 | 383 | // 过滤纯数字或纯字母+数字组合(如 A077, 1234) 384 | if (System.Text.RegularExpressions.Regex.IsMatch(name, @"^[A-Z0-9]{4,6}$")) 385 | { 386 | LogDebug($"Filtered MAC-like name: {name}"); 387 | return false; 388 | } 389 | 390 | // 过滤包含特殊字符的设备ID 391 | if (name.Contains("\\") || name.Contains("{") || name.Contains("}")) 392 | { 393 | return false; 394 | } 395 | 396 | return true; 397 | } 398 | 399 | private void LogDebug(string message) 400 | { 401 | try 402 | { 403 | string logPath = "debug_log.txt"; 404 | string logMessage = $"{DateTime.Now:yyyy/MM/dd HH:mm:ss}: {message}\n"; 405 | File.AppendAllText(logPath, logMessage); 406 | } 407 | catch { } 408 | } 409 | 410 | private void UsbWatcher_Added(Windows.Devices.Enumeration.DeviceWatcher sender, Windows.Devices.Enumeration.DeviceInformation args) 411 | { 412 | _deviceMap.TryAdd(args.Id, args.Name); 413 | 414 | if (_isUsbEnumComplete && !string.IsNullOrEmpty(args.Name)) 415 | { 416 | Dispatcher.Invoke(() => ShowDeviceNotification($"USB: {args.Name}", true)); 417 | } 418 | } 419 | 420 | private void UsbWatcher_Removed(Windows.Devices.Enumeration.DeviceWatcher sender, Windows.Devices.Enumeration.DeviceInformationUpdate args) 421 | { 422 | if (_deviceMap.TryRemove(args.Id, out string deviceName)) 423 | { 424 | if (_isUsbEnumComplete && !string.IsNullOrEmpty(deviceName)) 425 | { 426 | Dispatcher.Invoke(() => ShowDeviceNotification($"USB: {deviceName}", false)); 427 | } 428 | } 429 | } 430 | 431 | private void ShowDeviceNotification(string deviceName, bool isConnected) 432 | { 433 | ActivateNotification(); 434 | 435 | 436 | NotificationText.Text = deviceName; 437 | 438 | if (isConnected) 439 | { 440 | IconConnect.Visibility = Visibility.Visible; 441 | IconDisconnect.Visibility = Visibility.Collapsed; 442 | IconMessage.Visibility = Visibility.Collapsed; 443 | NotificationText.Foreground = new SolidColorBrush(Color.FromRgb(0, 255, 204)); // Green 444 | } 445 | else 446 | { 447 | IconConnect.Visibility = Visibility.Collapsed; 448 | IconDisconnect.Visibility = Visibility.Visible; 449 | IconMessage.Visibility = Visibility.Collapsed; 450 | NotificationText.Foreground = new SolidColorBrush(Color.FromRgb(255, 51, 51)); // Red 451 | } 452 | 453 | PlayFlipAnimation(); 454 | } 455 | 456 | private void ShowMessageNotification(string message) 457 | { 458 | ActivateNotification(); 459 | 460 | NotificationText.Text = message; 461 | 462 | IconConnect.Visibility = Visibility.Collapsed; 463 | IconDisconnect.Visibility = Visibility.Collapsed; 464 | IconMessage.Visibility = Visibility.Visible; 465 | NotificationText.Foreground = new SolidColorBrush(Color.FromRgb(0, 191, 255)); // DeepSkyBlue 466 | 467 | PlayFlipAnimation(); 468 | } 469 | 470 | private void ActivateNotification() 471 | { 472 | _isNotificationActive = true; 473 | _notificationTimer.Stop(); 474 | _notificationTimer.Start(); 475 | 476 | // 隐藏其他内容 477 | AlbumCover.Visibility = Visibility.Collapsed; 478 | SongTitle.Visibility = Visibility.Collapsed; 479 | ControlPanel.Visibility = Visibility.Collapsed; 480 | VisualizerContainer.Visibility = Visibility.Collapsed; 481 | 482 | // 显示通知面板 483 | NotificationPanel.Visibility = Visibility.Visible; 484 | NotificationPanel.Opacity = 0; 485 | DrinkWaterPanel.Visibility = Visibility.Collapsed; 486 | TodoPanel.Visibility = Visibility.Collapsed; 487 | FileStationPanel.Visibility = Visibility.Collapsed; 488 | 489 | DynamicIsland.IsHitTestVisible = true; // 允许鼠标交互 490 | SetClickThrough(false); // 激活通知时允许交互 491 | 492 | // 清除动画锁定,确保 1.0 生效 493 | DynamicIsland.BeginAnimation(UIElement.OpacityProperty, null); 494 | DynamicIsland.Opacity = 1.0; 495 | 496 | // 设定通知尺寸 497 | _widthSpring.Target = 320; 498 | _heightSpring.Target = 50; 499 | 500 | // 内容淡入动画 501 | var fadeIn = new DoubleAnimation 502 | { 503 | From = 0, 504 | To = 1, 505 | Duration = TimeSpan.FromMilliseconds(400), 506 | BeginTime = TimeSpan.FromMilliseconds(200), 507 | EasingFunction = new CubicEase { EasingMode = EasingMode.EaseOut } 508 | }; 509 | NotificationPanel.BeginAnimation(UIElement.OpacityProperty, fadeIn); 510 | } 511 | 512 | private void PlayFlipAnimation() 513 | { 514 | // 3D 翻转效果动画 515 | var flipAnimation = new DoubleAnimationUsingKeyFrames(); 516 | flipAnimation.KeyFrames.Add(new EasingDoubleKeyFrame(1, KeyTime.FromTimeSpan(TimeSpan.FromMilliseconds(0)))); 517 | flipAnimation.KeyFrames.Add(new EasingDoubleKeyFrame(0, KeyTime.FromTimeSpan(TimeSpan.FromMilliseconds(200))) 518 | { 519 | EasingFunction = new CubicEase { EasingMode = EasingMode.EaseIn } 520 | }); 521 | flipAnimation.KeyFrames.Add(new EasingDoubleKeyFrame(1.2, KeyTime.FromTimeSpan(TimeSpan.FromMilliseconds(400))) 522 | { 523 | EasingFunction = new BackEase { EasingMode = EasingMode.EaseOut, Amplitude = 0.5 } 524 | }); 525 | flipAnimation.KeyFrames.Add(new EasingDoubleKeyFrame(1, KeyTime.FromTimeSpan(TimeSpan.FromMilliseconds(500))) 526 | { 527 | EasingFunction = new CubicEase { EasingMode = EasingMode.EaseOut } 528 | }); 529 | 530 | var scaleYAnimation = new DoubleAnimationUsingKeyFrames(); 531 | scaleYAnimation.KeyFrames.Add(new EasingDoubleKeyFrame(1, KeyTime.FromTimeSpan(TimeSpan.FromMilliseconds(0)))); 532 | scaleYAnimation.KeyFrames.Add(new EasingDoubleKeyFrame(1.1, KeyTime.FromTimeSpan(TimeSpan.FromMilliseconds(200))) 533 | { 534 | EasingFunction = new CubicEase { EasingMode = EasingMode.EaseOut } 535 | }); 536 | scaleYAnimation.KeyFrames.Add(new EasingDoubleKeyFrame(1, KeyTime.FromTimeSpan(TimeSpan.FromMilliseconds(500))) 537 | { 538 | EasingFunction = new CubicEase { EasingMode = EasingMode.EaseOut } 539 | }); 540 | 541 | NotificationIconScale.BeginAnimation(ScaleTransform.ScaleXProperty, flipAnimation); 542 | NotificationIconScale.BeginAnimation(ScaleTransform.ScaleYProperty, scaleYAnimation); 543 | } 544 | 545 | private void HideNotification() 546 | { 547 | _isNotificationActive = false; 548 | _notificationTimer.Stop(); 549 | 550 | // 淡出动画 551 | var fadeOut = new DoubleAnimation 552 | { 553 | From = 1, 554 | To = 0, 555 | Duration = TimeSpan.FromMilliseconds(300), 556 | EasingFunction = new CubicEase { EasingMode = EasingMode.EaseIn } 557 | }; 558 | 559 | fadeOut.Completed += (s, e) => 560 | { 561 | NotificationPanel.Visibility = Visibility.Collapsed; 562 | DrinkWaterPanel.Visibility = Visibility.Collapsed; 563 | TodoPanel.Visibility = Visibility.Collapsed; 564 | 565 | // 恢复之前的状态 566 | CheckCurrentSession(); 567 | }; 568 | 569 | if (NotificationPanel.Visibility == Visibility.Visible) 570 | NotificationPanel.BeginAnimation(UIElement.OpacityProperty, fadeOut); 571 | else if (DrinkWaterPanel.Visibility == Visibility.Visible) 572 | DrinkWaterPanel.BeginAnimation(UIElement.OpacityProperty, fadeOut); 573 | else if (TodoPanel.Visibility == Visibility.Visible) 574 | TodoPanel.BeginAnimation(UIElement.OpacityProperty, fadeOut); 575 | else 576 | { 577 | // 如果没有可见的面板,直接恢复状态 578 | CheckCurrentSession(); 579 | } 580 | } 581 | 582 | #endregion 583 | 584 | #region Drink Water Notification 585 | 586 | private DispatcherTimer _drinkWaterScheduler; 587 | private DateTime _nextDrinkTime; 588 | private AppSettings _settings; 589 | 590 | private void InitializeDrinkWaterFeature() 591 | { 592 | _settings = AppSettings.Load(); 593 | 594 | if (_drinkWaterScheduler == null) 595 | { 596 | _drinkWaterScheduler = new DispatcherTimer(); 597 | _drinkWaterScheduler.Interval = TimeSpan.FromSeconds(30); // 每30秒检查一次 598 | _drinkWaterScheduler.Tick += DrinkWaterScheduler_Tick; 599 | } 600 | 601 | if (_settings.DrinkWaterEnabled) 602 | { 603 | // 如果是首次启用或重新启用,设置下一次提醒时间 604 | if (!_drinkWaterScheduler.IsEnabled) 605 | { 606 | ResetNextDrinkTime(); 607 | _drinkWaterScheduler.Start(); 608 | } 609 | } 610 | else 611 | { 612 | _drinkWaterScheduler.Stop(); 613 | } 614 | } 615 | 616 | private void ResetNextDrinkTime() 617 | { 618 | _nextDrinkTime = DateTime.Now.AddMinutes(_settings.DrinkWaterIntervalMinutes); 619 | LogDebug($"Next drink time set to: {_nextDrinkTime}"); 620 | } 621 | 622 | private string _lastTriggeredCustomTime = ""; 623 | 624 | private void DrinkWaterScheduler_Tick(object sender, EventArgs e) 625 | { 626 | if (!_settings.DrinkWaterEnabled) return; 627 | 628 | if (_settings.DrinkWaterMode == DrinkWaterMode.Interval) 629 | { 630 | // 检查是否在活动时间段内 631 | if (!IsWithinActiveHours()) return; 632 | 633 | if (DateTime.Now >= _nextDrinkTime) 634 | { 635 | // 该提醒了! 636 | ShowDrinkWaterNotification(); 637 | ResetNextDrinkTime(); 638 | } 639 | } 640 | else // 自定义模式 641 | { 642 | var nowStr = DateTime.Now.ToString("HH:mm"); 643 | if (_settings.CustomDrinkWaterTimes != null && _settings.CustomDrinkWaterTimes.Contains(nowStr)) 644 | { 645 | // 防止同一分钟内重复提醒 646 | if (_lastTriggeredCustomTime != nowStr) 647 | { 648 | ShowDrinkWaterNotification(); 649 | _lastTriggeredCustomTime = nowStr; 650 | } 651 | } 652 | } 653 | } 654 | 655 | private bool IsWithinActiveHours() 656 | { 657 | try 658 | { 659 | if (TimeSpan.TryParse(_settings.DrinkWaterStartTime, out TimeSpan start) && 660 | TimeSpan.TryParse(_settings.DrinkWaterEndTime, out TimeSpan end)) 661 | { 662 | var now = DateTime.Now.TimeOfDay; 663 | if (start <= end) 664 | { 665 | return now >= start && now <= end; 666 | } 667 | else 668 | { 669 | // 跨午夜 (例如 22:00 到 06:00) 670 | return now >= start || now <= end; 671 | } 672 | } 673 | } 674 | catch { } 675 | return true; // 如果解析失败默认为 true 676 | } 677 | 678 | private void ShowDrinkWaterNotification() 679 | { 680 | Dispatcher.Invoke(() => 681 | { 682 | _isNotificationActive = true; 683 | _notificationTimer.Stop(); // 停止其他通知的自动隐藏计时器 684 | 685 | // 隐藏其他内容 686 | AlbumCover.Visibility = Visibility.Collapsed; 687 | SongTitle.Visibility = Visibility.Collapsed; 688 | ControlPanel.Visibility = Visibility.Collapsed; 689 | VisualizerContainer.Visibility = Visibility.Collapsed; 690 | NotificationPanel.Visibility = Visibility.Collapsed; 691 | TodoPanel.Visibility = Visibility.Collapsed; 692 | FileStationPanel.Visibility = Visibility.Collapsed; 693 | 694 | // 显示喝水提醒 695 | DrinkWaterPanel.Visibility = Visibility.Visible; 696 | DrinkWaterPanel.Opacity = 0; 697 | 698 | DynamicIsland.IsHitTestVisible = true; // 允许鼠标交互 699 | SetClickThrough(false); 700 | 701 | // 清除动画锁定 702 | DynamicIsland.BeginAnimation(UIElement.OpacityProperty, null); 703 | DynamicIsland.Opacity = 1.0; 704 | 705 | // 展开动画 (更宽一点) 706 | _widthSpring.Target = 280; 707 | _heightSpring.Target = 50; 708 | 709 | // 岛屿发光脉冲 (蓝色) 710 | PlayIslandGlowEffect(Colors.DeepSkyBlue); 711 | 712 | // 内容进场 (上浮淡入) 713 | PlayContentEntranceAnimation(DrinkWaterPanel); 714 | 715 | // 水滴动画 (优化版) 716 | PlayWaterDropAnimation(); 717 | }); 718 | } 719 | 720 | private void PlayWaterDropAnimation() 721 | { 722 | // 水滴悬浮动画 723 | var floatAnim = new DoubleAnimationUsingKeyFrames { RepeatBehavior = RepeatBehavior.Forever }; 724 | floatAnim.KeyFrames.Add(new EasingDoubleKeyFrame(0, KeyTime.FromTimeSpan(TimeSpan.Zero))); 725 | floatAnim.KeyFrames.Add(new EasingDoubleKeyFrame(-3, KeyTime.FromTimeSpan(TimeSpan.FromMilliseconds(1000))) 726 | { EasingFunction = new SineEase { EasingMode = EasingMode.EaseInOut } }); 727 | floatAnim.KeyFrames.Add(new EasingDoubleKeyFrame(0, KeyTime.FromTimeSpan(TimeSpan.FromMilliseconds(2000))) 728 | { EasingFunction = new SineEase { EasingMode = EasingMode.EaseInOut } }); 729 | 730 | WaterIconTranslate?.BeginAnimation(TranslateTransform.YProperty, floatAnim); 731 | } 732 | 733 | private void BtnDrank_Click(object sender, RoutedEventArgs e) 734 | { 735 | // 用户已确认 736 | HideNotification(); // 复用隐藏逻辑来重置状态 737 | ResetNextDrinkTime(); // 重置周期 738 | } 739 | 740 | #endregion 741 | 742 | #region Todo Notification 743 | 744 | private DispatcherTimer _todoScheduler; 745 | private TodoItem _currentTodoItem; 746 | 747 | private void InitializeTodoFeature() 748 | { 749 | _settings = AppSettings.Load(); // 确保是最新的设置 750 | 751 | if (_todoScheduler == null) 752 | { 753 | _todoScheduler = new DispatcherTimer(); 754 | _todoScheduler.Interval = TimeSpan.FromSeconds(15); // 每15秒检查一次 755 | _todoScheduler.Tick += TodoScheduler_Tick; 756 | } 757 | 758 | if (_settings.TodoEnabled) 759 | { 760 | if (!_todoScheduler.IsEnabled) _todoScheduler.Start(); 761 | } 762 | else 763 | { 764 | _todoScheduler.Stop(); 765 | } 766 | } 767 | 768 | private void TodoScheduler_Tick(object sender, EventArgs e) 769 | { 770 | if (!_settings.TodoEnabled || _settings.TodoList == null) return; 771 | 772 | var now = DateTime.Now; 773 | 774 | foreach (var item in _settings.TodoList) 775 | { 776 | if (!item.IsCompleted && item.ReminderTime <= now) 777 | { 778 | // 找到了! 779 | _currentTodoItem = item; 780 | ShowTodoNotification(item); 781 | 782 | if (_isNotificationActive && TxtTodoMessage.Text == item.Content) return; 783 | 784 | break; // 一次只显示一个 785 | } 786 | } 787 | } 788 | 789 | private void ShowTodoNotification(TodoItem item) 790 | { 791 | Dispatcher.Invoke(() => 792 | { 793 | _isNotificationActive = true; 794 | _notificationTimer.Stop(); 795 | 796 | AlbumCover.Visibility = Visibility.Collapsed; 797 | SongTitle.Visibility = Visibility.Collapsed; 798 | ControlPanel.Visibility = Visibility.Collapsed; 799 | VisualizerContainer.Visibility = Visibility.Collapsed; 800 | NotificationPanel.Visibility = Visibility.Collapsed; 801 | DrinkWaterPanel.Visibility = Visibility.Collapsed; 802 | FileStationPanel.Visibility = Visibility.Collapsed; 803 | 804 | TodoPanel.Visibility = Visibility.Visible; 805 | TodoPanel.Opacity = 0; 806 | TxtTodoMessage.Text = item.Content; 807 | 808 | DynamicIsland.IsHitTestVisible = true; 809 | SetClickThrough(false); 810 | 811 | DynamicIsland.BeginAnimation(UIElement.OpacityProperty, null); 812 | DynamicIsland.Opacity = 1.0; 813 | 814 | // 展开动画 (稍微更宽) 815 | _widthSpring.Target = 320; 816 | _heightSpring.Target = 50; 817 | 818 | // 岛屿发光脉冲 (橙色) 819 | PlayIslandGlowEffect(Colors.Orange); 820 | 821 | // 内容进场 822 | PlayContentEntranceAnimation(TodoPanel); 823 | 824 | // 图标动画 825 | PlayTodoIconAnimation(); 826 | }); 827 | } 828 | 829 | private void PlayTodoIconAnimation() 830 | { 831 | var rotateAnim = new DoubleAnimationUsingKeyFrames{ RepeatBehavior = RepeatBehavior.Forever }; 832 | rotateAnim.KeyFrames.Add(new EasingDoubleKeyFrame(0, KeyTime.FromTimeSpan(TimeSpan.Zero))); 833 | rotateAnim.KeyFrames.Add(new EasingDoubleKeyFrame(-15, KeyTime.FromTimeSpan(TimeSpan.FromMilliseconds(150))) { EasingFunction = new SineEase() }); 834 | rotateAnim.KeyFrames.Add(new EasingDoubleKeyFrame(15, KeyTime.FromTimeSpan(TimeSpan.FromMilliseconds(450))) { EasingFunction = new SineEase() }); 835 | rotateAnim.KeyFrames.Add(new EasingDoubleKeyFrame(0, KeyTime.FromTimeSpan(TimeSpan.FromMilliseconds(600))) { EasingFunction = new SineEase() }); 836 | rotateAnim.KeyFrames.Add(new EasingDoubleKeyFrame(0, KeyTime.FromTimeSpan(TimeSpan.FromMilliseconds(2000)))); 837 | 838 | TodoIconRotate?.BeginAnimation(RotateTransform.AngleProperty, rotateAnim); 839 | } 840 | 841 | private void PlayContentEntranceAnimation(FrameworkElement element) 842 | { 843 | // 确保 RenderTransform 准备就绪 844 | TranslateTransform translate = null; 845 | if (element.RenderTransform is TranslateTransform tt) 846 | { 847 | translate = tt; 848 | } 849 | else if (element.RenderTransform is TransformGroup tg) 850 | { 851 | foreach(var t in tg.Children) if(t is TranslateTransform) translate = t as TranslateTransform; 852 | } 853 | 854 | if (translate == null) 855 | { 856 | translate = new TranslateTransform(); 857 | element.RenderTransform = translate; 858 | } 859 | 860 | // 重置状态 861 | translate.Y = 20; 862 | element.Opacity = 0; 863 | 864 | // 淡入 865 | var fadeIn = new DoubleAnimation(1, TimeSpan.FromMilliseconds(300)) 866 | { 867 | BeginTime = TimeSpan.FromMilliseconds(150), 868 | EasingFunction = new CubicEase { EasingMode = EasingMode.EaseOut } 869 | }; 870 | 871 | // 上浮 872 | var slideUp = new DoubleAnimation(0, TimeSpan.FromMilliseconds(400)) 873 | { 874 | BeginTime = TimeSpan.FromMilliseconds(150), 875 | EasingFunction = new BackEase { EasingMode = EasingMode.EaseOut, Amplitude = 0.4 } 876 | }; 877 | 878 | element.BeginAnimation(UIElement.OpacityProperty, fadeIn); 879 | translate.BeginAnimation(TranslateTransform.YProperty, slideUp); 880 | } 881 | 882 | private void PlayIslandGlowEffect(Color glowColor) 883 | { 884 | if (DynamicIsland.Effect is DropShadowEffect shadow) 885 | { 886 | var colorAnim = new ColorAnimation(glowColor, TimeSpan.FromMilliseconds(300)) 887 | { 888 | AutoReverse = true, 889 | RepeatBehavior = new RepeatBehavior(2), 890 | FillBehavior = FillBehavior.Stop 891 | }; 892 | 893 | // 稍微扩散一下 Shadow 894 | var blurAnim = new DoubleAnimation(shadow.BlurRadius, 30, TimeSpan.FromMilliseconds(300)) 895 | { 896 | AutoReverse = true, 897 | RepeatBehavior = new RepeatBehavior(2), 898 | FillBehavior = FillBehavior.Stop 899 | }; 900 | 901 | shadow.BeginAnimation(DropShadowEffect.ColorProperty, colorAnim); 902 | shadow.BeginAnimation(DropShadowEffect.BlurRadiusProperty, blurAnim); 903 | } 904 | } 905 | 906 | private void BtnTodoDone_Click(object sender, RoutedEventArgs e) 907 | { 908 | if (_currentTodoItem != null) 909 | { 910 | _currentTodoItem.IsCompleted = true; 911 | _settings.Save(); 912 | _currentTodoItem = null; 913 | } 914 | 915 | HideNotification(); 916 | } 917 | 918 | #endregion 919 | 920 | #region 1. 按钮控制逻辑 921 | 922 | private async void BtnPrev_Click(object sender, RoutedEventArgs e) 923 | { 924 | try { if (_currentSession != null) await _currentSession.TrySkipPreviousAsync(); } catch { CheckCurrentSession(); } 925 | } 926 | 927 | private async void BtnPlayPause_Click(object sender, RoutedEventArgs e) 928 | { 929 | try { if (_currentSession != null) await _currentSession.TryTogglePlayPauseAsync(); } catch { CheckCurrentSession(); } 930 | } 931 | 932 | private async void BtnNext_Click(object sender, RoutedEventArgs e) 933 | { 934 | try { if (_currentSession != null) await _currentSession.TrySkipNextAsync(); } catch { CheckCurrentSession(); } 935 | } 936 | 937 | #endregion 938 | 939 | #region 3. 媒体信息 940 | private async void InitializeMediaListener() 941 | { 942 | try 943 | { 944 | _mediaManager = await GlobalSystemMediaTransportControlsSessionManager.RequestAsync(); 945 | _mediaManager.CurrentSessionChanged += (s, e) => CheckCurrentSession(); 946 | CheckCurrentSession(); 947 | } 948 | catch { } 949 | } 950 | 951 | private void CheckCurrentSession() 952 | { 953 | // 如果正在显示通知,不要打断它,等通知结束后会自动调用此方法 954 | if (_isNotificationActive) return; 955 | 956 | // 如果正在进行文件拖放或有文件存储,不要打断 957 | if (_isFileStationActive) return; 958 | 959 | // 如果文件中转站有文件,优先显示中转站 960 | if (_storedFiles.Count > 0) 961 | { 962 | ShowFileStationState(); 963 | return; 964 | } 965 | 966 | try 967 | { 968 | var session = _mediaManager.GetCurrentSession(); 969 | if (session != null) 970 | { 971 | _currentSession = session; 972 | _currentSession.MediaPropertiesChanged += async (s, e) => await UpdateMediaInfo(s); 973 | _currentSession.PlaybackInfoChanged += (s, e) => UpdatePlaybackStatus(s); 974 | var t = UpdateMediaInfo(_currentSession); 975 | UpdatePlaybackStatus(_currentSession); 976 | } 977 | else 978 | { 979 | EnterStandbyMode(); 980 | } 981 | } 982 | catch 983 | { 984 | EnterStandbyMode(); 985 | } 986 | } 987 | 988 | private void EnterStandbyMode() 989 | { 990 | _currentSession = null; 991 | _widthSpring.Target = 120; 992 | _heightSpring.Target = 35; 993 | 994 | Dispatcher.Invoke(() => 995 | { 996 | ControlPanel.Visibility = Visibility.Collapsed; 997 | VisualizerContainer.Visibility = Visibility.Collapsed; 998 | AlbumCover.Visibility = Visibility.Collapsed; 999 | SongTitle.Visibility = Visibility.Visible; // 确保控件存在 1000 | SongTitle.Text = ""; 1001 | NotificationPanel.Visibility = Visibility.Collapsed; 1002 | DrinkWaterPanel.Visibility = Visibility.Collapsed; 1003 | TodoPanel.Visibility = Visibility.Collapsed; 1004 | FileStationPanel.Visibility = Visibility.Collapsed; 1005 | 1006 | // 启用交互以支持文件拖放 1007 | DynamicIsland.IsHitTestVisible = true; 1008 | SetClickThrough(false); 1009 | 1010 | // 清除可能存在的动画锁定,确保透明度设置生效 1011 | DynamicIsland.BeginAnimation(UIElement.OpacityProperty, null); 1012 | DynamicIsland.Opacity = 0.4; // 待机透明度 1013 | }); 1014 | 1015 | } 1016 | 1017 | private async Task UpdateMediaInfo(GlobalSystemMediaTransportControlsSession session) 1018 | { 1019 | if (_isNotificationActive) return; 1020 | 1021 | try 1022 | { 1023 | var info = await session.TryGetMediaPropertiesAsync(); 1024 | Dispatcher.Invoke(() => 1025 | { 1026 | if (info != null) 1027 | { 1028 | var oldTargetW = _widthSpring.Target; 1029 | _widthSpring.Target = 400; 1030 | _heightSpring.Target = 60; 1031 | 1032 | // 如果尺寸发生变化,重置最后帧时间以获得平滑过渡 1033 | if(Math.Abs(oldTargetW - 400) > 1) _lastFrameTime = DateTime.Now; 1034 | 1035 | SongTitle.Visibility = Visibility.Visible; 1036 | SongTitle.Text = info.Title; 1037 | 1038 | AlbumCover.Visibility = Visibility.Visible; 1039 | VisualizerContainer.Visibility = Visibility.Visible; 1040 | ControlPanel.Visibility = Visibility.Visible; 1041 | NotificationPanel.Visibility = Visibility.Collapsed; 1042 | DrinkWaterPanel.Visibility = Visibility.Collapsed; 1043 | TodoPanel.Visibility = Visibility.Collapsed; 1044 | FileStationPanel.Visibility = Visibility.Collapsed; 1045 | 1046 | DynamicIsland.IsHitTestVisible = true; // 允许鼠标交互 1047 | SetClickThrough(false); // 媒体播放时允许交互 1048 | 1049 | // 清除动画锁定 1050 | DynamicIsland.BeginAnimation(UIElement.OpacityProperty, null); 1051 | DynamicIsland.Opacity = 1.0; 1052 | 1053 | if (info.Thumbnail != null) LoadThumbnail(info.Thumbnail); 1054 | } 1055 | }); 1056 | } 1057 | catch { CheckCurrentSession(); } 1058 | } 1059 | #endregion 1060 | 1061 | private void UpdatePlaybackStatus(GlobalSystemMediaTransportControlsSession session) 1062 | { 1063 | if (_isNotificationActive) return; 1064 | try 1065 | { 1066 | var info = session.GetPlaybackInfo(); 1067 | if (info == null) return; 1068 | 1069 | Dispatcher.Invoke(() => 1070 | { 1071 | if (info.PlaybackStatus == GlobalSystemMediaTransportControlsSessionPlaybackStatus.Playing) 1072 | { 1073 | IconPlay.Visibility = Visibility.Collapsed; 1074 | IconPause.Visibility = Visibility.Visible; 1075 | } 1076 | else 1077 | { 1078 | IconPlay.Visibility = Visibility.Visible; 1079 | IconPause.Visibility = Visibility.Collapsed; 1080 | } 1081 | }); 1082 | } 1083 | catch { } 1084 | } 1085 | 1086 | #region 4. 物理与渲染 1087 | private void InitializePhysics() 1088 | { 1089 | _widthSpring = new Spring(120); 1090 | _heightSpring = new Spring(35); 1091 | _lastFrameTime = DateTime.Now; 1092 | CompositionTarget.Rendering += OnRendering; 1093 | } 1094 | 1095 | private void OnRendering(object sender, EventArgs e) 1096 | { 1097 | if (DynamicIsland == null) return; 1098 | 1099 | var now = DateTime.Now; 1100 | var dt = (now - _lastFrameTime).TotalSeconds; 1101 | _lastFrameTime = now; 1102 | if (dt > 0.05) dt = 0.05; 1103 | 1104 | var newWidth = _widthSpring.Update(dt); 1105 | var newHeight = _heightSpring.Update(dt); 1106 | 1107 | DynamicIsland.Width = Math.Max(1, newWidth); 1108 | DynamicIsland.Height = Math.Max(1, newHeight); 1109 | 1110 | if (DynamicIsland.Height > 0) 1111 | DynamicIsland.CornerRadius = new CornerRadius(DynamicIsland.Height / 2); 1112 | 1113 | if (Bar1 != null && VisualizerContainer.Visibility == Visibility.Visible) 1114 | UpdateVisualizer(); 1115 | } 1116 | 1117 | private void InitializeAudioCapture() 1118 | { 1119 | try { _capture = new WasapiLoopbackCapture(); _capture.DataAvailable += OnAudioDataAvailable; _capture.StartRecording(); } catch { } 1120 | } 1121 | private void OnAudioDataAvailable(object sender, WaveInEventArgs e) 1122 | { 1123 | float max = 0; 1124 | for (int i = 0; i < e.BytesRecorded; i += 8) 1125 | { 1126 | short sample = BitConverter.ToInt16(e.Buffer, i); 1127 | var normalized = Math.Abs(sample / 32768f); 1128 | if (normalized > max) max = normalized; 1129 | } 1130 | _currentVolume = max; 1131 | } 1132 | private void UpdateVisualizer() 1133 | { 1134 | var time = DateTime.Now.TimeOfDay.TotalSeconds; 1135 | double baseH = 4 + (_currentVolume * 40); 1136 | 1137 | double h3 = Math.Max(4, baseH * (0.9 + 0.3 * Math.Sin(time * 20))); 1138 | double h2 = Math.Max(4, baseH * (0.7 + 0.25 * Math.Cos(time * 18 + 1))); 1139 | double h4 = Math.Max(4, baseH * (0.7 + 0.25 * Math.Cos(time * 16 + 2))); 1140 | double h1 = Math.Max(4, baseH * (0.5 + 0.2 * Math.Sin(time * 14 + 3))); 1141 | double h5 = Math.Max(4, baseH * (0.5 + 0.2 * Math.Sin(time * 12 + 4))); 1142 | 1143 | Bar1.Height = h1; 1144 | Bar2.Height = h2; 1145 | Bar3.Height = h3; 1146 | Bar4.Height = h4; 1147 | Bar5.Height = h5; 1148 | } 1149 | private async void LoadThumbnail(IRandomAccessStreamReference thumbnail) 1150 | { 1151 | try { var s = await thumbnail.OpenReadAsync(); var b = new BitmapImage(); b.BeginInit(); b.StreamSource = s.AsStream(); b.CacheOption = BitmapCacheOption.OnLoad; b.EndInit(); AlbumCover.Source = b; } catch { } 1152 | } 1153 | private void DynamicIsland_MouseDown(object sender, System.Windows.Input.MouseButtonEventArgs e) 1154 | { 1155 | // 只有按住 Ctrl 键时才能拖动 1156 | if (e.LeftButton == System.Windows.Input.MouseButtonState.Pressed && 1157 | System.Windows.Input.Keyboard.Modifiers == System.Windows.Input.ModifierKeys.Control) 1158 | { 1159 | DragMove(); 1160 | } 1161 | } 1162 | private void DynamicIsland_MouseEnter(object sender, System.Windows.Input.MouseEventArgs e) 1163 | { 1164 | // 鼠标悬停时保持不透明,防止误触导致看不清 1165 | } 1166 | 1167 | private void DynamicIsland_MouseLeave(object sender, System.Windows.Input.MouseEventArgs e) 1168 | { 1169 | // 鼠标离开不做特殊处理,交由状态机控制 1170 | } 1171 | 1172 | #endregion 1173 | 1174 | #region File Station (Black Hole) 1175 | 1176 | private void DynamicIsland_DragEnter(object sender, System.Windows.DragEventArgs e) 1177 | { 1178 | if (e.Data.GetDataPresent(System.Windows.DataFormats.FileDrop)) 1179 | { 1180 | e.Effects = System.Windows.DragDropEffects.Copy; 1181 | _isFileStationActive = true; 1182 | EnterFileStationMode(dragging: true); 1183 | } 1184 | else 1185 | { 1186 | e.Effects = System.Windows.DragDropEffects.None; 1187 | } 1188 | e.Handled = true; 1189 | } 1190 | 1191 | private void DynamicIsland_DragOver(object sender, System.Windows.DragEventArgs e) 1192 | { 1193 | if (e.Data.GetDataPresent(System.Windows.DataFormats.FileDrop)) 1194 | { 1195 | e.Effects = System.Windows.DragDropEffects.Copy; 1196 | } 1197 | e.Handled = true; 1198 | } 1199 | 1200 | private void DynamicIsland_DragLeave(object sender, System.Windows.DragEventArgs e) 1201 | { 1202 | // 如果没有真正存入文件就离开了,恢复原状 1203 | if (_storedFiles.Count == 0 && !IsMouseOver) 1204 | { 1205 | _isFileStationActive = false; 1206 | CheckCurrentSession(); // 恢复媒体或待机 1207 | } 1208 | else if (_storedFiles.Count > 0) 1209 | { 1210 | // 如果有文件,恢复到紧凑的“由文件”状态 1211 | EnterFileStationMode(dragging: false); 1212 | } 1213 | } 1214 | 1215 | private void DynamicIsland_Drop(object sender, System.Windows.DragEventArgs e) 1216 | { 1217 | if (e.Data.GetDataPresent(System.Windows.DataFormats.FileDrop)) 1218 | { 1219 | var files = (string[])e.Data.GetData(System.Windows.DataFormats.FileDrop); 1220 | if (files != null && files.Length > 0) 1221 | { 1222 | _storedFiles.AddRange(files); 1223 | UpdateFileStationUI(); 1224 | 1225 | // 播放吸入动画序列 1226 | PlaySuckInSequence(); 1227 | } 1228 | } 1229 | // _isFileStationActive = (_storedFiles.Count > 0); // Moved to inside sequence 1230 | } 1231 | 1232 | private void PlaySuckInSequence() 1233 | { 1234 | // 1. 加速旋转并收缩 (吞噬效果) 1235 | var consumeAnim = new DoubleAnimation(0, TimeSpan.FromMilliseconds(300)) 1236 | { 1237 | EasingFunction = new BackEase { EasingMode = EasingMode.EaseIn, Amplitude = 0.5 } 1238 | }; 1239 | 1240 | // 2. 岛屿伴随黑洞震颤 1241 | PlayIslandGlowEffect(Colors.Purple); 1242 | 1243 | consumeAnim.Completed += (s, ev) => 1244 | { 1245 | _isFileStationActive = (_storedFiles.Count > 0); 1246 | EnterFileStationMode(dragging: false); 1247 | }; 1248 | 1249 | BlackHoleScale.BeginAnimation(ScaleTransform.ScaleXProperty, consumeAnim); 1250 | BlackHoleScale.BeginAnimation(ScaleTransform.ScaleYProperty, consumeAnim); 1251 | } 1252 | 1253 | private void EnterFileStationMode(bool dragging) 1254 | { 1255 | // 隐藏其他面板 1256 | AlbumCover.Visibility = Visibility.Collapsed; 1257 | SongTitle.Visibility = Visibility.Collapsed; 1258 | ControlPanel.Visibility = Visibility.Collapsed; 1259 | VisualizerContainer.Visibility = Visibility.Collapsed; 1260 | NotificationPanel.Visibility = Visibility.Collapsed; 1261 | DrinkWaterPanel.Visibility = Visibility.Collapsed; 1262 | TodoPanel.Visibility = Visibility.Collapsed; 1263 | 1264 | // 显示中转站 1265 | FileStationPanel.Visibility = Visibility.Visible; 1266 | DynamicIsland.Opacity = 1.0; 1267 | SetClickThrough(false); 1268 | 1269 | if (dragging) 1270 | { 1271 | // 拖拽进入时:黑洞张开 1272 | _widthSpring.Target = 150; 1273 | _heightSpring.Target = 150; 1274 | 1275 | DropHintText.Opacity = 1; 1276 | FileStackDisplay.Visibility = Visibility.Collapsed; 1277 | 1278 | // 黑洞扩张动画 1279 | var scaleAnim = new DoubleAnimation(0, 1, TimeSpan.FromMilliseconds(400)) { EasingFunction = new BackEase { EasingMode = EasingMode.EaseOut } }; 1280 | BlackHoleScale.BeginAnimation(ScaleTransform.ScaleXProperty, scaleAnim); 1281 | BlackHoleScale.BeginAnimation(ScaleTransform.ScaleYProperty, scaleAnim); 1282 | 1283 | // 漩涡旋转 1284 | var spinAnim = new DoubleAnimation(0, 360, TimeSpan.FromSeconds(2)) { RepeatBehavior = RepeatBehavior.Forever }; 1285 | VortexRotation.BeginAnimation(RotateTransform.AngleProperty, spinAnim); 1286 | } 1287 | else 1288 | { 1289 | // 存储状态:紧凑显示 1290 | ShowFileStationState(); 1291 | } 1292 | } 1293 | 1294 | private void ShowFileStationState() 1295 | { 1296 | // 隐藏其他面板确保安全 1297 | AlbumCover.Visibility = Visibility.Collapsed; 1298 | SongTitle.Visibility = Visibility.Collapsed; 1299 | ControlPanel.Visibility = Visibility.Collapsed; 1300 | VisualizerContainer.Visibility = Visibility.Collapsed; 1301 | NotificationPanel.Visibility = Visibility.Collapsed; 1302 | DrinkWaterPanel.Visibility = Visibility.Collapsed; 1303 | TodoPanel.Visibility = Visibility.Collapsed; 1304 | 1305 | FileStationPanel.Visibility = Visibility.Visible; 1306 | DropHintText.Opacity = 0; 1307 | 1308 | // 黑洞收缩 1309 | BlackHoleScale.BeginAnimation(ScaleTransform.ScaleXProperty, null); 1310 | BlackHoleScale.BeginAnimation(ScaleTransform.ScaleYProperty, null); 1311 | BlackHoleScale.ScaleX = 0; 1312 | BlackHoleScale.ScaleY = 0; 1313 | 1314 | // 显示文件堆栈 1315 | FileStackDisplay.Visibility = Visibility.Visible; 1316 | UpdateFileStationUI(); 1317 | 1318 | _widthSpring.Target = 100; // 紧凑宽度 1319 | _heightSpring.Target = 35; // 标准高度 1320 | DynamicIsland.Opacity = 1.0; 1321 | SetClickThrough(false); 1322 | } 1323 | 1324 | private void UpdateFileStationUI() 1325 | { 1326 | FileCountText.Text = _storedFiles.Count.ToString(); 1327 | } 1328 | 1329 | private void PlayBlackHoleSuckAnimation() 1330 | { 1331 | // 简单的震动反馈或闪光 1332 | PlayIslandGlowEffect(Colors.Purple); 1333 | } 1334 | 1335 | private void FileStack_MouseDown(object sender, System.Windows.Input.MouseButtonEventArgs e) 1336 | { 1337 | if (_storedFiles.Count > 0) 1338 | { 1339 | // 开始拖出操作 1340 | var data = new System.Windows.DataObject(System.Windows.DataFormats.FileDrop, _storedFiles.ToArray()); 1341 | System.Windows.DragDrop.DoDragDrop(FileStackDisplay, data, System.Windows.DragDropEffects.Copy | System.Windows.DragDropEffects.Move); 1342 | 1343 | // 拖拽完成后清空 (假设用户拖走就是拿走了) 1344 | // 注意:DoDragDrop 是阻塞的,直到拖拽结束 1345 | _storedFiles.Clear(); 1346 | _isFileStationActive = false; 1347 | 1348 | // 恢复正常状态 1349 | CheckCurrentSession(); 1350 | } 1351 | } 1352 | 1353 | #endregion 1354 | 1355 | } 1356 | } --------------------------------------------------------------------------------