Still WIP

This commit is contained in:
Profitroll 2023-01-23 00:13:30 +01:00
parent 021dbf560f
commit cab36a0fdb
6 changed files with 343 additions and 33 deletions

View File

@ -1,15 +1,16 @@
from os import path
from tkinter import NW, E, N, S, W, messagebox, ttk
from tkinter import NSEW, NW, E, N, S, W, messagebox, ttk
import requests
import sv_ttk
from ttkthemes import ThemedTk
from classes.custom.themed_frame import ThemedFrame
from classes.frames.devices import FrameDevices
from classes.custom.scrollable_frame import FIT_HEIGHT, FIT_WIDTH
from classes.custom.themed_frame import ThemedFrame
from classes.frames.devices import FrameDevices, FrameDevicesEmpty
from classes.frames.errors import FrameErrorConnection, FrameErrorSavesFolder, FrameErrorUnconfigured
from classes.frames.saves import FrameSaves
from classes.frames.settings import FrameSettings
from classes.frames.errors import FrameErrorConnection, FrameErrorSavesFolder, FrameErrorUnconfigured
from classes.toplevel.welcome import ToplevelWelcome
from modules.theme_titlebar import theme_title_bar
from modules.utils import configGet, get_string_mode, use_dark_mode
@ -21,7 +22,9 @@ class App(ThemedTk):
super().__init__()
self.window_width = 650
self.__version__ = "0.1.0"
self.window_width = 600
self.window_height = 400
self.screen_width = self.winfo_screenwidth()
@ -66,16 +69,30 @@ class App(ThemedTk):
def frame_saves(self):
self.grid_rowconfigure(0, weight=1)
self.grid_columnconfigure(1, weight=1)
self.frame_saves_object = FrameSaves(self)
self.frame_saves_object.grid(column=1, row=0, sticky=N+S+W+E)
self.frame_saves_object.grid(column=1, row=0, sticky=NSEW)
return
def frame_devices(self):
self.frame_devices_object = FrameDevices(self)
self.frame_devices_object.grid(column=1, row=0, sticky=N+S+W+E)
self.grid_rowconfigure(0, weight=1)
self.grid_columnconfigure(1, weight=1)
self.frame_devices_devices = requests.get(f'{configGet("address")}/devices', headers={"apikey": configGet("apikey")}, verify=not configGet("allow_self_signed"))
if self.frame_devices_devices.status_code == 200 and isinstance(self.frame_devices_devices.json(), list) is True and len(self.frame_devices_devices.json()) > 0:
self.frame_devices_object = FrameDevices(self, self.frame_devices_devices.json(), vscroll=True)
# self.frame_devices_object.resize(FIT_HEIGHT)
else:
self.frame_devices_object = FrameDevicesEmpty(self)
#self.frame_devices_object = FrameDevices(self, hscroll=False, vscroll=True)
#self.frame_devices_object.resize(FIT_WIDTH)
#self.frame_devices_object.resize(FIT_HEIGHT)
self.frame_devices_object.grid(column=1, row=0, sticky=NSEW)
return
def frame_settings(self):
self.grid_rowconfigure(0, weight=1)
self.grid_columnconfigure(1, weight=1)
self.frame_settings_object = FrameSettings(self)
self.frame_settings_object.grid(column=1, row=0, sticky=N+S+W+E)
return
@ -113,6 +130,8 @@ class App(ThemedTk):
if configGet("address") in ["", None] or configGet("apikey") in ["", None]:
self.item_saves.state(["disabled"])
self.item_devices.state(["disabled"])
self.grid_rowconfigure(0, weight=1)
self.grid_columnconfigure(1, weight=1)
self.connection_error = FrameErrorUnconfigured(self)
self.connection_error.grid(column=1, row=0, rowspan=2, sticky=N+S+W+E)
# messagebox.showerror(title="Configuration error", message="Your client is not properly configured.")
@ -121,6 +140,8 @@ class App(ThemedTk):
if self.verify_authorization() is False:
self.item_saves.state(["disabled"])
self.item_devices.state(["disabled"])
self.grid_rowconfigure(0, weight=1)
self.grid_columnconfigure(1, weight=1)
self.connection_error = FrameErrorConnection(self)
self.connection_error.grid(column=1, row=0, rowspan=2, sticky=N+S+W+E)
messagebox.showerror(title="Authentication error", message="Your API key seems to be invalid.")
@ -128,6 +149,8 @@ class App(ThemedTk):
if self.verify_saves_dir() is False:
self.item_saves.state(["disabled"])
self.grid_rowconfigure(0, weight=1)
self.grid_columnconfigure(1, weight=1)
self.connection_error = FrameErrorSavesFolder(self)
self.connection_error.grid(column=1, row=0, rowspan=2, sticky=N+S+W+E)
messagebox.showerror(title="Configuration error", message="Saves folder seems to be invalid.")

