Skip to content

Commit c75ceb2

Browse files
Update version to 1.1.0 and enhance project import functionality
1 parent e16688a commit c75ceb2

File tree

3 files changed

+159
-48
lines changed

3 files changed

+159
-48
lines changed

Echo_hub.py

Lines changed: 157 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Jack Murray
22
# Nova Foundry / Echo Hub
3-
# v1.0.4
3+
# v1.1.0
44

55
import os
66
import sys
@@ -11,13 +11,17 @@
1111
import customtkinter as ctk
1212
from tkinter import filedialog
1313
from PIL import Image
14+
import urllib.request
15+
import json
1416

1517
# ---------- CONFIG ----------
1618
IMPORT_DESTINATION = r"Working_game"
1719
EXPORT_SOURCE = r"Working_game"
1820
DEFAULT_WIDTH = 600
1921
DEFAULT_HEIGHT = 640 # taller window to fit new button
2022
PROGRESS_AREA_HEIGHT = 70
23+
VERSION = "2.1"
24+
GITURL = "https://github.com/DirectedHunt42/EchoEngine"
2125

2226
# ---------- Helper Functions ----------
2327
def show_custom_message(title, message, is_error=False):
@@ -75,6 +79,9 @@ def load_resized_image(path, max_size=64):
7579
print(f"Could not load image: {e}")
7680
return None
7781

82+
def version_tuple(v):
83+
return tuple(map(int, v.split('.')))
84+
7885
# ---------- Progress Bar Logic ----------
7986
def show_progress_indicators():
8087
new_height = DEFAULT_HEIGHT + PROGRESS_AREA_HEIGHT
@@ -94,18 +101,19 @@ def hide_progress_indicators():
94101
app.geometry(f"{DEFAULT_WIDTH}x{DEFAULT_HEIGHT}")
95102
app.minsize(DEFAULT_WIDTH, DEFAULT_HEIGHT)
96103

97-
def update_progress_indicators(current, total, progress):
98-
progress_bar.set(progress)
99-
file_status_label.configure(text=f"File {current} of {total}")
100-
app.update_idletasks()
101-
102104
def run_with_progress(task_name, actions):
103105
def task():
104106
total_files = len(actions)
105-
for i, action in enumerate(actions, start=1):
107+
for i, item in enumerate(actions, start=1):
108+
if isinstance(item, tuple):
109+
action, desc = item
110+
else:
111+
action = item
112+
desc = "Processing"
113+
app.after(0, lambda d=desc, c=i, t=total_files: file_status_label.configure(text=f"{d} ({c}/{t})"))
106114
action()
107115
progress = i / total_files if total_files else 1
108-
app.after(0, lambda c=i, t=total_files, p=progress: update_progress_indicators(c, t, p))
116+
app.after(0, lambda p=progress: progress_bar.set(p))
109117
app.after(0, task_done)
110118
def task_done():
111119
hide_progress_indicators()
@@ -119,38 +127,54 @@ def task_done():
119127
threading.Thread(target=task, daemon=True).start()
120128

121129
# ---------- Folder Utilities ----------
130+
def get_clear_actions(folder_path):
131+
actions = []
132+
for root, dirs, files in os.walk(folder_path, topdown=False):
133+
for filename in files:
134+
file_path = os.path.join(root, filename)
135+
actions.append((lambda p=file_path: os.unlink(p), f"Deleting {os.path.relpath(file_path, folder_path)}"))
136+
for dir_name in dirs:
137+
dir_path = os.path.join(root, dir_name)
138+
actions.append((lambda p=dir_path: os.rmdir(p), f"Removing directory {os.path.relpath(dir_path, folder_path)}"))
139+
return actions
140+
122141
def clear_folder(folder_path):
123142
if not os.path.exists(folder_path):
124143
show_custom_message("Info", f"'{folder_path}' does not exist.")
125144
return
126145
if not ask_confirmation("Confirm Deletion",
127146
f"The contents of '{folder_path}' will be permanently deleted.\nProceed?"):
128147
return
129-
for filename in os.listdir(folder_path):
130-
file_path = os.path.join(folder_path, filename)
131-
try:
132-
if os.path.isfile(file_path) or os.path.islink(file_path):
133-
os.unlink(file_path)
134-
elif os.path.isdir(file_path):
135-
shutil.rmtree(file_path)
136-
except Exception as e:
137-
print(f"Failed to delete {file_path}. Reason: {e}")
148+
actions = get_clear_actions(folder_path)
149+
if actions:
150+
run_with_progress("Clearing working directory", actions)
151+
else:
152+
show_custom_message("Info", "Directory is already empty.")
138153

