SyncTk/classes/frames/settings.py
2023-01-23 00:13:30 +01:00

236 lines
13 KiB
Python

from os import getenv, path
import platform
from tkinter import N, NSEW, S, W, E, END, IntVar, StringVar, filedialog, messagebox, ttk
from ttkthemes import ThemedTk
from classes.custom.themed_frame import ThemedFrame
from modules.theme_titlebar import theme_title_bar
from modules.utils import configGet, configSet, get_string_mode, use_dark_mode
from modules.logger import logger
from urllib.parse import urlencode, quote
import requests
import sv_ttk
class FrameSettings(ThemedFrame):
def __init__(self, master: ThemedTk, **kwargs) -> None:
super().__init__(master, **kwargs)
master.title("Settings - Stardew Sync")
self.columnconfigure(0, weight=1)
self.columnconfigure(1, weight=3)
master.columnconfigure(1, weight=1)
# Name
self.name_label = ttk.Label(self, text="Name:")
self.name_label.grid(column=0, row=0, sticky=W, padx=9, pady=9)
self.name_entry = ttk.Entry(self)
self.name_entry.grid(column=1, row=0, sticky=N+S+E+W, padx=9, pady=9)
if configGet("name") is not None:
self.name_entry.insert(0, configGet("name"))
else:
self.name_entry.insert(0, str(platform.node()))
# ====
# Address
self.address_label = ttk.Label(self, text="Address:")
self.address_label.grid(column=0, row=1, sticky=W, padx=9, pady=9)
self.address_entry = ttk.Entry(self)
self.address_entry.grid(column=1, row=1, sticky=N+S+E+W, padx=9, pady=9)
if configGet("address") is not None:
self.address_entry.insert(0, configGet("address"))
# =======
# API Key
self.apikey_label = ttk.Label(self, text="API key:")
self.apikey_label.grid(column=0, row=2, sticky=W, padx=9, pady=9)
self.apikey_entry = ttk.Entry(self)
self.apikey_entry.grid(column=1, row=2, sticky=N+S+E+W, padx=9, pady=9)
if configGet("apikey") is not None:
self.apikey_entry.insert(0, configGet("apikey"))
# =======
# Self-signed
if configGet("allow_self_signed") is not None:
self.self_signed_check_bool = IntVar(value=int(configGet("allow_self_signed")))
else:
self.self_signed_check_bool = IntVar()
self.self_signed_check = ttk.Checkbutton(self, text="Allow self-signed certificates", variable=self.self_signed_check_bool)
self.self_signed_check.grid(column=1, row=3, sticky=W, padx=9, pady=9)
# ===========
# Saves location
self.saves_location_label = ttk.Label(self, text="Saves location:")
self.saves_location_label.grid(column=0, row=4, sticky=W, padx=9, pady=9)
self.saves_frame = ThemedFrame(self)
self.saves_frame.grid(column=1, row=4, sticky=N+S+E+W, padx=9, pady=9)
self.saves_frame.grid_columnconfigure(0, weight=1)
self.saves_frame.grid_columnconfigure(1, weight=3)
self.saves_location_entry = ttk.Entry(self.saves_frame, width=30)
self.saves_location_entry.grid(column=0, row=0, sticky=NSEW)
if configGet("saves_location") is not None:
self.saves_location_entry.insert(0, configGet("saves_location"))
self.saves_location_button = ttk.Button(self.saves_frame, text="Browse", width=6, command=lambda:self.select_location(self.saves_location_entry))
self.saves_location_button.grid(column=1, row=0, sticky=E)
# ==============
# Saves location
self.saves_location_label = ttk.Label(self, text="Color theme:")
self.saves_location_label.grid(column=0, row=5, sticky=W, padx=9, pady=9)
if configGet("dark_mode_auto") is True:
self.default_theme = "Auto "
else:
self.default_theme = "Dark " if configGet("dark_mode") is True else "Light "
self.chosen_theme = StringVar()
self.themes = ("Auto ", "Light ", "Dark ")
self.saves_location_button = ttk.OptionMenu(self, self.chosen_theme, self.default_theme, *self.themes, direction="below", command=self.change_theme)
self.saves_location_button.grid(column=1, row=5, sticky=W, padx=9, pady=9)
# ==============
self.buttons_frame = ThemedFrame(self)
self.buttons_frame.grid(column=0, columnspan=2, row=6, sticky=NSEW, padx=9, pady=9)
self.buttons_frame.grid_columnconfigure(0, weight=1)
self.validate_button = ttk.Button(self.buttons_frame, text="Validate", width=11, command=self.validate_configuration)
self.validate_button.grid(column=0, row=0, sticky=E, padx=9)
self.save_button = ttk.Button(self.buttons_frame, text="Save", style="Accent.TButton", width=11, state="disabled", command=self.save_configuration)
self.save_button.grid(column=1, row=0, sticky=E)
def change_theme(self, *args):
if self.chosen_theme.get().strip().lower() == "auto":
self.chosen_theme_real = "dark" if use_dark_mode(no_config=True) is True else "light"
else:
self.chosen_theme_real = self.chosen_theme.get().strip().lower()
theme_title_bar(self.master, self.chosen_theme_real)
def 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 save_configuration(self):
if len(self.address_entry.get()) > 0:
if self.address_entry.get().endswith("/"):
self.address_text = self.address_entry.get()[:-1]
else:
self.address_text = self.address_entry.get()
else:
self.address_text = None
# =========================
if self.name_entry.get().strip() == configGet("name"):
existing_device = requests.get(f'{self.address_entry.get()}/devices/{quote(configGet("name").encode("utf-8"))}', headers={"apikey": self.apikey_entry.get()}, verify=not bool(self.self_signed_check_bool.get()))
if existing_device.status_code != 200:
requests.post(f'{self.address_entry.get()}/devices?{urlencode({"name": quote(configGet("name").encode("utf-8")), "os": platform.system()+" "+platform.release(), "client": f"SyncTk {self.master.__version__}"})}', headers={"apikey": self.apikey_entry.get()}, verify=not bool(self.self_signed_check_bool.get()))
else:
if configGet("name") is not None:
existing_device_before = requests.get(f'{self.address_entry.get()}/devices/{quote(configGet("name").encode("utf-8"))}', headers={"apikey": self.apikey_entry.get()}, verify=not bool(self.self_signed_check_bool.get()))
if existing_device_before.status_code == 200:
requests.patch(f'{self.address_entry.get()}/devices/{quote(configGet("name").encode("utf-8"))}?{urlencode({"new_name": quote(self.name_entry.get().strip().encode("utf-8")), "os": platform.system()+" "+platform.release(), "client": f"SyncTk {self.master.__version__}"})}', headers={"apikey": self.apikey_entry.get()}, verify=not bool(self.self_signed_check_bool.get()))
else:
device_created = requests.post(f'{self.address_entry.get()}/devices?{urlencode({"name": quote(self.name_entry.get().strip().encode("utf-8")), "os": platform.system()+" "+platform.release(), "client": f"SyncTk {self.master.__version__}"})}', headers={"apikey": self.apikey_entry.get()}, verify=not bool(self.self_signed_check_bool.get()))
if device_created.status_code != 204:
messagebox.showerror(title="Name error", message=f"Could not register device in database using name '{self.name_entry.get().strip()}' with error:\n\n{device_created.json()}")
return
# =========================
configSet(["name"], self.name_entry.get().strip())
configSet(["address"], self.address_entry.get())
configSet(["apikey"], self.apikey_entry.get())
configSet(["allow_self_signed"], bool(self.self_signed_check_bool.get()))
configSet(["saves_location"], self.saves_location_entry.get())
if self.chosen_theme.get().strip().lower() == "auto":
configSet(["dark_mode_auto"], True)
else:
configSet(["dark_mode_auto"], False)
if self.chosen_theme.get().strip().lower() == "dark":
configSet(["dark_mode"], True)
else:
configSet(["dark_mode"], False)
# messagebox.showinfo(title="Configuration saved", message="Your client's configuration has been saved")
self.save_button.state(["disabled"])
self.master.item_saves.state(["!disabled"])
self.master.item_devices.state(["!disabled"])
def validate_configuration(self):
if self.name_entry.get().strip() == "":
logger.error(f"Name {self.name_entry.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 len(self.address_entry.get()) > 0:
if self.address_entry.get().endswith("/"):
self.address_text = self.address_entry.get()
self.address_entry.delete(0, END)
self.address_entry.insert(0, self.address_text[:-1])
try:
requests.get(self.address_entry.get()+"/check", verify=not bool(self.self_signed_check_bool.get()))
except (requests.exceptions.InvalidURL, requests.exceptions.InvalidSchema, requests.exceptions.MissingSchema) as exp:
logger.error(f"Could not validate '{self.address_entry.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.address_entry.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.address_entry.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.address_entry.get()+"/apikey", headers={"apikey": self.apikey_entry.get()}, verify=not bool(self.self_signed_check_bool.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
if not path.exists(self.saves_location_entry.get()):
logger.error(f"Path {self.saves_location_entry.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
# =========================
if self.name_entry.get().strip() != configGet("name"):
existing_device_after = requests.get(f'{self.address_entry.get()}/devices/{self.name_entry.get().strip()}', headers={"apikey": self.apikey_entry.get()}, verify=not bool(self.self_signed_check_bool.get()))
if existing_device_after == 200:
logger.error(f"Device with name {self.name_entry.get().strip()} already exists")
messagebox.showerror(title="Name error", message=f"Device with name '{self.name_entry.get().strip()}' already exists on the server. Please choose another name or rename that device first.")
return
# =========================
self.save_button.state(["!disabled"])
# messagebox.showinfo(title="Configuration completed", message="Your client is now configured and ready to use!")