1
0
mirror of https://github.com/TheFunny/ArisuAutoSweeper synced 2026-04-01 08:35:14 +00:00

Compare commits

...

21 Commits

Author SHA1 Message Date
RedDeadDepresso
2cad7ebbac feat: input helper
Just a small program for users to quickly find students name.
2023-12-28 01:20:07 +08:00
RedDeadDepresso
bb76e0bba4 fix: task 2023-12-28 01:20:07 +08:00
RedDeadDepresso
662b1535e9 fix: sweep event 2023-12-28 01:20:07 +08:00
RedDeadDepresso
85c6c5c127 feat: added assets event 2023-12-28 01:20:07 +08:00
RedDeadDepresso
2f3a357ff7 fix: task for EN 2023-12-28 01:20:07 +08:00
RedDeadDepresso
672bc30edb refactor: MCE Manager
changed queue thread into daemon thread
2023-12-28 01:20:07 +08:00
RedDeadDepresso
9126c58e9c fix: close random popup in main after login 2023-12-28 01:20:07 +08:00
RedDeadDepresso
42a9c964c9 feat: auto-generate MCE/config.json 2023-12-28 01:20:07 +08:00
RedDeadDepresso
6cf05da422 revert changes in task 2023-12-28 01:20:07 +08:00
RedDeadDepresso
3680b22407 Update .gitignore 2023-12-28 01:20:07 +08:00
RedDeadDepresso
908b134e50 fix: updated regex for mission 2023-12-28 01:20:07 +08:00
RedDeadDepresso
ca2fc4ac46 fix: tasks 2023-12-28 01:20:07 +08:00
RedDeadDepresso
f1b6d4cd09 feat: mission/commissions/event 2023-12-28 01:20:07 +08:00
0d66a5c424
fix(tc): change refresh time 2023-12-25 13:56:42 +08:00
37bbfba299
feat: add tasks assets for jp 2023-12-25 13:17:34 +08:00
34c5323df3
fix(momotalk): correct sort switch 2023-12-23 17:23:40 +08:00
30d63fa193
fix(momotalk): correct sidebar switch 2023-12-23 17:03:15 +08:00
be3fdb0988
fix(task): check all claimed 2023-12-23 16:21:46 +08:00
ba9472d0b1
lang: add zh for new settings 2023-12-23 16:09:06 +08:00
RedDeadDepresso
1791b4b05c
Added Tasks, Shop, MomoTalk (#11)
* feat: tasks

Added module tasks for EN

* refactor: gui

added tree view Farm and Reward.

* feat: shop

* feat: momotalk

---------

Co-authored-by: YoursFunny <admin@yoursfunny.top>
2023-12-22 13:35:33 +08:00
f8d1ba3d4e
perf: change tc priority 2023-12-21 17:56:06 +08:00
105 changed files with 5408 additions and 132 deletions

1
.gitignore vendored
View File

@ -20,6 +20,7 @@ config/reloadalas
test.py test.py
test/ test/
note.md note.md
MCE/config.json
# Created by .ignore support plugin (hsz.mobi) # Created by .ignore support plugin (hsz.mobi)

102
Input Helper.py Normal file
View File

@ -0,0 +1,102 @@
import customtkinter
import json
from MCE.custom_widgets.ctk_scrollable_dropdown import CTkScrollableDropdown
import os
from tkinter import END
class InputHelper(customtkinter.CTk):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.create_tabview()
self.create_invite_student_widgets()
self.create_lessons_widgets()
def create_tabview(self):
self.tabview = customtkinter.CTkTabview(master=self)
self.tabview.grid(row=0, column=0)
self.cafe_tab = self.tabview.add("Cafe") # add tab at the end
self.lessons_tab = self.tabview.add("Lessons") # add tab at the end
self.tabview.set("Cafe") # set currently visible
def create_invite_student_widgets(self):
self.invitation_label = customtkinter.CTkLabel(master=self.cafe_tab, text="Copy and paste this in AAS Invitation Settings:")
self.invitation_label.grid(row=0, column=0, padx=60)
self.invitation_entry = customtkinter.CTkEntry(master=self.cafe_tab, width=500)
self.invitation_entry.grid(row=1, column=0)
self.invite_copy_button = customtkinter.CTkButton(master=self.cafe_tab, text="Copy", width=40, command=lambda : self.copy_entry(self.invitation_entry, self.invite_copy_button))
self.invite_copy_button.grid(row=1, column=3, padx=5)
self.invite_clear_button = customtkinter.CTkButton(master=self.cafe_tab, text="Clear", width=40, fg_color="crimson", command=lambda : self.invitation_entry.delete(0, END))
self.invite_clear_button.grid(row=1, column=4, padx=5)
self.invite_frame = customtkinter.CTkFrame(master=self.cafe_tab, fg_color="transparent")
self.invite_frame.grid(row=2, column=0, padx=20, pady=20)
self.server_dropdown = customtkinter.CTkOptionMenu(master=self.invite_frame, values=self.find_json_files("MCE/student_list"), command=self.switch_server, width=40)
self.server_dropdown.grid(row=0, column=0)
self.student_entry = customtkinter.CTkComboBox(master=self.invite_frame, width=300)
self.student_entry.grid(row=0, column=1, padx=(50,0))
self.student_dropdown = CTkScrollableDropdown(self.student_entry, width=300, height=550, autocomplete=True, command=lambda choice: self.insert(choice, self.invitation_entry), values=[""])
self.server_dropdown.set("EN")
self.switch_server("EN")
def create_lessons_widgets(self):
self.lessons_label = customtkinter.CTkLabel(master=self.lessons_tab, text="Copy and paste this in AAS Lessons Settings:")
self.lessons_label.grid(row=0, column=0, padx=60)
self.lessons_entry = customtkinter.CTkEntry(master=self.lessons_tab, width=500)
self.lessons_entry.grid(row=1, column=0)
self.lessons_copy_button = customtkinter.CTkButton(master=self.lessons_tab, text="Copy", width=40, command=lambda : self.copy_entry(self.lessons_entry, self.lessons_copy_button))
self.lessons_copy_button.grid(row=1, column=1, padx=5)
self.lessons_clear_button = customtkinter.CTkButton(master=self.lessons_tab, text="Clear", width=40, fg_color="crimson", command=lambda : self.lessons_entry.delete(0, END))
self.lessons_clear_button.grid(row=1, column=2, padx=5)
self.lessons_buttons_frame = customtkinter.CTkFrame(master=self.lessons_tab, fg_color="transparent")
self.lessons_buttons_frame.grid(row=2, column=0, padx=20, pady=20)
for i in range(9):
self.lesson_button = customtkinter.CTkButton(master=self.lessons_buttons_frame, text=str(i+1), command=lambda choice=str(i+1): self.insert(choice, self.lessons_entry), width=40)
self.lesson_button.grid(row=0, column=i, padx=5)
def find_json_files(self,folder_path):
json_files = []
for root, dirs, files in os.walk(folder_path):
for file in files:
if file.endswith(".json"):
json_files.append(os.path.splitext(file)[0])
return json_files
def switch_server(self, server):
with open(f"MCE/student_list/{server}.json", "r") as f:
student_list = json.load(f)
self.student_dropdown.configure(values=student_list)
def insert(self, value, entry):
entry.insert(index=END, string=value + " > ")
def copy_entry(self, entry, button):
text_to_copy = entry.get()
# Check if there is text to copy
if text_to_copy:
# Clear the clipboard and set the new text
self.clipboard_clear()
self.clipboard_append(text_to_copy)
self.update() # This is necessary on some systems to update the clipboard
button_color = button.cget("fg_color")
button.configure(fg_color="green")
self.after(2000, lambda : button.configure(fg_color=['#3B8ED0', '#1F6AA5']))
if __name__ == "__main__":
app = InputHelper()
app.title("Input Helper")
app.mainloop()

437
MCE Manager.py Normal file
View File

@ -0,0 +1,437 @@
import customtkinter
import tkinter as tk
import random
import re
from MCE.custom_widgets.ctkmessagebox import CTkMessagebox
from MCE.custom_widgets.ctk_tooltip import CTkToolTip
from MCE.custom_widgets.ctk_timeentry import CTkTimeEntry
from MCE.custom_widgets.ctk_integerspinbox import CTkIntegerSpinbox
from MCE.custom_widgets.ctk_templatedialog import CTkTemplateDialog
from MCE.custom_widgets.ctk_notification import CTkNotification
from MCE.utils import Linker, Config
from filelock import FileLock, Timeout
import threading
import time
class MCE_Manager(customtkinter.CTk):
def __init__(self, linker, config, **kwargs):
super().__init__(**kwargs)
self.linker = linker
self.config = config
self.create_widgets()
# Load Template Data
self.load_template_data()
# Load queue Data
self.load_queue_data()
def create_widgets(self):
self.create_mission_commissions_widgets()
def create_mission_commissions_widgets(self):
# Create Mission/Commissions/Event Checkbox
self.create_mission_commissions_checkbox()
# Create Reset Daily Widgets
self.create_reset_daily_widgets()
# Create Recharge AP and Event Checkboxes
self.create_recharge_and_event_checkboxes()
# Create Preferred Template Selection
self.create_preferred_template_selection()
# Create Mission Tabview with Template and Queue Tabs
self.create_mission_tabview()
# Create Top-Level Window for Template Editing
self.create_template_queue_editor()
# Create Template Frame and Queue Frame
self.create_template_and_queue_frames()
# Create Lists to Store Frame Widgets
self.create_frame_lists()
# Initialize Preferred Template and Templates List
self.initialize_preferred_template()
# Create OptionMenu for Selecting a Template
self.create_template_option_menu()
# Create Delete Template Button
self.create_delete_template_button()
# Helper method to create Mission/Commissions/Event Checkbox
def create_mission_commissions_checkbox(self):
self.mission_commissions_checkbox = customtkinter.CTkLabel(self, text="Mission/Commissions/Event", width=60, font=customtkinter.CTkFont(family="Inter", size=20, weight="bold"))
self.mission_commissions_checkbox.grid(row=11, column=0, sticky="nw", padx=20, pady=20)
self.notification = CTkNotification(master=self, text="Config saved")
self.notification.grid(row=11, column=1)
# Helper method to create Reset Daily Widgets
def create_reset_daily_widgets(self):
self.reset_daily = customtkinter.CTkCheckBox(self, text="Reset Daily", font=customtkinter.CTkFont(family="Inter", size=16, underline=True), command=lambda x=["ResetDaily"]: self.config.save_to_json(x))
self.reset_daily.grid(row=12, column=0, sticky="nw", padx=80)
self.reset_daily_tooltip = CTkToolTip(self.reset_daily, wraplength=400,
message="If enabled and if current time >= reset time,\
the queue will automatically be cleared and repopulated with preferred template stages. Only activated once a day.")
self.reset_daily_sub_label = customtkinter.CTkLabel(self, text="hh/mm/ss", font=customtkinter.CTkFont(family="Inter", size=12))
self.reset_daily_sub_label.grid(row=13, column=0, padx=80)
self.reset_time = CTkTimeEntry(self)
self.reset_time.grid(row=12, column=1)
self.reset_time.hour_entry.bind("<KeyRelease>", lambda event, x=["ResetTime"]: self.config.save_to_json(x))
self.reset_time.minute_entry.bind("<KeyRelease>", lambda event, x=["ResetTime"]: self.config.save_to_json(x))
self.reset_time.second_entry.bind("<KeyRelease>", lambda event, x=["ResetTime"]: self.config.save_to_json(x))
self.linker.widgets["ResetDaily"] = self.reset_daily
self.linker.widgets["ResetTime"] = self.reset_time
# Helper method to create Recharge AP and Event Checkboxes
def create_recharge_and_event_checkboxes(self):
self.recharge_checkbox = customtkinter.CTkCheckBox(self, text="Recharge AP", command=lambda x=["RechargeAP"]: self.config.save_to_json(x), font=customtkinter.CTkFont(family="Inter", size=16, underline=True))
self.recharge_checkbox.grid(row=14, column=0, sticky="nw", padx=80, pady=20)
self.linker.widgets["RechargeAP"] = self.recharge_checkbox
self.recharge_tooltip = CTkToolTip(self.recharge_checkbox, wraplength=400,
message="When enabled, recharge AP when low via cafe earnings, tasks, club and mailbox if they are enabled in their respective sections.")
self.event_checkbox = customtkinter.CTkCheckBox(self, text="Sweep Event Stages", command=lambda x=["Event"]: self.config.save_to_json(x), font=customtkinter.CTkFont(family="Inter", size=16, underline=True))
self.event_tooltip = CTkToolTip(self.event_checkbox, wraplength=400, message="When enabled, the script will sweep event stages. Otherwise, it will ignore them.")
self.event_checkbox.grid(row=15, column=0, sticky="nw", padx=80)
self.linker.widgets["Event"] = self.event_checkbox
# Helper method to create Preferred Template Selection
def create_preferred_template_selection(self):
self.templates = self.config.config_data["Templates"]
self.templates_list = list(self.templates.keys())
self.preferred_template_label = customtkinter.CTkLabel(self, text="Preferred Template:", font=customtkinter.CTkFont(family="Inter", size=16, underline=True))
self.preferred_template_label.grid(row=16, column=0, pady=20)
self.preferred_template_tooltip = CTkToolTip(self.preferred_template_label, wraplength=400,
message="The template from which to repopulate the queue when it is empty or reset daily is activated")
self.preferred_template_optionmenu = customtkinter.CTkOptionMenu(self, values=self.templates_list, command=lambda x, y=["PreferredTemplate"]: self.config.save_to_json(y))
self.preferred_template_optionmenu.grid(row=16, column=1, pady=20)
self.linker.widgets["PreferredTemplate"] = self.preferred_template_optionmenu
# Helper method to create Mission Tabview with Template and Queue Tabs
def create_mission_tabview(self):
self.mission_tabview = customtkinter.CTkTabview(self, height=500)
self.mission_tabview.grid(row=17, column=0, columnspan=3, padx=20)
self.tab_template = self.mission_tabview.add('Template')
self.tab_queue = self.mission_tabview.add('Queue')
# Helper method to create Template Queue Editor
def create_template_queue_editor(self):
self.queue_buttons = []
for i in [self.tab_queue, self.tab_template]:
queue = True if i == self.tab_queue else False
self.template_labels = customtkinter.CTkFrame(i)
self.template_labels.grid(row=0, column=0, sticky="ew")
self.mode_label = customtkinter.CTkLabel(self.template_labels, text="Mode:", font=customtkinter.CTkFont(underline=True))
self.mode_tooltip = CTkToolTip(self.mode_label, message="N:Mission Normal\nH:Mission Hard\nE:Event Quest\nBD:Commissions EXP\nIR:Commissions Credits\n")
self.mode_label.grid(row=1, column=0, padx=(130, 0), pady=5)
self.stage_label = customtkinter.CTkLabel(self.template_labels, text="Stage:", font=customtkinter.CTkFont(underline=True))
self.stage_tooltip = CTkToolTip(self.stage_label, message="Valid format for Mission: 1-1\nValid format for Commissions/Event: 01")
self.stage_label.grid(row=1, column=1, padx=(40, 20), pady=5)
self.run_times_label = customtkinter.CTkLabel(self.template_labels, text="Number of Sweeps:", font=customtkinter.CTkFont(underline=True))
self.run_times_tooltip = CTkToolTip(self.run_times_label, message="How many times do you want to sweep the stage?")
self.run_times_label.grid(row=1, column=2, pady=5)
self.template_buttons_frame = customtkinter.CTkFrame(i)
self.template_buttons_frame.grid(row=3, column=0)
self.highlight_label = customtkinter.CTkLabel(self.template_buttons_frame, text="*You can double click an entry and press up or down arrow to change its position", font=customtkinter.CTkFont(family="Inter", size=12))
self.highlight_label.grid(row=0, column=0, columnspan=3)
self.add_button = customtkinter.CTkButton(self.template_buttons_frame , text="Add", command=lambda queue=queue: self.add_frame(queue=queue))
self.add_button.grid(row=1, column=0, padx=5, pady=5)
# Clear button to clear all frames
self.clear_button = customtkinter.CTkButton(self.template_buttons_frame, text="Clear All", command=lambda queue=queue: self.clear_frames(queue=queue), fg_color="crimson")
self.clear_button.grid(row=1, column=1, padx=5, pady=5)
# Save button to save data
self.save_button = customtkinter.CTkButton(self.template_buttons_frame, text="Save", command=lambda queue=queue: self.save_data(queue=queue), fg_color="#DC621D")
self.save_button.grid(row=1, column=2, padx=5, pady=5)
if queue:
self.queue_buttons = [self.add_button, self.clear_button, self.save_button]
# Helper method to create Template Frame and Queue Frame
def create_template_and_queue_frames(self):
self.template_frame = customtkinter.CTkScrollableFrame(self.tab_template, width=400, height=350)
self.template_frame.grid(row=1, column=0, sticky="nsew")
self.queue_frame = customtkinter.CTkScrollableFrame(self.tab_queue, width=400, height=350)
self.queue_frame.grid(row=1, column=0, sticky="nsew")
# Helper method to create Lists to Store Frame Widgets
def create_frame_lists(self):
self.template_frames = []
self.queue_frames = []
self.highlighted_frame = None
# Helper method to initialize Preferred Template and Templates List
def initialize_preferred_template(self):
self.preferred_template = self.config.config_data["PreferredTemplate"]
self.templates_list.append("Add New Template")
# Helper method to create OptionMenu for Selecting a Template
def create_template_option_menu(self):
self.selected_template = tk.StringVar(self.template_frame)
self.selected_template.set(self.preferred_template) # Set the initial value to the preferred template
self.template_optionmenu = customtkinter.CTkOptionMenu(self.template_labels, values=self.templates_list, variable=self.selected_template, command=lambda *args: self.load_template_data())
self.template_optionmenu.grid(row=0, column=0, padx=5, pady=5)
# Helper method to create Delete Template Button
def create_delete_template_button(self):
self.delete_template_button = customtkinter.CTkButton(self.template_labels, width=40, text="Delete", command=self.delete_template)
self.delete_template_button.grid(row=0, column=1)
# Helper method to add frames from Configuration Data
def load_queue_data(self, state="normal"):
for entry in self.config.config_data['Queue']:
self.add_frame(entry, queue=True, state=state)
# Function to load template data into frames
def load_template_data(self):
selected = self.selected_template.get()
if selected == "Add New Template":
dialog = CTkTemplateDialog(text="Type in new template name. Template name MUST be different from other templates!", title="Template Name", values=self.templates_list[:-1])
template_name, template_import = dialog.get_input()
if template_name.replace(" ", "") == "":
self.template_optionmenu.set(self.previous_selected)
return
elif template_name in self.templates_list:
CTkMessagebox(title="Error", message="Name is invalid.", icon="cancel")
self.template_optionmenu.set(self.previous_selected)
return
else:
if template_import != "":
self.templates[template_name] = self.templates[template_import]
else:
self.templates[template_name] = []
self.templates_list[-1] = template_name
self.preferred_template_optionmenu.configure(values=self.templates_list)
selected = template_name
self.template_optionmenu.set(selected)
self.templates_list.append("Add New Template")
self.template_optionmenu.configure(values=self.templates_list)
self.clear_frames()
for entry in self.templates[selected]:
self.add_frame(entry)
self.previous_selected = selected
def delete_template(self):
msg = CTkMessagebox(title="Template Deletetion", message=f"Are you sure you want to delete Template {self.previous_selected}?",
icon="question", option_1="No", option_2="Yes")
response = msg.get()
if response=="Yes":
if len(self.templates) != 1:
del self.templates[self.previous_selected]
self.templates_list = list(self.templates.keys())
self.preferred_template_optionmenu.configure(values=self.templates_list)
if self.preferred_template == self.previous_selected:
self.preferred_template = random.choice(self.templates_list)
self.config.config_data["PreferredTemplate"] = self.preferred_template
self.selected_template.set(self.preferred_template) # Set the initial value to the preferred template
self.preferred_template_optionmenu.set(self.preferred_template)
self.load_template_data()
self.config.save_file()
self.templates_list.append("Add New Template")
self.template_optionmenu.configure(values=self.templates_list)
self.template_optionmenu.set(self.preferred_template)
else:
CTkMessagebox(title="Error", message="At least one template must exist!!!", icon="cancel")
return
# Function to add a frame with widgets
def add_frame(self, inner_list=None, queue=False, state="normal"):
frames = self.queue_frames if queue else self.template_frames
parent_frame = self.queue_frame if queue else self.template_frame
row_index = len(frames) + 1 # Calculate the row for the new frame
# Create a frame
frame = tk.Frame(parent_frame, bg="gray17")
frame.grid(row=row_index, column=0, columnspan=4, padx=10, pady=10, sticky="w")
frames.append(frame)
# "Up" button to move the frame up
up_button = customtkinter.CTkButton(frame, text="Up", width=5, command=lambda f=frame, queue=queue: self.move_frame_up(f, queue), state=state)
up_button.grid(row=0, column=0, padx=5, pady=5, sticky="w")
# "Down" button to move the frame down
down_button = customtkinter.CTkButton(frame, text="Down", width=5, command=lambda f=frame, queue=queue: self.move_frame_down(f, queue), state=state)
down_button.grid(row=0, column=1, padx=5, pady=5, sticky="w")
# Dropdown menu for mode
mode_optionmenu = customtkinter.CTkOptionMenu(frame, width=60, values=["N", "H", "E", "BD", "IR"], state=state)
mode_optionmenu.set(inner_list[0] if inner_list else "N")
mode_optionmenu.grid(row=0, column=2, padx=5, pady=5, sticky="w")
# Entry widget for stage
stage_var = tk.StringVar(value=inner_list[1] if inner_list else "")
stage_entry = customtkinter.CTkEntry(frame, width=60, textvariable=stage_var, state=state)
stage_entry.grid(row=0, column=3, padx=5, pady=5, sticky="w")
mode_optionmenu.configure(command=lambda choice, x=mode_optionmenu, y=stage_entry : self.check_entry(x,y))
stage_entry.bind('<KeyRelease>', command=lambda event, x=mode_optionmenu, y=stage_entry : self.check_entry(x,y))
self.check_entry(mode_optionmenu, stage_entry)
# Entry widget for run times (only accepts numbers)
run_times_spinbox = CTkIntegerSpinbox(frame, step_size=1, min_value=1)
run_times_spinbox.set(value=inner_list[2] if inner_list else 1)
run_times_spinbox.grid(row=0, column=4, padx=5, pady=5, sticky="w")
# Delete button to delete the frame
delete_button = customtkinter.CTkButton(frame, text="Delete", width=5, command=lambda f=frame, queue=queue: self.delete_frame(f, queue), state=state)
delete_button.grid(row=0, column=5, padx=5, pady=5, sticky="w")
frame.bind("<Double-Button-1>", lambda event, f=frame: self.highlight_frame(f))
# Function to clear all frames
def clear_frames(self, queue=False):
frames = self.queue_frames if queue else self.template_frames
for frame in frames:
frame.destroy()
frames.clear()
# Function to save frames as data
def save_data(self, queue=False):
entries = []
frames = self.queue_frames if queue else self.template_frames
name = "Queue" if queue else "Template"
for frame in frames:
mode_optionmenu = frame.winfo_children()[2]
stage_entry = frame.winfo_children()[3]
if not self.check_entry(mode_optionmenu, stage_entry):
CTkMessagebox(title="Error", message="Configuration not saved. Some entries are incomplete or have incorect input.", icon="cancel")
return
mode = frame.winfo_children()[2].get()
stage = frame.winfo_children()[3].get().strip()
run_times = frame.winfo_children()[4].get()
entries.append([mode, stage, int(run_times)])
if queue:
self.config.config_data['Queue'] = entries
else:
selected = self.selected_template.get()
self.templates[selected] = entries
self.config.save_file(name)
def check_entry(self, mode_dropdown, stage_entry):
mode = mode_dropdown.get()
stage = stage_entry.get()
if mode in ["N", "H"]:
pattern = r'\d{1,2}-[0-9A-Z]'
else:
pattern = r"^\d{2}$"
if re.match(pattern, stage):
stage_entry.configure(border_color=['#979DA2', '#565B5E'])
return True
else:
stage_entry.configure(border_color='crimson')
return False
# Function to move a frame up
def move_frame_up(self, frame, queue=False):
frames = self.queue_frames if queue else self.template_frames
index = frames.index(frame)
if index > 0:
frames[index], frames[index - 1] = frames[index - 1], frames[index]
self.update_frame_positions(queue=queue)
# Function to move a frame down
def move_frame_down(self, frame, queue=False):
frames = self.queue_frames if queue else self.template_frames
index = frames.index(frame)
if index < len(frames) - 1:
frames[index], frames[index + 1] = frames[index + 1], frames[index]
self.update_frame_positions(queue=queue)
# Function to update frame positions in the grid
def update_frame_positions(self, queue=False):
frames = self.queue_frames if queue else self.template_frames
for index, frame in enumerate(frames):
frame.grid(row=index, column=0, columnspan=4, padx=10, pady=10, sticky="w")
# Function to delete a frame
def delete_frame(self, frame, queue=False):
if queue:
self.queue_frames.remove(frame)
else:
self.template_frames.remove(frame)
frame.destroy()
# Update the positions of remaining frames
self.update_frame_positions(queue=queue)
def highlight_frame(self, frame):
try:
if self.highlighted_frame is not None:
self.highlighted_frame.unbind("<Up>")
self.highlighted_frame.unbind("<Down>")
self.highlighted_frame.config(bg="gray17")
except:
pass
if self.highlighted_frame == frame:
self.highlighted_frame = None
else:
up_button = frame.winfo_children()[0]
down_button = frame.winfo_children()[1]
frame.config(bg="yellow")
frame.bind("<Up>", lambda event: up_button.invoke())
frame.bind("<Down>", lambda event: down_button.invoke())
frame.focus_set()
self.highlighted_frame = frame
def check_lock(self):
while 1:
try:
lock = FileLock("MCE\config.json.lock")
lock.acquire(timeout=1)
except Timeout:
if not self.config.locked:
self.after(10, lambda : (self.queue_changed(), self.update_queue(), self.switch_queue_state("disabled")))
self.config.locked = True
elif self.config.locked and self.queue_changed():
self.after(10, lambda : (self.update_queue(), self.switch_queue_state("disabled")))
else:
lock.release()
if self.config.locked:
self.after(10, lambda : (self.queue_changed(), self.update_queue(), self.switch_queue_state("normal")))
self.config.locked = False
finally:
time.sleep(2)
def switch_queue_state(self, state):
for button in self.queue_buttons:
button.configure(state=state)
for frame in self.queue_frames:
for widget in frame.winfo_children():
widget.configure(state=state)
def update_queue(self):
self.clear_frames(queue=True)
for entry in self.config.config_data['Queue']:
self.add_frame(entry, queue=True)
def queue_changed(self):
new_config_data = self.config.read()
changed = self.config.config_data["Queue"] != new_config_data["Queue"] or self.config.config_data["LastRun"] != new_config_data["LastRun"]
if changed:
self.config.config_data["LastRun"] = new_config_data["LastRun"]
self.config.config_data['Queue'] = new_config_data['Queue']
return changed
if __name__ == "__main__":
linker = Linker()
config = Config(linker, "MCE\config.json")
app = MCE_Manager(linker, config)
app.title("MCE Manager")
linker.sidebar = app
config.load_config()
daemon_thread = threading.Thread(target=app.check_lock, daemon=True)
daemon_thread.start()
app.mainloop()

View File

View File

@ -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('<ButtonPress-1>', self.start_decrementing)
self.subtract_button.bind('<ButtonRelease-1>', 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('<ButtonPress-1>', self.start_incrementing)
self.add_button.bind('<ButtonRelease-1>', 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)

View File

@ -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")

View File

@ -0,0 +1,337 @@
'''
Advanced Scrollable Dropdown class for customtkinter widgets
Author: Akash Bora
'''
import customtkinter
import sys
import time
import difflib
class CTkScrollableDropdown(customtkinter.CTkToplevel):
def __init__(self, attach, x=None, y=None, button_color=None, height: int = 200, width: int = None,
fg_color=None, button_height: int = 20, justify="center", scrollbar_button_color=None,
scrollbar=True, scrollbar_button_hover_color=None, frame_border_width=2, values=[],
command=None, image_values=[], alpha: float = 0.97, frame_corner_radius=20, double_click=False,
resize=True, frame_border_color=None, text_color=None, autocomplete=False, **button_kwargs):
super().__init__(takefocus=1)
self.focus()
self.lift()
self.alpha = alpha
self.attach = attach
self.corner = frame_corner_radius
self.padding = 0
self.focus_something = False
self.disable = True
self.update()
if sys.platform.startswith("win"):
self.after(100, lambda: self.overrideredirect(True))
self.transparent_color = self._apply_appearance_mode(self._fg_color)
self.attributes("-transparentcolor", self.transparent_color)
elif sys.platform.startswith("darwin"):
self.overrideredirect(True)
self.transparent_color = 'systemTransparent'
self.attributes("-transparent", True)
self.focus_something = True
else:
self.overrideredirect(True)
self.transparent_color = '#000001'
self.corner = 0
self.padding = 18
self.withdraw()
self.hide = True
self.attach.bind('<Configure>', lambda e: self._withdraw() if not self.disable else None, add="+")
self.attach.winfo_toplevel().bind('<Configure>', lambda e: self._withdraw() if not self.disable else None, add="+")
self.attach.winfo_toplevel().bind("<ButtonPress>", lambda e: self._withdraw() if not self.disable else None, add="+")
self.attributes('-alpha', 0)
self.disable = False
self.fg_color = customtkinter.ThemeManager.theme["CTkFrame"]["fg_color"] if fg_color is None else fg_color
self.scroll_button_color = customtkinter.ThemeManager.theme["CTkScrollbar"]["button_color"] if scrollbar_button_color is None else scrollbar_button_color
self.scroll_hover_color = customtkinter.ThemeManager.theme["CTkScrollbar"]["button_hover_color"] if scrollbar_button_hover_color is None else scrollbar_button_hover_color
self.frame_border_color = customtkinter.ThemeManager.theme["CTkFrame"]["border_color"] if frame_border_color is None else frame_border_color
self.button_color = customtkinter.ThemeManager.theme["CTkFrame"]["top_fg_color"] if button_color is None else button_color
self.text_color = customtkinter.ThemeManager.theme["CTkLabel"]["text_color"] if text_color is None else text_color
if scrollbar is False:
self.scroll_button_color = self.fg_color
self.scroll_hover_color = self.fg_color
self.frame = customtkinter.CTkScrollableFrame(self, bg_color=self.transparent_color, fg_color=self.fg_color,
scrollbar_button_hover_color=self.scroll_hover_color,
corner_radius=self.corner, border_width=frame_border_width,
scrollbar_button_color=self.scroll_button_color,
border_color=self.frame_border_color)
self.frame._scrollbar.grid_configure(padx=3)
self.frame.pack(expand=True, fill="both")
self.dummy_entry = customtkinter.CTkEntry(self.frame, fg_color="transparent", border_width=0, height=1, width=1)
self.no_match = customtkinter.CTkLabel(self.frame, text="No Match")
self.height = height
self.height_new = height
self.width = width
self.command = command
self.fade = False
self.resize = resize
self.autocomplete = autocomplete
self.var_update = customtkinter.StringVar()
self.appear = False
if justify.lower()=="left":
self.justify = "w"
elif justify.lower()=="right":
self.justify = "e"
else:
self.justify = "c"
self.button_height = button_height
self.values = values
self.button_num = len(self.values)
self.image_values = None if len(image_values)!=len(self.values) else image_values
self.resizable(width=False, height=False)
self.transient(self.master)
self._init_buttons(**button_kwargs)
# Add binding for different ctk widgets
if double_click or self.attach.winfo_name().startswith("!ctkentry") or self.attach.winfo_name().startswith("!ctkcombobox"):
self.attach.bind('<Double-Button-1>', lambda e: self._iconify(), add="+")
else:
self.attach.bind('<Button-1>', lambda e: self._iconify(), add="+")
if self.attach.winfo_name().startswith("!ctkcombobox"):
self.attach._canvas.tag_bind("right_parts", "<Button-1>", lambda e: self._iconify())
self.attach._canvas.tag_bind("dropdown_arrow", "<Button-1>", lambda e: self._iconify())
if self.command is None:
self.command = self.attach.set
if self.attach.winfo_name().startswith("!ctkoptionmenu"):
self.attach._canvas.bind("<Button-1>", lambda e: self._iconify())
self.attach._text_label.bind("<Button-1>", lambda e: self._iconify())
if self.command is None:
self.command = self.attach.set
self.attach.bind("<Destroy>", lambda _: self._destroy(), add="+")
self.update_idletasks()
self.x = x
self.y = y
if self.autocomplete:
self.bind_autocomplete()
self.deiconify()
self.withdraw()
self.attributes("-alpha", self.alpha)
def _destroy(self):
self.after(500, self.destroy_popup)
def _withdraw(self):
if self.winfo_viewable() and self.hide:
self.withdraw()
self.event_generate("<<Closed>>")
self.hide = True
def _update(self, a, b, c):
self.live_update(self.attach._entry.get())
def bind_autocomplete(self, ):
def appear(x):
self.appear = True
if self.attach.winfo_name().startswith("!ctkcombobox"):
self.attach._entry.configure(textvariable=self.var_update)
self.attach._entry.bind("<Key>", appear)
self.attach.set(self.values[0])
self.var_update.trace_add('write', self._update)
if self.attach.winfo_name().startswith("!ctkentry"):
self.attach.configure(textvariable=self.var_update)
self.attach.bind("<Key>", appear)
self.var_update.trace_add('write', self._update)
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/100)
def fade_in(self):
for i in range(0,100,10):
if not self.winfo_exists():
break
self.attributes("-alpha", i/100)
self.update()
time.sleep(1/100)
def _init_buttons(self, **button_kwargs):
self.i = 0
self.widgets = {}
for row in self.values:
self.widgets[self.i] = customtkinter.CTkButton(self.frame,
text=row,
height=self.button_height,
fg_color=self.button_color,
text_color=self.text_color,
image=self.image_values[i] if self.image_values is not None else None,
anchor=self.justify,
command=lambda k=row: self._attach_key_press(k), **button_kwargs)
self.widgets[self.i].pack(fill="x", pady=2, padx=(self.padding, 0))
self.i+=1
self.hide = False
def destroy_popup(self):
self.destroy()
self.disable = True
def place_dropdown(self):
self.x_pos = self.attach.winfo_rootx() if self.x is None else self.x + self.attach.winfo_rootx()
self.y_pos = self.attach.winfo_rooty() + self.attach.winfo_reqheight() + 5 if self.y is None else self.y + self.attach.winfo_rooty()
self.width_new = self.attach.winfo_width() if self.width is None else self.width
if self.resize:
if self.button_num<=5:
self.height_new = self.button_height * self.button_num + 55
else:
self.height_new = self.button_height * self.button_num + 35
if self.height_new>self.height:
self.height_new = self.height
self.geometry('{}x{}+{}+{}'.format(self.width_new, self.height_new,
self.x_pos, self.y_pos))
self.fade_in()
self.attributes('-alpha', self.alpha)
self.attach.focus()
def _iconify(self):
if self.disable: return
if self.hide:
self.event_generate("<<Opened>>")
self._deiconify()
self.focus()
self.hide = False
self.place_dropdown()
if self.focus_something:
self.dummy_entry.pack()
self.dummy_entry.focus_set()
self.after(100, self.dummy_entry.pack_forget)
else:
self.withdraw()
self.hide = True
def _attach_key_press(self, k):
self.event_generate("<<Selected>>")
self.fade = True
if self.command:
self.command(k)
self.fade = False
self.fade_out()
self.withdraw()
self.hide = True
def live_update(self, string=None):
if not self.appear: return
if self.disable: return
if self.fade: return
if string:
string = string.lower()
self._deiconify()
i=1
for key in self.widgets.keys():
s = self.widgets[key].cget("text").lower()
text_similarity = difflib.SequenceMatcher(None, s[0:len(string)], string).ratio()
similar = s.startswith(string) or text_similarity > 0.75
if not similar:
self.widgets[key].pack_forget()
else:
self.widgets[key].pack(fill="x", pady=2, padx=(self.padding, 0))
i+=1
if i==1:
self.no_match.pack(fill="x", pady=2, padx=(self.padding, 0))
else:
self.no_match.pack_forget()
self.button_num = i
self.place_dropdown()
else:
self.no_match.pack_forget()
self.button_num = len(self.values)
for key in self.widgets.keys():
self.widgets[key].destroy()
self._init_buttons()
self.place_dropdown()
self.frame._parent_canvas.yview_moveto(0.0)
self.appear = False
def insert(self, value, **kwargs):
self.widgets[self.i] = customtkinter.CTkButton(self.frame,
text=value,
height=self.button_height,
fg_color=self.button_color,
text_color=self.text_color,
anchor=self.justify,
command=lambda k=value: self._attach_key_press(k), **kwargs)
self.widgets[self.i].pack(fill="x", pady=2, padx=(self.padding, 0))
self.i+=1
self.values.append(value)
def _deiconify(self):
if len(self.values)>0:
self.deiconify()
def popup(self, x=None, y=None):
self.x = x
self.y = y
self.hide = True
self._iconify()
def configure(self, **kwargs):
if "height" in kwargs:
self.height = kwargs.pop("height")
self.height_new = self.height
if "alpha" in kwargs:
self.alpha = kwargs.pop("alpha")
if "width" in kwargs:
self.width = kwargs.pop("width")
if "fg_color" in kwargs:
self.frame.configure(fg_color=kwargs.pop("fg_color"))
if "values" in kwargs:
self.values = kwargs.pop("values")
self.image_values = None
self.button_num = len(self.values)
for key in self.widgets.keys():
self.widgets[key].destroy()
self._init_buttons()
if "image_values" in kwargs:
self.image_values = kwargs.pop("image_values")
self.image_values = None if len(self.image_values)!=len(self.values) else self.image_values
if self.image_values is not None:
i=0
for key in self.widgets.keys():
self.widgets[key].configure(image=self.image_values[i])
i+=1
if "button_color" in kwargs:
for key in self.widgets.keys():
self.widgets[key].configure(fg_color=kwargs.pop("button_color"))
for key in self.widgets.keys():
self.widgets[key].configure(**kwargs)

View File

@ -0,0 +1,291 @@
'''
Advanced Scrollable Dropdown Frame class for customtkinter widgets
Author: Akash Bora
'''
import customtkinter
import sys
import difflib
class CTkScrollableDropdownFrame(customtkinter.CTkFrame):
def __init__(self, attach, x=None, y=None, button_color=None, height: int = 200, width: int = None,
fg_color=None, button_height: int = 20, justify="center", scrollbar_button_color=None,
scrollbar=True, scrollbar_button_hover_color=None, frame_border_width=2, values=[],
command=None, image_values=[], double_click=False, frame_corner_radius=True, resize=True, frame_border_color=None,
text_color=None, autocomplete=False, **button_kwargs):
super().__init__(master=attach.winfo_toplevel(), bg_color=attach.cget("bg_color"))
self.attach = attach
self.corner = 11 if frame_corner_radius else 0
self.padding = 0
self.disable = True
self.hide = True
self.attach.bind('<Configure>', lambda e: self._withdraw() if not self.disable else None, add="+")
self.attach.winfo_toplevel().bind("<ButtonPress>", lambda e: self._withdraw() if not self.disable else None, add="+")
self.disable = False
self.fg_color = customtkinter.ThemeManager.theme["CTkFrame"]["fg_color"] if fg_color is None else fg_color
self.scroll_button_color = customtkinter.ThemeManager.theme["CTkScrollbar"]["button_color"] if scrollbar_button_color is None else scrollbar_button_color
self.scroll_hover_color = customtkinter.ThemeManager.theme["CTkScrollbar"]["button_hover_color"] if scrollbar_button_hover_color is None else scrollbar_button_hover_color
self.frame_border_color = customtkinter.ThemeManager.theme["CTkFrame"]["border_color"] if frame_border_color is None else frame_border_color
self.button_color = customtkinter.ThemeManager.theme["CTkFrame"]["top_fg_color"] if button_color is None else button_color
self.text_color = customtkinter.ThemeManager.theme["CTkLabel"]["text_color"] if text_color is None else text_color
if scrollbar is False:
self.scroll_button_color = self.fg_color
self.scroll_hover_color = self.fg_color
self.frame = customtkinter.CTkScrollableFrame(self, fg_color=self.fg_color, bg_color=attach.cget("bg_color"),
scrollbar_button_hover_color=self.scroll_hover_color,
corner_radius=self.corner, border_width=frame_border_width,
scrollbar_button_color=self.scroll_button_color,
border_color=self.frame_border_color)
self.frame._scrollbar.grid_configure(padx=3)
self.frame.pack(expand=True, fill="both")
if self.corner==0:
self.corner = 21
self.dummy_entry = customtkinter.CTkEntry(self.frame, fg_color="transparent", border_width=0, height=1, width=1)
self.no_match = customtkinter.CTkLabel(self.frame, text="No Match")
self.height = height
self.height_new = height
self.width = width
self.command = command
self.fade = False
self.resize = resize
self.autocomplete = autocomplete
self.var_update = customtkinter.StringVar()
self.appear = False
if justify.lower()=="left":
self.justify = "w"
elif justify.lower()=="right":
self.justify = "e"
else:
self.justify = "c"
self.button_height = button_height
self.values = values
self.button_num = len(self.values)
self.image_values = None if len(image_values)!=len(self.values) else image_values
self._init_buttons(**button_kwargs)
# Add binding for different ctk widgets
if double_click or self.attach.winfo_name().startswith("!ctkentry") or self.attach.winfo_name().startswith("!ctkcombobox"):
self.attach.bind('<Double-Button-1>', lambda e: self._iconify(), add="+")
self.attach._entry.bind('<FocusOut>', lambda e: self._withdraw() if not self.disable else None, add="+")
else:
self.attach.bind('<Button-1>', lambda e: self._iconify(), add="+")
if self.attach.winfo_name().startswith("!ctkcombobox"):
self.attach._canvas.tag_bind("right_parts", "<Button-1>", lambda e: self._iconify())
self.attach._canvas.tag_bind("dropdown_arrow", "<Button-1>", lambda e: self._iconify())
if self.command is None:
self.command = self.attach.set
if self.attach.winfo_name().startswith("!ctkoptionmenu"):
self.attach._canvas.bind("<Button-1>", lambda e: self._iconify())
self.attach._text_label.bind("<Button-1>", lambda e: self._iconify())
if self.command is None:
self.command = self.attach.set
self.x = x
self.y = y
self.attach.bind("<Destroy>", lambda _: self._destroy(), add="+")
if self.autocomplete:
self.bind_autocomplete()
def _destroy(self):
self.after(500, self.destroy_popup)
def _withdraw(self):
if self.winfo_viewable() and self.hide:
self.place_forget()
self.event_generate("<<Closed>>")
self.hide = True
def _update(self, a, b, c):
self.live_update(self.attach._entry.get())
def bind_autocomplete(self, ):
def appear(x):
self.appear = True
if self.attach.winfo_name().startswith("!ctkcombobox"):
self.attach._entry.configure(textvariable=self.var_update)
self.attach.set(self.values[0])
self.attach._entry.bind("<Key>", appear)
self.var_update.trace_add('write', self._update)
if self.attach.winfo_name().startswith("!ctkentry"):
self.attach.configure(textvariable=self.var_update)
self.attach.bind("<Key>", appear)
self.var_update.trace_add('write', self._update)
def _init_buttons(self, **button_kwargs):
self.i = 0
self.widgets = {}
for row in self.values:
self.widgets[self.i] = customtkinter.CTkButton(self.frame,
text=row,
height=self.button_height,
fg_color=self.button_color,
text_color=self.text_color,
image=self.image_values[i] if self.image_values is not None else None,
anchor=self.justify,
command=lambda k=row: self._attach_key_press(k), **button_kwargs)
self.widgets[self.i].pack(fill="x", pady=2, padx=(self.padding, 0))
self.i+=1
self.hide = False
def destroy_popup(self):
self.destroy()
self.disable = True
def place_dropdown(self):
self.x_pos = self.attach.winfo_x() if self.x is None else self.x + self.attach.winfo_rootx()
self.y_pos = self.attach.winfo_y() + self.attach.winfo_reqheight() + 5 if self.y is None else self.y + self.attach.winfo_rooty()
self.width_new = self.attach.winfo_width()-45+self.corner if self.width is None else self.width
if self.resize:
if self.button_num<=5:
self.height_new = self.button_height * self.button_num + 55
else:
self.height_new = self.button_height * self.button_num + 35
if self.height_new>self.height:
self.height_new = self.height
self.frame.configure(width=self.width_new, height=self.height_new)
self.place(x=self.x_pos, y=self.y_pos)
if sys.platform.startswith("darwin"):
self.dummy_entry.pack()
self.after(100, self.dummy_entry.pack_forget())
self.lift()
self.attach.focus()
def _iconify(self):
if self.disable: return
if self.hide:
self.event_generate("<<Opened>>")
self.hide = False
self.place_dropdown()
else:
self.place_forget()
self.hide = True
def _attach_key_press(self, k):
self.event_generate("<<Selected>>")
self.fade = True
if self.command:
self.command(k)
self.fade = False
self.place_forget()
self.hide = True
def live_update(self, string=None):
if not self.appear: return
if self.disable: return
if self.fade: return
if string:
string = string.lower()
self._deiconify()
i=1
for key in self.widgets.keys():
s = self.widgets[key].cget("text").lower()
text_similarity = difflib.SequenceMatcher(None, s[0:len(string)], string).ratio()
similar = s.startswith(string) or text_similarity > 0.75
if not similar:
self.widgets[key].pack_forget()
else:
self.widgets[key].pack(fill="x", pady=2, padx=(self.padding, 0))
i+=1
if i==1:
self.no_match.pack(fill="x", pady=2, padx=(self.padding, 0))
else:
self.no_match.pack_forget()
self.button_num = i
self.place_dropdown()
else:
self.no_match.pack_forget()
self.button_num = len(self.values)
for key in self.widgets.keys():
self.widgets[key].destroy()
self._init_buttons()
self.place_dropdown()
self.frame._parent_canvas.yview_moveto(0.0)
self.appear = False
def insert(self, value, **kwargs):
self.widgets[self.i] = customtkinter.CTkButton(self.frame,
text=value,
height=self.button_height,
fg_color=self.button_color,
text_color=self.text_color,
anchor=self.justify,
command=lambda k=value: self._attach_key_press(k), **kwargs)
self.widgets[self.i].pack(fill="x", pady=2, padx=(self.padding, 0))
self.i+=1
self.values.append(value)
def _deiconify(self):
if len(self.values)>0:
self.pack_forget()
def popup(self, x=None, y=None):
self.x = x
self.y = y
self.hide = True
self._iconify()
def configure(self, **kwargs):
if "height" in kwargs:
self.height = kwargs.pop("height")
self.height_new = self.height
if "alpha" in kwargs:
self.alpha = kwargs.pop("alpha")
if "width" in kwargs:
self.width = kwargs.pop("width")
if "fg_color" in kwargs:
self.frame.configure(fg_color=kwargs.pop("fg_color"))
if "values" in kwargs:
self.values = kwargs.pop("values")
self.image_values = None
self.button_num = len(self.values)
for key in self.widgets.keys():
self.widgets[key].destroy()
self._init_buttons()
if "image_values" in kwargs:
self.image_values = kwargs.pop("image_values")
self.image_values = None if len(self.image_values)!=len(self.values) else self.image_values
if self.image_values is not None:
i=0
for key in self.widgets.keys():
self.widgets[key].configure(image=self.image_values[i])
i+=1
if "button_color" in kwargs:
for key in self.widgets.keys():
self.widgets[key].configure(fg_color=kwargs.pop("button_color"))
for key in self.widgets.keys():
self.widgets[key].configure(**kwargs)

View File

@ -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("<Return>", 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

View File

@ -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)}"

