WIP: Saves windows

This commit is contained in:
Profitroll 2023-01-25 16:23:18 +01:00
parent 3666db25c6
commit b1aeb06a09
7 changed files with 276 additions and 208 deletions

View File

@ -9,7 +9,7 @@ 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, FrameErrorFirstStart, FrameErrorSavesFolder, FrameErrorUnconfigured
from classes.frames.saves import FrameSaves
from classes.frames.saves import FrameSaves, FrameSavesEmpty
from classes.frames.settings import FrameSettings
from classes.toplevel.welcome import ToplevelWelcome
from modules.theme_titlebar import theme_title_bar
@ -25,11 +25,11 @@ class App(ThemedTk):
self.__version__ = "0.1.0"
resize_window(self, 610, 400)
self.resizable(False, True)
self.minsize(610, 200)
self.title("Stardew Sync")
self.resizable(False, True)
sv_ttk.init_theme(self)
if use_dark_mode():
@ -63,7 +63,11 @@ 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_saves = requests.get(f'{configGet("address")}/saves?only_ids=True', headers={"apikey": configGet("apikey")}, verify=not configGet("allow_self_signed"))
if self.frame_saves_saves.status_code == 200 and isinstance(self.frame_saves_saves.json(), list) is True and len(self.frame_saves_saves.json()) > 0:
self.frame_saves_object = FrameSaves(self, self.frame_saves_saves.json(), vscroll=True)
else:
self.frame_saves_object = FrameSavesEmpty(self)
self.frame_saves_object.grid(column=1, row=0, sticky=NSEW)
return
@ -73,12 +77,8 @@ class App(ThemedTk):
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

148
classes/frames/device.py Normal file
View File