View File

@ -0,0 +1,151 @@
from tkinter import NSEW, Canvas, Event, ttk
from classes.custom.themed_frame import ThemedFrame
FIT_WIDTH = "fit_width"
FIT_HEIGHT = "fit_height"
class ScrollableFrame(ThemedFrame):
"""
There is no way to scroll <tkinter.Frame> so we are
going to create a canvas and place the frame there.
Scrolling the canvas will give the illusion of scrolling
the frame
Partly taken from:
https://blog.tecladocode.com/tkinter-scrollable-frames/
https://stackoverflow.com/a/17457843/11106801
master_frame---------------------------------------------------------
| dummy_canvas----------------------------------------- y_scroll-- |
| | self--------------------------------------------- | | | |
| | | | | | | |
| | | | | | | |
| | | | | | | |
| | ------------------------------------------------ | | | |
| ---------------------------------------------------- --------- |
| x_scroll--------------------------------------------- |
| | | |
| ---------------------------------------------------- |
--------------------------------------------------------------------
"""
def __init__(self, master=None, scroll_speed:int=2, hscroll:bool=False, vscroll:bool=True, scrollbar_kwargs={}, **kwargs):
assert isinstance(scroll_speed, int), "`scroll_speed` must be an int"
self.scroll_speed = scroll_speed
self.master_frame = ThemedFrame(master)
self.master_frame.grid_rowconfigure(0, weight=1)
self.master_frame.grid_columnconfigure(0, weight=1)
self.dummy_canvas = Canvas(self.master_frame, highlightthickness=0, **kwargs)
super().__init__(self.dummy_canvas)
# Create the 2 scrollbars
if vscroll:
self.v_scrollbar = ttk.Scrollbar(self.master_frame, orient="vertical", command=self.dummy_canvas.yview, **scrollbar_kwargs)
self.v_scrollbar.grid(row=0, column=1, sticky=NSEW)
self.dummy_canvas.configure(yscrollcommand=self.v_scrollbar.set)
if hscroll:
self.h_scrollbar = ttk.Scrollbar(self.master_frame, orient="horizontal", command=self.dummy_canvas.xview, **scrollbar_kwargs)
self.h_scrollbar.grid(row=1, column=0, sticky=NSEW)
self.dummy_canvas.configure(xscrollcommand=self.h_scrollbar.set)
# Bind to the mousewheel scrolling
self.dummy_canvas.bind_all("<MouseWheel>", self.scrolling_windows, add=True)
self.dummy_canvas.bind_all("<Button-4>", self.scrolling_linux, add=True)
self.dummy_canvas.bind_all("<Button-5>", self.scrolling_linux, add=True)
self.bind("<Configure>", self.scrollbar_scrolling, add=True)
# Place `self` inside `dummy_canvas`
self.dummy_canvas.create_window((0, 0), window=self, anchor="nw")
# Place `dummy_canvas` inside `master_frame`
self.dummy_canvas.grid(row=0, column=0, sticky=NSEW)
self.pack = self.master_frame.pack
self.grid = self.master_frame.grid
self.place = self.master_frame.place
self.pack_forget = self.master_frame.pack_forget
self.grid_forget = self.master_frame.grid_forget
self.place_forget = self.master_frame.place_forget
def scrolling_windows(self, event:Event) -> None:
assert event.delta != 0, "On Windows, `event.delta` should never be 0"
y_steps = int(-event.delta/abs(event.delta)*self.scroll_speed)
self.dummy_canvas.yview_scroll(y_steps, "units")
def scrolling_linux(self, event:Event) -> None:
y_steps = self.scroll_speed
if event.num == 4:
y_steps *= -1
self.dummy_canvas.yview_scroll(y_steps, "units")
def scrollbar_scrolling(self, event:Event) -> None:
region = list(self.dummy_canvas.bbox("all"))
region[2] = max(self.dummy_canvas.winfo_width(), region[2])
region[3] = max(self.dummy_canvas.winfo_height(), region[3])
self.dummy_canvas.configure(scrollregion=region)
def resize(self, fit:str=None, height:int=None, width:int=None) -> None:
"""
Resizes the frame to fit the widgets inside. You must either
specify (the `fit`) or (the `height` or/and the `width`) parameter.
Parameters:
fit:str `fit` can be either `FIT_WIDTH` or `FIT_HEIGHT`.
`FIT_WIDTH` makes sure that the frame's width can
fit all of the widgets. `FIT_HEIGHT` is simmilar
height:int specifies the height of the frame in pixels
width:int specifies the width of the frame in pixels
To do:
ALWAYS_FIT_WIDTH
ALWAYS_FIT_HEIGHT
"""
if height is not None:
self.dummy_canvas.config(height=height)
if width is not None:
self.dummy_canvas.config(width=width)
if fit == FIT_WIDTH:
super().update()
self.dummy_canvas.config(width=super().winfo_width())
elif fit == FIT_HEIGHT:
super().update()
self.dummy_canvas.config(height=super().winfo_height())
else:
raise ValueError("Unknow value for the `fit` parameter.")
fit = resize
# import tkinter as tk
# from tkinter import ttk
# from classes.custom.themed_frame import ThemedFrame
# class ScrollableFrame(ThemedFrame):
# def __init__(self, container, *args, **kwargs):
# super().__init__(container, *args, **kwargs)
# self.canvas = tk.Canvas(self)
# scrollbar = ttk.Scrollbar(self, orient="vertical", command=self.canvas.yview)
# self.canvas.bind_all("<MouseWheel>", self._on_mousewheel)
# self.scrollable_frame = ThemedFrame(self.canvas)
# self.scrollable_frame.bind(
# "<Configure>",
# lambda e: self.canvas.configure(
# scrollregion=self.canvas.bbox("all")
# )
# )
# self.canvas.create_window((0, 0), window=self.scrollable_frame, anchor="nw")
# self.canvas.configure(yscrollcommand=scrollbar.set)
# self.canvas.pack(side="left", fill="both", expand=True)
# scrollbar.pack(side="right", fill="y")
# def _on_mousewheel(self, event):
# shift = (event.state & 0x1) != 0
# scroll = -1 if event.delta > 0 else 1
# if shift:
# self.canvas.xview_scroll(scroll, "units")
# else:
# self.canvas.yview_scroll(scroll, "units")