View File

@ -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("<Enter>", self.on_enter, add="+")
self.widget.bind("<Leave>", self.on_leave, add="+")
self.widget.bind("<Motion>", self.on_enter, add="+")
self.widget.bind("<B1-Motion>", self.on_enter, add="+")
self.widget.bind("<Destroy>", 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)

View File

@ -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("<B1-Motion>", self.move_window)
self.frame_top.bind("<ButtonPress-1>", 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("<B1-Motion>", self.move_window)
self.title_label.bind("<ButtonPress-1>", 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("<Return>", lambda event: self.button_event(self.option_text_1))
self.bind("<Escape>", 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("<Return>", lambda event: self.button_event(self.selected_option))
except AttributeError:
return
self.bind("<Left>", lambda e: self.change_left())
self.bind("<Right>", lambda e: self.change_right())
def change_left(self):
if self.option_focus==3:
return
self.selected_button.unbind("<Return>")
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("<Return>")
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()

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

158
MCE/student_list/EN.json Normal file
View File

@ -0,0 +1,158 @@
[
"Airi",
"Akane",
"Akane (Bunny Girl)",
"Akari",
"Ako",
"Arisu",
"Arisu (Maid)",
"Aru",
"Aru (New Year)",
"Asuna",
"Asuna (Bunny Girl)",
"Atsuko",
"Ayane",
"Ayane (Swimsuit)",
"Azusa",
"Azusa (Swimsuit)",
"Cherino",
"Cherino (Hot Spring)",
"Chihiro",
"Chinatsu",
"Chinatsu (Hot Spring)",
"Chise",
"Chise (Swimsuit)",
"Eimi",
"Fubuki",
"Fuuka",
"Fuuka (New Year)",
"Hanae",
"Hanae (Christmas)",
"Hanako",
"Hanako (Swimsuit)",
"Hare",
"Haruka",
"Haruka (New Year)",
"Haruna",
"Haruna (New Year)",
"Haruna (Sportswear)",
"Hasumi",
"Hasumi (Sportswear)",
"Hatsune Miku",
"Hibiki",
"Hibiki (Cheerleader)",
"Hifumi",
"Hifumi (Swimsuit)",
"Himari",
"Hina",
"Hina (Swimsuit)",
"Hinata",
"Hinata (Swimsuit)",
"Hiyori",
"Hoshino",
"Hoshino (Swimsuit)",
"Iori",
"Iori (Swimsuit)",
"Iroha",
"Izumi",
"Izumi (Swimsuit)",
"Izuna",
"Izuna (Swimsuit)",
"Junko",
"Junko (New Year)",
"Juri",
"Kaede",
"Kaho",
"Kanna",
"Karin",
"Karin (Bunny Girl)",
"Kayoko",
"Kayoko (New Year)",
"Kazusa",
"Kirino",
"Koharu",
"Koharu (Swimsuit)",
"Kokona",
"Kotama",
"Kotori",
"Kotori (Cheerleader)",
"Koyuki",
"Maki",
"Mari",
"Mari (Sportswear)",
"Marina",
"Mashiro",
"Mashiro (Swimsuit)",
"Megu",
"Meru",
"Michiru",
"Midori",
"Mika",
"Mimori",
"Mimori (Swimsuit)",
"Mina",
"Mine",
"Minori",
"Misaki",
"Miyako",
"Miyako (Swimsuit)",
"Miyu",
"Miyu (Swimsuit)",
"Moe",
"Momiji",
"Momoi",
"Mutsuki",
"Mutsuki (New Year)",
"Nagisa",
"Natsu",
"Neru",
"Neru (Bunny Girl)",
"Noa",
"Nodoka",
"Nodoka (Hot Spring)",
"Nonomi",
"Nonomi (Swimsuit)",
"Pina",
"Reisa",
"Rumi",
"Saki",
"Saki (Swimsuit)",
"Sakurako",
"Saori",
"Saya",
"Saya (Casual)",
"Sena",
"Serika",
"Serika (New Year)",
"Serina",
"Serina (Christmas)",
"Shigure",
"Shimiko",
"Shiroko",
"Shiroko (Riding)",
"Shiroko (Swimsuit)",
"Shizuko",
"Shizuko (Swimsuit)",
"Shun",
"Shun (Kid)",
"Sumire",
"Suzumi",
"Toki",
"Toki (Bunny Girl)",
"Tomoe",
"Tsubaki",
"Tsukuyo",
"Tsurugi",
"Tsurugi (Swimsuit)",
"Ui",
"Ui (Swimsuit)",
"Utaha",
"Utaha (Cheerleader)",
"Wakamo",
"Wakamo (Swimsuit)",
"Yoshimi",
"Yuuka",
"Yuuka (Sportswear)",
"Yuzu",
"Yuzu (Maid)"
]

168
MCE/student_list/JP.json Normal file
View File

@ -0,0 +1,168 @@
[
"\u30a2\u30a4\u30ea",
"\u30a2\u30ab\u30cd",
"\u30a2\u30ab\u30cd\n\uff08\u30d0\u30cb\u30fc\u30ac\u30fc\u30eb\uff09",
"\u30a2\u30ab\u30ea",
"\u30a2\u30b3",
"\u30a2\u30b9\u30ca",
"\u30a2\u30b9\u30ca\n\uff08\u30d0\u30cb\u30fc\u30ac\u30fc\u30eb\uff09",
"\u30a2\u30ba\u30b5",
"\u30a2\u30ba\u30b5\uff08\u6c34\u7740\uff09",
"\u30a2\u30c4\u30b3",
"\u30a2\u30e4\u30cd",
"\u30a2\u30e4\u30cd\uff08\u6c34\u7740\uff09",
"\u30a2\u30ea\u30b9",
"\u30a2\u30ea\u30b9\uff08\u30e1\u30a4\u30c9\uff09",
"\u30a2\u30eb",
"\u30a2\u30eb\uff08\u6b63\u6708\uff09",
"\u30a4\u30aa\u30ea",
"\u30a4\u30aa\u30ea\uff08\u6c34\u7740\uff09",
"\u30a4\u30ba\u30ca",
"\u30a4\u30ba\u30ca\uff08\u6c34\u7740\uff09",
"\u30a4\u30ba\u30df",
"\u30a4\u30ba\u30df\uff08\u6c34\u7740\uff09",
"\u30a4\u30c1\u30ab",
"\u30a4\u30ed\u30cf",
"\u30a6\u30a4",
"\u30a6\u30a4\uff08\u6c34\u7740\uff09",
"\u30a6\u30bf\u30cf",
"\u30a6\u30bf\u30cf\uff08\u5fdc\u63f4\u56e3\uff09",
"\u30a8\u30a4\u30df",
"\u30a8\u30a4\u30df\uff08\u6c34\u7740\uff09",
"\u30ab\u30a8\u30c7",
"\u30ab\u30b9\u30df",
"\u30ab\u30ba\u30b5",
"\u30ab\u30db",
"\u30ab\u30e8\u30b3",
"\u30ab\u30e8\u30b3\uff08\u6b63\u6708\uff09",
"\u30ab\u30ea\u30f3",
"\u30ab\u30ea\u30f3\n\uff08\u30d0\u30cb\u30fc\u30ac\u30fc\u30eb\uff09",
"\u30ab\u30f3\u30ca",
"\u30ad\u30ad\u30e7\u30a6",
"\u30ad\u30ea\u30ce",
"\u30b3\u30b3\u30ca",
"\u30b3\u30bf\u30de",
"\u30b3\u30c8\u30ea",
"\u30b3\u30c8\u30ea\uff08\u5fdc\u63f4\u56e3\uff09",
"\u30b3\u30cf\u30eb",
"\u30b3\u30cf\u30eb\uff08\u6c34\u7740\uff09",
"\u30b3\u30e6\u30ad",
"\u30b5\u30aa\u30ea",
"\u30b5\u30ad",
"\u30b5\u30ad\uff08\u6c34\u7740\uff09",
"\u30b5\u30af\u30e9\u30b3",
"\u30b5\u30e4",
"\u30b5\u30e4\uff08\u79c1\u670d\uff09",
"\u30b7\u30b0\u30ec",
"\u30b7\u30b0\u30ec\uff08\u6e29\u6cc9\uff09",
"\u30b7\u30ba\u30b3",
"\u30b7\u30ba\u30b3\uff08\u6c34\u7740\uff09",
"\u30b7\u30df\u30b3",
"\u30b7\u30e5\u30f3",
"\u30b7\u30e5\u30f3\uff08\u5e7c\u5973\uff09",
"\u30b7\u30ed\u30b3",
"\u30b7\u30ed\u30b3\n\uff08\u30e9\u30a4\u30c7\u30a3\u30f3\u30b0\uff09",
"\u30b7\u30ed\u30b3\uff08\u6c34\u7740\uff09",
"\u30b8\u30e5\u30ea",
"\u30b8\u30e5\u30f3\u30b3",
"\u30b8\u30e5\u30f3\u30b3\uff08\u6b63\u6708\uff09",
"\u30b9\u30ba\u30df",
"\u30b9\u30df\u30ec",
"\u30bb\u30ca",
"\u30bb\u30ea\u30ab",
"\u30bb\u30ea\u30ab\uff08\u6b63\u6708\uff09",
"\u30bb\u30ea\u30ca",
"\u30bb\u30ea\u30ca\n\uff08\u30af\u30ea\u30b9\u30de\u30b9\uff09",
"\u30c1\u30a7\u30ea\u30ce",
"\u30c1\u30a7\u30ea\u30ce\uff08\u6e29\u6cc9\uff09",
"\u30c1\u30bb",
"\u30c1\u30bb\uff08\u6c34\u7740\uff09",
"\u30c1\u30ca\u30c4",
"\u30c1\u30ca\u30c4\uff08\u6e29\u6cc9\uff09",
"\u30c1\u30d2\u30ed",
"\u30c4\u30af\u30e8",
"\u30c4\u30d0\u30ad",
"\u30c4\u30eb\u30ae",
"\u30c4\u30eb\u30ae\uff08\u6c34\u7740\uff09",
"\u30c8\u30ad",
"\u30c8\u30ad\n\uff08\u30d0\u30cb\u30fc\u30ac\u30fc\u30eb\uff09",
"\u30c8\u30e2\u30a8",
"\u30ca\u30ae\u30b5",
"\u30ca\u30c4",
"\u30cd\u30eb",
"\u30cd\u30eb\n\uff08\u30d0\u30cb\u30fc\u30ac\u30fc\u30eb\uff09",
"\u30ce\u30a2",
"\u30ce\u30c9\u30ab",
"\u30ce\u30c9\u30ab\uff08\u6e29\u6cc9\uff09",
"\u30ce\u30ce\u30df",
"\u30ce\u30ce\u30df\uff08\u6c34\u7740\uff09",
"\u30cf\u30b9\u30df",
"\u30cf\u30b9\u30df\uff08\u4f53\u64cd\u670d\uff09",
"\u30cf\u30ca\u30a8",
"\u30cf\u30ca\u30a8\n\uff08\u30af\u30ea\u30b9\u30de\u30b9\uff09",
"\u30cf\u30ca\u30b3",
"\u30cf\u30ca\u30b3\uff08\u6c34\u7740\uff09",
"\u30cf\u30eb\u30ab",
"\u30cf\u30eb\u30ab\uff08\u6b63\u6708\uff09",
"\u30cf\u30eb\u30ca",
"\u30cf\u30eb\u30ca\uff08\u4f53\u64cd\u670d\uff09",
"\u30cf\u30eb\u30ca\uff08\u6b63\u6708\uff09",
"\u30cf\u30ec",
"\u30d2\u30ca",
"\u30d2\u30ca\u30bf",
"\u30d2\u30ca\u30bf\uff08\u6c34\u7740\uff09",
"\u30d2\u30ca\uff08\u6c34\u7740\uff09",
"\u30d2\u30d3\u30ad",
"\u30d2\u30d3\u30ad\uff08\u5fdc\u63f4\u56e3\uff09",
"\u30d2\u30d5\u30df",
"\u30d2\u30d5\u30df\uff08\u6c34\u7740\uff09",
"\u30d2\u30de\u30ea",
"\u30d2\u30e8\u30ea",
"\u30d5\u30a3\u30fc\u30ca",
"\u30d5\u30a6\u30ab",
"\u30d5\u30a6\u30ab\uff08\u6b63\u6708\uff09",
"\u30d5\u30d6\u30ad",
"\u30db\u30b7\u30ce",
"\u30db\u30b7\u30ce\uff08\u6c34\u7740\uff09",
"\u30de\u30ad",
"\u30de\u30b7\u30ed",
"\u30de\u30b7\u30ed\uff08\u6c34\u7740\uff09",
"\u30de\u30ea\u30ca",
"\u30de\u30ea\u30fc",
"\u30de\u30ea\u30fc\uff08\u4f53\u64cd\u670d\uff09",
"\u30df\u30ab",
"\u30df\u30b5\u30ad",
"\u30df\u30c1\u30eb",
"\u30df\u30c9\u30ea",
"\u30df\u30ca",
"\u30df\u30cd",
"\u30df\u30ce\u30ea",
"\u30df\u30e2\u30ea",
"\u30df\u30e2\u30ea\uff08\u6c34\u7740\uff09",
"\u30df\u30e4\u30b3",
"\u30df\u30e4\u30b3\uff08\u6c34\u7740\uff09",
"\u30df\u30e6",
"\u30df\u30e6\uff08\u6c34\u7740\uff09",
"\u30e0\u30c4\u30ad",
"\u30e0\u30c4\u30ad\uff08\u6b63\u6708\uff09",
"\u30e1\u30b0",
"\u30e1\u30eb",
"\u30e2\u30a8",
"\u30e2\u30df\u30b8",
"\u30e2\u30e2\u30a4",
"\u30e6\u30a6\u30ab",
"\u30e6\u30a6\u30ab\uff08\u4f53\u64cd\u670d\uff09",
"\u30e6\u30ab\u30ea",
"\u30e6\u30ba",
"\u30e6\u30ba\uff08\u30e1\u30a4\u30c9\uff09",
"\u30e8\u30b7\u30df",
"\u30eb\u30df",
"\u30ec\u30a4\u30b5",
"\u30ec\u30f3\u30b2",
"\u30ef\u30ab\u30e2",
"\u30ef\u30ab\u30e2\uff08\u6c34\u7740\uff09",
"\u4f50\u5929\u6d99\u5b50",
"\u521d\u97f3\u30df\u30af",
"\u5fa1\u5742\u7f8e\u7434",
"\u98df\u8702\u64cd\u7948"
]

109
MCE/utils.py Normal file
View File

@ -0,0 +1,109 @@
import customtkinter
import json
import sys
import os
class Config:
def __init__(self, linker, config_file):
self.default_config = {
"ResetDaily": False,
"LastRun": "2023-12-24 21:41:55",
"ResetTime": "11:21:30",
"RechargeAP": False,
"PreferredTemplate": "template1",
"Queue": [],
"Event": False,
"Templates": {
"template1": []
}
}
self.linker = linker
self.config_file = config_file
if not os.path.exists(self.config_file):
with open(self.config_file, "w") as f:
json.dump(self.default_config, f, indent=2)
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)

17
aas.py
View File

@ -46,11 +46,26 @@ class ArisuAutoSweeper(AzurLaneAutoScript):
from tasks.tactical_challenge.tactical_challenge import TacticalChallenge from tasks.tactical_challenge.tactical_challenge import TacticalChallenge
TacticalChallenge(config=self.config, device=self.device).run() TacticalChallenge(config=self.config, device=self.device).run()
def task(self):
from tasks.task.task import Task
Task(config=self.config, device=self.device).run()
def shop(self):
from tasks.shop.shop import Shop
Shop(config=self.config, device=self.device).run()
def momotalk(self):
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): def data_update(self):
from tasks.item.data_update import DataUpdate from tasks.item.data_update import DataUpdate
DataUpdate(config=self.config, device=self.device).run() DataUpdate(config=self.config, device=self.device).run()
if __name__ == '__main__': if __name__ == '__main__':
aas = ArisuAutoSweeper('aas') aas = ArisuAutoSweeper('aas')
aas.loop() aas.loop()

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

BIN
assets/en/mission/LEFT.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

BIN
assets/en/mission/RIGHT.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

BIN
assets/en/momotalk/MENU.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

BIN
assets/en/momotalk/SKIP.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 274 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

BIN
assets/en/shop/PURCHASE.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

BIN
assets/en/shop/REFRESH.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

BIN
assets/en/shop/TC_OFF.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

BIN
assets/en/shop/TC_ON.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

BIN
assets/en/task/CLAIM.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

BIN
assets/en/task/CLAIMED.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
assets/en/task/COMPLETE.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

BIN
assets/jp/task/CLAIM.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

BIN
assets/jp/task/CLAIMED.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -33,6 +33,22 @@
"ServerUpdate": "04:00" "ServerUpdate": "04:00"
} }
}, },
"DataUpdate": {
"Scheduler": {
"Enable": true,
"NextRun": "2020-01-01 00:00:00",
"Command": "DataUpdate",
"ServerUpdate": "04:00"
},
"ItemStorage": {
"AP": {},
"Credit": {},
"Pyroxene": {},
"BountyTicket": {},
"ScrimmageTicket": {},
"TacticalChallengeTicket": {}
}
},
"Cafe": { "Cafe": {
"Scheduler": { "Scheduler": {
"Enable": true, "Enable": true,
@ -54,20 +70,55 @@
"Substitute": false "Substitute": false
} }
}, },
"Circle": { "Shop": {
"Scheduler": { "Scheduler": {
"Enable": true, "Enable": false,
"NextRun": "2020-01-01 00:00:00", "NextRun": "2020-01-01 00:00:00",
"Command": "Circle", "Command": "Shop",
"ServerUpdate": "04:00"
}
},
"Mail": {
"Scheduler": {
"Enable": true,
"NextRun": "2020-01-01 00:00:00",
"Command": "Mail",
"ServerUpdate": "04:00" "ServerUpdate": "04:00"
},
"NormalShop": {
"Enable": false,
"Purchases": 1,
"1": false,
"2": false,
"3": false,
"4": false,
"5": false,
"6": false,
"7": false,
"8": false,
"9": false,
"10": false,
"11": false,
"12": false,
"13": false,
"14": false,
"15": false,
"16": false,
"17": false,
"18": false,
"19": false,
"20": false
},
"TacticalChallengeShop": {
"Enable": false,
"Purchases": 1,
"1": false,
"2": false,
"3": false,
"4": false,
"5": false,
"6": false,
"7": false,
"8": false,
"9": false,
"10": false,
"11": false,
"12": false,
"13": false,
"14": false,
"15": false
} }
}, },
"Bounty": { "Bounty": {
@ -121,26 +172,50 @@
"Enable": true, "Enable": true,
"NextRun": "2020-01-01 00:00:00", "NextRun": "2020-01-01 00:00:00",
"Command": "TacticalChallenge", "Command": "TacticalChallenge",
"ServerUpdate": "15:00" "ServerUpdate": "14:00"
}, },
"TacticalChallenge": { "TacticalChallenge": {
"PlayerSelect": 0 "PlayerSelect": 0
} }
}, },
"DataUpdate": { "Mission": {
"Scheduler": {
"Enable": false,
"NextRun": "2020-01-01 00:00:00",
"Command": "Mission",
"ServerUpdate": "04:00"
}
},
"Circle": {
"Scheduler": { "Scheduler": {
"Enable": true, "Enable": true,
"NextRun": "2020-01-01 00:00:00", "NextRun": "2020-01-01 00:00:00",
"Command": "DataUpdate", "Command": "Circle",
"ServerUpdate": "04:00"
}
},
"Task": {
"Scheduler": {
"Enable": false,
"NextRun": "2020-01-01 00:00:00",
"Command": "Task",
"ServerUpdate": "04:00"
}
},
"Mail": {
"Scheduler": {
"Enable": true,
"NextRun": "2020-01-01 00:00:00",
"Command": "Mail",
"ServerUpdate": "04:00"
}
},
"Momotalk": {
"Scheduler": {
"Enable": false,
"NextRun": "2020-01-01 00:00:00",
"Command": "Momotalk",
"ServerUpdate": "04:00" "ServerUpdate": "04:00"
},
"ItemStorage": {
"AP": {},
"Credit": {},
"Pyroxene": {},
"BountyTicket": {},
"ScrimmageTicket": {},
"TacticalChallengeTicket": {}
} }
} }
} }

