diff --git a/MCE/config.json b/MCE/config.json new file mode 100644 index 0000000..796b553 --- /dev/null +++ b/MCE/config.json @@ -0,0 +1,136 @@ +{ + "ResetDaily": true, + "LastRun": "2023-12-24 21:41:55", + "ResetTime": "20:21:30", + "RechargeAP": true, + "PreferredTemplate": "commissions", + "Queue": [ + [ + "N", + "7-2", + 17 + ], + [ + "H", + "7-3", + 6 + ], + [ + "BD", + "01", + 1 + ], + [ + "N", + "7-2", + 1 + ], + [ + "IR", + "02", + 1 + ], + [ + "N", + "7-2", + 1 + ], + [ + "N", + "7-3", + 1 + ] + ], + "Event": false, + "Templates": { + "template1": [ + [ + "H", + "5-1", + 3 + ], + [ + "H", + "4-3", + 3 + ], + [ + "H", + "5-3", + 3 + ], + [ + "H", + "3-3", + 3 + ], + [ + "H", + "2-2", + 3 + ], + [ + "H", + "1-1", + 3 + ], + [ + "H", + "1-3", + 3 + ], + [ + "E", + "08", + 30 + ], + [ + "E", + "08", + 30 + ], + [ + "E", + "30", + 30 + ] + ], + "commissions": [ + [ + "N", + "7-2", + 20 + ], + [ + "H", + "7-3", + 6 + ], + [ + "BD", + "01", + 1 + ], + [ + "N", + "7-2", + 1 + ], + [ + "IR", + "02", + 1 + ], + [ + "N", + "7-2", + 1 + ], + [ + "N", + "7-3", + 1 + ] + ] + } +} \ No newline at end of file diff --git a/MCE/custom_widgets/__init__.py b/MCE/custom_widgets/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/MCE/custom_widgets/ctk_integerspinbox.py b/MCE/custom_widgets/ctk_integerspinbox.py new file mode 100644 index 0000000..d3a2c56 --- /dev/null +++ b/MCE/custom_widgets/ctk_integerspinbox.py @@ -0,0 +1,95 @@ +import customtkinter +import re +from typing import Callable + +class CTkIntegerSpinbox(customtkinter.CTkFrame): + def __init__(self, *args, + width: int = 100, + height: int = 32, + step_size: int = 1, + min_value: int = 0, + command: Callable = None, + **kwargs): + super().__init__(*args, width=width, height=height, **kwargs) + + self.step_size = step_size + self.min_value = min_value + self.command = command + self.after_id = None + + self.grid_columnconfigure((0, 2), weight=0) + self.grid_columnconfigure(1, weight=1) + + self.subtract_button = customtkinter.CTkButton(self, text="-", width=height-6, height=height-6) + self.subtract_button.grid(row=0, column=0, padx=(3, 0), pady=3) + self.subtract_button.bind('', self.start_decrementing) + self.subtract_button.bind('', self.stop_decrementing) + + self.entry = customtkinter.CTkEntry(self, width=width-(2*height), height=height-6, border_width=0) + self.entry.grid(row=0, column=1, columnspan=1, padx=3, pady=3, sticky="ew") + + self.add_button = customtkinter.CTkButton(self, text="+", width=height-6, height=height-6) + self.add_button.grid(row=0, column=2, padx=(0, 3), pady=3) + self.add_button.bind('', self.start_incrementing) + self.add_button.bind('', self.stop_incrementing) + + self.entry.insert(0, "0") + + # Configure validatecommand to allow only integers + vcmd = (self.entry.register(self.validate_input), '%P') + self.entry.configure(validate='key', validatecommand=vcmd) + + def start_incrementing(self, event): + self.increment() + self.after_id = self.after(150, self.start_incrementing, event) + + def stop_incrementing(self, event): + if self.after_id: + self.after_cancel(self.after_id) + self.after_id = None + + def start_decrementing(self, event): + self.decrement() + self.after_id = self.after(150, self.start_decrementing, event) + + def stop_decrementing(self, event): + if self.after_id: + self.after_cancel(self.after_id) + self.after_id = None + + def increment(self): + value = int(self.entry.get()) + self.step_size + self.entry.delete(0, "end") + self.entry.insert(0, max(self.min_value, value)) # Ensure the value is not less than 1 + if self.command is not None: + self.command() + + def decrement(self): + value = int(self.entry.get()) - self.step_size + self.entry.delete(0, "end") + self.entry.insert(0, max(self.min_value, value)) # Ensure the value is not less than 0 + if self.command is not None: + self.command() + + def validate_input(self, new_value): + # Validate that the input is a non-negative integer + return re.match(r'^\d*$', new_value) is not None + + def get(self) -> int: + try: + return int(self.entry.get()) + except ValueError: + return 0 + + def set(self, value: int): + self.entry.delete(0, "end") + self.entry.insert(0, max(self.min_value, value)) # Ensure the value is not less than 0 + + def configure(self, **kwargs): + state = kwargs.get("state", None) + if state is not None: + self.subtract_button.configure(state=state) + self.add_button.configure(state=state) + self.entry.configure(state=state) + kwargs.pop("state") + super().configure(**kwargs) \ No newline at end of file diff --git a/MCE/custom_widgets/ctk_notification.py b/MCE/custom_widgets/ctk_notification.py new file mode 100644 index 0000000..713ecfd --- /dev/null +++ b/MCE/custom_widgets/ctk_notification.py @@ -0,0 +1,27 @@ +import customtkinter +class CTkNotification(customtkinter.CTkFrame): + def __init__(self, text, master, **kwargs): + self.master_color = master.cget("fg_color") + super().__init__(master=master, **kwargs, fg_color=self.master_color) + self.label = customtkinter.CTkLabel(self, text=text, text_color=self.master_color, width=200, wraplength=200, font=("Inter", 16)) + self.label.grid(row=0, column=0, sticky="nsew") + self.close_button = customtkinter.CTkButton( + self, width=40, text="X", text_color_disabled=self.master_color, command=self.hide, fg_color="transparent", state="disabled") + self.close_button.grid(row=0, column=1) + self.progress_bar = customtkinter.CTkProgressBar(self, determinate_speed=0.4, fg_color=self.master_color, progress_color=self.master_color) + self.progress_bar.grid(row=1, column=0, columnspan=2, sticky="nsew") + + def hide(self): + self.configure(fg_color="transparent") + self.progress_bar.stop() + self.progress_bar.configure(progress_color=self.master_color) + self.close_button.configure(state="disabled") + self.label.configure(text_color=self.master_color) + + def show(self): + self.configure(fg_color="green") + self.progress_bar.configure(progress_color="white") + self.progress_bar.set(0) + self.progress_bar.start() + self.close_button.configure(state="normal") + self.label.configure(text_color="white") \ No newline at end of file diff --git a/MCE/custom_widgets/ctk_templatedialog.py b/MCE/custom_widgets/ctk_templatedialog.py new file mode 100644 index 0000000..553bed0 --- /dev/null +++ b/MCE/custom_widgets/ctk_templatedialog.py @@ -0,0 +1,133 @@ +from typing import Union, Tuple, Optional +from customtkinter import CTkLabel, CTkEntry, CTkButton, ThemeManager, CTkToplevel, CTkFont, CTkOptionMenu + + +class CTkTemplateDialog(CTkToplevel): + """ + Dialog with extra window, message, entry widget, cancel and ok button. + For detailed information check out the documentation. + """ + + def __init__(self, + fg_color: Optional[Union[str, Tuple[str, str]]] = None, + text_color: Optional[Union[str, Tuple[str, str]]] = None, + button_fg_color: Optional[Union[str, Tuple[str, str]]] = None, + button_hover_color: Optional[Union[str, Tuple[str, str]]] = None, + button_text_color: Optional[Union[str, Tuple[str, str]]] = None, + entry_fg_color: Optional[Union[str, Tuple[str, str]]] = None, + entry_border_color: Optional[Union[str, Tuple[str, str]]] = None, + entry_text_color: Optional[Union[str, Tuple[str, str]]] = None, + + title: str = "CTkDialog", + font: Optional[Union[tuple, CTkFont]] = None, + text: str = "CTkDialog", + values = []): + + super().__init__(fg_color=fg_color) + + self._fg_color = ThemeManager.theme["CTkToplevel"]["fg_color"] if fg_color is None else self._check_color_type(fg_color) + self._text_color = ThemeManager.theme["CTkLabel"]["text_color"] if text_color is None else self._check_color_type(button_hover_color) + self._button_fg_color = ThemeManager.theme["CTkButton"]["fg_color"] if button_fg_color is None else self._check_color_type(button_fg_color) + self._button_hover_color = ThemeManager.theme["CTkButton"]["hover_color"] if button_hover_color is None else self._check_color_type(button_hover_color) + self._button_text_color = ThemeManager.theme["CTkButton"]["text_color"] if button_text_color is None else self._check_color_type(button_text_color) + self._entry_fg_color = ThemeManager.theme["CTkEntry"]["fg_color"] if entry_fg_color is None else self._check_color_type(entry_fg_color) + self._entry_border_color = ThemeManager.theme["CTkEntry"]["border_color"] if entry_border_color is None else self._check_color_type(entry_border_color) + self._entry_text_color = ThemeManager.theme["CTkEntry"]["text_color"] if entry_text_color is None else self._check_color_type(entry_text_color) + + self._user_input = ("", "") + self._running: bool = False + self._title = title + self._text = text + self._font = font + self._values = [""] + values + + self.title(self._title) + self.lift() # lift window on top + self.attributes("-topmost", True) # stay on top + self.protocol("WM_DELETE_WINDOW", self._on_closing) + self.after(10, self._create_widgets) # create widgets with slight delay, to avoid white flickering of background + self.resizable(False, False) + self.grab_set() # make other windows not clickable + + def _create_widgets(self): + self.grid_columnconfigure((0, 1), weight=1) + self.rowconfigure(0, weight=1) + + self._label = CTkLabel(master=self, + width=300, + wraplength=300, + fg_color="transparent", + text_color=self._text_color, + text=self._text, + font=self._font) + self._label.grid(row=0, column=0, columnspan=2, padx=20, pady=20, sticky="ew") + + self._entry = CTkEntry(master=self, + width=230, + fg_color=self._entry_fg_color, + border_color=self._entry_border_color, + text_color=self._entry_text_color, + font=self._font) + self._entry.grid(row=1, column=0, columnspan=2, padx=20, pady=(0, 20), sticky="ew") + + self._label2 = CTkLabel(master=self, + width=100, + wraplength=100, + fg_color="transparent", + text_color=self._text_color, + text="Import stages from: ", + font=self._font) + self._label2.grid(row=2, column=0, columnspan=1, padx=(20, 10), pady=(0, 20), sticky="ew") + + self._template_optionmenu = CTkOptionMenu(master=self, + width=100, + fg_color=self._button_fg_color, + button_hover_color=self._button_hover_color, + text_color=self._button_text_color, + font=self._font, + values=self._values + ) + self._template_optionmenu.grid(row=2, column=1, columnspan=1, padx=(10, 20), pady=(0, 20), sticky="ew") + + + self._ok_button = CTkButton(master=self, + width=100, + border_width=0, + fg_color=self._button_fg_color, + hover_color=self._button_hover_color, + text_color=self._button_text_color, + text='Ok', + font=self._font, + command=self._ok_event) + self._ok_button.grid(row=3, column=0, columnspan=1, padx=(20, 10), pady=(0, 20), sticky="ew") + + self._cancel_button = CTkButton(master=self, + width=100, + border_width=0, + fg_color=self._button_fg_color, + hover_color=self._button_hover_color, + text_color=self._button_text_color, + text='Cancel', + font=self._font, + command=self._cancel_event) + self._cancel_button.grid(row=3, column=1, columnspan=1, padx=(10, 20), pady=(0, 20), sticky="ew") + + self.after(150, lambda: self._entry.focus()) # set focus to entry with slight delay, otherwise it won't work + self._entry.bind("", self._ok_event) + + def _ok_event(self, event=None): + self._user_input = self._entry.get(), self._template_optionmenu.get() + self.grab_release() + self.destroy() + + def _on_closing(self): + self.grab_release() + self.destroy() + + def _cancel_event(self): + self.grab_release() + self.destroy() + + def get_input(self): + self.master.wait_window(self) + return self._user_input diff --git a/MCE/custom_widgets/ctk_timeentry.py b/MCE/custom_widgets/ctk_timeentry.py new file mode 100644 index 0000000..ee0aea0 --- /dev/null +++ b/MCE/custom_widgets/ctk_timeentry.py @@ -0,0 +1,36 @@ +import customtkinter +import tkinter as tk + +class CTkTimeEntry(customtkinter.CTkFrame): + def __init__(self, master=None, **kwargs): + super().__init__(master, **kwargs) + self.hour = tk.StringVar() + self.minute = tk.StringVar() + self.second = tk.StringVar() + + self.hour_entry = customtkinter.CTkEntry(self, width=50, textvariable=self.hour, validate="key", validatecommand=(self.register(self.validate_hour), '%P')) + self.hour_entry.pack(side=tk.LEFT) + + self.minute_entry = customtkinter.CTkEntry(self,width=50, textvariable=self.minute, validate="key", validatecommand=(self.register(self.validate_min_sec), '%P')) + self.minute_entry.pack(side=tk.LEFT) + + self.second_entry = customtkinter.CTkEntry(self, width=50, textvariable=self.second, validate="key", validatecommand=(self.register(self.validate_min_sec), '%P')) + self.second_entry.pack(side=tk.LEFT) + + def validate_hour(self, P): + return len(P) <= 2 and (P.isdigit() and int(P) <= 23 or P == "") + + def validate_min_sec(self, P): + return len(P) <= 2 and (P.isdigit() and int(P) <= 59 or P == "") + + def set(self, time_str): + h, m, s = map(str, time_str.split(':')) + self.hour.set(h) + self.minute.set(m) + self.second.set(s) + + def get(self): + h = self.hour.get() if self.hour.get() else "00" + m = self.minute.get() if self.minute.get() else "00" + s = self.second.get() if self.second.get() else "00" + return f"{h.zfill(2)}:{m.zfill(2)}:{s.zfill(2)}" \ No newline at end of file diff --git a/MCE/custom_widgets/ctk_tooltip.py b/MCE/custom_widgets/ctk_tooltip.py new file mode 100644 index 0000000..7304965 --- /dev/null +++ b/MCE/custom_widgets/ctk_tooltip.py @@ -0,0 +1,212 @@ +""" +CTkToolTip Widget +version: 0.8 +""" + +import time +import sys +import customtkinter +from tkinter import Toplevel, Frame + + +class CTkToolTip(Toplevel): + """ + Creates a ToolTip (pop-up) widget for customtkinter. + """ + + def __init__( + self, + widget: any = None, + message: str = None, + delay: float = 0.2, + follow: bool = True, + x_offset: int = +20, + y_offset: int = +10, + bg_color: str = None, + corner_radius: int = 10, + border_width: int = 0, + border_color: str = None, + alpha: float = 0.95, + padding: tuple = (10, 2), + **message_kwargs): + + super().__init__() + + self.widget = widget + + self.withdraw() + + # Disable ToolTip's title bar + self.overrideredirect(True) + + if sys.platform.startswith("win"): + self.transparent_color = self.widget._apply_appearance_mode( + customtkinter.ThemeManager.theme["CTkToplevel"]["fg_color"]) + self.attributes("-transparentcolor", self.transparent_color) + self.transient() + elif sys.platform.startswith("darwin"): + self.transparent_color = 'systemTransparent' + self.attributes("-transparent", True) + self.transient(self.master) + else: + self.transparent_color = '#000001' + corner_radius = 0 + self.transient() + + self.resizable(width=True, height=True) + + # Make the background transparent + self.config(background=self.transparent_color) + + # StringVar instance for msg string + self.messageVar = customtkinter.StringVar() + self.message = message + self.messageVar.set(self.message) + + self.delay = delay + self.follow = follow + self.x_offset = x_offset + self.y_offset = y_offset + self.corner_radius = corner_radius + self.alpha = alpha + self.border_width = border_width + self.padding = padding + self.bg_color = customtkinter.ThemeManager.theme["CTkFrame"]["fg_color"] if bg_color is None else bg_color + self.border_color = border_color + self.disable = False + + # visibility status of the ToolTip inside|outside|visible + self.status = "outside" + self.last_moved = 0 + self.attributes('-alpha', self.alpha) + + if sys.platform.startswith("win"): + if self.widget._apply_appearance_mode(self.bg_color) == self.transparent_color: + self.transparent_color = "#000001" + self.config(background=self.transparent_color) + self.attributes("-transparentcolor", self.transparent_color) + + # Add the message widget inside the tooltip + self.transparent_frame = Frame(self, bg=self.transparent_color) + self.transparent_frame.pack(padx=0, pady=0, fill="both", expand=True) + + self.frame = customtkinter.CTkFrame(self.transparent_frame, bg_color=self.transparent_color, + corner_radius=self.corner_radius, + border_width=self.border_width, fg_color=self.bg_color, + border_color=self.border_color) + self.frame.pack(padx=0, pady=0, fill="both", expand=True) + + self.message_label = customtkinter.CTkLabel(self.frame, textvariable=self.messageVar, **message_kwargs) + self.message_label.pack(fill="both", padx=self.padding[0] + self.border_width, + pady=self.padding[1] + self.border_width, expand=True) + + if self.widget.winfo_name() != "tk": + if self.frame.cget("fg_color") == self.widget.cget("bg_color"): + if not bg_color: + self._top_fg_color = self.frame._apply_appearance_mode( + customtkinter.ThemeManager.theme["CTkFrame"]["top_fg_color"]) + if self._top_fg_color != self.transparent_color: + self.frame.configure(fg_color=self._top_fg_color) + + # Add bindings to the widget without overriding the existing ones + self.widget.bind("", self.on_enter, add="+") + self.widget.bind("", self.on_leave, add="+") + self.widget.bind("", self.on_enter, add="+") + self.widget.bind("", self.on_enter, add="+") + self.widget.bind("", lambda _: self.hide(), add="+") + + def show(self) -> None: + """ + Enable the widget. + """ + self.disable = False + + def on_enter(self, event) -> None: + """ + Processes motion within the widget including entering and moving. + """ + + if self.disable: + return + self.last_moved = time.time() + + # Set the status as inside for the very first time + if self.status == "outside": + self.status = "inside" + + # If the follow flag is not set, motion within the widget will make the ToolTip dissapear + if not self.follow: + self.status = "inside" + self.withdraw() + + # Calculate available space on the right side of the widget relative to the screen + root_width = self.winfo_screenwidth() + widget_x = event.x_root + space_on_right = root_width - widget_x + + # Calculate the width of the tooltip's text based on the length of the message string + text_width = self.message_label.winfo_reqwidth() + + # Calculate the offset based on available space and text width to avoid going off-screen on the right side + offset_x = self.x_offset + if space_on_right < text_width + 20: # Adjust the threshold as needed + offset_x = -text_width - 20 # Negative offset when space is limited on the right side + + # Offsets the ToolTip using the coordinates od an event as an origin + self.geometry(f"+{event.x_root + offset_x}+{event.y_root + self.y_offset}") + + # Time is in integer: milliseconds + self.after(int(self.delay * 1000), self._show) + + def on_leave(self, event=None) -> None: + """ + Hides the ToolTip temporarily. + """ + + if self.disable: return + self.status = "outside" + self.withdraw() + + def _show(self) -> None: + """ + Displays the ToolTip. + """ + + if not self.widget.winfo_exists(): + self.hide() + self.destroy() + + if self.status == "inside" and time.time() - self.last_moved >= self.delay: + self.status = "visible" + self.deiconify() + + def hide(self) -> None: + """ + Disable the widget from appearing. + """ + if not self.winfo_exists(): + return + self.withdraw() + self.disable = True + + def is_disabled(self) -> None: + """ + Return the window state + """ + return self.disable + + def get(self) -> None: + """ + Returns the text on the tooltip. + """ + return self.messageVar.get() + + def configure(self, message: str = None, delay: float = None, bg_color: str = None, **kwargs): + """ + Set new message or configure the label parameters. + """ + if delay: self.delay = delay + if bg_color: self.frame.configure(fg_color=bg_color) + + self.messageVar.set(message) + self.message_label.configure(**kwargs) diff --git a/MCE/custom_widgets/ctkmessagebox.py b/MCE/custom_widgets/ctkmessagebox.py new file mode 100644 index 0000000..1a7465c --- /dev/null +++ b/MCE/custom_widgets/ctkmessagebox.py @@ -0,0 +1,448 @@ +""" +CustomTkinter Messagebox +Author: Akash Bora +Version: 2.5 +""" + +import customtkinter +from PIL import Image, ImageTk +import os +import sys +import time +from typing import Literal + +class CTkMessagebox(customtkinter.CTkToplevel): + ICONS = { + "check": None, + "cancel": None, + "info": None, + "question": None, + "warning": None + } + ICON_BITMAP = {} + def __init__(self, + master: any = None, + width: int = 400, + height: int = 200, + title: str = "CTkMessagebox", + message: str = "This is a CTkMessagebox!", + option_1: str = "OK", + option_2: str = None, + option_3: str = None, + options: list = [], + border_width: int = 1, + border_color: str = "default", + button_color: str = "default", + bg_color: str = "default", + fg_color: str = "default", + text_color: str = "default", + title_color: str = "default", + button_text_color: str = "default", + button_width: int = None, + button_height: int = None, + cancel_button_color: str = None, + cancel_button: str = None, # types: circle, cross or none + button_hover_color: str = "default", + icon: str = "info", + icon_size: tuple = None, + corner_radius: int = 15, + justify: str = "right", + font: tuple = None, + header: bool = False, + topmost: bool = True, + fade_in_duration: int = 0, + sound: bool = False, + option_focus: Literal[1, 2, 3] = None): + + super().__init__() + + + self.master_window = master + + self.width = 250 if width<250 else width + self.height = 150 if height<150 else height + + if self.master_window is None: + self.spawn_x = int((self.winfo_screenwidth()-self.width)/2) + self.spawn_y = int((self.winfo_screenheight()-self.height)/2) + else: + self.spawn_x = int(self.master_window.winfo_width() * .5 + self.master_window.winfo_x() - .5 * self.width + 7) + self.spawn_y = int(self.master_window.winfo_height() * .5 + self.master_window.winfo_y() - .5 * self.height + 20) + + self.after(10) + self.geometry(f"{self.width}x{self.height}+{self.spawn_x}+{self.spawn_y}") + self.title(title) + self.resizable(width=False, height=False) + self.fade = fade_in_duration + + if self.fade: + self.fade = 20 if self.fade<20 else self.fade + self.attributes("-alpha", 0) + + if not header: + self.overrideredirect(1) + + if topmost: + self.attributes("-topmost", True) + else: + self.transient(self.master_window) + + if sys.platform.startswith("win"): + self.transparent_color = self._apply_appearance_mode(self.cget("fg_color")) + self.attributes("-transparentcolor", self.transparent_color) + default_cancel_button = "cross" + elif sys.platform.startswith("darwin"): + self.transparent_color = 'systemTransparent' + self.attributes("-transparent", True) + default_cancel_button = "circle" + else: + self.transparent_color = '#000001' + corner_radius = 0 + default_cancel_button = "cross" + + self.lift() + + self.config(background=self.transparent_color) + self.protocol("WM_DELETE_WINDOW", self.button_event) + self.grid_columnconfigure(0, weight=1) + self.grid_rowconfigure(0, weight=1) + self.x = self.winfo_x() + self.y = self.winfo_y() + self._title = title + self.message = message + self.font = font + self.justify = justify + self.sound = sound + self.cancel_button = cancel_button if cancel_button else default_cancel_button + self.round_corners = corner_radius if corner_radius<=30 else 30 + self.button_width = button_width if button_width else self.width/4 + self.button_height = button_height if button_height else 28 + + if self.fade: self.attributes("-alpha", 0) + + if self.button_height>self.height/4: self.button_height = self.height/4 -20 + self.dot_color = cancel_button_color + self.border_width = border_width if border_width<6 else 5 + + if type(options) is list and len(options)>0: + try: + option_1 = options[-1] + option_2 = options[-2] + option_3 = options[-3] + except IndexError: None + + if bg_color=="default": + self.bg_color = self._apply_appearance_mode(customtkinter.ThemeManager.theme["CTkFrame"]["fg_color"]) + else: + self.bg_color = bg_color + + if fg_color=="default": + self.fg_color = self._apply_appearance_mode(customtkinter.ThemeManager.theme["CTkFrame"]["top_fg_color"]) + else: + self.fg_color = fg_color + + default_button_color = self._apply_appearance_mode(customtkinter.ThemeManager.theme["CTkButton"]["fg_color"]) + + if sys.platform.startswith("win"): + if self.bg_color==self.transparent_color or self.fg_color==self.transparent_color: + self.configure(fg_color="#000001") + self.transparent_color = "#000001" + self.attributes("-transparentcolor", self.transparent_color) + + if button_color=="default": + self.button_color = (default_button_color, default_button_color, default_button_color) + else: + if type(button_color) is tuple: + if len(button_color)==2: + self.button_color = (button_color[0], button_color[1], default_button_color) + elif len(button_color)==1: + self.button_color = (button_color[0], default_button_color, default_button_color) + else: + self.button_color = button_color + else: + self.button_color = (button_color, button_color, button_color) + + if text_color=="default": + self.text_color = self._apply_appearance_mode(customtkinter.ThemeManager.theme["CTkLabel"]["text_color"]) + else: + self.text_color = text_color + + if title_color=="default": + self.title_color = self._apply_appearance_mode(customtkinter.ThemeManager.theme["CTkLabel"]["text_color"]) + else: + self.title_color = title_color + + if button_text_color=="default": + self.bt_text_color = self._apply_appearance_mode(customtkinter.ThemeManager.theme["CTkButton"]["text_color"]) + else: + self.bt_text_color = button_text_color + + if button_hover_color=="default": + self.bt_hv_color = self._apply_appearance_mode(customtkinter.ThemeManager.theme["CTkButton"]["hover_color"]) + else: + self.bt_hv_color = button_hover_color + + if border_color=="default": + self.border_color = self._apply_appearance_mode(customtkinter.ThemeManager.theme["CTkFrame"]["border_color"]) + else: + self.border_color = border_color + + if icon_size: + self.size_height = icon_size[1] if icon_size[1]<=self.height-100 else self.height-100 + self.size = (icon_size[0], self.size_height) + else: + self.size = (self.height/4, self.height/4) + + self.icon = self.load_icon(icon, icon_size) if icon else None + + self.frame_top = customtkinter.CTkFrame(self, corner_radius=self.round_corners, width=self.width, border_width=self.border_width, + bg_color=self.transparent_color, fg_color=self.bg_color, border_color=self.border_color) + self.frame_top.grid(sticky="nswe") + + if button_width: + self.frame_top.grid_columnconfigure(0, weight=1) + else: + self.frame_top.grid_columnconfigure((1,2,3), weight=1) + + if button_height: + self.frame_top.grid_rowconfigure((0,1,3), weight=1) + else: + self.frame_top.grid_rowconfigure((0,1,2), weight=1) + + self.frame_top.bind("", self.move_window) + self.frame_top.bind("", self.oldxyset) + + if self.cancel_button=="cross": + self.button_close = customtkinter.CTkButton(self.frame_top, corner_radius=10, width=0, height=0, hover=False, border_width=0, + text_color=self.dot_color if self.dot_color else self.title_color, + text="✕", fg_color="transparent", command=self.button_event) + self.button_close.grid(row=0, column=5, sticky="ne", padx=5+self.border_width, pady=5+self.border_width) + elif self.cancel_button=="circle": + self.button_close = customtkinter.CTkButton(self.frame_top, corner_radius=10, width=10, height=10, hover=False, border_width=0, + text="", fg_color=self.dot_color if self.dot_color else "#c42b1c", command=self.button_event) + self.button_close.grid(row=0, column=5, sticky="ne", padx=10, pady=10) + + self.title_label = customtkinter.CTkLabel(self.frame_top, width=1, text=self._title, text_color=self.title_color, font=self.font) + self.title_label.grid(row=0, column=0, columnspan=6, sticky="nw", padx=(15,30), pady=5) + self.title_label.bind("", self.move_window) + self.title_label.bind("", self.oldxyset) + + self.info = customtkinter.CTkButton(self.frame_top, width=1, height=self.height/2, corner_radius=0, text=self.message, font=self.font, + fg_color=self.fg_color, hover=False, text_color=self.text_color, image=self.icon) + self.info._text_label.configure(wraplength=self.width/2, justify="left") + self.info.grid(row=1, column=0, columnspan=6, sticky="nwes", padx=self.border_width) + + if self.info._text_label.winfo_reqheight()>self.height/2: + height_offset = int((self.info._text_label.winfo_reqheight())-(self.height/2) + self.height) + self.geometry(f"{self.width}x{height_offset}") + + + self.option_text_1 = option_1 + + self.button_1 = customtkinter.CTkButton(self.frame_top, text=self.option_text_1, fg_color=self.button_color[0], + width=self.button_width, font=self.font, text_color=self.bt_text_color, + hover_color=self.bt_hv_color, height=self.button_height, + command=lambda: self.button_event(self.option_text_1)) + + + self.option_text_2 = option_2 + if option_2: + self.button_2 = customtkinter.CTkButton(self.frame_top, text=self.option_text_2, fg_color=self.button_color[1], + width=self.button_width, font=self.font, text_color=self.bt_text_color, + hover_color=self.bt_hv_color, height=self.button_height, + command=lambda: self.button_event(self.option_text_2)) + + self.option_text_3 = option_3 + if option_3: + self.button_3 = customtkinter.CTkButton(self.frame_top, text=self.option_text_3, fg_color=self.button_color[2], + width=self.button_width, font=self.font, text_color=self.bt_text_color, + hover_color=self.bt_hv_color, height=self.button_height, + command=lambda: self.button_event(self.option_text_3)) + + if self.justify=="center": + if button_width: + columns = [4,3,2] + span = 1 + else: + columns = [4,2,0] + span = 2 + if option_3: + self.frame_top.columnconfigure((0,1,2,3,4,5), weight=1) + self.button_1.grid(row=2, column=columns[0], columnspan=span, sticky="news", padx=(0,10), pady=10) + self.button_2.grid(row=2, column=columns[1], columnspan=span, sticky="news", padx=10, pady=10) + self.button_3.grid(row=2, column=columns[2], columnspan=span, sticky="news", padx=(10,0), pady=10) + elif option_2: + self.frame_top.columnconfigure((0,5), weight=1) + columns = [2,3] + self.button_1.grid(row=2, column=columns[0], sticky="news", padx=(0,5), pady=10) + self.button_2.grid(row=2, column=columns[1], sticky="news", padx=(5,0), pady=10) + else: + if button_width: + self.frame_top.columnconfigure((0,1,2,3,4,5), weight=1) + else: + self.frame_top.columnconfigure((0,2,4), weight=2) + self.button_1.grid(row=2, column=columns[1], columnspan=span, sticky="news", padx=(0,10), pady=10) + elif self.justify=="left": + self.frame_top.columnconfigure((0,1,2,3,4,5), weight=1) + if button_width: + columns = [0,1,2] + span = 1 + else: + columns = [0,2,4] + span = 2 + if option_3: + self.button_1.grid(row=2, column=columns[2], columnspan=span, sticky="news", padx=(0,10), pady=10) + self.button_2.grid(row=2, column=columns[1], columnspan=span, sticky="news", padx=10, pady=10) + self.button_3.grid(row=2, column=columns[0], columnspan=span, sticky="news", padx=(10,0), pady=10) + elif option_2: + self.button_1.grid(row=2, column=columns[1], columnspan=span, sticky="news", padx=10, pady=10) + self.button_2.grid(row=2, column=columns[0], columnspan=span, sticky="news", padx=(10,0), pady=10) + else: + self.button_1.grid(row=2, column=columns[0], columnspan=span, sticky="news", padx=(10,0), pady=10) + else: + self.frame_top.columnconfigure((0,1,2,3,4,5), weight=1) + if button_width: + columns = [5,4,3] + span = 1 + else: + columns = [4,2,0] + span = 2 + self.button_1.grid(row=2, column=columns[0], columnspan=span, sticky="news", padx=(0,10), pady=10) + if option_2: + self.button_2.grid(row=2, column=columns[1], columnspan=span, sticky="news", padx=10, pady=10) + if option_3: + self.button_3.grid(row=2, column=columns[2], columnspan=span, sticky="news", padx=(10,0), pady=10) + + if header: + self.title_label.configure(text="") + self.title_label.grid_configure(pady=0) + self.button_close.configure(text_color=self.bg_color) + self.frame_top.configure(corner_radius=0) + + if self.winfo_exists(): + self.grab_set() + + if self.sound: + self.bell() + + if self.fade: + self.fade_in() + + if option_focus: + self.option_focus = option_focus + self.focus_button(self.option_focus) + else: + if not self.option_text_2 and not self.option_text_3: + self.button_1.focus() + self.button_1.bind("", lambda event: self.button_event(self.option_text_1)) + + self.bind("", lambda e: self.button_event()) + + def focus_button(self, option_focus): + try: + self.selected_button = getattr(self, "button_"+str(option_focus)) + self.selected_button.focus() + self.selected_button.configure(border_color=self.bt_hv_color, border_width=3) + self.selected_option = getattr(self, "option_text_"+str(option_focus)) + self.selected_button.bind("", lambda event: self.button_event(self.selected_option)) + except AttributeError: + return + + self.bind("", lambda e: self.change_left()) + self.bind("", lambda e: self.change_right()) + + def change_left(self): + if self.option_focus==3: + return + + self.selected_button.unbind("") + self.selected_button.configure(border_width=0) + + if self.option_focus==1: + if self.option_text_2: + self.option_focus = 2 + + elif self.option_focus==2: + if self.option_text_3: + self.option_focus = 3 + + self.focus_button(self.option_focus) + + def change_right(self): + if self.option_focus==1: + return + + self.selected_button.unbind("") + self.selected_button.configure(border_width=0) + + if self.option_focus==2: + self.option_focus = 1 + + elif self.option_focus==3: + self.option_focus = 2 + + self.focus_button(self.option_focus) + + def load_icon(self, icon, icon_size): + if icon not in self.ICONS or self.ICONS[icon] is None: + if icon in ["check", "cancel", "info", "question", "warning"]: + image_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'icons', icon + '.png') + else: + image_path = icon + if icon_size: + size_height = icon_size[1] if icon_size[1] <= self.height - 100 else self.height - 100 + size = (icon_size[0], size_height) + else: + size = (self.height / 4, self.height / 4) + self.ICONS[icon] = customtkinter.CTkImage(Image.open(image_path), size=size) + self.ICON_BITMAP[icon] = ImageTk.PhotoImage(file=image_path) + self.after(200, lambda: self.iconphoto(False, self.ICON_BITMAP[icon])) + return self.ICONS[icon] + + def fade_in(self): + for i in range(0,110,10): + if not self.winfo_exists(): + break + self.attributes("-alpha", i/100) + self.update() + time.sleep(1/self.fade) + + def fade_out(self): + for i in range(100,0,-10): + if not self.winfo_exists(): + break + self.attributes("-alpha", i/100) + self.update() + time.sleep(1/self.fade) + + def get(self): + if self.winfo_exists(): + self.master.wait_window(self) + return self.event + + def oldxyset(self, event): + self.oldx = event.x + self.oldy = event.y + + def move_window(self, event): + self.y = event.y_root - self.oldy + self.x = event.x_root - self.oldx + self.geometry(f'+{self.x}+{self.y}') + + def button_event(self, event=None): + try: + self.button_1.configure(state="disabled") + self.button_2.configure(state="disabled") + self.button_3.configure(state="disabled") + except AttributeError: + pass + + if self.fade: + self.fade_out() + self.grab_release() + self.destroy() + self.event = event + +if __name__ == "__main__": + app = CTkMessagebox() + app.mainloop() diff --git a/MCE/custom_widgets/icons/cancel.png b/MCE/custom_widgets/icons/cancel.png new file mode 100644 index 0000000..dcae66e Binary files /dev/null and b/MCE/custom_widgets/icons/cancel.png differ diff --git a/MCE/custom_widgets/icons/check.png b/MCE/custom_widgets/icons/check.png new file mode 100644 index 0000000..dc0892a Binary files /dev/null and b/MCE/custom_widgets/icons/check.png differ diff --git a/MCE/custom_widgets/icons/info.png b/MCE/custom_widgets/icons/info.png new file mode 100644 index 0000000..4b38288 Binary files /dev/null and b/MCE/custom_widgets/icons/info.png differ diff --git a/MCE/custom_widgets/icons/question.png b/MCE/custom_widgets/icons/question.png new file mode 100644 index 0000000..24f77c3 Binary files /dev/null and b/MCE/custom_widgets/icons/question.png differ diff --git a/MCE/custom_widgets/icons/warning.png b/MCE/custom_widgets/icons/warning.png new file mode 100644 index 0000000..bc6aea6 Binary files /dev/null and b/MCE/custom_widgets/icons/warning.png differ diff --git a/MCE/utils.py b/MCE/utils.py new file mode 100644 index 0000000..3647a88 --- /dev/null +++ b/MCE/utils.py @@ -0,0 +1,92 @@ +import customtkinter +import json +import sys +from MCE.custom_widgets.ctk_notification import CTkNotification + +class Config: + def __init__(self, linker, config_file): + self.linker = linker + self.config_file = config_file + self.config_data = self.read() + self.linker.widgets = self.set_values_to_none(self.config_data) + self.locked = False + linker.config = self + + def read(self): + # Read the JSON file + try: + with open(self.config_file, 'r') as json_file: + config_data = json.load(json_file) + return config_data + except FileNotFoundError: + print(f"Config file '{self.config_file}' not found.") + sys.exit(1) + except json.JSONDecodeError: + print(f"Invalid JSON format in '{self.config_file}'.") + sys.exit(1) + + def set_values_to_none(self, input_dict): + result = {} + for key, value in input_dict.items(): + if isinstance(value, dict): + result[key] = self.set_values_to_none(value) + else: + result[key] = None + return result + + def load_config(self, widgets=None, config_data=None): + if widgets == None: + widgets = self.linker.widgets + config_data = self.config_data + for key in widgets: + if isinstance(widgets[key], dict) and isinstance(config_data[key], dict): + self.load_config(widgets[key], config_data[key]) + else: + if widgets[key] is not None: + if isinstance(widgets[key], customtkinter.CTkCheckBox): + if config_data[key] == True: + widgets[key].select() + else: + widgets[key].deselect() + elif isinstance(widgets[key], customtkinter.CTkEntry): + widgets[key].insert(0, config_data[key]) + else: + widgets[key].set(config_data[key]) + + def save_to_json(self, list_keys): + widget = self.linker.widgets + data = self.config_data + for i in list_keys[:-1]: + widget = widget[i] + data = data[i] + widget = widget[list_keys[-1]] + value = widget.get() + if isinstance(widget, customtkinter.CTkCheckBox): + value = True if value==1 else False + data[list_keys[-1]] = value + self.save_file("Configuration") + + def save_file(self, name=None): + if self.locked: + with open("MCE\config.json", "r") as config_file: + new_config = json.load(config_file) + self.config_data["Queue"] = new_config["Queue"] + self.config_data["LastRun"] = new_config["LastRun"] + with open("MCE\config.json", "w") as config_file: + json.dump(self.config_data, config_file, indent=2) + if name: + self.linker.show_notification(name) + +class Linker: + def __init__(self): + self.capitalise = lambda word: " ".join(x.title() for x in word.split("_")) + self.config = None + self.widgets = {} + self.sidebar = None + self.event_id = None + + def show_notification(self, text): + if self.event_id: + self.sidebar.after_cancel(self.event_id) + self.sidebar.notification.show() + self.event_id = self.sidebar.after(2500, self.sidebar.notification.hide) diff --git a/aas.py b/aas.py index 311a24c..e95cd3e 100644 --- a/aas.py +++ b/aas.py @@ -58,6 +58,10 @@ class ArisuAutoSweeper(AzurLaneAutoScript): from tasks.momotalk.momotalk import MomoTalk MomoTalk(config=self.config, device=self.device).run() + def mission(self): + from tasks.mission.mission import Mission + Mission(config=self.config, device=self.device).run() + def data_update(self): from tasks.item.data_update import DataUpdate DataUpdate(config=self.config, device=self.device).run() diff --git a/assets/en/base/page/WORK_GO_TO_TACTICAL_CHALLENGE.png b/assets/en/base/page/WORK_GO_TO_TACTICAL_CHALLENGE.png index 8700d5a..8b3a753 100644 Binary files a/assets/en/base/page/WORK_GO_TO_TACTICAL_CHALLENGE.png and b/assets/en/base/page/WORK_GO_TO_TACTICAL_CHALLENGE.png differ diff --git a/assets/en/mission/CHECK_BD.png b/assets/en/mission/CHECK_BD.png new file mode 100644 index 0000000..c3413b1 Binary files /dev/null and b/assets/en/mission/CHECK_BD.png differ diff --git a/assets/en/mission/CHECK_COMMISSIONS.png b/assets/en/mission/CHECK_COMMISSIONS.png new file mode 100644 index 0000000..a4e92e6 Binary files /dev/null and b/assets/en/mission/CHECK_COMMISSIONS.png differ diff --git a/assets/en/mission/CHECK_IR.png b/assets/en/mission/CHECK_IR.png new file mode 100644 index 0000000..6ba97cd Binary files /dev/null and b/assets/en/mission/CHECK_IR.png differ diff --git a/assets/en/mission/CHECK_MISSION_SWEEP.png b/assets/en/mission/CHECK_MISSION_SWEEP.png new file mode 100644 index 0000000..df4347e Binary files /dev/null and b/assets/en/mission/CHECK_MISSION_SWEEP.png differ diff --git a/assets/en/mission/HARD_OFF.png b/assets/en/mission/HARD_OFF.png new file mode 100644 index 0000000..e41bc2a Binary files /dev/null and b/assets/en/mission/HARD_OFF.png differ diff --git a/assets/en/mission/HARD_ON.png b/assets/en/mission/HARD_ON.png new file mode 100644 index 0000000..7f7dc90 Binary files /dev/null and b/assets/en/mission/HARD_ON.png differ diff --git a/assets/en/mission/LEFT.png b/assets/en/mission/LEFT.png new file mode 100644 index 0000000..f1bc365 Binary files /dev/null and b/assets/en/mission/LEFT.png differ diff --git a/assets/en/mission/NORMAL_OFF.png b/assets/en/mission/NORMAL_OFF.png new file mode 100644 index 0000000..2a5fab9 Binary files /dev/null and b/assets/en/mission/NORMAL_OFF.png differ diff --git a/assets/en/mission/NORMAL_ON.png b/assets/en/mission/NORMAL_ON.png new file mode 100644 index 0000000..39b0af3 Binary files /dev/null and b/assets/en/mission/NORMAL_ON.png differ diff --git a/assets/en/mission/OCR_AREA.png b/assets/en/mission/OCR_AREA.png new file mode 100644 index 0000000..7602382 Binary files /dev/null and b/assets/en/mission/OCR_AREA.png differ diff --git a/assets/en/mission/RIGHT.png b/assets/en/mission/RIGHT.png new file mode 100644 index 0000000..e4c0e74 Binary files /dev/null and b/assets/en/mission/RIGHT.png differ diff --git a/assets/en/mission/SELECT_BD.png b/assets/en/mission/SELECT_BD.png new file mode 100644 index 0000000..b09f052 Binary files /dev/null and b/assets/en/mission/SELECT_BD.png differ diff --git a/assets/en/mission/SELECT_IR.png b/assets/en/mission/SELECT_IR.png new file mode 100644 index 0000000..39c493d Binary files /dev/null and b/assets/en/mission/SELECT_IR.png differ diff --git a/config/template.json b/config/template.json index 887e481..32f08f8 100644 --- a/config/template.json +++ b/config/template.json @@ -172,12 +172,20 @@ "Enable": true, "NextRun": "2020-01-01 00:00:00", "Command": "TacticalChallenge", - "ServerUpdate": "15:00" + "ServerUpdate": "14:00" }, "TacticalChallenge": { "PlayerSelect": 0 } }, + "Mission": { + "Scheduler": { + "Enable": false, + "NextRun": "2020-01-01 00:00:00", + "Command": "Mission", + "ServerUpdate": "04:00" + } + }, "Circle": { "Scheduler": { "Enable": true, diff --git a/module/config/argument/args.json b/module/config/argument/args.json index bb8d15d..e3acdf9 100644 --- a/module/config/argument/args.json +++ b/module/config/argument/args.json @@ -730,7 +730,7 @@ }, "ServerUpdate": { "type": "input", - "value": "15:00", + "value": "14:00", "display": "hide" } }, @@ -747,6 +747,33 @@ } } }, + "Mission": { + "Scheduler": { + "Enable": { + "type": "checkbox", + "value": false, + "option": [ + true, + false + ] + }, + "NextRun": { + "type": "datetime", + "value": "2020-01-01 00:00:00", + "validate": "datetime" + }, + "Command": { + "type": "input", + "value": "Mission", + "display": "hide" + }, + "ServerUpdate": { + "type": "input", + "value": "04:00", + "display": "hide" + } + } + }, "Circle": { "Scheduler": { "Enable": { diff --git a/module/config/argument/menu.json b/module/config/argument/menu.json index e0e2e9a..82c45c1 100644 --- a/module/config/argument/menu.json +++ b/module/config/argument/menu.json @@ -22,7 +22,8 @@ "tasks": [ "Bounty", "Scrimmage", - "TacticalChallenge" + "TacticalChallenge", + "Mission" ] }, "Reward": { diff --git a/module/config/argument/task.yaml b/module/config/argument/task.yaml index 33dc2ea..12c9a0a 100644 --- a/module/config/argument/task.yaml +++ b/module/config/argument/task.yaml @@ -55,6 +55,8 @@ Farm: TacticalChallenge: - Scheduler - TacticalChallenge + Mission: + - Scheduler # ==================== Rewards ==================== diff --git a/module/config/config_manual.py b/module/config/config_manual.py index d1fe26f..e2b7a06 100644 --- a/module/config/config_manual.py +++ b/module/config/config_manual.py @@ -9,7 +9,7 @@ class ManualConfig: SCHEDULER_PRIORITY = """ Restart > Cafe > TacticalChallenge > Circle > Mail - > DataUpdate > Bounty > Scrimmage > Task > Shop > Momotalk + > DataUpdate > Bounty > Scrimmage > Task > Shop > Mission > Momotalk """ """ diff --git a/module/config/i18n/en-US.json b/module/config/i18n/en-US.json index 258def7..6d3bf99 100644 --- a/module/config/i18n/en-US.json +++ b/module/config/i18n/en-US.json @@ -50,6 +50,10 @@ "name": "Tactical Challenge", "help": "" }, + "Mission": { + "name": "Mission/Commissions/Event", + "help": "Open MCE Manager for additional settings" + }, "Circle": { "name": "Club", "help": "" diff --git a/module/config/i18n/zh-CN.json b/module/config/i18n/zh-CN.json index 07927ee..4ab9e1f 100644 --- a/module/config/i18n/zh-CN.json +++ b/module/config/i18n/zh-CN.json @@ -50,6 +50,10 @@ "name": "战术对抗赛", "help": "战术大赛 / 竞技场" }, + "Mission": { + "name": "Task.Mission.name", + "help": "Task.Mission.help" + }, "Circle": { "name": "公会", "help": "社团 / 小组" diff --git a/tasks/base/assets/assets_base_page.py b/tasks/base/assets/assets_base_page.py index 1d9bf47..f24ba41 100644 --- a/tasks/base/assets/assets_base_page.py +++ b/tasks/base/assets/assets_base_page.py @@ -538,9 +538,9 @@ WORK_GO_TO_TACTICAL_CHALLENGE = ButtonWrapper( ), en=Button( file='./assets/en/base/page/WORK_GO_TO_TACTICAL_CHALLENGE.png', - area=(1034, 435, 1162, 466), - search=(1014, 415, 1182, 486), - color=(179, 199, 221), - button=(1034, 435, 1162, 466), + area=(1012, 532, 1152, 591), + search=(992, 512, 1172, 611), + color=(199, 211, 227), + button=(1012, 532, 1152, 591), ), ) diff --git a/tasks/mission/assets/assets_mission.py b/tasks/mission/assets/assets_mission.py new file mode 100644 index 0000000..4f591e5 --- /dev/null +++ b/tasks/mission/assets/assets_mission.py @@ -0,0 +1,148 @@ +from module.base.button import Button, ButtonWrapper + +# This file was auto-generated, do not modify it manually. To generate: +# ``` python -m dev_tools.button_extract ``` + +CHECK_BD = ButtonWrapper( + name='CHECK_BD', + jp=None, + en=Button( + file='./assets/en/mission/CHECK_BD.png', + area=(94, 135, 325, 194), + search=(74, 115, 345, 214), + color=(208, 215, 220), + button=(94, 135, 325, 194), + ), +) +CHECK_COMMISSIONS = ButtonWrapper( + name='CHECK_COMMISSIONS', + jp=None, + en=Button( + file='./assets/en/mission/CHECK_COMMISSIONS.png', + area=(646, 78, 909, 135), + search=(626, 58, 929, 155), + color=(70, 96, 124), + button=(646, 78, 909, 135), + ), +) +CHECK_IR = ButtonWrapper( + name='CHECK_IR', + jp=None, + en=Button( + file='./assets/en/mission/CHECK_IR.png', + area=(97, 137, 340, 191), + search=(77, 117, 360, 211), + color=(213, 220, 223), + button=(97, 137, 340, 191), + ), +) +CHECK_MISSION_SWEEP = ButtonWrapper( + name='CHECK_MISSION_SWEEP', + jp=None, + en=Button( + file='./assets/en/mission/CHECK_MISSION_SWEEP.png', + area=(654, 184, 703, 209), + search=(634, 164, 723, 229), + color=(208, 213, 219), + button=(654, 184, 703, 209), + ), +) +HARD_OFF = ButtonWrapper( + name='HARD_OFF', + jp=None, + en=Button( + file='./assets/en/mission/HARD_OFF.png', + area=(947, 132, 1193, 182), + search=(927, 112, 1213, 202), + color=(242, 246, 248), + button=(947, 132, 1193, 182), + ), +) +HARD_ON = ButtonWrapper( + name='HARD_ON', + jp=None, + en=Button( + file='./assets/en/mission/HARD_ON.png', + area=(940, 133, 1189, 186), + search=(920, 113, 1209, 206), + color=(200, 71, 63), + button=(940, 133, 1189, 186), + ), +) +LEFT = ButtonWrapper( + name='LEFT', + jp=None, + en=Button( + file='./assets/en/mission/LEFT.png', + area=(0, 301, 89, 408), + search=(0, 281, 109, 428), + color=(193, 224, 241), + button=(0, 301, 89, 408), + ), +) +NORMAL_OFF = ButtonWrapper( + name='NORMAL_OFF', + jp=None, + en=Button( + file='./assets/en/mission/NORMAL_OFF.png', + area=(682, 135, 927, 182), + search=(662, 115, 947, 202), + color=(238, 243, 246), + button=(682, 135, 927, 182), + ), +) +NORMAL_ON = ButtonWrapper( + name='NORMAL_ON', + jp=None, + en=Button( + file='./assets/en/mission/NORMAL_ON.png', + area=(682, 137, 924, 185), + search=(662, 117, 944, 205), + color=(62, 81, 89), + button=(682, 137, 924, 185), + ), +) +OCR_AREA = ButtonWrapper( + name='OCR_AREA', + jp=None, + en=Button( + file='./assets/en/mission/OCR_AREA.png', + area=(108, 176, 176, 219), + search=(88, 156, 196, 239), + color=(237, 238, 240), + button=(108, 176, 176, 219), + ), +) +RIGHT = ButtonWrapper( + name='RIGHT', + jp=None, + en=Button( + file='./assets/en/mission/RIGHT.png', + area=(1202, 311, 1280, 412), + search=(1182, 291, 1280, 432), + color=(193, 223, 241), + button=(1202, 311, 1280, 412), + ), +) +SELECT_BD = ButtonWrapper( + name='SELECT_BD', + jp=None, + en=Button( + file='./assets/en/mission/SELECT_BD.png', + area=(1016, 165, 1227, 211), + search=(996, 145, 1247, 231), + color=(205, 212, 220), + button=(1016, 165, 1227, 211), + ), +) +SELECT_IR = ButtonWrapper( + name='SELECT_IR', + jp=None, + en=Button( + file='./assets/en/mission/SELECT_IR.png', + area=(1004, 267, 1237, 321), + search=(984, 247, 1257, 341), + color=(214, 220, 227), + button=(1004, 267, 1237, 321), + ), +) diff --git a/tasks/mission/mission.py b/tasks/mission/mission.py new file mode 100644 index 0000000..9b6ccc0 --- /dev/null +++ b/tasks/mission/mission.py @@ -0,0 +1,260 @@ +from enum import Enum + +from module.base.timer import Timer +from module.exception import RequestHumanTakeover +from module.logger import logger +from tasks.mission.ui import MissionUI, CommissionsUI +from tasks.stage.ap import AP +from tasks.cafe.cafe import Cafe +from tasks.circle.circle import Circle +from tasks.task.task import Task +from tasks.mail.mail import Mail +from tasks.item.data_update import DataUpdate +import json +import math +from filelock import FileLock +from datetime import datetime + +class MissionStatus(Enum): + AP = 0 # Calculate AP and decide to terminate Mission module or not + NAVIGATE = 1 # Navigate to the stage page for example the commissions page or mission page + SELECT = 2 # Select the stage mode for example hard or normal in mission + ENTER = 3 # Search and for the stage in the stage list and enter + SWEEP = 4 # Sweep the stage + RECHARGE = 5 # Recharge AP from other taks if they are enabled + FINISH = -1 # Inidicate termination of Mission module + + +class Mission(MissionUI, CommissionsUI): + _stage_ap = [10, 15, 15, 15] + + @property + def stage_ap(self): + return self._stage_ap + + @property + def mission_info(self) -> list: + """ + Read the config from MCE/config.json and extract the queue, a list of list. + If queue is empty repopulate from preferred template. + + Format of each element in queue: [mode, stage, sweep_num] + e.g. ["N", "1-1", 3] + + Mode Acronyms: + "N" : Normal Mission + "H" : Hard Mission + "E" : Event Quest + "IR" : Item Retrieval / Commission where you get credit + "BD" : Base Defense / Commission where you get exp + + Returns: + list of list + """ + queue = [] + try: + with open("MCE/config.json") as json_file: + config_data = json.load(json_file) + queue = config_data["Queue"] + self.recharge_AP = config_data["RechargeAP"] + self.reset_daily = config_data["ResetDaily"] + self.reset_time = config_data["ResetTime"] + self.last_run = config_data["LastRun"] + self.event = config_data["Event"] + if self.check_reset_daily() or not queue: + preferred_template = config_data["PreferredTemplate"] + queue = config_data["Templates"][preferred_template] + if not self.event: + queue = [x for x in queue if x[0] != "E"] + except: + logger.error("Failed to read configuration file") + finally: + return queue + + def check_reset_daily(self): + # Check if it's time to reset the queue + if self.reset_daily: + current_datetime = datetime.now().replace(microsecond=0) # Round to the nearest second + current_date = current_datetime.date() + current_time = current_datetime.time() + last_run_datetime = datetime.strptime(self.last_run, "%Y-%m-%d %H:%M:%S") + reset_time = datetime.strptime(self.reset_time, "%H:%M:%S").time() + + if current_date != last_run_datetime.date() and current_time >= reset_time: + self.last_run = str(datetime.now().replace(microsecond=0)) + logger.info("Reset Daily activated.") + return True + return False + + @property + def valid_task(self) -> list: + task = self.mission_info + if not task: + logger.warning('Mission enabled but no task set') + #self.error_handler() + return task + + @property + def current_mode(self): + return self.task[0][0] + + @property + def current_stage(self): + return self.task[0][1] + + @property + def current_count(self): + if self.current_mode == "H" and self.task[0][2] > 3: + return 3 + return self.task[0][2] + + @current_count.setter + def current_count(self, value): + self.task[0][2] = value + + def select(self) -> bool: + """ + A wrapper method to select the current_mode + by calling the specific method based on its type. + + Return + True if selection happens without any problem, False otherwise. + """ + if self.current_mode in ["N", "H"]: + return self.select_mission(self.current_mode, self.current_stage) + elif self.current_mode in ["BD", "IR"]: + return self.select_commission(self.current_mode) + elif self.current_mode == "E": + #return self.select_mode(SWITCH_QUEST) + logger.error("Event not yet implemented") + return False + else: + logger.error("Uknown mode") + return False + + def get_realistic_count(self) -> int: + """ + Calculate the possible number of sweeps based on the current AP + """ + ap_cost = 20 if self.current_mode == "H" else 10 + required_ap = ap_cost * self.current_count + return math.floor(min(required_ap, self.current_ap) / ap_cost) + + def update_task(self, failure=False): + """ + Update self.task and save the current state of the queue in + MCE/config.json. + """ + try: + if failure or self.current_count == self.realistic_count: + self.previous_mode = self.current_mode + self.task.pop(0) + else: + self.previous_mode = None + self.current_count -= self.realistic_count + with open("MCE/config.json", "r") as json_file: + config_data = json.load(json_file) + with open("MCE/config.json", "w") as json_file: + config_data["Queue"] = self.task + config_data["LastRun"] = self.last_run + json.dump(config_data, json_file, indent=2) + except: + logger.error("Failed to save configuration") + self.task = [] + + def update_ap(self): + ap_cost = 20 if self.current_mode == "H" else 10 + ap = self.config.stored.AP + ap_old = ap.value + ap_new = ap_old - ap_cost * self.realistic_count + ap.set(ap_new, ap.total) + logger.info(f'Set AP: {ap_old} -> {ap_new}') + + def recharge(self) -> bool: + """ + Check if AP related modules such as cafe, circle, task, mail are enabled and run them if they are. + task_call only works after the current task is finished so is not suitable. + """ + cafe_reward = self.config.cross_get(["Cafe", "Scheduler", "Enable"]) and self.config.cross_get(["Cafe", "Cafe", "Reward"]) + circle = self.config.cross_get(["Circle", "Scheduler", "Enable"]) + task = self.config.cross_get(["Task", "Scheduler", "Enable"]) + mail = self.config.cross_get(["Mail", "Scheduler", "Enable"]) + ap_tasks = [(cafe_reward,Cafe), (circle, Circle), (task, Task), (mail, Mail)] + modules = [x[1] for x in ap_tasks if x[0]] + if not modules: + logger.info("Recharge AP was enabled but no AP related modules were enabled") + return False + for module in modules: + module(config=self.config, device=self.device).run() + return True + + def handle_mission(self, status): + match status: + case MissionStatus.AP: + if not self.task: + return MissionStatus.FINISH + self.realistic_count = self.get_realistic_count() + if self.realistic_count == 0 and self.recharge_AP: + self.recharge_AP = False + return MissionStatus.RECHARGE + elif self.realistic_count == 0 and not self.recharge_AP: + return MissionStatus.FINISH + else: + return MissionStatus.NAVIGATE + case MissionStatus.NAVIGATE: + self.navigate(self.previous_mode, self.current_mode) + return MissionStatus.SELECT + case MissionStatus.SELECT: + if self.select(): + return MissionStatus.ENTER + self.update_task(failure=True) + return MissionStatus.AP + case MissionStatus.ENTER: + if self.enter_stage(self.current_stage): + return MissionStatus.SWEEP + self.update_task(failure=True) + return MissionStatus.AP + case MissionStatus.SWEEP: + if self.do_sweep(self.current_mode, self.realistic_count): + self.update_ap() + self.update_task() + else: + self.update_task(failure=True) + return MissionStatus.AP + case MissionStatus.RECHARGE: + if self.recharge(): + DataUpdate(config=self.config, device=self.device).run() + return MissionStatus.AP + return MissionStatus.FINISH + case MissionStatus.FINISH: + return status + case _: + logger.warning(f'Invalid status: {status}') + return status + + def run(self): + self.lock = FileLock("MCE/config.json.lock") + with self.lock.acquire(): + self.previous_mode = None + self.task = self.valid_task + action_timer = Timer(0.5, 1) + status = MissionStatus.AP + + """Update the dashboard to accurately calculate AP""" + DataUpdate(config=self.config, device=self.device).run() + + while 1: + self.device.screenshot() + + if self.ui_additional(): + continue + + if action_timer.reached_and_reset(): + logger.attr('Status', status) + status = self.handle_mission(status) + + if status == MissionStatus.FINISH: + break + + self.config.task_delay(server_update=True) + \ No newline at end of file diff --git a/tasks/mission/ui.py b/tasks/mission/ui.py new file mode 100644 index 0000000..2be9c0c --- /dev/null +++ b/tasks/mission/ui.py @@ -0,0 +1,147 @@ +from module.base.timer import Timer +from module.logger import logger +from module.ui.switch import Switch +from module.ocr.ocr import Digit +from tasks.base.assets.assets_base_page import BACK, MISSION_CHECK +from tasks.base.page import page_mission, page_commissions +from tasks.base.ui import UI +from tasks.mission.assets.assets_mission import * +from tasks.stage.ap import AP +from tasks.stage.mission_list import StageList +from tasks.stage.sweep import StageSweep + + +SHARED_LIST = StageList('SharedList') +MISSION_SWEEP = StageSweep('MissionSweep', 60) +MISSION_SWEEP.set_button(button_check=CHECK_MISSION_SWEEP) # Check sweep is different for mission +SHARED_SWEEP = StageSweep('SharedSweep', 60) + +SWITCH_NORMAL = Switch("Normal_switch") +SWITCH_NORMAL.add_state("on", NORMAL_ON) +SWITCH_NORMAL.add_state("off", NORMAL_OFF) + +SWITCH_HARD = Switch("HARD_switch") +SWITCH_HARD.add_state("on", HARD_ON) +SWITCH_HARD.add_state("off", HARD_OFF) + +SWITCH_QUEST = None + +""" +A dictionary that maps the mode to a tuple where +the first element is an argument to go_back and second is for ui_ensure +Missing for "E" because there are no event in Global and no page_event +""" +MODE_TO_PAGE = { + "N": (MISSION_CHECK, page_mission), + "H": (MISSION_CHECK, page_mission), + "BD": (CHECK_BD, page_commissions), + "IR": (CHECK_IR, page_commissions), + "E" : () +} + + +class MissionUI(UI, AP): + def select_mission(self, mode, stage): + area = int(stage.split("-")[0]) + if not self.select_area(area): + logger.warning("Area not found") + return False + + to_switch = { + "N": SWITCH_NORMAL, + "H": SWITCH_HARD + } + switch = to_switch[mode] + if not self.select_mode(switch) and not self.select_area(area): + return False + return True + + def select_area(self, num): + """" + May require further error handling for these cases. + 1. Fails to ocr area number + 2. May trigger too many click exception when clicking left or right too many times + 3. Area not unlocked. Simplest way if left or right button are still present + but problem is it is expensive to check every time and they always keep moving. + """ + tries = 0 + ocr_area = Digit(OCR_AREA) + while 1: + try: + self.device.screenshot() + current_area = int(ocr_area.ocr_single_line(self.device.image)) + if current_area == num: + return True + elif current_area > num: + [self.click_with_interval(LEFT, interval=1) for x in range(abs(current_area-num))] + elif current_area < num: + [self.click_with_interval(RIGHT, interval=1) for x in range(abs(current_area-num))] + except: + tries += 1 + if tries > 5: + return False + + def select_mode(self, switch): + """ + Set switch to on. + Returns: + True if switch is set, False if switch not found + """ + if not switch.appear(main=self): + logger.info(f'{switch.name} not found') + return False + switch.set('on', main=self) + return True + + def enter_stage(self, index: int) -> bool: + if not index: + index = SHARED_LIST.insight_max_sweepable_index(self) + if SHARED_LIST.select_index_enter(self, index): + return True + return False + + def do_sweep(self, mode, num: int) -> bool: + if mode in ["N", "H"]: + return MISSION_SWEEP.do_sweep(self, num=num) + else: + return SHARED_SWEEP.do_sweep(self, num=num) + + def navigate(self, prev, next): + """ + go_back is called when the previous stage and next stage are in + the same game mode. + For example, "N" and "H" are in Mission so we call go_back. + If different, ui_ensure is called for example, "N" and "IR". + """ + if prev==next or (prev in ["N", "H"] and next in ["N", "H"]): + self.go_back(MODE_TO_PAGE[next][0]) + elif prev in ["BD", "IR"] and next in ["BD", "IR"]: + self.go_back(CHECK_COMMISSIONS) + else: + self.ui_ensure(MODE_TO_PAGE[next][1]) + + def go_back(self, check): + while 1: + self.device.screenshot() + if self.match_color(check) and self.appear(check): + return True + self.click_with_interval(BACK, interval=2) + +class CommissionsUI(UI, AP): + """Works the same way as select_bounty""" + def select_commission(self, mode): + to_button = { + "IR": (SELECT_IR, CHECK_IR), + "BD": (SELECT_BD, CHECK_BD) + } + dest_enter, dest_check = to_button[mode] + timer = Timer(5, 10).start() + while 1: + self.device.screenshot() + self.appear_then_click(dest_enter, interval=1) + if self.appear(dest_check): + return True + if timer.reached(): + return False + + \ No newline at end of file diff --git a/tasks/stage/mission_list.py b/tasks/stage/mission_list.py new file mode 100644 index 0000000..52896f3 --- /dev/null +++ b/tasks/stage/mission_list.py @@ -0,0 +1,252 @@ +import re + +import numpy as np + +from module.base.base import ModuleBase +from module.base.timer import Timer +from module.base.utils import area_pad, area_size, area_offset +from module.logger import logger +from module.ocr.ocr import Ocr +from tasks.stage.assets.assets_stage_list import * + + +class StageList: + swipe_vector_range = (0.65, 0.70) + + def __init__( + self, + name, + button_list: ButtonWrapper = None, + button_index: ButtonWrapper = None, + button_item: ButtonWrapper = None, + button_enter: ButtonWrapper = None, + button_stars: ButtonWrapper = None, + swipe_direction: str = "down" + ): + self.name = name + self.stage = button_list if button_list else STAGE_LIST + self.index_ocr = Ocr(button_index if button_index else OCR_INDEX, lang='en') + self.stage_item = (button_item if button_item else STAGE_ITEM).button + self.enter = button_enter if button_enter else STAGE_ENTER + self.sweepable = button_stars if button_stars else STAGE_STARS + self.swipe_direction = swipe_direction + + self.current_index_min = 1 + self.current_index_max = 1 + self.current_indexes: list[tuple[str, tuple]] = [] + + def __str__(self): + return f'StageList({self.name})' + + __repr__ = __str__ + + def __eq__(self, other): + return str(self) == str(other) + + def __hash__(self): + return hash(self.name) + + @property + def _indexes(self) -> list[int]: + return [x[0] for x in self.current_indexes] + + def load_stage_indexes(self, main: ModuleBase): + self.current_indexes = list( + filter( + lambda x: re.match(r'^\d{1,2}-?\d?$', x[0]) and x[0] != '00', + map(lambda x: (x.ocr_text, x.box), self.index_ocr.detect_and_ocr(main.device.image)) + ) + ) + if not self.current_indexes: + logger.warning(f'No valid index in {self.index_ocr.name}') + return + indexes = self._indexes + + self.current_index_min = min(indexes) + self.current_index_max = max(indexes) + logger.attr(self.index_ocr.name, f'Index range: {self.current_index_min} - {self.current_index_max}') + + def swipe_page(self, direction: str, main: ModuleBase, vector_range=None, reverse=False): + """ + Args: + direction: up, down + main: + vector_range (tuple[float, float]): + reverse (bool): + """ + if vector_range is None: + vector_range = self.swipe_vector_range + vector = np.random.uniform(*vector_range) + width, height = area_size(self.stage.button) + if direction == 'up': + vector = (0, vector * height) + elif direction == 'down': + vector = (0, -vector * height) + else: + logger.warning(f'Unknown swipe direction: {direction}') + return + + if reverse: + vector = (-vector[0], -vector[1]) + main.device.swipe_vector(vector, self.stage.button, name=f'{self.name}_SWIPE') + + def insight_index(self, index: int, main: ModuleBase, skip_first_screenshot=True) -> bool: + """ + Args: + index: + main: + skip_first_screenshot: + + Returns: + If success + """ + logger.info(f'Insight index: {index}') + last_indexes: set[int] = set() + + while 1: + if skip_first_screenshot: + skip_first_screenshot = False + else: + main.device.screenshot() + + self.load_stage_indexes(main=main) + + if self.current_index_min <= index <= self.current_index_max: + return True + + indexes = self._indexes + if indexes and last_indexes == set(indexes): + logger.warning(f'No more index {index}') + return False + last_indexes = set(indexes) + + if index < self.current_index_min: + self.swipe_page(self.swipe_direction, main, reverse=True) + elif index > self.current_index_max: + self.swipe_page(self.swipe_direction, main) + + main.wait_until_stable( + self.stage.button, + timer=Timer(0, 0), + timeout=Timer(1.5, 5) + ) + + def insight_max_sweepable_index(self, main: ModuleBase, skip_first_screenshot=True) -> int: + """ + Args: + main: + skip_first_screenshot: + + Returns: + Index of max sweepable stage + """ + logger.info('Insight sweepable index') + max_sweepable_index = 0 + last_max_sweepable_index = 0 + + while 1: + if skip_first_screenshot: + skip_first_screenshot = False + else: + main.device.screenshot() + + self.load_stage_indexes(main=main) + + sweepable_index = next( + filter( + lambda x: not self.is_sweepable(main, self.search_box(x[-1][:2])), + self.current_indexes + ), None + ) + + # all sweepable + if sweepable_index is None: + logger.info('All sweepable') + max_sweepable_index = self.current_index_max + self.swipe_page(self.swipe_direction, main) + if max_sweepable_index == last_max_sweepable_index: + logger.info(f'Max sweepable index: {max_sweepable_index}') + return max_sweepable_index + last_max_sweepable_index = max_sweepable_index + # all not sweepable + elif int(sweepable_index[0]) == self.current_index_min: + logger.info('All not sweepable') + if int(sweepable_index[0]) == 1: + logger.warning('No sweepable index') + return 0 + self.swipe_page(self.swipe_direction, main, reverse=True) + else: + logger.info(f'Sweepable index: {int(sweepable_index[0]) - 1}') + return int(sweepable_index[0]) - 1 + + main.wait_until_stable( + self.stage.button, + timer=Timer(0, 0), + timeout=Timer(1.5, 5) + ) + + def is_sweepable(self, main: ModuleBase, search_box) -> bool: + self.sweepable.load_search(search_box) + return main.appear(self.sweepable, similarity=0.8) + + def search_box( + self, + index_cord: tuple[int, int], + padding: tuple[int, int] = (-20, -15) + ) -> tuple[int, int, int, int]: + stage_item_box = area_pad((*padding, *area_size(self.stage_item))) + return area_offset(stage_item_box, index_cord) + + def select_index_enter( + self, + main: ModuleBase, + index: int, + insight: bool = True, + sweepable: bool = True, + padding: tuple[int, int] = (-20, -15), + skip_first_screenshot: bool = True, + interval: int = 1.5 + ) -> bool: + # insight index, if failed, return False + if insight and not self.insight_index(index, main, skip_first_screenshot): + return False + logger.info(f'Select index: {index}') + click_interval = Timer(interval) + load_index_interval = Timer(1) + timeout = Timer(15, 10).start() + while 1: + if skip_first_screenshot: + skip_first_screenshot = False + else: + main.device.screenshot() + + # load index if not insight + if load_index_interval.reached_and_reset(): + self.load_stage_indexes(main=main) + + # find box of index + index_box = next(filter(lambda x: x[0] == index, self.current_indexes), None) + + if index_box is None: + logger.warning(f'No index {index} in {self.index_ocr.name}') + continue + + search_box = self.search_box(index_box[-1][:2], padding) + if sweepable and not self.is_sweepable(main, search_box): + logger.warning(f'Index {index} is not sweepable') + return False + + self.enter.load_search(search_box) + click_button = self.enter.match_multi_template(main.device.image) + + if not click_button: + logger.warning(f'No clickable {self.enter.name}') + continue + + if click_interval.reached_and_reset(): + main.device.click(click_button[0]) + return True + + if timeout.reached(): + logger.warning(f'{self.enter.name} failed') + return False diff --git a/tasks/task/assets/assets_task.py b/tasks/task/assets/assets_task.py index d08f0bb..5c6a15c 100644 --- a/tasks/task/assets/assets_task.py +++ b/tasks/task/assets/assets_task.py @@ -5,7 +5,13 @@ from module.base.button import Button, ButtonWrapper CLAIM = ButtonWrapper( name='CLAIM', - jp=None, + jp=Button( + file='./assets/jp/task/CLAIM.png', + area=(936, 641, 1010, 696), + search=(916, 621, 1030, 716), + color=(230, 210, 63), + button=(936, 641, 1010, 696), + ), en=Button( file='./assets/en/task/CLAIM.png', area=(935, 639, 1015, 698), @@ -16,7 +22,13 @@ CLAIM = ButtonWrapper( ) CLAIMED = ButtonWrapper( name='CLAIMED', - jp=None, + jp=Button( + file='./assets/jp/task/CLAIMED.png', + area=(935, 641, 1010, 696), + search=(915, 621, 1030, 716), + color=(211, 211, 210), + button=(935, 641, 1010, 696), + ), en=Button( file='./assets/en/task/CLAIMED.png', area=(937, 641, 1010, 696), @@ -27,7 +39,13 @@ CLAIMED = ButtonWrapper( ) CLAIMED_ALL = ButtonWrapper( name='CLAIMED_ALL', - jp=None, + jp=Button( + file='./assets/jp/task/CLAIMED_ALL.png', + area=(1057, 640, 1242, 701), + search=(1037, 620, 1262, 720), + color=(192, 193, 196), + button=(1057, 640, 1242, 701), + ), en=Button( file='./assets/en/task/CLAIMED_ALL.png', area=(1058, 641, 1240, 701), @@ -38,7 +56,13 @@ CLAIMED_ALL = ButtonWrapper( ) CLAIM_ALL = ButtonWrapper( name='CLAIM_ALL', - jp=None, + jp=Button( + file='./assets/jp/task/CLAIM_ALL.png', + area=(1058, 641, 1242, 701), + search=(1038, 621, 1262, 720), + color=(235, 218, 67), + button=(1058, 641, 1242, 701), + ), en=Button( file='./assets/en/task/CLAIM_ALL.png', area=(1054, 642, 1243, 700),