139154
def copy_folder_with_progress(src, dest):
155+
actions = []
140156
if os.path.exists(dest):
141157
if not ask_confirmation("Overwrite Project",
142158
f"The working directory '{dest}' contains project files.\nOverwrite its contents?"):
143159
return []
144-
clear_folder(dest)
145-
os.makedirs(dest, exist_ok=True)
146-
actions = []
147-
for item in os.listdir(src):
148-
s = os.path.join(src, item)
149-
d = os.path.join(dest, item)
150-
if os.path.isdir(s):
151-
actions.append(lambda s=s, d=d: shutil.copytree(s, d, dirs_exist_ok=True))
152-
else:
153-
actions.append(lambda s=s, d=d: shutil.copy2(s, d))
160+
actions.extend(get_clear_actions(dest))
161+
# Collect directory creation actions
162+
for root, dirs, _ in os.walk(src):
163+
rel_root = os.path.relpath(root, src)
164+
dest_root = os.path.join(dest, rel_root)
165+
for dir_name in dirs:
166+
dir_path = os.path.join(dest_root, dir_name)
167+
rel_dir = os.path.join(rel_root, dir_name)
168+
actions.append((lambda p=dir_path: os.makedirs(p, exist_ok=True), f"Creating directory {rel_dir}"))
169+
# Collect file copy actions
170+
for root, _, files in os.walk(src):
171+
rel_root = os.path.relpath(root, src)
172+
dest_root = os.path.join(dest, rel_root)
173+
for filename in files:
174+
src_path = os.path.join(root, filename)
175+
dest_path = os.path.join(dest_root, filename)
176+
rel_path = os.path.join(rel_root, filename)
177+
actions.append((lambda s=src_path, d=dest_path: shutil.copy2(s, d), f"Copying {rel_path}"))
154178
return actions
155179

156180
# ---------- Main Actions ----------
@@ -168,25 +192,43 @@ def import_zip():
168192
zip_path = filedialog.askopenfilename(filetypes=[("Echo Project", "*.echo")])
169193
if not zip_path:
170194
return
171-
import_project_from_path(zip_path)
195+
import_project(zip_path)
172196