View File

@ -162,6 +162,85 @@
} }
} }
}, },
"DataUpdate": {
"Scheduler": {
"Enable": {
"type": "state",
"value": true,
"option": [
true
],
"option_bold": [
true
]
},
"NextRun": {
"type": "datetime",
"value": "2020-01-01 00:00:00",
"validate": "datetime"
},
"Command": {
"type": "input",
"value": "DataUpdate",
"display": "hide"
},
"ServerUpdate": {
"type": "input",
"value": "04:00",
"display": "hide"
}
},
"ItemStorage": {
"AP": {
"type": "stored",
"value": {},
"display": "hide",
"stored": "StoredAP",
"order": 1,
"color": "#62ea6e"
},
"Credit": {
"type": "stored",
"value": {},
"display": "hide",
"stored": "StoredInt",
"order": 2,
"color": "#fdec00"
},
"Pyroxene": {
"type": "stored",
"value": {},
"display": "hide",
"stored": "StoredInt",
"order": 3,
"color": "#21befc"
},
"BountyTicket": {
"type": "stored",
"value": {},
"display": "hide",
"stored": "StoredBountyTicket",
"order": 4,
"color": "#94cb44"
},
"ScrimmageTicket": {
"type": "stored",
"value": {},
"display": "hide",
"stored": "StoredScrimmageTicket",
"order": 5,
"color": "#f86c6a"
},
"TacticalChallengeTicket": {
"type": "stored",
"value": {},
"display": "hide",
"stored": "StoredTacticalChallengeTicket",
"order": 6,
"color": "#7ac8e5"
}
}
},
"Cafe": { "Cafe": {
"Scheduler": { "Scheduler": {
"Enable": { "Enable": {
@ -239,11 +318,11 @@
} }
} }
}, },
"Circle": { "Shop": {
"Scheduler": { "Scheduler": {
"Enable": { "Enable": {
"type": "checkbox", "type": "checkbox",
"value": true, "value": false,
"option": [ "option": [
true, true,
false false
@ -256,7 +335,7 @@
}, },
"Command": { "Command": {
"type": "input", "type": "input",
"value": "Circle", "value": "Shop",
"display": "hide" "display": "hide"
}, },
"ServerUpdate": { "ServerUpdate": {
@ -264,32 +343,177 @@
"value": "04:00", "value": "04:00",
"display": "hide" "display": "hide"
} }
} },
}, "NormalShop": {
"Mail": {
"Scheduler": {
"Enable": { "Enable": {
"type": "checkbox", "type": "checkbox",
"value": true, "value": false
},
"Purchases": {
"type": "select",
"value": 1,
"option": [ "option": [
true, 1,
false 2,
3,
4
] ]
}, },
"NextRun": { "1": {
"type": "datetime", "type": "checkbox",
"value": "2020-01-01 00:00:00", "value": false
"validate": "datetime"
}, },
"Command": { "2": {
"type": "input", "type": "checkbox",
"value": "Mail", "value": false
"display": "hide"
}, },
"ServerUpdate": { "3": {
"type": "input", "type": "checkbox",
"value": "04:00", "value": false
"display": "hide" },
"4": {
"type": "checkbox",
"value": false
},
"5": {
"type": "checkbox",
"value": false
},
"6": {
"type": "checkbox",
"value": false
},
"7": {
"type": "checkbox",
"value": false
},
"8": {
"type": "checkbox",
"value": false
},
"9": {
"type": "checkbox",
"value": false
},
"10": {
"type": "checkbox",
"value": false
},
"11": {
"type": "checkbox",
"value": false
},
"12": {
"type": "checkbox",
"value": false
},
"13": {
"type": "checkbox",
"value": false
},
"14": {
"type": "checkbox",
"value": false
},
"15": {
"type": "checkbox",
"value": false
},
"16": {
"type": "checkbox",
"value": false
},
"17": {
"type": "checkbox",
"value": false
},
"18": {
"type": "checkbox",
"value": false
},
"19": {
"type": "checkbox",
"value": false
},
"20": {
"type": "checkbox",
"value": false
}
},
"TacticalChallengeShop": {
"Enable": {
"type": "checkbox",
"value": false
},
"Purchases": {
"type": "select",
"value": 1,
"option": [
1,
2,
3,
4
]
},
"1": {
"type": "checkbox",
"value": false
},
"2": {
"type": "checkbox",
"value": false
},
"3": {
"type": "checkbox",
"value": false
},
"4": {
"type": "checkbox",
"value": false
},
"5": {
"type": "checkbox",
"value": false
},
"6": {
"type": "checkbox",
"value": false
},
"7": {
"type": "checkbox",
"value": false
},
"8": {
"type": "checkbox",
"value": false
},
"9": {
"type": "checkbox",
"value": false
},
"10": {
"type": "checkbox",
"value": false
},
"11": {
"type": "checkbox",
"value": false
},
"12": {
"type": "checkbox",
"value": false
},
"13": {
"type": "checkbox",
"value": false
},
"14": {
"type": "checkbox",
"value": false
},
"15": {
"type": "checkbox",
"value": false
} }
} }
}, },
@ -506,7 +730,7 @@
}, },
"ServerUpdate": { "ServerUpdate": {
"type": "input", "type": "input",
"value": "15:00", "value": "14:00",
"display": "hide" "display": "hide"
} }
}, },
@ -523,16 +747,14 @@
} }
} }
}, },
"DataUpdate": { "Mission": {
"Scheduler": { "Scheduler": {
"Enable": { "Enable": {
"type": "state", "type": "checkbox",
"value": true, "value": false,
"option": [ "option": [
true true,
], false
"option_bold": [
true
] ]
}, },
"NextRun": { "NextRun": {
@ -542,7 +764,7 @@
}, },
"Command": { "Command": {
"type": "input", "type": "input",
"value": "DataUpdate", "value": "Mission",
"display": "hide" "display": "hide"
}, },
"ServerUpdate": { "ServerUpdate": {
@ -550,55 +772,113 @@
"value": "04:00", "value": "04:00",
"display": "hide" "display": "hide"
} }
}, }
"ItemStorage": { },
"AP": { "Circle": {
"type": "stored", "Scheduler": {
"value": {}, "Enable": {
"display": "hide", "type": "checkbox",
"stored": "StoredAP", "value": true,
"order": 1, "option": [
"color": "#62ea6e" true,
false
]
}, },
"Credit": { "NextRun": {
"type": "stored", "type": "datetime",
"value": {}, "value": "2020-01-01 00:00:00",
"display": "hide", "validate": "datetime"
"stored": "StoredInt",
"order": 2,
"color": "#fdec00"
}, },
"Pyroxene": { "Command": {
"type": "stored", "type": "input",
"value": {}, "value": "Circle",
"display": "hide", "display": "hide"
"stored": "StoredInt",
"order": 3,
"color": "#21befc"
}, },
"BountyTicket": { "ServerUpdate": {
"type": "stored", "type": "input",
"value": {}, "value": "04:00",
"display": "hide", "display": "hide"
"stored": "StoredBountyTicket", }
"order": 4, }
"color": "#94cb44" },
"Task": {
"Scheduler": {
"Enable": {
"type": "checkbox",
"value": false,
"option": [
true,
false
]
}, },
"ScrimmageTicket": { "NextRun": {
"type": "stored", "type": "datetime",
"value": {}, "value": "2020-01-01 00:00:00",
"display": "hide", "validate": "datetime"
"stored": "StoredScrimmageTicket",
"order": 5,
"color": "#f86c6a"
}, },
"TacticalChallengeTicket": { "Command": {
"type": "stored", "type": "input",
"value": {}, "value": "Task",
"display": "hide", "display": "hide"
"stored": "StoredTacticalChallengeTicket", },
"order": 6, "ServerUpdate": {
"color": "#7ac8e5" "type": "input",
"value": "04:00",
"display": "hide"
}
}
},
"Mail": {
"Scheduler": {
"Enable": {
"type": "checkbox",
"value": true,
"option": [
true,
false
]
},
"NextRun": {
"type": "datetime",
"value": "2020-01-01 00:00:00",
"validate": "datetime"
},
"Command": {
"type": "input",
"value": "Mail",
"display": "hide"
},
"ServerUpdate": {
"type": "input",
"value": "04:00",
"display": "hide"
}
}
},
"Momotalk": {
"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": "Momotalk",
"display": "hide"
},
"ServerUpdate": {
"type": "input",
"value": "04:00",
"display": "hide"
} }
} }
} }

