2023-01-24 16:27:07 +02:00
from os import getenv , path
import platform
from tkinter import CENTER , END , E , LEFT , NSEW , Image , IntVar , N , S , PhotoImage , StringVar , Toplevel , W , filedialog , messagebox , ttk
from urllib . parse import quote , urlencode
2023-01-22 21:06:55 +02:00
import requests
import sv_ttk
from classes . custom . image_label import ImageLabel
2023-01-23 17:38:47 +02:00
from classes . custom . themed_frame import ThemedFrame
2023-01-22 21:06:55 +02:00
from classes . custom . themed_toplevel import ThemedToplevel
2023-01-26 14:29:30 +02:00
from classes . enums import Theme
2023-01-22 21:06:55 +02:00
from modules . logger import logger
from modules . theme_titlebar import theme_title_bar
2023-01-23 17:38:47 +02:00
from modules . utils import configGet , configSet , resize_window , set_icon , use_dark_mode
2023-01-22 21:06:55 +02:00
class ToplevelWelcome ( ThemedToplevel ) :
def __init__ ( self , parent ) :
super ( ) . __init__ ( parent )
2023-01-23 17:38:47 +02:00
self . protocol ( " WM_DELETE_WINDOW " , self . __exit )
2023-01-22 21:06:55 +02:00
2023-01-23 17:38:47 +02:00
resize_window ( self , 380 , 350 )
2023-01-22 21:06:55 +02:00
self . title ( " Welcome to Stardew Sync " )
2023-01-23 17:38:47 +02:00
self . resizable ( False , False )
2023-01-22 21:06:55 +02:00
sv_ttk . init_theme ( self )
if use_dark_mode ( ) :
2023-01-26 14:29:30 +02:00
theme_title_bar ( self , mode = Theme . DARK )
2023-01-22 21:06:55 +02:00
self . update ( )
2023-01-23 13:22:54 +02:00
set_icon ( self )
2023-01-22 21:06:55 +02:00
self . focus_set ( )
self . grid_columnconfigure ( 0 , weight = 1 )
2023-01-23 17:38:47 +02:00
self . window_frame = ThemedFrame ( self )
self . window_frame . grid ( column = 0 , row = 0 , sticky = NSEW )
2023-01-24 16:27:07 +02:00
# self.window_frame["borderwidth"] = 1
# self.window_frame["relief"] = "solid"
2023-01-23 17:38:47 +02:00
self . window_frame . grid_columnconfigure ( 0 , weight = 1 )
self . welcome_pic = ImageLabel ( self . window_frame )
2023-01-22 21:06:55 +02:00
self . welcome_pic . grid ( column = 0 , row = 0 , pady = 20 )
self . welcome_pic . load ( path . join ( " assets " , " welcome.gif " ) )
2023-01-23 17:38:47 +02:00
self . welcome_text = ttk . Label ( self . window_frame , text = " Welcome to Stardew Sync " , font = ( " SunValleyBodyFont " , 14 ) )
2023-01-22 21:06:55 +02:00
self . welcome_text . grid ( column = 0 , row = 1 , pady = 10 )
2023-01-23 17:38:47 +02:00
self . welcome_subtext = ttk . Label ( self . window_frame , text = " This small open-source application will help you \n to synchronize your Stardew Valley save files \n between all your devices " , justify = CENTER )
2023-01-22 21:06:55 +02:00
self . welcome_subtext . grid ( column = 0 , row = 2 )
2023-01-23 17:38:47 +02:00
self . welcome_button = ttk . Button ( self . window_frame , text = " Begin " , style = " Accent.TButton " , width = 10 , command = self . stage_address )
2023-01-22 21:06:55 +02:00
self . welcome_button . grid ( column = 0 , row = 3 , pady = 20 )
2023-01-23 17:38:47 +02:00
def stage_address ( self ) :
for widget in self . window_frame . winfo_children ( ) :
widget . destroy ( )
self . window_frame . grid_rowconfigure ( 0 , weight = 2 )
self . window_frame . grid_rowconfigure ( 1 , weight = 2 )
#self.window_frame.grid_rowconfigure(2, weight=3)
self . stage_label = ttk . Label ( self . window_frame , text = " Connection " , font = ( " SunValleyBodyFont " , 14 ) )
2023-01-24 16:27:07 +02:00
self . stage_label . grid ( column = 0 , row = 0 , pady = 29 )
2023-01-23 17:38:47 +02:00
self . stage_label_1 = ttk . Label ( self . window_frame , text = " Server address " , justify = LEFT )
2023-01-24 16:27:07 +02:00
self . stage_label_1 . grid ( column = 0 , row = 1 , padx = 35 , pady = 5 , sticky = W )
2023-01-23 17:38:47 +02:00
self . stage_entry_1 = ttk . Entry ( self . window_frame )
2023-01-24 16:27:07 +02:00
self . stage_entry_1 . grid ( column = 0 , row = 2 , padx = 35 , pady = 5 , sticky = W + E )
2023-01-23 17:38:47 +02:00
2023-01-24 16:27:07 +02:00
self . stage_label_2 = ttk . Label ( self . window_frame , text = " Personal API key " , justify = LEFT )
self . stage_label_2 . grid ( column = 0 , row = 3 , padx = 35 , pady = 5 , sticky = W )
self . stage_entry_2 = ttk . Entry ( self . window_frame )
self . stage_entry_2 . grid ( column = 0 , row = 4 , padx = 35 , pady = 5 , sticky = W + E )
self . stage_checkbox_var = IntVar ( )
self . stage_checkbox = ttk . Checkbutton ( self . window_frame , text = " Allow self-signed certificates " , variable = self . stage_checkbox_var )
self . stage_checkbox . grid ( column = 0 , row = 5 , sticky = W , padx = 35 , pady = 9 )
self . stage_button = ttk . Button ( self . window_frame , text = " Continue " , style = " Accent.TButton " , width = 10 , command = self . stage_address_validate )
self . stage_button . grid ( column = 0 , row = 6 , pady = 28 )
def stage_address_validate ( self ) :
if len ( self . stage_entry_1 . get ( ) ) > 0 :
if self . stage_entry_1 . get ( ) . endswith ( " / " ) :
self . address_text = self . stage_entry_1 . get ( )
self . stage_entry_1 . delete ( 0 , END )
self . stage_entry_1 . insert ( 0 , self . address_text [ : - 1 ] )
try :
2023-01-25 15:01:13 +02:00
response_check = requests . get ( self . stage_entry_1 . get ( ) + " /check " , verify = not bool ( self . stage_checkbox_var . get ( ) ) )
if response_check . status_code != 200 :
logger . error ( f " Could not connect to ' { self . stage_entry_1 . get ( ) } ' because it returned { response_check . status_code } " )
messagebox . showerror ( title = " Connection error " , message = f " Server response is { response_check . status_code } . Check if your API server and it ' s reverse proxy (if there ' s one) properly configured and then try again. " )
return
2023-01-24 16:27:07 +02:00
except ( requests . exceptions . InvalidURL , requests . exceptions . InvalidSchema , requests . exceptions . MissingSchema ) as exp :
logger . error ( f " Could not validate ' { self . stage_entry_1 . get ( ) } ' due to { exp } " )
messagebox . showerror ( title = " Invalid address " , message = " Address entered does not seem to be correct one. Please provide API address starting with http:// or https:// \n \n For example: \n - https://your-api.com:8043 \n - https://your-api.com " )
return
except ( requests . exceptions . SSLError ) :
logger . error ( f " SSL certificate of ' { self . stage_entry_1 . get ( ) } ' does not seem to be valid or is self-signed " )
messagebox . showerror ( title = " Invalid SSL " , message = " SSL certificate seems to be invalid or self-signed and thus won ' t be trusted. You can overwrite this by checking ' Allow self-signed certificates ' . " )
return
except Exception as exp :
logger . error ( f " Could not reach ' { self . stage_entry_1 . get ( ) } ' due to { exp } " )
messagebox . showerror ( title = " Connection error " , message = " Address entered does not seem to be reachable. Please make sure you ' ve entered the correct API address. " )
return
response_apikey = requests . get ( self . stage_entry_1 . get ( ) + " /apikey " , headers = { " apikey " : self . stage_entry_2 . get ( ) } , verify = not bool ( self . stage_checkbox_var . get ( ) ) )
if response_apikey . status_code != 200 :
logger . error ( f " API key seems to be invalid. API returned { response_apikey . status_code } as a status code " )
messagebox . showerror ( title = " Invalid apikey " , message = " API key provided does not seem to be valid. Please check for any mistakes and if none found - take a look at the docs to learn how to generate one. " )
return
configSet ( [ " address " ] , self . stage_entry_1 . get ( ) )
configSet ( [ " apikey " ] , self . stage_entry_2 . get ( ) )
configSet ( [ " allow_self_signed " ] , bool ( self . stage_checkbox_var . get ( ) ) )
self . stage_name ( )
def stage_name ( self ) :
for widget in self . window_frame . winfo_children ( ) :
widget . destroy ( )
self . window_frame . grid_rowconfigure ( 0 , weight = 2 )
self . window_frame . grid_rowconfigure ( 1 , weight = 2 )
self . stage_label = ttk . Label ( self . window_frame , text = " Connection " , font = ( " SunValleyBodyFont " , 14 ) )
self . stage_label . grid ( column = 0 , row = 0 , pady = 29 )
self . divider_1 = ttk . Separator ( self . window_frame )
self . divider_1 . grid ( column = 0 , row = 1 , pady = 15 )
self . stage_label_1 = ttk . Label ( self . window_frame , text = " Device name " , justify = LEFT )
self . stage_label_1 . grid ( column = 0 , row = 2 , padx = 35 , pady = 5 , sticky = W )
2023-01-23 17:38:47 +02:00
self . stage_entry_1 = ttk . Entry ( self . window_frame )
2023-01-24 16:27:07 +02:00
self . stage_entry_1 . grid ( column = 0 , row = 3 , padx = 35 , pady = 5 , sticky = W + E )
self . stage_entry_1 . insert ( 0 , str ( platform . node ( ) ) )
self . divider_2 = ttk . Separator ( self . window_frame )
self . divider_2 . grid ( column = 0 , row = 4 , pady = 40 )
self . stage_button = ttk . Button ( self . window_frame , text = " Continue " , style = " Accent.TButton " , width = 10 , command = self . stage_name_validate )
self . stage_button . grid ( column = 0 , row = 5 , pady = 28 )
def stage_name_validate ( self ) :
if self . stage_entry_1 . get ( ) . strip ( ) == " " :
logger . error ( f " Name { self . stage_entry_1 . get ( ) . strip ( ) } is not a valid name " )
messagebox . showerror ( title = " Name error " , message = " Provided device name is not valid. Please provide a valid one. " )
return
try :
quote ( self . stage_entry_1 . get ( ) . strip ( ) )
except :
logger . error ( f " Name { self . stage_entry_1 . get ( ) . strip ( ) } is not a valid name " )
messagebox . showerror ( title = " Name error " , message = " Provided device name is not valid. Please provide a valid one. " )
return
if self . stage_entry_1 . get ( ) . strip ( ) == configGet ( " name " ) :
existing_device = requests . get ( f ' { configGet ( " address " ) } ' , headers = { " apikey " : configGet ( " apikey " ) } , verify = not configGet ( " allow_self_signed " ) )
if existing_device . status_code != 200 :
requests . post ( f ' { configGet ( " address " ) } /devices? { urlencode ( { " name " : quote ( configGet ( " name " ) . encode ( " utf-8 " ) ) , " os " : platform . system ( ) + " " + platform . release ( ) , " client " : f " SyncTk { self . master . __version__ } " } ) } ' , headers = { " apikey " : configGet ( " apikey " ) } , verify = not configGet ( " allow_self_signed " ) )
else :
if configGet ( " name " ) is not None :
existing_device_before = requests . get ( f ' { configGet ( " address " ) } ' , headers = { " apikey " : configGet ( " apikey " ) } , verify = not configGet ( " allow_self_signed " ) )
if existing_device_before . status_code == 200 :
requests . patch ( f ' { configGet ( " address " ) } ? { urlencode ( { " new_name " : quote ( self . stage_entry_1 . get ( ) . strip ( ) . encode ( " utf-8 " ) ) , " os " : platform . system ( ) + " " + platform . release ( ) , " client " : f " SyncTk { self . master . __version__ } " } ) } ' , headers = { " apikey " : configGet ( " apikey " ) } , verify = not configGet ( " allow_self_signed " ) )
else :
device_created = requests . post ( f ' { configGet ( " address " ) } /devices? { urlencode ( { " name " : quote ( self . stage_entry_1 . get ( ) . strip ( ) . encode ( " utf-8 " ) ) , " os " : platform . system ( ) + " " + platform . release ( ) , " client " : f " SyncTk { self . master . __version__ } " } ) } ' , headers = { " apikey " : configGet ( " apikey " ) } , verify = not configGet ( " allow_self_signed " ) )
if device_created . status_code != 204 :
messagebox . showerror ( title = " Name error " , message = f " Could not register device in database using name ' { self . stage_entry_1 . get ( ) . strip ( ) } ' with error: \n \n { device_created . json ( ) } " )
return
configSet ( [ " name " ] , self . stage_entry_1 . get ( ) . strip ( ) )
self . stage_location ( )
def stage_location ( self ) :
for widget in self . window_frame . winfo_children ( ) :
widget . destroy ( )
self . window_frame . grid_rowconfigure ( 0 , weight = 2 )
self . window_frame . grid_rowconfigure ( 1 , weight = 2 )
self . stage_label = ttk . Label ( self . window_frame , text = " Local saves " , font = ( " SunValleyBodyFont " , 14 ) )
self . stage_label . grid ( column = 0 , row = 0 , pady = 29 )
self . divider_1 = ttk . Separator ( self . window_frame )
self . divider_1 . grid ( column = 0 , row = 1 , pady = 15 )
self . stage_label_1 = ttk . Label ( self . window_frame , text = " Save files location " , justify = LEFT )
self . stage_label_1 . grid ( column = 0 , row = 2 , padx = 35 , pady = 5 , sticky = W )
self . stage_frame = ThemedFrame ( self . window_frame )
self . stage_frame . grid ( column = 0 , row = 3 , padx = 35 , pady = 5 , sticky = W + E )
self . stage_frame . grid_columnconfigure ( 0 , weight = 2 )
# self.saves_frame.grid_columnconfigure(1, weight=3)
self . stage_entry_1 = ttk . Entry ( self . stage_frame )
self . stage_entry_1 . grid ( column = 0 , row = 0 , sticky = W + E )
self . divider_2 = ttk . Separator ( self . stage_frame , orient = " vertical " )
self . divider_2 . grid ( column = 1 , row = 0 , padx = 3 )
self . saves_location_button = ttk . Button ( self . stage_frame , text = " Browse " , width = 6 , command = lambda : self . stage_location_select_location ( self . stage_entry_1 ) )
self . saves_location_button . grid ( column = 2 , row = 0 , sticky = E )
self . divider_3 = ttk . Separator ( self . window_frame )
self . divider_3 . grid ( column = 0 , row = 4 , pady = 40 )
self . stage_button = ttk . Button ( self . window_frame , text = " Continue " , style = " Accent.TButton " , width = 10 , command = self . stage_location_validate )
self . stage_button . grid ( column = 0 , row = 5 , pady = 26 )
def stage_location_select_location ( self , entry : ttk . Entry ) :
2023-01-27 13:17:33 +02:00
self . path_start = None
for guess in [ [ str ( getenv ( " APPDATA " ) ) , " Stardew Valley " ] , [ str ( getenv ( " APPDATA " ) ) , " StardewValley " ] , [ path . expanduser ( " ~ " ) , " .config " , " Stardew Valley " ] , [ path . expanduser ( " ~ " ) , " .config " , " StardewValley " ] ] :
joined_path = path . join ( * guess )
if path . exists ( joined_path ) :
self . path_start = joined_path
break
2023-01-24 16:27:07 +02:00
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 )
2023-01-23 17:38:47 +02:00
2023-01-24 16:27:07 +02:00
def stage_location_validate ( self ) :
if not path . exists ( self . stage_entry_1 . get ( ) ) :
logger . error ( f " Path { self . stage_entry_1 . get ( ) } does not seem to exist " )
messagebox . showerror ( title = " Location error " , message = " Saves folder seems to be invalid. Please provide a valid directory path where Stardew Valley ' s save files (and folders) are stored. " )
return
configSet ( [ " saves_location " ] , self . stage_entry_1 . get ( ) )
self . stage_theme ( )
def stage_theme ( self ) :
for widget in self . window_frame . winfo_children ( ) :
widget . destroy ( )
self . window_frame . grid_rowconfigure ( 0 , weight = 2 )
self . window_frame . grid_rowconfigure ( 1 , weight = 2 )
self . stage_label = ttk . Label ( self . window_frame , text = " Almost there " , font = ( " SunValleyBodyFont " , 14 ) )
self . stage_label . grid ( column = 0 , row = 0 , pady = 29 )
self . divider_1 = ttk . Separator ( self . window_frame )
self . divider_1 . grid ( column = 0 , row = 1 , pady = 15 )
self . stage_label_1 = ttk . Label ( self . window_frame , text = " Theme " , justify = CENTER )
self . stage_label_1 . grid ( column = 0 , row = 2 , padx = 35 , pady = 5 )
self . stage_option_menu_var = StringVar ( )
self . themes = ( " Auto " , " Light " , " Dark " )
self . stage_option_menu = ttk . OptionMenu ( self . window_frame , self . stage_option_menu_var , " Auto " , * self . themes , direction = " below " , command = self . change_theme )
self . stage_option_menu . grid ( column = 0 , row = 3 , padx = 35 , pady = 5 )
self . divider_2 = ttk . Separator ( self . window_frame )
self . divider_2 . grid ( column = 0 , row = 4 , pady = 40 )
self . stage_button = ttk . Button ( self . window_frame , text = " Continue " , style = " Accent.TButton " , width = 10 , command = self . stage_theme_validate )
self . stage_button . grid ( column = 0 , row = 5 , pady = 28 )
def change_theme ( self , * args ) :
2023-01-26 14:29:30 +02:00
if self . stage_option_menu_var . get ( ) . strip ( ) . lower ( ) == Theme . AUTO . value :
self . stage_option_menu_var_real = Theme . DARK if use_dark_mode ( no_config = True ) is True else Theme . LIGHT
2023-01-24 16:27:07 +02:00
else :
2023-01-26 14:29:30 +02:00
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
2023-01-24 16:27:07 +02:00
theme_title_bar ( self , self . stage_option_menu_var_real )
theme_title_bar ( self . master , self . stage_option_menu_var_real )
def stage_theme_validate ( self ) :
2023-01-26 14:29:30 +02:00
if self . stage_option_menu_var . get ( ) . strip ( ) . lower ( ) == Theme . AUTO . value :
2023-01-24 16:27:07 +02:00
configSet ( [ " dark_mode_auto " ] , True )
else :
configSet ( [ " dark_mode_auto " ] , False )
2023-01-26 14:29:30 +02:00
if self . stage_option_menu_var . get ( ) . strip ( ) . lower ( ) == Theme . DARK . value :
2023-01-24 16:27:07 +02:00
configSet ( [ " dark_mode " ] , True )
else :
configSet ( [ " dark_mode " ] , False )
self . stage_mobile_app ( )
def stage_mobile_app ( self ) :
# To Do
self . stage_completed ( )
def stage_completed ( self ) :
for widget in self . window_frame . winfo_children ( ) :
widget . destroy ( )
self . window_frame . grid_columnconfigure ( 0 , weight = 1 )
self . welcome_pic = ImageLabel ( self . window_frame )
self . welcome_pic . grid ( column = 0 , row = 0 , pady = 20 )
self . welcome_pic . load ( path . join ( " assets " , " welcome.gif " ) )
self . welcome_text = ttk . Label ( self . window_frame , text = " You ' re all set! " , font = ( " SunValleyBodyFont " , 14 ) )
self . welcome_text . grid ( column = 0 , row = 1 , pady = 10 )
self . welcome_subtext = ttk . Label ( self . window_frame , text = " Configuration step is completed. \n You can now jump into app and enjoy your \n save files synchronization " , justify = CENTER )
self . welcome_subtext . grid ( column = 0 , row = 2 )
self . welcome_button = ttk . Button ( self . window_frame , text = " Finish " , style = " Accent.TButton " , width = 10 , command = self . acknowledged )
self . welcome_button . grid ( column = 0 , row = 3 , pady = 20 )
2023-01-23 17:38:47 +02:00
2023-01-22 21:06:55 +02:00
def acknowledged ( self ) :
configSet ( [ " first_run " ] , False )
2023-01-23 17:38:47 +02:00
self . destroy ( )
def __exit ( self ) :
decision = messagebox . askyesno ( title = " Exit confirmation " , message = " Are you sure? If you exit now, your app will remain unconfigured. " )
if decision is True :
self . quit ( )