messageflight 0.2.4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
message_flight/i18n.py ADDED
@@ -0,0 +1,370 @@
1
+ """Lightweight UI translation helpers."""
2
+ from __future__ import annotations
3
+
4
+ import locale
5
+ from typing import Optional
6
+
7
+ LANGUAGES = ("zh", "en", "ja", "ko", "id", "th", "vi", "ms")
8
+ _LANGUAGE_NAMES = {
9
+ "zh": "简体中文",
10
+ "en": "English",
11
+ "ja": "日本語",
12
+ "ko": "한국어",
13
+ "id": "Bahasa Indonesia",
14
+ "th": "ไทย",
15
+ "vi": "Tiếng Việt",
16
+ "ms": "Bahasa Melayu",
17
+ }
18
+
19
+ _TRANSLATIONS = {
20
+ "zh": {
21
+ "tray.show": "显示飞机",
22
+ "tray.pause": "暂停飞行",
23
+ "tray.resume": "继续飞行",
24
+ "tray.demo": "发送演示通知",
25
+ "tray.dnd": "免打扰",
26
+ "tray.settings": "设置...",
27
+ "tray.preset_editor": "飞船编辑器",
28
+ "tray.notification_status.checking": "通知权限: 检查中...",
29
+ "tray.notification_status.unavailable": "通知权限: winsdk 未安装",
30
+ "tray.notification_status": "通知权限: {label} ({status})",
31
+ "status.unspecified": "未指定",
32
+ "status.allowed": "已允许",
33
+ "status.denied": "已拒绝",
34
+ "status.unknown": "未知",
35
+ "tray.autostart": "开机自启",
36
+ "tray.quit": "退出",
37
+ "settings.title": "MessageFlight 设置",
38
+ "settings.language": "语言:",
39
+ "settings.flight_mode": "飞行模式:",
40
+ "settings.fly_path": "飞行路径:",
41
+ "settings.color_scheme": "配色:",
42
+ "settings.tts_engine": "TTS 引擎:",
43
+ "settings.minimax_key": "MiniMax 订阅 Key:",
44
+ "settings.minimax_key_placeholder": "Token Plan 订阅 Key",
45
+ "settings.color.plane_color": "飞机主体",
46
+ "settings.color.wing_color": "机翼",
47
+ "settings.color.accent_color": "眼睛/高光",
48
+ "settings.color.decor_color": "小圆装饰",
49
+ "settings.color.banner_color": "横幅背景",
50
+ "settings.color.text_color": "横幅文字",
51
+ "settings.color.thruster_outer_color": "推进器外焰",
52
+ "settings.color.thruster_middle_color": "推进器中焰",
53
+ "settings.color.thruster_inner_color": "推进器内焰",
54
+ "settings.preset.default": "默认粉",
55
+ "settings.preset.retro": "复古绿",
56
+ "settings.preset.cyber": "未来赛博",
57
+ "preset_editor.title": "飞船编辑器",
58
+ "preset_editor.preset": "预设:",
59
+ "preset_editor.choose_color": "选择 {label}",
60
+ },
61
+ "en": {
62
+ "tray.show": "Show plane",
63
+ "tray.pause": "Pause flight",
64
+ "tray.resume": "Resume flight",
65
+ "tray.demo": "Send demo notification",
66
+ "tray.dnd": "Do Not Disturb",
67
+ "tray.settings": "Settings...",
68
+ "tray.preset_editor": "Ship editor",
69
+ "tray.notification_status.checking": "Notification permission: checking...",
70
+ "tray.notification_status.unavailable": "Notification permission: winsdk not installed",
71
+ "tray.notification_status": "Notification permission: {label} ({status})",
72
+ "status.unspecified": "unspecified",
73
+ "status.allowed": "allowed",
74
+ "status.denied": "denied",
75
+ "status.unknown": "unknown",
76
+ "tray.autostart": "Start with Windows",
77
+ "tray.quit": "Quit",
78
+ "settings.title": "MessageFlight Settings",
79
+ "settings.language": "Language:",
80
+ "settings.flight_mode": "Flight mode:",
81
+ "settings.fly_path": "Flight path:",
82
+ "settings.color_scheme": "Color scheme:",
83
+ "settings.tts_engine": "TTS engine:",
84
+ "settings.minimax_key": "MiniMax subscription key:",
85
+ "settings.minimax_key_placeholder": "Token Plan subscription key",
86
+ "settings.color.plane_color": "Plane body",
87
+ "settings.color.wing_color": "Wing",
88
+ "settings.color.accent_color": "Eyes / highlight",
89
+ "settings.color.decor_color": "Decor dots",
90
+ "settings.color.banner_color": "Banner background",
91
+ "settings.color.text_color": "Banner text",
92
+ "settings.color.thruster_outer_color": "Outer thruster flame",
93
+ "settings.color.thruster_middle_color": "Middle thruster flame",
94
+ "settings.color.thruster_inner_color": "Inner thruster flame",
95
+ "settings.preset.default": "Default pink",
96
+ "settings.preset.retro": "Retro green",
97
+ "settings.preset.cyber": "Cyber future",
98
+ "preset_editor.title": "Ship editor",
99
+ "preset_editor.preset": "Preset:",
100
+ "preset_editor.choose_color": "Choose {label}",
101
+ },
102
+ "ja": {
103
+ "tray.show": "飛行機を表示",
104
+ "tray.pause": "飛行を一時停止",
105
+ "tray.resume": "飛行を再開",
106
+ "tray.demo": "デモ通知を送信",
107
+ "tray.dnd": "通知をオフ",
108
+ "tray.settings": "設定...",
109
+ "tray.preset_editor": "機体エディター",
110
+ "tray.notification_status.checking": "通知権限: 確認中...",
111
+ "tray.notification_status.unavailable": "通知権限: winsdk 未インストール",
112
+ "tray.notification_status": "通知権限: {label} ({status})",
113
+ "status.unspecified": "未指定",
114
+ "status.allowed": "許可済み",
115
+ "status.denied": "拒否済み",
116
+ "status.unknown": "不明",
117
+ "tray.autostart": "Windows 起動時に開始",
118
+ "tray.quit": "終了",
119
+ "settings.title": "MessageFlight 設定",
120
+ "settings.language": "言語:",
121
+ "settings.flight_mode": "飛行モード:",
122
+ "settings.fly_path": "飛行ルート:",
123
+ "settings.color_scheme": "配色:",
124
+ "settings.tts_engine": "TTS エンジン:",
125
+ "settings.minimax_key": "MiniMax サブスクリプションキー:",
126
+ "settings.minimax_key_placeholder": "Token Plan サブスクリプションキー",
127
+ "settings.color.plane_color": "機体",
128
+ "settings.color.wing_color": "翼",
129
+ "settings.color.accent_color": "目 / ハイライト",
130
+ "settings.color.decor_color": "装飾ドット",
131
+ "settings.color.banner_color": "バナー背景",
132
+ "settings.color.text_color": "バナーテキスト",
133
+ "settings.color.thruster_outer_color": "外側スラスター炎",
134
+ "settings.color.thruster_middle_color": "中央スラスター炎",
135
+ "settings.color.thruster_inner_color": "内側スラスター炎",
136
+ "settings.preset.default": "デフォルトピンク",
137
+ "settings.preset.retro": "レトログリーン",
138
+ "settings.preset.cyber": "サイバー未来",
139
+ "preset_editor.title": "機体エディター",
140
+ "preset_editor.preset": "プリセット:",
141
+ "preset_editor.choose_color": "{label} を選択",
142
+ },
143
+ "ko": {
144
+ "tray.show": "비행기 표시",
145
+ "tray.pause": "비행 일시정지",
146
+ "tray.resume": "비행 계속",
147
+ "tray.demo": "데모 알림 보내기",
148
+ "tray.dnd": "방해 금지",
149
+ "tray.settings": "설정...",
150
+ "tray.preset_editor": "비행체 편집기",
151
+ "tray.notification_status.checking": "알림 권한: 확인 중...",
152
+ "tray.notification_status.unavailable": "알림 권한: winsdk 미설치",
153
+ "tray.notification_status": "알림 권한: {label} ({status})",
154
+ "status.unspecified": "미지정",
155
+ "status.allowed": "허용됨",
156
+ "status.denied": "거부됨",
157
+ "status.unknown": "알 수 없음",
158
+ "tray.autostart": "Windows 시작 시 실행",
159
+ "tray.quit": "종료",
160
+ "settings.title": "MessageFlight 설정",
161
+ "settings.language": "언어:",
162
+ "settings.flight_mode": "비행 모드:",
163
+ "settings.fly_path": "비행 경로:",
164
+ "settings.color_scheme": "색상:",
165
+ "settings.tts_engine": "TTS 엔진:",
166
+ "settings.minimax_key": "MiniMax 구독 키:",
167
+ "settings.minimax_key_placeholder": "Token Plan 구독 키",
168
+ "settings.color.plane_color": "기체 본체",
169
+ "settings.color.wing_color": "날개",
170
+ "settings.color.accent_color": "눈 / 하이라이트",
171
+ "settings.color.decor_color": "장식 점",
172
+ "settings.color.banner_color": "배너 배경",
173
+ "settings.color.text_color": "배너 텍스트",
174
+ "settings.color.thruster_outer_color": "외부 추진 화염",
175
+ "settings.color.thruster_middle_color": "중간 추진 화염",
176
+ "settings.color.thruster_inner_color": "내부 추진 화염",
177
+ "settings.preset.default": "기본 핑크",
178
+ "settings.preset.retro": "레트로 그린",
179
+ "settings.preset.cyber": "사이버 퓨처",
180
+ "preset_editor.title": "비행체 편집기",
181
+ "preset_editor.preset": "프리셋:",
182
+ "preset_editor.choose_color": "{label} 선택",
183
+ },
184
+ "id": {
185
+ "tray.show": "Tampilkan pesawat",
186
+ "tray.pause": "Jeda penerbangan",
187
+ "tray.resume": "Lanjutkan penerbangan",
188
+ "tray.demo": "Kirim notifikasi demo",
189
+ "tray.dnd": "Jangan ganggu",
190
+ "tray.settings": "Pengaturan...",
191
+ "tray.preset_editor": "Editor wahana",
192
+ "tray.notification_status.checking": "Izin notifikasi: memeriksa...",
193
+ "tray.notification_status.unavailable": "Izin notifikasi: winsdk belum terpasang",
194
+ "tray.notification_status": "Izin notifikasi: {label} ({status})",
195
+ "status.unspecified": "belum ditentukan",
196
+ "status.allowed": "diizinkan",
197
+ "status.denied": "ditolak",
198
+ "status.unknown": "tidak diketahui",
199
+ "tray.autostart": "Mulai bersama Windows",
200
+ "tray.quit": "Keluar",
201
+ "settings.title": "Pengaturan MessageFlight",
202
+ "settings.language": "Bahasa:",
203
+ "settings.flight_mode": "Mode terbang:",
204
+ "settings.fly_path": "Jalur terbang:",
205
+ "settings.color_scheme": "Skema warna:",
206
+ "settings.tts_engine": "Mesin TTS:",
207
+ "settings.minimax_key": "Kunci langganan MiniMax:",
208
+ "settings.minimax_key_placeholder": "Kunci langganan Token Plan",
209
+ "settings.color.plane_color": "Badan pesawat",
210
+ "settings.color.wing_color": "Sayap",
211
+ "settings.color.accent_color": "Mata / sorotan",
212
+ "settings.color.decor_color": "Titik dekorasi",
213
+ "settings.color.banner_color": "Latar banner",
214
+ "settings.color.text_color": "Teks banner",
215
+ "settings.color.thruster_outer_color": "Api pendorong luar",
216
+ "settings.color.thruster_middle_color": "Api pendorong tengah",
217
+ "settings.color.thruster_inner_color": "Api pendorong dalam",
218
+ "settings.preset.default": "Pink bawaan",
219
+ "settings.preset.retro": "Hijau retro",
220
+ "settings.preset.cyber": "Masa depan siber",
221
+ "preset_editor.title": "Editor wahana",
222
+ "preset_editor.preset": "Preset:",
223
+ "preset_editor.choose_color": "Pilih {label}",
224
+ },
225
+ "th": {
226
+ "tray.show": "แสดงเครื่องบิน",
227
+ "tray.pause": "หยุดบินชั่วคราว",
228
+ "tray.resume": "บินต่อ",
229
+ "tray.demo": "ส่งการแจ้งเตือนตัวอย่าง",
230
+ "tray.dnd": "ห้ามรบกวน",
231
+ "tray.settings": "การตั้งค่า...",
232
+ "tray.preset_editor": "ตัวแก้ไขยานบิน",
233
+ "tray.notification_status.checking": "สิทธิ์การแจ้งเตือน: กำลังตรวจสอบ...",
234
+ "tray.notification_status.unavailable": "สิทธิ์การแจ้งเตือน: ยังไม่ได้ติดตั้ง winsdk",
235
+ "tray.notification_status": "สิทธิ์การแจ้งเตือน: {label} ({status})",
236
+ "status.unspecified": "ยังไม่ระบุ",
237
+ "status.allowed": "อนุญาตแล้ว",
238
+ "status.denied": "ถูกปฏิเสธ",
239
+ "status.unknown": "ไม่ทราบ",
240
+ "tray.autostart": "เริ่มพร้อม Windows",
241
+ "tray.quit": "ออก",
242
+ "settings.title": "การตั้งค่า MessageFlight",
243
+ "settings.language": "ภาษา:",
244
+ "settings.flight_mode": "โหมดการบิน:",
245
+ "settings.fly_path": "เส้นทางบิน:",
246
+ "settings.color_scheme": "ชุดสี:",
247
+ "settings.tts_engine": "เอนจิน TTS:",
248
+ "settings.minimax_key": "คีย์สมัครสมาชิก MiniMax:",
249
+ "settings.minimax_key_placeholder": "คีย์สมัครสมาชิก Token Plan",
250
+ "settings.color.plane_color": "ลำตัวเครื่องบิน",
251
+ "settings.color.wing_color": "ปีก",
252
+ "settings.color.accent_color": "ตา / ไฮไลต์",
253
+ "settings.color.decor_color": "จุดตกแต่ง",
254
+ "settings.color.banner_color": "พื้นหลังแบนเนอร์",
255
+ "settings.color.text_color": "ข้อความแบนเนอร์",
256
+ "settings.color.thruster_outer_color": "ไฟขับดันชั้นนอก",
257
+ "settings.color.thruster_middle_color": "ไฟขับดันชั้นกลาง",
258
+ "settings.color.thruster_inner_color": "ไฟขับดันชั้นใน",
259
+ "settings.preset.default": "ชมพูเริ่มต้น",
260
+ "settings.preset.retro": "เขียวเรโทร",
261
+ "settings.preset.cyber": "ไซเบอร์อนาคต",
262
+ "preset_editor.title": "ตัวแก้ไขยานบิน",
263
+ "preset_editor.preset": "พรีเซ็ต:",
264
+ "preset_editor.choose_color": "เลือก {label}",
265
+ },
266
+ "vi": {
267
+ "tray.show": "Hiển thị máy bay",
268
+ "tray.pause": "Tạm dừng bay",
269
+ "tray.resume": "Tiếp tục bay",
270
+ "tray.demo": "Gửi thông báo demo",
271
+ "tray.dnd": "Không làm phiền",
272
+ "tray.settings": "Cài đặt...",
273
+ "tray.preset_editor": "Trình sửa phương tiện",
274
+ "tray.notification_status.checking": "Quyền thông báo: đang kiểm tra...",
275
+ "tray.notification_status.unavailable": "Quyền thông báo: chưa cài winsdk",
276
+ "tray.notification_status": "Quyền thông báo: {label} ({status})",
277
+ "status.unspecified": "chưa xác định",
278
+ "status.allowed": "đã cho phép",
279
+ "status.denied": "bị từ chối",
280
+ "status.unknown": "không rõ",
281
+ "tray.autostart": "Khởi động cùng Windows",
282
+ "tray.quit": "Thoát",
283
+ "settings.title": "Cài đặt MessageFlight",
284
+ "settings.language": "Ngôn ngữ:",
285
+ "settings.flight_mode": "Chế độ bay:",
286
+ "settings.fly_path": "Đường bay:",
287
+ "settings.color_scheme": "Bảng màu:",
288
+ "settings.tts_engine": "Công cụ TTS:",
289
+ "settings.minimax_key": "Khóa đăng ký MiniMax:",
290
+ "settings.minimax_key_placeholder": "Khóa đăng ký Token Plan",
291
+ "settings.color.plane_color": "Thân máy bay",
292
+ "settings.color.wing_color": "Cánh",
293
+ "settings.color.accent_color": "Mắt / điểm nhấn",
294
+ "settings.color.decor_color": "Chấm trang trí",
295
+ "settings.color.banner_color": "Nền banner",
296
+ "settings.color.text_color": "Chữ banner",
297
+ "settings.color.thruster_outer_color": "Lửa đẩy ngoài",
298
+ "settings.color.thruster_middle_color": "Lửa đẩy giữa",
299
+ "settings.color.thruster_inner_color": "Lửa đẩy trong",
300
+ "settings.preset.default": "Hồng mặc định",
301
+ "settings.preset.retro": "Xanh retro",
302
+ "settings.preset.cyber": "Tương lai cyber",
303
+ "preset_editor.title": "Trình sửa phương tiện",
304
+ "preset_editor.preset": "Preset:",
305
+ "preset_editor.choose_color": "Chọn {label}",
306
+ },
307
+ "ms": {
308
+ "tray.show": "Tunjukkan pesawat",
309
+ "tray.pause": "Jeda penerbangan",
310
+ "tray.resume": "Sambung penerbangan",
311
+ "tray.demo": "Hantar notifikasi demo",
312
+ "tray.dnd": "Jangan ganggu",
313
+ "tray.settings": "Tetapan...",
314
+ "tray.preset_editor": "Editor kenderaan",
315
+ "tray.notification_status.checking": "Kebenaran notifikasi: menyemak...",
316
+ "tray.notification_status.unavailable": "Kebenaran notifikasi: winsdk belum dipasang",
317
+ "tray.notification_status": "Kebenaran notifikasi: {label} ({status})",
318
+ "status.unspecified": "belum ditentukan",
319
+ "status.allowed": "dibenarkan",
320
+ "status.denied": "ditolak",
321
+ "status.unknown": "tidak diketahui",
322
+ "tray.autostart": "Mula bersama Windows",
323
+ "tray.quit": "Keluar",
324
+ "settings.title": "Tetapan MessageFlight",
325
+ "settings.language": "Bahasa:",
326
+ "settings.flight_mode": "Mod penerbangan:",
327
+ "settings.fly_path": "Laluan penerbangan:",
328
+ "settings.color_scheme": "Skema warna:",
329
+ "settings.tts_engine": "Enjin TTS:",
330
+ "settings.minimax_key": "Kunci langganan MiniMax:",
331
+ "settings.minimax_key_placeholder": "Kunci langganan Token Plan",
332
+ "settings.color.plane_color": "Badan pesawat",
333
+ "settings.color.wing_color": "Sayap",
334
+ "settings.color.accent_color": "Mata / sorotan",
335
+ "settings.color.decor_color": "Titik hiasan",
336
+ "settings.color.banner_color": "Latar banner",
337
+ "settings.color.text_color": "Teks banner",
338
+ "settings.color.thruster_outer_color": "Api pendorong luar",
339
+ "settings.color.thruster_middle_color": "Api pendorong tengah",
340
+ "settings.color.thruster_inner_color": "Api pendorong dalam",
341
+ "settings.preset.default": "Merah jambu lalai",
342
+ "settings.preset.retro": "Hijau retro",
343
+ "settings.preset.cyber": "Masa depan siber",
344
+ "preset_editor.title": "Editor kenderaan",
345
+ "preset_editor.preset": "Preset:",
346
+ "preset_editor.choose_color": "Pilih {label}",
347
+ },
348
+ }
349
+
350
+
351
+ def normalize_language(language: str) -> str:
352
+ return language if language in LANGUAGES else "zh"
353
+
354
+
355
+ def detect_system_language(locale_name: Optional[str] = None) -> str:
356
+ name = locale_name or locale.getlocale()[0] or ""
357
+ prefix = name.split("_")[0].split("-")[0].lower()
358
+ return normalize_language(prefix)
359
+
360
+
361
+ def language_name(language: str) -> str:
362
+ return _LANGUAGE_NAMES[normalize_language(language)]
363
+
364
+
365
+ def tr(key: str, language: str = "zh", **kwargs: object) -> str:
366
+ normalized = normalize_language(language)
367
+ template = _TRANSLATIONS.get(normalized, _TRANSLATIONS["zh"]).get(key)
368
+ if template is None:
369
+ template = _TRANSLATIONS["zh"].get(key, key)
370
+ return template.format(**kwargs)
@@ -0,0 +1,72 @@
1
+ """FIFO notification queue with bounded capacity.
2
+
3
+ The queue drops the oldest item when capacity is exceeded so that a
4
+ notification flood (e.g. a chat app ping-spamming the listener) cannot
5
+ grow memory unbounded. Callers should treat the queue as the single
6
+ source of truth for "what to show next" so that rapid-fire notifications
7
+ do not interrupt the in-flight animation.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ from collections import deque
12
+ from typing import Optional
13
+
14
+
15
+ class NotificationQueue:
16
+ """Bounded FIFO queue for notification display text.
17
+
18
+ Args:
19
+ max_size: Maximum number of items to retain. When ``len()`` would
20
+ exceed this value after an ``enqueue``, the oldest item is
21
+ dropped. Defaults to 20.
22
+ """
23
+
24
+ DEFAULT_MAX_SIZE = 20
25
+
26
+ def __init__(self, max_size: int = DEFAULT_MAX_SIZE) -> None:
27
+ if max_size < 1:
28
+ raise ValueError(f"max_size must be >= 1, got {max_size}")
29
+ self._max_size = int(max_size)
30
+ self._items: deque[str] = deque()
31
+
32
+ @property
33
+ def max_size(self) -> int:
34
+ return self._max_size
35
+
36
+ def enqueue(self, text: str) -> Optional[str]:
37
+ """Append *text* to the tail of the queue.
38
+
39
+ Returns the text that was dropped from the head to make room, or
40
+ ``None`` if the queue was not full. Callers can use the return
41
+ value to surface a "N messages dropped" hint to the user.
42
+ """
43
+ dropped: Optional[str] = None
44
+ if len(self._items) >= self._max_size:
45
+ dropped = self._items.popleft()
46
+ self._items.append(text)
47
+ return dropped
48
+
49
+ def dequeue(self) -> Optional[str]:
50
+ """Remove and return the head item, or ``None`` if empty."""
51
+ if not self._items:
52
+ return None
53
+ return self._items.popleft()
54
+
55
+ def peek(self) -> Optional[str]:
56
+ """Return the head item without removing it, or ``None`` if empty."""
57
+ if not self._items:
58
+ return None
59
+ return self._items[0]
60
+
61
+ def clear(self) -> None:
62
+ """Remove all items from the queue."""
63
+ self._items.clear()
64
+
65
+ def is_empty(self) -> bool:
66
+ return len(self._items) == 0
67
+
68
+ def __len__(self) -> int:
69
+ return len(self._items)
70
+
71
+ def __bool__(self) -> bool:
72
+ return bool(self._items)
@@ -0,0 +1,98 @@
1
+ """Background thread for listening to the Windows notification center."""
2
+ import asyncio
3
+ from typing import Set
4
+
5
+ from PyQt6.QtCore import QThread, pyqtSignal
6
+
7
+ try:
8
+ from winsdk.windows.ui.notifications import KnownNotificationBindings, NotificationKinds
9
+ from winsdk.windows.ui.notifications.management import UserNotificationListener
10
+ WINSOK_AVAILABLE = True
11
+ except ImportError:
12
+ WINSOK_AVAILABLE = False
13
+
14
+
15
+ class NotificationWorker(QThread):
16
+ """后台线程:轮询 Windows 通知中心,发现新通知时发射信号"""
17
+ notification_received = pyqtSignal(str, str) # app_name, message_text
18
+ access_status_changed = pyqtSignal(int) # 0=Unspecified, 1=Allowed, 2=Denied
19
+
20
+ def __init__(self) -> None:
21
+ super().__init__()
22
+ self._running = True
23
+ self._seen_ids: Set[int] = set()
24
+ self._initialized = False
25
+
26
+ def run(self):
27
+ if not WINSOK_AVAILABLE:
28
+ return
29
+
30
+ loop = asyncio.new_event_loop()
31
+ asyncio.set_event_loop(loop)
32
+ try:
33
+ while self._running:
34
+ try:
35
+ access, notifications = loop.run_until_complete(self._poll())
36
+ self.access_status_changed.emit(access)
37
+
38
+ if access == 1:
39
+ if not self._initialized:
40
+ # 第一次:把当前所有通知标记为已见过,避免历史通知刷屏
41
+ for n in notifications:
42
+ self._seen_ids.add(n['id'])
43
+ self._initialized = True
44
+ else:
45
+ for n in notifications:
46
+ if n['id'] not in self._seen_ids:
47
+ self._seen_ids.add(n['id'])
48
+ self.notification_received.emit(n['app'], n['text'])
49
+
50
+ # 定期清理旧 ID,防止无限增长
51
+ if len(self._seen_ids) > 500:
52
+ current_ids = {n['id'] for n in notifications}
53
+ self._seen_ids = current_ids | set(
54
+ list(self._seen_ids)[-200:]
55
+ )
56
+ except Exception as e:
57
+ print(f"Notification poll error: {e}")
58
+
59
+ self.msleep(2000)
60
+ finally:
61
+ loop.close()
62
+
63
+ async def _poll(self):
64
+ listener = UserNotificationListener.current
65
+ access = listener.get_access_status()
66
+ if access != 1:
67
+ return access, []
68
+
69
+ notifications = await listener.get_notifications_async(NotificationKinds.TOAST)
70
+ result = []
71
+ for n in notifications:
72
+ try:
73
+ app_name = "Unknown"
74
+ if hasattr(n, 'app_info') and n.app_info:
75
+ app_name = n.app_info.display_info.display_name
76
+
77
+ binding = n.notification.visual.get_binding(KnownNotificationBindings.toast_generic)
78
+ lines = []
79
+ if binding:
80
+ texts = binding.get_text_elements()
81
+ it = iter(texts)
82
+ while it.has_current:
83
+ lines.append(it.current.text)
84
+ it.move_next()
85
+
86
+ if lines:
87
+ result.append({
88
+ 'id': n.id,
89
+ 'app': app_name,
90
+ 'text': ' | '.join(lines)
91
+ })
92
+ except Exception:
93
+ pass
94
+ return access, result
95
+
96
+ def stop(self):
97
+ self._running = False
98
+ self.wait(1500)