├── 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 | 
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 | 
32 | * **Drag & Drop 2.0**:当你拖拽文件至屏幕顶部,灵动岛瞬间化身紫色引力黑洞。
33 | * **暂存任意文件**:松手即吸入。文件被安全托管在岛内,不再占用你的鼠标和剪贴板。
34 | * **跨应用传输**:切换到微信、PS 或邮件窗口,从岛上轻轻一拖,文件即刻释放。
35 |
36 | ### 2. 🎵 沉浸式媒体接管
37 | **让音乐不仅好听,而且好看。**
38 | 
39 | * **全局兼容**:完美支持网易云音乐、Spotify、Apple Music 等主流播放器。
40 | * **视觉律动**:内置实时音频频谱分析,灵动岛会随着重低音的节奏跳动呼吸。
41 | * **打扰更少**:切歌、暂停,一切操作均在顶部微型窗口完成,无需离开当前工作区。
42 |
43 | ### 3. ⚡ 硬件级感知与全场景通知接管
44 | **彻底取代 Windows 原生通知,更加优雅,更懂你。**
45 | 
46 | * **消息接管**:QQ、钉钉... 所有应用的消息通知都将被灵动岛统一接管。它会自动提取核心内容展示,并支持**自动清理系统原生通知**,还你一个干净的侧边栏。
47 | * **硬件感知**:AirPods 连接了?U盘拔出了?灵动岛会以优雅的 3D 翻转动画告知你,随后自动隐退。
48 |
49 | ### 4. 💻 极客组件 (Geek Tools)
50 | **专为极客打造的系统级感知能力。**
51 | * **系统仪表盘**:待机时可显示 CPU 使用率、内存负载以及实时上传/下载网速,时刻掌握电脑状态。
52 | * **通用进度投射**:自动捕获浏览器下载、文件解压等任务栏进度条,将其同步投射到灵动岛上,无需反复切换窗口查看进度。
53 |
54 | ### 5. 🧘 赛博养生与专注
55 | 
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 |
97 |
98 |
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 |
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 |
256 |
257 |
258 |
259 |
260 |
261 |
262 |
263 |
264 |
265 |
266 |
267 |
268 |
269 |
270 |
271 |
272 |
273 |
274 |
275 |
276 |
277 |
278 |
279 |
280 |
281 |
282 |
283 |
284 |
285 |
286 |
287 |
288 |
289 |
290 |
291 |
292 |
293 |
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 |
--------------------------------------------------------------------------------
/MainWindow.xaml:
--------------------------------------------------------------------------------
1 |
20 |
21 |
22 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
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 | }
--------------------------------------------------------------------------------