SyncTk/classes/toplevel/welcome.py

339 lines
16 KiB
Python

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 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="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:
requests.get(self.stage_entry_1.get()+"/check", verify=not bool(self.stage_checkbox_var.get()))
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):
if path.exists(path.join(str(getenv("APPDATA")), "Stardew Valley")):
self.path_start = path.join(str(getenv("APPDATA")), "Stardew Valley")
elif path.exists(path.join(path.expanduser("~"), ".config", "Stardew Valley")):
self.path_start = path.join(path.expanduser("~"), ".config", "Stardew Valley")
else:
self.path_start = None
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() == "auto":
self.stage_option_menu_var_real = "dark" if use_dark_mode(no_config=True) is True else "light"
else:
self.stage_option_menu_var_real = self.stage_option_menu_var.get().strip().lower()
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() == "auto":
configSet(["dark_mode_auto"], True)
else:
configSet(["dark_mode_auto"], False)
if self.stage_option_menu_var.get().strip().lower() == "dark":
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()