version checking nearly working

This commit is contained in:
Andrew Ward
2025-03-22 11:27:22 +00:00
parent 6054bd378f
commit 7633788da6
7 changed files with 276 additions and 21 deletions

View File

@@ -0,0 +1,5 @@
{
"latestVersion": "1.4.0",
"downloadUrl": "https://www.scorchsoft.com/blog/text-to-mic-for-meetings/",
"notificationMessage": "A new version of Text to Mic is available. Please update to access the latest features."
}

View File

@@ -61,7 +61,7 @@ class PresetsManager:
compound=tk.LEFT, compound=tk.LEFT,
text=" Presets", text=" Presets",
command=self.toggle_presets, command=self.toggle_presets,
style="Flat.TButton" style="PresetsButton.TButton"
) )
# Use grid instead of pack for the button to avoid mixing layout managers # Use grid instead of pack for the button to avoid mixing layout managers
self.presets_button.grid(column=0, row=0, sticky=tk.W, padx=0, pady=2) self.presets_button.grid(column=0, row=0, sticky=tk.W, padx=0, pady=2)
@@ -84,6 +84,14 @@ class PresetsManager:
anchor="center", anchor="center",
background=bg_color) background=bg_color)
# Create specific style for the presets button with smaller font
self.parent.style.configure("PresetsButton.TButton",
borderwidth=0,
highlightthickness=0,
font=("Arial", 10), # Smaller font size
anchor="center",
background=bg_color)
# Create compact styles for arrow buttons # Create compact styles for arrow buttons
self.parent.style.configure("Arrow.TButton", self.parent.style.configure("Arrow.TButton",
borderwidth=0, borderwidth=0,
@@ -578,6 +586,60 @@ class PresetsManager:
else: else:
messagebox.showinfo("Error", "Please enter text and select a category before saving.") messagebox.showinfo("Error", "Please enter text and select a category before saving.")
def show_save_preset_dialog(self):
"""Show a dialog to save the current text as a preset."""
text = self.parent.text_input.get("1.0", tk.END).strip()
if not text:
messagebox.showinfo("Error", "Please enter some text before saving as a preset.")
return
# Create dialog window
dialog = tk.Toplevel(self.parent)
dialog.title("Save As Preset")
dialog.geometry("300x200")
dialog.transient(self.parent)
dialog.grab_set()
dialog.focus_set()
# Add widgets to the dialog
ttk.Label(dialog, text="Enter preset category:").pack(pady=(15, 5))
# Add existing categories dropdown
categories = ["Select Category"] + list(set([cat["category"] for cat in self.presets]))
category_var = tk.StringVar(value="Select Category")
category_combo = ttk.Combobox(dialog, textvariable=category_var, values=categories)
category_combo.pack(pady=5, padx=20, fill=tk.X)
ttk.Label(dialog, text="Or enter a new category:").pack(pady=(10, 5))
new_category_entry = ttk.Entry(dialog)
new_category_entry.pack(pady=5, padx=20, fill=tk.X)
def save_preset():
new_category = new_category_entry.get().strip()
selected_category = category_var.get()
# Use new category if provided, otherwise use the dropdown selection
category = new_category if new_category else selected_category
if category and category != "Select Category":
self.add_preset(category, text, is_favourite=False)
messagebox.showinfo("Save Successful", f"The text has been successfully saved to the category: '{category}'.")
dialog.destroy()
else:
messagebox.showinfo("Error", "Please select an existing category or enter a new one.")
# Add save button
save_button = ttk.Button(dialog, text="Save", command=save_preset)
save_button.pack(pady=15)
# Center the dialog on the parent window
dialog.update_idletasks()
x = self.parent.winfo_x() + (self.parent.winfo_width() // 2) - (dialog.winfo_width() // 2)
y = self.parent.winfo_y() + (self.parent.winfo_height() // 2) - (dialog.winfo_height() // 2)
dialog.geometry(f"+{x}+{y}")
def load_presets(self): def load_presets(self):
""" """
Load presets from the JSON file, copying from example if necessary. Load presets from the JSON file, copying from example if necessary.

View File

@@ -25,8 +25,9 @@ class SettingsManager:
"model": "gpt-4o-mini", "model": "gpt-4o-mini",
"prompt": "", "prompt": "",
"auto_apply_ai_to_recording": False, "auto_apply_ai_to_recording": False,
"current_tone": "None",
"hide_banner": False, "hide_banner": False,
"auto_check_version": True,
"current_tone": "None",
"input_device": "Default", "input_device": "Default",
"primary_device": "Select Device", "primary_device": "Select Device",
"secondary_device": "None", "secondary_device": "None",

View File

@@ -8,6 +8,7 @@ import webbrowser
import json import json
import sys import sys
import time import time
import requests
from pystray import Icon as icon, MenuItem as item, Menu as menu from pystray import Icon as icon, MenuItem as item, Menu as menu
from PIL import Image, ImageDraw, ImageTk from PIL import Image, ImageDraw, ImageTk
@@ -27,6 +28,7 @@ from utils.presets_manager import PresetsManager
from utils.ai_editor_manager import AIEditorManager from utils.ai_editor_manager import AIEditorManager
from utils.settings_manager import SettingsManager from utils.settings_manager import SettingsManager
from utils.app_text import AppText from utils.app_text import AppText
from utils.version_checker import VersionChecker
# Modify the load environment variables to load from config/.env # Modify the load environment variables to load from config/.env
def load_env_file(): def load_env_file():
@@ -125,14 +127,14 @@ class TextToMic(tk.Tk):
self.tone_presets = TonePresetsManager.load_tone_presets(self) self.tone_presets = TonePresetsManager.load_tone_presets(self)
self.current_tone_name = self.load_current_tone_from_settings() self.current_tone_name = self.load_current_tone_from_settings()
# Create the category variable for the dropdown # Initialize settings before creating menu
self.category_var = tk.StringVar(value="Select Category")
# Add toggle for banner visibility before presets manager initialization
self.banner_var = tk.BooleanVar()
settings = self.load_settings() settings = self.load_settings()
self.banner_var = tk.BooleanVar()
self.banner_var.set(settings.get("hide_banner", False)) self.banner_var.set(settings.get("hide_banner", False))
# Initialize auto_check_version before creating menu
self.auto_check_version = tk.BooleanVar(value=settings.get("auto_check_version", True))
# Create the presets manager before initializing the GUI # Create the presets manager before initializing the GUI
self.presets_manager = PresetsManager(self) self.presets_manager = PresetsManager(self)
@@ -142,6 +144,9 @@ class TextToMic(tk.Tk):
# Store reference to presets state # Store reference to presets state
self.presets_collapsed = self.presets_manager.presets_collapsed self.presets_collapsed = self.presets_manager.presets_collapsed
# Initialize the main frame as a class variable for version notification to work
self.main_frame = None
# Create menu and initialize GUI after presets manager is created # Create menu and initialize GUI after presets manager is created
self.create_menu() self.create_menu()
self.initialize_gui() self.initialize_gui()
@@ -149,9 +154,18 @@ class TextToMic(tk.Tk):
# Initialize our HotkeyManager # Initialize our HotkeyManager
self.hotkey_manager = HotkeyManager(self) self.hotkey_manager = HotkeyManager(self)
# Initialize version checker
self.version_checker = VersionChecker(self, self.version)
# If banner should be hidden based on settings, hide it now # If banner should be hidden based on settings, hide it now
if self.banner_var.get(): if self.banner_var.get():
self.toggle_banner() self.toggle_banner()
# Schedule version check after app is fully loaded
# Only check automatically if the setting is enabled
if self.auto_check_version.get():
# Delay the check to ensure UI is fully loaded
self.after(2000, self.version_checker.check_version, False)
def ensure_config_directory(self): def ensure_config_directory(self):
"""Ensure the config directory exists.""" """Ensure the config directory exists."""
@@ -198,9 +212,12 @@ class TextToMic(tk.Tk):
self.menubar.add_cascade(label="Help", menu=help_menu) self.menubar.add_cascade(label="Help", menu=help_menu)
help_menu.add_command(label="How to Use", command=self.show_instructions) help_menu.add_command(label="How to Use", command=self.show_instructions)
help_menu.add_command(label="Terms of Use and Licence", command=self.show_terms_of_use) help_menu.add_command(label="Terms of Use and Licence", command=self.show_terms_of_use)
help_menu.add_command(label="Version", command=self.show_version) help_menu.add_command(label="Check Version", command=self.check_version)
help_menu.add_command(label="Hotkey Instructions", command=self.show_hotkey_instructions) help_menu.add_command(label="Hotkey Instructions", command=self.show_hotkey_instructions)
# Add toggle for automatic version checking
help_menu.add_checkbutton(label="Auto Check for Updates", variable=self.auto_check_version, command=self.toggle_auto_version_check)
# Add toggle for banner visibility - use the existing banner_var from __init__ # Add toggle for banner visibility - use the existing banner_var from __init__
help_menu.add_checkbutton(label="Hide Banner", variable=self.banner_var, command=self.toggle_banner) help_menu.add_checkbutton(label="Hide Banner", variable=self.banner_var, command=self.toggle_banner)
@@ -272,6 +289,9 @@ class TextToMic(tk.Tk):
main_frame.grid(column=0, row=0, sticky=(tk.W, tk.E, tk.N, tk.S)) main_frame.grid(column=0, row=0, sticky=(tk.W, tk.E, tk.N, tk.S))
self.columnconfigure(0, weight=1) self.columnconfigure(0, weight=1)
self.rowconfigure(0, weight=1) self.rowconfigure(0, weight=1)
# Store reference to main_frame for version notification
self.main_frame = main_frame
# Use the background color from our style for the text widget # Use the background color from our style for the text widget
bg_color = self.style.lookup('TFrame', 'background') bg_color = self.style.lookup('TFrame', 'background')
@@ -373,18 +393,10 @@ class TextToMic(tk.Tk):
save_frame = ttk.Frame(text_read_frame) save_frame = ttk.Frame(text_read_frame)
save_frame.grid(column=1, row=0, sticky=tk.E) save_frame.grid(column=1, row=0, sticky=tk.E)
# Preset Category dropdown # Create a compact style for the button
categories = [cat["category"] for cat in self.presets_manager.presets]
category_menu = ttk.OptionMenu(save_frame, self.category_var, *categories)
category_menu.grid(column=0, row=0, sticky=tk.E, padx=(0, 5))
category_menu.config(style='Compact.TMenubutton')
# Create a compact style for the Save button to match dropdown height
self.style.configure('Compact.TButton', padding=(2, 1)) self.style.configure('Compact.TButton', padding=(2, 1))
save_as_preset_button = ttk.Button(save_frame, text="Save As Preset", width=15, style='Compact.TButton', command=self.show_save_preset_dialog)
# Save button with matching height save_as_preset_button.grid(column=0, row=0, sticky=tk.E)
save_button = ttk.Button(save_frame, text="Save", width=8, style='Compact.TButton', command=self.save_current_text_as_preset)
save_button.grid(column=1, row=0, sticky=tk.E)
# Text input area with proper spacing # Text input area with proper spacing
self.text_input = tk.Text(main_frame, height=5, width=68) self.text_input = tk.Text(main_frame, height=5, width=68)
@@ -485,7 +497,7 @@ class TextToMic(tk.Tk):
def save_current_text_as_preset(self): def save_current_text_as_preset(self):
"""Forward the save request to the presets manager.""" """Forward the save request to the presets manager."""
self.presets_manager.save_current_text_as_preset() self.show_save_preset_dialog()
def show_instructions(self): def show_instructions(self):
instruction_window = tk.Toplevel(self) instruction_window = tk.Toplevel(self)
@@ -1227,6 +1239,11 @@ class TextToMic(tk.Tk):
# Use grid (not pack) to ensure proper positioning # Use grid (not pack) to ensure proper positioning
self.presets_manager.presets_button.grid_configure(column=0, row=0, sticky=tk.W, padx=0, pady=2) self.presets_manager.presets_button.grid_configure(column=0, row=0, sticky=tk.W, padx=0, pady=2)
# If we have a version notification visible, ensure it remains at the top
if hasattr(self, 'version_checker') and self.version_checker.notification_visible:
self.version_checker.notification_frame.grid(row=0, column=0, sticky="ew")
self.main_frame.grid(row=1, column=0, sticky="nsew")
def toggle_presets(self): def toggle_presets(self):
"""Toggle the visibility of the presets panel.""" """Toggle the visibility of the presets panel."""
if hasattr(self, 'presets_manager'): if hasattr(self, 'presets_manager'):
@@ -1265,6 +1282,11 @@ class TextToMic(tk.Tk):
if not self.presets_collapsed: if not self.presets_collapsed:
self.presets_manager.refresh_presets_display() self.presets_manager.refresh_presets_display()
# If we have a version notification visible, ensure it remains at the top
if hasattr(self, 'version_checker') and self.version_checker.notification_visible:
self.version_checker.notification_frame.grid(row=0, column=0, sticky="ew")
self.main_frame.grid(row=1, column=0, sticky="nsew")
def update_buttons_for_playback(self, is_playing): def update_buttons_for_playback(self, is_playing):
"""Update button text based on playback state.""" """Update button text based on playback state."""
try: try:
@@ -1326,4 +1348,18 @@ class TextToMic(tk.Tk):
settings["secondary_device"] = device_name settings["secondary_device"] = device_name
self.save_settings_to_JSON(settings) self.save_settings_to_JSON(settings)
def show_save_preset_dialog(self):
"""Show the save preset dialog."""
self.presets_manager.show_save_preset_dialog()
def check_version(self):
"""Run the version checker and show the result"""
self.version_checker.check_version(True) # True means show result even if no update available
def toggle_auto_version_check(self):
"""Toggle automatic version checking and save the setting"""
settings = self.load_settings()
settings["auto_check_version"] = self.auto_check_version.get()
self.save_settings_to_JSON(settings)

151
utils/version_checker.py Normal file
View File

@@ -0,0 +1,151 @@
import threading
import requests
import json
import webbrowser
import tkinter as tk
from tkinter import ttk, messagebox
from packaging import version
import time
class VersionChecker:
def __init__(self, app, version):
self.app = app
self.current_version = version
self.version_url = "https://www.scorchsoft.com/public/blog/text-to-mic/leatest-version.json"
self.notification_visible = False
self.notification_frame = None
def check_version(self, show_result=True):
"""
Check if a new version is available.
If show_result is True, show a message even if no new version is found.
"""
thread = threading.Thread(target=self._check_version_thread, args=(show_result,))
thread.daemon = True # Make thread terminate when main program exits
thread.start()
def _check_version_thread(self, show_result):
"""Run the version check in a background thread to avoid blocking the UI"""
try:
# Add a small timeout to prevent hanging
response = requests.get(self.version_url, timeout=5)
if response.status_code == 200:
data = response.json()
latest_version = data.get("latestVersion")
download_url = data.get("downloadUrl")
message = data.get("notificationMessage")
# Ensure these values are present
if not latest_version or not download_url:
if show_result:
self.app.after(0, lambda: messagebox.showwarning(
"Version Check Failed",
"The update information is incomplete. Please try again later."
))
return
# Use packaging.version for proper version comparison
try:
if version.parse(latest_version) > version.parse(self.current_version):
# New version available - show notification in UI thread
self.app.after(0, lambda: self.show_update_notification(latest_version, download_url, message))
elif show_result:
# No new version, but user requested check
self.app.after(0, lambda: messagebox.showinfo(
"Version Check",
f"You have the latest version ({self.current_version})."
))
except (version.InvalidVersion, TypeError) as e:
if show_result:
self.app.after(0, lambda: messagebox.showwarning(
"Version Check Failed",
f"Could not compare versions: {str(e)}"
))
else:
if show_result:
self.app.after(0, lambda: messagebox.showwarning(
"Version Check Failed",
f"Could not check for updates. Server returned status code: {response.status_code}"
))
except requests.RequestException as e:
if show_result:
self.app.after(0, lambda: messagebox.showwarning(
"Version Check Failed",
f"Could not connect to update server: {str(e)}"
))
except json.JSONDecodeError:
if show_result:
self.app.after(0, lambda: messagebox.showwarning(
"Version Check Failed",
"Invalid update information received."
))
except Exception as e:
if show_result:
self.app.after(0, lambda: messagebox.showwarning(
"Version Check Failed",
f"Could not check for updates: {str(e)}"
))
def show_update_notification(self, latest_version, download_url, message):
"""Display an update notification banner in the app"""
if self.notification_visible:
return # Already showing notification
# Create notification frame
self.notification_frame = ttk.Frame(self.app, style='Notification.TFrame')
# Configure notification style (light yellow background)
self.app.style.configure('Notification.TFrame', background='#fff3cd')
self.app.style.configure('Notification.TLabel', background='#fff3cd', foreground='#856404')
self.app.style.configure('Notification.TButton', background='#fff3cd')
# Create notification content
notification_text = message or f"A new version ({latest_version}) is available. You're currently using version {self.current_version}."
label = ttk.Label(
self.notification_frame,
text=notification_text,
style='Notification.TLabel',
wraplength=400
)
label.grid(row=0, column=0, padx=(10, 5), pady=10, sticky="w")
# Create buttons
download_button = ttk.Button(
self.notification_frame,
text="Download",
command=lambda: self.open_download_page(download_url)
)
download_button.grid(row=0, column=1, padx=5, pady=10)
close_button = ttk.Button(
self.notification_frame,
text="×",
width=2,
command=self.dismiss_notification
)
close_button.grid(row=0, column=2, padx=(0, 5), pady=10)
# Insert at the top of the application, below menu
self.notification_frame.grid(row=0, column=0, sticky="ew", padx=0, pady=0)
# Move other content down
self.app.main_frame.grid(row=1, column=0, sticky="nsew")
self.notification_visible = True
def dismiss_notification(self):
"""Remove the notification banner"""
if self.notification_frame:
self.notification_frame.grid_forget()
self.notification_frame = None
# Move main frame back to top position
self.app.main_frame.grid(row=0, column=0, sticky="nsew")
self.notification_visible = False
def open_download_page(self, url):
"""Open the download URL in a web browser"""
webbrowser.open(url)
self.dismiss_notification()