forked from Hsdi/Shortcut_Sync_PotPlayer
235 lines
6.8 KiB
Python
235 lines
6.8 KiB
Python
import time
|
||
import tkinter as tk
|
||
|
||
import keyboard
|
||
import win32api
|
||
import win32con
|
||
import win32gui
|
||
|
||
|
||
POTPLAYER_CLASSES = {"PotPlayer64", "PotPlayer"}
|
||
APP_TITLE = "PotPlayer 同步器"
|
||
WINDOW_SIZE = "420x240"
|
||
PLAY_PAUSE_COOLDOWN = 0.25
|
||
|
||
WM_APPCOMMAND = 0x0319
|
||
APPCOMMAND_MEDIA_PLAY_PAUSE = 14
|
||
|
||
|
||
def safe_get_class_name(hwnd):
|
||
try:
|
||
return win32gui.GetClassName(hwnd)
|
||
except win32gui.error:
|
||
return ""
|
||
|
||
|
||
def safe_get_window_text(hwnd):
|
||
try:
|
||
return win32gui.GetWindowText(hwnd)
|
||
except win32gui.error:
|
||
return ""
|
||
|
||
|
||
def is_potplayer_window(hwnd):
|
||
return safe_get_class_name(hwnd) in POTPLAYER_CLASSES
|
||
|
||
|
||
def build_key_lparam(vk_code, is_keydown):
|
||
scan_code = win32api.MapVirtualKey(vk_code, 0)
|
||
is_extended = vk_code in {
|
||
win32con.VK_LEFT,
|
||
win32con.VK_RIGHT,
|
||
win32con.VK_UP,
|
||
win32con.VK_DOWN,
|
||
win32con.VK_CONTROL,
|
||
win32con.VK_RCONTROL,
|
||
win32con.VK_MENU,
|
||
win32con.VK_RMENU,
|
||
}
|
||
|
||
lparam = 1 | (scan_code << 16) | (int(is_extended) << 24)
|
||
if not is_keydown:
|
||
lparam |= (1 << 30) | (1 << 31)
|
||
return lparam
|
||
|
||
|
||
def get_potplayer_windows():
|
||
hwnds = []
|
||
|
||
def callback(hwnd, _extra):
|
||
if win32gui.IsWindowVisible(hwnd) and is_potplayer_window(hwnd):
|
||
hwnds.append(hwnd)
|
||
return True
|
||
|
||
win32gui.EnumWindows(callback, None)
|
||
return hwnds
|
||
|
||
|
||
class PotPlayerSyncApp:
|
||
def __init__(self):
|
||
self.root = tk.Tk()
|
||
self.root.title(APP_TITLE)
|
||
self.root.geometry(WINDOW_SIZE)
|
||
self.root.resizable(False, False)
|
||
self.root.attributes("-topmost", True)
|
||
self.root.protocol("WM_DELETE_WINDOW", self.on_close)
|
||
|
||
self.status_var = tk.StringVar(value="等待操作...")
|
||
self.last_play_pause_at = 0.0
|
||
self.hotkey_handles = []
|
||
|
||
self.build_ui()
|
||
self.register_hotkeys()
|
||
self.refresh_initial_status()
|
||
|
||
def build_ui(self):
|
||
tk.Label(
|
||
self.root,
|
||
text="PotPlayer 联动控制台",
|
||
font=("Microsoft YaHei", 13, "bold"),
|
||
pady=10,
|
||
).pack()
|
||
|
||
tips = (
|
||
"只在 PotPlayer 窗口或本工具窗口内操作时生效\n"
|
||
"\n"
|
||
"空格: 同步播放 / 暂停\n"
|
||
"Left / Right: 同步快退 / 快进\n"
|
||
"Shift + Left / Right: 同步微调进度\n"
|
||
"Ctrl + Left / Right: 同步大幅调整进度"
|
||
)
|
||
tk.Label(
|
||
self.root,
|
||
text=tips,
|
||
font=("Microsoft YaHei", 10),
|
||
justify="left",
|
||
fg="#333333",
|
||
padx=16,
|
||
pady=4,
|
||
).pack(anchor="w")
|
||
|
||
tk.Label(
|
||
self.root,
|
||
textvariable=self.status_var,
|
||
font=("Microsoft YaHei", 10, "bold"),
|
||
pady=10,
|
||
fg="#0066CC",
|
||
wraplength=380,
|
||
justify="left",
|
||
).pack()
|
||
|
||
def register_hotkeys(self):
|
||
self.hotkey_handles.extend(
|
||
[
|
||
keyboard.on_press_key("space", self.handle_space, suppress=False),
|
||
keyboard.on_press_key("left", self.handle_left, suppress=False),
|
||
keyboard.on_press_key("right", self.handle_right, suppress=False),
|
||
]
|
||
)
|
||
|
||
def refresh_initial_status(self):
|
||
count = len(get_potplayer_windows())
|
||
if count:
|
||
self.update_status(f"已检测到 {count} 个 PotPlayer 窗口,等待操作...")
|
||
else:
|
||
self.update_status("暂未检测到 PotPlayer 窗口。")
|
||
|
||
def update_status(self, message):
|
||
self.root.after(0, lambda: self.status_var.set(message))
|
||
|
||
def is_app_window(self, hwnd):
|
||
return safe_get_window_text(hwnd) == APP_TITLE
|
||
|
||
def get_target_windows(self):
|
||
foreground_hwnd = win32gui.GetForegroundWindow()
|
||
in_potplayer = is_potplayer_window(foreground_hwnd)
|
||
in_app = self.is_app_window(foreground_hwnd)
|
||
|
||
if not (in_potplayer or in_app):
|
||
return [], "请先聚焦 PotPlayer 窗口或同步器窗口。"
|
||
|
||
hwnds = get_potplayer_windows()
|
||
if not hwnds:
|
||
return [], "未检测到可同步的 PotPlayer 窗口。"
|
||
|
||
targets = [hwnd for hwnd in hwnds if hwnd != foreground_hwnd] if in_potplayer else hwnds
|
||
if not targets:
|
||
return [], "当前只有一个 PotPlayer 窗口,无需同步。"
|
||
|
||
return targets, ""
|
||
|
||
def post_play_pause(self, hwnd):
|
||
lparam = APPCOMMAND_MEDIA_PLAY_PAUSE << 16
|
||
win32gui.PostMessage(hwnd, WM_APPCOMMAND, 0, lparam)
|
||
|
||
def post_combo(self, hwnd, vk_codes):
|
||
for vk_code in vk_codes:
|
||
win32gui.PostMessage(hwnd, win32con.WM_KEYDOWN, vk_code, build_key_lparam(vk_code, True))
|
||
for vk_code in reversed(vk_codes):
|
||
win32gui.PostMessage(hwnd, win32con.WM_KEYUP, vk_code, build_key_lparam(vk_code, False))
|
||
|
||
def sync_action(self, action_name, sender):
|
||
target_hwnds, message = self.get_target_windows()
|
||
if not target_hwnds:
|
||
self.update_status(message)
|
||
return
|
||
|
||
for hwnd in target_hwnds:
|
||
try:
|
||
sender(hwnd)
|
||
except win32gui.error:
|
||
continue
|
||
|
||
self.update_status(f"已同步: {action_name} -> {len(target_hwnds)} 个窗口")
|
||
|
||
def handle_space(self, _event):
|
||
now = time.monotonic()
|
||
if now - self.last_play_pause_at < PLAY_PAUSE_COOLDOWN:
|
||
return
|
||
|
||
self.last_play_pause_at = now
|
||
self.sync_action("空格(播放/暂停)", self.post_play_pause)
|
||
|
||
def handle_left(self, _event):
|
||
if keyboard.is_pressed("shift"):
|
||
vk_codes = [win32con.VK_SHIFT, win32con.VK_LEFT]
|
||
action_name = "Shift + Left(微调后退)"
|
||
elif keyboard.is_pressed("ctrl"):
|
||
vk_codes = [win32con.VK_CONTROL, win32con.VK_LEFT]
|
||
action_name = "Ctrl + Left(大步后退)"
|
||
else:
|
||
vk_codes = [win32con.VK_LEFT]
|
||
action_name = "Left(后退)"
|
||
|
||
self.sync_action(action_name, lambda hwnd: self.post_combo(hwnd, vk_codes))
|
||
|
||
def handle_right(self, _event):
|
||
if keyboard.is_pressed("shift"):
|
||
vk_codes = [win32con.VK_SHIFT, win32con.VK_RIGHT]
|
||
action_name = "Shift + Right(微调前进)"
|
||
elif keyboard.is_pressed("ctrl"):
|
||
vk_codes = [win32con.VK_CONTROL, win32con.VK_RIGHT]
|
||
action_name = "Ctrl + Right(大步前进)"
|
||
else:
|
||
vk_codes = [win32con.VK_RIGHT]
|
||
action_name = "Right(前进)"
|
||
|
||
self.sync_action(action_name, lambda hwnd: self.post_combo(hwnd, vk_codes))
|
||
|
||
def on_close(self):
|
||
for handle in self.hotkey_handles:
|
||
keyboard.unhook(handle)
|
||
self.root.destroy()
|
||
|
||
def run(self):
|
||
self.root.mainloop()
|
||
|
||
|
||
def main():
|
||
app = PotPlayerSyncApp()
|
||
app.run()
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|