View File

@ -140,6 +140,53 @@ TacticalChallenge:
value: 0 value: 0
option: [ 0, 1, 2, 3 ] option: [ 0, 1, 2, 3 ]
NormalShop:
Enable: false
Purchases:
value: 1
option: [ 1, 2, 3, 4 ]
"1": false
"2": false
"3": false
"4": false
"5": false
"6": false
"7": false
"8": false
"9": false
"10": false
"11": false
"12": false
"13": false
"14": false
"15": false
"16": false
"17": false
"18": false
"19": false
"20": false
TacticalChallengeShop:
Enable: false
Purchases:
value: 1
option: [ 1, 2, 3, 4 ]
"1": false
"2": false
"3": false
"4": false
"5": false
"6": false
"7": false
"8": false
"9": false
"10": false
"11": false
"12": false
"13": false
"14": false
"15": false
ItemStorage: ItemStorage:
AP: AP:
stored: StoredAP stored: StoredAP

View File

@ -4,20 +4,36 @@
"page": "setting", "page": "setting",
"tasks": [ "tasks": [
"Alas", "Alas",
"Restart" "Restart",
"DataUpdate"
] ]
}, },
"Daily": { "Daily": {
"menu": "list", "menu": "collapse",
"page": "setting", "page": "setting",
"tasks": [ "tasks": [
"Cafe", "Cafe",
"Circle", "Shop"
"Mail", ]
},
"Farm": {
"menu": "collapse",
"page": "setting",
"tasks": [
"Bounty", "Bounty",
"Scrimmage", "Scrimmage",
"TacticalChallenge", "TacticalChallenge",
"DataUpdate" "Mission"
]
},
"Reward": {
"menu": "collapse",
"page": "setting",
"tasks": [
"Circle",
"Task",
"Mail",
"Momotalk"
] ]
} }
} }

