33import subprocess
44import threading
55import tkinter as tk
6- from tkinter import ttk , messagebox
7- import requests
6+ from tkinter import messagebox
7+ import sys
8+ import time
9+
10+ try :
11+ import requests
12+ import ttkbootstrap as ttk
13+ from ttkbootstrap .constants import *
14+ except ImportError :
15+ # Just in case dependencies aren't installed yet, fail gracefully or fallback
16+ import tkinter .ttk as ttk
17+ from tkinter import TOP , BOTTOM , LEFT , RIGHT , BOTH , X , Y , END
818
919# Constants
1020LUA_FILES_DIR = r"d:\antigravity\luapatcher\All Games Files"
@@ -15,191 +25,229 @@ class SteamPatcherApp:
1525 def __init__ (self , root ):
1626 self .root = root
1727 self .root .title ("Steam Lua Patcher" )
18- self .root .geometry ("600x500" )
19-
20- # Style
21- style = ttk .Style ()
22- style .theme_use ('clam' )
28+ self .root .geometry ("800x600" )
2329
2430 # UI Elements
2531 self .create_widgets ()
2632
2733 # Data
2834 self .search_results = []
35+ self .debounce_timer = None
36+ self .current_search_id = 0
2937
3038 def create_widgets (self ):
31- # Search Frame
32- search_frame = ttk .LabelFrame (self .root , text = "Search Game" , padding = (10 , 10 ))
33- search_frame .pack (fill = "x" , padx = 10 , pady = 5 )
39+ # Main Container with padding
40+ main_container = ttk .Frame (self .root , padding = 20 )
41+ main_container .pack (fill = BOTH , expand = True )
42+
43+ # Header
44+ header_frame = ttk .Frame (main_container )
45+ header_frame .pack (fill = X , pady = (0 , 20 ))
46+
47+ title_lbl = ttk .Label (header_frame , text = "Steam Lua Patcher" , font = ("Helvetica" , 24 , "bold" ), bootstyle = "primary" )
48+ title_lbl .pack (side = LEFT )
49+
50+ # Search Section
51+ search_frame = ttk .Labelframe (main_container , text = "Game Search" , padding = 15 , bootstyle = "info" )
52+ search_frame .pack (fill = X , pady = (0 , 20 ))
3453
3554 self .search_var = tk .StringVar ()
3655 self .search_var .trace_add ("write" , self .on_search_change )
37- self .search_entry = ttk .Entry (search_frame , textvariable = self .search_var )
38- self .search_entry .pack (side = "left" , fill = "x" , expand = True , padx = 5 )
39- # self.search_entry.bind("<Return>", lambda e: self.start_search()) # Enter key still works but not strictly needed with auto-search
4056
41- search_btn = ttk .Button (search_frame , text = "Search" , command = self . start_search )
42- search_btn .pack (side = "right" , padx = 5 )
57+ entry_frame = ttk .Frame (search_frame )
58+ entry_frame .pack (fill = X )
4359
44- # Debounce timer
45- self .debounce_timer = None
60+ search_icon_lbl = ttk .Label (entry_frame , text = "🔍" , font = ("Segoe UI Emoji" , 12 ))
61+ search_icon_lbl .pack (side = LEFT , padx = (0 , 10 ))
62+
63+ self .search_entry = ttk .Entry (entry_frame , textvariable = self .search_var , font = ("Helvetica" , 11 ))
64+ self .search_entry .pack (side = LEFT , fill = X , expand = True )
65+ self .search_entry .focus_set ()
4666
47- # Results Frame
48- results_frame = ttk .LabelFrame ( self . root , text = "Results" , padding = ( 10 , 10 ) )
49- results_frame .pack (fill = "both" , expand = True , padx = 10 , pady = 5 )
67+ # Results Section
68+ results_frame = ttk .Labelframe ( main_container , text = "Search Results" , padding = 15 , bootstyle = "default" )
69+ results_frame .pack (fill = BOTH , expand = True , pady = ( 0 , 20 ) )
5070
5171 columns = ("name" , "appid" , "status" )
52- self .tree = ttk .Treeview (results_frame , columns = columns , show = "headings" , selectmode = "browse" )
72+ self .tree = ttk .Treeview (results_frame , columns = columns , show = "headings" , selectmode = "browse" , bootstyle = "info" )
73+
5374 self .tree .heading ("name" , text = "Game Name" )
5475 self .tree .heading ("appid" , text = "App ID" )
55- self .tree .heading ("status" , text = "Lua File Status" )
56- self . tree . column ( "name" , width = 300 )
57- self .tree .column ("appid " , width = 100 )
58- self .tree .column ("status " , width = 120 )
59- self .tree .pack ( fill = "both " , expand = True , side = "left " )
76+ self .tree .heading ("status" , text = "Lua Patch Status" )
77+
78+ self .tree .column ("name " , width = 400 , anchor = "w" )
79+ self .tree .column ("appid " , width = 100 , anchor = "center" )
80+ self .tree .column ( "status " , width = 150 , anchor = "center " )
6081
82+ # Scrollbar
6183 scrollbar = ttk .Scrollbar (results_frame , orient = "vertical" , command = self .tree .yview )
62- scrollbar .pack (side = "right" , fill = "y" )
84+ scrollbar .pack (side = RIGHT , fill = Y )
6385 self .tree .configure (yscrollcommand = scrollbar .set )
6486
65- # self.tree.bind("<<TreeviewSelect>>", self.on_select)
87+ self .tree .pack (fill = BOTH , expand = True , side = LEFT )
88+ self .tree .bind ("<<TreeviewSelect>>" , self .on_select )
6689
67- # Actions Frame
68- actions_frame = ttk .Frame (self . root , padding = ( 10 , 10 ) )
69- actions_frame .pack (fill = "x" , padx = 10 , pady = 5 )
90+ # Actions Section
91+ actions_frame = ttk .Frame (main_container )
92+ actions_frame .pack (fill = X )
7093
71- self .patch_btn = ttk .Button (actions_frame , text = "Patch (Copy Lua) " , command = self .patch_selected )
72- self .patch_btn .pack (side = "left" , padx = 5 )
94+ self .patch_btn = ttk .Button (actions_frame , text = "Patch Selected Game " , command = self .patch_selected , state = "disabled" , bootstyle = "success-outline" , width = 25 )
95+ self .patch_btn .pack (side = LEFT , padx = ( 0 , 10 ) )
7396
74- self .restart_btn = ttk .Button (actions_frame , text = "Restart Steam" , command = self .restart_steam )
75- self .restart_btn .pack (side = "right" , padx = 5 )
76-
77- # Status Bar
78- self .status_var = tk .StringVar (value = "Ready" )
79- status_bar = ttk .Label (self .root , textvariable = self .status_var , relief = "sunken" , anchor = "w" )
80- status_bar .pack (fill = "x" , side = "bottom" )
97+ self .restart_btn = ttk .Button (actions_frame , text = "Restart Steam" , command = self .restart_steam , bootstyle = "danger-outline" , width = 20 )
98+ self .restart_btn .pack (side = RIGHT )
99+
100+ # Footer / Status
101+ self .status_var = tk .StringVar (value = "Ready to search " )
102+ status_lbl = ttk .Label (self .root , textvariable = self .status_var , relief = "sunken" , anchor = "w" , padding = ( 10 , 5 ), bootstyle = "secondary-inverse " )
103+ status_lbl .pack (fill = X , side = BOTTOM )
81104
82105 def on_search_change (self , * args ):
83106 if self .debounce_timer :
84107 self .root .after_cancel (self .debounce_timer )
85- self .debounce_timer = self .root .after (600 , self .start_search )
108+ self .debounce_timer = self .root .after (400 , self .start_search ) # Reduced debounce to 400ms
86109
87110 def start_search (self ):
88111 query = self .search_var .get ().strip ()
89112 if not query :
90113 return
91114
92- # Debounce cleanup if called manually
93115 if self .debounce_timer :
94116 self .root .after_cancel (self .debounce_timer )
95117 self .debounce_timer = None
118+
119+ self .status_var .set (f"Searching for '{ query } '..." )
96120
97- self .patch_btn .config (state = "disabled" )
98- self .status_var .set ("Searching..." )
99- self .tree .delete (* self .tree .get_children ())
121+ # Increment search ID to handle race conditions
122+ self .current_search_id += 1
123+ search_id = self .current_search_id
124+
125+ # Clear previous results immediately if new search starts?
126+ # Optional: keeping old results until new ones arrive looks smoother.
127+ # self.tree.delete(*self.tree.get_children())
100128
101- # Run in thread to not freeze UI
102- threading .Thread (target = self .search_logic , args = (query ,), daemon = True ).start ()
129+ threading .Thread (target = self .search_logic , args = (query , search_id ), daemon = True ).start ()
103130
104- def search_logic (self , query ):
131+ def search_logic (self , query , search_id ):
105132 try :
106133 url = "https://store.steampowered.com/api/storesearch"
107134 params = {
108135 "term" : query ,
109136 "l" : "english" ,
110137 "cc" : "US"
111138 }
112- response = requests .get (url , params = params )
139+ response = requests .get (url , params = params , timeout = 10 )
113140 response .raise_for_status ()
114141 data = response .json ()
115142
116143 items = data .get ("items" , [])
117- self .root .after (0 , self .update_results , items )
118144
145+ # Check if this thread is still relevant
146+ if search_id == self .current_search_id :
147+ self .root .after (0 , self .update_results , items )
148+ else :
149+ print (f"Ignoring stale search result (ID: { search_id } )" )
150+
151+ except requests .RequestException as e :
152+ if search_id == self .current_search_id :
153+ self .root .after (0 , lambda : self .status_var .set (f"Network Error: { e } " ))
119154 except Exception as e :
120- self .root .after (0 , lambda : self .status_var .set (f"Error: { e } " ))
155+ if search_id == self .current_search_id :
156+ self .root .after (0 , lambda : self .status_var .set (f"Error: { e } " ))
121157
122158 def update_results (self , items ):
159+ self .tree .delete (* self .tree .get_children ())
123160 self .search_results = items
161+
162+ if not items :
163+ self .status_var .set ("No results found." )
164+ return
165+
124166 for item in items :
125167 name = item .get ("name" )
126168 appid = item .get ("id" )
127169
128170 # Check if lua file exists
129171 lua_path = os .path .join (LUA_FILES_DIR , f"{ appid } .lua" )
130- status = "Found" if os .path .exists (lua_path ) else "Not Found"
131-
132- self .tree .insert ("" , "end" , values = (name , appid , status ))
172+ exists = os .path .exists (lua_path )
173+ status = "AVAILABLE" if exists else "Missing"
133174
175+ # Insert with tags for coloring
176+ # We need to map boolean to a tag if using ttkbootstrap specific row colors,
177+ # but simpler to just use text for now or configure tags.
178+ self .tree .insert ("" , "end" , values = (name , appid , status ), tags = ("found" if exists else "missing" ,))
179+
180+ # Configure tag colors (if standard ttk, bootstyle handles defaults differently)
181+ # self.tree.tag_configure("found", foreground="green") # bootstyle might override
182+
134183 self .status_var .set (f"Found { len (items )} results." )
135- self .patch_btn .config (state = "normal" )
184+
185+ def on_select (self , event ):
186+ selected = self .tree .selection ()
187+ if selected :
188+ item_values = self .tree .item (selected [0 ])['values' ]
189+ status = item_values [2 ] # "AVAILABLE" or "Missing"
190+ if status == "AVAILABLE" :
191+ self .patch_btn .config (state = "normal" )
192+ self .status_var .set (f"Selected: { item_values [0 ]} " )
193+ else :
194+ self .patch_btn .config (state = "disabled" )
195+ self .status_var .set (f"Lua file missing for: { item_values [0 ]} " )
196+ else :
197+ self .patch_btn .config (state = "disabled" )
136198
137199 def patch_selected (self ):
138200 selected = self .tree .selection ()
139201 if not selected :
140- messagebox .showwarning ("No Selection" , "Please select a game to patch." )
141202 return
142203
143204 item_values = self .tree .item (selected [0 ])['values' ]
144205 name = item_values [0 ]
145206 appid = str (item_values [1 ])
146- status = item_values [2 ]
147207
148- if status != "Found" :
149- messagebox .showerror ("Error" , f"Lua file for '{ name } ' (AppID: { appid } ) not found in repository." )
150- return
151-
152208 src_file = os .path .join (LUA_FILES_DIR , f"{ appid } .lua" )
153209 dest_file = os .path .join (STEAM_PLUGIN_DIR , f"{ appid } .lua" )
154210
155211 try :
156- # Ensure destination dir exists
157212 if not os .path .exists (STEAM_PLUGIN_DIR ):
158213 os .makedirs (STEAM_PLUGIN_DIR )
159214
160215 shutil .copy2 (src_file , dest_file )
161- messagebox .showinfo ("Success" , f"Patched '{ name } ' successfully!\n Copied to: { dest_file } " )
162- self .status_var .set (f"Patched { name } " )
216+ messagebox .showinfo ("Success" , f"Patched '{ name } ' successfully!" , parent = self . root )
217+ self .status_var .set (f"Successfully patched { name } " )
163218 except Exception as e :
164- messagebox .showerror ("Error" , f"Failed to copy file: { e } " )
219+ messagebox .showerror ("Error" , f"Failed to copy file: { e } " , parent = self . root )
165220
166221 def restart_steam (self ):
167- if not messagebox .askyesno ("Confirm Restart" , "This will close Steam and all running games. Continue?" ):
222+ if not messagebox .askyesno ("Confirm Restart" , "This will close Steam and all running games. Continue?" , parent = self . root ):
168223 return
169224
170225 self .status_var .set ("Restarting Steam..." )
171226
172227 def restart_thread ():
173228 try :
174- # Kill Steam
229+ # Taskkill is reliable
175230 subprocess .run ("taskkill /F /IM steam.exe" , shell = True , stdout = subprocess .DEVNULL , stderr = subprocess .DEVNULL )
176-
177- # Wait a bit
178- import time
179231 time .sleep (2 )
180232
181- # Start Steam
182233 if os .path .exists (STEAM_EXE_PATH ):
183234 subprocess .Popen ([STEAM_EXE_PATH ])
184235 self .root .after (0 , lambda : self .status_var .set ("Steam restarted." ))
185236 else :
186- # Try protocol handler
187237 subprocess .run ("start steam://open/main" , shell = True )
188238 self .root .after (0 , lambda : self .status_var .set ("Steam restart command sent." ))
189239
190240 except Exception as e :
191- self .root .after (0 , lambda : messagebox .showerror ("Error" , f"Failed to restart Steam: { e } " ))
241+ self .root .after (0 , lambda : messagebox .showerror ("Error" , f"Failed to restart Steam: { e } " , parent = self . root ))
192242
193243 threading .Thread (target = restart_thread , daemon = True ).start ()
194244
195245if __name__ == "__main__" :
196- # Check dependencies check
246+ # Theme setup
197247 try :
198- import requests
199- except ImportError :
200- messagebox .showerror ("Error" , "Missing 'requests' library. Please run 'pip install requests'" )
201- sys .exit (1 )
202-
203- root = tk .Tk ()
248+ root = ttk .Window (themename = "darkly" ) # modern dark theme
249+ except NameError :
250+ root = tk .Tk ()
251+
204252 app = SteamPatcherApp (root )
205253 root .mainloop ()
0 commit comments