diff --git a/assets/leatest-version.example.json b/assets/leatest-version.example.json new file mode 100644 index 0000000..6f3bba1 --- /dev/null +++ b/assets/leatest-version.example.json @@ -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." +} \ No newline at end of file diff --git a/utils/__pycache__/presets_manager.cpython-312.pyc b/utils/__pycache__/presets_manager.cpython-312.pyc index ef4d15f..e4f7197 100644 Binary files a/utils/__pycache__/presets_manager.cpython-312.pyc and b/utils/__pycache__/presets_manager.cpython-312.pyc differ diff --git a/utils/__pycache__/text_to_mic.cpython-312.pyc b/utils/__pycache__/text_to_mic.cpython-312.pyc index fba07e6..5a709b8 100644 Binary files a/utils/__pycache__/text_to_mic.cpython-312.pyc and b/utils/__pycache__/text_to_mic.cpython-312.pyc differ diff --git a/utils/presets_manager.py b/utils/presets_manager.py index c206520..6101925 100644 --- a/utils/presets_manager.py +++ b/utils/presets_manager.py @@ -61,7 +61,7 @@ class PresetsManager: compound=tk.LEFT, text=" Presets", command=self.toggle_presets, - style="Flat.TButton" + style="PresetsButton.TButton" ) # 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) @@ -84,6 +84,14 @@ class PresetsManager: anchor="center", 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 self.parent.style.configure("Arrow.TButton", borderwidth=0, @@ -578,6 +586,60 @@ class PresetsManager: else: 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): """ Load presets from the JSON file, copying from example if necessary. diff --git a/utils/settings_manager.py b/utils/settings_manager.py index 4aaca94..d6718ec 100644 --- a/utils/settings_manager.py +++ b/utils/settings_manager.py @@ -25,8 +25,9 @@ class SettingsManager: "model": "gpt-4o-mini", "prompt": "", "auto_apply_ai_to_recording": False, - "current_tone": "None", "hide_banner": False, + "auto_check_version": True, + "current_tone": "None", "input_device": "Default", "primary_device": "Select Device", "secondary_device": "None", diff --git a/utils/text_to_mic.py b/utils/text_to_mic.py index b552762..70e3ebe 100644 --- a/utils/text_to_mic.py +++ b/utils/text_to_mic.py @@ -8,6 +8,7 @@ import webbrowser import json import sys import time +import requests from pystray import Icon as icon, MenuItem as item, Menu as menu 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.settings_manager import SettingsManager from utils.app_text import AppText +from utils.version_checker import VersionChecker # Modify the load environment variables to load from config/.env def load_env_file(): @@ -125,14 +127,14 @@ class TextToMic(tk.Tk): self.tone_presets = TonePresetsManager.load_tone_presets(self) self.current_tone_name = self.load_current_tone_from_settings() - # Create the category variable for the dropdown - self.category_var = tk.StringVar(value="Select Category") - - # Add toggle for banner visibility before presets manager initialization - self.banner_var = tk.BooleanVar() + # Initialize settings before creating menu settings = self.load_settings() + self.banner_var = tk.BooleanVar() 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 self.presets_manager = PresetsManager(self) @@ -142,6 +144,9 @@ class TextToMic(tk.Tk): # Store reference to presets state 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 self.create_menu() self.initialize_gui() @@ -149,9 +154,18 @@ class TextToMic(tk.Tk): # Initialize our HotkeyManager 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 self.banner_var.get(): 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): """Ensure the config directory exists.""" @@ -198,9 +212,12 @@ class TextToMic(tk.Tk): 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="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) + # 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__ 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)) self.columnconfigure(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 bg_color = self.style.lookup('TFrame', 'background') @@ -373,18 +393,10 @@ class TextToMic(tk.Tk): save_frame = ttk.Frame(text_read_frame) save_frame.grid(column=1, row=0, sticky=tk.E) - # Preset Category dropdown - 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 + # Create a compact style for the button self.style.configure('Compact.TButton', padding=(2, 1)) - - # Save button with matching height - 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) + save_as_preset_button = ttk.Button(save_frame, text="Save As Preset", width=15, style='Compact.TButton', command=self.show_save_preset_dialog) + save_as_preset_button.grid(column=0, row=0, sticky=tk.E) # Text input area with proper spacing 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): """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): instruction_window = tk.Toplevel(self) @@ -1227,6 +1239,11 @@ class TextToMic(tk.Tk): # 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) + # 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): """Toggle the visibility of the presets panel.""" if hasattr(self, 'presets_manager'): @@ -1265,6 +1282,11 @@ class TextToMic(tk.Tk): if not self.presets_collapsed: 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): """Update button text based on playback state.""" try: @@ -1326,4 +1348,18 @@ class TextToMic(tk.Tk): settings["secondary_device"] = device_name 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) + diff --git a/utils/version_checker.py b/utils/version_checker.py new file mode 100644 index 0000000..fed1a3d --- /dev/null +++ b/utils/version_checker.py @@ -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() \ No newline at end of file