View File

@ -29,7 +29,7 @@ Cafe:
TacticalChallenge: TacticalChallenge:
Scheduler: Scheduler:
ServerUpdate: "15:00" ServerUpdate: "14:00"
DataUpdate: DataUpdate:
Scheduler: Scheduler:

View File

@ -15,21 +15,31 @@ Alas:
- Optimization - Optimization
Restart: Restart:
- Scheduler - Scheduler
DataUpdate:
- Scheduler
- ItemStorage
# ==================== Daily ==================== # ==================== Daily ====================
Daily: Daily:
menu: 'list' menu: 'collapse'
page: 'setting' page: 'setting'
tasks: tasks:
Cafe: Cafe:
- Scheduler - Scheduler
- Cafe - Cafe
- Invitation - Invitation
Circle: Shop:
- Scheduler
Mail:
- Scheduler - Scheduler
- NormalShop
- TacticalChallengeShop
# ==================== Farm ====================
Farm:
menu: 'collapse'
page: 'setting'
tasks:
Bounty: Bounty:
- Scheduler - Scheduler
- Bounty - Bounty
@ -45,6 +55,20 @@ Daily:
TacticalChallenge: TacticalChallenge:
- Scheduler - Scheduler
- TacticalChallenge - TacticalChallenge
DataUpdate: Mission:
- Scheduler - Scheduler
- ItemStorage
# ==================== Rewards ====================
Reward:
menu: 'collapse'
page: 'setting'
tasks:
Circle:
- Scheduler
Task:
- Scheduler
Mail:
- Scheduler
Momotalk:
- Scheduler

