339 lines
16 KiB
Python
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() |