Compare commits

...

33 Commits

Author SHA1 Message Date
Profitroll
7a2c28d2d9 Updated ignore 2023-01-29 17:05:46 +01:00
Profitroll
217c1c4691 Fixed folder removal technique 2023-01-29 17:04:58 +01:00
Profitroll
ac8b60c775 Changed saves fetching 2023-01-29 12:30:40 +01:00
Profitroll
fba043a052 Got tired of re-downloading reqs all the time 2023-01-29 12:30:19 +01:00
Profitroll
3bab4faa46 Changed run tasks behavior 2023-01-29 11:06:23 +01:00
Profitroll
e631d1717e Fixed dirs in tasks 2023-01-28 18:50:40 +01:00
Profitroll
b39a0253d1 Updated tasks even more 2023-01-28 18:45:57 +01:00
Profitroll
3bae2b646a Updated ignores 2023-01-28 18:35:39 +01:00
Profitroll
76cbf53789 New project tree 2023-01-28 17:50:19 +01:00
Profitroll
fb53eec810 Updated tasks for new project tree 2023-01-28 17:50:11 +01:00
69f26cbb03 Updated build script 2023-01-28 18:41:14 +01:00
Profitroll
d2ca33396e Moved IF to internal directory 2023-01-28 10:04:00 +01:00
Profitroll
d2066e44cb Renamed and reconfigured build tasks 2023-01-28 10:03:19 +01:00
b186aea266 Fixed some stuff for linux tasks 2023-01-27 10:35:19 -05:00
cc69506408 Improved tasks for Linux 2023-01-27 10:33:35 -05:00
70b2048788 Fixed tasks errors on Linux 2023-01-27 10:29:59 -05:00
Profitroll
a37196ab22 And one more tasks update 2023-01-27 16:09:07 +01:00
Profitroll
437a37f569 And once more improved tasks 2023-01-27 15:55:09 +01:00
Profitroll
cf0c8f8e4c Improved tasks once more 2023-01-27 15:53:55 +01:00
Profitroll
97bea5cd90 Tweaked build task for Windows 2023-01-27 15:45:56 +01:00
Profitroll
76defcea2f VSCode tasks optimized for Windows and Linux 2023-01-27 15:34:20 +01:00
Profitroll
c4813d62c8 Updated IF task 2023-01-27 14:43:21 +01:00
Profitroll
f075cf6c29 InstallForge configure task 2023-01-27 13:55:57 +01:00
Profitroll
ab25fe7b43 Updated favicon 2023-01-27 13:43:00 +01:00
Profitroll
d724ed22b1 Updated tasks 2023-01-27 12:53:13 +01:00
Profitroll
8b9119afa9 Changed empty saved behavior 2023-01-27 12:18:10 +01:00
Profitroll
4f8e60ace2 Ignore updated 2023-01-27 12:17:53 +01:00
Profitroll
cd6b6599c5 Build tasks updated 2023-01-27 12:17:46 +01:00
Profitroll
29d84082f1 Paths updated 2023-01-27 12:17:33 +01:00
d844d182f6 WIP: Remote/local saves 2023-01-26 16:27:32 +01:00
062a38ceec Improved enums and error handling 2023-01-26 13:29:30 +01:00
Profitroll
bfcdcce11d Added cleanup task 2023-01-26 10:58:20 +01:00
b376fc3011 Updated config 2023-01-26 11:45:19 +01:00
49 changed files with 668 additions and 184 deletions

13
.gitignore vendored
View File