View File

@ -85,6 +85,49 @@ class GeneratedConfig:
# Group `TacticalChallenge` # Group `TacticalChallenge`
TacticalChallenge_PlayerSelect = 0 # 0, 1, 2, 3 TacticalChallenge_PlayerSelect = 0 # 0, 1, 2, 3
# Group `NormalShop`
NormalShop_Enable = False
NormalShop_Purchases = 1 # 1, 2, 3, 4
NormalShop_1 = False
NormalShop_2 = False
NormalShop_3 = False
NormalShop_4 = False
NormalShop_5 = False
NormalShop_6 = False
NormalShop_7 = False
NormalShop_8 = False
NormalShop_9 = False
NormalShop_10 = False
NormalShop_11 = False
NormalShop_12 = False
NormalShop_13 = False
NormalShop_14 = False
NormalShop_15 = False
NormalShop_16 = False
NormalShop_17 = False
NormalShop_18 = False
NormalShop_19 = False
NormalShop_20 = False
# Group `TacticalChallengeShop`
TacticalChallengeShop_Enable = False
TacticalChallengeShop_Purchases = 1 # 1, 2, 3, 4
TacticalChallengeShop_1 = False
TacticalChallengeShop_2 = False
TacticalChallengeShop_3 = False
TacticalChallengeShop_4 = False
TacticalChallengeShop_5 = False
TacticalChallengeShop_6 = False
TacticalChallengeShop_7 = False
TacticalChallengeShop_8 = False
TacticalChallengeShop_9 = False
TacticalChallengeShop_10 = False
TacticalChallengeShop_11 = False
TacticalChallengeShop_12 = False
TacticalChallengeShop_13 = False
TacticalChallengeShop_14 = False
TacticalChallengeShop_15 = False
# Group `ItemStorage` # Group `ItemStorage`
ItemStorage_AP = {} ItemStorage_AP = {}
ItemStorage_Credit = {} ItemStorage_Credit = {}

View File

@ -8,8 +8,8 @@ class ManualConfig:
SCHEDULER_PRIORITY = """ SCHEDULER_PRIORITY = """
Restart Restart
> Cafe > Circle > Mail > DataUpdate > Bounty > Cafe > TacticalChallenge > Circle > Mail
> Scrimmage > TacticalChallenge > DataUpdate > Bounty > Scrimmage > Task > Shop > Mission > Momotalk
""" """
""" """

View File

