from os import getenv, path import platform from tkinter import CENTER, END, E, LEFT, NSEW, Image, IntVar, N, S, PhotoImage, StringVar, Toplevel, W, filedialog, messagebox, ttk from urllib.parse import quote, urlencode import requests import sv_ttk from classes.custom.image_label import ImageLabel from classes.custom.themed_frame import ThemedFrame from classes.custom.themed_toplevel import ThemedToplevel from classes.enums import Theme from modules.logger import logger from modules.theme_titlebar import theme_title_bar from modules.utils import configGet, configSet, resize_window, set_icon, use_dark_mode class ToplevelWelcome(ThemedToplevel): def __init__(self, parent): super().__init__(parent) self.protocol("WM_DELETE_WINDOW", self.__exit) resize_window(self, 380, 350) self.title("Welcome to Stardew Sync") self.resizable(False, False) sv_ttk.init_theme(self) if use_dark_mode(): theme_title_bar(self, mode=Theme.DARK) self.update() set_icon(self) self.focus_set() self.grid_columnconfigure(0, weight=1) self.window_frame = ThemedFrame(self) self.window_frame.grid(column=0, row=0, sticky=NSEW) # self.window_frame["borderwidth"] = 1 # self.window_frame["relief"] = "solid" self.window_frame.grid_columnconfigure(0, weight=1) self.welcome_pic = ImageLabel(self.window_frame) self.welcome_pic.grid(column=0, row=0, pady=20) self.welcome_pic.load(path.join("assets", "welcome.gif")) self.welcome_text = ttk.Label(self.window_frame, text="Welcome to Stardew Sync", font=("SunValleyBodyFont", 14)) self.welcome_text.grid(column=0, row=1, pady=10) self.welcome_subtext = ttk.Label(self.window_frame, text="This small open-source application will help you\nto synchronize your Stardew Valley save files\nbetween all your devices", justify=CENTER) self.welcome_subtext.grid(column=0, row=2) self.welcome_button = ttk.Button(self.window_frame, text="Begin", style="Accent.TButton", width=10, command=self.stage_address) self.welcome_button.grid(column=0, row=3, pady=20) def stage_address(self): for widget in self.window_frame.winfo_children(): widget.destroy() self.window_frame.grid_rowconfigure(0, weight=2) self.window_frame.grid_rowconfigure(1, weight=2) #self.window_frame.grid_rowconfigure(2, weight=3) self.stage_label = ttk.Label(self.window_frame, text="Connection", font=("SunValleyBodyFont", 14)) self.stage_label.grid(column=0, row=0, pady=29) self.stage_label_1 = ttk.Label(self.window_frame, text="Server address", justify=LEFT) self.stage_label_1.grid(column=0, row=1, padx=35, pady=5, sticky=W) self.stage_entry_1 = ttk.Entry(self.window_frame) self.stage_entry_1.grid(column=0, row=2, padx=35, pady=5, sticky=W+E) self.stage_label_2 = ttk.Label(self.window_frame, text="Personal API key", justify=LEFT) self.stage_label_2.grid(column=0, row=3, padx=35, pady=5, sticky=W) self.stage_entry_2 = ttk.Entry(self.window_frame) self.stage_entry_2.grid(column=0, row=4, padx=35, pady=5, sticky=W+E) self.stage_checkbox_var = IntVar() self.stage_checkbox = ttk.Checkbutton(self.window_frame, text="Allow self-signed certificates", variable=self.stage_checkbox_var) self.stage_checkbox.grid(column=0, row=5, sticky=W, padx=35, pady=9) self.stage_button = ttk.Button(self.window_frame, text="Continue", style="Accent.TButton", width=10, command=self.stage_address_validate) self.stage_button.grid(column=0, row=6, pady=28) def stage_address_validate(self): if len(self.stage_entry_1.get()) > 0: if self.stage_entry_1.get().endswith("/"): self.address_text = self.stage_entry_1.get() self.stage_entry_1.delete(0, END) self.stage_entry_1.insert(0, self.address_text[:-1]) try: response_check = requests.get(self.stage_entry_1.get()+"/check", verify=not bool(self.stage_checkbox_var.get())) if response_check.status_code != 200: logger.error(f"Could not connect to '{self.stage_entry_1.get()}' because it returned {response_check.status_code}") messagebox.showerror(title="Connection error", message=f"Server response is {response_check.status_code}. Check if your API server and it's reverse proxy (if there's one) properly configured and then try again.") return except (requests.exceptions.InvalidURL, requests.exceptions.InvalidSchema, requests.exceptions.MissingSchema) as exp: logger.error(f"Could not validate '{self.stage_entry_1.get()}' due to {exp}") messagebox.showerror(title="Invalid address", message="Address entered does not seem to be correct one. Please provide API address starting with http:// or https:// \n\nFor example:\n- https://your-api.com:8043\n- https://your-api.com") return except (requests.exceptions.SSLError): logger.error(f"SSL certificate of '{self.stage_entry_1.get()}' does not seem to be valid or is self-signed") messagebox.showerror(title="Invalid SSL", message="SSL certificate seems to be invalid or self-signed and thus won't be trusted. You can overwrite this by checking 'Allow self-signed certificates'.") return except Exception as exp: logger.error(f"Could not reach '{self.stage_entry_1.get()}' due to {exp}") messagebox.showerror(title="Connection error", message="Address entered does not seem to be reachable. Please make sure you've entered the correct API address.") return response_apikey = requests.get(self.stage_entry_1.get()+"/apikey", headers={"apikey": self.stage_entry_2.get()}, verify=not bool(self.stage_checkbox_var.get())) if response_apikey.status_code != 200: logger.error(f"API key seems to be invalid. API returned {response_apikey.status_code} as a status code") messagebox.showerror(title="Invalid apikey", message="API key provided does not seem to be valid. Please check for any mistakes and if none found - take a look at the docs to learn how to generate one.") return configSet(["address"], self.stage_entry_1.get()) configSet(["apikey"], self.stage_entry_2.get()) configSet(["allow_self_signed"], bool(self.stage_checkbox_var.get())) self.stage_name() def stage_name(self): for widget in self.window_frame.winfo_children(): widget.destroy() self.window_frame.grid_rowconfigure(0, weight=2) self.window_frame.grid_rowconfigure(1, weight=2) self.stage_label = ttk.Label(self.window_frame, text="Connection", font=("SunValleyBodyFont", 14)) self.stage_label.grid(column=0, row=0, pady=29) self.divider_1 = ttk.Separator(self.window_frame) self.divider_1.grid(column=0, row=1, pady=15) self.stage_label_1 = ttk.Label(self.window_frame, text="Device name", justify=LEFT) self.stage_label_1.grid(column=0, row=2, padx=35, pady=5, sticky=W) self.stage_entry_1 = ttk.Entry(self.window_frame) self.stage_entry_1.grid(column=0, row=3, padx=35, pady=5, sticky=W+E) self.stage_entry_1.insert(0, str(platform.node())) self.divider_2 = ttk.Separator(self.window_frame) self.divider_2.grid(column=0, row=4, pady=40) self.stage_button = ttk.Button(self.window_frame, text="Continue", style="Accent.TButton", width=10, command=self.stage_name_validate) self.stage_button.grid(column=0, row=5, pady=28) def stage_name_validate(self): if self.stage_entry_1.get().strip() == "": logger.error(f"Name {self.stage_entry_1.get().strip()} is not a valid name") messagebox.showerror(title="Name error", message="Provided device name is not valid. Please provide a valid one.") return try: quote(self.stage_entry_1.get().strip()) except: logger.error(f"Name {self.stage_entry_1.get().strip()} is not a valid name") messagebox.showerror(title="Name error", message="Provided device name is not valid. Please provide a valid one.") return if self.stage_entry_1.get().strip() == configGet("name"): existing_device = requests.get(f'{configGet("address")}', headers={"apikey": configGet("apikey")}, verify=not configGet("allow_self_signed")) if existing_device.status_code != 200: requests.post(f'{configGet("address")}/devices?{urlencode({"name": quote(configGet("name").encode("utf-8")), "os": platform.system()+" "+platform.release(), "client": f"SyncTk {self.master.__version__}"})}', headers={"apikey": configGet("apikey")}, verify=not configGet("allow_self_signed")) else: if configGet("name") is not None: existing_device_before = requests.get(f'{configGet("address")}', headers={"apikey": configGet("apikey")}, verify=not configGet("allow_self_signed")) if existing_device_before.status_code == 200: requests.patch(f'{configGet("address")}?{urlencode({"new_name": quote(self.stage_entry_1.get().strip().encode("utf-8")), "os": platform.system()+" "+platform.release(), "client": f"SyncTk {self.master.__version__}"})}', headers={"apikey": configGet("apikey")}, verify=not configGet("allow_self_signed")) else: device_created = requests.post(f'{configGet("address")}/devices?{urlencode({"name": quote(self.stage_entry_1.get().strip().encode("utf-8")), "os": platform.system()+" "+platform.release(), "client": f"SyncTk {self.master.__version__}"})}', headers={"apikey": configGet("apikey")}, verify=not configGet("allow_self_signed")) if device_created.status_code != 204: messagebox.showerror(title="Name error", message=f"Could not register device in database using name '{self.stage_entry_1.get().strip()}' with error:\n\n{device_created.json()}") return configSet(["name"], self.stage_entry_1.get().strip()) self.stage_location() def stage_location(self): for widget in self.window_frame.winfo_children(): widget.destroy() self.window_frame.grid_rowconfigure(0, weight=2) self.window_frame.grid_rowconfigure(1, weight=2) self.stage_label = ttk.Label(self.window_frame, text="Local saves", font=("SunValleyBodyFont", 14)) self.stage_label.grid(column=0, row=0, pady=29) self.divider_1 = ttk.Separator(self.window_frame) self.divider_1.grid(column=0, row=1, pady=15) self.stage_label_1 = ttk.Label(self.window_frame, text="Save files location", justify=LEFT) self.stage_label_1.grid(column=0, row=2, padx=35, pady=5, sticky=W) self.stage_frame = ThemedFrame(self.window_frame) self.stage_frame.grid(column=0, row=3, padx=35, pady=5, sticky=W+E) self.stage_frame.grid_columnconfigure(0, weight=2) # self.saves_frame.grid_columnconfigure(1, weight=3) self.stage_entry_1 = ttk.Entry(self.stage_frame) self.stage_entry_1.grid(column=0, row=0, sticky=W+E) self.divider_2 = ttk.Separator(self.stage_frame, orient="vertical") self.divider_2.grid(column=1, row=0, padx=3) self.saves_location_button = ttk.Button(self.stage_frame, text="Browse", width=6, command=lambda:self.stage_location_select_location(self.stage_entry_1)) self.saves_location_button.grid(column=2, row=0, sticky=E) self.divider_3 = ttk.Separator(self.window_frame) self.divider_3.grid(column=0, row=4, pady=40) self.stage_button = ttk.Button(self.window_frame, text="Continue", style="Accent.TButton", width=10, command=self.stage_location_validate) self.stage_button.grid(column=0, row=5, pady=26) def stage_location_select_location(self, entry: ttk.Entry): self.path_start = None for guess in [ [str(getenv("APPDATA")), "Stardew Valley"], [str(getenv("APPDATA")), "StardewValley"], [path.expanduser("~"), ".config", "Stardew Valley"], [path.expanduser("~"), ".config", "StardewValley"] ]: joined_path = path.join(*guess) if path.exists(joined_path): self.path_start = joined_path break self.path_dir = filedialog.askdirectory(initialdir=self.path_start, title="Select Stardew Valley Saves folder") if self.path_dir != "": entry.delete(0, END) entry.insert(0, self.path_dir) def stage_location_validate(self): if not path.exists(self.stage_entry_1.get()): logger.error(f"Path {self.stage_entry_1.get()} does not seem to exist") messagebox.showerror(title="Location error", message="Saves folder seems to be invalid. Please provide a valid directory path where Stardew Valley's save files (and folders) are stored.") return configSet(["saves_location"], self.stage_entry_1.get()) self.stage_theme() def stage_theme(self): for widget in self.window_frame.winfo_children(): widget.destroy() self.window_frame.grid_rowconfigure(0, weight=2) self.window_frame.grid_rowconfigure(1, weight=2) self.stage_label = ttk.Label(self.window_frame, text="Almost there", font=("SunValleyBodyFont", 14)) self.stage_label.grid(column=0, row=0, pady=29) self.divider_1 = ttk.Separator(self.window_frame) self.divider_1.grid(column=0, row=1, pady=15) self.stage_label_1 = ttk.Label(self.window_frame, text="Theme", justify=CENTER) self.stage_label_1.grid(column=0, row=2, padx=35, pady=5) self.stage_option_menu_var = StringVar() self.themes = ("Auto ", "Light ", "Dark ") self.stage_option_menu = ttk.OptionMenu(self.window_frame, self.stage_option_menu_var, "Auto ", *self.themes, direction="below", command=self.change_theme) self.stage_option_menu.grid(column=0, row=3, padx=35, pady=5) self.divider_2 = ttk.Separator(self.window_frame) self.divider_2.grid(column=0, row=4, pady=40) self.stage_button = ttk.Button(self.window_frame, text="Continue", style="Accent.TButton", width=10, command=self.stage_theme_validate) self.stage_button.grid(column=0, row=5, pady=28) def change_theme(self, *args): if self.stage_option_menu_var.get().strip().lower() == Theme.AUTO.value: self.stage_option_menu_var_real = Theme.DARK if use_dark_mode(no_config=True) is True else Theme.LIGHT else: if self.stage_option_menu_var.get().strip().lower() == Theme.LIGHT.value: self.stage_option_menu_var_real = Theme.LIGHT else: self.stage_option_menu_var_real = Theme.DARK theme_title_bar(self, self.stage_option_menu_var_real) theme_title_bar(self.master, self.stage_option_menu_var_real) def stage_theme_validate(self): if self.stage_option_menu_var.get().strip().lower() == Theme.AUTO.value: configSet(["dark_mode_auto"], True) else: configSet(["dark_mode_auto"], False) if self.stage_option_menu_var.get().strip().lower() == Theme.DARK.value: configSet(["dark_mode"], True) else: configSet(["dark_mode"], False) self.stage_mobile_app() def stage_mobile_app(self): # To Do self.stage_completed() def stage_completed(self): for widget in self.window_frame.winfo_children(): widget.destroy() self.window_frame.grid_columnconfigure(0, weight=1) self.welcome_pic = ImageLabel(self.window_frame) self.welcome_pic.grid(column=0, row=0, pady=20) self.welcome_pic.load(path.join("assets", "welcome.gif")) self.welcome_text = ttk.Label(self.window_frame, text="You're all set!", font=("SunValleyBodyFont", 14)) self.welcome_text.grid(column=0, row=1, pady=10) self.welcome_subtext = ttk.Label(self.window_frame, text="Configuration step is completed.\nYou can now jump into app and enjoy your\nsave files synchronization", justify=CENTER) self.welcome_subtext.grid(column=0, row=2) self.welcome_button = ttk.Button(self.window_frame, text="Finish", style="Accent.TButton", width=10, command=self.acknowledged) self.welcome_button.grid(column=0, row=3, pady=20) def acknowledged(self): configSet(["first_run"], False) self.destroy() def __exit(self): decision = messagebox.askyesno(title="Exit confirmation", message="Are you sure? If you exit now, your app will remain unconfigured.") if decision is True: self.quit()