-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathoverlay.py
More file actions
426 lines (354 loc) · 17.8 KB
/
overlay.py
File metadata and controls
426 lines (354 loc) · 17.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
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
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
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
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
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
import tkinter as tk
from tkinter import ttk, messagebox, font
import requests
import threading
import time
import json
import os
# ------------------- 配置参数 -------------------
REFRESH_INTERVAL = 1 # 自动刷新间隔(秒)
WINDOW_ALPHA = 0.9 # 窗口透明度(0.0-1.0)
STATUS_CHANGE_DURATION = 2000 # 状态变化提示持续时间(毫秒)
ALERT_DURATION = 3000 # 地图变化提示框显示时间(毫秒)
ALERT_OFFSET = 20 # 提示框与屏幕边缘的偏移量(像素)
CONFIG_FILE = "hypixel_config.json" # 配置文件路径
class HypixelStatusGUI:
def __init__(self, root):
self.root = root
self.root.title("Hypixel 状态监控")
# 核心配置:窗口置顶 + 半透明
self.root.wm_attributes('-topmost', True)
self.root.wm_attributes('-alpha', WINDOW_ALPHA)
# 状态变量
self.hypixel_api_key = "" # API密钥
self.player_nickname = ""
self.player_uuid = ""
self.after_id = None
self.is_monitoring = False
self.is_thread_running = False
self.previous_online_status = None # 用于检测状态变化
self.previous_map = None # 用于检测地图变化
self.status_change_timer = None # 状态变化提示定时器
self.alert_window = None # 地图变化提示窗口
# 加载保存的配置
self._load_config()
# 初始化GUI
self._create_widgets()
# 让窗口自动适应内容大小
self.root.update_idletasks()
self.root.minsize(self.root.winfo_width(), self.root.winfo_height())
def _create_widgets(self):
"""创建GUI控件"""
# 全局样式调整
style = ttk.Style()
style.configure("TLabel", font=("Arial", 9))
style.configure("TEntry", font=("Arial", 9))
style.configure("TButton", font=("Arial", 9), padding=3)
style.configure("TLabelframe", font=("Arial", 10, "bold"), padding=(8, 4))
# 1. API密钥配置区域
api_frame = ttk.LabelFrame(self.root, text="API密钥配置", padding=(8, 3))
api_frame.grid(row=0, column=0, padx=15, pady=5, sticky="we")
api_frame.columnconfigure(1, weight=1)
ttk.Label(api_frame, text="密钥:").grid(row=0, column=0, padx=3, pady=5, sticky="w")
self.api_key_entry = ttk.Entry(api_frame, show="*", width=40) # 密码框显示方式
self.api_key_entry.grid(row=0, column=1, padx=3, pady=5, sticky="we")
self.api_key_entry.insert(0, self.hypixel_api_key) # 填充已保存的密钥
self.save_api_btn = ttk.Button(api_frame, text="保存密钥", command=self._save_api_key)
self.save_api_btn.grid(row=0, column=2, padx=5, pady=5)
# 2. 输入区域
input_frame = ttk.LabelFrame(self.root, text="玩家信息", padding=(8, 3))
input_frame.grid(row=1, column=0, padx=15, pady=5, sticky="we")
input_frame.columnconfigure(1, weight=1)
ttk.Label(input_frame, text="昵称:").grid(row=0, column=0, padx=3, pady=5, sticky="w")
self.nickname_entry = ttk.Entry(input_frame)
self.nickname_entry.grid(row=0, column=1, padx=3, pady=5, sticky="we")
self.nickname_entry.focus()
self.query_btn = ttk.Button(input_frame, text="开始监控", command=self.toggle_monitoring)
self.query_btn.grid(row=0, column=2, padx=5, pady=5)
# 3. 状态显示区域
status_frame = ttk.LabelFrame(self.root, text="实时状态", padding=(8, 3))
status_frame.grid(row=2, column=0, padx=15, pady=5, sticky="we")
status_frame.columnconfigure((1, 3), weight=1)
# UUID 显示
ttk.Label(status_frame, text="UUID:").grid(row=0, column=0, padx=3, pady=3, sticky="w")
self.uuid_no_dash = ttk.Label(status_frame, text="——", foreground="#666")
self.uuid_no_dash.grid(row=0, column=1, padx=3, pady=3, sticky="w")
# 在线状态(突出显示)
ttk.Label(status_frame, text="状态:").grid(row=1, column=0, padx=3, pady=3, sticky="w")
self.online_status = ttk.Label(status_frame, text="未监控", foreground="#999")
self.online_status.grid(row=1, column=1, padx=3, pady=3, sticky="w")
# 游戏信息
ttk.Label(status_frame, text="游戏:").grid(row=2, column=0, padx=3, pady=3, sticky="w")
self.game_info = ttk.Label(status_frame, text="—— | —— | ——", foreground="#666")
self.game_info.grid(row=2, column=1, padx=3, pady=3, sticky="w", columnspan=3)
# 4. 底部提示
self.bottom_label = ttk.Label(self.root, text=f"刷新间隔:{REFRESH_INTERVAL}s | 线程:未运行",
font=("Arial", 8), foreground="#888")
self.bottom_label.grid(row=3, column=0, padx=15, pady=8, sticky="we")
def _load_config(self):
"""加载保存的配置(包括API密钥)"""
try:
if os.path.exists(CONFIG_FILE):
with open(CONFIG_FILE, 'r') as f:
config = json.load(f)
self.hypixel_api_key = config.get('api_key', '')
except Exception as e:
print(f"加载配置失败:{str(e)}")
# 加载失败时使用空密钥
def _save_api_key(self):
"""保存API密钥到配置文件"""
api_key = self.api_key_entry.get().strip()
if not api_key:
messagebox.showwarning("提示", "API密钥不能为空!")
return
try:
# 保存到内存
self.hypixel_api_key = api_key
# 保存到文件
config = {'api_key': api_key}
with open(CONFIG_FILE, 'w') as f:
json.dump(config, f)
messagebox.showinfo("成功", "API密钥已保存!")
except Exception as e:
messagebox.showerror("错误", f"保存API密钥失败:{str(e)}")
def _show_map_change_alert(self, player_name, old_map, new_map):
"""显示地图变化提示框(位于屏幕右上角)"""
# 如果已有提示窗口,先关闭
if self.alert_window:
self.alert_window.destroy()
# 创建新的提示窗口
self.alert_window = tk.Toplevel(self.root)
self.alert_window.title("地图变化提示")
self.alert_window.geometry("400x150")
self.alert_window.wm_attributes('-topmost', True) # 提示框置顶
self.alert_window.wm_attributes('-alpha', 0.95) # 稍微透明
# 创建大字体
alert_font = font.Font(family="Arial", size=14, weight="bold")
# 添加提示内容
ttk.Label(
self.alert_window,
text=f"{player_name} 已切换地图",
font=alert_font,
foreground="#ff0000"
).pack(pady=5)
ttk.Label(
self.alert_window,
text=f"从: {old_map}",
font=("Arial", 11)
).pack(pady=3)
ttk.Label(
self.alert_window,
text=f"到: {new_map}",
font=("Arial", 11)
).pack(pady=3)
# 设置自动关闭
self.alert_window.after(ALERT_DURATION, self._close_alert_window)
# 确保窗口显示在屏幕右上角
self.alert_window.update_idletasks()
width = self.alert_window.winfo_width()
height = self.alert_window.winfo_height()
# 计算右上角位置:屏幕宽度 - 窗口宽度 - 偏移量,顶部 + 偏移量
x = (self.root.winfo_screenwidth() - width - ALERT_OFFSET)
y = ALERT_OFFSET
self.alert_window.geometry('{}x{}+{}+{}'.format(width, height, x, y))
def _close_alert_window(self):
"""关闭地图变化提示框"""
if self.alert_window:
self.alert_window.destroy()
self.alert_window = None
def _get_uuid(self, nickname: str) -> tuple[bool, str, str]:
"""获取UUID"""
try:
response = requests.get(
f"https://api.mojang.com/users/profiles/minecraft/{nickname}",
timeout=8
)
response.raise_for_status()
data = response.json()
if "id" not in data:
return False, "无UUID返回", ""
uuid_no_dash = data["id"]
uuid_with_dash = f"{uuid_no_dash[:8]}-{uuid_no_dash[8:12]}-{uuid_no_dash[12:16]}-{uuid_no_dash[16:20]}-{uuid_no_dash[20:]}"
return True, uuid_no_dash, uuid_with_dash
except requests.exceptions.HTTPError as e:
if response.status_code == 404:
return False, f"昵称不存在", ""
elif response.status_code == 429:
return False, "Mojang超限", ""
else:
return False, f"HTTP {response.status_code}", ""
except requests.exceptions.RequestException as e:
return False, f"网络错:{str(e)[:10]}", ""
except Exception as e:
return False, f"未知错:{str(e)[:10]}", ""
def _get_hypixel_status(self, uuid: str) -> tuple[bool, dict, str]:
"""获取Hypixel状态"""
# 检查API密钥是否已配置
if not self.hypixel_api_key:
return False, None, "未配置API密钥"
try:
response = requests.get(
"https://api.hypixel.net/v2/status",
headers={"API-Key": self.hypixel_api_key},
params={"uuid": uuid},
timeout=10
)
response.raise_for_status()
data = response.json()
if not data.get("success"):
return False, None, data.get("cause", "API失败")
return True, data, ""
except requests.exceptions.HTTPError as e:
if response.status_code == 403:
return False, None, "密钥无效或无权限"
elif response.status_code == 429:
reset = response.headers.get("RateLimit-Reset", "?")
return False, None, f"请求超限({reset}s)"
else:
return False, None, f"HTTP {response.status_code}"
except requests.exceptions.RequestException as e:
return False, None, f"网络错:{str(e)[:10]}"
except Exception as e:
return False, None, f"未知错:{str(e)[:10]}"
def _reset_status_color(self):
"""重置状态文字颜色为正常状态"""
if self.previous_online_status is not None:
self.online_status.config(
foreground="#4CAF50" if self.previous_online_status else "#f44"
)
self.status_change_timer = None
def _update_gui(self, is_success: bool, data: dict = None, error_msg: str = ""):
"""更新GUI显示"""
if not is_success:
# 错误状态显示
self.online_status.config(text=f"❌ {error_msg}", foreground="#f44")
self.uuid_no_dash.config(text="——", foreground="#666")
self.game_info.config(text="—— | —— | ——", foreground="#666")
self.bottom_label.config(text=f"刷新间隔:{REFRESH_INTERVAL}s | 线程:空闲")
self.previous_online_status = None
self.previous_map = None
return
# 解析数据
uuid_no_dash = data.get("uuid", "未知")
session = data.get("session", {})
is_online = session.get("online", False)
game_type = session.get("gameType", "未知")
game_mode = session.get("mode", "未知")
game_map = session.get("map", "未知")
# 优化游戏类型显示
game_type_display = {
"SKYBLOCK": "SkyBlock", "BEDWARS": "BedWars",
"SKYWARS": "SkyWars", "DUELS": "Duels"
}.get(game_type, game_type)
# 检查状态是否变化
status_changed = self.previous_online_status is not None and self.previous_online_status != is_online
# 检查地图是否变化(仅当玩家在线时)
map_changed = False
if is_online and self.previous_online_status and self.previous_map is not None:
map_changed = self.previous_map != game_map
# 如果地图变化,显示提示框
if map_changed:
self._show_map_change_alert(self.player_nickname, self.previous_map, game_map)
# 更新控件
self.uuid_no_dash.config(text=uuid_no_dash[:16] + "..." if len(uuid_no_dash) > 16 else uuid_no_dash)
# 设置状态文字和颜色
status_text = "✅ 在线" if is_online else "❌ 离线"
if status_changed:
# 状态变化时设置为红色提示
self.online_status.config(text=status_text, foreground="#ff0000")
# 取消之前的定时器
if self.status_change_timer:
self.root.after_cancel(self.status_change_timer)
# 设置新的定时器,一段时间后恢复正常颜色
self.status_change_timer = self.root.after(STATUS_CHANGE_DURATION, self._reset_status_color)
else:
# 正常状态颜色
self.online_status.config(
text=status_text,
foreground="#4CAF50" if is_online else "#f44"
)
self.game_info.config(
text=f"{game_type_display} | {game_mode} | {game_map}" if is_online
else "—— | —— | ——",
foreground="#000" if is_online else "#666"
)
self.bottom_label.config(text=f"刷新间隔:{REFRESH_INTERVAL}s | 线程:运行中")
# 更新之前的状态记录
self.previous_online_status = is_online
self.previous_map = game_map if is_online else None
def _fetch_status_thread(self):
"""查询线程"""
self.is_thread_running = True
nickname = self.player_nickname.strip()
# 获取UUID
uuid_success, uuid_data, _ = self._get_uuid(nickname)
if not uuid_success:
self.root.after(0, self._update_gui, False, None, uuid_data)
self.is_thread_running = False
self._schedule_next_refresh()
return
# 获取Hypixel状态
self.player_uuid = uuid_data
status_success, status_data, status_error = self._get_hypixel_status(uuid_data)
if status_success:
self.root.after(0, self._update_gui, True, status_data)
else:
self.root.after(0, self._update_gui, False, None, status_error)
# 标记线程结束,调度下一次刷新
self.is_thread_running = False
self._schedule_next_refresh()
def _schedule_next_refresh(self):
"""调度下一次查询"""
if self.is_monitoring:
self.after_id = self.root.after(REFRESH_INTERVAL * 1000, self._start_thread_safe)
def _start_thread_safe(self):
"""安全启动线程"""
if not self.is_thread_running and self.is_monitoring:
threading.Thread(target=self._fetch_status_thread, daemon=True).start()
def toggle_monitoring(self):
"""切换监控状态"""
if not self.is_monitoring:
# 检查API密钥是否已配置
if not self.hypixel_api_key:
messagebox.showwarning("提示", "请先配置并保存API密钥!")
return
# 开始监控
nickname = self.nickname_entry.get().strip()
if not nickname:
messagebox.showwarning("提示", "请输入玩家昵称!")
self.nickname_entry.focus()
return
self.player_nickname = nickname
self.is_monitoring = True
self.query_btn.config(text="停止监控")
self.online_status.config(text="查询中...", foreground="#ff9800")
self.bottom_label.config(text=f"刷新间隔:{REFRESH_INTERVAL}s | 线程:启动中")
self.previous_online_status = None # 重置状态记录
self.previous_map = None # 重置地图记录
# 启动第一次查询
self._start_thread_safe()
else:
# 停止监控
self.is_monitoring = False
self.query_btn.config(text="开始监控")
# 取消定时任务
if self.after_id:
self.root.after_cancel(self.after_id)
self.after_id = None
# 取消状态变化定时器和提示窗口
if self.status_change_timer:
self.root.after_cancel(self.status_change_timer)
self.status_change_timer = None
self._close_alert_window()
# 重置显示
self.online_status.config(text="已停止", foreground="#999")
self.uuid_no_dash.config(text="——", foreground="#666")
self.game_info.config(text="—— | —— | ——", foreground="#666")
self.bottom_label.config(text=f"刷新间隔:{REFRESH_INTERVAL}s | 线程:已停止")
self.previous_online_status = None
self.previous_map = None
if __name__ == "__main__":
# 启动GUI
root = tk.Tk()
app = HypixelStatusGUI(root)
root.mainloop()