@ -7,6 +7,14 @@
"Daily": { "Daily": {
"name": "Daily", "name": "Daily",
"help": "" "help": ""
},
"Farm": {
"name": "Farm",
"help": ""
},
"Reward": {
"name": "Reward",
"help": ""
} }
}, },
"Task": { "Task": {
@ -18,16 +26,16 @@
"name": "Error Handling", "name": "Error Handling",
"help": "" "help": ""
}, },
"DataUpdate": {
"name": "Dashboard Upd",
"help": ""
},
"Cafe": { "Cafe": {
"name": "Cafe", "name": "Cafe",
"help": "" "help": ""
}, },
"Circle": { "Shop": {
"name": "Club", "name": "Shop",
"help": ""
},
"Mail": {
"name": "Mailbox",
"help": "" "help": ""
}, },
"Bounty": { "Bounty": {
@ -42,8 +50,24 @@
"name": "Tactical Challenge", "name": "Tactical Challenge",
"help": "" "help": ""
}, },
"DataUpdate": { "Mission": {
"name": "Dashboard Upd", "name": "Mission/Commissions/Event",
"help": "Open MCE Manager for additional settings. Must be opened if it's your first time!"
},
"Circle": {
"name": "Club",
"help": ""
},
"Task": {
"name": "Tasks",
"help": ""
},
"Mail": {
"name": "Mailbox",
"help": ""
},
"Momotalk": {
"name": "MomoTalk",
"help": "" "help": ""
} }
}, },
@ -420,6 +444,182 @@
"3": "Third" "3": "Third"
} }
}, },
"NormalShop": {
"_info": {
"name": "Normal Shop Settings",
"help": ""
},
"Enable": {
"name": "Enable",
"help": ""
},
"Purchases": {
"name": "Number of Purchases",
"help": "Default can be purchased once + number of refreshes = number of purchases, for example, 2 purchases = 1 default purchase + 1 refresh purchase",
"1": "1",
"2": "2",
"3": "3",
"4": "4"
},
"1": {
"name": "1",
"help": "x5 Novice Activity Report - 12,500 Credits"
},
"2": {
"name": "2",
"help": "x5 Normal Activity Report - 125,000 Credits"
},
"3": {
"name": "3",
"help": "x3 Advanced Activity Report - 300,000 Credits"
},
"4": {
"name": "4",
"help": "x1 Superior Activity Report - 500,000 Credits"
},
"5": {
"name": "5",
"help": "x5 Lesser Enhancement Stone - 10,000 Credits"
},
"6": {
"name": "6",
"help": "x5 Normal Enhancement Stone - 40,000 Credits"
},
"7": {
"name": "7",
"help": "x3 Advanced Enhancement Stone - 96,000 Credits"
},
"8": {
"name": "8",
"help": "x1 Superior Enhancement Stone - 128,000 Credits"
},
"9": {
"name": "9",
"help": "x5 Lesser Enhancement Stone - 10,000 Credits"
},
"10": {
"name": "10",
"help": "x5 Normal Enhancement Stone - 40,000 Credits"
},
"11": {
"name": "11",
"help": "x3 Advanced Enhancement Stone - 96,000 Credits"
},
"12": {
"name": "12",
"help": "x1 Superior Enhancement Stone - 128,000 Credits"
},
"13": {
"name": "13",
"help": "x10 Lesser Enhancement Stone - 20,000 Credits"
},
"14": {
"name": "14",
"help": "x10 Normal Enhancement Stone - 80,000 Credits"
},
"15": {
"name": "15",
"help": "x6 Advanced Enhancement Stone - 192,000 Credits"
},
"16": {
"name": "16",
"help": "x2 Superior Enhancement Stone - 256,000 Credits"
},
"17": {
"name": "17",
"help": "x1 Random Selection - 8,000 Credits"
},
"18": {
"name": "18",
"help": "x1 Random Selection - 8,000 Credits"
},
"19": {
"name": "19",
"help": "x1 Random Selection - 25,000 Credits"
},
"20": {
"name": "20",
"help": "x1 Random Selection - 25,000 Credits"
}
},
"TacticalChallengeShop": {
"_info": {
"name": "Tactical Challenge Shop Settings",
"help": ""
},
"Enable": {
"name": "Enable",
"help": ""
},
"Purchases": {
"name": "Number of Purchases",
"help": "",
"1": "1",
"2": "2",
"3": "3",
"4": "4"
},
"1": {
"name": "1",
"help": "x5 Shizuko's Eleph - 50 Coins"
},
"2": {
"name": "2",
"help": "x5 Mashiro's Eleph - 50 Coins"
},
"3": {
"name": "3",
"help": "x5 Saya's Eleph - 50 Coins"
},
"4": {
"name": "4",
"help": "x5 Fuuka's Eleph - 50 Coins"
},
"5": {
"name": "5",
"help": "x5 Utaha's Eleph - 50 Coins"
},
"6": {
"name": "6",
"help": "x1 Lesser Drink(30 AP) - 15 Coins"
},
"7": {
"name": "7",
"help": "x1 Normal Drink(60 AP) - 30 Coins"
},
"8": {
"name": "8",
"help": "x10 Novice Activity Report - 5 Coins"
},
"9": {
"name": "9",
"help": "x5 Normal Activity Report - 25 Coins"
},
"10": {
"name": "10",
"help": "x3 Advanced Activity Report - 60 Coins"
},
"11": {
"name": "11",
"help": "x1 Superior Activity Report - 100 Coins"
},
"12": {
"name": "12",
"help": "x5000 Credits - 4 Coins"
},
"13": {
"name": "13",
"help": "x25k Credits - 20 Coins"
},
"14": {
"name": "14",
"help": "x75k Credits - 60 Coins"
},
"15": {
"name": "15",
"help": "x125k Credits - 100 Coins"
}
},
"ItemStorage": { "ItemStorage": {
"_info": { "_info": {
"name": "ItemStorage._info.name", "name": "ItemStorage._info.name",

View File

@ -7,6 +7,14 @@
"Daily": { "Daily": {
"name": "每日", "name": "每日",
"help": "" "help": ""
},
"Farm": {
"name": "扫荡",
"help": ""
},
"Reward": {
"name": "收菜",
"help": ""
} }
}, },
"Task": { "Task": {
@ -18,16 +26,16 @@
"name": "异常处理", "name": "异常处理",
"help": "" "help": ""
}, },
"DataUpdate": {
"name": "仪表盘更新",
"help": ""
},
"Cafe": { "Cafe": {
"name": "咖啡厅", "name": "咖啡厅",
"help": "" "help": ""
}, },
"Circle": { "Shop": {
"name": "公会", "name": "商店",
"help": "社团 / 小组"
},
"Mail": {
"name": "邮箱",
"help": "" "help": ""
}, },
"Bounty": { "Bounty": {
@ -42,8 +50,24 @@
"name": "战术对抗赛", "name": "战术对抗赛",
"help": "战术大赛 / 竞技场" "help": "战术大赛 / 竞技场"
}, },
"DataUpdate": { "Mission": {
"name": "仪表盘更新", "name": "Task.Mission.name",
"help": "Task.Mission.help"
},
"Circle": {
"name": "公会",
"help": "社团 / 小组"
},
"Task": {
"name": "任务",
"help": ""
},
"Mail": {
"name": "邮箱",
"help": ""
},
"Momotalk": {
"name": "Momotalk",
"help": "" "help": ""
} }
}, },
@ -420,6 +444,182 @@
"3": "第三位" "3": "第三位"
} }
}, },
"NormalShop": {
"_info": {
"name": "普通商店",
"help": ""
},
"Enable": {
"name": "启用",
"help": ""
},
"Purchases": {
"name": "购买次数",
"help": "指定物品购买 X 次。例:\n购买 2 次 = 商店默认 1 次 + 刷新购买 1 次",
"1": "1",
"2": "2",
"3": "3",
"4": "4"
},
"1": {
"name": "1",
"help": "x5 初级活动报告书 - 12,500 信用点"
},
"2": {
"name": "2",
"help": "x5 中级活动报告书 - 125,000 信用点"
},
"3": {
"name": "3",
"help": "x3 高级活动报告书 - 300,000 信用点"
},
"4": {
"name": "4",
"help": "x1 特级活动报告书 - 500,000 信用点"
},
"5": {
"name": "5",
"help": "x5 低级强化珠 - 10,000 信用点"
},
"6": {
"name": "6",
"help": "x5 中级强化珠 - 40,000 信用点"
},
"7": {
"name": "7",
"help": "x3 高级强化珠 - 96,000 信用点"
},
"8": {
"name": "8",
"help": "x1 特级强化珠 - 128,000 信用点"
},
"9": {
"name": "9",
"help": "x5 低级强化珠 - 10,000 信用点"
},
"10": {
"name": "10",
"help": "x5 中级强化珠 - 40,000 信用点"
},
"11": {
"name": "11",
"help": "x3 高级强化珠 - 96,000 信用点"
},
"12": {
"name": "12",
"help": "x1 特级强化珠 - 128,000 信用点"
},
"13": {
"name": "13",
"help": "x5 低级强化珠 - 10,000 信用点"
},
"14": {
"name": "14",
"help": "x5 中级强化珠 - 40,000 信用点"
},
"15": {
"name": "15",
"help": "x3 高级强化珠 - 96,000 信用点"
},
"16": {
"name": "16",
"help": "x1 特级强化珠 - 128,000 信用点"
},
"17": {
"name": "17",
"help": "x1 随机材料 - 8,000 信用点"
},
"18": {
"name": "18",
"help": "x1 随机材料 - 8,000 信用点"
},
"19": {
"name": "19",
"help": "x1 随机材料 - 8,000 信用点"
},
"20": {
"name": "20",
"help": "x1 随机材料 - 8,000 信用点"
}
},
"TacticalChallengeShop": {
"_info": {
"name": "战术对抗赛商店",
"help": ""
},
"Enable": {
"name": "启用",
"help": ""
},
"Purchases": {
"name": "购买次数",
"help": "指定物品购买 X 次。例:\n购买 2 次 = 商店默认 1 次 + 刷新购买 1 次",
"1": "1",
"2": "2",
"3": "3",
"4": "4"
},
"1": {
"name": "1",
"help": "x5 静子的神名文字 - 50 战术硬币"
},
"2": {
"name": "2",
"help": "x5 真白的神名文字 - 50 战术硬币"
},
"3": {
"name": "3",
"help": "x5 纱绫的神名文字 - 50 战术硬币"
},
"4": {
"name": "4",
"help": "x5 枫香的神名文字 - 50 战术硬币"
},
"5": {
"name": "5",
"help": "x5 歌原的神名文字 - 50 战术硬币"
},
"6": {
"name": "6",
"help": "x1 低级能量饮料 (30 体力) - 15 战术硬币"
},
"7": {
"name": "7",
"help": "x1 中级能量饮料 (60 体力) - 30 战术硬币"
},
"8": {
"name": "8",
"help": "x5 初级活动报告书 - 5 战术硬币"
},
"9": {
"name": "9",
"help": "x5 中级活动报告书 - 25 战术硬币"
},
"10": {
"name": "10",
"help": "x3 高级活动报告书 - 60 战术硬币"
},
"11": {
"name": "11",
"help": "x1 特级活动报告书 - 100 战术硬币"
},
"12": {
"name": "12",
"help": "x5000 信用点 - 4 战术硬币"
},
"13": {
"name": "13",
"help": "x25k 信用点 - 20 战术硬币"
},
"14": {
"name": "14",
"help": "x75k 信用点 - 60 战术硬币"
},
"15": {
"name": "15",
"help": "x125k 信用点 - 100 战术硬币"
}
},
"ItemStorage": { "ItemStorage": {
"_info": { "_info": {
"name": "ItemStorage._info.name", "name": "ItemStorage._info.name",

View File

@ -1,6 +1,10 @@
from module.config.stored.classes import ( from module.config.stored.classes import (
StoredAP, StoredAP,
StoredBase,
StoredBountyTicket, StoredBountyTicket,
StoredCounter,
StoredExpiredAt0400,
StoredExpiredAtMonday0400,
StoredInt, StoredInt,
StoredScrimmageTicket, StoredScrimmageTicket,
StoredTacticalChallengeTicket, StoredTacticalChallengeTicket,

View File

@ -115,6 +115,17 @@ CRAFTING_CHECK = ButtonWrapper(
button=(103, 4, 226, 41), button=(103, 4, 226, 41),
), ),
) )
EVENT_CHECK = ButtonWrapper(
name='EVENT_CHECK',
jp=None,
en=Button(
file='./assets/en/base/page/EVENT_CHECK.png',
area=(102, 6, 187, 40),
search=(82, 0, 207, 60),
color=(200, 209, 216),
button=(102, 6, 187, 40),
),
)
GACHA_CHECK = ButtonWrapper( GACHA_CHECK = ButtonWrapper(
name='GACHA_CHECK', name='GACHA_CHECK',
jp=Button( jp=Button(
@ -476,6 +487,17 @@ WORK_GO_TO_COMMISSIONS = ButtonWrapper(
button=(656, 494, 803, 517), button=(656, 494, 803, 517),
), ),
) )
WORK_GO_TO_EVENT = ButtonWrapper(
name='WORK_GO_TO_EVENT',
jp=None,
en=Button(
file='./assets/en/base/page/WORK_GO_TO_EVENT.png',
area=(67, 131, 142, 201),
search=(47, 111, 162, 221),
color=(134, 153, 166),
button=(67, 131, 142, 201),
),
)
WORK_GO_TO_MISSION = ButtonWrapper( WORK_GO_TO_MISSION = ButtonWrapper(
name='WORK_GO_TO_MISSION', name='WORK_GO_TO_MISSION',
jp=Button( jp=Button(
@ -538,9 +560,9 @@ WORK_GO_TO_TACTICAL_CHALLENGE = ButtonWrapper(
), ),
en=Button( en=Button(
file='./assets/en/base/page/WORK_GO_TO_TACTICAL_CHALLENGE.png', file='./assets/en/base/page/WORK_GO_TO_TACTICAL_CHALLENGE.png',
area=(1034, 435, 1162, 466), area=(1012, 532, 1152, 591),
search=(1014, 415, 1182, 486), search=(992, 512, 1172, 611),
color=(179, 199, 221), color=(199, 211, 227),
button=(1034, 435, 1162, 466), button=(1012, 532, 1152, 591),
), ),
) )

View File

@ -5,6 +5,7 @@ from module.logger import logger
from tasks.base.page import page_main from tasks.base.page import page_main
from tasks.base.ui import UI from tasks.base.ui import UI
from tasks.login.assets.assets_login import LOGIN_CONFIRM, LOGIN_LOADING, UPDATE, SURVEY from tasks.login.assets.assets_login import LOGIN_CONFIRM, LOGIN_LOADING, UPDATE, SURVEY
from tasks.base.assets.assets_base_page import MAIN_GO_TO_MAIL
class Login(UI): class Login(UI):
@ -93,6 +94,10 @@ class Login(UI):
continue continue
if self.ui_additional(): if self.ui_additional():
continue continue
# press emulator back button when random popup in main
if self.appear(MAIN_GO_TO_MAIL) and not self.match_color(MAIN_GO_TO_MAIL):
self.device.u2.press("back")
continue
return True return True

View File

@ -0,0 +1,170 @@
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),
),
)
QUEST_OFF = ButtonWrapper(
name='QUEST_OFF',
jp=None,
en=Button(
file='./assets/en/mission/QUEST_OFF.png',
area=(859, 69, 1025, 125),
search=(839, 49, 1045, 145),
color=(231, 235, 235),
button=(859, 69, 1025, 125),
),
)
QUEST_ON = ButtonWrapper(
name='QUEST_ON',
jp=None,
en=Button(
file='./assets/en/mission/QUEST_ON.png',
area=(861, 71, 1025, 124),
search=(841, 51, 1045, 144),
color=(49, 68, 76),
button=(861, 71, 1025, 124),
),
)
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),
),
)

258
tasks/mission/mission.py Normal file
View File

@ -0,0 +1,258 @@
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_event()
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)

150
tasks/mission/ui.py Normal file
View File