173-
def import_project_from_path(zip_path):
197+
def import_project(zip_path):
174198
if os.path.exists(IMPORT_DESTINATION):
175199
if not ask_confirmation("Overwrite Project",
176200
f"The working directory '{IMPORT_DESTINATION}' contains project files.\nOverwrite its contents?"):
177201
return
178-
clear_folder(IMPORT_DESTINATION)
179-
os.makedirs(IMPORT_DESTINATION, exist_ok=True)
180-
try:
181-
with zipfile.ZipFile(zip_path, 'r') as zip_ref:
182-
file_list = [info for info in zip_ref.infolist() if not info.is_dir()]
183-
actions = [lambda info=info, z=zip_ref: z.extract(info.filename, IMPORT_DESTINATION) for info in file_list]
184-
if actions:
185-
run_with_progress("Importing project", actions)
186-
else:
187-
show_custom_message("Success", "Project imported successfully!")
188-
except Exception as e:
189-
show_custom_message("Error", str(e), is_error=True)
202+
for btn in (copy_btn, import_btn, export_btn, open_btn, clear_btn):
203+
btn.configure(state='disabled')
204+
status_label.configure(text="Importing project...")
205+
show_progress_indicators()
206+
def task_done(success=True, message="Project imported successfully!"):
207+
hide_progress_indicators()
208+
for btn in (copy_btn, import_btn, export_btn, open_btn, clear_btn):
209+
btn.configure(state='normal')
210+
if success:
211+
show_custom_message("Success", message)
212+
else:
213+
show_custom_message("Error", message, is_error=True)
214+
def import_task():
215+
try:
216+
os.makedirs(IMPORT_DESTINATION, exist_ok=True)
217+
with zipfile.ZipFile(zip_path, 'r') as zip_ref:
218+
file_list = [info for info in zip_ref.infolist() if not info.is_dir()]
219+
total_files = len(file_list)
220+
if not total_files:
221+
app.after(0, task_done, True, "Empty project")
222+
return
223+
for i, info in enumerate(file_list, start=1):
224+
app.after(0, lambda f=info.filename, c=i, t=total_files: file_status_label.configure(text=f"Extracting {f} ({c}/{t})"))
225+
zip_ref.extract(info, IMPORT_DESTINATION)
226+
progress = i / total_files
227+
app.after(0, lambda p=progress: progress_bar.set(p))
228+
app.after(0, task_done)
229+
except Exception as e:
230+
app.after(0, task_done, False, str(e))
231+
threading.Thread(target=import_task, daemon=True).start()
190232