@ -0,0 +1,148 @@
import platform
from datetime import datetime
from functools import partial
from tkinter import LEFT, NSEW, E, Misc, W, messagebox, ttk
from tkinter.messagebox import askyesno
from urllib.parse import quote, urlencode
import requests
from classes.custom.themed_frame import ThemedFrame
from modules.logger import logger
from modules.utils import configGet, configSet
class FrameDevice(ThemedFrame):
def __init__(self, master: Misc, device_dict: str, **kwargs) -> None:
super().__init__(master, style="Card.TFrame", **kwargs)
self.grid_columnconfigure(0, weight=1)
self.grid_columnconfigure(1, weight=3)
self.grid_columnconfigure(2, weight=3)
self.name = device_dict["name"]
self.title = ttk.Label(self, text=self.name, font=("SunValleyBodyStrongFont", 12, "bold"), justify=LEFT, width=46)
self.title.grid(column=0, row=0, padx=9, pady=9, sticky=W)
last_upload = "N/A" if device_dict["last_save"] == 0 else datetime.utcfromtimestamp(device_dict["last_save"]).strftime("%d.%m.%Y %H:%M")
self.description = ttk.Label(self, text=f'OS: {device_dict["os"]}\nClient: {device_dict["client"]}\nLast upload: {last_upload}', width=46)
self.description.grid(column=0, row=1, padx=9, pady=9, sticky=W)
self.buttons = ThemedFrame(self)
self.buttons.grid(column=0, columnspan=2, row=2, sticky=NSEW, padx=9, pady=9)
self.buttons.grid_columnconfigure(0, weight=1)
self.button_device_rename_action = partial(self.rename)
self.button_device_rename = ttk.Button(self.buttons, text="Rename", width=11, command=self.button_device_rename_action)
self.button_device_rename.grid(column=0, row=0, padx=9, sticky=E)
self.button_device_delete_action = partial(self.delete)
self.button_device_delete = ttk.Button(self.buttons, text="Delete", style="Accent.TButton", width=11, command=self.button_device_delete_action)
self.button_device_delete.grid(column=1, row=0, sticky=W)
if self.name == configGet("name"):
self.button_device_delete.state(["disabled"])
def rename(self):
self.rename_entry = ttk.Entry(self, font=("SunValleyBodyFont", 12), justify=LEFT, width=27)
self.rename_entry.insert(0, self.name)
self.rename_entry.grid(column=0, row=0, padx=9, pady=9, sticky=W)
button_device_cancel_action = partial(self.rename_cancel)
button_device_cancel = ttk.Button(self.buttons, text="Cancel", width=11, command=button_device_cancel_action)
button_device_cancel.grid(column=0, row=0, padx=9, sticky=E)
button_device_save_action = partial(self.rename_verify)
button_device_save = ttk.Button(self.buttons, text="Save", style="Accent.TButton", width=11, command=button_device_save_action)
button_device_save.grid(column=1, row=0, sticky=W)
def rename_verify(self):
self.name_before = configGet("name")
if (self.rename_entry.get().strip() == "") or ("?" in self.rename_entry.get().strip()) or ("/" in self.rename_entry.get().strip()):
logger.error(f"Name {self.rename_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
try:
quote(self.rename_entry.get().strip())
except:
logger.error(f"Name {self.rename_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
existing_device_before = requests.get(f'{configGet("address")}/devices/{self.name}', headers={"apikey": configGet("apikey")}, verify=not configGet("allow_self_signed"))
if existing_device_before.status_code == 200:
response = requests.patch(f'{configGet("address")}/devices/{self.name}?{urlencode({"new_name": self.rename_entry.get().strip(), "os": platform.system()+" "+platform.release(), "client": f"SyncTk {self.master.master.master.master.__version__}"})}', headers={"apikey": configGet("apikey")}, verify=not configGet("allow_self_signed"))
if response.status_code != 204:
logger.error(f"Name {self.rename_entry.get().strip()} could not be set because server returned {response.status_code}")
messagebox.showerror(title="Name error", message=f"Provided device name is not valid.\n\nServer response: {response.json()}")
return
else:
logger.error(f"Tried to rename {self.name} into {self.rename_entry.get().strip()} but server returned {existing_device_before.status_code}")
messagebox.showerror(title="Rename error", message="It seems like this device no longer exists.")
return
self.name = self.rename_entry.get().strip()
for widget in self.winfo_children():
if isinstance(widget, ttk.Entry):
widget.destroy()
device_title = ttk.Label(self, text=self.name, font=("SunValleyBodyStrongFont", 12, "bold"), justify=LEFT, width=46)
device_title.grid(column=0, row=0, padx=9, pady=9, sticky=W)
button_device_rename_action = partial(self.rename)
button_device_rename = ttk.Button(self.buttons, text="Rename", width=11, command=button_device_rename_action)
button_device_rename.grid(column=0, row=0, padx=9, sticky=E)
button_device_delete_action = partial(self.delete)
button_device_delete = ttk.Button(self.buttons, text="Delete", style="Accent.TButton", width=11, command=button_device_delete_action)
button_device_delete.grid(column=1, row=0, sticky=W)
if self.name_before == configGet("name"):
configSet(["name"], self.name)
if self.name == configGet("name"):
button_device_delete.state(["disabled"])
def rename_cancel(self):
for widget in self.winfo_children():
if isinstance(widget, ttk.Entry):
widget.destroy()
device_title = ttk.Label(self, text=self.name, font=("SunValleyBodyStrongFont", 12, "bold"), justify=LEFT, width=46)
device_title.grid(column=0, row=0, padx=9, pady=9, sticky=W)
button_device_rename_action = partial(self.rename)
button_device_rename = ttk.Button(self.buttons, text="Rename", width=11, command=button_device_rename_action)
button_device_rename.grid(column=0, row=0, padx=9, sticky=E)
button_device_delete_action = partial(self.delete)
button_device_delete = ttk.Button(self.buttons, text="Delete", style="Accent.TButton", width=11, command=button_device_delete_action)
button_device_delete.grid(column=1, row=0, sticky=W)
if self.name == configGet("name"):
button_device_delete.state(["disabled"])
def delete(self):
decision = askyesno(title="Device removal", message=f"You are about to remove the device '{self.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/{self.name}', headers={"apikey": configGet("apikey")}, verify=not configGet("allow_self_signed"))
for k in range(len(self.master.devices)):
if self.master.devices[k]["name"] == self.name:
del self.master.devices[k]
break
self.destroy()

View File

@ -1,159 +1,10 @@
from datetime import datetime
from functools import partial
import platform
from tkinter import LEFT, NSEW, E, W, Misc, StringVar, ttk
from tkinter import messagebox
from tkinter.messagebox import askyesno, showinfo
from tkinter.simpledialog import askstring
from urllib.parse import quote, urlencode
from tkinter import NSEW, ttk
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, configSet
from modules.logger import logger
class FrameDevice(ThemedFrame):
def __init__(self, master: Misc, device_dict: str, **kwargs) -> None:
super().__init__(master, style="Card.TFrame", **kwargs)
self["borderwidth"] = 1
self["relief"] = "solid"
self.grid_columnconfigure(0, weight=1)
self.grid_columnconfigure(1, weight=3)
self.grid_columnconfigure(2, weight=3)
self.name = device_dict["name"]
self.title = ttk.Label(self, text=self.name, font=("SunValleyBodyFont", 12), justify=LEFT, width=46)
self.title.grid(column=0, row=0, padx=9, pady=9, sticky=W)
last_upload = "N/A" if device_dict["last_save"] == 0 else datetime.utcfromtimestamp(device_dict["last_save"]).strftime("%d.%m.%Y %H:%M")
self.description = ttk.Label(self, text=f'OS: {device_dict["os"]}\nClient: {device_dict["client"]}\nLast upload: {last_upload}', width=46)
self.description.grid(column=0, row=1, padx=9, pady=9, sticky=W)
self.buttons = ThemedFrame(self)
self.buttons.grid(column=0, columnspan=2, row=2, sticky=NSEW, padx=9, pady=9)
self.buttons.grid_columnconfigure(0, weight=1)
self.button_device_rename_action = partial(self.rename)
self.button_device_rename = ttk.Button(self.buttons, text="Rename", width=11, command=self.button_device_rename_action)
self.button_device_rename.grid(column=0, row=0, padx=9, sticky=E)
self.button_device_delete_action = partial(self.delete)
self.button_device_delete = ttk.Button(self.buttons, text="Delete", style="Accent.TButton", width=11, command=self.button_device_delete_action)
self.button_device_delete.grid(column=1, row=0, sticky=W)
if self.name == configGet("name"):
# button_device_rename.state(["disabled"])
self.button_device_delete.state(["disabled"])
def rename(self):
self.rename_entry = ttk.Entry(self, font=("SunValleyBodyFont", 12), justify=LEFT, width=27)
self.rename_entry.insert(0, self.name)
self.rename_entry.grid(column=0, row=0, padx=9, pady=9, sticky=W)
button_device_cancel_action = partial(self.rename_cancel)
button_device_cancel = ttk.Button(self.buttons, text="Cancel", width=11, command=button_device_cancel_action)
button_device_cancel.grid(column=0, row=0, padx=9, sticky=E)
button_device_save_action = partial(self.rename_verify)
button_device_save = ttk.Button(self.buttons, text="Save", style="Accent.TButton", width=11, command=button_device_save_action)
button_device_save.grid(column=1, row=0, sticky=W)
def rename_verify(self):
self.name_before = configGet("name")
if (self.rename_entry.get().strip() == "") or ("?" in self.rename_entry.get().strip()) or ("/" in self.rename_entry.get().strip()):
logger.error(f"Name {self.rename_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
try:
quote(self.rename_entry.get().strip())
except:
logger.error(f"Name {self.rename_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
existing_device_before = requests.get(f'{configGet("address")}/devices/{self.name}', headers={"apikey": configGet("apikey")}, verify=not configGet("allow_self_signed"))
if existing_device_before.status_code == 200:
response = requests.patch(f'{configGet("address")}/devices/{self.name}?{urlencode({"new_name": self.rename_entry.get().strip(), "os": platform.system()+" "+platform.release(), "client": f"SyncTk {self.master.master.master.master.__version__}"})}', headers={"apikey": configGet("apikey")}, verify=not configGet("allow_self_signed"))
if response.status_code != 204:
logger.error(f"Name {self.rename_entry.get().strip()} could not be set because server returned {response.status_code}")
messagebox.showerror(title="Name error", message=f"Provided device name is not valid.\n\nServer response: {response.json()}")
return
else:
logger.error(f"Tried to rename {self.name} into {self.rename_entry.get().strip()} but server returned {existing_device_before.status_code}")
messagebox.showerror(title="Rename error", message="It seems like this device no longer exists.")
return
self.name = self.rename_entry.get().strip()
for widget in self.winfo_children():
if isinstance(widget, ttk.Entry):
widget.destroy()
device_title = ttk.Label(self, text=self.name, font=("SunValleyBodyFont", 12), justify=LEFT, width=46)
device_title.grid(column=0, row=0, padx=9, pady=9, sticky=W)
button_device_rename_action = partial(self.rename)
button_device_rename = ttk.Button(self.buttons, text="Rename", width=11, command=button_device_rename_action)
button_device_rename.grid(column=0, row=0, padx=9, sticky=E)
button_device_delete_action = partial(self.delete)
button_device_delete = ttk.Button(self.buttons, text="Delete", style="Accent.TButton", width=11, command=button_device_delete_action)
button_device_delete.grid(column=1, row=0, sticky=W)
if self.name_before == configGet("name"):
configSet(["name"], self.name)
if self.name == configGet("name"):
button_device_delete.state(["disabled"])
def rename_cancel(self):
for widget in self.winfo_children():
if isinstance(widget, ttk.Entry):
widget.destroy()
device_title = ttk.Label(self, text=self.name, font=("SunValleyBodyFont", 12), justify=LEFT, width=46)
device_title.grid(column=0, row=0, padx=9, pady=9, sticky=W)
button_device_rename_action = partial(self.rename)
button_device_rename = ttk.Button(self.buttons, text="Rename", width=11, command=button_device_rename_action)
button_device_rename.grid(column=0, row=0, padx=9, sticky=E)
button_device_delete_action = partial(self.delete)
button_device_delete = ttk.Button(self.buttons, text="Delete", style="Accent.TButton", width=11, command=button_device_delete_action)
button_device_delete.grid(column=1, row=0, sticky=W)
if self.name == configGet("name"):
button_device_delete.state(["disabled"])
def delete(self):
decision = askyesno(title="Device removal", message=f"You are about to remove the device '{self.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/{self.name}', headers={"apikey": configGet("apikey")}, verify=not configGet("allow_self_signed"))
for k in range(len(self.master.devices)):
if self.master.devices[k]["name"] == self.name:
del self.master.devices[k]
break
self.destroy()
from classes.frames.device import FrameDevice
class FrameDevices(ScrollableFrame):
@ -164,14 +15,8 @@ class FrameDevices(ScrollableFrame):
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()
@ -183,36 +28,6 @@ class FrameDevices(ScrollableFrame):
device_frame = FrameDevice(self, device_dict=device)
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_action = partial(self.device_rename, device_frame, buttons_frame, device["name"])
# button_device_rename = ttk.Button(buttons_frame, text="Rename", width=11, command=button_device_rename_action)
# 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
@ -229,9 +44,6 @@ class FrameDevicesEmpty(ThemedFrame):
master.title("Devices - Stardew Sync")
# self["borderwidth"] = 1
# self["relief"] = "solid"
self.grid_columnconfigure(0, weight=1)
self.grid_rowconfigure(0, weight=2)

37
classes/frames/save.py Normal file
View File

@ -0,0 +1,37 @@
from datetime import datetime, timezone
from tkinter import LEFT, NSEW, Misc, S, W, ttk
from classes.custom.themed_frame import ThemedFrame
class FrameSave(ThemedFrame):
def __init__(self, master: Misc, save_dict: str, **kwargs) -> None:
super().__init__(master, style="Card.TFrame", **kwargs)
self.grid_columnconfigure(0, weight=1)
self.grid_columnconfigure(1, weight=3)
self.grid_columnconfigure(2, weight=3)
self.title = ttk.Label(self, text=save_dict["data"]["farmer"], font=("SunValleyBodyStrongFont", 12, "bold"), justify=LEFT, width=46)
self.title.grid(column=0, row=0, padx=9, pady=9, sticky=W)
self.description = ttk.Label(self, text=f'Money: {save_dict["data"]["money"]}\nGame version: {save_dict["data"]["game_version"]}\nID: {save_dict["id"]}', width=46)
self.description.grid(column=0, row=1, padx=9, pady=9, sticky=W)
self.buttons = ThemedFrame(self)
self.buttons.grid(column=0, columnspan=2, row=2, sticky=NSEW, padx=9, pady=9)
self.buttons.grid_columnconfigure(0, weight=1)
upload_date = datetime.utcfromtimestamp(save_dict["date"]).replace(tzinfo=timezone.utc).astimezone(tz=None)
self.last_upload = ttk.Label(self.buttons, text=f'{upload_date.strftime("%A, %d %b %Y")}\nUploaded at {upload_date.strftime("%H:%M")} by {save_dict["device"]}', font=("SunValleyBodyFont", 8), justify=LEFT, width=46)
self.last_upload.grid(column=0, row=0, sticky=W+S)
# self.button_device_rename_action = partial(self.rename)
# self.button_device_rename = ttk.Button(self.buttons, text="Rename", width=11, command=self.button_device_rename_action)
# self.button_device_rename.grid(column=0, row=0, padx=9, sticky=E)
#self.button_device_delete_action = partial(self.delete)
self.button_device_delete = ttk.Button(self.buttons, text="Synchronize", style="Accent.TButton", width=11) #, command=self.button_device_delete_action)
self.button_device_delete.grid(column=1, row=0, sticky=W)

View File

@ -1,11 +1,81 @@
from tkinter import EW, NS, ttk
from os import path, walk
from tkinter import NSEW, ttk
import xmltodict
from ttkthemes import ThemedTk
from classes.custom.scrollable_frame import ScrollableFrame
from classes.custom.themed_frame import ThemedFrame
from classes.frames.save import FrameSave
from modules.utils import configGet
class FrameSaves(ThemedFrame):
class FrameSaves(ScrollableFrame):
def __init__(self, master: ThemedTk, saves: list, **kwargs) -> None:
super().__init__(master, **kwargs)
master.title("Saves - Stardew Sync")
self.saves = saves
self.saves_local = []
for subdir, dirs, files in walk(configGet("saves_location")):
try:
for dir in dirs:
with open(path.join(dir, "SaveGameInfo"), "r", encoding="utf-8") as file:
save_dict = xmltodict.parse(file.read())
self.saves_local.append(
{
"id": int(save_dict["SaveGame"]["uniqueIDForThisGame"]),
"user": None,
"device": configGet("name"),
"date": None,
"data": {
"farmer": save_dict["Farmer"]["name"],
"money": int(save_dict["Farmer"]["money"]),
"played": int(save_dict["Farmer"]["millisecondsPlayed"]),
"save_time": int(save_dict["Farmer"]["saveTime"]),
"year": int(save_dict["Farmer"]["yearForSaveGame"]),
"season": int(save_dict["Farmer"]["seasonForSaveGame"]),
"day": int(save_dict["Farmer"]["dayOfMonthForSaveGame"]),
"game_version": save_dict["Farmer"]["gameVersion"]
},
"file": {
"name": None,
"uuid": None,
"path": None
}
}
)
except:
pass
# Merge local and remote saves.
# Maybe add something that indicates availability of an
# remote update to pull or local new version to push.
master.columnconfigure(1, weight=1)
self.render_saves()
def render_saves(self):
i = 0
for save in self.saves:
save_frame = FrameSave(self, save_dict=save)
save_frame.grid(column=0, row=i, pady=9, padx=9, sticky=NSEW)
i += 1
if i+1 != len(self.saves):
divider = ttk.Separator(self, orient="horizontal")
divider.grid(column=0, row=i+1, pady=9)
i += 1
class FrameSavesEmpty(ThemedFrame):
def __init__(self, master: ThemedTk, **kwargs) -> None:
@ -13,13 +83,10 @@ class FrameSaves(ThemedFrame):
master.title("Saves - Stardew Sync")
self["borderwidth"] = 2
self["relief"] = "solid"
self.grid_columnconfigure(0, weight=1)
self.grid_rowconfigure(0, weight=2)
self.inside_frame = ThemedFrame(self)
master.columnconfigure(1, weight=1)
for btn in range(0, 10):
ttk.Button(self.inside_frame, text="SAVES "+str(btn)).grid(column=0, row=btn, padx=9, pady=9, sticky=EW)
self.scrollbar = ttk.Scrollbar(self.inside_frame, orient="vertical")
self.scrollbar.grid(column=1, row=0, sticky=NS)
self.label = ttk.Label(self, text="No saves found")
self.label.grid(column=0, row=0, padx=9, pady=9)

View File

@ -1,3 +1,4 @@
import locale
from os import path
from classes.app import App
from modules.utils import jsonSave
@ -16,6 +17,8 @@ if not path.exists("config.json"):
},
"config.json"
)
locale.setlocale(locale.LC_ALL, '')
if __name__ == "__main__":

View File

@ -1,4 +1,5 @@
darkdetect~=0.8.0
xmltodict~=0.13.0
ttkthemes~=3.2.2
requests~=2.28.2
sv_ttk~=2.4