@ -0,0 +1,150 @@
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, EVENT_CHECK
from tasks.base.page import page_mission, page_commissions #,page_event
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 = Switch("QUEST_switch")
SWITCH_QUEST.add_state("on",QUEST_ON)
SWITCH_QUEST.add_state("off",QUEST_OFF)
"""
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" : (EVENT_CHECK) #page_event
}
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 > 3:
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 select_event(self):
return self.select_mode(SWITCH_QUEST)
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", "E"]:
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

View File

@ -0,0 +1,225 @@
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 ```
BEGIN_STORY = ButtonWrapper(
name='BEGIN_STORY',
jp=None,
en=Button(
file='./assets/en/momotalk/BEGIN_STORY.png',
area=(796, 540, 1059, 591),
search=(776, 520, 1079, 611),
color=(107, 197, 230),
button=(796, 540, 1059, 591),
),
)
CHAT_AREA = ButtonWrapper(
name='CHAT_AREA',
jp=None,
en=Button(
file='./assets/en/momotalk/CHAT_AREA.png',
area=(760, 149, 1149, 628),
search=(740, 129, 1169, 648),
color=(206, 212, 217),
button=(760, 149, 1149, 628),
),
)
CONFIRM_SKIP = ButtonWrapper(
name='CONFIRM_SKIP',
jp=None,
en=Button(
file='./assets/en/momotalk/CONFIRM_SKIP.png',
area=(674, 486, 871, 555),
search=(654, 466, 891, 575),
color=(112, 207, 241),
button=(674, 486, 871, 555),
),
)
CONFIRM_SORT = ButtonWrapper(
name='CONFIRM_SORT',
jp=None,
en=Button(
file='./assets/en/momotalk/CONFIRM_SORT.png',
area=(239, 407, 657, 439),
search=(219, 387, 677, 459),
color=(248, 249, 249),
button=(239, 407, 657, 439),
),
)
FIRST_UNREAD = ButtonWrapper(
name='FIRST_UNREAD',
jp=None,
en=Button(
file='./assets/en/momotalk/FIRST_UNREAD.png',
area=(636, 239, 661, 265),
search=(616, 219, 681, 285),
color=(251, 86, 45),
button=(636, 239, 661, 265),
),
)
MENU = ButtonWrapper(
name='MENU',
jp=None,
en=Button(
file='./assets/en/momotalk/MENU.png',
area=(1156, 15, 1251, 63),
search=(1136, 0, 1271, 83),
color=(215, 219, 222),
button=(1156, 15, 1251, 63),
),
)
NOTIFICATION_BADGE = ButtonWrapper(
name='NOTIFICATION_BADGE',
jp=None,
en=Button(
file='./assets/en/momotalk/NOTIFICATION_BADGE.png',
area=(171, 109, 200, 128),
search=(151, 89, 220, 148),
color=(242, 101, 47),
button=(171, 109, 200, 128),
),
)
REPLY = ButtonWrapper(
name='REPLY',
jp=None,
en=Button(
file='./assets/en/momotalk/REPLY.png',
area=(791, 431, 855, 462),
search=(771, 411, 875, 482),
color=(198, 210, 216),
button=(791, 431, 855, 462),
),
)
SELECT_STUDENT = ButtonWrapper(
name='SELECT_STUDENT',
jp=None,
en=Button(
file='./assets/en/momotalk/SELECT_STUDENT.png',
area=(839, 369, 998, 403),
search=(819, 349, 1018, 423),
color=(241, 242, 242),
button=(839, 369, 998, 403),
),
)
SKIP = ButtonWrapper(
name='SKIP',
jp=None,
en=Button(
file='./assets/en/momotalk/SKIP.png',
area=(1192, 103, 1229, 141),
search=(1172, 83, 1249, 161),
color=(108, 125, 145),
button=(1192, 103, 1229, 141),
),
)
SORT_ASCENDING = ButtonWrapper(
name='SORT_ASCENDING',
jp=None,
en=Button(
file='./assets/en/momotalk/SORT_ASCENDING.png',
area=(631, 166, 648, 188),
search=(611, 146, 668, 208),
color=(221, 228, 233),
button=(631, 166, 648, 188),
),
)
SORT_DESCENDING = ButtonWrapper(
name='SORT_DESCENDING',
jp=None,
en=Button(
file='./assets/en/momotalk/SORT_DESCENDING.png',
area=(631, 166, 648, 189),
search=(611, 146, 668, 209),
color=(221, 228, 234),
button=(631, 166, 648, 189),
),
)
STORY = ButtonWrapper(
name='STORY',
jp=None,
en=Button(
file='./assets/en/momotalk/STORY.png',
area=(790, 529, 979, 557),
search=(770, 509, 999, 577),
color=(220, 208, 214),
button=(790, 529, 979, 557),
),
)
SWITCH_MESSAGE = ButtonWrapper(
name='SWITCH_MESSAGE',
jp=None,
en=Button(
file='./assets/en/momotalk/SWITCH_MESSAGE.png',
area=(147, 271, 194, 297),
search=(127, 251, 214, 317),
color=(187, 191, 201),
button=(147, 271, 194, 297),
),
)
SWITCH_MESSAGE_CHECK = ButtonWrapper(
name='SWITCH_MESSAGE_CHECK',
jp=None,
en=Button(
file='./assets/en/momotalk/SWITCH_MESSAGE_CHECK.png',
area=(228, 165, 391, 194),
search=(208, 145, 411, 214),
color=(211, 215, 217),
button=(228, 165, 391, 194),
),
)
SWITCH_STUDENT = ButtonWrapper(
name='SWITCH_STUDENT',
jp=None,
en=Button(
file='./assets/en/momotalk/SWITCH_STUDENT.png',
area=(148, 167, 194, 217),
search=(128, 147, 214, 237),
color=(157, 166, 179),
button=(148, 167, 194, 217),
),
)
SWITCH_STUDENT_CHECK = ButtonWrapper(
name='SWITCH_STUDENT_CHECK',
jp=None,
en=Button(
file='./assets/en/momotalk/SWITCH_STUDENT_CHECK.png',
area=(229, 166, 320, 192),
search=(209, 146, 340, 212),
color=(199, 203, 205),
button=(229, 166, 320, 192),
),
)
UNREAD = ButtonWrapper(
name='UNREAD',
jp=None,
en=Button(
file='./assets/en/momotalk/UNREAD.png',
area=(454, 160, 568, 193),
search=(434, 140, 588, 213),
color=(241, 242, 243),
button=(454, 160, 568, 193),
),
)
UNREAD_OFF = ButtonWrapper(
name='UNREAD_OFF',
jp=None,
en=Button(
file='./assets/en/momotalk/UNREAD_OFF.png',
area=(456, 273, 658, 316),
search=(436, 253, 678, 336),
color=(252, 252, 251),
button=(456, 273, 658, 316),
),
)
UNREAD_ON = ButtonWrapper(
name='UNREAD_ON',
jp=None,
en=Button(
file='./assets/en/momotalk/UNREAD_ON.png',
area=(456, 272, 658, 314),
search=(436, 252, 678, 334),
color=(245, 120, 144),
button=(456, 272, 658, 314),
),
)

View File

@ -0,0 +1,61 @@
from enum import Enum
from module.base.timer import Timer
from module.logger import logger
from tasks.tactical_challenge.assets.assets_tactical_challenge import *
from tasks.momotalk.ui import MomoTalkUI
class MomoTalkStatus(Enum):
OPEN = 0
SORT = 1
CHECK = 2
CHAT = 3
STORY = 4
FINISHED = -1
class MomoTalk(MomoTalkUI):
def handle_momotalk(self, status):
match status:
case MomoTalkStatus.OPEN:
if self.open_momotalk():
return MomoTalkStatus.SORT
return MomoTalkStatus.FINISHED
case MomoTalkStatus.SORT:
if self.sort_messages():
return MomoTalkStatus.CHECK
case MomoTalkStatus.CHECK:
if self.check_first_student():
return MomoTalkStatus.CHAT
return MomoTalkStatus.FINISHED
case MomoTalkStatus.CHAT:
if self.chat():
return MomoTalkStatus.STORY
return MomoTalkStatus.OPEN
case MomoTalkStatus.STORY:
if self.skip_story():
return MomoTalkStatus.OPEN
case MomoTalkStatus.FINISHED:
return status
case _:
logger.warning(f'Invalid status: {status}')
return status
def run(self):
action_timer = Timer(0.5, 1)
status = MomoTalkStatus.OPEN
while 1:
self.device.screenshot()
if self.ui_additional():
continue
if action_timer.reached_and_reset():
logger.attr('Status', status)
status = self.handle_momotalk(status)
if status == MomoTalkStatus.FINISHED:
break
self.config.task_delay(server_update=True)

190
tasks/momotalk/ui.py Normal file
View File

@ -0,0 +1,190 @@
import cv2
import numpy as np
from module.base.base import ModuleBase
from module.base.timer import Timer
from module.base.utils import point_in_area, area_size
from module.logger import logger
from module.ui.switch import Switch
from tasks.base.page import page_main, page_momo_talk
from tasks.base.ui import UI
from tasks.momotalk.assets.assets_momotalk import *
SWITCH_SIDEBAR = Switch("Sidebar_switch", is_selector=True)
SWITCH_SIDEBAR.add_state("student", SWITCH_STUDENT_CHECK, SWITCH_STUDENT)
SWITCH_SIDEBAR.add_state("message", SWITCH_MESSAGE_CHECK, SWITCH_MESSAGE)
SWITCH_UNREAD = Switch("Unread_switch")
SWITCH_UNREAD.add_state("on", UNREAD_ON)
SWITCH_UNREAD.add_state("off", UNREAD_OFF)
SWITCH_SORT = Switch("Sort_switch")
SWITCH_SORT.add_state("ascending", SORT_ASCENDING)
SWITCH_SORT.add_state("descending", SORT_DESCENDING)
"""Required for template matching as reply and story
button can be found in different locations"""
REPLY_TEMPLATE = REPLY.matched_button.image
STORY_TEMPLATE = STORY.matched_button.image
class MomoTalkUI(UI):
def __init__(self, config, device):
super().__init__(config, device)
self.swipe_vector_range = (0.65, 0.85)
self.list = CHAT_AREA
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.list.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.list.button)
def select_then_check(self, dest_enter: ButtonWrapper, dest_check: ButtonWrapper, similarity=0.85):
timer = Timer(5, 10).start()
while 1:
self.device.screenshot()
self.appear_then_click(dest_enter, interval=1, similarity=similarity)
if self.appear(dest_check, similarity=similarity):
return True
if timer.reached():
return False
def select_then_disappear(self, dest_enter: ButtonWrapper, dest_check: ButtonWrapper, force_select=False):
timer = Timer(5, 10).start()
while 1:
self.device.screenshot()
if force_select or self.appear(dest_enter):
self.click_with_interval(dest_enter, interval=1)
if not self.appear(dest_check):
return True
if timer.reached():
return False
def set_switch(self, switch):
"""
Set switch to on. However, unsure why is inaccurate in momotalk.
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 click_all(self, template, x_add=0, y_add=0):
"""
Find the all the locations of the template adding an offset if specified and click them.
TODO: filter coords that are not inside the chat area as otherwise it will close momotalk.
If after filter, no coords then swipe.
"""
click_coords = self.device.click_methods.get(self.config.Emulator_ControlMethod, self.device.click_adb)
image = self.device.screenshot()
result = cv2.matchTemplate(image, template, cv2.TM_CCOEFF_NORMED)
threshold = 0.8
locations = np.where(result >= threshold)
seen = set()
for pt in zip(*locations[::-1]):
center_pt = (int(pt[0] + template.shape[1] / 2 + x_add), int(pt[1] + template.shape[0] / 2 + y_add))
seen.add(center_pt)
if seen:
seen = filter(lambda x: point_in_area(x, CHAT_AREA.area), seen)
[click_coords(coords[0], coords[1]) for coords in seen]
self.swipe_page("down", self)
return True
return False
def open_momotalk(self):
"""
Go to main and check if there are any red notifications in momotalk.
If yes, open it otherwise it means no student available for interaction.
"""
self.ui_ensure(page_main)
if self.match_color(NOTIFICATION_BADGE, threshold=80):
self.ui_ensure(page_momo_talk)
while SWITCH_SIDEBAR.get(self) != "message":
SWITCH_SIDEBAR.set("message", self)
return True
logger.warning("No students available for interaction")
return False
def sort_messages(self):
"""
Switch from newest to unread and sort the messages in descending order
"""
logger.info("Sorting messages...")
steps = [UNREAD, CONFIRM_SORT, UNREAD_OFF, UNREAD_ON]
for i in range(len(steps)-2):
self.select_then_check(steps[i], steps[i+1], similarity=0.95)
return not self.appear(CONFIRM_SORT) and self.appear(UNREAD) and self.appear(SORT_ON)
def check_first_student(self):
"""
If the first student has a red notification return True and start chat.
Otherwise it means no students are available for interaction.
"""
if self.match_color(FIRST_UNREAD, threshold=80) and self.select_then_disappear(FIRST_UNREAD, SELECT_STUDENT, force_select=True):
return True
logger.warning("No students available for interaction")
return False
def chat(self):
"""
Waits for the chat area to be stable and then
check if a reply or story button is found and click them.
If the begin story button is found skip story.
"""
logger.info("Chatting with student...")
stability_counter = 0
while 1:
self.wait_until_stable(CHAT_AREA, timer=Timer(10, 10))
if self.appear(BEGIN_STORY):
logger.info("Begin Story detected")
return True
if self.click_all(REPLY_TEMPLATE, y_add=62):
logger.info("Clicked on reply")
stability_counter = 0
continue
if self.click_all(STORY_TEMPLATE, y_add=62):
logger.info("Clicked on story")
stability_counter = 0
continue
logger.info("No new message detected")
stability_counter += 1
if stability_counter > 3:
return False
def skip_story(self):
"""
Skip story by executing a series of steps. Returns True if the confirm skip
button is clicked and disappears
"""
logger.info("Attempting to skip story...")
steps = [BEGIN_STORY, MENU, SKIP]
for step in steps:
self.appear_then_click(step)
if self.appear_then_click(CONFIRM_SKIP) and not self.appear(CONFIRM_SKIP, interval=5):
logger.info("Skipped story successfully")
return True
return False

View File

@ -0,0 +1,115 @@
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 ```
CONFIRM_PURCHASE = ButtonWrapper(
name='CONFIRM_PURCHASE',
jp=None,
en=Button(
file='./assets/en/shop/CONFIRM_PURCHASE.png',
area=(467, 231, 807, 309),
search=(447, 211, 827, 329),
color=(217, 218, 219),
button=(668, 458, 865, 514),
),
)
CONFIRM_REFRESH = ButtonWrapper(
name='CONFIRM_REFRESH',
jp=None,
en=Button(
file='./assets/en/shop/CONFIRM_REFRESH.png',
area=(474, 271, 806, 306),
search=(454, 251, 826, 326),
color=(202, 203, 204),
button=(675, 434, 863, 500),
),
)
ITEM_LIST = ButtonWrapper(
name='ITEM_LIST',
jp=None,
en=Button(
file='./assets/en/shop/ITEM_LIST.png',
area=(625, 127, 1244, 610),
search=(605, 107, 1264, 630),
color=(193, 206, 213),
button=(625, 127, 1244, 610),
),
)
NORMAL_OFF = ButtonWrapper(
name='NORMAL_OFF',
jp=None,
en=Button(
file='./assets/en/shop/NORMAL_OFF.png',
area=(4, 111, 213, 167),
search=(0, 91, 233, 187),
color=(248, 248, 245),
button=(4, 111, 213, 167),
),
)
NORMAL_ON = ButtonWrapper(
name='NORMAL_ON',
jp=None,
en=Button(
file='./assets/en/shop/NORMAL_ON.png',
area=(4, 109, 212, 170),
search=(0, 89, 232, 190),
color=(57, 78, 96),
button=(4, 109, 212, 170),
),
)
OCR_REFRESH = ButtonWrapper(
name='OCR_REFRESH',
jp=None,
en=Button(
file='./assets/en/shop/OCR_REFRESH.png',
area=(712, 302, 762, 344),
search=(692, 282, 782, 364),
color=(225, 225, 226),
button=(712, 302, 762, 344),
),
)
PURCHASE = ButtonWrapper(
name='PURCHASE',
jp=None,
en=Button(
file='./assets/en/shop/PURCHASE.png',
area=(1102, 640, 1227, 684),
search=(1082, 620, 1247, 704),
color=(226, 206, 65),
button=(1102, 640, 1227, 684),
),
)
REFRESH = ButtonWrapper(
name='REFRESH',
jp=None,
en=Button(
file='./assets/en/shop/REFRESH.png',
area=(1098, 643, 1223, 682),
search=(1078, 623, 1243, 702),
color=(231, 234, 237),
button=(1098, 643, 1223, 682),
),
)
TC_OFF = ButtonWrapper(
name='TC_OFF',
jp=None,
en=Button(
file='./assets/en/shop/TC_OFF.png',
area=(2, 503, 209, 558),
search=(0, 483, 229, 578),
color=(239, 242, 244),
button=(2, 503, 209, 558),
),
)
TC_ON = ButtonWrapper(
name='TC_ON',
jp=None,
en=Button(
file='./assets/en/shop/TC_ON.png',
area=(3, 493, 208, 548),
search=(0, 473, 228, 568),
color=(62, 84, 99),
button=(3, 493, 208, 548),
),
)

Some files were not shown because too many files have changed in this diff Show More