Merge pull request #6 from HamsterSpaceNerd3000/dev

0.5.0
This commit is contained in:
HamsterSpaceNerd3000
2025-10-21 20:13:49 -04:00
committed by GitHub
4 changed files with 663 additions and 71 deletions

BIN
dist/exe/0.5.0/main.exe vendored Normal file

Binary file not shown.

BIN
dist/installers/0.5.0/RLCInstaller.exe vendored Normal file

Binary file not shown.

726
main.py
View File

@@ -1,7 +1,9 @@
import tkinter as tk import tkinter as tk
from tkinter import colorchooser
import time import time
import threading import threading
from datetime import datetime, timedelta from datetime import datetime, timedelta
import re
import requests import requests
import csv import csv
import io import io
@@ -12,7 +14,6 @@ try:
except Exception: except Exception:
ZoneInfo = None ZoneInfo = None
# Get the user's Documents folder (cross-platform) # Get the user's Documents folder (cross-platform)
documents_folder = os.path.join(os.path.expanduser("~"), "Documents") documents_folder = os.path.join(os.path.expanduser("~"), "Documents")
@@ -25,29 +26,52 @@ COUNTDOWN_HTML = os.path.join(app_folder, "countdown.html")
GONOGO_HTML = os.path.join(app_folder, "gonogo.html") GONOGO_HTML = os.path.join(app_folder, "gonogo.html")
SHEET_LINK = "" SHEET_LINK = ""
session = requests.Session() session = requests.Session()
appVersion = "0.4.0" appVersion = "0.5.0"
SETTINGS_FILE = os.path.join(app_folder, "settings.json") SETTINGS_FILE = os.path.join(app_folder, "settings.json")
# Default settings # Default settings
DEFAULT_SETTINGS = { DEFAULT_SETTINGS = {
"mode": "spreadsheet", # or 'buttons' "mode": "spreadsheet",
"sheet_link": SHEET_LINK, "sheet_link": SHEET_LINK,
# rows are 1-based as shown in spreadsheet; default 2,3,4 -> indices 1,2,3
"range_row": 2, "range_row": 2,
"weather_row": 3, "weather_row": 3,
"vehicle_row": 4, "vehicle_row": 4,
"column": 12 # 1-based column (default column 12 -> index 11) "column": 12
} }
# default timezone: 'local' uses system local tz, otherwise an IANA name or 'UTC' # default timezone: 'local' uses system local tz, otherwise an IANA name or 'UTC'
DEFAULT_SETTINGS.setdefault('timezone', 'local') DEFAULT_SETTINGS.setdefault('timezone', 'local')
# Appearance defaults
DEFAULT_SETTINGS.setdefault('bg_color', '#000000')
DEFAULT_SETTINGS.setdefault('text_color', '#FFFFFF')
DEFAULT_SETTINGS.setdefault('font_family', 'Consolas')
DEFAULT_SETTINGS.setdefault('mission_font_px', 24)
DEFAULT_SETTINGS.setdefault('timer_font_px', 80)
DEFAULT_SETTINGS.setdefault('gn_bg_color', '#111111')
DEFAULT_SETTINGS.setdefault('gn_border_color', '#FFFFFF')
DEFAULT_SETTINGS.setdefault('gn_go_color', '#00FF00')
DEFAULT_SETTINGS.setdefault('gn_nogo_color', '#FF0000')
DEFAULT_SETTINGS.setdefault('gn_font_px', 20)
DEFAULT_SETTINGS.setdefault('appearance_mode', 'dark')
# HTML-only appearance defaults (these should not affect the Python GUI)
DEFAULT_SETTINGS.setdefault('html_bg_color', DEFAULT_SETTINGS.get('bg_color', '#000000'))
DEFAULT_SETTINGS.setdefault('html_text_color', DEFAULT_SETTINGS.get('text_color', '#FFFFFF'))
DEFAULT_SETTINGS.setdefault('html_font_family', DEFAULT_SETTINGS.get('font_family', 'Consolas'))
DEFAULT_SETTINGS.setdefault('html_mission_font_px', DEFAULT_SETTINGS.get('mission_font_px', 24))
DEFAULT_SETTINGS.setdefault('html_timer_font_px', DEFAULT_SETTINGS.get('timer_font_px', 80))
DEFAULT_SETTINGS.setdefault('html_gn_bg_color', DEFAULT_SETTINGS.get('gn_bg_color', '#111111'))
DEFAULT_SETTINGS.setdefault('html_gn_border_color', DEFAULT_SETTINGS.get('gn_border_color', '#FFFFFF'))
DEFAULT_SETTINGS.setdefault('html_gn_go_color', DEFAULT_SETTINGS.get('gn_go_color', '#00FF00'))
DEFAULT_SETTINGS.setdefault('html_gn_nogo_color', DEFAULT_SETTINGS.get('gn_nogo_color', '#FF0000'))
DEFAULT_SETTINGS.setdefault('html_gn_font_px', DEFAULT_SETTINGS.get('gn_font_px', 20))
# A small list of common timezone choices. # A small list of common timezone choices.
TIMEZONE_CHOICES = [ TIMEZONE_CHOICES = [
'local', 'UTC', 'US/Eastern', 'US/Central', 'US/Mountain', 'US/Pacific', 'local', 'UTC', 'US/Eastern', 'US/Central', 'US/Mountain', 'US/Pacific',
'Europe/London', 'Europe/Paris', 'Asia/Tokyo', 'Australia/Sydney' 'Europe/London', 'Europe/Paris', 'Asia/Tokyo', 'Australia/Sydney'
] ]
def load_settings(): def load_settings():
try: try:
if os.path.exists(SETTINGS_FILE): if os.path.exists(SETTINGS_FILE):
@@ -59,7 +83,6 @@ def load_settings():
save_settings(DEFAULT_SETTINGS) save_settings(DEFAULT_SETTINGS)
return DEFAULT_SETTINGS.copy() return DEFAULT_SETTINGS.copy()
def save_settings(s): def save_settings(s):
try: try:
with open(SETTINGS_FILE, 'w', encoding='utf-8') as fh: with open(SETTINGS_FILE, 'w', encoding='utf-8') as fh:
@@ -114,14 +137,42 @@ def fetch_gonogo():
def get_status_color(status): def get_status_color(status):
"""Return color name for a Go/No-Go status string.""" """Return color name for a Go/No-Go status string."""
try: try:
return "green" if str(status).strip().upper() == "GO" else "red" s = str(status or '').strip().upper()
# normalize to letters only so variants like 'NO GO', 'NO-GO', 'NOGO' match
norm = re.sub(r'[^A-Z]', '', s)
if norm == 'GO':
return 'green'
if norm == 'NOGO':
return 'red'
# fallback: treat unknown/empty as white
return 'white'
except Exception: except Exception:
return "white" return "white"
def format_status_display(status):
try:
s = str(status or '').strip().upper()
norm = re.sub(r'[^A-Z]', '', s)
if norm == 'GO':
return 'GO'
if norm == 'NOGO':
return 'NO-GO'
return s
except Exception:
return str(status or '')
# ------------------------- # -------------------------
# Write Countdown HTML # Write Countdown HTML
# ------------------------- # -------------------------
def write_countdown_html(mission_name, timer_text): def write_countdown_html(mission_name, timer_text):
s = load_settings()
# Prefer HTML-specific settings; fall back to GUI appearance settings for backwards compatibility
bg = s.get('html_bg_color', s.get('bg_color', '#000000'))
text = s.get('html_text_color', s.get('text_color', '#FFFFFF'))
font = s.get('html_font_family', s.get('font_family', 'Consolas, monospace'))
mission_px = int(s.get('html_mission_font_px', s.get('mission_font_px', 48)))
timer_px = int(s.get('html_timer_font_px', s.get('timer_font_px', 120)))
html = f"""<!DOCTYPE html> html = f"""<!DOCTYPE html>
<html> <html>
<head> <head>
@@ -129,16 +180,16 @@ def write_countdown_html(mission_name, timer_text):
<style> <style>
body {{ body {{
margin: 0; margin: 0;
background-color: black; background-color: {bg};
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
color: white; color: {text};
font-family: Consolas, monospace; font-family: {font};
}} }}
#mission {{ font-size: 4vw; margin-bottom: 0; }} #mission {{ font-size: {mission_px}px; margin-bottom: 0; }}
#timer {{ font-size: 8vw; margin-bottom: 40px; }} #timer {{ font-size: {timer_px}px; margin-bottom: 40px; }}
</style> </style>
<script> <script>
setTimeout(() => location.reload(), 1000); setTimeout(() => location.reload(), 1000);
@@ -158,6 +209,24 @@ setTimeout(() => location.reload(), 1000);
def write_gonogo_html(gonogo_values=None): def write_gonogo_html(gonogo_values=None):
if gonogo_values is None: if gonogo_values is None:
gonogo_values = ["N/A", "N/A", "N/A"] gonogo_values = ["N/A", "N/A", "N/A"]
s = load_settings()
# Prefer HTML-specific settings; fall back to GUI appearance settings for backwards compatibility
bg = s.get('html_bg_color', s.get('bg_color', '#000000'))
text = s.get('html_text_color', s.get('text_color', '#FFFFFF'))
font = s.get('html_font_family', s.get('font_family', 'Consolas, monospace'))
gn_bg = s.get('html_gn_bg_color', s.get('gn_bg_color', '#111111'))
gn_border = s.get('html_gn_border_color', s.get('gn_border_color', '#FFFFFF'))
gn_go = s.get('html_gn_go_color', s.get('gn_go_color', '#00FF00'))
gn_nogo = s.get('html_gn_nogo_color', s.get('gn_nogo_color', '#FF0000'))
gn_px = int(s.get('html_gn_font_px', s.get('gn_font_px', 28)))
# normalize and format display values so variants like 'NO GO' become 'NO-GO'
disp0 = format_status_display(gonogo_values[0])
disp1 = format_status_display(gonogo_values[1])
disp2 = format_status_display(gonogo_values[2])
n0 = re.sub(r'[^A-Z]', '', (str(gonogo_values[0] or '')).strip().upper())
n1 = re.sub(r'[^A-Z]', '', (str(gonogo_values[1] or '')).strip().upper())
n2 = re.sub(r'[^A-Z]', '', (str(gonogo_values[2] or '')).strip().upper())
html = f"""<!DOCTYPE html> html = f"""<!DOCTYPE html>
<html> <html>
<head> <head>
@@ -165,9 +234,9 @@ def write_gonogo_html(gonogo_values=None):
<style> <style>
body {{ body {{
margin: 0; margin: 0;
background-color: black; background-color: {bg};
color: white; color: {text};
font-family: Consolas, monospace; font-family: {font};
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
@@ -178,24 +247,24 @@ body {{
gap: 40px; gap: 40px;
}} }}
.status-box {{ .status-box {{
border: 2px solid white; border: 2px solid {gn_border};
padding: 20px 40px; padding: 20px 40px;
font-size: 2.5vw; font-size: {gn_px}px;
text-align: center; text-align: center;
background-color: #111; background-color: {gn_bg};
}} }}
.go {{ color: #0f0; }} .go {{ color: {gn_go}; }}
.nogo {{ color: #f00; }} .nogo {{ color: {gn_nogo}; }}
</style> </style>
<script> <script>
setTimeout(() => location.reload(), 5000); setTimeout(() => location.reload(), 5000);
</script> </script>
</head> </head>
<body> <body>
<div id="gonogo"> <div id="gonogo">
<div class="status-box {'go' if gonogo_values[0].lower()=='go' else 'nogo'}">Range: {gonogo_values[0]}</div> <div class="status-box {'go' if n0=='GO' else 'nogo'}">Range: {disp0}</div>
<div class="status-box {'go' if gonogo_values[2].lower()=='go' else 'nogo'}">Vehicle: {gonogo_values[2]}</div> <div class="status-box {'go' if n2=='GO' else 'nogo'}">Vehicle: {disp2}</div>
<div class="status-box {'go' if gonogo_values[1].lower()=='go' else 'nogo'}">Weather: {gonogo_values[1]}</div> <div class="status-box {'go' if n1=='GO' else 'nogo'}">Weather: {disp1}</div>
</div> </div>
</body> </body>
</html>""" </html>"""
@@ -227,7 +296,7 @@ class CountdownApp:
self.last_gonogo_update = time.time() self.last_gonogo_update = time.time()
# Title # Title
self.titletext = tk.Label(root, text="RocketLaunchCountdown", font=("Consolas", 24), fg="white", bg="black") self.titletext = tk.Label(root, text=f"RocketLaunchCountdown {appVersion}", font=("Consolas", 24), fg="white", bg="black")
self.titletext.pack(pady=(10, 0)) self.titletext.pack(pady=(10, 0))
# Display # Display
@@ -325,8 +394,8 @@ class CountdownApp:
self.reset_btn = tk.Button(frame_buttons, text="⟳ Reset", command=self.reset, font=("Arial", 14)) self.reset_btn = tk.Button(frame_buttons, text="⟳ Reset", command=self.reset, font=("Arial", 14))
self.reset_btn.grid(row=0, column=3, padx=5) self.reset_btn.grid(row=0, column=3, padx=5)
# Settings button moved next to control buttons (match size/style) # Settings button moved next to control buttons (match size/style)
settings_btn = tk.Button(frame_buttons, text="Settings", command=self.show_settings_window, font=("Arial", 14), width=10) self.settings_btn = tk.Button(frame_buttons, text="Settings", command=self.show_settings_window, font=("Arial", 14), width=10)
settings_btn.grid(row=0, column=4, padx=6) self.settings_btn.grid(row=0, column=4, padx=6)
# Note: gonogo mode switching remains in Settings; manual buttons appear when mode == 'buttons' # Note: gonogo mode switching remains in Settings; manual buttons appear when mode == 'buttons'
@@ -372,6 +441,11 @@ class CountdownApp:
self.update_inputs() self.update_inputs()
# set initial manual button visibility from settings # set initial manual button visibility from settings
self.update_manual_visibility() self.update_manual_visibility()
# Apply appearance settings at startup so the mission entry and other widgets reflect the saved mode
try:
self.apply_appearance_settings()
except Exception:
pass
self.update_clock() self.update_clock()
# ---------------------------- # ----------------------------
@@ -379,37 +453,68 @@ class CountdownApp:
# ---------------------------- # ----------------------------
def show_settings_window(self): def show_settings_window(self):
settings = load_settings() settings = load_settings()
win = tk.Toplevel(self.root) win = tk.Toplevel(self.root)
win.config(bg="black")
win.title("Settings")
win.geometry("560x250")
win.transient(self.root) win.transient(self.root)
win.title("Settings")
win.geometry("560x275")
# apply current appearance mode so the settings window matches the main UI
s_local = load_settings()
mode_local = s_local.get('appearance_mode', 'dark')
if mode_local == 'dark':
win_bg = '#000000'; win_text = '#FFFFFF'; btn_bg = '#FFFFFF'; btn_fg = '#000000'
else:
win_bg = '#FFFFFF'; win_text = '#000000'; btn_bg = '#000000'; btn_fg = '#FFFFFF'
win.config(bg=win_bg)
# set per-window widget defaults so nested widgets inherit the chosen theme
try:
win.option_add('*Foreground', win_text)
win.option_add('*Background', win_bg)
# entry specific defaults
win.option_add('*Entry.Background', '#222' if mode_local == 'dark' else '#b4b4b4')
win.option_add('*Entry.Foreground', win_text if mode_local == 'dark' else '#000000')
except Exception:
pass
# keep track of this Toplevel so other dialogs can close it if needed
try:
self.settings_win = win
def _clear_settings_ref(evt=None):
try:
self.settings_win = None
except Exception:
pass
win.bind('<Destroy>', _clear_settings_ref)
except Exception:
pass
# Mode selection # Mode selection
frame_mode = tk.Frame(win) frame_mode = tk.Frame(win)
frame_mode.config(bg="black") frame_mode.config(bg=win_bg)
frame_mode.pack(fill='x', pady=8, padx=8) frame_mode.pack(fill='x', pady=8, padx=8)
tk.Label(frame_mode, text="Mode:", fg="white", bg="black").pack(side='left') tk.Label(frame_mode, text="Mode:", fg=win_text, bg=win_bg).pack(side='left')
mode_var = tk.StringVar(value=settings.get('mode', 'spreadsheet')) mode_var = tk.StringVar(value=settings.get('mode', 'spreadsheet'))
tk.Radiobutton(frame_mode, text='Spreadsheet', variable=mode_var, value='spreadsheet', fg="white", bg="black", selectcolor="black").pack(side='left', padx=8) tk.Radiobutton(frame_mode, text='Spreadsheet', variable=mode_var, value='spreadsheet', fg=win_text, bg=win_bg, selectcolor=win_bg).pack(side='left', padx=8)
tk.Radiobutton(frame_mode, text='Buttons (manual)', variable=mode_var, value='buttons', fg="white", bg="black", selectcolor="black").pack(side='left', padx=8) tk.Radiobutton(frame_mode, text='Buttons (manual)', variable=mode_var, value='buttons', fg=win_text, bg=win_bg, selectcolor=win_bg).pack(side='left', padx=8)
# Spreadsheet config # Spreadsheet config
frame_sheet = tk.LabelFrame(win, text='Spreadsheet configuration', fg='white', bg='black') frame_sheet = tk.LabelFrame(win, text='Spreadsheet configuration', fg=win_text, bg=win_bg)
frame_sheet.config(bg="black") frame_sheet.config(bg=win_bg)
frame_sheet.pack(fill='x', padx=8, pady=6) frame_sheet.pack(fill='x', padx=8, pady=6)
tk.Label(frame_sheet, text='Sheet link (CSV export):', fg='white', bg='black').pack(anchor='w') tk.Label(frame_sheet, text='Sheet link (CSV export):', fg=win_text, bg=win_bg).pack(anchor='w')
sheet_entry = tk.Entry(frame_sheet, width=80, fg='white', bg='#222', insertbackground='white') # entry background chosen to contrast with window background
sheet_entry_bg = '#222' if mode_local == 'dark' else '#b4b4b4'
sheet_entry_fg = win_text if mode_local == 'dark' else '#000000'
sheet_entry = tk.Entry(frame_sheet, width=80, fg=sheet_entry_fg, bg=sheet_entry_bg, insertbackground=sheet_entry_fg)
sheet_entry.pack(fill='x', padx=6, pady=4) sheet_entry.pack(fill='x', padx=6, pady=4)
sheet_entry.insert(0, settings.get('sheet_link', SHEET_LINK)) sheet_entry.insert(0, settings.get('sheet_link', SHEET_LINK))
# Accept cells in 'L3' format for each parameter # Accept cells in 'L3' format for each parameter
cell_frame = tk.Frame(frame_sheet) cell_frame = tk.Frame(frame_sheet)
cell_frame.config(bg="black") cell_frame.config(bg=win_bg)
cell_frame.pack(fill='x', padx=6, pady=2) cell_frame.pack(fill='x', padx=6, pady=2)
tk.Label(cell_frame, text='Range cell (e.g. L3):', fg='white', bg='black').grid(row=0, column=0) tk.Label(cell_frame, text='Range cell (e.g. L3):', fg=win_text, bg=win_bg).grid(row=0, column=0)
range_cell = tk.Entry(cell_frame, width=8, fg='white', bg='#222', insertbackground='white') range_cell_bg = '#222' if mode_local == 'dark' else '#b4b4b4'
range_cell_fg = win_text if mode_local == 'dark' else '#000000'
range_cell = tk.Entry(cell_frame, width=8, fg=range_cell_fg, bg=range_cell_bg, insertbackground=range_cell_fg)
range_cell.grid(row=0, column=1, padx=4) range_cell.grid(row=0, column=1, padx=4)
# show as L3 if present, otherwise build from numeric settings # show as L3 if present, otherwise build from numeric settings
try: try:
@@ -430,8 +535,8 @@ class CountdownApp:
except Exception: except Exception:
range_cell.insert(0, f"L3") range_cell.insert(0, f"L3")
tk.Label(cell_frame, text='Weather cell (e.g. L4):', fg='white', bg='black').grid(row=0, column=2) tk.Label(cell_frame, text='Weather cell (e.g. L4):', fg=win_text, bg=win_bg).grid(row=0, column=2)
weather_cell = tk.Entry(cell_frame, width=8, fg='white', bg='#222', insertbackground='white') weather_cell = tk.Entry(cell_frame, width=8, fg=range_cell_fg, bg=range_cell_bg, insertbackground=range_cell_fg)
weather_cell.grid(row=0, column=3, padx=4) weather_cell.grid(row=0, column=3, padx=4)
try: try:
if 'weather_cell' in settings: if 'weather_cell' in settings:
@@ -449,8 +554,8 @@ class CountdownApp:
except Exception: except Exception:
weather_cell.insert(0, f"L4") weather_cell.insert(0, f"L4")
tk.Label(cell_frame, text='Vehicle cell (e.g. L5):', fg='white', bg='black').grid(row=0, column=4) tk.Label(cell_frame, text='Vehicle cell (e.g. L5):', fg=win_text, bg=win_bg).grid(row=0, column=4)
vehicle_cell = tk.Entry(cell_frame, width=8, fg='white', bg='#222', insertbackground='white') vehicle_cell = tk.Entry(cell_frame, width=8, fg=range_cell_fg, bg=range_cell_bg, insertbackground=range_cell_fg)
vehicle_cell.grid(row=0, column=5, padx=4) vehicle_cell.grid(row=0, column=5, padx=4)
try: try:
if 'vehicle_cell' in settings: if 'vehicle_cell' in settings:
@@ -469,18 +574,23 @@ class CountdownApp:
vehicle_cell.insert(0, f"L5") vehicle_cell.insert(0, f"L5")
# Manual buttons config # Manual buttons config
frame_buttons_cfg = tk.LabelFrame(win, text='Manual Go/No-Go (Buttons mode)', fg='white', bg='black') frame_buttons_cfg = tk.LabelFrame(win, text='Manual Go/No-Go (Buttons mode)', fg=win_text, bg=win_bg)
frame_buttons_cfg.config(bg='black') frame_buttons_cfg.config(bg=win_bg)
frame_buttons_cfg.pack(fill='x', padx=8, pady=6) frame_buttons_cfg.pack(fill='x', padx=8, pady=6)
# Appearance settings are in a separate window
frame_appearance_btn = tk.Frame(win, bg=win_bg)
frame_appearance_btn.pack(fill='x', padx=8, pady=6)
tk.Button(frame_appearance_btn, text='Appearance...', command=lambda: self.show_appearance_window(), fg=btn_fg, bg=btn_bg, activebackground='#444').pack(side='left')
# Timezone selector # Timezone selector
tz_frame = tk.Frame(frame_sheet, bg='black') tz_frame = tk.Frame(frame_sheet, bg=win_bg)
tz_frame.pack(fill='x', padx=6, pady=4) tz_frame.pack(fill='x', padx=6, pady=4)
tk.Label(tz_frame, text='Timezone:', fg='white', bg='black').pack(side='left') tk.Label(tz_frame, text='Timezone:', fg=win_text, bg=win_bg).pack(side='left')
tz_var = tk.StringVar(value=settings.get('timezone', DEFAULT_SETTINGS.get('timezone', 'local'))) tz_var = tk.StringVar(value=settings.get('timezone', DEFAULT_SETTINGS.get('timezone', 'local')))
# OptionMenu with a few choices, but user may edit the text to any IANA name # OptionMenu with a few choices, but user may edit the text to any IANA name
tz_menu = tk.OptionMenu(tz_frame, tz_var, *TIMEZONE_CHOICES) tz_menu = tk.OptionMenu(tz_frame, tz_var, *TIMEZONE_CHOICES)
tz_menu.config(fg='white', bg='#222', activebackground='#333') tz_menu.config(fg=win_text, bg=range_cell_bg, activebackground='#333')
tz_menu.pack(side='left', padx=6) tz_menu.pack(side='left', padx=6)
def set_manual(val_type, val): def set_manual(val_type, val):
@@ -554,24 +664,45 @@ class CountdownApp:
'manual_range': getattr(fetch_gonogo, 'manual_range', None), 'manual_range': getattr(fetch_gonogo, 'manual_range', None),
'manual_weather': getattr(fetch_gonogo, 'manual_weather', None), 'manual_weather': getattr(fetch_gonogo, 'manual_weather', None),
'manual_vehicle': getattr(fetch_gonogo, 'manual_vehicle', None), 'manual_vehicle': getattr(fetch_gonogo, 'manual_vehicle', None),
'timezone': tz_var.get() 'timezone': tz_var.get(),
# preserve appearance settings (edited in Appearance window)
'bg_color': settings.get('bg_color', '#000000'),
'text_color': settings.get('text_color', '#FFFFFF'),
'gn_bg_color': settings.get('gn_bg_color', '#111111'),
'gn_border_color': settings.get('gn_border_color', '#FFFFFF'),
'gn_go_color': settings.get('gn_go_color', '#00FF00'),
'gn_nogo_color': settings.get('gn_nogo_color', '#FF0000'),
'font_family': settings.get('font_family', 'Consolas'),
'mission_font_px': int(settings.get('mission_font_px', 48)),
'timer_font_px': int(settings.get('timer_font_px', 120)),
'gn_font_px': int(settings.get('gn_font_px', 28))
} }
# preserve the appearance_mode so saving Settings doesn't accidentally remove it
try:
new_settings['appearance_mode'] = settings.get('appearance_mode', DEFAULT_SETTINGS.get('appearance_mode', 'dark'))
except Exception:
new_settings['appearance_mode'] = DEFAULT_SETTINGS.get('appearance_mode', 'dark')
save_settings(new_settings) save_settings(new_settings)
# update immediately # update immediately
self.gonogo_values = fetch_gonogo() self.gonogo_values = fetch_gonogo()
write_gonogo_html(self.gonogo_values) write_gonogo_html(self.gonogo_values)
# update manual visibility in main UI # update manual visibility in main UI
self.update_manual_visibility() self.update_manual_visibility()
# appearance changes are applied only from the Appearance window
win.destroy() win.destroy()
def on_cancel(): def on_cancel():
win.destroy() win.destroy()
btn_frame = tk.Frame(win) btn_frame = tk.Frame(win, bg=win_bg)
btn_frame = tk.Frame(win, bg='black')
btn_frame.pack(fill='x', pady=8) btn_frame.pack(fill='x', pady=8)
tk.Button(btn_frame, text='Save', command=on_save, fg='white', bg='#333', activebackground='#444').pack(side='right', padx=8) tk.Button(btn_frame, text='Save', command=on_save, fg=btn_fg, bg=btn_bg, activebackground='#444').pack(side='right', padx=8)
tk.Button(btn_frame, text='Cancel', command=on_cancel, fg='white', bg='#333', activebackground='#444').pack(side='right') tk.Button(btn_frame, text='Cancel', command=on_cancel, fg=btn_fg, bg=btn_bg, activebackground='#444').pack(side='right')
# ensure the new toplevel gets recursively themed like the main window
try:
self._theme_recursive(win, win_bg, win_text, btn_bg, btn_fg)
except Exception:
pass
# ---------------------------- # ----------------------------
@@ -634,6 +765,436 @@ class CountdownApp:
else: else:
self.manual_frame.pack_forget() self.manual_frame.pack_forget()
def apply_appearance_settings(self):
"""Apply appearance-related settings to the running Tk UI."""
s = load_settings()
# If an appearance_mode preset is selected, override specific settings with the preset
mode = s.get('appearance_mode', None)
if mode == 'dark':
s.update({
'bg_color': '#000000', 'text_color': '#FFFFFF', 'gn_bg_color': '#111111',
'gn_border_color': '#FFFFFF', 'gn_go_color': '#00FF00', 'gn_nogo_color': '#FF0000',
'font_family': 'Consolas', 'mission_font_px': 44, 'timer_font_px': 80, 'gn_font_px': 24
})
elif mode == 'light':
s.update({
'bg_color': '#FFFFFF', 'text_color': '#000000', 'gn_bg_color': '#EEEEEE',
'gn_border_color': '#333333', 'gn_go_color': '#008800', 'gn_nogo_color': '#AA0000',
'font_family': 'Consolas', 'mission_font_px': 44, 'timer_font_px': 80, 'gn_font_px': 24
})
bg = s.get('bg_color', '#000000')
text = s.get('text_color', '#FFFFFF')
font_family = s.get('font_family', 'Consolas')
timer_px = int(s.get('timer_font_px', 100))
mission_px = int(s.get('mission_font_px', 48))
gn_px = int(s.get('gn_font_px', 24))
gn_bg = s.get('gn_bg_color', '#111111')
gn_border = s.get('gn_border_color', '#FFFFFF')
gn_go = s.get('gn_go_color', '#00FF00')
gn_nogo = s.get('gn_nogo_color', '#FF0000')
# apply to main window elements
try:
self.root.config(bg=bg)
self.titletext.config(fg=text, bg=bg, font=(font_family, 20))
# timer label
self.text.config(fg=text, bg=bg, font=(font_family, timer_px, 'bold'))
# GN labels: set bg and font, and color depending on GO/NOGO
def style_gn_label(lbl, value):
try:
lbl.config(bg=bg, font=(font_family, gn_px))
v = (value or '').strip().upper()
if v == 'GO':
lbl.config(fg=gn_go)
elif v in ('NOGO', 'NO-GO'):
lbl.config(fg=gn_nogo)
else:
lbl.config(fg=text)
except Exception:
pass
style_gn_label(self.range_label, getattr(self, 'range_status', None))
style_gn_label(self.weather_label, getattr(self, 'weather', None))
style_gn_label(self.vehicle_label, getattr(self, 'vehicle', None))
# Buttons: invert colors depending on mode
# dark mode -> buttons white bg, black text
# light mode -> buttons black bg, white text
if mode == 'dark':
btn_bg = '#FFFFFF'
btn_fg = '#000000'
active_bg = '#DDDDDD'
else:
btn_bg = '#000000'
btn_fg = '#FFFFFF'
active_bg = '#222222'
for btn in (self.start_btn, self.hold_btn, self.resume_btn, self.scrub_btn, self.reset_btn, self.settings_btn):
try:
# preserve scrub button's custom color (red) if set
try:
cur_fg = btn.cget('fg')
except Exception:
cur_fg = None
if btn is getattr(self, 'scrub_btn', None) and cur_fg:
# keep existing foreground (usually red)
btn.config(bg=btn_bg, activebackground=active_bg)
else:
btn.config(bg=btn_bg, fg=btn_fg, activebackground=active_bg)
except Exception:
pass
# Manual toggle buttons
for btn in (self.range_toggle_btn, self.weather_toggle_btn, self.vehicle_toggle_btn):
try:
btn.config(bg=btn_bg, fg=btn_fg)
except Exception:
pass
# manual frame and footer
try:
self.manual_frame.config(bg=bg)
# Footer should invert colors depending on mode:
# - dark mode -> white background, black text
# - light mode -> black background, white text
mode = s.get('appearance_mode', 'dark')
if mode == 'dark':
footer_bg = '#FFFFFF'
footer_fg = '#000000'
else:
footer_bg = '#000000'
footer_fg = '#FFFFFF'
try:
self.footer_label.config(bg=footer_bg, fg=footer_fg)
except Exception:
# fall back to generic theme
self.footer_label.config(bg=bg, fg=text)
except Exception:
pass
except Exception:
pass
# Recursively theme frames and common widgets so no frame is left with old colors
try:
self._theme_recursive(self.root, bg, text, btn_bg, btn_fg)
except Exception:
pass
def update_gn_labels(self, range_val, weather_val, vehicle_val):
"""Update GN label texts and apply theme-aware styling."""
s = load_settings()
gn_px = int(s.get('gn_font_px', 28))
font_family = s.get('font_family', 'Consolas')
bg = s.get('bg_color', '#000000')
text = s.get('text_color', '#FFFFFF')
gn_go = s.get('gn_go_color', '#00FF00')
gn_nogo = s.get('gn_nogo_color', '#FF0000')
# Range
try:
display_range = format_status_display(range_val)
self.range_label.config(text=f"RANGE: {display_range}", bg=bg, font=(font_family, gn_px))
rv = (range_val or '').strip().upper()
rnorm = re.sub(r'[^A-Z]', '', rv)
if rnorm == 'GO':
self.range_label.config(fg=gn_go)
elif rnorm == 'NOGO':
self.range_label.config(fg=gn_nogo)
else:
self.range_label.config(fg=text)
except Exception:
pass
# Weather
try:
display_weather = format_status_display(weather_val)
self.weather_label.config(text=f"WEATHER: {display_weather}", bg=bg, font=(font_family, gn_px))
wv = (weather_val or '').strip().upper()
wnorm = re.sub(r'[^A-Z]', '', wv)
if wnorm == 'GO':
self.weather_label.config(fg=gn_go)
elif wnorm == 'NOGO':
self.weather_label.config(fg=gn_nogo)
else:
self.weather_label.config(fg=text)
except Exception:
pass
# Vehicle
try:
display_vehicle = format_status_display(vehicle_val)
self.vehicle_label.config(text=f"VEHICLE: {display_vehicle}", bg=bg, font=(font_family, gn_px))
vv = (vehicle_val or '').strip().upper()
vnorm = re.sub(r'[^A-Z]', '', vv)
if vnorm == 'GO':
self.vehicle_label.config(fg=gn_go)
elif vnorm == 'NOGO':
self.vehicle_label.config(fg=gn_nogo)
else:
self.vehicle_label.config(fg=text)
except Exception:
pass
def _theme_recursive(self, widget, bg, text, btn_bg, btn_fg):
# load settings so we can theme GN label backgrounds if configured
s = load_settings()
for child in widget.winfo_children():
# Frame and LabelFrame
try:
if isinstance(child, (tk.Frame, tk.LabelFrame)):
try:
child.config(bg=bg)
except Exception:
pass
# Labels: set bg, but don't override GN label fg
if isinstance(child, tk.Label):
try:
# preserve GN label fg colors and don't override the footer label (it has a special inverted style)
if child in (getattr(self, 'range_label', None), getattr(self, 'weather_label', None), getattr(self, 'vehicle_label', None)):
# GN labels keep fg but should have themed bg
child.config(bg=s.get('gn_bg_color', bg))
elif child is getattr(self, 'footer_label', None):
# footer_label was already styled by apply_appearance_settings; don't override it here
pass
else:
child.config(bg=bg, fg=text)
except Exception:
pass
# Entries: avoid overriding entries that were explicitly styled (like mission_entry)
if isinstance(child, tk.Entry):
try:
# Set entry bg/fg depending on appearance mode
mode_local = s.get('appearance_mode', 'dark')
if mode_local == 'dark':
child.config(bg='#222222', fg=text, insertbackground=text)
else:
# light mode entries should contrast with the white background
child.config(bg='#b4b4b4', fg='#000000', insertbackground='#000000')
except Exception:
pass
# OptionMenu/Menubutton
if isinstance(child, tk.Menubutton):
try:
child.config(bg=btn_bg, fg=btn_fg, activebackground='#555')
except Exception:
pass
# Radiobutton / Checkbutton
if isinstance(child, (tk.Radiobutton, tk.Checkbutton)):
try:
# selectcolor is the indicator background; set it to match the overall bg for neatness
child.config(bg=bg, fg=text, selectcolor=bg, activebackground=bg)
except Exception:
pass
# Buttons: ensure themed background and correct fg
if isinstance(child, tk.Button):
try:
# don't override scrub button's fg if it has a special color
if child is getattr(self, 'scrub_btn', None):
child.config(bg=btn_bg, activebackground='#555')
else:
child.config(bg=btn_bg, fg=btn_fg, activebackground='#555')
except Exception:
pass
except Exception:
pass
# Recurse
try:
if hasattr(child, 'winfo_children'):
self._theme_recursive(child, bg, text, btn_bg, btn_fg)
except Exception:
pass
def show_appearance_window(self):
# Re-implemented appearance window with proper theming and layout
settings = load_settings()
win = tk.Toplevel(self.root)
win.transient(self.root)
win.title('Appearance')
win.geometry('520x450')
# derive colors from appearance_mode so the dialog matches the main UI
mode_local = settings.get('appearance_mode', 'dark')
if mode_local == 'dark':
win_bg = '#000000'; win_text = '#FFFFFF'; btn_bg = '#FFFFFF'; btn_fg = '#000000'; entry_bg = '#222'; entry_fg = '#FFFFFF'
else:
win_bg = '#FFFFFF'; win_text = '#000000'; btn_bg = '#000000'; btn_fg = '#FFFFFF'; entry_bg = '#b4b4b4'; entry_fg = '#000000'
win.config(bg=win_bg)
tk.Label(win, text='Choose UI mode:', fg=win_text, bg=win_bg).pack(anchor='w', padx=12, pady=(10,0))
mode_var = tk.StringVar(value=settings.get('appearance_mode', 'dark'))
modes = ['dark', 'light']
mode_menu = tk.OptionMenu(win, mode_var, *modes)
mode_menu.config(fg=win_text, bg=entry_bg, activebackground='#333')
mode_menu.pack(anchor='w', padx=12, pady=6)
def on_save_mode():
choice = mode_var.get()
presets = {
'dark': {
'bg_color': '#000000', 'text_color': '#FFFFFF', 'gn_bg_color': '#111111',
'gn_border_color': '#FFFFFF', 'gn_go_color': '#00FF00', 'gn_nogo_color': '#FF0000',
'font_family': 'Consolas', 'mission_font_px': 24, 'timer_font_px': 80, 'gn_font_px': 20
},
'light': {
'bg_color': '#FFFFFF', 'text_color': '#000000', 'gn_bg_color': '#EEEEEE',
'gn_border_color': '#333333', 'gn_go_color': '#008800', 'gn_nogo_color': '#AA0000',
'font_family': 'Consolas', 'mission_font_px': 24, 'timer_font_px': 80, 'gn_font_px': 20
}
}
p = presets.get(choice, {})
s = load_settings()
s['appearance_mode'] = choice
s.update(p)
save_settings(s)
try:
self.apply_appearance_settings()
write_countdown_html(self.mission_name, self.text.cget('text'))
write_gonogo_html(self.gonogo_values)
except Exception:
pass
# close appearance window
win.destroy()
# also close the settings window if it is open
try:
if getattr(self, 'settings_win', None):
try:
self.settings_win.destroy()
except Exception:
pass
except Exception:
pass
def choose_color(entry_widget):
try:
col = colorchooser.askcolor()
if col and col[1]:
entry_widget.delete(0, tk.END)
entry_widget.insert(0, col[1])
except Exception:
pass
s = load_settings()
html_frame = tk.LabelFrame(win, text='HTML appearance (streaming)', fg=win_text, bg=win_bg)
html_frame.config(bg=win_bg)
html_frame.pack(fill='x', padx=8, pady=6)
# layout HTML appearance fields in a grid
tk.Label(html_frame, text='Background:', fg=win_text, bg=win_bg).grid(row=0, column=0, sticky='w', padx=6, pady=4)
bg_entry = tk.Entry(html_frame, width=12, fg=entry_fg, bg=entry_bg, insertbackground=entry_fg)
bg_entry.grid(row=0, column=1, padx=6, pady=4)
bg_entry.insert(0, s.get('html_bg_color', s.get('bg_color', '#000000')))
tk.Button(html_frame, text='Choose', command=lambda: choose_color(bg_entry), fg=btn_fg, bg=btn_bg).grid(row=0, column=2, padx=6)
tk.Label(html_frame, text='Text:', fg=win_text, bg=win_bg).grid(row=1, column=0, sticky='w', padx=6, pady=4)
text_entry = tk.Entry(html_frame, width=12, fg=entry_fg, bg=entry_bg, insertbackground=entry_fg)
text_entry.grid(row=1, column=1, padx=6, pady=4)
text_entry.insert(0, s.get('html_text_color', s.get('text_color', '#FFFFFF')))
tk.Button(html_frame, text='Choose', command=lambda: choose_color(text_entry), fg=btn_fg, bg=btn_bg).grid(row=1, column=2, padx=6)
tk.Label(html_frame, text='GN GO:', fg=win_text, bg=win_bg).grid(row=2, column=0, sticky='w', padx=6, pady=4)
gn_go_entry = tk.Entry(html_frame, width=12, fg=entry_fg, bg=entry_bg, insertbackground=entry_fg)
gn_go_entry.grid(row=2, column=1, padx=6, pady=4)
gn_go_entry.insert(0, s.get('html_gn_go_color', s.get('gn_go_color', '#00FF00')))
tk.Button(html_frame, text='Choose', command=lambda: choose_color(gn_go_entry), fg=btn_fg, bg=btn_bg).grid(row=2, column=2, padx=6)
tk.Label(html_frame, text='GN NO-GO:', fg=win_text, bg=win_bg).grid(row=3, column=0, sticky='w', padx=6, pady=4)
gn_nogo_entry = tk.Entry(html_frame, width=12, fg=entry_fg, bg=entry_bg, insertbackground=entry_fg)
gn_nogo_entry.grid(row=3, column=1, padx=6, pady=4)
gn_nogo_entry.insert(0, s.get('html_gn_nogo_color', s.get('gn_nogo_color', '#FF0000')))
tk.Button(html_frame, text='Choose', command=lambda: choose_color(gn_nogo_entry), fg=btn_fg, bg=btn_bg).grid(row=3, column=2, padx=6)
tk.Label(html_frame, text='GN box bg:', fg=win_text, bg=win_bg).grid(row=4, column=0, sticky='w', padx=6, pady=4)
gn_box_bg_entry = tk.Entry(html_frame, width=12, fg=entry_fg, bg=entry_bg, insertbackground=entry_fg)
gn_box_bg_entry.grid(row=4, column=1, padx=6, pady=4)
gn_box_bg_entry.insert(0, s.get('html_gn_bg_color', s.get('gn_bg_color', '#111111')))
tk.Button(html_frame, text='Choose', command=lambda: choose_color(gn_box_bg_entry), fg=btn_fg, bg=btn_bg).grid(row=4, column=2, padx=6)
tk.Label(html_frame, text='GN border:', fg=win_text, bg=win_bg).grid(row=5, column=0, sticky='w', padx=6, pady=4)
gn_border_entry = tk.Entry(html_frame, width=12, fg=entry_fg, bg=entry_bg, insertbackground=entry_fg)
gn_border_entry.grid(row=5, column=1, padx=6, pady=4)
gn_border_entry.insert(0, s.get('html_gn_border_color', s.get('gn_border_color', '#FFFFFF')))
tk.Button(html_frame, text='Choose', command=lambda: choose_color(gn_border_entry), fg=btn_fg, bg=btn_bg).grid(row=5, column=2, padx=6)
tk.Label(html_frame, text='Font family:', fg=win_text, bg=win_bg).grid(row=6, column=0, sticky='w', padx=6, pady=4)
font_entry = tk.Entry(html_frame, width=20, fg=entry_fg, bg=entry_bg, insertbackground=entry_fg)
font_entry.grid(row=6, column=1, padx=6, pady=4, columnspan=2, sticky='w')
font_entry.insert(0, s.get('html_font_family', s.get('font_family', 'Consolas')))
tk.Label(html_frame, text='Mission px:', fg=win_text, bg=win_bg).grid(row=7, column=0, sticky='w', padx=6, pady=4)
mission_px_entry = tk.Entry(html_frame, width=6, fg=entry_fg, bg=entry_bg, insertbackground=entry_fg)
mission_px_entry.grid(row=7, column=1, padx=6, pady=4, sticky='w')
mission_px_entry.insert(0, str(s.get('html_mission_font_px', s.get('mission_font_px', 24))))
tk.Label(html_frame, text='Timer px:', fg=win_text, bg=win_bg).grid(row=8, column=0, sticky='w', padx=6, pady=4)
timer_px_entry = tk.Entry(html_frame, width=6, fg=entry_fg, bg=entry_bg, insertbackground=entry_fg)
timer_px_entry.grid(row=8, column=1, padx=6, pady=4, sticky='w')
timer_px_entry.insert(0, str(s.get('html_timer_font_px', s.get('timer_font_px', 80))))
def save_html_prefs():
try:
s_local = load_settings()
s_local['html_bg_color'] = bg_entry.get().strip() or s_local.get('html_bg_color')
s_local['html_text_color'] = text_entry.get().strip() or s_local.get('html_text_color')
s_local['html_gn_go_color'] = gn_go_entry.get().strip() or s_local.get('html_gn_go_color')
s_local['html_gn_nogo_color'] = gn_nogo_entry.get().strip() or s_local.get('html_gn_nogo_color')
s_local['html_gn_bg_color'] = gn_box_bg_entry.get().strip() or s_local.get('html_gn_bg_color')
s_local['html_gn_border_color'] = gn_border_entry.get().strip() or s_local.get('html_gn_border_color')
s_local['html_font_family'] = font_entry.get().strip() or s_local.get('html_font_family')
try:
s_local['html_mission_font_px'] = int(mission_px_entry.get())
except Exception:
pass
try:
s_local['html_timer_font_px'] = int(timer_px_entry.get())
except Exception:
pass
save_settings(s_local)
write_countdown_html(self.mission_name, self.text.cget('text'))
write_gonogo_html(self.gonogo_values)
except Exception:
pass
def reset_html_defaults():
try:
s_local = load_settings()
s_local['html_bg_color'] = DEFAULT_SETTINGS.get('html_bg_color')
s_local['html_text_color'] = DEFAULT_SETTINGS.get('html_text_color')
s_local['html_font_family'] = DEFAULT_SETTINGS.get('html_font_family')
s_local['html_mission_font_px'] = DEFAULT_SETTINGS.get('html_mission_font_px')
s_local['html_timer_font_px'] = DEFAULT_SETTINGS.get('html_timer_font_px')
s_local['html_gn_bg_color'] = DEFAULT_SETTINGS.get('html_gn_bg_color')
s_local['html_gn_border_color'] = DEFAULT_SETTINGS.get('html_gn_border_color')
s_local['html_gn_go_color'] = DEFAULT_SETTINGS.get('html_gn_go_color')
s_local['html_gn_nogo_color'] = DEFAULT_SETTINGS.get('html_gn_nogo_color')
s_local['html_gn_font_px'] = DEFAULT_SETTINGS.get('html_gn_font_px')
save_settings(s_local)
# update UI fields
bg_entry.delete(0, tk.END); bg_entry.insert(0, s_local['html_bg_color'])
text_entry.delete(0, tk.END); text_entry.insert(0, s_local['html_text_color'])
gn_go_entry.delete(0, tk.END); gn_go_entry.insert(0, s_local['html_gn_go_color'])
gn_nogo_entry.delete(0, tk.END); gn_nogo_entry.insert(0, s_local['html_gn_nogo_color'])
gn_box_bg_entry.delete(0, tk.END); gn_box_bg_entry.insert(0, s_local['html_gn_bg_color'])
gn_border_entry.delete(0, tk.END); gn_border_entry.insert(0, s_local['html_gn_border_color'])
font_entry.delete(0, tk.END); font_entry.insert(0, s_local['html_font_family'])
mission_px_entry.delete(0, tk.END); mission_px_entry.insert(0, str(s_local['html_mission_font_px']))
timer_px_entry.delete(0, tk.END); timer_px_entry.insert(0, str(s_local['html_timer_font_px']))
write_countdown_html(self.mission_name, self.text.cget('text'))
write_gonogo_html(self.gonogo_values)
except Exception:
pass
html_btns = tk.Frame(html_frame, bg=win_bg)
html_btns.grid(row=9, column=0, columnspan=3, pady=6)
tk.Button(html_btns, text='Save (HTML only)', command=save_html_prefs, fg=btn_fg, bg=btn_bg).pack(side='right', padx=6)
tk.Button(html_btns, text='Reset HTML defaults', command=reset_html_defaults, fg=btn_fg, bg=btn_bg).pack(side='right')
btn_frame = tk.Frame(win, bg=win_bg)
btn_frame.pack(fill='x', pady=8, padx=8)
tk.Button(btn_frame, text='Save', command=on_save_mode, fg=btn_fg, bg=btn_bg).pack(side='right', padx=6)
tk.Button(btn_frame, text='Cancel', command=win.destroy, fg=btn_fg, bg=btn_bg).pack(side='right')
try:
self._theme_recursive(win, win_bg, win_text, btn_bg, btn_fg)
except Exception:
pass
def _toggle_manual(self, which): def _toggle_manual(self, which):
# get current values (Range, Weather, Vehicle) # get current values (Range, Weather, Vehicle)
cur = fetch_gonogo() cur = fetch_gonogo()
@@ -645,7 +1206,7 @@ class CountdownApp:
except Exception: except Exception:
cur_val = 'N/A' cur_val = 'N/A'
# toggle: if GO -> NOGO, else -> GO # toggle: if GO -> NOGO, else -> GO
new_val = 'NOGO' if cur_val == 'GO' else 'GO' new_val = 'NO-GO' if cur_val == 'GO' else 'GO'
self.set_manual(which, new_val) self.set_manual(which, new_val)
# ---------------------------- # ----------------------------
@@ -781,9 +1342,14 @@ class CountdownApp:
if now_time - self.last_gonogo_update > 0.1: if now_time - self.last_gonogo_update > 0.1:
# fetch_gonogo returns [Range, Weather, Vehicle] # fetch_gonogo returns [Range, Weather, Vehicle]
self.range_status, self.weather, self.vehicle = fetch_gonogo() self.range_status, self.weather, self.vehicle = fetch_gonogo()
self.range_label.config(text=f"RANGE: {self.range_status}", fg=get_status_color(self.range_status)) # update texts and styles using theme
self.weather_label.config(text=f"WEATHER: {self.weather}", fg=get_status_color(self.weather)) try:
self.vehicle_label.config(text=f"VEHICLE: {self.vehicle}", fg=get_status_color(self.vehicle)) self.update_gn_labels(self.range_status, self.weather, self.vehicle)
except Exception:
# fallback to simple config
self.range_label.config(text=f"RANGE: {self.range_status}")
self.weather_label.config(text=f"WEATHER: {self.weather}")
self.vehicle_label.config(text=f"VEHICLE: {self.vehicle}")
self.gonogo_values = [self.range_status, self.weather, self.vehicle] self.gonogo_values = [self.range_status, self.weather, self.vehicle]
write_gonogo_html(self.gonogo_values) write_gonogo_html(self.gonogo_values)
self.last_gonogo_update = now_time self.last_gonogo_update = now_time
@@ -816,12 +1382,22 @@ if __name__ == "__main__":
footer_frame = tk.Frame(splash, bg="black") footer_frame = tk.Frame(splash, bg="black")
footer_frame.pack(side="bottom", pady=0, fill="x") footer_frame.pack(side="bottom", pady=0, fill="x")
# Footer uses inverted colors: white bg/black text in dark mode, black bg/white text in light mode
s = load_settings()
splash_mode = s.get('appearance_mode', 'dark')
if splash_mode == 'dark':
splash_footer_bg = '#FFFFFF'
splash_footer_fg = '#000000'
else:
splash_footer_bg = '#000000'
splash_footer_fg = '#FFFFFF'
footer_label = tk.Label( footer_label = tk.Label(
footer_frame, footer_frame,
text="Made by HamsterSpaceNerd3000", # or whatever you want text="Made by HamsterSpaceNerd3000",
font=("Consolas", 12), font=("Consolas", 12),
fg="black", fg=splash_footer_fg,
bg="white" bg=splash_footer_bg
) )
footer_label.pack(fill="x") footer_label.pack(fill="x")
@@ -847,9 +1423,24 @@ if __name__ == "__main__":
if init_state['error']: if init_state['error']:
info.config(text=f"Initialization error: {init_state['error']}") info.config(text=f"Initialization error: {init_state['error']}")
else: else:
info.config(text="Ready. You may open browser sources now, then click Continue.") # show a visible countdown before auto-start; allow Continue to skip
cont_btn.config(state="normal") AUTO_START_SECONDS = 5
splash.after(5000, on_continue) remaining = AUTO_START_SECONDS
cont_btn.config(state='normal')
def tick():
nonlocal remaining
if remaining <= 0:
on_continue()
return
info.config(text=f"Ready — auto-starting in {remaining}...")
cont_btn.config(text=f"Continue ({remaining})")
remaining -= 1
splash.after(1000, tick)
# clicking Continue will immediately proceed
cont_btn.config(command=on_continue)
tick()
return return
splash.after(200, check_init) splash.after(200, check_init)
@@ -860,7 +1451,6 @@ if __name__ == "__main__":
app = CountdownApp(root) app = CountdownApp(root)
root.mainloop() root.mainloop()
cont_btn.config(command=on_continue)
# begin polling # begin polling
splash.after(100, check_init) splash.after(100, check_init)
splash.mainloop() splash.mainloop()

View File

@@ -1,9 +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. 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.4.0 THE CLOCKS UPDATE](https://github.com/HamsterSpaceNerd3000/RocketLaunchCountdown/releases/tag/Prealpha_040) Latest release: [RocketLaunchCountdown 0.5.0](https://github.com/HamsterSpaceNerd3000/RocketLaunchCountdown/releases/tag/Prealpha_050)
Features Added: Features Added:
Full rework of the clock system (countdown method based on clock time, not how long until). 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.
Minor background fixes More background optimizations and simplifications.
Added game version to title on main screen.
Added more Nitrogen
INSTALL INSTRUCTIONS INSTALL INSTRUCTIONS