191233
def export_zip():
192234
zip_path = filedialog.asksaveasfilename(defaultextension=".echo",
@@ -213,15 +255,16 @@ def export_task():
213255
full_path = os.path.join(root, file)
214256
arcname = os.path.relpath(full_path, EXPORT_SOURCE)
215257
file_paths.append((full_path, arcname))
216-
if not file_paths:
258+
total_files = len(file_paths)
259+
if not total_files:
217260
app.after(0, task_done, True, "No project found")
218261
return
219-
total_files = len(file_paths)
220262
with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zip_ref:
221263
for i, (full_path, arcname) in enumerate(file_paths, start=1):
264+
app.after(0, lambda a=arcname, c=i, t=total_files: file_status_label.configure(text=f"Adding {a} ({c}/{t})"))
222265
zip_ref.write(full_path, arcname)
223266
progress = i / total_files
224-
app.after(0, lambda c=i, t=total_files, p=progress: update_progress_indicators(c, t, p))
267+
app.after(0, lambda p=progress: progress_bar.set(p))
225268
app.after(0, task_done)
226269
except Exception as e:
227270
app.after(0, task_done, False, str(e))
@@ -234,12 +277,79 @@ def open_project():
234277
except Exception as e:
235278
show_custom_message("Error", str(e), is_error=True)
236279

280+
# ---------- Auto Update ----------
281+
def check_for_update():
282+
def check_task():
283+
try:
284+
url = "https://api.github.com/repos/DirectedHunt42/EchoEngine/releases/latest"
285+
req = urllib.request.Request(url, headers={'User-Agent': 'EchoHub', 'Accept': 'application/vnd.github.v3+json'})
286+
with urllib.request.urlopen(req) as response:
287+
data = json.loads(response.read().decode('utf-8'))
288+
app.after(0, lambda d=data: do_update_confirm(d))
289+
except:
290+
pass
291+
threading.Thread(target=check_task, daemon=True).start()
292+
293+
def do_update_confirm(data):
294+
try:
295+
title = data.get('name', '')
296+
if title.startswith("Release "):
297+
new_ver = title[len("Release "):].strip()
298+
else:
299+
return
300+
current_ver = VERSION
301+
current_t = version_tuple(current_ver)
302+
new_t = version_tuple(new_ver)
303+
if new_t > current_t:
304+
if ask_confirmation("Update Available", f"New version {new_ver} available (current {current_ver}).\nDownload and install?"):
305+
download_and_install(data)
306+
except:
307+
pass
308+
309+
def download_and_install(data):
310+
assets = data.get('assets', [])
311+
download_url = None
312+
for asset in assets:
313+
if asset['name'] == "Echo_Editor_Setup.exe":
314+
download_url = asset['browser_download_url']
315+
break
316+
if not download_url:
317+
show_custom_message("Error", "Update file not found.", is_error=True)
318+
return
319+
setup_file = os.path.join(os.path.dirname(sys.argv[0]), "Echo_Editor_Setup.exe")
320+
for btn in (copy_btn, import_btn, export_btn, open_btn, clear_btn):
321+
btn.configure(state='disabled')
322+
status_label.configure(text="Downloading update...")
323+
show_progress_indicators()
324+
def download_task():
325+
try:
326+
def reporthook(count, block_size, total_size):
327+
if total_size <= 0:
328+
app.after(0, file_status_label.configure(text="Downloading..."))
329+
return
330+
progress = min(1.0, float(count * block_size) / total_size)
331+
mb_done = (count * block_size) / (1024 ** 2)
332+
mb_total = total_size / (1024 ** 2)
333+
app.after(0, file_status_label.configure(text=f"Downloading ({mb_done:.2f}/{mb_total:.2f} MB)"))
334+
app.after(0, lambda p=progress: progress_bar.set(p))
335+
urllib.request.urlretrieve(download_url, setup_file, reporthook)
336+
app.after(0, hide_progress_indicators)
337+
app.after(0, lambda: show_custom_message("Update Ready", "Update downloaded. Installing..."))
338+
subprocess.Popen([setup_file])
339+
app.after(100, app.destroy)
340+
except Exception as e:
341+
app.after(0, hide_progress_indicators)
342+
app.after(0, lambda: show_custom_message("Error", str(e), is_error=True))
343+
for btn in (copy_btn, import_btn, export_btn, open_btn, clear_btn):
344+
btn.configure(state='normal')
345+
threading.Thread(target=download_task, daemon=True).start()
346+
237347
# ---------- Startup File Handling ----------
238348
def check_startup_file():
239349
if len(sys.argv) > 1:
240350
file_path = sys.argv[1]
241351
if file_path.lower().endswith(".echo") and os.path.exists(file_path):
242-
import_project_from_path(file_path)
352+
import_project(file_path)
243353

244354
# ---------- App Setup ----------
245355
ctk.set_appearance_mode("dark")
@@ -312,9 +422,10 @@ def check_startup_file():
312422
logo_label = ctk.CTkLabel(frame, image=logo_ctk, text="")
313423
logo_label.pack(pady=10)
314424

315-
ctk.CTkLabel(frame, text="v1.0.4", font=("Segoe UI", 10), text_color="gray").pack(pady=(0, 10))
425+
ctk.CTkLabel(frame, text="v1.1.0", font=("Segoe UI", 10), text_color="gray").pack(pady=(0, 10))
316426

317427
# ---------- Start ----------
318428
hide_progress_indicators()
429+
check_for_update()
319430
check_startup_file()
320431
app.mainloop()

Engine_base/obj/Debug/net9.0-windows/Engine_base.AssemblyInfo.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
[assembly: System.Reflection.AssemblyCompanyAttribute("Engine_base")]
1414
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
1515
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
16-
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+e66ff178223dcf0d10a84cf09dc06469d703212d")]
16+
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+e16688ac567b7ae221b4eafe3068a21512b89eb3")]
1717
[assembly: System.Reflection.AssemblyProductAttribute("Engine_base")]
1818
[assembly: System.Reflection.AssemblyTitleAttribute("Engine_base")]
1919
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
c0453af64c8913a7c97e0f761da3aad0e4a38b779380afd09c0446946ed984f8
1+
f60532682c94c7035d2ce101ebc8a6dbad3d8186950c02cfa49d155b066c6665

0 commit comments

Comments
 (0)