@@ -153,7 +153,7 @@ cython_debug/
#.idea/ #.idea/
# ---> VisualStudioCode # ---> VisualStudioCode
.vscode/* .vscode/sftp.json
!.vscode/settings.json !.vscode/settings.json
!.vscode/launch.json !.vscode/launch.json
!.vscode/extensions.json !.vscode/extensions.json
@@ -165,5 +165,14 @@ cython_debug/
# Built Visual Studio Code Extensions # Built Visual Studio Code Extensions
*.vsix *.vsix
# InstallForge projects
*.ifp
# Project # Project
cleanup.* venv
buildif
buildenv
sv_ttk
distassets
testbinary
testpython

155
.vscode/tasks.json vendored Normal file
View File

@@ -0,0 +1,155 @@
{
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
"version": "2.0.0",
"tasks": [
{
"label": "Clean up",
"detail": "Remove all .pyco and and __pycache__ in the whole project",
"type": "shell",
"windows": {
"command": "./.vscode/tasks/windows/cleanup.bat"
},
"linux": {
"command": "bash ./.vscode/tasks/linux/cleanup.sh"
},
"problemMatcher": []
},
{
"label": "Clean up everything",
"detail": "Remove all .pyco and __pycache__ as well as virtual envs, build and dist dirs",
"type": "shell",
"windows": {
"command": "./.vscode/tasks/windows/cleanup_everything.bat"
},
"linux": {
"command": "bash ./.vscode/tasks/linux/cleanup_everything.sh"
},
"problemMatcher": [],
"dependsOn": ["Clean up"]
},
{
"label": "Run (Python) [Dirty]",
"detail": "Remove all cache files and start main.py within venv",
"type": "shell",
"windows": {
"command": "./.vscode/tasks/windows/run_python.bat"
},
"linux": {
"command": "bash ./.vscode/tasks/linux/run_python.sh"
},
"group": {
"kind": "test",
"isDefault": true
},
"problemMatcher": [],
"dependsOn": ["Clean up"]
},
{
"label": "Run (Python) [Clean]",
"detail": "Needs to be ran before the dirty version can be used. Copy source code from 'src' dir into 'testpython', install requirements and start main.py within venv",
"type": "shell",
"windows": {
"command": "./.vscode/tasks/windows/flush_and_run_python.bat"
},
"linux": {
"command": "bash ./.vscode/tasks/linux/flush_and_run_python.sh"
},
"group": {
"kind": "test",
"isDefault": true
},
"problemMatcher": [],
"dependsOn": ["Clean up"]
//, "Install requirements"]
},
{
"label": "Run (Binary) [Dirty]",
"detail": "Copy compiled binaries from 'dest/%os%' dir into 'testbinary/%os%' and start it",
"type": "shell",
"windows": {
"command": "./.vscode/tasks/windows/run_binary.bat"
},
"linux": {
"command": "bash ./.vscode/tasks/linux/run_binary.sh"
},
"group": {
"kind": "test",
"isDefault": false
},
"problemMatcher": [],
"dependsOn": ["Build"]
},
{
"label": "Run (Binary) [Clean]",
"detail": "Needs to be ran before the dirty version can be used. Copy compiled binaries from 'dest/%os%' dir into 'testbinary/%os%' and start it",
"type": "shell",
"windows": {
"command": "./.vscode/tasks/windows/flush_and_run_binary.bat"
},
"linux": {
"command": "bash ./.vscode/tasks/linux/flush_and_run_binary.sh"
},
"group": {
"kind": "test",
"isDefault": false
},
"problemMatcher": [],
"dependsOn": ["Build"]
},
{
"label": "Configure Setup",
"detail": "Configure IF setup file on Windows and .deb package on Linux",
"type": "shell",
"windows": {
"command": "./.vscode/tasks/windows/configure_setup.bat"
},
"linux": {
"command": "echo \"Not implemented\""
},
"problemMatcher": [],
"dependsOn": ["Clean up"]
},
{
"label": "Install build requirements",
"detail": "Create buildenv and install the modules to run Pyinstaller's build",
"type": "shell",
"windows": {
"command": "./.vscode/tasks/windows/install_build_requirements.bat"
},
"linux": {
"command": "bash ./.vscode/tasks/linux/install_build_requirements.sh"
},
"problemMatcher": []
},
{
"label": "Install requirements",
"detail": "Create venv and install the modules to run 'src/main.py'",
"type": "shell",
"windows": {
"command": "./.vscode/tasks/windows/install_requirements.bat"
},
"linux": {
"command": "bash ./.vscode/tasks/linux/install_requirements.sh"
},
"problemMatcher": []
},
{
"label": "Build",
"detail": "Configure .spec and run Pyinstaller",
"type": "shell",
"windows": {
"command": "./.vscode/tasks/windows/build.bat"
},
"linux": {
"command": "bash ./.vscode/tasks/linux/build.sh"
},
"problemMatcher": [],
"dependsOn": ["Clean up", "Install build requirements"],
"group": {
"kind": "build",
"isDefault": true
}
},
]
}

5
.vscode/tasks/linux/build.sh vendored Normal file
View File

@@ -0,0 +1,5 @@
#!/bin/bash
rm -rf dist/linux/StardewSync
source buildenv/bin/activate && pyi-makespec ./src/main.py -n "StardewSync" -p "buildenv" -i "./src/assets/favicon.ico" --add-data "./src/config.json:." --add-data "./src/assets:assets" --add-data "buildenv/lib/python3.9/site-packages/sv_ttk/sv.tcl:sv_ttk/" --add-data "buildenv/lib/python3.9/site-packages/sv_ttk/theme/*:sv_ttk/theme/" --noconsole && pyinstaller "StardewSync.spec" --noconfirm --distpath=dist/linux --workpath=build/linux

4
.vscode/tasks/linux/cleanup.sh vendored Normal file
View File

@@ -0,0 +1,4 @@
#!/bin/bash
python3 -Bc "import pathlib; [p.unlink() for p in pathlib.Path('.').rglob('*.py[co]')]"
python3 -Bc "import pathlib; [p.rmdir() for p in pathlib.Path('.').rglob('__pycache__')]"

View File

@@ -0,0 +1,6 @@
#!/bin/bash
rm -rf ./build
rm -rf ./dist/linux
rm -rf ./venv
rm -rf ./buildenv

View File

@@ -0,0 +1,3 @@
#!/bin/bash
# .\dist\InstallForge\install_forge.ifp

View File

@@ -0,0 +1,12 @@
#!/bin/bash
rm -rf ./testbinary/linux
mkdir ./testbinary/linux
cp -r ./dist/linux/* ./testbinary/linux/
cd "testbinary/linux/"
chmod +x "StardewSync"
"./StardewSync"

View File

@@ -0,0 +1,8 @@
#!/bin/bash
rm -rf testpython
mkdir testpython
cp -r ./src/* ./testpython/
source venv/bin/activate && cd ./testpython && python ./main.py && deactivate

View File

@@ -0,0 +1,7 @@
#!/bin/bash
pip3 install virtualenv
virtualenv buildenv
source buildenv/bin/activate && pip install --upgrade Pyinstaller && pip install --upgrade -r ./src/requirements.txt && deactivate

View File

@@ -0,0 +1,7 @@
#!/bin/bash
pip3 install virtualenv
virtualenv venv
source venv/bin/activate && pip install --upgrade -r ./src/requirements.txt && deactivate

7
.vscode/tasks/linux/run_binary.sh vendored Normal file
View File

@@ -0,0 +1,7 @@
#!/bin/bash
cd "testbinary/linux/"
chmod +x "StardewSync"
"./StardewSync"

3
.vscode/tasks/linux/run_python.sh vendored Normal file
View File

@@ -0,0 +1,3 @@
#!/bin/bash
source venv/bin/activate && cd ./testpython && python ./main.py && deactivate

3
.vscode/tasks/windows/build.bat vendored Normal file
View File

@@ -0,0 +1,3 @@
rmdir /S /Q "dist\windows\StardewSync"
.\buildenv\scripts\activate && pyi-makespec .\src\main.py -n "StardewSync" -p "buildenv" -i "src/assets/favicon.ico" --add-data "src/config.json;." --add-data "src/assets;assets" --add-data "buildenv/Lib/site-packages/sv_ttk/sv.tcl;sv_ttk/" --add-data "buildenv/Lib/site-packages/sv_ttk/theme/*;sv_ttk/theme/" --noconsole && pyinstaller ".\StardewSync.spec" --noconfirm --distpath=dist\windows --workpath=build\windows

2
.vscode/tasks/windows/cleanup.bat vendored Normal file
View File

@@ -0,0 +1,2 @@
python -Bc "import pathlib; [p.unlink() for p in pathlib.Path('.').rglob('*.py[co]')]"
python -Bc "import pathlib; [p.rmdir() for p in pathlib.Path('.').rglob('__pycache__')]"

View File

@@ -0,0 +1,4 @@
rmdir /S /Q .\build
rmdir /S /Q .\dist\windows
rmdir /S /Q .\venv
rmdir /S /Q .\buildenv

View File

@@ -0,0 +1 @@
.\dist\windows\install_forge.ifp

View File

@@ -0,0 +1,7 @@
rmdir /S /Q testbinary\windows
mkdir testbinary\windows
xcopy ".\dist\windows\" ".\testbinary\windows\" /h /i /c /k /e /r /y
cd ".\testbinary\windows\StardewSync"
".\StardewSync.exe"

View File

@@ -0,0 +1,6 @@
rmdir /S /Q testpython
mkdir testpython
xcopy ".\src\" ".\testpython\" /h /i /c /k /e /r /y
venv\Scripts\activate && cd .\testpython && python .\main.py && deactivate

View File

@@ -0,0 +1,5 @@
pip install virtualenv
virtualenv buildenv
buildenv\Scripts\activate && pip install --upgrade Pyinstaller && pip install --upgrade -r .\src\requirements.txt && deactivate

View File

@@ -0,0 +1,5 @@
pip install virtualenv
virtualenv venv
venv\Scripts\activate && pip install --upgrade -r .\src\requirements.txt && deactivate

2
.vscode/tasks/windows/run_binary.bat vendored Normal file
View File

@@ -0,0 +1,2 @@
cd ".\testbinary\windows\StardewSync"
".\StardewSync.exe"

1
.vscode/tasks/windows/run_python.bat vendored Normal file
View File

@@ -0,0 +1 @@
venv\Scripts\activate && cd .\testpython && python .\main.py && deactivate

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

View File

@@ -1,46 +0,0 @@
from datetime import datetime, timezone
from tkinter import LEFT, NSEW, Misc, S, W, ttk
from classes.custom.themed_frame import ThemedFrame
from modules.utils import osname
class FrameSave(ThemedFrame):
def __init__(self, master: Misc, save_dict: str, **kwargs) -> None:
super().__init__(master, style="Card.TFrame", **kwargs)
self.widget_width = 47 if osname == "nt" else 42
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=self.widget_width)
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=self.widget_width)
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=self.widget_width)
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)
def convert_date(self, year: int, month: int, day: int) -> str:
pass
def convert_playtime(self, seconds: int) -> str:
pass

View File

@@ -1,95 +0,0 @@
from os import path, walk
from tkinter import E, NSEW, W, 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(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 = []
# self["borderwidth"] = 1
# self["relief"] = "solid"
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=W+E)
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:
super().__init__(master, **kwargs)
master.title("Saves - Stardew Sync")
self.grid_columnconfigure(0, weight=1)
self.grid_rowconfigure(0, weight=2)
master.columnconfigure(1, weight=1)
self.label = ttk.Label(self, text="No saves found")
self.label.grid(column=0, row=0, padx=9, pady=9)

BIN
src/assets/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

View File

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

View File

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -1,5 +1,6 @@
from os import path from os import path
from tkinter import NSEW, NW, E, N, S, W, PhotoImage, messagebox, ttk from tkinter import NSEW, NW, E, N, S, W, PhotoImage, messagebox, ttk
from traceback import format_exc
import requests import requests
import sv_ttk import sv_ttk
@@ -7,13 +8,15 @@ from ttkthemes import ThemedTk
from classes.custom.scrollable_frame import FIT_HEIGHT, FIT_WIDTH from classes.custom.scrollable_frame import FIT_HEIGHT, FIT_WIDTH
from classes.custom.themed_frame import ThemedFrame from classes.custom.themed_frame import ThemedFrame
from classes.enums import ConnectionState, Theme
from classes.frames.devices import FrameDevices, FrameDevicesEmpty from classes.frames.devices import FrameDevices, FrameDevicesEmpty
from classes.frames.errors import FrameErrorConnection, FrameErrorFirstStart, FrameErrorSavesFolder, FrameErrorUnconfigured from classes.frames.errors import FrameErrorConnection, FrameErrorFirstStart, FrameErrorSavesFolder, FrameErrorUnconfigured
from classes.frames.saves import FrameSaves, FrameSavesEmpty from classes.frames.saves import FrameSaves
from classes.frames.settings import FrameSettings from classes.frames.settings import FrameSettings
from classes.toplevel.welcome import ToplevelWelcome from classes.toplevel.welcome import ToplevelWelcome
from modules.theme_titlebar import theme_title_bar from modules.theme_titlebar import theme_title_bar
from modules.utils import configGet, get_string_mode, resize_window, set_icon, use_dark_mode from modules.utils import configGet, get_string_mode, resize_window, set_icon, use_dark_mode
from modules.logger import logger
class App(ThemedTk): class App(ThemedTk):
@@ -33,7 +36,7 @@ class App(ThemedTk):
sv_ttk.init_theme(self) sv_ttk.init_theme(self)
if use_dark_mode(): if use_dark_mode():
theme_title_bar(self, mode="dark") theme_title_bar(self, mode=Theme.DARK)
self.update() self.update()
set_icon(self) set_icon(self)
@@ -43,14 +46,11 @@ class App(ThemedTk):
self.draw_main() self.draw_main()
def verify_authorization(self): def verify_authorization(self) -> ConnectionState:
try: try:
if requests.get(configGet("address")+"/apikey", headers={"apikey": configGet("apikey")}, verify=not configGet("allow_self_signed")).status_code == 200: return ConnectionState.UNAUTHORIZED if requests.get(configGet("address")+"/apikey", headers={"apikey": configGet("apikey")}, verify=not configGet("allow_self_signed")).status_code == 403 else ConnectionState.OK
return True
else:
return False
except: except:
return False return ConnectionState.BAD
def verify_saves_dir(self): def verify_saves_dir(self):
@@ -63,18 +63,33 @@ class App(ThemedTk):
def frame_saves(self): def frame_saves(self):
self.grid_rowconfigure(0, weight=1) self.grid_rowconfigure(0, weight=1)
self.grid_columnconfigure(1, weight=1) self.grid_columnconfigure(1, weight=1)
self.frame_saves_saves = requests.get(f'{configGet("address")}/saves?only_ids=True&sort={(configGet("prefer_saves").split())[1]}', headers={"apikey": configGet("apikey")}, verify=not configGet("allow_self_signed")) # try:
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_saves = requests.get(f'{configGet("address")}/saves?only_ids=True&sort={(configGet("prefer_saves").split())[1]}', headers={"apikey": configGet("apikey")}, verify=not configGet("allow_self_signed"))
self.frame_saves_object = FrameSaves(self, self.frame_saves_saves.json(), vscroll=True) # except Exception as exp:
else: # messagebox.showerror(title="Connection error", message=f"We could not reach the server to check for save entries\n\n{exp}")
self.frame_saves_object = FrameSavesEmpty(self) # logger.error(format_exc())
# self.frame_saves_object = FrameErrorConnection(self)
# self.frame_saves_object.grid(column=1, row=0, sticky=NSEW)
# return
# 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 = FrameSaves(self, [], vscroll=True)
self.frame_saves_object = FrameSaves(self, vscroll=True)
self.frame_saves_object.grid(column=1, row=0, sticky=NSEW) self.frame_saves_object.grid(column=1, row=0, sticky=NSEW)
return return
def frame_devices(self): def frame_devices(self):
self.grid_rowconfigure(0, weight=1) self.grid_rowconfigure(0, weight=1)
self.grid_columnconfigure(1, 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")) try:
self.frame_devices_devices = requests.get(f'{configGet("address")}/devices', headers={"apikey": configGet("apikey")}, verify=not configGet("allow_self_signed"))
except Exception as exp:
messagebox.showerror(title="Connection error", message=f"We could not reach the server to check the devices list\n\n{exp}")
logger.error(format_exc())
self.frame_devices_object = FrameErrorConnection(self)
self.frame_devices_object.grid(column=1, row=0, sticky=NSEW)
return
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: 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 = FrameDevices(self, self.frame_devices_devices.json(), vscroll=True)
else: else:
@@ -136,14 +151,19 @@ class App(ThemedTk):
# messagebox.showerror(title="Configuration error", message="Your client is not properly configured.") # messagebox.showerror(title="Configuration error", message="Your client is not properly configured.")
return return
if self.verify_authorization() is False: self.verified = self.verify_authorization()
if self.verified in [ConnectionState.BAD, ConnectionState.UNAUTHORIZED]:
self.item_saves.state(["disabled"]) self.item_saves.state(["disabled"])
self.item_devices.state(["disabled"]) self.item_devices.state(["disabled"])
self.grid_rowconfigure(0, weight=1) self.grid_rowconfigure(0, weight=1)
self.grid_columnconfigure(1, weight=1) self.grid_columnconfigure(1, weight=1)
self.connection_error = FrameErrorConnection(self) self.connection_error = FrameErrorConnection(self)
self.connection_error.grid(column=1, row=0, rowspan=2, sticky=N+S+W+E) 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.") if self.verified == ConnectionState.UNAUTHORIZED:
messagebox.showerror(title="Authentication error", message="Your API key seems to be invalid.")
else:
messagebox.showerror(title="Connection error", message="Server is not reachable or your client configuration is incorrect. Please check your network connection and client configuration.")
return return
if self.verify_saves_dir() is False: if self.verify_saves_dir() is False:

34
src/classes/enums.py Normal file
View File

@@ -0,0 +1,34 @@
from enum import Enum
class SavesPreference(Enum):
LATEST_UPLOAD = "latest upload"
LATEST_PROGRESS = "latest progress"
class SavesPreferenceButton(Enum):
LATEST_UPLOAD = "Latest upload "
LATEST_PROGRESS = "Latest progress "
class Theme(Enum):
AUTO = "auto"
LIGHT = "light"
DARK = "dark"
class ThemeButton(Enum):
AUTO = "Auto "
LIGHT = "Light "
DARK = "Dark "
class ConnectionState(Enum):
OK = "ok"
BAD = "bad"
UNAUTHORIZED = "unauthorized"
class SaveType(Enum):
LOCAL = "local"
REMOTE = "remote"
BOTH = "both"
class SaveState(Enum):
OUTDATED = "outdated"
RECENT = "recent"
CURRENT = "current"

View File

@@ -11,13 +11,13 @@ from modules.utils import configGet, get_string_mode
def try_connecting(master: Any): def try_connecting(master: Any):
try: try:
if requests.get(configGet("address")+"/apikey", headers={"apikey": configGet("apikey")}, verify=not configGet("allow_self_signed")).status_code != 200: if requests.get(configGet("address")+"/apikey", headers={"apikey": configGet("apikey")}, verify=not configGet("allow_self_signed")).status_code == 403:
messagebox.showerror(title="Authentication error", message="Your API key seems to be invalid.") messagebox.showerror(title="Authentication error", message="Your API key seems to be invalid.")
return return
# messagebox.showinfo(title="Connection succeeded", message="Server is reachable, apikey is valid, so your client is now ready to be used!") # messagebox.showinfo(title="Connection succeeded", message="Server is reachable, apikey is valid, so your client is now ready to be used!")
master.destroy_everything() master.destroy_everything()
except: except:
messagebox.showerror(title="Configuration error", message="Your client configuration is incorrect.") messagebox.showerror(title="Connection error", message="Server is not reachable or your client configuration is incorrect. Please check your network connection and client configuration.")
# master.destroy_everything() # master.destroy_everything()

124
src/classes/frames/save.py Normal file
View File

@@ -0,0 +1,124 @@
from datetime import datetime, timedelta, timezone
from functools import partial
from os import makedirs, path, remove
from tkinter import LEFT, NSEW, Misc, S, W, ttk
from urllib.parse import urlencode
from zipfile import ZipFile
import requests
from classes.custom.themed_frame import ThemedFrame
from classes.enums import SaveState, SaveType
from modules.utils import configGet, osname
class FrameSave(ThemedFrame):
def __init__(self, master: Misc, save_dict: dict, **kwargs) -> None:
super().__init__(master, style="Card.TFrame", **kwargs)
self.widget_width = 47 if osname == "nt" else 42
self.grid_columnconfigure(0, weight=1)
self.grid_columnconfigure(1, weight=3)
self.grid_columnconfigure(2, weight=3)
self.save_dict = save_dict
self.title = ttk.Label(self, text=f'{save_dict["data"]["farmer"]} ({save_dict["type"].value.upper()}, {save_dict["state"].value.upper()})', font=("SunValleyBodyStrongFont", 12, "bold"), justify=LEFT, width=self.widget_width)
self.title.grid(column=0, row=0, padx=9, pady=9, sticky=W)
self.description = ttk.Label(self, text=f'{self.convert_date(year=save_dict["data"]["year"], season=save_dict["data"]["season"], day=save_dict["data"]["day"])}\n{save_dict["data"]["money"]} Gold, {int((save_dict["data"]["played"]/(1000*60*60))%24)} hours played\nGame version: {save_dict["data"]["game_version"]}', width=self.widget_width)
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)
if save_dict["date"] != None:
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=self.widget_width)
else:
self.last_upload = ttk.Label(self.buttons, text=f'Exists only locally', font=("SunValleyBodyFont", 8), justify=LEFT, width=self.widget_width)
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_synchronize_action = partial(self.nothing)
if save_dict["type"] is SaveType.LOCAL:
if save_dict["state"] is SaveState.RECENT:
self.button_synchronize_action = partial(self.upload, save_dict["id"])
print(f'{save_dict["id"]}, local, recent')
elif save_dict["type"] is SaveType.REMOTE:
if save_dict["state"] is SaveState.RECENT:
self.button_synchronize_action = partial(self.download, save_dict["id"], save_dict["date"])
print(f'{save_dict["id"]}, remote, recent')
else:
if save_dict["state"] is SaveState.OUTDATED:
self.button_synchronize_action = partial(self.download, save_dict["id"], save_dict["date"])
print(f'{save_dict["id"]}, both, outdated')
elif save_dict["state"] is SaveState.RECENT:
self.button_synchronize_action = partial(self.upload, save_dict["id"])
print(f'{save_dict["id"]}, both, recent')
self.button_synchronize = ttk.Button(self.buttons, text="Synchronize", style="Accent.TButton", width=11, command=self.button_synchronize_action)
self.button_synchronize.grid(column=1, row=0, sticky=W)
if save_dict["state"] is SaveState.CURRENT:
self.button_synchronize.state(["disabled"])
def convert_date(self, year: int, season: int, day: int) -> str:
if season == 0:
season_name = "Spring"
elif season == 1:
season_name = "Summer"
elif season == 2:
season_name = "Fall"
else:
season_name = "Winter"
return "Day {0} of {1}, Year {2}".format(day, season_name, year)
def convert_playtime(self, seconds: int) -> str:
return ""
def upload(self, id: int):
print(f"Upload pressed for {id}")
files = [("files", open(path.join(configGet("saves_location"), f'{self.save_dict["data"]["farmer"]}_{self.save_dict["id"]}', f'{self.save_dict["data"]["farmer"]}_{self.save_dict["id"]}'), "rb")), ("files", open(path.join(configGet("saves_location"), f'{self.save_dict["data"]["farmer"]}_{self.save_dict["id"]}', "SaveGameInfo"), "rb"))]
response = requests.post(f'{configGet("address")}/saves?{urlencode({"device": configGet("name")})}', files=files, headers={"apikey": configGet("apikey")}, verify=not configGet("allow_self_signed"))
print(response.status_code)
self.master.render_saves()
def download(self, id: int, date: int):
print(f"Download pressed for {id}")
response = requests.get(f'{configGet("address")}/saves/{id}/{date}/download', headers={"apikey": configGet("apikey")}, verify=not configGet("allow_self_signed"))
makedirs("tmp", exist_ok=True)
with open(path.join("tmp", f"{id}.svsave"), "wb") as file:
file.write(response.content)
makedirs(path.join(configGet("saves_location"), f'{self.save_dict["data"]["farmer"]}_{self.save_dict["id"]}'), exist_ok=True)
with ZipFile(path.join("tmp", f"{id}.svsave"), "r") as file:
file.extractall(path.join(configGet("saves_location"), f'{self.save_dict["data"]["farmer"]}_{self.save_dict["id"]}'))
remove(path.join("tmp", f"{id}.svsave"))
print(response.status_code)
self.master.render_saves()
def nothing(self):
pass

173
src/classes/frames/saves.py Normal file
View File

@@ -0,0 +1,173 @@
from os import path, walk
from tkinter import E, NSEW, W, ttk
from traceback import print_exc
import xmltodict
import requests
from ttkthemes import ThemedTk
from classes.custom.scrollable_frame import ScrollableFrame
from classes.custom.themed_frame import ThemedFrame
from classes.enums import SaveState, SaveType
from classes.frames.save import FrameSave
from modules.utils import configGet
class FrameSaves(ScrollableFrame):
def __init__(self, master: ThemedTk, **kwargs) -> None:
super().__init__(master, **kwargs)
master.title("Saves - Stardew Sync")
self.saves = self.fetch_saves()
# self["borderwidth"] = 1
# self["relief"] = "solid"
master.columnconfigure(1, weight=1)
if len(self.saves) == 0:
self.grid_columnconfigure(0, weight=1)
self.grid_rowconfigure(0, weight=2)
master.columnconfigure(1, weight=1)
self.label = ttk.Label(self, text="No saves found")
self.label.grid(column=0, row=0, padx=9, pady=9)
else:
self.render_saves(refetch=False)
def fetch_saves_remote(self) -> list:
response = self.frame_saves_saves = requests.get(f'{configGet("address")}/saves?only_ids=True&sort={(configGet("prefer_saves").split())[1]}', headers={"apikey": configGet("apikey")}, verify=not configGet("allow_self_signed"))
return response.json() if response.status_code == 200 else []
def fetch_saves(self) -> list:
self.saves = self.fetch_saves_remote()
for index, entry in enumerate(self.saves):
self.saves[index]["state"] = SaveState.RECENT
self.saves[index]["type"] = SaveType.REMOTE
for subdir, dirs, files in walk(configGet("saves_location")):
try:
for dir in dirs:
print("Processing", dir)
with open(path.join(configGet("saves_location"), dir, "SaveGameInfo"), "r", encoding="utf-8") as file:
save_info_dict = xmltodict.parse(file.read())
with open(path.join(configGet("saves_location"), dir, dir), "r", encoding="utf-8") as file:
save_dict = xmltodict.parse(file.read())
save_date = None
save_type = SaveType.LOCAL
save_state = SaveState.RECENT
use_remote = False
for index, entry in enumerate(self.saves):
if entry["id"] == int(save_dict["SaveGame"]["uniqueIDForThisGame"]):
print(f'Remote: {entry["data"]["save_time"]}; Local: {int(save_info_dict["Farmer"]["saveTime"])}')
if int(save_info_dict["Farmer"]["saveTime"]) > entry["data"]["save_time"]:
self.saves[index]["type"] = SaveType.BOTH
save_type = SaveType.BOTH
save_state = SaveState.RECENT
save_date = entry["date"]
elif int(save_info_dict["Farmer"]["saveTime"]) < entry["data"]["save_time"]:
self.saves[index]["type"] = SaveType.BOTH
use_remote = True
else:
self.saves[index]["type"] = SaveType.BOTH
self.saves[index]["state"] = SaveState.CURRENT
use_remote = True
if use_remote is True:
continue
if save_state == SaveState.RECENT:
for index, entry in enumerate(self.saves):
if entry["id"] == int(save_dict["SaveGame"]["uniqueIDForThisGame"]):
del self.saves[index]
break
self.saves.append(
{
"id": int(save_dict["SaveGame"]["uniqueIDForThisGame"]),
"user": None,
"device": configGet("name"),
"date": save_date,
"type": save_type,
"state": SaveState.RECENT,
"data": {
"farmer": save_info_dict["Farmer"]["name"],
"money": int(save_info_dict["Farmer"]["money"]),
"played": int(save_info_dict["Farmer"]["millisecondsPlayed"]),
"save_time": int(save_info_dict["Farmer"]["saveTime"]),
"year": int(save_info_dict["Farmer"]["yearForSaveGame"]),
"season": int(save_info_dict["Farmer"]["seasonForSaveGame"]),
"day": int(save_info_dict["Farmer"]["dayOfMonthForSaveGame"]),
"game_version": save_info_dict["Farmer"]["gameVersion"]
},
"file": {
"name": None,
"uuid": None,
"path": None
}
}
)
except:
print_exc()
return self.saves
def render_saves(self, refetch: bool = True):
if refetch is True:
self.saves = self.fetch_saves()
for widget in self.winfo_children():
widget.destroy()
i = 0
for save in self.saves:
# print(save)
save_frame = FrameSave(self, save_dict=save)
save_frame.grid(column=0, row=i, pady=9, padx=9, sticky=W+E)
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:
# super().__init__(master, **kwargs)
# master.title("Saves - Stardew Sync")
# self.grid_columnconfigure(0, weight=1)
# self.grid_rowconfigure(0, weight=2)
# master.columnconfigure(1, weight=1)
# self.label = ttk.Label(self, text="No saves found")
# self.label.grid(column=0, row=0, padx=9, pady=9)

View File

@@ -4,6 +4,7 @@ from tkinter import N, NSEW, S, W, E, END, IntVar, StringVar, filedialog, messag
from ttkthemes import ThemedTk from ttkthemes import ThemedTk
from classes.custom.themed_frame import ThemedFrame from classes.custom.themed_frame import ThemedFrame
from classes.enums import SavesPreference, SavesPreferenceButton, Theme
from modules.theme_titlebar import theme_title_bar from modules.theme_titlebar import theme_title_bar
from modules.utils import configGet, configSet, get_string_mode, use_dark_mode from modules.utils import configGet, configSet, get_string_mode, use_dark_mode
@@ -92,10 +93,10 @@ class FrameSettings(ThemedFrame):
self.saves_preference_label = ttk.Label(self, text="Saves preference:") self.saves_preference_label = ttk.Label(self, text="Saves preference:")
self.saves_preference_label.grid(column=0, row=4, sticky=W, padx=9, pady=9) self.saves_preference_label.grid(column=0, row=4, sticky=W, padx=9, pady=9)
self.default_preference = "Latest upload " if configGet("prefer_saves") == "latest upload" else "Latest progress " self.default_preference = SavesPreferenceButton.LATEST_UPLOAD.value if configGet("prefer_saves") == SavesPreference.LATEST_UPLOAD.value else SavesPreferenceButton.LATEST_PROGRESS.value
self.chosen_preference = StringVar() self.chosen_preference = StringVar()
self.preferences = ("Latest upload ", "Latest progress ") self.preferences = (SavesPreferenceButton.LATEST_UPLOAD.value, SavesPreferenceButton.LATEST_PROGRESS.value)
self.saves_preference_button = ttk.OptionMenu(self, self.chosen_preference, self.default_preference, *self.preferences, direction="below") self.saves_preference_button = ttk.OptionMenu(self, self.chosen_preference, self.default_preference, *self.preferences, direction="below")
self.saves_preference_button.grid(column=1, row=4, sticky=W, padx=9, pady=9) self.saves_preference_button.grid(column=1, row=4, sticky=W, padx=9, pady=9)
# ================ # ================
@@ -127,10 +128,13 @@ class FrameSettings(ThemedFrame):
def change_theme(self, *args): def change_theme(self, *args):
if self.chosen_theme.get().strip().lower() == "auto": if self.chosen_theme.get().strip().lower() == Theme.AUTO.value:
self.chosen_theme_real = "dark" if use_dark_mode(no_config=True) is True else "light" self.chosen_theme_real = Theme.DARK if use_dark_mode(no_config=True) is True else Theme.LIGHT
else: else:
self.chosen_theme_real = self.chosen_theme.get().strip().lower() if self.chosen_theme.get().strip().lower() == Theme.LIGHT.value:
self.chosen_theme_real = Theme.LIGHT
else:
self.chosen_theme_real = Theme.DARK
theme_title_bar(self.master, self.chosen_theme_real) theme_title_bar(self.master, self.chosen_theme_real)
def select_location(self, entry: ttk.Entry): def select_location(self, entry: ttk.Entry):

View File

@@ -9,6 +9,7 @@ from classes.custom.image_label import ImageLabel
from classes.custom.themed_frame import ThemedFrame from classes.custom.themed_frame import ThemedFrame
from classes.custom.themed_toplevel import ThemedToplevel from classes.custom.themed_toplevel import ThemedToplevel
from classes.enums import Theme
from modules.logger import logger from modules.logger import logger
from modules.theme_titlebar import theme_title_bar from modules.theme_titlebar import theme_title_bar
from modules.utils import configGet, configSet, resize_window, set_icon, use_dark_mode from modules.utils import configGet, configSet, resize_window, set_icon, use_dark_mode
@@ -31,7 +32,7 @@ class ToplevelWelcome(ThemedToplevel):
sv_ttk.init_theme(self) sv_ttk.init_theme(self)
if use_dark_mode(): if use_dark_mode():
theme_title_bar(self, mode="dark") theme_title_bar(self, mode=Theme.DARK)
self.update() self.update()
set_icon(self) set_icon(self)
@@ -233,12 +234,13 @@ class ToplevelWelcome(ThemedToplevel):
def stage_location_select_location(self, entry: ttk.Entry): def stage_location_select_location(self, entry: ttk.Entry):
if path.exists(path.join(str(getenv("APPDATA")), "Stardew Valley")): self.path_start = None
self.path_start = path.join(str(getenv("APPDATA")), "Stardew Valley")
elif path.exists(path.join(path.expanduser("~"), ".config", "Stardew Valley")): for guess in [ [str(getenv("APPDATA")), "Stardew Valley"], [str(getenv("APPDATA")), "StardewValley"], [path.expanduser("~"), ".config", "Stardew Valley"], [path.expanduser("~"), ".config", "StardewValley"] ]:
self.path_start = path.join(path.expanduser("~"), ".config", "Stardew Valley") joined_path = path.join(*guess)
else: if path.exists(joined_path):
self.path_start = None self.path_start = joined_path
break
self.path_dir = filedialog.askdirectory(initialdir=self.path_start, title="Select Stardew Valley Saves folder") self.path_dir = filedialog.askdirectory(initialdir=self.path_start, title="Select Stardew Valley Saves folder")
@@ -287,20 +289,23 @@ class ToplevelWelcome(ThemedToplevel):
def change_theme(self, *args): def change_theme(self, *args):
if self.stage_option_menu_var.get().strip().lower() == "auto": if self.stage_option_menu_var.get().strip().lower() == Theme.AUTO.value:
self.stage_option_menu_var_real = "dark" if use_dark_mode(no_config=True) is True else "light" self.stage_option_menu_var_real = Theme.DARK if use_dark_mode(no_config=True) is True else Theme.LIGHT
else: else:
self.stage_option_menu_var_real = self.stage_option_menu_var.get().strip().lower() if self.stage_option_menu_var.get().strip().lower() == Theme.LIGHT.value:
self.stage_option_menu_var_real = Theme.LIGHT
else:
self.stage_option_menu_var_real = Theme.DARK
theme_title_bar(self, self.stage_option_menu_var_real) theme_title_bar(self, self.stage_option_menu_var_real)
theme_title_bar(self.master, self.stage_option_menu_var_real) theme_title_bar(self.master, self.stage_option_menu_var_real)
def stage_theme_validate(self): def stage_theme_validate(self):
if self.stage_option_menu_var.get().strip().lower() == "auto": if self.stage_option_menu_var.get().strip().lower() == Theme.AUTO.value:
configSet(["dark_mode_auto"], True) configSet(["dark_mode_auto"], True)
else: else:
configSet(["dark_mode_auto"], False) configSet(["dark_mode_auto"], False)
if self.stage_option_menu_var.get().strip().lower() == "dark": if self.stage_option_menu_var.get().strip().lower() == Theme.DARK.value:
configSet(["dark_mode"], True) configSet(["dark_mode"], True)
else: else:
configSet(["dark_mode"], False) configSet(["dark_mode"], False)

View File

@@ -6,5 +6,6 @@
"saves_location": null, "saves_location": null,
"dark_mode": false, "dark_mode": false,
"dark_mode_auto": true, "dark_mode_auto": true,
"first_run": true "first_run": true,
"prefer_saves": "latest upload"
} }

View File

@@ -3,25 +3,27 @@ import sys
import sv_ttk import sv_ttk
from distutils.version import StrictVersion as Version from distutils.version import StrictVersion as Version
from os import system from os import system
from tkinter import Tcl, Toplevel from tkinter import Misc, Tcl, Toplevel
from typing import Literal, Union from typing import Literal, Union
from ttkthemes import ThemedTk from ttkthemes import ThemedTk
from classes.enums import Theme
if sys.platform.startswith("win"): if sys.platform.startswith("win"):
from ctypes import byref, c_int, sizeof, windll from ctypes import byref, c_int, sizeof, windll
def theme_title_bar(window: Union[ThemedTk, Toplevel], mode: Literal["dark", "light"]) -> None: def theme_title_bar(window: Union[ThemedTk, Toplevel, Misc], mode: Literal[Theme.DARK, Theme.LIGHT]) -> None:
""" """
MORE INFO: MORE INFO:
https://learn.microsoft.com/en-us/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute https://learn.microsoft.com/en-us/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute
""" """
if mode == "dark": if mode.value == "dark":
value = 1 value = 1
#window.configure(background="#1c1c1c") #window.configure(background="#1c1c1c")
elif mode == "light": elif mode.value == "light":
value = 0 value = 0
#window.configure(background="#ffffff") #window.configure(background="#ffffff")
else: else:
@@ -29,7 +31,7 @@ def theme_title_bar(window: Union[ThemedTk, Toplevel], mode: Literal["dark", "li
try: try:
sv_ttk.set_theme(mode) sv_ttk.set_theme(mode.value)
window.update() window.update()