diff --git a/RLC.ico b/RLC.ico new file mode 100644 index 0000000..17cfcae Binary files /dev/null and b/RLC.ico differ diff --git a/background/exemaker.py b/background/exemaker.py deleted file mode 100644 index 4c1d334..0000000 --- a/background/exemaker.py +++ /dev/null @@ -1,243 +0,0 @@ -#!/usr/bin/env python3 -""" -Simple Tkinter GUI to run PyInstaller and make an exe from a .py file. - -Features: -- choose script -- choose onefile / dir -- choose windowed (noconsole) -- add icon -- add additional data (file or folder; multiple, separated by semicolons) -- set output folder -- show live PyInstaller output -- cancel build -""" - -import tkinter as tk -from tkinter import filedialog, messagebox, scrolledtext -import subprocess -import threading -import sys -import shlex -import os -from pathlib import Path - -class BuilderGUI(tk.Tk): - def __init__(self): - super().__init__() - self.title("Python → EXE (PyInstaller GUI)") - self.geometry("800x600") - self.create_widgets() - self.proc = None # subprocess handle - self.stop_requested = False - - def create_widgets(self): - frame_top = tk.Frame(self) - frame_top.pack(fill="x", padx=10, pady=8) - - tk.Label(frame_top, text="Script:").grid(row=0, column=0, sticky="e") - self.script_entry = tk.Entry(frame_top, width=70) - self.script_entry.grid(row=0, column=1, padx=6) - tk.Button(frame_top, text="Browse...", command=self.browse_script).grid(row=0, column=2) - - tk.Label(frame_top, text="Output folder:").grid(row=1, column=0, sticky="e") - self.out_entry = tk.Entry(frame_top, width=70) - self.out_entry.grid(row=1, column=1, padx=6) - tk.Button(frame_top, text="Choose...", command=self.choose_output).grid(row=1, column=2) - - opts_frame = tk.LabelFrame(self, text="Options", padx=8, pady=8) - opts_frame.pack(fill="x", padx=10) - - self.onefile_var = tk.BooleanVar(value=True) - tk.Checkbutton(opts_frame, text="Onefile (single exe)", variable=self.onefile_var).grid(row=0, column=0, sticky="w", padx=6, pady=2) - - self.windowed_var = tk.BooleanVar(value=False) - tk.Checkbutton(opts_frame, text="Windowed (no console) / --noconsole", variable=self.windowed_var).grid(row=0, column=1, sticky="w", padx=6, pady=2) - - tk.Label(opts_frame, text="Icon (.ico):").grid(row=1, column=0, sticky="e") - self.icon_entry = tk.Entry(opts_frame, width=50) - self.icon_entry.grid(row=1, column=1, sticky="w", padx=6) - tk.Button(opts_frame, text="Browse", command=self.browse_icon).grid(row=1, column=2) - - tk.Label(opts_frame, text="Additional data (src;dest pairs separated by ';', e.g. resources;resources):").grid(row=2, column=0, columnspan=3, sticky="w", pady=(6,0)) - self.data_entry = tk.Entry(opts_frame, width=110) - self.data_entry.grid(row=3, column=0, columnspan=3, padx=6, pady=4) - - tk.Label(opts_frame, text="Extra PyInstaller args:").grid(row=4, column=0, sticky="w") - self.extra_entry = tk.Entry(opts_frame, width=80) - self.extra_entry.grid(row=4, column=1, columnspan=2, padx=6, pady=4, sticky="w") - - run_frame = tk.Frame(self) - run_frame.pack(fill="x", padx=10, pady=8) - - self.build_btn = tk.Button(run_frame, text="Build EXE", command=self.start_build, bg="#2b7a78", fg="white") - self.build_btn.pack(side="left", padx=(0,6)) - - self.cancel_btn = tk.Button(run_frame, text="Cancel", command=self.request_cancel, state="disabled", bg="#b00020", fg="white") - self.cancel_btn.pack(side="left") - - clear_btn = tk.Button(run_frame, text="Clear Log", command=self.clear_log) - clear_btn.pack(side="left", padx=6) - - open_out_btn = tk.Button(run_frame, text="Open Output Folder", command=self.open_output) - open_out_btn.pack(side="right") - - self.log = scrolledtext.ScrolledText(self, height=18, font=("Consolas", 10)) - self.log.pack(fill="both", expand=True, padx=10, pady=(0,10)) - - def browse_script(self): - path = filedialog.askopenfilename(filetypes=[("Python files", "*.py")]) - if path: - self.script_entry.delete(0, tk.END) - self.script_entry.insert(0, path) - # default output to script parent /dist - parent = os.path.dirname(path) - default_out = os.path.join(parent, "dist") - self.out_entry.delete(0, tk.END) - self.out_entry.insert(0, default_out) - - def choose_output(self): - path = filedialog.askdirectory() - if path: - self.out_entry.delete(0, tk.END) - self.out_entry.insert(0, path) - - def browse_icon(self): - path = filedialog.askopenfilename(filetypes=[("Icon files", "*.ico")]) - if path: - self.icon_entry.delete(0, tk.END) - self.icon_entry.insert(0, path) - - def clear_log(self): - self.log.delete("1.0", tk.END) - - def open_output(self): - out = self.out_entry.get().strip() - if not out: - messagebox.showinfo("Output folder", "No output folder set.") - return - os.startfile(out) if os.name == "nt" else subprocess.run(["xdg-open", out]) - - def request_cancel(self): - if self.proc and self.proc.poll() is None: - self.stop_requested = True - # try terminate politely - try: - self.proc.terminate() - except Exception: - pass - self.log_insert("\nCancellation requested...\n") - self.cancel_btn.config(state="disabled") - - def log_insert(self, text): - self.log.insert(tk.END, text) - self.log.see(tk.END) - - def start_build(self): - script = self.script_entry.get().strip() - if not script or not os.path.isfile(script): - messagebox.showerror("Error", "Please choose a valid Python script to build.") - return - - out_dir = self.out_entry.get().strip() or os.path.dirname(script) - os.makedirs(out_dir, exist_ok=True) - - self.build_btn.config(state="disabled") - self.cancel_btn.config(state="normal") - self.stop_requested = False - self.clear_log() - - # Start build on a thread to keep UI responsive - thread = threading.Thread(target=self.run_pyinstaller, args=(script, out_dir), daemon=True) - thread.start() - - def run_pyinstaller(self, script, out_dir): - # Build the PyInstaller command - cmd = [sys.executable, "-m", "PyInstaller"] - - if self.onefile_var.get(): - cmd.append("--onefile") - else: - cmd.append("--onedir") - - if self.windowed_var.get(): - cmd.append("--noconsole") - - icon = self.icon_entry.get().strip() - if icon: - cmd.extend(["--icon", icon]) - - # add additional data: user can provide pairs like "data;data" or "assets;assets" - data_spec = self.data_entry.get().strip() - if data_spec: - # support multiple separated by semicolons or vertical bars - pairs = [p for p in (data_spec.split(";") + data_spec.split("|")) if p.strip()] - # normalize pairs to PyInstaller format: src;dest (on Windows use ';' in CLI but PyInstaller expects src;dest as single argument) - for p in pairs: - # if user typed "src:dest" or "src->dest", replace with semicolon - p_fixed = p.replace(":", ";").replace("->", ";") - cmd.extend(["--add-data", p_fixed]) - - # user extra args (raw) - extra = self.extra_entry.get().strip() - if extra: - # split carefully - cmd.extend(shlex.split(extra)) - - # ensure output path goes to chosen dir: use --distpath - cmd.extend(["--distpath", out_dir]) - - # entry script - cmd.append(script) - - self.log_insert("Running PyInstaller with command:\n" + " ".join(shlex.quote(c) for c in cmd) + "\n\n") - - # spawn the process - try: - self.proc = subprocess.Popen( - cmd, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - bufsize=1, - universal_newlines=True, - ) - except Exception as e: - self.log_insert(f"Failed to start PyInstaller: {e}\n") - self.build_btn.config(state="normal") - self.cancel_btn.config(state="disabled") - return - - # Stream output line by line - try: - for line in self.proc.stdout: - if line: - self.log_insert(line) - if self.stop_requested: - try: - self.proc.terminate() - except Exception: - pass - break - self.proc.wait(timeout=30) - except subprocess.TimeoutExpired: - self.log_insert("Process did not exit in time after termination.\n") - except Exception as e: - self.log_insert(f"Error while running PyInstaller: {e}\n") - - retcode = self.proc.returncode if self.proc else None - if self.stop_requested: - self.log_insert("\nBuild cancelled by user.\n") - elif retcode == 0: - self.log_insert("\nBuild finished successfully.\n") - else: - self.log_insert(f"\nBuild finished with return code {retcode}.\n") - - # Re-enable buttons - self.build_btn.config(state="normal") - self.cancel_btn.config(state="disabled") - self.proc = None - self.stop_requested = False - -if __name__ == "__main__": - app = BuilderGUI() - app.mainloop() diff --git a/background/experiment.py b/background/experiment.py deleted file mode 100644 index 60929ae..0000000 --- a/background/experiment.py +++ /dev/null @@ -1,143 +0,0 @@ -import tkinter as tk -import requests -import threading -import time -import json - -SETTINGS_FILE = "settings.json" - -class CountdownApp: - def __init__(self, root): - self.root = root - self.root.title("Launch Control - GO/NOGO") - - self.go_nogo_labels = {} - self.sheet_data = {} - self.last_data = {} - self.running = True - - # Load settings - self.settings = self.load_settings() - - tk.Label(root, text="GO/NOGO STATUS", font=("Arial", 16, "bold")).pack(pady=10) - - # Create display area - self.frame = tk.Frame(root) - self.frame.pack(pady=10) - - # Buttons - tk.Button(root, text="Add Spreadsheet", command=self.add_spreadsheet_window).pack(pady=5) - tk.Button(root, text="Stop", command=self.stop).pack(pady=5) - - self.start_update_thread() - - def load_settings(self): - try: - with open(SETTINGS_FILE, "r") as f: - return json.load(f) - except FileNotFoundError: - return {"spreadsheets": []} - - def save_settings(self): - with open(SETTINGS_FILE, "w") as f: - json.dump(self.settings, f, indent=4) - - def add_spreadsheet_window(self): - win = tk.Toplevel(self.root) - win.title("Add Spreadsheet") - - tk.Label(win, text="Name:").grid(row=0, column=0) - name_entry = tk.Entry(win) - name_entry.grid(row=0, column=1) - - tk.Label(win, text="Link (CSV export or share link):").grid(row=1, column=0) - link_entry = tk.Entry(win, width=60) - link_entry.grid(row=1, column=1) - - tk.Label(win, text="Range cell (e.g., L2):").grid(row=2, column=0) - range_entry = tk.Entry(win) - range_entry.grid(row=2, column=1) - - def save_sheet(): - name = name_entry.get().strip() - link = link_entry.get().strip() - cell = range_entry.get().strip().upper() - if name and link and cell: - self.settings["spreadsheets"].append({ - "name": name, - "link": link, - "cell": cell - }) - self.save_settings() - self.add_go_nogo_label(name) - win.destroy() - - tk.Button(win, text="Save", command=save_sheet).grid(row=3, column=0, columnspan=2, pady=10) - - def add_go_nogo_label(self, name): - if name not in self.go_nogo_labels: - label = tk.Label(self.frame, text=f"{name}: ---", font=("Arial", 14), width=25) - label.pack(pady=2) - self.go_nogo_labels[name] = label - - def update_labels(self): - for sheet in self.settings["spreadsheets"]: - name = sheet["name"] - link = sheet["link"] - cell = sheet["cell"] - - # Convert normal sheet link to CSV export link if needed - if "/edit" in link and "export" not in link: - link = link.split("/edit")[0] + "/gviz/tq?tqx=out:csv" - - try: - r = requests.get(link, timeout=5) - if r.status_code == 200: - content = r.text - if name not in self.last_data or self.last_data[name] != content: - self.last_data[name] = content - # Just read raw content and extract cell text if possible - value = self.extract_cell_value(content, cell) - self.update_label_color(name, value) - except Exception as e: - print(f"Error updating {name}: {e}") - - def extract_cell_value(self, csv_data, cell): - # Simple CSV parser to get cell data like L2 - try: - rows = [r.split(",") for r in csv_data.splitlines() if r.strip()] - col = ord(cell[0]) - 65 - row = int(cell[1:]) - 1 - return rows[row][col].strip().upper() - except Exception: - return "ERR" - - def update_label_color(self, name, value): - label = self.go_nogo_labels.get(name) - if not label: - return - - if "GO" in value: - label.config(text=f"{name}: GO", bg="green", fg="white") - elif "NO" in value: - label.config(text=f"{name}: NO GO", bg="red", fg="white") - else: - label.config(text=f"{name}: ---", bg="gray", fg="black") - - def start_update_thread(self): - threading.Thread(target=self.update_loop, daemon=True).start() - - def update_loop(self): - while self.running: - self.update_labels() - time.sleep(0.1) - - def stop(self): - self.running = False - self.root.destroy() - - -if __name__ == "__main__": - root = tk.Tk() - app = CountdownApp(root) - root.mainloop() diff --git a/dist/exe/0.6.0/main.exe b/dist/exe/0.6.0/main.exe new file mode 100644 index 0000000..62305ea Binary files /dev/null and b/dist/exe/0.6.0/main.exe differ diff --git a/dist/installers/0.6.0/RLCInstaller.exe b/dist/installers/0.6.0/RLCInstaller.exe new file mode 100644 index 0000000..35daa96 Binary files /dev/null and b/dist/installers/0.6.0/RLCInstaller.exe differ diff --git a/readme.md b/readme.md index 27a392a..589dc1c 100644 --- a/readme.md +++ b/readme.md @@ -1,11 +1,11 @@ RocketLaunchCountdown is a python and HTML system to operate a launch countdown and go/nogo indicator. The go/nogo indicator is operated either by a spreadsheet, using a sharelink inserted in the settings windows. Or there are buttons in the app that can manage it as well. Which mode is used is dictated by a simple radio button in the settings window. -Latest release: [RocketLaunchCountdown 0.5.0](https://github.com/HamsterSpaceNerd3000/RocketLaunchCountdown/releases/tag/Prealpha_050) +Latest release: [RocketLaunchCountdown 0.6.0](https://github.com/HamsterSpaceNerd3000/RocketLaunchCountdown/releases/tag/v0.6.0) Features Added: - UI Customization is here! Light mode has been added and can be turned on in the settings window. Also in the settings window is the ability to adjust the colors of the HTML files for the clock and go/no-go. - More background optimizations and simplifications. - Added game version to title on main screen. - Added more Nitrogen + T-/L- Switching has been added. + Light/Dark mode no longer effects the HTML files. + Added ability to remove mission name from the HTML output. + Created/added app icon (will be added to the window in future version, currently on desktop shortcut) INSTALL INSTRUCTIONS