diff --git a/build.ps1 b/build.ps1 index ac53d63..fa6c6ba 100644 --- a/build.ps1 +++ b/build.ps1 @@ -55,6 +55,8 @@ $InstallerArgsDebug = @( ".\src\assets\*.ico;.\assets", # "--add-data", # ".\src\assets\*.pdf;.\assets", + "--collect-data", + "sv_ttk", "--paths", ".\src", "--clean", diff --git a/metadata.yml b/metadata.yml index 701396a..4995f2e 100644 --- a/metadata.yml +++ b/metadata.yml @@ -1,4 +1,4 @@ -Version: 1.1.0 +Version: 1.2.0 CompanyName: Chenglong Ma FileDescription: A launcher for starting LOL in different languages InternalName: LOLauncher diff --git a/requirements.txt b/requirements.txt index 4f95376..e161e3c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,4 +6,5 @@ watchdog>=4.0.1 pystray>=0.19.5 pillow>=10.3.0 keyboard>=0.13.5 -mouse>=0.7.1 \ No newline at end of file +mouse>=0.7.1 +sv_ttk>=2.6.0 \ No newline at end of file diff --git a/src/assets/tray_icon.png b/src/assets/tray_icon.png index 46d44a4..28e58ea 100644 Binary files a/src/assets/tray_icon.png and b/src/assets/tray_icon.png differ diff --git a/src/main.py b/src/main.py index f4e28b8..f76957c 100644 --- a/src/main.py +++ b/src/main.py @@ -11,7 +11,7 @@ config = read_json(CONFIG_FILENAME) or {} ui_config = read_json(GUI_CONFIG_FILENAME) or {} - setting_file = verify_metadata_file(config) + setting_files = verify_metadata_file(config) - app = App(setting_file, config, ui_config) + app = App(setting_files, config, ui_config) app.run() diff --git a/src/ui/app.py b/src/ui/app.py index b907de7..656164a 100644 --- a/src/ui/app.py +++ b/src/ui/app.py @@ -1,6 +1,7 @@ import threading import tkinter as tk -from tkinter import ttk, messagebox +from tkinter import ttk, messagebox, font +import sv_ttk import pystray from PIL import Image @@ -8,27 +9,39 @@ from assets import get_asset from ui.quick_chat import QuickChatDialog -from ui.utils import create_warning_label +from ui.utils import create_warning_label, THEME_COLOR from utils import * +def change_font(font_family): + for font_name in font.names(): + _font = font.nametofont(font_name) + _font.config(family=font_family) + + class App: - def __init__(self, setting_file, config, gui_config): - self.setting_file = setting_file + def __init__(self, setting_files, config, gui_config): + self.setting_files: [str] = setting_files self.config = config self.ui_config = gui_config self.locale_dict = {value: key for key, value in LOCALE_CODES.items()} self.game_client = config.get("GameClient", "") - self.observer: Observer | None = None + self.observers: [Observer] = [] self.watching = False self.watch_thread = None self.root = tk.Tk() + self.theme: str = self.ui_config.get("Theme", "light") + if "dark" in self.theme.lower(): + sv_ttk.use_dark_theme() + else: + sv_ttk.use_light_theme() + change_font("Microsoft YaHei UI") self.root.title(f"{APP_NAME} v{VERSION}") - self.window_width = 300 - self.window_height = 330 - self.control_padding = 8 - self.layout_padding = 10 + self.window_width = 350 + self.window_height = 420 + self.control_padding = 10 + self.layout_padding = 15 self.screen_width = self.root.winfo_screenwidth() self.screen_height = self.root.winfo_screenheight() @@ -42,6 +55,8 @@ def __init__(self, setting_file, config, gui_config): self.create_menu_bar() self.selected_locale = config.get("Locale", "zh_CN") + if self.selected_locale not in LOCALE_CODES.keys(): + self.selected_locale = "zh_CN" self.create_locale_groupbox() self.create_quick_chat_groupbox() @@ -82,6 +97,8 @@ def create_menu_bar(self): self.setting_menu.add_command(label="自动检测游戏配置文件", command=self.detect_metadata_file) self.setting_menu.add_command(label="手动选择游戏配置文件", command=self.choose_metadata_file) # self.setting_menu.add_command(label="恢复默认", command=self.reset_settings) + self.setting_menu.add_separator() + self.setting_menu.add_command(label="切换颜色主题", command=lambda: self.root.after(0, self.toggle_theme)) self.minimize_on_closing = tk.BooleanVar(value=self.config.get("MinimizeOnClosing", True)) self.setting_menu.add_checkbutton(label="关闭时最小化到托盘", variable=self.minimize_on_closing) self.menu_bar.add_cascade(label="设置", menu=self.setting_menu) @@ -91,8 +108,14 @@ def create_menu_bar(self): self.menu_bar.add_cascade(label="帮助", menu=self.help_menu) self.root.config(menu=self.menu_bar) + def toggle_theme(self): + sv_ttk.toggle_theme() + self.theme = sv_ttk.get_theme() + self.quick_chat_warning_label.config(background=THEME_COLOR[self.theme]["warning_bg"], foreground=THEME_COLOR[self.theme]["warning_fg"]) + self.quick_chat_warning_label.tag_config("link", foreground=THEME_COLOR[self.theme]["accent"], underline=True) + def create_locale_groupbox(self): - self.locale_groupbox = tk.LabelFrame(self.root, text="语言设置") + self.locale_groupbox = ttk.LabelFrame(self.root, text="语言设置", style='Card.TFrame') self.locale_var = tk.StringVar(value=LOCALE_CODES[self.selected_locale]) self.locale_dropdown = ttk.Combobox(self.locale_groupbox, textvariable=self.locale_var, state="readonly", exportselection=True) self.locale_dropdown['values'] = list(self.locale_dict.keys()) @@ -127,25 +150,36 @@ def on_quick_chat_enable_change(self, *args): self.quick_chat_dialog.disable_hotkey() def create_quick_chat_groupbox(self): - self.quick_chat_groupbox = tk.LabelFrame(self.root, text="一键喊话设置") + self.quick_chat_groupbox = ttk.LabelFrame(self.root, text="一键喊话设置", style='Card.TFrame') self.quick_chat_doc = self.config.get("QuickChatDoc", QUICK_CHAT_DOC) - self.quick_chat_warning_label = create_warning_label(self.quick_chat_groupbox, "\u26A1 使用前请仔细阅读", "注意事项", self.quick_chat_doc) + self.quick_chat_warning_label = create_warning_label( + self.quick_chat_groupbox, + "\u26A1 使用前请仔细阅读", "注意事项", + self.quick_chat_doc, + theme=self.theme + ) self.quick_chat_warning_label.pack(padx=self.control_padding, pady=self.control_padding, fill=tk.BOTH) self.quick_chat_enabled_setting = self.config.get("QuickChatEnabled", False) self.quick_chat_enabled = tk.BooleanVar(value=self.quick_chat_enabled_setting) self.quick_chat_enabled.trace("w", self.on_quick_chat_enable_change) - state = tk.NORMAL if self.quick_chat_enabled.get() else tk.DISABLED - self.quick_chat_checkbox = tk.Checkbutton(self.quick_chat_groupbox, text="一键喊话", variable=self.quick_chat_enabled) + + self.quick_chat_checkbox = ttk.Checkbutton(self.quick_chat_groupbox, text="一键喊话", variable=self.quick_chat_enabled, + style='Switch.TCheckbutton') self.quick_chat_checkbox.pack() - self.shortcut_frame = tk.Frame(self.quick_chat_groupbox) + self.shortcut_frame = ttk.Frame(self.quick_chat_groupbox) - self.shortcut_label = tk.Label(self.shortcut_frame, text="快捷键") - self.shortcut_label.pack(side=tk.LEFT) - self.shortcut_var = tk.StringVar(value=self.config.get("QuickChatShortcut", "`")) + self.shortcut_label = ttk.Label(self.shortcut_frame, text="快捷键") + self.shortcut_label.pack(side=tk.LEFT, padx=self.control_padding) + available_shortcuts = ["`", "Alt", "Ctrl", "Shift", "Tab"] + setting_value = self.config.get("QuickChatShortcut", "`") + if setting_value not in available_shortcuts: + available_shortcuts.append(setting_value) + self.shortcut_var = tk.StringVar(value=setting_value) + state = "readonly" if self.quick_chat_enabled.get() else tk.DISABLED self.shortcut_dropdown = ttk.Combobox(self.shortcut_frame, state=state, textvariable=self.shortcut_var, exportselection=True) - available_shortcuts = ["`", "Alt", "Ctrl", "Shift", "Tab"] + self.shortcut_dropdown['values'] = available_shortcuts self.shortcut_dropdown.current(available_shortcuts.index(self.shortcut_var.get())) self.shortcut_dropdown.bind("<>", self.on_shortcut_changed) @@ -153,7 +187,8 @@ def create_quick_chat_groupbox(self): self.shortcut_frame.pack(padx=self.layout_padding) - self.set_chat_button = tk.Button(self.quick_chat_groupbox, text="设置喊话内容", state=state, command=self.open_quick_chat_file) + state = tk.NORMAL if self.quick_chat_enabled.get() else tk.DISABLED + self.set_chat_button = ttk.Button(self.quick_chat_groupbox, text="设置喊话内容", state=state, command=self.open_quick_chat_file) self.set_chat_button.pack(padx=self.control_padding, pady=self.control_padding, fill=tk.BOTH) self.quick_chat_groupbox.pack(fill=tk.BOTH, padx=self.layout_padding, pady=self.layout_padding) @@ -171,14 +206,16 @@ def open_quick_chat_file(self): subprocess.run(['notepad.exe', QUICK_CHAT_FILENAME], check=False) def create_launch_button(self): + style = ttk.Style() + style.configure('CustomAccent.TButton', font=('Microsoft YaHei', 16, 'bold')) self.image = tk.PhotoImage(file=get_asset("button_icon.png")) - self.launch_button = tk.Button(self.root, text="英雄联盟,启动!", image=self.image, compound=tk.LEFT, - command=self.start) + self.launch_button = ttk.Button(self.root, text="英雄联盟,启动!", image=self.image, compound=tk.LEFT, + command=self.start, style="CustomAccent.TButton") self.launch_button.pack(side=tk.BOTTOM, pady=self.layout_padding) def create_status_bar(self): self.status_var = tk.StringVar(value="准备就绪") - self.status_bar = tk.Label(self.root, textvariable=self.status_var, bd=1, relief=tk.RIDGE, anchor=tk.W, foreground="gray") + self.status_bar = ttk.Label(self.root, textvariable=self.status_var, relief=tk.RIDGE, anchor=tk.W, foreground="gray") self.status_bar.pack(side=tk.BOTTOM, fill=tk.X) def update_status(self, message): @@ -193,12 +230,13 @@ def show_about(self, icon=None, item=None): self.about_window.protocol("WM_DELETE_WINDOW", lambda: self.on_about_window_closing(create_tray=icon is not None)) self.app_name_label = tk.Label(self.about_window, text=f"{APP_NAME} v{VERSION}") self.app_name_label.pack(padx=self.control_padding, pady=pady) + accent_color = THEME_COLOR[self.theme]["accent"] - self.author_label = tk.Label(self.about_window, text="作者:Chenglong Ma", fg="blue", cursor="hand2") + self.author_label = tk.Label(self.about_window, text="作者:Chenglong Ma", fg=accent_color, cursor="hand2") self.author_label.pack(padx=self.control_padding, pady=pady) self.author_label.bind("", lambda event: open_my_homepage()) - self.homepage_label = tk.Label(self.about_window, text=f"GitHub:{REPO_NAME}", fg="blue", cursor="hand2") + self.homepage_label = tk.Label(self.about_window, text=f"GitHub:{REPO_NAME}", fg=accent_color, cursor="hand2") self.homepage_label.pack(padx=self.control_padding, pady=pady) self.homepage_label.bind("", lambda event: open_repo_page()) @@ -207,25 +245,25 @@ def show_about(self, icon=None, item=None): def on_about_window_closing(self, create_tray=False): self.about_window.destroy() - # if create_tray: - # self.run_tray_app() def no_new_version_fn(self): messagebox.showinfo("检查更新", "当前已是最新版本") self.update_status("当前已是最新版本") - def start_game(self, settings): + def start_game(self, *settings): self.update_status("正在启动游戏...") - product_install_root = settings['product_install_root'] - - product_install_root = product_install_root if os.path.exists(product_install_root) else "C:/" - - game_clients_in_settings = [os.path.join(product_install_root, "Riot Client/RiotClientServices.exe")] - + game_clients_in_settings = [os.path.join(setting['product_install_root'], "Riot Client/RiotClientServices.exe") for setting in settings] game_clients = filter_existing_files(to_list(self.game_client) + game_clients_in_settings) if not game_clients or len(game_clients) == 0: self.update_status("未找到 RiotClientServices.exe,请手动启动游戏。") return + if len(game_clients) > 1: + msg = "请从以下选项中选择一个游戏启动路径" + title = "找到多个游戏路径,请选择一个" + choice = easygui.choicebox(msg, title, game_clients) + if not choice: + return + game_clients = [choice] self.update_status("英雄联盟,启动!") self.game_client = os.path.normpath(game_clients[0]) @@ -233,22 +271,28 @@ def start_game(self, settings): def start(self): self.update_status("正在更新配置文件...") - settings = update_settings(self.setting_file, self.selected_locale, msg_callback_fn=self.update_status) - if not settings: + settings = [] + for setting_file in self.setting_files: + setting = update_settings(setting_file, self.selected_locale, msg_callback_fn=self.update_status) + if setting: + settings.append(setting) + + if len(settings) == 0: messagebox.showerror("错误", "配置文件更新失败,无法启动游戏。") return - self.start_game(settings) + self.start_game(*settings) self.on_window_minimizing(True) def wait_for_observer_stopping(self): print("Stopping observer...") self.watching = False - if self.observer is not None and self.observer.is_alive(): - self.observer.stop() - # self.observer.join() - print("Observer stopped") - self.observer = None + for observer in self.observers: + if observer is not None and observer.is_alive(): + observer.stop() + # observer.join() + print("Observer stopped") + self.observers = [] def start_watch_thread(self): self.wait_for_observer_stopping() @@ -257,28 +301,33 @@ def start_watch_thread(self): def watch_file(self): self.wait_for_observer_stopping() - event_handler = FileWatcher(self.setting_file, self.selected_locale) - self.observer = Observer() - self.observer.schedule(event_handler, path=os.path.dirname(self.setting_file), recursive=False) + event_handler = FileWatcher(*self.setting_files, selected_locale=self.selected_locale) + unique_dirs = set(os.path.dirname(file) for file in self.setting_files) + for directory in unique_dirs: + observer = Observer() + observer.schedule(event_handler, path=directory, recursive=False) + observer.start() + self.observers.append(observer) self.watching = True - self.observer.start() - print(f"Watching {self.setting_file}...") + print(f"Start Watching...") try: while self.watching: time.sleep(1) except KeyboardInterrupt: - self.observer.stop() + [observer.stop() for observer in self.observers] + self.watching = False except Exception as e: self.update_status(f"Error: {e}") - self.observer.stop() + [observer.stop() for observer in self.observers] + self.watching = False def detect_metadata_file(self): is_yes = tk.messagebox.askyesno("提示", "您确定要重新检测以修改已有配置?") if is_yes: setting_files = detect_metadata_file() if setting_files: - self.setting_file = setting_files[0] # TODO: multiple files support + self.setting_files = setting_files self.sync_config() msg = "游戏配置文件已更新" else: @@ -293,7 +342,7 @@ def choose_metadata_file(self): file_types=[('Riot 配置文件', '*.yaml'), ('所有文件', '*.*')], initial_dir=DEFAULT_METADATA_DIR, initial_file=DEFAULT_METADATA_FILE) if selected_file: - self.setting_file = selected_file + self.setting_files = [selected_file] self.sync_config() msg = "游戏配置文件已更新" else: @@ -338,7 +387,7 @@ def sync_config(self): default_config = { "@注意": r"请使用\或/做为路径分隔符,如 C:\ProgramData 或 C:/ProgramData", "@SettingFile": "请在下方填写 league_of_legends.live.product_settings.yaml 文件路径", - "SettingFile": self.setting_file, + "SettingFile": self.setting_files, "@GameClient": "请在下方填写 RiotClientServices.exe 文件路径", "GameClient": self.game_client, "Locale": self.selected_locale, @@ -351,6 +400,7 @@ def sync_config(self): self.config.update(default_config) self.config.update(self.quick_chat_dialog.user_config) write_json(CONFIG_FILENAME, self.config) + self.ui_config["Theme"] = self.theme self.ui_config.update(self.quick_chat_dialog.ui_config) write_json(GUI_CONFIG_FILENAME, self.ui_config) print("Configuration file updated") diff --git a/src/ui/quick_chat.py b/src/ui/quick_chat.py index 0825ff8..4a86425 100644 --- a/src/ui/quick_chat.py +++ b/src/ui/quick_chat.py @@ -2,6 +2,7 @@ import threading import time import tkinter as tk +from tkinter import ttk import easygui import keyboard diff --git a/src/ui/utils.py b/src/ui/utils.py index 8ddb7f0..924e73e 100644 --- a/src/ui/utils.py +++ b/src/ui/utils.py @@ -4,6 +4,27 @@ from assets import get_asset from utils import open_web +THEME_COLOR = { + "dark": { + "warning_bg": "#333333", + "warning_fg": "white", + "border": "#927934", + "border_inactive": "#463714", + "hover_bg": "#C89B3C", + "hover_fg": "#010A13", + "accent": "#C89B3C", + }, + "light": { + "warning_bg": "#F0E6D2", + "warning_fg": "#010A13", + "border": "#927934", + "border_inactive": "#463714", + "hover_bg": "#C89B3C", + "hover_fg": "#010A13", + "accent": "blue", + } +} + def reset_list_box_colors(list_box, default_bg, default_fg): for i in range(list_box.size()): @@ -18,11 +39,12 @@ def open_asset(uri): subprocess.run(['start', file_path], shell=True, check=False) -def create_warning_label(parent, normal_text, link_text, uri): - label = tk.Text(parent, height=1, background="#ffffe0", relief=tk.SOLID, borderwidth=1) +def create_warning_label(parent, normal_text, link_text, uri, theme="dark"): + label = tk.Text(parent, height=1, background=THEME_COLOR[theme]["warning_bg"], foreground=THEME_COLOR[theme]["warning_fg"], relief=tk.SOLID, + borderwidth=1) label.insert(1.0, normal_text + " ", tk.CENTER) label.insert(tk.END, link_text, "link") - label.tag_config("link", foreground="blue", underline=True) + label.tag_config("link", foreground=THEME_COLOR[theme]["accent"], underline=True) label.tag_bind("link", "", lambda e: open_asset(uri)) label.tag_bind("link", "", lambda e: label.config(cursor="hand2")) label.tag_bind("link", "", lambda e: label.config(cursor="")) diff --git a/src/utils.py b/src/utils.py index 8f911c9..356bb1f 100644 --- a/src/utils.py +++ b/src/utils.py @@ -20,11 +20,20 @@ from github import Github from watchdog.events import FileSystemEventHandler -VERSION = '1.1.0' +VERSION = '1.2.0' APP_NAME = 'LOLauncher' REPO_NAME = 'ChenglongMa/LOLauncher' -DEFAULT_METADATA_DIR = r"C:\ProgramData\Riot Games\Metadata\league_of_legends.live\league_of_legends.live.product_settings.yaml" -DEFAULT_METADATA_FILE = f"{DEFAULT_METADATA_DIR}\\league_of_legends.live.product_settings.yaml" +SUPPORTED_PATCH_LINEs = ['live', 'pbe'] +DEFAULT_PATCH_LINE = 'live' +DEFAULT_DRIVE = r"C:" +# DEFAULT_METADATA_DIR = rf"{DEFAULT_DRIVE}\ProgramData\Riot Games\Metadata\league_of_legends.live" +# DEFAULT_METADATA_FILE = f"{DEFAULT_METADATA_DIR}\\league_of_legends.live.product_settings.yaml" +# PBE_METADATA_DIR = rf"{DEFAULT_DRIVE}\ProgramData\Riot Games\Metadata\league_of_legends.pbe" +# PBE_METADATA_FILE = f"{PBE_METADATA_DIR}\\league_of_legends.pbe.product_settings.yaml" +METADATA_DIR_FORMAT = r"{drive}\ProgramData\Riot Games\Metadata\league_of_legends.{patch_line}" +METADATA_FILE_FORMAT = "league_of_legends.{patch_line}.product_settings.yaml" +DEFAULT_METADATA_DIR = METADATA_DIR_FORMAT.format(drive=DEFAULT_DRIVE, patch_line=DEFAULT_PATCH_LINE) +DEFAULT_METADATA_FILE = os.path.join(DEFAULT_METADATA_DIR, METADATA_FILE_FORMAT.format(patch_line=DEFAULT_PATCH_LINE)) LOCALE_CODES = { "zh_CN": "简体中文(国服)", @@ -79,14 +88,16 @@ ######################################################################################## class FileWatcher(FileSystemEventHandler): - def __init__(self, file_path, selected_locale, msg_callback_fn=None): - self.file_path = file_path + def __init__(self, *watching_files, selected_locale, msg_callback_fn=None): + self.watching_files = filter_existing_files(watching_files) + print(f"Watching files: {self.watching_files}") self.selected_locale = selected_locale self.msg_callback_fn = msg_callback_fn or print super().__init__() def on_modified(self, event): - if event.src_path == self.file_path: + print(f'event type: {event.event_type} path : {event.src_path}') + if normalize_file_path(event.src_path) in self.watching_files: print(f'File {event.src_path} has been modified') content = read_yaml(event.src_path) if not is_valid_settings(content): @@ -95,7 +106,6 @@ def on_modified(self, event): curr_locale = content['settings']['locale'] if curr_locale != self.selected_locale: self.msg_callback_fn(f'正在将语言 {curr_locale} 更新为 {self.selected_locale} ...') - # print(f'Updating locale from {curr_locale} to {self.selected_locale} ...') update_settings(event.src_path, self.selected_locale, self.msg_callback_fn) @@ -145,66 +155,6 @@ def enum_windows_proc(hwnd, lparam): return False # stop enumerating windows return True # continue enumerating windows - print("Bringing to front and clicking") - enum_windows_proc = ctypes.WINFUNCTYPE(ctypes.c_bool, wintypes.HWND, wintypes.LPARAM)(enum_windows_proc) - ctypes.windll.user32.EnumWindows(enum_windows_proc, 0) - - -def bring_to_front(pid): - """ - Bring the window of the process to the front and set focus - :param pid: the process ID - :return: True if the window is already in the front or successfully brought to the front and focus, False otherwise - """ - foreground_window = ctypes.windll.user32.GetForegroundWindow() - pid_foreground_window = wintypes.DWORD() - ctypes.windll.user32.GetWindowThreadProcessId(foreground_window, ctypes.byref(pid_foreground_window)) - if pid_foreground_window.value == pid: - # ctypes.windll.user32.SetFocus(foreground_window) - return True - - def enum_windows_proc(hwnd, lparam): - pid_window = wintypes.DWORD() - ctypes.windll.user32.GetWindowThreadProcessId(hwnd, ctypes.byref(pid_window)) - if pid_window.value == pid: - ctypes.windll.user32.SetForegroundWindow(hwnd) - ctypes.windll.user32.SetFocus(hwnd) - # Simulate Alt key press and release - ctypes.windll.user32.keybd_event(0x12, 0, 0, 0) # Alt key down - ctypes.windll.user32.keybd_event(0x12, 0, 2, 0) # Alt key up - return False # stop enumerating windows - return True # continue enumerating windows - - enum_windows_proc = ctypes.WINFUNCTYPE(ctypes.c_bool, wintypes.HWND, wintypes.LPARAM)(enum_windows_proc) - ctypes.windll.user32.EnumWindows(enum_windows_proc, 0) - - -def bring_to_front2(pid): - """ - Bring the window of the process to the front and set focus - :param pid: the process ID - :return: True if the window is already in the front or successfully brought to the front and focus, False otherwise - """ - foreground_window = ctypes.windll.user32.GetForegroundWindow() - pid_foreground_window = wintypes.DWORD() - ctypes.windll.user32.GetWindowThreadProcessId(foreground_window, ctypes.byref(pid_foreground_window)) - if pid_foreground_window.value == pid: - ctypes.windll.user32.SetFocus(foreground_window) - return True - - def enum_windows_proc(hwnd, lparam): - pid_window = wintypes.DWORD() - ctypes.windll.user32.GetWindowThreadProcessId(hwnd, ctypes.byref(pid_window)) - if pid_window.value == pid: - # Attach the input processing mechanism of the foreground window's thread to our thread - ctypes.windll.user32.AttachThreadInput(ctypes.windll.kernel32.GetCurrentThreadId(), pid_foreground_window.value, True) - ctypes.windll.user32.SetForegroundWindow(hwnd) - ctypes.windll.user32.SetFocus(hwnd) - # Detach the input processing mechanism after setting the focus - ctypes.windll.user32.AttachThreadInput(ctypes.windll.kernel32.GetCurrentThreadId(), pid_foreground_window.value, False) - return False # stop enumerating windows - return True # continue enumerating windows - enum_windows_proc = ctypes.WINFUNCTYPE(ctypes.c_bool, wintypes.HWND, wintypes.LPARAM)(enum_windows_proc) ctypes.windll.user32.EnumWindows(enum_windows_proc, 0) @@ -274,12 +224,19 @@ def check_for_updates(no_new_version_callback=None): no_new_version_callback() +def normalize_file_path(file_path): + file_path = os.path.abspath(file_path) + file_path = os.path.realpath(file_path) + file_path = os.path.normpath(file_path) + return file_path + + def filter_existing_files(file_paths): - return list(set(filter(os.path.exists, file_paths))) + return list(set(filter(os.path.exists, map(normalize_file_path, file_paths)))) -def filter_valid_metadata_files(filenames): - return list(filter(is_valid_metadata_file, filenames)) +def filter_valid_metadata_files(*file_paths): + return list(set(filter(is_valid_metadata_file, map(normalize_file_path, file_paths)))) def to_list(data): @@ -342,11 +299,12 @@ def open_metadata_file_dialog(title, file_types, initial_dir=DEFAULT_METADATA_DI def find_setting_files(): while True: - selected_file = open_metadata_file_dialog(title="请选择 league_of_legends.live.product_settings.yaml 文件", + selected_file = open_metadata_file_dialog(title="请选择 league_of_legends.[live|pbe].product_settings.yaml 文件", file_types=[('Riot 配置文件', '*.yaml'), ('所有文件', '*.*')], initial_dir=DEFAULT_METADATA_DIR, initial_file=DEFAULT_METADATA_FILE) - if selected_file: - return [selected_file] + selected_files = filter_valid_metadata_files(*to_list(selected_file)) + if len(selected_files) > 0: + return selected_files else: decision = easygui.buttonbox( "您要重新选择文件,还是退出?\n", @@ -373,44 +331,33 @@ def is_valid_metadata_file(filename): def detect_metadata_file(): print("Detecting metadata file...") setting_files = [] - filename_suffix = r"ProgramData\Riot Games\Metadata\league_of_legends.live\league_of_legends.live.product_settings.yaml" + + filename_suffixes = [os.path.join( + METADATA_DIR_FORMAT.format(drive="", patch_line=patch_line), + METADATA_FILE_FORMAT.format(patch_line=patch_line) + ) for patch_line in SUPPORTED_PATCH_LINEs] for drive in get_drives(): - filename = os.path.join(drive, filename_suffix) - if is_valid_metadata_file(filename): - print(f"Found metadata file: {filename}") - setting_files.append(filename) + for filename_suffix in filename_suffixes: + filename = os.path.join(drive, filename_suffix) + if is_valid_metadata_file(filename): + print(f"Found metadata file: {filename}") + setting_files.append(filename) return setting_files -def verify_metadata_file(config) -> str: - setting_files = filter_valid_metadata_files(to_list(config.get('SettingFile', []))) +def verify_metadata_file(config) -> [str]: + setting_files = detect_metadata_file() + setting_files = filter_valid_metadata_files(*to_list(config.get('SettingFile', [])), *setting_files) if not setting_files or len(setting_files) < 1: - print("开始尝试自动查找配置文件...") - setting_files = detect_metadata_file() - if not setting_files or len(setting_files) < 1: - decision = easygui.buttonbox("该文件通常位于:\n\n" - r"[系统盘]:\ProgramData\Riot Games\Metadata\league_of_legends.live" - r"\league_of_legends.live.product_settings.yaml", - "未找到配置文件,请手动选择", ["手动选择", "退出"]) - if decision.lower() == '手动选择': - setting_files = find_setting_files() - else: - sys.exit() - if len(setting_files) > 1: - msg = "请从以下选项中选择一个配置文件" - title = "找到多个配置文件,请选择一个" - choice = easygui.choicebox(msg, title, setting_files) - setting_files = [choice] - if not is_valid_metadata_file(setting_files[0]): - decision = easygui.buttonbox("该文件通常位于:" - r"[系统盘]:\ProgramData\Riot Games\Metadata\league_of_legends.live" - r"\league_of_legends.live.product_settings.yaml", - "无效的配置文件,请重新选择", ["重新选择", "退出"]) - if decision.lower() == '重新选择': + decision = easygui.buttonbox("该文件通常位于:\n\n" + rf"[系统盘]:{DEFAULT_METADATA_DIR}" + rf"{DEFAULT_METADATA_FILE}", + "未找到配置文件,请手动选择", ["手动选择", "退出"]) + if decision.lower() == '手动选择': setting_files = find_setting_files() else: sys.exit() - return setting_files[0] + return setting_files def is_read_only(file_path):