View File

@ -1,9 +1,11 @@
from tkinter import Misc
import sv_ttk
from tkinter.ttk import Frame
import sv_ttk
from modules.utils import get_string_mode
class ThemedFrame(Frame):
def __init__(self, master: Misc, **kwargs) -> None:

View File

@ -1,11 +1,101 @@
from tkinter import EW, NS, ttk
from datetime import datetime
from functools import partial
from tkinter import LEFT, NSEW, E, W, ttk
from tkinter.messagebox import askyesno
import requests
from ttkthemes import ThemedTk
from classes.custom.scrollable_frame import ScrollableFrame
from classes.custom.themed_frame import ThemedFrame
from modules.utils import configGet
class FrameDevices(ThemedFrame):
class FrameDevices(ScrollableFrame):
def __init__(self, master: ThemedTk, devices: list, **kwargs) -> None:
super().__init__(master, **kwargs)
master.title("Devices - Stardew Sync")
# self["borderwidth"] = 1
# self["relief"] = "solid"
self.devices = devices
# self.grid_columnconfigure(0, weight=1)
# self.grid_rowconfigure(0, weight=2)
master.columnconfigure(1, weight=1)
self.render_devices()
def render_devices(self):
i = 0
for device in self.devices:
device_frame = ThemedFrame(self, style="Card.TFrame")
device_frame.grid(column=0, row=i, pady=9, padx=9, sticky=NSEW)
# self.device_frame["borderwidth"] = 1
# self.device_frame["relief"] = "solid"
device_frame.grid_columnconfigure(0, weight=1)
device_frame.grid_columnconfigure(1, weight=3)
device_frame.grid_columnconfigure(2, weight=3)
device_title = ttk.Label(device_frame, text=device["name"], font=("SunValleyBodyFont", 12), justify=LEFT, width=46)
device_title.grid(column=0, row=0, padx=9, pady=9, sticky=W)
last_upload = "N/A" if device["last_save"] == 0 else datetime.utcfromtimestamp(device["last_save"]).strftime("%d.%m.%Y %H:%M")
device_description = ttk.Label(device_frame, text=f'OS: {device["os"]}\nClient: {device["client"]}\nLast upload: {last_upload}', width=46)
device_description.grid(column=0, row=1, padx=9, pady=9, sticky=W)
buttons_frame = ThemedFrame(device_frame)
buttons_frame.grid(column=0, columnspan=2, row=2, sticky=NSEW, padx=9, pady=9)
buttons_frame.grid_columnconfigure(0, weight=1)
button_device_rename = ttk.Button(buttons_frame, text="Rename", width=11)
button_device_rename.grid(column=0, row=0, padx=9, sticky=E)
button_device_delete_action = partial(self.device_delete, device["name"])
button_device_delete = ttk.Button(buttons_frame, text="Delete", style="Accent.TButton", width=11, command=button_device_delete_action)
button_device_delete.grid(column=1, row=0, sticky=W)
if device["name"] == configGet("name"):
button_device_rename.state(["disabled"])
button_device_delete.state(["disabled"])
i += 1
if i+1 != len(self.devices):
divider = ttk.Separator(self, orient="horizontal")
divider.grid(column=0, row=i+1, pady=9)
i += 1
def device_delete(self, name: str):
decision = askyesno(title="Device removal", message=f"You are about to remove the device '{name}' and this will also remove all the save files uploaded by this device. Are you sure you want to continue?")
if decision is False:
return
requests.delete(f'{configGet("address")}/devices/{name}', headers={"apikey": configGet("apikey")}, verify=not configGet("allow_self_signed"))
for k in range(len(self.devices)):
print(k)
if self.devices[k]["name"] == name:
del self.devices[k]
break
for widget in self.winfo_children():
widget.destroy()
self.render_devices()
class FrameDevicesEmpty(ThemedFrame):
def __init__(self, master: ThemedTk, **kwargs) -> None:
@ -13,11 +103,13 @@ class FrameDevices(ThemedFrame):
master.title("Devices - Stardew Sync")
self["borderwidth"] = 2
self["relief"] = "solid"
# self["borderwidth"] = 1
# self["relief"] = "solid"
for btn in range(0, 10):
ttk.Button(self, text="DEVICES "+str(btn)).grid(column=0, row=btn, padx=9, pady=9, sticky=EW)
self.grid_columnconfigure(0, weight=1)
self.grid_rowconfigure(0, weight=2)
self.scrollbar = ttk.Scrollbar(self, orient="vertical")
self.scrollbar.grid(column=1, row=0, sticky=NS)
master.columnconfigure(1, weight=1)
self.label = ttk.Label(self, text="No devices found")
self.label.grid(column=0, row=0, padx=9, pady=9)

