diff --git a/README.md b/README.md index 77f4a99..b5a372f 100644 --- a/README.md +++ b/README.md @@ -82,7 +82,7 @@ MAA多账号管理与自动化软件 ![Alt](https://repobeats.axiom.co/api/embed/6c2f834141eff1ac297db70d12bd11c6236a58a5.svg "Repobeats analytics image") -感谢 @ClozyA 为本项目提供的下载服务器 +感谢 [AoXuan (@ClozyA)](https://github.com/ClozyA) 为本项目提供的下载服务器 ## Star History diff --git a/app/core/config.py b/app/core/config.py index 02febe3..dd18d58 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -30,6 +30,9 @@ import sqlite3 import json import sys import shutil +import re +from datetime import datetime +from collections import defaultdict from pathlib import Path from qfluentwidgets import ( QConfig, @@ -42,6 +45,7 @@ from qfluentwidgets import ( OptionsValidator, qconfig, ) +from typing import Union, Dict, List, Tuple class AppConfig: @@ -54,7 +58,7 @@ class AppConfig: self.log_path = self.app_path / "debug/AUTO_MAA.log" self.database_path = self.app_path / "data/data.db" self.config_path = self.app_path / "config/config.json" - self.history_path = self.app_path / "config/history.json" + self.history_path = self.app_path / "history/main.json" self.key_path = self.app_path / "data/key" self.gameid_path = self.app_path / "data/gameid.txt" self.version_path = self.app_path / "resources/version.json" @@ -74,6 +78,7 @@ class AppConfig: (self.app_path / "config").mkdir(parents=True, exist_ok=True) (self.app_path / "data").mkdir(parents=True, exist_ok=True) (self.app_path / "debug").mkdir(parents=True, exist_ok=True) + (self.app_path / "history").mkdir(parents=True, exist_ok=True) # 生成版本信息文件 if not self.version_path.exists(): @@ -461,6 +466,224 @@ class AppConfig: cur.close() db.close() + def save_maa_log(self, log_path: Path, logs: list, maa_result: str) -> bool: + """保存MAA日志""" + + data: Dict[str, Union[str, Dict[str, Union[int, dict]]]] = { + "recruit_statistics": defaultdict(int), + "drop_statistics": defaultdict(dict), + "maa_result": maa_result, + } + + if_six_star = False + + # 公招统计(仅统计招募到的) + confirmed_recruit = False + current_star_level = None + i = 0 + while i < len(logs): + if "公招识别结果:" in logs[i]: + current_star_level = None # 每次识别公招时清空之前的星级 + i += 1 + while i < len(logs) and "Tags" not in logs[i]: # 读取所有公招标签 + i += 1 + + if i < len(logs) and "Tags" in logs[i]: # 识别星级 + star_match = re.search(r"(\d+)\s*★ Tags", logs[i]) + if star_match: + current_star_level = f"{star_match.group(1)}★" + if current_star_level == "6★": + if_six_star = True + + if "已确认招募" in logs[i]: # 只有确认招募后才统计 + confirmed_recruit = True + + if confirmed_recruit and current_star_level: + data["recruit_statistics"][current_star_level] += 1 + confirmed_recruit = False # 重置,等待下一次公招 + current_star_level = None # 清空已处理的星级 + + i += 1 + + # 掉落统计 + current_stage = None + stage_drops = {} + + for i, line in enumerate(logs): + drop_match = re.search(r"([A-Za-z0-9\-]+) 掉落统计:", line) + if drop_match: + # 发现新关卡,保存前一个关卡数据 + if current_stage and stage_drops: + data["drop_statistics"][current_stage] = stage_drops + + current_stage = drop_match.group(1) + if current_stage == "WE": + current_stage = "剿灭模式" + stage_drops = {} + continue + + if current_stage: + item_match: List[str] = re.findall( + r"^(?!\[)([\u4e00-\u9fa5A-Za-z0-9\-]+)\s*:\s*([\d,]+)(?:\s*\(\+[\d,]+\))?", + line, + re.M, + ) + for item, total in item_match: + # 解析数值时去掉逗号 (如 2,160 -> 2160) + total = int(total.replace(",", "")) + + # 黑名单 + if item not in [ + "当前次数", + "理智", + "最快截图耗时", + "专精等级", + "剩余时间", + ]: + stage_drops[item] = total + + # 处理最后一个关卡的掉落数据 + if current_stage and stage_drops: + data["drop_statistics"][current_stage] = stage_drops + + # 保存日志 + log_path.parent.mkdir(parents=True, exist_ok=True) + with log_path.open("w", encoding="utf-8") as f: + f.writelines(logs) + with log_path.with_suffix(".json").open("w", encoding="utf-8") as f: + json.dump(data, f, ensure_ascii=False, indent=4) + + logger.info(f"处理完成:{log_path}") + + self.merge_maa_logs("所有项", log_path.parent) + + return if_six_star + + def merge_maa_logs(self, mode: str, logs_path: Union[Path, List[Path]]) -> dict: + """合并指定数据统计信息文件""" + + data = { + "recruit_statistics": defaultdict(int), + "drop_statistics": defaultdict(dict), + "maa_result": defaultdict(str), + } + + if mode == "所有项": + logs_path_list = list(logs_path.glob("*.json")) + elif mode == "指定项": + logs_path_list = logs_path + + for json_file in logs_path_list: + + with json_file.open("r", encoding="utf-8") as f: + single_data: Dict[str, Union[str, Dict[str, Union[int, dict]]]] = ( + json.load(f) + ) + + # 合并公招统计 + for star_level, count in single_data["recruit_statistics"].items(): + data["recruit_statistics"][star_level] += count + + # 合并掉落统计 + for stage, drops in single_data["drop_statistics"].items(): + if stage not in data["drop_statistics"]: + data["drop_statistics"][stage] = {} # 初始化关卡 + + for item, count in drops.items(): + + if item in data["drop_statistics"][stage]: + data["drop_statistics"][stage][item] += count + else: + data["drop_statistics"][stage][item] = count + + # 合并MAA结果 + data["maa_result"][ + json_file.name.replace(".json", "").replace("-", ":") + ] = single_data["maa_result"] + + # 生成汇总 JSON 文件 + if mode == "所有项": + + with logs_path.with_suffix(".json").open("w", encoding="utf-8") as f: + json.dump(data, f, ensure_ascii=False, indent=4) + + logger.info(f"统计完成:{logs_path.with_suffix(".json")}") + + return data + + def load_maa_logs( + self, mode: str, json_path: Path + ) -> Dict[str, Union[str, list, Dict[str, list]]]: + """加载MAA日志统计信息""" + + if mode == "总览": + + with json_path.open("r", encoding="utf-8") as f: + info: Dict[str, Dict[str, Union[int, dict]]] = json.load(f) + + data = {} + data["条目索引"] = [ + [k, "完成" if v == "Success!" else "异常"] + for k, v in info["maa_result"].items() + ] + data["条目索引"].insert(0, ["数据总览", "运行"]) + data["统计数据"] = {"公招统计": list(info["recruit_statistics"].items())} + + for game_id, drops in info["drop_statistics"].items(): + data["统计数据"][f"掉落统计:{game_id}"] = list(drops.items()) + + data["统计数据"]["报错汇总"] = [ + [k, v] for k, v in info["maa_result"].items() if v != "Success!" + ] + + elif mode == "单项": + + with json_path.open("r", encoding="utf-8") as f: + info: Dict[str, Union[str, Dict[str, Union[int, dict]]]] = json.load(f) + + data = {} + + data["统计数据"] = {"公招统计": list(info["recruit_statistics"].items())} + + for game_id, drops in info["drop_statistics"].items(): + data["统计数据"][f"掉落统计:{game_id}"] = list(drops.items()) + + with json_path.with_suffix(".log").open("r", encoding="utf-8") as f: + log = f.read() + + data["日志信息"] = log + + return data + + def search_history(self) -> dict: + """搜索所有历史记录""" + + history_dict = {} + + for date_folder in (Config.app_path / "history").iterdir(): + if not date_folder.is_dir(): + continue # 只处理日期文件夹 + + try: + + date = datetime.strptime(date_folder.name, "%Y-%m-%d") + + history_dict[date.strftime("%Y年 %m月 %d日")] = list( + date_folder.glob("*.json") + ) + + except ValueError: + logger.warning(f"非日期格式的目录: {date_folder}") + + return { + k: v + for k, v in sorted( + history_dict.items(), + key=lambda x: datetime.strptime(x[0], "%Y年 %m月 %d日"), + reverse=True, + ) + } + def save_history(self, key: str, content: dict) -> None: """保存历史记录""" @@ -540,6 +763,15 @@ class AppConfig: class GlobalConfig(QConfig): """全局配置""" + function_HomeImageMode = OptionsConfigItem( + "Function", + "HomeImageMode", + "默认", + OptionsValidator(["默认", "自定义", "主题图像"]), + ) + function_HistoryRetentionTime = OptionsConfigItem( + "Function", "HistoryRetentionTime", 0, OptionsValidator([7, 15, 30, 60, 0]) + ) function_IfAllowSleep = ConfigItem( "Function", "IfAllowSleep", False, BoolValidator() ) @@ -551,6 +783,9 @@ class GlobalConfig(QConfig): start_IfSelfStart = ConfigItem("Start", "IfSelfStart", False, BoolValidator()) start_IfRunDirectly = ConfigItem("Start", "IfRunDirectly", False, BoolValidator()) + start_IfMinimizeDirectly = ConfigItem( + "Start", "IfMinimizeDirectly", False, BoolValidator() + ) ui_IfShowTray = ConfigItem("UI", "IfShowTray", False, BoolValidator()) ui_IfToTray = ConfigItem("UI", "IfToTray", False, BoolValidator()) @@ -558,11 +793,18 @@ class GlobalConfig(QConfig): ui_location = ConfigItem("UI", "location", "100x100") ui_maximized = ConfigItem("UI", "maximized", False, BoolValidator()) + notify_SendTaskResultTime = OptionsConfigItem( + "Notify", + "SendTaskResultTime", + "不推送", + OptionsValidator(["不推送", "任何时刻", "仅失败时"]), + ) + notify_IfSendStatistic = ConfigItem( + "Notify", "IfSendStatistic", False, BoolValidator() + ) + notify_IfSendSixStar = ConfigItem("Notify", "IfSendSixStar", False, BoolValidator()) notify_IfPushPlyer = ConfigItem("Notify", "IfPushPlyer", False, BoolValidator()) notify_IfSendMail = ConfigItem("Notify", "IfSendMail", False, BoolValidator()) - notify_IfSendErrorOnly = ConfigItem( - "Notify", "IfSendErrorOnly", False, BoolValidator() - ) notify_SMTPServerAddress = ConfigItem("Notify", "SMTPServerAddress", "") notify_AuthorizationCode = ConfigItem("Notify", "AuthorizationCode", "") notify_FromAddress = ConfigItem("Notify", "FromAddress", "") diff --git a/app/core/timer.py b/app/core/timer.py index 97357b2..449ceba 100644 --- a/app/core/timer.py +++ b/app/core/timer.py @@ -39,10 +39,7 @@ from app.services import System class _MainTimer(QWidget): - def __init__( - self, - parent=None, - ): + def __init__(self, parent=None): super().__init__(parent) self.if_FailSafeException = False diff --git a/app/models/MAA.py b/app/models/MAA.py index 0b65cae..28938d7 100644 --- a/app/models/MAA.py +++ b/app/models/MAA.py @@ -34,7 +34,8 @@ import subprocess import shutil import time from pathlib import Path -from typing import List +from jinja2 import Environment, FileSystemLoader +from typing import Union, List from app.core import Config from app.services import Notify, System @@ -176,6 +177,9 @@ class MaaManager(QObject): [True, "routine", "日常"], ] + user_logs_list = [] + user_start_time = datetime.now() + # 尝试次数循环 for i in range(self.set["RunSet"]["RunTimesLimit"]): @@ -273,7 +277,7 @@ class MaaManager(QObject): System.kill_process(self.maa_exe_path) self.if_open_emulator = True # 推送异常通知 - Notify.push_notification( + Notify.push_plyer( "用户自动代理出现异常!", f"用户 {user[0].replace("_", " 今天的")}的{mode_book[j][5:7]}部分出现一次异常", f"{user[0].replace("_", " ")}的{mode_book[j][5:7]}出现异常", @@ -287,13 +291,38 @@ class MaaManager(QObject): # 移除静默进程标记 Config.silence_list.remove(self.emulator_path) + # 保存运行日志以及统计信息 + if_six_star = Config.save_maa_log( + Config.app_path + / f"history/{curdate}/{self.data[user[2]][0]}/{start_time.strftime("%H-%M-%S")}.log", + self.check_maa_log(start_time, mode_book[j]), + self.maa_result, + ) + user_logs_list.append( + Config.app_path + / f"history/{curdate}/{self.data[user[2]][0]}/{start_time.strftime("%H-%M-%S")}.json", + ) + + if ( + Config.global_config.get( + Config.global_config.notify_IfSendSixStar + ) + and if_six_star + ): + + self.push_notification( + "公招六星", + f"喜报:用户 {user[0]} 公招出六星啦!", + {"user_name": self.data[user[2]][0]}, + ) + # 成功完成代理的用户修改相关参数 if run_book[0] and run_book[1]: if self.data[user[2]][14] == 0 and self.data[user[2]][3] != -1: self.data[user[2]][3] -= 1 self.data[user[2]][14] += 1 user[1] = "完成" - Notify.push_notification( + Notify.push_plyer( "成功完成一个自动代理任务!", f"已完成用户 {user[0].replace("_", " 今天的")}任务", f"已完成 {user[0].replace("_", " 的")}", @@ -301,6 +330,27 @@ class MaaManager(QObject): ) break + if Config.global_config.get( + Config.global_config.notify_IfSendStatistic + ): + + statistics = Config.merge_maa_logs("指定项", user_logs_list) + statistics["user_info"] = user[0] + statistics["start_time"] = user_start_time.strftime( + "%Y-%m-%d %H:%M:%S" + ) + statistics["end_time"] = datetime.now().strftime( + "%Y-%m-%d %H:%M:%S" + ) + statistics["maa_result"] = ( + "代理任务全部完成" + if (run_book[0] and run_book[1]) + else "代理任务未全部完成" + ) + self.push_notification( + "统计信息", f"用户 {user[0]} 的自动代理统计报告", statistics + ) + # 录入代理失败的用户 if not (run_book[0] and run_book[1]): user[1] = "异常" @@ -433,7 +483,7 @@ class MaaManager(QObject): self.user_config_path.mkdir(parents=True, exist_ok=True) shutil.copy(self.maa_set_path, self.user_config_path) - end_log = "" + result_text = "" # 导出结果 if self.mode in ["自动代理", "人工排查"]: @@ -456,52 +506,49 @@ class MaaManager(QObject): wait_index = [_[2] for _ in self.user_list if _[1] == "等待"] # 保存运行日志 - end_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") - end_log = ( - f"任务开始时间:{begin_time},结束时间:{end_time}\n" - f"已完成数:{len(over_index)},未完成数:{len(error_index) + len(wait_index)}\n\n" - ) - - if len(error_index) != 0: - end_log += ( - f"{self.mode[2:4]}未成功的用户:\n" - f"{"\n".join([self.data[_][0] for _ in error_index])}\n" - ) - if len(wait_index) != 0: - end_log += ( - f"\n未开始{self.mode[2:4]}的用户:\n" - f"{"\n".join([self.data[_][0] for _ in wait_index])}\n" - ) - title = ( f"{self.set["MaaSet"]["Name"]}的{self.mode[:4]}任务报告" if self.set["MaaSet"]["Name"] != "" else f"{self.mode[:4]}任务报告" ) + result = { + "title": f"{self.mode[:4]}任务报告", + "script_name": ( + self.set["MaaSet"]["Name"] + if self.set["MaaSet"]["Name"] != "" + else "空白" + ), + "start_time": begin_time, + "end_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), + "completed_count": len(over_index), + "uncompleted_count": len(error_index) + len(wait_index), + "failed_user": [self.data[_][0] for _ in error_index], + "waiting_user": [self.data[_][0] for _ in wait_index], + } # 推送代理结果通知 - Notify.push_notification( + Notify.push_plyer( title.replace("报告", "已完成!"), f"已完成用户数:{len(over_index)},未完成用户数:{len(error_index) + len(wait_index)}", f"已完成用户数:{len(over_index)},未完成用户数:{len(error_index) + len(wait_index)}", 10, ) - if not Config.global_config.get( - Config.global_config.notify_IfSendErrorOnly - ) or ( - Config.global_config.get(Config.global_config.notify_IfSendErrorOnly) + if Config.global_config.get( + Config.global_config.notify_SendTaskResultTime + ) == "任何时刻" or ( + Config.global_config.get(Config.global_config.notify_SendTaskResultTime) + == "仅失败时" and len(error_index) + len(wait_index) != 0 ): - Notify.send_mail( - title, - f"{end_log}\n\nAUTO_MAA 敬上\n\n我们根据您在 AUTO_MAA 中的设置发送了这封电子邮件,本邮件无需回复\n", + result_text = self.push_notification("代理结果", title, result) + else: + result_text = self.push_notification( + "代理结果", title, result, if_get_text_only=True ) - Notify.ServerChanPush(title, f"{end_log}\n\nAUTO_MAA 敬上") - Notify.CompanyWebHookBotPush(title, f"{end_log}AUTO_MAA 敬上") self.agree_bilibili(False) self.log_monitor.deleteLater() self.log_monitor_timer.deleteLater() - self.accomplish.emit({"Time": begin_time, "History": end_log}) + self.accomplish.emit({"Time": begin_time, "History": result_text}) def requestInterruption(self) -> None: logger.info(f"{self.name} | 收到任务中止申请") @@ -509,7 +556,7 @@ class MaaManager(QObject): if len(self.log_monitor.files()) != 0: self.interrupt.emit() - self.maa_result = "您中止了本次任务" + self.maa_result = "任务被手动中止" self.isInterruptionRequested = True def push_question(self, title: str, message: str) -> bool: @@ -530,8 +577,8 @@ class MaaManager(QObject): with self.maa_log_path.open(mode="r", encoding="utf-8") as f: pass - def check_maa_log(self, start_time: datetime, mode: str) -> None: - """检查MAA日志以判断MAA程序运行状态""" + def check_maa_log(self, start_time: datetime, mode: str) -> list: + """获取MAA日志并检查以判断MAA程序运行状态""" # 获取日志 logs = [] @@ -580,6 +627,7 @@ class MaaManager(QObject): self.maa_result = "Success!" elif ( ("请「检查连接设置」或「尝试重启模拟器与 ADB」或「重启电脑」" in log) + or ("未检测到任何模拟器" in log) or ("已停止" in log) or ("MaaAssistantArknights GUI exited" in log) ): @@ -588,6 +636,8 @@ class MaaManager(QObject): minutes=self.set["RunSet"][time_book[mode]] ): self.maa_result = "检测到MAA进程超时" + elif self.isInterruptionRequested: + self.maa_result = "任务被手动中止" else: self.maa_result = "Wait" @@ -596,10 +646,13 @@ class MaaManager(QObject): self.maa_result = "Success!" elif ( ("请「检查连接设置」或「尝试重启模拟器与 ADB」或「重启电脑」" in log) + or ("未检测到任何模拟器" in log) or ("已停止" in log) or ("MaaAssistantArknights GUI exited" in log) ): self.maa_result = "检测到MAA进程异常" + elif self.isInterruptionRequested: + self.maa_result = "任务被手动中止" else: self.maa_result = "Wait" @@ -613,6 +666,8 @@ class MaaManager(QObject): self.quit_monitor() + return logs + def start_monitor(self, start_time: datetime, mode: str) -> None: """开始监视MAA日志""" @@ -734,13 +789,13 @@ class MaaManager(QObject): data["Global"][ "VersionUpdate.ScheduledUpdateCheck" - ] = "True" # 定时检查更新 + ] = "False" # 定时检查更新 data["Global"][ "VersionUpdate.AutoDownloadUpdatePackage" - ] = "True" # 自动下载更新包 + ] = "False" # 自动下载更新包 data["Global"][ "VersionUpdate.AutoInstallUpdatePackage" - ] = "True" # 自动安装更新包 + ] = "False" # 自动安装更新包 data["Configurations"]["Default"]["Start.ClientType"] = self.data[ index ][ @@ -1118,3 +1173,86 @@ class MaaManager(QObject): if dt.time() < datetime.min.time().replace(hour=4): dt = dt - timedelta(days=1) return dt.strftime("%Y-%m-%d") + + def push_notification( + self, + mode: str, + title: str, + message: Union[str, dict], + if_get_text_only: bool = False, + ) -> str: + """通过所有渠道推送通知""" + + env = Environment( + loader=FileSystemLoader(str(Config.app_path / "resources/html")) + ) + + if mode == "代理结果": + + # 生成文本通知内容 + message_text = ( + f"任务开始时间:{message["start_time"]},结束时间:{message["end_time"]}\n" + f"已完成数:{message["completed_count"]},未完成数:{message["uncompleted_count"]}\n\n" + ) + + if len(message["failed_user"]) > 0: + message_text += f"{self.mode[2:4]}未成功的用户:\n{"\n".join(message["failed_user"])}\n" + if len(message["waiting_user"]) > 0: + message_text += f"\n未开始{self.mode[2:4]}的用户:\n{"\n".join(message["waiting_user"])}\n" + + if if_get_text_only: + return message_text + + # 生成HTML通知内容 + message["failed_user"] = "、".join(message["failed_user"]) + message["waiting_user"] = "、".join(message["waiting_user"]) + + template = env.get_template("MAA_result.html") + message_html = template.render(message) + + Notify.send_mail("网页", title, message_html) + Notify.ServerChanPush(title, f"{message_text}\n\nAUTO_MAA 敬上") + Notify.CompanyWebHookBotPush(title, f"{message_text}\n\nAUTO_MAA 敬上") + + return message_text + + elif mode == "统计信息": + + # 生成文本通知内容 + formatted = [] + for stage, items in message["drop_statistics"].items(): + formatted.append(f"掉落统计({stage}):") + for item, quantity in items.items(): + formatted.append(f" {item}: {quantity}") + drop_text = "\n".join(formatted) + + formatted = ["招募统计:"] + for star, count in message["recruit_statistics"].items(): + formatted.append(f" {star}: {count}") + recruit_text = "\n".join(formatted) + + message_text = ( + f"开始时间: {message['start_time']}\n" + f"结束时间: {message['end_time']}\n" + f"MAA执行结果: {message['maa_result']}\n\n" + f"{recruit_text}\n" + f"{drop_text}" + ) + + # 生成HTML通知内容 + template = env.get_template("MAA_statistics.html") + message_html = template.render(message) + + Notify.send_mail("网页", title, message_html) + Notify.ServerChanPush(title, f"{message_text}\n\nAUTO_MAA 敬上") + Notify.CompanyWebHookBotPush(title, f"{message_text}\n\nAUTO_MAA 敬上") + + elif mode == "公招六星": + + # 生成HTML通知内容 + template = env.get_template("MAA_six_star.html") + message_html = template.render(message) + + Notify.send_mail("网页", title, message_html) + Notify.ServerChanPush(title, "好羡慕~\n\nAUTO_MAA 敬上") + Notify.CompanyWebHookBotPush(title, "好羡慕~\n\nAUTO_MAA 敬上") diff --git a/app/services/notification.py b/app/services/notification.py index 4f21411..a8eeda1 100644 --- a/app/services/notification.py +++ b/app/services/notification.py @@ -24,23 +24,33 @@ AUTO_MAA通知服务 v4.2 作者:DLmaster_361 """ + +from PySide6.QtWidgets import QWidget +from PySide6.QtCore import Signal import requests from loguru import logger from plyer import notification +import re import smtplib from email.mime.text import MIMEText +from email.mime.multipart import MIMEMultipart from email.header import Header from email.utils import formataddr from serverchan_sdk import sc_send -from app.core import Config, MainInfoBar +from app.core import Config from app.services.security import Crypto -class Notification: +class Notification(QWidget): - def push_notification(self, title, message, ticker, t): + push_info_bar = Signal(str, str, str, int) + + def __init__(self, parent=None): + super().__init__(parent) + + def push_plyer(self, title, message, ticker, t): """推送系统通知""" if Config.global_config.get(Config.global_config.notify_IfPushPlyer): @@ -57,14 +67,50 @@ class Notification: return True - def send_mail(self, title, content): + def send_mail(self, mode, title, content) -> None: """推送邮件通知""" if Config.global_config.get(Config.global_config.notify_IfSendMail): + if ( + Config.global_config.get(Config.global_config.notify_SMTPServerAddress) + == "" + or Config.global_config.get( + Config.global_config.notify_AuthorizationCode + ) + == "" + or not bool( + re.match( + r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$", + Config.global_config.get( + Config.global_config.notify_FromAddress + ), + ) + ) + or not bool( + re.match( + r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$", + Config.global_config.get(Config.global_config.notify_ToAddress), + ) + ) + ): + logger.error( + "请正确设置邮件通知的SMTP服务器地址、授权码、发件人地址和收件人地址" + ) + self.push_info_bar.emit( + "error", + "邮件通知推送异常", + "请正确设置邮件通知的SMTP服务器地址、授权码、发件人地址和收件人地址", + -1, + ) + return None + try: # 定义邮件正文 - message = MIMEText(content, "plain", "utf-8") + if mode == "文本": + message = MIMEText(content, "plain", "utf-8") + elif mode == "网页": + message = MIMEMultipart("alternative") message["From"] = formataddr( ( Header("AUTO_MAA通知服务", "utf-8").encode(), @@ -81,6 +127,9 @@ class Notification: ) # 收件人显示的名字 message["Subject"] = Header(title, "utf-8") + if mode == "网页": + message.attach(MIMEText(content, "html", "utf-8")) + smtpObj = smtplib.SMTP_SSL( Config.global_config.get( Config.global_config.notify_SMTPServerAddress @@ -104,7 +153,7 @@ class Notification: logger.success("邮件发送成功") except Exception as e: logger.error(f"发送邮件时出错:\n{e}") - MainInfoBar.push_info_bar("error", "发送邮件时出错", f"{e}", -1) + self.push_info_bar.emit("error", "发送邮件时出错", f"{e}", -1) def ServerChanPush(self, title, content): """使用Server酱推送通知""" @@ -133,7 +182,7 @@ class Notification: else: option["tags"] = "" logger.warning("请正确设置Auto_MAA中ServerChan的Tag。") - MainInfoBar.push_info_bar( + self.push_info_bar.emit( "warning", "Server酱通知推送异常", "请正确设置Auto_MAA中ServerChan的Tag。", @@ -145,7 +194,7 @@ class Notification: else: option["channel"] = "" logger.warning("请正确设置Auto_MAA中ServerChan的Channel。") - MainInfoBar.push_info_bar( + self.push_info_bar.emit( "warning", "Server酱通知推送异常", "请正确设置Auto_MAA中ServerChan的Channel。", @@ -159,7 +208,7 @@ class Notification: else: logger.info("Server酱推送通知失败") logger.error(response) - MainInfoBar.push_info_bar( + self.push_info_bar.emit( "error", "Server酱通知推送失败", f'使用Server酱推送通知时出错:\n{response["data"]['error']}', @@ -184,7 +233,7 @@ class Notification: else: logger.info("企业微信群机器人推送通知失败") logger.error(response.json()) - MainInfoBar.push_info_bar( + self.push_info_bar.emit( "error", "企业微信群机器人通知推送失败", f'使用企业微信群机器人推送通知时出错:\n{response.json()["errmsg"]}', diff --git a/app/ui/Widget.py b/app/ui/Widget.py index 6ac1d6b..1fe9610 100644 --- a/app/ui/Widget.py +++ b/app/ui/Widget.py @@ -25,9 +25,9 @@ v4.2 作者:DLmaster_361 """ -from PySide6.QtCore import Qt, QTime -from PySide6.QtGui import QIcon from PySide6.QtWidgets import QWidget, QHBoxLayout +from PySide6.QtCore import Qt, QTime, QEvent +from PySide6.QtGui import QIcon, QPixmap, QPainter, QPainterPath from qfluentwidgets import ( LineEdit, PasswordLineEdit, @@ -39,12 +39,21 @@ from qfluentwidgets import ( Signal, ComboBox, CheckBox, + IconWidget, + FluentIcon, + CardWidget, + BodyLabel, qconfig, ConfigItem, TimeEdit, OptionsConfigItem, + TeachingTip, + TransparentToolButton, + TeachingTipTailPosition, ) -from typing import Union, List +from qfluentwidgets.common.overload import singledispatchmethod +import os +from typing import Optional, Union, List from app.services import Crypto @@ -319,3 +328,207 @@ class TimeEditSettingCard(SettingCard): qconfig.set(self.configItem_time, value) self.TimeEdit.setTime(QTime.fromString(value, "HH:mm")) + + +class StatefulItemCard(CardWidget): + + def __init__(self, item: list, parent=None): + super().__init__(parent) + + self.Layout = QHBoxLayout(self) + + self.Label = BodyLabel(item[0], self) + self.icon = IconWidget(FluentIcon.MORE, self) + self.icon.setFixedSize(16, 16) + self.update_status(item[1]) + + self.Layout.addWidget(self.icon) + self.Layout.addWidget(self.Label) + self.Layout.addStretch(1) + + def update_status(self, status: str): + + if status == "完成": + self.icon.setIcon(FluentIcon.ACCEPT) + self.Label.setTextColor("#0eb840", "#0eb840") + elif status == "等待": + self.icon.setIcon(FluentIcon.MORE) + self.Label.setTextColor("#161823", "#e3f9fd") + elif status == "运行": + self.icon.setIcon(FluentIcon.PLAY) + self.Label.setTextColor("#177cb0", "#70f3ff") + elif status == "跳过": + self.icon.setIcon(FluentIcon.REMOVE) + self.Label.setTextColor("#75878a", "#7397ab") + elif status == "异常": + self.icon.setIcon(FluentIcon.CLOSE) + self.Label.setTextColor("#ff2121", "#ff2121") + + +class QuantifiedItemCard(CardWidget): + + def __init__(self, item: list, parent=None): + super().__init__(parent) + + self.Layout = QHBoxLayout(self) + + self.Name = BodyLabel(item[0], self) + self.Numb = BodyLabel(str(item[1]), self) + + self.Layout.addWidget(self.Name) + self.Layout.addStretch(1) + self.Layout.addWidget(self.Numb) + + +class IconButton(TransparentToolButton): + """包含下拉框的自定义设置卡片类。""" + + @singledispatchmethod + def __init__(self, parent: QWidget = None): + TransparentToolButton.__init__(self, parent) + + self._tooltip: Optional[TeachingTip] = None + + @__init__.register + def _(self, icon: Union[str, QIcon, FluentIconBase], parent: QWidget = None): + self.__init__(parent) + self.setIcon(icon) + + @__init__.register + def _( + self, + icon: Union[str, QIcon, FluentIconBase], + isTooltip: bool, + tip_title: str, + tip_content: str, + parent: QWidget = None, + ): + self.__init__(parent) + self.setIcon(icon) + + # 处理工具提示 + if isTooltip: + self.installEventFilter(self) + + self.tip_title: str = tip_title + self.tip_content: str = tip_content + + def eventFilter(self, obj, event: QEvent) -> bool: + """处理鼠标事件。""" + if event.type() == QEvent.Type.Enter: + self._show_tooltip() + elif event.type() == QEvent.Type.Leave: + self._hide_tooltip() + return super().eventFilter(obj, event) + + def _show_tooltip(self) -> None: + """显示工具提示。""" + self._tooltip = TeachingTip.create( + target=self, + title=self.tip_title, + content=self.tip_content, + tailPosition=TeachingTipTailPosition.RIGHT, + isClosable=False, + duration=-1, + parent=self, + ) + # 设置偏移 + if self._tooltip: + tooltip_pos = self.mapToGlobal(self.rect().topRight()) + + tooltip_pos.setX( + tooltip_pos.x() - self._tooltip.size().width() - 40 + ) # 水平偏移 + tooltip_pos.setY( + tooltip_pos.y() - self._tooltip.size().height() / 2 + 35 + ) # 垂直偏移 + + self._tooltip.move(tooltip_pos) + + def _hide_tooltip(self) -> None: + """隐藏工具提示。""" + if self._tooltip: + self._tooltip.close() + self._tooltip = None + + def __hash__(self): + return id(self) + + def __eq__(self, other): + if not isinstance(other, self.__class__): + return NotImplemented + return self is other + + +class Banner(QWidget): + """展示带有圆角的固定大小横幅小部件""" + + def __init__(self, image_path: str = None, parent=None): + QWidget.__init__(self, parent) + self.image_path = None + self.banner_image = None + self.scaled_image = None + + if image_path: + self.set_banner_image(image_path) + + def set_banner_image(self, image_path: str): + """设置横幅图片""" + self.image_path = image_path + self.banner_image = self.load_banner_image(image_path) + self.update_scaled_image() + + def load_banner_image(self, image_path: str) -> QPixmap: + """加载横幅图片,或创建渐变备用图片""" + if os.path.isfile(image_path): + return QPixmap(image_path) + return self._create_fallback_image() + + def _create_fallback_image(self): + """创建渐变备用图片""" + fallback_image = QPixmap(2560, 1280) # 使用原始图片的大小 + fallback_image.fill(Qt.GlobalColor.gray) + return fallback_image + + def update_scaled_image(self): + """按高度缩放图片,宽度保持比例,超出裁剪""" + if self.banner_image: + self.scaled_image = self.banner_image.scaled( + self.size(), + Qt.AspectRatioMode.KeepAspectRatioByExpanding, + Qt.TransformationMode.SmoothTransformation, + ) + self.update() + + def paintEvent(self, event): + """重载 paintEvent 以绘制缩放后的图片""" + if self.scaled_image: + painter = QPainter(self) + painter.setRenderHint(QPainter.RenderHint.Antialiasing) + painter.setRenderHint(QPainter.RenderHint.SmoothPixmapTransform) + + # 创建圆角路径 + path = QPainterPath() + path.addRoundedRect(self.rect(), 20, 20) + painter.setClipPath(path) + + # 计算绘制位置,使图片居中 + x = (self.width() - self.scaled_image.width()) // 2 + y = (self.height() - self.scaled_image.height()) // 2 + + # 绘制缩放后的图片 + painter.drawPixmap(x, y, self.scaled_image) + + def resizeEvent(self, event): + """重载 resizeEvent 以更新缩放后的图片""" + self.update_scaled_image() + QWidget.resizeEvent(self, event) + + def set_percentage_size(self, width_percentage, height_percentage): + """设置 Banner 的大小为窗口大小的百分比""" + parent = self.parentWidget() + if parent: + new_width = int(parent.width() * width_percentage) + new_height = int(parent.height() * height_percentage) + self.setFixedSize(new_width, new_height) + self.update_scaled_image() diff --git a/app/ui/dispatch_center.py b/app/ui/dispatch_center.py index fd9b5ec..8810568 100644 --- a/app/ui/dispatch_center.py +++ b/app/ui/dispatch_center.py @@ -34,8 +34,6 @@ from PySide6.QtWidgets import ( ) from qfluentwidgets import ( CardWidget, - IconWidget, - BodyLabel, Pivot, ScrollArea, FluentIcon, @@ -53,6 +51,7 @@ import json from app.core import Config, TaskManager, Task, MainInfoBar +from .Widget import StatefulItemCard class DispatchCenter(QWidget): @@ -335,7 +334,7 @@ class DispatchBox(QWidget): self.viewLayout.addLayout(self.Layout) self.viewLayout.setContentsMargins(3, 0, 3, 3) - self.task_cards: List[ItemCard] = [] + self.task_cards: List[StatefulItemCard] = [] def create_task(self, task_list: list): """创建任务队列""" @@ -351,7 +350,7 @@ class DispatchBox(QWidget): for task in task_list: - self.task_cards.append(ItemCard(task)) + self.task_cards.append(StatefulItemCard(task)) self.Layout.addWidget(self.task_cards[-1]) self.Layout.addStretch(1) @@ -373,7 +372,7 @@ class DispatchBox(QWidget): self.viewLayout.addLayout(self.Layout) self.viewLayout.setContentsMargins(3, 0, 3, 3) - self.user_cards: List[ItemCard] = [] + self.user_cards: List[StatefulItemCard] = [] def create_user(self, user_list: list): """创建用户队列""" @@ -389,7 +388,7 @@ class DispatchBox(QWidget): for user in user_list: - self.user_cards.append(ItemCard(user)) + self.user_cards.append(StatefulItemCard(user)) self.Layout.addWidget(self.user_cards[-1]) self.Layout.addStretch(1) @@ -419,38 +418,3 @@ class DispatchBox(QWidget): self.text.moveCursor(QTextCursor.End) self.text.ensureCursorVisible() - - -class ItemCard(CardWidget): - - def __init__(self, task_item: list, parent=None): - super().__init__(parent) - - self.Layout = QHBoxLayout(self) - - self.Label = BodyLabel(task_item[0], self) - self.icon = IconWidget(FluentIcon.MORE, self) - self.icon.setFixedSize(16, 16) - self.update_status(task_item[1]) - - self.Layout.addWidget(self.icon) - self.Layout.addWidget(self.Label) - self.Layout.addStretch(1) - - def update_status(self, status: str): - - if status == "完成": - self.icon.setIcon(FluentIcon.ACCEPT) - self.Label.setTextColor("#0eb840", "#0eb840") - elif status == "等待": - self.icon.setIcon(FluentIcon.MORE) - self.Label.setTextColor("#7397ab", "#7397ab") - elif status == "运行": - self.icon.setIcon(FluentIcon.PLAY) - self.Label.setTextColor("#2e4e7e", "#2e4e7e") - elif status == "跳过": - self.icon.setIcon(FluentIcon.REMOVE) - self.Label.setTextColor("#606060", "#d2d2d2") - elif status == "异常": - self.icon.setIcon(FluentIcon.CLOSE) - self.Label.setTextColor("#ff2121", "#ff2121") diff --git a/app/ui/history.py b/app/ui/history.py new file mode 100644 index 0000000..49d4d16 --- /dev/null +++ b/app/ui/history.py @@ -0,0 +1,258 @@ +# +# Copyright © <2024> + +# This file is part of AUTO_MAA. + +# AUTO_MAA is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published +# by the Free Software Foundation, either version 3 of the License, +# or (at your option) any later version. + +# AUTO_MAA is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty +# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See +# the GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with AUTO_MAA. If not, see . + +# DLmaster_361@163.com + +""" +AUTO_MAA +AUTO_MAA历史记录界面 +v4.2 +作者:DLmaster_361 +""" + +from loguru import logger +from PySide6.QtWidgets import ( + QWidget, + QVBoxLayout, + QHBoxLayout, +) +from qfluentwidgets import ( + ScrollArea, + FluentIcon, + HeaderCardWidget, + PushButton, + ExpandGroupSettingCard, + TextBrowser, +) +from PySide6.QtCore import Signal +import os +from functools import partial +from pathlib import Path +from typing import List + + +from app.core import Config +from .Widget import StatefulItemCard, QuantifiedItemCard + + +class History(QWidget): + + def __init__(self, parent=None): + super().__init__(parent) + self.setObjectName("历史记录") + + content_widget = QWidget() + self.content_layout = QVBoxLayout(content_widget) + + scrollArea = ScrollArea() + scrollArea.setWidgetResizable(True) + scrollArea.setWidget(content_widget) + layout = QVBoxLayout() + layout.addWidget(scrollArea) + self.setLayout(layout) + + self.history_card_list = [] + + self.refresh() + + def refresh(self): + """刷新脚本实例界面""" + + while self.content_layout.count() > 0: + item = self.content_layout.takeAt(0) + if item.spacerItem(): + self.content_layout.removeItem(item.spacerItem()) + elif item.widget(): + item.widget().deleteLater() + + self.history_card_list = [] + + history_dict = Config.search_history() + + for date, user_list in history_dict.items(): + + self.history_card_list.append(HistoryCard(date, user_list, self)) + self.content_layout.addWidget(self.history_card_list[-1]) + + self.content_layout.addStretch(1) + + +class HistoryCard(ExpandGroupSettingCard): + + def __init__(self, date: str, user_list: List[Path], parent=None): + super().__init__( + FluentIcon.HISTORY, date, f"{date}的历史运行记录与统计信息", parent + ) + + widget = QWidget() + Layout = QVBoxLayout(widget) + self.viewLayout.setContentsMargins(0, 0, 0, 0) + self.viewLayout.setSpacing(0) + self.addGroupWidget(widget) + + self.user_history_card_list = [] + + for user_path in user_list: + + self.user_history_card_list.append(self.UserHistoryCard(user_path, self)) + Layout.addWidget(self.user_history_card_list[-1]) + + class UserHistoryCard(HeaderCardWidget): + + def __init__( + self, + user_history_path: Path, + parent=None, + ): + super().__init__(parent) + + self.setTitle(user_history_path.name.replace(".json", "")) + + self.user_history_path = user_history_path + self.main_history = Config.load_maa_logs("总览", user_history_path) + + self.index_card = self.IndexCard(self.main_history["条目索引"], self) + self.statistics_card = QHBoxLayout() + self.log_card = self.LogCard(self) + + self.index_card.index_changed.connect(self.update_info) + + self.viewLayout.addWidget(self.index_card) + self.viewLayout.addLayout(self.statistics_card) + self.viewLayout.addWidget(self.log_card) + self.viewLayout.setContentsMargins(0, 0, 0, 0) + self.viewLayout.setSpacing(0) + self.viewLayout.setStretch(0, 1) + self.viewLayout.setStretch(2, 4) + + self.update_info("数据总览") + + def update_info(self, index: str) -> None: + """更新信息""" + + if index == "数据总览": + + while self.statistics_card.count() > 0: + item = self.statistics_card.takeAt(0) + if item.spacerItem(): + self.statistics_card.removeItem(item.spacerItem()) + elif item.widget(): + item.widget().deleteLater() + + for name, item_list in self.main_history["统计数据"].items(): + + statistics_card = self.StatisticsCard(name, item_list, self) + self.statistics_card.addWidget(statistics_card) + + self.log_card.hide() + + else: + + single_history = Config.load_maa_logs( + "单项", + self.user_history_path.with_suffix("") + / f"{index.replace(":","-")}.json", + ) + + while self.statistics_card.count() > 0: + item = self.statistics_card.takeAt(0) + if item.spacerItem(): + self.statistics_card.removeItem(item.spacerItem()) + elif item.widget(): + item.widget().deleteLater() + + for name, item_list in single_history["统计数据"].items(): + + statistics_card = self.StatisticsCard(name, item_list, self) + self.statistics_card.addWidget(statistics_card) + + self.log_card.text.setText(single_history["日志信息"]) + self.log_card.button.clicked.disconnect() + self.log_card.button.clicked.connect( + lambda: os.startfile( + self.user_history_path.with_suffix("") + / f"{index.replace(":","-")}.log" + ) + ) + self.log_card.show() + + self.viewLayout.setStretch(1, self.statistics_card.count()) + + self.setMinimumHeight(300) + + class IndexCard(HeaderCardWidget): + + index_changed = Signal(str) + + def __init__(self, index_list: list, parent=None): + super().__init__(parent) + self.setTitle("记录条目") + + self.Layout = QVBoxLayout() + self.viewLayout.addLayout(self.Layout) + self.viewLayout.setContentsMargins(3, 0, 3, 3) + + self.index_cards: List[StatefulItemCard] = [] + + for index in index_list: + + self.index_cards.append(StatefulItemCard(index)) + self.index_cards[-1].clicked.connect( + partial(self.index_changed.emit, index[0]) + ) + self.Layout.addWidget(self.index_cards[-1]) + + self.Layout.addStretch(1) + + class StatisticsCard(HeaderCardWidget): + + def __init__(self, name: str, item_list: list, parent=None): + super().__init__(parent) + self.setTitle(name) + + self.Layout = QVBoxLayout() + self.viewLayout.addLayout(self.Layout) + self.viewLayout.setContentsMargins(3, 0, 3, 3) + + self.item_cards: List[QuantifiedItemCard] = [] + + for item in item_list: + + self.item_cards.append(QuantifiedItemCard(item)) + self.Layout.addWidget(self.item_cards[-1]) + + if len(item_list) == 0: + self.Layout.addWidget(QuantifiedItemCard(["暂无记录", ""])) + + self.Layout.addStretch(1) + + class LogCard(HeaderCardWidget): + + def __init__(self, parent=None): + super().__init__(parent) + self.setTitle("日志") + + self.text = TextBrowser(self) + self.button = PushButton("打开日志文件", self) + self.button.clicked.connect(lambda: print("打开日志文件")) + + Layout = QVBoxLayout() + Layout.addWidget(self.text) + Layout.addWidget(self.button) + self.viewLayout.setContentsMargins(3, 0, 3, 3) + self.viewLayout.addLayout(Layout) diff --git a/app/ui/home.py b/app/ui/home.py new file mode 100644 index 0000000..e9ae41a --- /dev/null +++ b/app/ui/home.py @@ -0,0 +1,423 @@ +# +# Copyright © <2024> + +# This file is part of AUTO_MAA. + +# AUTO_MAA is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published +# by the Free Software Foundation, either version 3 of the License, +# or (at your option) any later version. + +# AUTO_MAA is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty +# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See +# the GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with AUTO_MAA. If not, see . + +# DLmaster_361@163.com + +""" +AUTO_MAA +AUTO_MAA主界面 +v4.2 +作者:DLmaster_361 +""" + +from loguru import logger +from PySide6.QtWidgets import ( + QWidget, + QVBoxLayout, + QHBoxLayout, + QSpacerItem, + QSizePolicy, + QFileDialog, +) +from PySide6.QtCore import Qt, QSize, QUrl +from PySide6.QtGui import QDesktopServices, QColor +from qfluentwidgets import ( + FluentIcon, + ScrollArea, + SimpleCardWidget, + PrimaryToolButton, + TextBrowser, +) +import re +import shutil +import requests +import json +import time +from datetime import datetime +from pathlib import Path + +from app.core import Config, MainInfoBar +from .Widget import Banner, IconButton + + +class Home(QWidget): + + def __init__(self, parent=None): + super().__init__(parent) + self.setObjectName("主页") + + self.banner = Banner() + self.banner_text = TextBrowser() + + widget = QWidget() + Layout = QVBoxLayout(widget) + + Layout.addWidget(self.banner) + Layout.addWidget(self.banner_text) + Layout.setStretch(0, 2) + Layout.setStretch(1, 3) + + v_layout = QVBoxLayout(self.banner) + v_layout.setContentsMargins(0, 0, 0, 15) + v_layout.setSpacing(5) + v_layout.setAlignment(Qt.AlignmentFlag.AlignTop) + + # 空白占位符 + v_layout.addItem( + QSpacerItem(10, 20, QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Minimum) + ) + + # 顶部部分 (按钮组) + h1_layout = QHBoxLayout() + h1_layout.setAlignment(Qt.AlignmentFlag.AlignTop) + + # 左边留白区域 + h1_layout.addStretch() + + # 按钮组 + buttonGroup = ButtonGroup() + buttonGroup.setMaximumHeight(320) + h1_layout.addWidget(buttonGroup) + + # 空白占位符 + h1_layout.addItem( + QSpacerItem(20, 10, QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Minimum) + ) + + # 将顶部水平布局添加到垂直布局 + v_layout.addLayout(h1_layout) + + # 中间留白区域 + v_layout.addItem( + QSpacerItem(10, 10, QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Minimum) + ) + v_layout.addStretch() + + # 中间留白区域 + v_layout.addItem( + QSpacerItem(10, 10, QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Minimum) + ) + v_layout.addStretch() + + # 底部部分 (图片切换按钮) + h2_layout = QHBoxLayout() + h2_layout.setAlignment(Qt.AlignmentFlag.AlignTop) + + # 左边留白区域 + h2_layout.addItem( + QSpacerItem(20, 10, QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Minimum) + ) + + # # 公告卡片 + # noticeCard = NoticeCard() + # h2_layout.addWidget(noticeCard) + + h2_layout.addStretch() + + # 自定义图像按钮布局 + self.imageButton = PrimaryToolButton(FluentIcon.IMAGE_EXPORT) + self.imageButton.setFixedSize(56, 56) + self.imageButton.setIconSize(QSize(32, 32)) + self.imageButton.clicked.connect(self.get_home_image) + + v1_layout = QVBoxLayout() + v1_layout.addWidget(self.imageButton, alignment=Qt.AlignmentFlag.AlignBottom) + + h2_layout.addLayout(v1_layout) + + # 空白占位符 + h2_layout.addItem( + QSpacerItem(25, 10, QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Minimum) + ) + + # 将底部水平布局添加到垂直布局 + v_layout.addLayout(h2_layout) + + layout = QVBoxLayout() + scrollArea = ScrollArea() + scrollArea.setWidgetResizable(True) + scrollArea.setWidget(widget) + layout.addWidget(scrollArea) + self.setLayout(layout) + + self.set_banner() + + def get_home_image(self) -> None: + """获取主页图片""" + + if ( + Config.global_config.get(Config.global_config.function_HomeImageMode) + == "默认" + ): + pass + elif ( + Config.global_config.get(Config.global_config.function_HomeImageMode) + == "自定义" + ): + + file_path, _ = QFileDialog.getOpenFileName( + self, "打开自定义主页图片", "", "图片文件 (*.png *.jpg *.bmp)" + ) + if file_path: + + for file in Config.app_path.glob( + "resources/images/Home/BannerCustomize.*" + ): + file.unlink() + + shutil.copy( + file_path, + Config.app_path + / f"resources/images/Home/BannerCustomize{Path(file_path).suffix}", + ) + + logger.info(f"自定义主页图片更换成功:{file_path}") + MainInfoBar.push_info_bar( + "success", + "主页图片更换成功", + "自定义主页图片更换成功!", + 3000, + ) + + else: + logger.warning("自定义主页图片更换失败:未选择图片文件") + MainInfoBar.push_info_bar( + "warning", + "主页图片更换失败", + "未选择图片文件!", + 5000, + ) + elif ( + Config.global_config.get(Config.global_config.function_HomeImageMode) + == "主题图像" + ): + + # 从远程服务器获取最新主题图像 + for _ in range(3): + try: + response = requests.get( + "https://gitee.com/DLmaster_361/AUTO_MAA/raw/server/theme_image.json" + ) + theme_image = response.json() + break + except Exception as e: + err = e + time.sleep(0.1) + else: + logger.error(f"获取最新主题图像时出错:\n{err}") + MainInfoBar.push_info_bar( + "error", + "主题图像获取失败", + f"获取最新主题图像信息时出错:\n{err}", + -1, + ) + return None + + if (Config.app_path / "resources/theme_image.json").exists(): + with (Config.app_path / "resources/theme_image.json").open( + mode="r", encoding="utf-8" + ) as f: + theme_image_local = json.load(f) + time_local = datetime.strptime( + theme_image_local["time"], "%Y-%m-%d %H:%M" + ) + else: + time_local = datetime.strptime("2000-01-01 00:00", "%Y-%m-%d %H:%M") + + if not ( + Config.app_path / "resources/images/Home/BannerTheme.jpg" + ).exists() or ( + datetime.now() + > datetime.strptime(theme_image["time"], "%Y-%m-%d %H:%M") + and datetime.strptime(theme_image["time"], "%Y-%m-%d %H:%M") + > time_local + ): + + response = requests.get(theme_image["url"]) + if response.status_code == 200: + + with open( + Config.app_path / "resources/images/Home/BannerTheme.jpg", "wb" + ) as file: + file.write(response.content) + + logger.info(f"主题图像「{theme_image["name"]}」下载成功") + MainInfoBar.push_info_bar( + "success", + "主题图像下载成功", + f"「{theme_image["name"]}」下载成功!", + 3000, + ) + + else: + + logger.error("主题图像下载失败") + MainInfoBar.push_info_bar( + "error", + "主题图像下载失败", + f"主题图像下载失败:{response.status_code}", + -1, + ) + + with (Config.app_path / "resources/theme_image.json").open( + mode="w", encoding="utf-8" + ) as f: + json.dump(theme_image, f, ensure_ascii=False, indent=4) + + else: + + logger.info("主题图像已是最新") + MainInfoBar.push_info_bar( + "info", + "主题图像已是最新", + "主题图像已是最新!", + 3000, + ) + + self.set_banner() + + def set_banner(self): + """设置主页图像""" + if ( + Config.global_config.get(Config.global_config.function_HomeImageMode) + == "默认" + ): + self.banner.set_banner_image( + str(Config.app_path / "resources/images/Home/BannerDefault.png") + ) + self.imageButton.hide() + self.banner_text.setVisible(False) + elif ( + Config.global_config.get(Config.global_config.function_HomeImageMode) + == "自定义" + ): + for file in Config.app_path.glob("resources/images/Home/BannerCustomize.*"): + self.banner.set_banner_image(str(file)) + break + self.imageButton.show() + self.banner_text.setVisible(False) + elif ( + Config.global_config.get(Config.global_config.function_HomeImageMode) + == "主题图像" + ): + self.banner.set_banner_image( + str(Config.app_path / "resources/images/Home/BannerTheme.jpg") + ) + self.imageButton.show() + self.banner_text.setVisible(True) + + if (Config.app_path / "resources/theme_image.json").exists(): + with (Config.app_path / "resources/theme_image.json").open( + mode="r", encoding="utf-8" + ) as f: + theme_image = json.load(f) + html_content = theme_image["html"] + else: + html_content = "

主题图像

主题图像信息未知

" + + self.banner_text.setHtml(re.sub(r"]*>", "", html_content)) + + +class ButtonGroup(SimpleCardWidget): + """显示主页和 GitHub 按钮的竖直按钮组""" + + def __init__(self, parent=None): + super().__init__(parent=parent) + + self.setFixedSize(56, 180) + + layout = QVBoxLayout(self) + layout.setAlignment(Qt.AlignmentFlag.AlignTop) + + # 创建主页按钮 + home_button = IconButton( + FluentIcon.HOME.icon(color=QColor("#fff")), + tip_title="AUTO_MAA官网", + tip_content="AUTO_MAA官方文档站", + isTooltip=True, + ) + home_button.setIconSize(QSize(32, 32)) + home_button.clicked.connect(self.open_home) + layout.addWidget(home_button) + + # 创建 GitHub 按钮 + github_button = IconButton( + FluentIcon.GITHUB.icon(color=QColor("#fff")), + tip_title="Github仓库", + tip_content="如果本项目有帮助到您~\n不妨给项目点一个Star⭐", + isTooltip=True, + ) + github_button.setIconSize(QSize(32, 32)) + github_button.clicked.connect(self.open_github) + layout.addWidget(github_button) + + # # 创建 文档 按钮 + # doc_button = IconButton( + # FluentIcon.DICTIONARY.icon(color=QColor("#fff")), + # tip_title="自助排障文档", + # tip_content="点击打开自助排障文档,好孩子都能看懂", + # isTooltip=True, + # ) + # doc_button.setIconSize(QSize(32, 32)) + # doc_button.clicked.connect(self.open_doc) + # layout.addWidget(doc_button) + + # 创建 Q群 按钮 + doc_button = IconButton( + FluentIcon.CHAT.icon(color=QColor("#fff")), + tip_title="官方社群", + tip_content="加入官方群聊【AUTO_MAA绝赞DeBug中!】", + isTooltip=True, + ) + doc_button.setIconSize(QSize(32, 32)) + doc_button.clicked.connect(self.open_chat) + layout.addWidget(doc_button) + + # 创建 官方店铺 按钮 (当然没有) + doc_button = IconButton( + FluentIcon.SHOPPING_CART.icon(color=QColor("#fff")), + tip_title="官方店铺", + tip_content="暂时没有官方店铺,但是可以加入官方群聊哦~", + isTooltip=True, + ) + doc_button.setIconSize(QSize(32, 32)) + doc_button.clicked.connect(self.open_sales) + layout.addWidget(doc_button) + + def _normalBackgroundColor(self): + return QColor(0, 0, 0, 96) + + def open_home(self): + """打开主页链接""" + QDesktopServices.openUrl(QUrl("https://clozya.github.io/AUTOMAA_docs")) + + def open_github(self): + """打开 GitHub 链接""" + QDesktopServices.openUrl(QUrl("https://github.com/DLmaster361/AUTO_MAA")) + + def open_chat(self): + """打开 Q群 链接""" + QDesktopServices.openUrl(QUrl("https://qm.qq.com/q/bd9fISNoME")) + + def open_doc(self): + """打开 文档 链接""" + QDesktopServices.openUrl(QUrl("https://clozya.github.io/AUTOMAA_docs")) + + def open_sales(self): + """其实还是打开 Q群 链接""" + QDesktopServices.openUrl(QUrl("https://qm.qq.com/q/bd9fISNoME")) diff --git a/app/ui/main_window.py b/app/ui/main_window.py index 227db62..d709762 100644 --- a/app/ui/main_window.py +++ b/app/ui/main_window.py @@ -46,13 +46,17 @@ from qfluentwidgets import ( from PySide6.QtGui import QIcon, QCloseEvent from PySide6.QtCore import Qt, QTimer import json +from datetime import datetime, timedelta +import shutil from app.core import Config, TaskManager, MainTimer, MainInfoBar from app.services import Notify, Crypto, System -from .setting import Setting +from .home import Home from .member_manager import MemberManager from .queue_manager import QueueManager from .dispatch_center import DispatchCenter +from .history import History +from .setting import Setting class AUTO_MAA(MSFluentWindow): @@ -72,17 +76,19 @@ class AUTO_MAA(MSFluentWindow): System.main_window = self.window() # 创建主窗口 - self.setting = Setting(self) + self.home = Home(self) self.member_manager = MemberManager(self) self.queue_manager = QueueManager(self) self.dispatch_center = DispatchCenter(self) + self.history = History(self) + self.setting = Setting(self) self.addSubInterface( - self.setting, - FluentIcon.SETTING, - "设置", - FluentIcon.SETTING, - NavigationItemPosition.BOTTOM, + self.home, + FluentIcon.HOME, + "主页", + FluentIcon.HOME, + NavigationItemPosition.TOP, ) self.addSubInterface( self.member_manager, @@ -105,6 +111,20 @@ class AUTO_MAA(MSFluentWindow): FluentIcon.IOT, NavigationItemPosition.TOP, ) + self.addSubInterface( + self.history, + FluentIcon.HISTORY, + "历史记录", + FluentIcon.HISTORY, + NavigationItemPosition.BOTTOM, + ) + self.addSubInterface( + self.setting, + FluentIcon.SETTING, + "设置", + FluentIcon.SETTING, + NavigationItemPosition.BOTTOM, + ) self.stackedWidget.currentChanged.connect( lambda index: (self.member_manager.refresh() if index == 1 else None) ) @@ -123,11 +143,13 @@ class AUTO_MAA(MSFluentWindow): self.dispatch_center.update_top_bar() if index == 3 else None ) ) + self.stackedWidget.currentChanged.connect( + lambda index: (self.history.refresh() if index == 4 else None) + ) # 创建系统托盘及其菜单 self.tray = QSystemTrayIcon( - QIcon(str(Config.app_path / "resources/icons/AUTO_MAA.ico")), - self, + QIcon(str(Config.app_path / "resources/icons/AUTO_MAA.ico")), self ) self.tray.setToolTip("AUTO_MAA") self.tray_menu = SystemTrayMenu("AUTO_MAA", self) @@ -166,10 +188,16 @@ class AUTO_MAA(MSFluentWindow): TaskManager.create_gui.connect(self.dispatch_center.add_board) TaskManager.connect_gui.connect(self.dispatch_center.connect_main_board) + Notify.push_info_bar.connect(MainInfoBar.push_info_bar) self.setting.ui.card_IfShowTray.checkedChanged.connect( lambda: self.show_ui("配置托盘") ) self.setting.ui.card_IfToTray.checkedChanged.connect(self.set_min_method) + self.setting.function.card_HomeImageMode.comboBox.currentIndexChanged.connect( + lambda index: ( + self.home.get_home_image() if index == 2 else self.home.set_banner() + ) + ) self.splashScreen.finish() @@ -197,9 +225,19 @@ class AUTO_MAA(MSFluentWindow): qconfig.load(Config.config_path, Config.global_config) Config.global_config.save() + # 清理旧日志 + self.clean_old_logs() + # 检查密码 self.setting.check_PASSWORD() + # 获取主题图像 + if ( + Config.global_config.get(Config.global_config.function_HomeImageMode) + == "主题图像" + ): + self.home.get_home_image() + # 获取公告 self.setting.show_notice(if_show=False) @@ -229,6 +267,11 @@ class AUTO_MAA(MSFluentWindow): self.start_main_task() + # 直接最小化 + if Config.global_config.get(Config.global_config.start_IfMinimizeDirectly): + + self.titleBar.minBtn.click() + def set_min_method(self) -> None: """设置最小化方法""" @@ -247,6 +290,40 @@ class AUTO_MAA(MSFluentWindow): if reason == QSystemTrayIcon.DoubleClick: self.show_ui("显示主窗口") + def clean_old_logs(self): + """ + 删除超过用户设定天数的日志文件(基于目录日期) + """ + + if ( + Config.global_config.get(Config.global_config.function_HistoryRetentionTime) + == 0 + ): + logger.info("由于用户设置日志永久保留,跳过日志清理") + return + + deleted_count = 0 + + for date_folder in (Config.app_path / "history").iterdir(): + if not date_folder.is_dir(): + continue # 只处理日期文件夹 + + try: + # 只检查 `YYYY-MM-DD` 格式的文件夹 + folder_date = datetime.strptime(date_folder.name, "%Y-%m-%d") + if datetime.now() - folder_date > timedelta( + days=Config.global_config.get( + Config.global_config.function_HistoryRetentionTime + ) + ): + shutil.rmtree(date_folder, ignore_errors=True) + deleted_count += 1 + logger.info(f"已删除超期日志目录: {date_folder}") + except ValueError: + logger.warning(f"非日期格式的目录: {date_folder}") + + logger.info(f"清理完成: {deleted_count} 个日期目录") + def start_main_task(self) -> None: """启动主任务""" @@ -296,6 +373,8 @@ class AUTO_MAA(MSFluentWindow): ) self.window().setGeometry(location[0], location[1], size[0], size[1]) self.window().show() + self.window().raise_() + self.window().activateWindow() if not if_quick: if Config.global_config.get(Config.global_config.ui_maximized): self.window().showMaximized() diff --git a/app/ui/member_manager.py b/app/ui/member_manager.py index 42049bf..940b7c7 100644 --- a/app/ui/member_manager.py +++ b/app/ui/member_manager.py @@ -72,10 +72,7 @@ from .Widget import ( class MemberManager(QWidget): - def __init__( - self, - parent=None, - ): + def __init__(self, parent=None): super().__init__(parent) self.setObjectName("脚本管理") @@ -315,8 +312,9 @@ class MemberManager(QWidget): if choice.input[0].currentText() == "MAA": + (Config.app_path / "script/MAA").mkdir(parents=True, exist_ok=True) folder = QFileDialog.getExistingDirectory( - self, "选择MAA下载目录", str(Config.app_path) + self, "选择MAA下载目录", str(Config.app_path / "script/MAA") ) if not folder: logger.warning("选择MAA下载目录时未选择文件夹") @@ -329,7 +327,7 @@ class MemberManager(QWidget): for _ in range(3): try: response = requests.get( - "https://mirrorc.top/api/resources/MAA/latest?user_agent=MaaWpfGui&os=win&arch=x64&channel=beta" + "https://mirrorc.top/api/resources/MAA/latest?user_agent=MaaWpfGui&os=win&arch=x64&channel=stable" ) maa_info = response.json() break @@ -358,7 +356,7 @@ class MemberManager(QWidget): maa_version.append(0) self.downloader = Updater(Path(folder), "MAA", maa_version, []) - self.downloader.ui.show() + self.downloader.show() def show_password(self): @@ -640,12 +638,7 @@ class MaaSettingBox(QWidget): class RunSetSettingCard(ExpandGroupSettingCard): def __init__(self, parent=None): - super().__init__( - FluentIcon.SETTING, - "运行", - "MAA运行调控选项", - parent, - ) + super().__init__(FluentIcon.SETTING, "运行", "MAA运行调控选项", parent) self.card_TaskTransitionMethod = ComboBoxSettingCard( configItem=Config.maa_config.RunSet_TaskTransitionMethod, diff --git a/app/ui/queue_manager.py b/app/ui/queue_manager.py index 4004220..4e82a93 100644 --- a/app/ui/queue_manager.py +++ b/app/ui/queue_manager.py @@ -60,10 +60,7 @@ from .Widget import ( class QueueManager(QWidget): - def __init__( - self, - parent=None, - ): + def __init__(self, parent=None): super().__init__(parent) self.setObjectName("调度队列") diff --git a/app/ui/setting.py b/app/ui/setting.py index d77e775..5aa6e92 100644 --- a/app/ui/setting.py +++ b/app/ui/setting.py @@ -58,19 +58,10 @@ from .Widget import LineEditMessageBox, LineEditSettingCard, PasswordLineEditSet class Setting(QWidget): - def __init__( - self, - parent=None, - ): + def __init__(self, parent=None): super().__init__(parent) - self.setObjectName("设置") - layout = QVBoxLayout() - - scrollArea = ScrollArea() - scrollArea.setWidgetResizable(True) - content_widget = QWidget() content_layout = QVBoxLayout(content_widget) @@ -97,10 +88,11 @@ class Setting(QWidget): content_layout.addWidget(self.updater) content_layout.addWidget(self.other) + scrollArea = ScrollArea() + scrollArea.setWidgetResizable(True) scrollArea.setWidget(content_widget) - + layout = QVBoxLayout() layout.addWidget(scrollArea) - self.setLayout(layout) def agree_bilibili(self) -> None: @@ -339,7 +331,7 @@ class Setting(QWidget): if main_version_remote > main_version_current: self.updater.update_process.accomplish.connect(self.update_main) # 显示更新页面 - self.updater.ui.show() + self.updater.show() # 更新主程序 elif main_version_remote > main_version_current: @@ -417,6 +409,20 @@ class FunctionSettingCard(HeaderCardWidget): super().__init__(parent) self.setTitle("功能") + self.card_HomeImageMode = ComboBoxSettingCard( + configItem=Config.global_config.function_HomeImageMode, + icon=FluentIcon.PAGE_RIGHT, + title="主页背景图模式", + content="选择主页背景图的来源", + texts=["默认", "自定义", "主题图像"], + ) + self.card_HistoryRetentionTime = ComboBoxSettingCard( + configItem=Config.global_config.function_HistoryRetentionTime, + icon=FluentIcon.PAGE_RIGHT, + title="历史记录保留时间", + content="选择历史记录的保留时间,超期自动清理", + texts=["7 天", "15 天", "30 天", "60 天", "永久"], + ) self.card_IfAllowSleep = SwitchSettingCard( icon=FluentIcon.PAGE_RIGHT, title="启动时阻止系统休眠", @@ -432,6 +438,8 @@ class FunctionSettingCard(HeaderCardWidget): ) Layout = QVBoxLayout() + Layout.addWidget(self.card_HomeImageMode) + Layout.addWidget(self.card_HistoryRetentionTime) Layout.addWidget(self.card_IfAllowSleep) Layout.addWidget(self.card_IfSilence) Layout.addWidget(self.card_IfAgreeBilibili) @@ -488,10 +496,17 @@ class StartSettingCard(HeaderCardWidget): content="启动AUTO_MAA后自动运行自动代理任务,优先级:调度队列 1 > 脚本 1", configItem=Config.global_config.start_IfRunDirectly, ) + self.card_IfMinimizeDirectly = SwitchSettingCard( + icon=FluentIcon.PAGE_RIGHT, + title="启动后直接最小化", + content="启动AUTO_MAA后直接最小化", + configItem=Config.global_config.start_IfMinimizeDirectly, + ) Layout = QVBoxLayout() Layout.addWidget(self.card_IfSelfStart) Layout.addWidget(self.card_IfRunDirectly) + Layout.addWidget(self.card_IfMinimizeDirectly) self.viewLayout.addLayout(Layout) @@ -527,38 +542,82 @@ class NotifySettingCard(HeaderCardWidget): self.setTitle("通知") - self.card_IfSendErrorOnly = SwitchSettingCard( - icon=FluentIcon.PAGE_RIGHT, - title="仅推送异常信息", - content="仅在任务出现异常时推送通知", - configItem=Config.global_config.notify_IfSendErrorOnly, - ) - self.card_IfPushPlyer = SwitchSettingCard( - icon=FluentIcon.PAGE_RIGHT, - title="推送系统通知", - content="推送系统级通知,不会在通知中心停留", - configItem=Config.global_config.notify_IfPushPlyer, - ) - self.card_SendMail = self.SendMailSettingCard(self) + self.card_NotifyContent = self.NotifyContentSettingCard(self) + self.card_Plyer = self.PlyerSettingCard(self) + self.card_EMail = self.EMailSettingCard(self) self.card_ServerChan = self.ServerChanSettingCard(self) self.card_CompanyWebhookBot = self.CompanyWechatPushSettingCard(self) Layout = QVBoxLayout() - Layout.addWidget(self.card_IfSendErrorOnly) - Layout.addWidget(self.card_IfPushPlyer) - Layout.addWidget(self.card_SendMail) + Layout.addWidget(self.card_NotifyContent) + Layout.addWidget(self.card_Plyer) + Layout.addWidget(self.card_EMail) Layout.addWidget(self.card_ServerChan) Layout.addWidget(self.card_CompanyWebhookBot) self.viewLayout.addLayout(Layout) - class SendMailSettingCard(ExpandGroupSettingCard): + class NotifyContentSettingCard(ExpandGroupSettingCard): def __init__(self, parent=None): super().__init__( - FluentIcon.SETTING, - "推送邮件通知", - "通过电子邮箱推送任务结果", - parent, + FluentIcon.SETTING, "通知内容选项", "选择需要推送的通知内容", parent + ) + + self.card_SendTaskResultTime = ComboBoxSettingCard( + configItem=Config.global_config.notify_SendTaskResultTime, + icon=FluentIcon.PAGE_RIGHT, + title="推送任务结果选项", + content="选择推送自动代理与人工排查任务结果的时机", + texts=["不推送", "任何时刻", "仅失败时"], + ) + self.card_IfSendStatistic = SwitchSettingCard( + icon=FluentIcon.PAGE_RIGHT, + title="推送统计信息", + content="推送自动代理统计信息的通知", + configItem=Config.global_config.notify_IfSendStatistic, + ) + self.card_IfSendSixStar = SwitchSettingCard( + icon=FluentIcon.PAGE_RIGHT, + title="推送公招高资喜报", + content="公招出现六星词条时推送喜报", + configItem=Config.global_config.notify_IfSendSixStar, + ) + + widget = QWidget() + Layout = QVBoxLayout(widget) + Layout.addWidget(self.card_SendTaskResultTime) + Layout.addWidget(self.card_IfSendStatistic) + Layout.addWidget(self.card_IfSendSixStar) + self.viewLayout.setContentsMargins(0, 0, 0, 0) + self.viewLayout.setSpacing(0) + self.addGroupWidget(widget) + + class PlyerSettingCard(ExpandGroupSettingCard): + + def __init__(self, parent=None): + super().__init__( + FluentIcon.SETTING, "推送系统通知", "Plyer系统通知推送渠道", parent + ) + + self.card_IfPushPlyer = SwitchSettingCard( + icon=FluentIcon.PAGE_RIGHT, + title="推送系统通知", + content="使用Plyer推送系统级通知,不会在通知中心停留", + configItem=Config.global_config.notify_IfPushPlyer, + ) + + widget = QWidget() + Layout = QVBoxLayout(widget) + Layout.addWidget(self.card_IfPushPlyer) + self.viewLayout.setContentsMargins(0, 0, 0, 0) + self.viewLayout.setSpacing(0) + self.addGroupWidget(widget) + + class EMailSettingCard(ExpandGroupSettingCard): + + def __init__(self, parent=None): + super().__init__( + FluentIcon.SETTING, "推送邮件通知", "电子邮箱通知推送渠道", parent ) self.card_IfSendMail = SwitchSettingCard( @@ -612,7 +671,7 @@ class NotifySettingCard(HeaderCardWidget): super().__init__( FluentIcon.SETTING, "推送ServerChan通知", - "通过ServerChan通知推送任务结果", + "ServerChan通知推送渠道", parent, ) @@ -659,7 +718,7 @@ class NotifySettingCard(HeaderCardWidget): super().__init__( FluentIcon.SETTING, "推送企业微信机器人通知", - "通过企业微信机器人Webhook通知推送任务结果", + "企业微信机器人Webhook通知推送渠道", parent, ) diff --git a/app/utils/Updater.py b/app/utils/Updater.py index aa83d4a..859de70 100644 --- a/app/utils/Updater.py +++ b/app/utils/Updater.py @@ -33,14 +33,16 @@ import subprocess import time from pathlib import Path -from PySide6.QtWidgets import ( - QApplication, - QDialog, - QVBoxLayout, +from PySide6.QtWidgets import QApplication, QDialog, QVBoxLayout, QHBoxLayout +from qfluentwidgets import ( + ProgressBar, + IndeterminateProgressBar, + BodyLabel, + PushButton, + EditableComboBox, ) -from qfluentwidgets import ProgressBar, IndeterminateProgressBar, BodyLabel -from PySide6.QtGui import QIcon -from PySide6.QtCore import QObject, QThread, Signal +from PySide6.QtGui import QIcon, QCloseEvent +from PySide6.QtCore import QThread, Signal, QEventLoop def version_text(version_numb: list) -> str: @@ -59,6 +61,8 @@ class UpdateProcess(QThread): info = Signal(str) progress = Signal(int, int, int) + question = Signal(dict) + question_response = Signal(str) accomplish = Signal() def __init__( @@ -72,6 +76,9 @@ class UpdateProcess(QThread): self.updater_version = updater_version self.download_path = app_path / "DOWNLOAD_TEMP.zip" # 临时下载文件的路径 self.version_path = app_path / "resources/version.json" + self.response = None + + self.question_response.connect(self._capture_response) def run(self) -> None: @@ -81,25 +88,41 @@ class UpdateProcess(QThread): self.info.emit("正在获取下载链接") url_list = self.get_download_url() + url_dict = {} + + # 验证下载地址 + for i, url in enumerate(url_list): + + if self.isInterruptionRequested(): + return None + + self.progress.emit(0, len(url_list), i) - # 验证下载地址并获取文件大小 - for i in range(len(url_list)): try: - self.info.emit(f"正在验证下载地址:{url_list[i]}") - response = requests.get(url_list[i], stream=True) + self.info.emit(f"正在验证下载地址:{url}") + response = requests.get(url, stream=True) if response.status_code != 200: - self.info.emit( - f"连接失败,错误代码 {response.status_code} ,正在切换代理({i+1}/{len(url_list)})" - ) + self.info.emit(f"连接失败,错误代码 {response.status_code}") time.sleep(1) continue - file_size = response.headers.get("Content-Length") - break + url_dict[url] = response.elapsed.total_seconds() except requests.RequestException: - self.info.emit(f"请求超时,正在切换代理({i+1}/{len(url_list)})") + self.info.emit(f"请求超时") time.sleep(1) - else: - self.info.emit(f"连接失败,已尝试所有{len(url_list)}个代理") + + download_url = self.push_question(url_dict) + + # 获取文件大小 + try: + self.info.emit(f"正在连接下载地址:{download_url}") + self.progress.emit(0, 0, 0) + response = requests.get(download_url, stream=True) + if response.status_code != 200: + self.info.emit(f"连接失败,错误代码 {response.status_code}") + return None + file_size = response.headers.get("Content-Length") + except requests.RequestException: + self.info.emit(f"请求超时") return None if file_size is None: @@ -118,6 +141,9 @@ class UpdateProcess(QThread): for chunk in response.iter_content(chunk_size=8192): + if self.isInterruptionRequested(): + break + # 写入已下载数据 f.write(chunk) downloaded_size += len(chunk) @@ -143,6 +169,10 @@ class UpdateProcess(QThread): ) self.progress.emit(0, 100, int(downloaded_size / file_size * 100)) + if self.isInterruptionRequested() and self.download_path.exists(): + self.download_path.unlink() + return None + except Exception as e: e = str(e) e = "\n".join([e[_ : _ + 75] for _ in range(0, len(e), 75)]) @@ -153,6 +183,9 @@ class UpdateProcess(QThread): try: while True: + if self.isInterruptionRequested(): + self.download_path.unlink() + return None try: self.info.emit("正在解压更新文件") self.progress.emit(0, 0, 0) @@ -178,7 +211,10 @@ class UpdateProcess(QThread): return None # 更新version文件 - if self.name in ["AUTO_MAA主程序", "AUTO_MAA更新器"]: + if not self.isInterruptionRequested and self.name in [ + "AUTO_MAA主程序", + "AUTO_MAA更新器", + ]: with open(self.version_path, "r", encoding="utf-8") as f: version_info = json.load(f) if self.name == "AUTO_MAA主程序": @@ -191,13 +227,13 @@ class UpdateProcess(QThread): json.dump(version_info, f, ensure_ascii=False, indent=4) # 主程序更新完成后打开AUTO_MAA - if self.name == "AUTO_MAA主程序": + if not self.isInterruptionRequested and self.name == "AUTO_MAA主程序": subprocess.Popen( str(self.app_path / "AUTO_MAA.exe"), shell=True, creationflags=subprocess.CREATE_NO_WINDOW, ) - elif self.name == "MAA": + elif not self.isInterruptionRequested and self.name == "MAA": subprocess.Popen( str(self.app_path / "MAA.exe"), shell=True, @@ -276,18 +312,26 @@ class UpdateProcess(QThread): ) return url_list + def push_question(self, url_dict: dict) -> str: + self.question.emit(url_dict) + loop = QEventLoop() + self.question_response.connect(loop.quit) + loop.exec() + return self.response -class Updater(QObject): + def _capture_response(self, response: str) -> None: + self.response = response + + +class Updater(QDialog): def __init__( self, app_path: Path, name: str, main_version: list, updater_version: list ) -> None: super().__init__() - self.ui = QDialog() - self.ui.setWindowTitle("AUTO_MAA更新器") - self.ui.resize(700, 70) - self.ui.setWindowIcon( + self.setWindowTitle("AUTO_MAA更新器") + self.setWindowIcon( QIcon( str( Path(sys.argv[0]).resolve().parent @@ -297,11 +341,17 @@ class Updater(QObject): ) # 创建垂直布局 - self.Layout = QVBoxLayout(self.ui) + self.Layout = QVBoxLayout(self) - self.info = BodyLabel("正在初始化", self.ui) - self.progress_1 = IndeterminateProgressBar(self.ui) - self.progress_2 = ProgressBar(self.ui) + self.info = BodyLabel("正在初始化", self) + self.progress_1 = IndeterminateProgressBar(self) + self.progress_2 = ProgressBar(self) + self.combo_box = EditableComboBox(self) + + self.button = PushButton("继续", self) + self.h_layout = QHBoxLayout() + self.h_layout.addStretch(1) + self.h_layout.addWidget(self.button) self.update_progress(0, 0, 0) @@ -309,6 +359,8 @@ class Updater(QObject): self.Layout.addStretch(1) self.Layout.addWidget(self.progress_1) self.Layout.addWidget(self.progress_2) + self.Layout.addWidget(self.combo_box) + self.Layout.addLayout(self.h_layout) self.Layout.addStretch(1) self.update_process = UpdateProcess( @@ -317,21 +369,59 @@ class Updater(QObject): self.update_process.info.connect(self.update_info) self.update_process.progress.connect(self.update_progress) + self.update_process.question.connect(self.question) self.update_process.start() def update_info(self, text: str) -> None: self.info.setText(text) - def update_progress(self, begin: int, end: int, current: int) -> None: - if begin == 0 and end == 0: + def update_progress( + self, begin: int, end: int, current: int, if_show_combo_box: bool = False + ) -> None: + + self.combo_box.setVisible(if_show_combo_box) + self.button.setVisible(if_show_combo_box) + + if if_show_combo_box: + self.progress_1.setVisible(False) + self.progress_2.setVisible(False) + self.resize(1000, 90) + elif begin == 0 and end == 0: self.progress_2.setVisible(False) self.progress_1.setVisible(True) + self.resize(700, 70) else: self.progress_1.setVisible(False) self.progress_2.setVisible(True) self.progress_2.setRange(begin, end) self.progress_2.setValue(current) + self.resize(700, 70) + + def question(self, url_dict: dict) -> None: + + self.update_info("测速完成,请选择或自行输入一个合适下载地址:") + self.update_progress(0, 0, 0, True) + + url_dict = dict(sorted(url_dict.items(), key=lambda item: item[1])) + + for url, time in url_dict.items(): + self.combo_box.addItem(f"{url} | 响应时间:{time:.3f}秒") + + self.button.clicked.connect( + lambda: self.update_process.question_response.emit( + self.combo_box.currentText().split(" | ")[0] + ) + ) + + def closeEvent(self, event: QCloseEvent): + """清理残余进程""" + + self.update_process.requestInterruption() + self.update_process.quit() + self.update_process.wait() + + event.accept() class AUTO_MAA_Updater(QApplication): @@ -341,7 +431,7 @@ class AUTO_MAA_Updater(QApplication): super().__init__() self.main = Updater(app_path, name, main_version, updater_version) - self.main.ui.show() + self.main.show() if __name__ == "__main__": diff --git a/requirements.txt b/requirements.txt index 95ceb9a..a8597de 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,5 +8,6 @@ pywin32 pyautogui pycryptodome requests +Jinja2 serverchan_sdk nuitka==2.6 \ No newline at end of file diff --git a/resources/html/MAA_result.html b/resources/html/MAA_result.html new file mode 100644 index 0000000..78328d2 --- /dev/null +++ b/resources/html/MAA_result.html @@ -0,0 +1,160 @@ + + + + + + + + +
+
+

{{ title }}

+ + +
+ +
+

脚本实例名称:{{ script_name }}

+

任务开始时间:{{ start_time }}

+

任务结束时间:{{ end_time }}

+

已完成数:{{ completed_count }}

+ {% if uncompleted_count %} +

未完成数:{{ uncompleted_count }}

+ {% endif %} + {% if failed_user %} +

代理未成功的用户: {{ failed_user }}

+ {% endif %} + {% if waiting_user %} +

未开始代理的用户: {{ waiting_user }}

+ {% endif %} +
+ +

AUTO_MAA 敬上

+ + + +
+ + + \ No newline at end of file diff --git a/resources/html/MAA_six_star.html b/resources/html/MAA_six_star.html new file mode 100644 index 0000000..38b461a --- /dev/null +++ b/resources/html/MAA_six_star.html @@ -0,0 +1,129 @@ + + + + + + + + +
+
+

喜报!

+ + +
+ +
+

恭喜用户 {{ user_name }} 喜提公开招募六星高资!

+
+ +

AUTO_MAA 敬上

+ + + +
+ + + \ No newline at end of file diff --git a/resources/html/MAA_statistics.html b/resources/html/MAA_statistics.html new file mode 100644 index 0000000..b3c3e56 --- /dev/null +++ b/resources/html/MAA_statistics.html @@ -0,0 +1,233 @@ + + + + + + + + +
+
+

自动代理统计报告

+ + +
+ +
+

用户代理信息:{{ user_info }}

+

任务开始时间:{{ start_time }}

+

任务结束时间:{{ end_time }}

+

MAA执行结果: + {% if maa_result == '代理任务全部完成' %} + {{ maa_result }} + {% elif maa_result == '代理任务未全部完成' %} + {{ maa_result }} + {% else %} + {{ maa_result }} + {% endif %} +

+ + {% if recruit_statistics %} +

公招统计

+ + + + + + {% for star, count in recruit_statistics %} + + + + + {% endfor %} +
星级数量
{{ star }}{{ count }}
+ {% endif %} + + {% if drop_statistics %} + {% for stage, items in drop_statistics.items() %} +

掉落统计({{ stage }})

+ + + + + + {% for item, amount in items.items() %} + + + + + {% endfor %} +
物品数量
{{ item }}{{ amount }}
+ {% endfor %} + {% endif %} +
+ +

AUTO_MAA 敬上

+ + + +
+ + + \ No newline at end of file diff --git a/resources/images/Home.png b/resources/images/Home.png new file mode 100644 index 0000000..0795101 Binary files /dev/null and b/resources/images/Home.png differ diff --git a/resources/images/Home/BannerDefault.png b/resources/images/Home/BannerDefault.png new file mode 100644 index 0000000..0795101 Binary files /dev/null and b/resources/images/Home/BannerDefault.png differ diff --git a/resources/version.json b/resources/version.json index 9cede48..3077e3b 100644 --- a/resources/version.json +++ b/resources/version.json @@ -1,7 +1,7 @@ { - "main_version": "4.2.4.0", - "updater_version": "1.1.2.0", - "announcement": "\n## 新增功能\n- 添加`简洁用户列表下相邻两个任务间的切换方式`可选项\n- 恢复启动后直接运行主任务功能以及相关托盘菜单\n## 修复BUG\n- 修复静默代理标记移除异常情况\n- 适配深色模式 #18\n## 程序优化\n- 优化MAA关闭方法\n- 添加高级代理文件校验过程\n- 升级日志监看方法\n- 优化主调度台默认选项\n- 配置MAA前关闭可能未正常退出的MAA进程\n- 接入镜像源", + "main_version": "4.2.5.0", + "updater_version": "1.1.2.1", + "announcement": "\n## 新增功能\n- 历史记录统计功能上线\n- 添加软件主页\n- 添加启动时直接最小化功能\n- 更新器拥有多网址测速功能\n- 添加统计信息通知功能(含六星监测)\n## 修复BUG\n- RMA70-12不能正确统计的问题\n- 更新器修正`channel`\n## 程序优化\n- 添加MAA监测字段:`未检测到任何模拟器`\n- 取消MAA运行中自动更新", "proxy_list": [ "", "https://gitproxy.click/",