View File

@ -1,4 +1,5 @@
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
@ -7,6 +8,7 @@ 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
@ -17,29 +19,41 @@ class FrameSettings(ThemedFrame):
super().__init__(master, **kwargs)
master.title("Configuration")
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=0, sticky=W, padx=9, pady=9)
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=0, sticky=N+S+E+W, padx=9, pady=9)
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=1, sticky=W, padx=9, pady=9)
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=1, sticky=N+S+E+W, padx=9, pady=9)
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"))
# =======
@ -50,19 +64,19 @@ class FrameSettings(ThemedFrame):
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=2, sticky=W, padx=9, pady=9)
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=3, sticky=W, padx=9, pady=9)
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=3, sticky=N+S+E+W, padx=9, pady=9)
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=36)
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"))
@ -87,7 +101,7 @@ class FrameSettings(ThemedFrame):
# ==============
self.buttons_frame = ThemedFrame(self)
self.buttons_frame.grid(column=0, columnspan=2, sticky=NSEW, padx=9, pady=9)
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)
@ -129,7 +143,25 @@ class FrameSettings(ThemedFrame):
else:
self.address_text = None
configSet(["address"], self.address_text)
# =========================
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())
@ -151,6 +183,11 @@ class FrameSettings(ThemedFrame):
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("/"):
@ -185,11 +222,15 @@ class FrameSettings(ThemedFrame):
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(["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.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!")
# messagebox.showinfo(title="Configuration completed", message="Your client is now configured and ready to use!")

View File

@ -5,6 +5,7 @@ from modules.utils import jsonSave
if not path.exists("config.json"):
jsonSave(
{
"name": None,
"address": None,
"apikey": None,
"allow_self_signed": False,