update presets performance and refactor class, plus change more icons
BIN
assets/icons/chevron-down-black.png
Normal file
|
After Width: | Height: | Size: 467 B |
BIN
assets/icons/chevron-left-black.png
Normal file
|
After Width: | Height: | Size: 446 B |
BIN
assets/icons/chevron-right-black.png
Normal file
|
After Width: | Height: | Size: 425 B |
BIN
assets/icons/chevron-up-black.png
Normal file
|
After Width: | Height: | Size: 465 B |
BIN
assets/icons/delete-black.png
Normal file
|
After Width: | Height: | Size: 528 B |
BIN
assets/icons/delete-white.png
Normal file
|
After Width: | Height: | Size: 553 B |
BIN
assets/icons/heart-black.png
Normal file
|
After Width: | Height: | Size: 942 B |
BIN
assets/icons/heart-fill-black.png
Normal file
|
After Width: | Height: | Size: 726 B |
BIN
assets/icons/heart-fill-white.png
Normal file
|
After Width: | Height: | Size: 772 B |
BIN
assets/icons/heart-white.png
Normal file
|
After Width: | Height: | Size: 1003 B |
9
check_image_size.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from PIL import Image
|
||||
import os
|
||||
|
||||
icon_dir = 'assets/icons'
|
||||
for filename in os.listdir(icon_dir):
|
||||
if filename.endswith('.png'):
|
||||
path = os.path.join(icon_dir, filename)
|
||||
img = Image.open(path)
|
||||
print(f"{filename}: {img.size}")
|
||||
BIN
utils/__pycache__/presets_manager.cpython-312.pyc
Normal file
545
utils/presets_manager.py
Normal file
@@ -0,0 +1,545 @@
|
||||
import tkinter as tk
|
||||
from tkinter import ttk, messagebox, Frame, Canvas, Scrollbar
|
||||
import json
|
||||
from pathlib import Path
|
||||
from PIL import Image, ImageTk
|
||||
import threading
|
||||
import time
|
||||
|
||||
class PresetsManager:
|
||||
"""
|
||||
A class to manage the presets functionality in the Text to Mic application.
|
||||
This handles the display, navigation, and interaction with text presets.
|
||||
"""
|
||||
|
||||
def __init__(self, parent):
|
||||
"""
|
||||
Initialize the PresetsManager.
|
||||
|
||||
Args:
|
||||
parent: The parent application instance (TextToMic)
|
||||
"""
|
||||
self.parent = parent
|
||||
self.presets = self.load_presets()
|
||||
self.current_category = "All"
|
||||
self.presets_collapsed = True
|
||||
self.icon_cache = {}
|
||||
|
||||
# Add save debounce variables
|
||||
self.save_pending = False
|
||||
self.save_timer = None
|
||||
self.preset_cards = {} # Dictionary to store references to preset cards for efficient updates
|
||||
|
||||
# Load navigation icons
|
||||
self.chevron_right = self.get_icon("assets/icons/chevron-right-black.png", 16)
|
||||
self.chevron_down = self.get_icon("assets/icons/chevron-down-black.png", 16)
|
||||
self.chevron_left = self.get_icon("assets/icons/chevron-left-black.png", 16)
|
||||
self.chevron_right_small = self.get_icon("assets/icons/chevron-right-black.png", 16)
|
||||
|
||||
# Initialize the UI components
|
||||
self.create_presets_section()
|
||||
|
||||
def create_presets_section(self):
|
||||
"""Create the presets section UI with accordion behavior."""
|
||||
# Accordion frame to show/hide presets section
|
||||
self.presets_frame = ttk.Frame(self.parent)
|
||||
|
||||
# Create toggle button with icon instead of text
|
||||
toggle_frame = ttk.Frame(self.parent)
|
||||
toggle_frame.grid(column=0, row=6, columnspan=2, sticky=tk.W)
|
||||
|
||||
# Use an icon for the toggle button
|
||||
self.presets_button = ttk.Button(
|
||||
toggle_frame,
|
||||
image=self.chevron_right,
|
||||
compound=tk.LEFT,
|
||||
text=" Presets",
|
||||
command=self.toggle_presets,
|
||||
style="Flat.TButton"
|
||||
)
|
||||
self.presets_button.pack(side=tk.LEFT, padx=0, pady=2)
|
||||
|
||||
self.presets_frame.grid(column=0, row=7, columnspan=2, sticky=(tk.W, tk.E))
|
||||
|
||||
# Tabs for categories with scrolling arrows
|
||||
self.tab_frame = ttk.Frame(self.presets_frame)
|
||||
self.tab_frame.pack(fill=tk.X)
|
||||
|
||||
# Style for flat buttons
|
||||
bg_color = self.parent.style.lookup('TFrame', 'background')
|
||||
accent_color = "#e0e0e4" # Slightly darker grey for accents
|
||||
|
||||
self.parent.style.configure("Flat.TButton",
|
||||
borderwidth=0,
|
||||
highlightthickness=0,
|
||||
font=("Arial", 12),
|
||||
anchor="center",
|
||||
background=bg_color)
|
||||
|
||||
# Create compact styles for arrow buttons
|
||||
self.parent.style.configure("Arrow.TButton",
|
||||
borderwidth=0,
|
||||
highlightthickness=0,
|
||||
padding=2,
|
||||
background=bg_color)
|
||||
|
||||
# Create common styles for preset cards
|
||||
self.setup_preset_card_styles(bg_color, accent_color)
|
||||
|
||||
# Left arrow with icon
|
||||
self.left_arrow = ttk.Button(
|
||||
self.tab_frame,
|
||||
image=self.chevron_left,
|
||||
command=self.scroll_left,
|
||||
style="Arrow.TButton"
|
||||
)
|
||||
self.left_arrow.pack(side=tk.LEFT, padx=1)
|
||||
|
||||
# Canvas for scrolling tabs horizontally, removing the horizontal scrollbar
|
||||
self.tabs_canvas = Canvas(self.tab_frame, height=30, bg=bg_color, highlightthickness=0)
|
||||
self.tabs_canvas.pack(side=tk.LEFT, fill=tk.X, expand=True)
|
||||
self.tabs_frame_inner = ttk.Frame(self.tabs_canvas)
|
||||
self.tabs_canvas.create_window((0, 0), window=self.tabs_frame_inner, anchor="nw")
|
||||
|
||||
# Right arrow with icon
|
||||
self.right_arrow = ttk.Button(
|
||||
self.tab_frame,
|
||||
image=self.chevron_right_small,
|
||||
command=self.scroll_right,
|
||||
style="Arrow.TButton"
|
||||
)
|
||||
self.right_arrow.pack(side=tk.RIGHT, padx=1)
|
||||
|
||||
# Presets display area with a fixed height and vertical scrollbar
|
||||
self.presets_canvas = Canvas(self.presets_frame, height=250, width=self.presets_frame.winfo_width(),
|
||||
bg=bg_color, highlightthickness=0)
|
||||
self.presets_scrollbar = Scrollbar(self.presets_frame, orient="vertical", command=self.presets_canvas.yview)
|
||||
self.presets_canvas.configure(yscrollcommand=self.presets_scrollbar.set)
|
||||
|
||||
# Frame inside the canvas to hold presets
|
||||
self.presets_scrollable_frame = ttk.Frame(self.presets_canvas)
|
||||
self.presets_canvas.create_window((0, 0), window=self.presets_scrollable_frame, anchor="nw")
|
||||
|
||||
# Pack the canvas and scrollbar
|
||||
self.presets_canvas.pack(side=tk.LEFT, fill=tk.X, expand=True)
|
||||
self.presets_scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
|
||||
|
||||
# Configure the scroll region to update when the frame changes
|
||||
self.presets_scrollable_frame.bind("<Configure>",
|
||||
lambda e: self.presets_canvas.configure(scrollregion=self.presets_canvas.bbox("all")))
|
||||
|
||||
# Populate tabs and presets
|
||||
self.populate_tabs() # Refresh tabs to show selection
|
||||
self.refresh_presets_display()
|
||||
# Initially toggle to show/hide based on the default state
|
||||
self.toggle_presets()
|
||||
self.toggle_presets()
|
||||
self.enable_mouse_wheel_scrolling()
|
||||
|
||||
def setup_preset_card_styles(self, bg_color, accent_color):
|
||||
"""Set up common styles for preset cards once to avoid recreating them repeatedly."""
|
||||
preset_bg_color = "#f0f4f8" # Light blue-gray that contrasts with the main background
|
||||
text_color = self.parent.style.lookup('TLabel', 'foreground')
|
||||
|
||||
# Common styles for cards, buttons, and labels
|
||||
self.parent.style.configure('PresetCard.TFrame',
|
||||
background=preset_bg_color,
|
||||
borderwidth=0,
|
||||
relief="flat")
|
||||
|
||||
self.parent.style.configure('PresetBottom.TFrame',
|
||||
background=preset_bg_color)
|
||||
|
||||
self.parent.style.configure('PresetLabel.TLabel',
|
||||
background=preset_bg_color,
|
||||
foreground=text_color)
|
||||
|
||||
self.parent.style.configure('PresetButton.TButton',
|
||||
borderwidth=0,
|
||||
highlightthickness=0,
|
||||
background=preset_bg_color)
|
||||
|
||||
# Cache common icons
|
||||
self.heart_icon = self.get_icon("assets/icons/heart-black.png", 24)
|
||||
self.heart_filled_icon = self.get_icon("assets/icons/heart-fill-black.png", 24)
|
||||
self.delete_icon = self.get_icon("assets/icons/delete-black.png", 24)
|
||||
|
||||
def scroll_left(self):
|
||||
"""Scroll the tabs canvas to the left."""
|
||||
self.tabs_canvas.xview_scroll(-5, "units")
|
||||
|
||||
def scroll_right(self):
|
||||
"""Scroll the tabs canvas to the right."""
|
||||
self.tabs_canvas.xview_scroll(5, "units")
|
||||
|
||||
def enable_mouse_wheel_scrolling(self):
|
||||
"""Enable conditional mouse wheel scrolling for the presets canvas and category tabs canvas."""
|
||||
|
||||
def on_vertical_scroll(event):
|
||||
# Scroll the presets_canvas vertically
|
||||
if event.num == 4: # macOS scroll up
|
||||
self.presets_canvas.yview_scroll(-1, "units")
|
||||
elif event.num == 5: # macOS scroll down
|
||||
self.presets_canvas.yview_scroll(1, "units")
|
||||
else: # Windows and Linux
|
||||
self.presets_canvas.yview_scroll(int(-1 * (event.delta / 120)), "units")
|
||||
|
||||
def on_horizontal_scroll(event):
|
||||
# Scroll the tabs_canvas horizontally
|
||||
if event.num == 4: # macOS scroll left
|
||||
self.tabs_canvas.xview_scroll(-1, "units")
|
||||
elif event.num == 5: # macOS scroll right
|
||||
self.tabs_canvas.xview_scroll(1, "units")
|
||||
else: # Windows and Linux
|
||||
self.tabs_canvas.xview_scroll(int(-1 * (event.delta / 120)), "units")
|
||||
|
||||
# Bind scroll events when mouse enters or leaves the presets canvas area
|
||||
self.presets_canvas.bind("<Enter>", lambda e: self.presets_canvas.bind_all("<MouseWheel>", on_vertical_scroll))
|
||||
self.presets_canvas.bind("<Leave>", lambda e: self.presets_canvas.unbind_all("<MouseWheel>"))
|
||||
|
||||
# Bind scroll events when mouse enters or leaves the tabs canvas area
|
||||
self.tabs_canvas.bind("<Enter>", lambda e: self.tabs_canvas.bind_all("<MouseWheel>", on_horizontal_scroll))
|
||||
self.tabs_canvas.bind("<Leave>", lambda e: self.tabs_canvas.unbind_all("<MouseWheel>"))
|
||||
|
||||
def populate_tabs(self):
|
||||
"""Populate the tabs for different preset categories."""
|
||||
# Clear current tabs
|
||||
for widget in self.tabs_frame_inner.winfo_children():
|
||||
widget.destroy()
|
||||
|
||||
# Add "All" and "Favourites" tabs along with dynamic categories
|
||||
for category in ["All", "Favourites"] + [cat["category"] for cat in self.presets if cat["category"] not in ["All", "Favourites"]]:
|
||||
btn = ttk.Button(self.tabs_frame_inner, text=category, command=lambda c=category: self.switch_category(c))
|
||||
btn.pack(side=tk.LEFT, padx=2)
|
||||
|
||||
# Style selected category
|
||||
if category == self.current_category:
|
||||
btn.state(['pressed']) # Visual style for selected tab
|
||||
else:
|
||||
btn.state(['!pressed'])
|
||||
|
||||
def switch_category(self, category):
|
||||
"""Switch displayed category."""
|
||||
self.current_category = category
|
||||
self.populate_tabs() # Refresh tabs to show selection
|
||||
self.refresh_presets_display()
|
||||
|
||||
def refresh_presets_display(self):
|
||||
"""Refresh displayed presets based on selected category."""
|
||||
# Clear existing items in the scrollable frame
|
||||
for widget in self.presets_scrollable_frame.winfo_children():
|
||||
widget.destroy()
|
||||
|
||||
# Clear the preset cards tracking dictionary
|
||||
self.preset_cards = {}
|
||||
|
||||
# Debounce - cancel any previous refresh call if pending
|
||||
if hasattr(self, 'refresh_handle'):
|
||||
self.parent.after_cancel(self.refresh_handle)
|
||||
self.refresh_handle = self.parent.after(100, self._populate_presets)
|
||||
|
||||
def _populate_presets(self):
|
||||
"""Populate presets into grid layout with responsive columns."""
|
||||
# Filter presets based on current category
|
||||
display_phrases = []
|
||||
if self.current_category == "All":
|
||||
for cat in self.presets:
|
||||
display_phrases.extend(cat["phrases"])
|
||||
elif self.current_category == "Favourites":
|
||||
for cat in self.presets:
|
||||
display_phrases.extend([p for p in cat["phrases"] if p["isFavourite"]])
|
||||
else:
|
||||
display_phrases = next((cat["phrases"] for cat in self.presets if cat["category"] == self.current_category), [])
|
||||
|
||||
# Update canvas width to calculate dynamic column width
|
||||
self.presets_canvas.update_idletasks()
|
||||
preset_width = max(self.presets_canvas.winfo_width() // 3, 150) # Minimum width of 150
|
||||
preset_height = 100
|
||||
|
||||
# Configure columns to fill available space
|
||||
for col in range(3):
|
||||
self.presets_scrollable_frame.columnconfigure(col, weight=1)
|
||||
|
||||
# Populate filtered presets in grid layout
|
||||
for i, phrase in enumerate(display_phrases):
|
||||
# Create a unique identifier for the card
|
||||
card_id = f"{phrase['text']}"
|
||||
|
||||
# Create a frame with no border for cleaner look
|
||||
frame = ttk.Frame(self.presets_scrollable_frame, width=preset_width, height=preset_height)
|
||||
frame.grid(row=i // 4, column=i % 4, padx=3, pady=3, sticky="nsew")
|
||||
frame.grid_propagate(False)
|
||||
|
||||
# Create inner frame with distinct background and no border - use common style
|
||||
inner_frame = ttk.Frame(frame, style='PresetCard.TFrame')
|
||||
inner_frame.pack(fill=tk.BOTH, expand=True, padx=1, pady=1)
|
||||
|
||||
self.presets_scrollable_frame.grid_columnconfigure(i % 4, weight=1) # Make columns expandable
|
||||
self.presets_scrollable_frame.grid_rowconfigure(i // 4, weight=1) # Make rows expandable
|
||||
|
||||
# Text label with truncation for long text - use common style
|
||||
wrapped_text = self.wrap_text(phrase["text"], max_lines=3, max_chars_per_line=20)
|
||||
label = ttk.Label(inner_frame, text=wrapped_text, anchor="center", justify="center",
|
||||
width=20, style='PresetLabel.TLabel')
|
||||
label.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
|
||||
label.bind("<Button-1>", lambda e, t=phrase["text"]: self.insert_text(t))
|
||||
label.bind("<Double-Button-1>", lambda e, t=phrase["text"]: self.play_preset(t))
|
||||
|
||||
# Bottom frame for icons - use common style
|
||||
bottom_frame = ttk.Frame(inner_frame, style='PresetBottom.TFrame')
|
||||
bottom_frame.pack(side=tk.BOTTOM, fill=tk.X, pady=2)
|
||||
|
||||
# Choose correct heart icon based on favorite status
|
||||
fav_img = self.heart_filled_icon if phrase["isFavourite"] else self.heart_icon
|
||||
|
||||
# Favourite button with image - use common style and command that includes card_id
|
||||
fav_btn = ttk.Button(bottom_frame, image=fav_img,
|
||||
command=lambda p=phrase, c_id=card_id: self.toggle_favourite(p, c_id),
|
||||
style='PresetButton.TButton')
|
||||
fav_btn.pack(side=tk.RIGHT, padx=2)
|
||||
|
||||
# Delete button with image - use common style
|
||||
del_btn = ttk.Button(bottom_frame, image=self.delete_icon,
|
||||
command=lambda t=phrase["text"]: self.delete_preset(self.current_category, t),
|
||||
style='PresetButton.TButton')
|
||||
del_btn.pack(side=tk.RIGHT, padx=2)
|
||||
|
||||
# Store references to the card components for efficient updates
|
||||
self.preset_cards[card_id] = {
|
||||
'frame': frame,
|
||||
'inner_frame': inner_frame,
|
||||
'label': label,
|
||||
'bottom_frame': bottom_frame,
|
||||
'fav_btn': fav_btn,
|
||||
'del_btn': del_btn,
|
||||
'phrase': phrase
|
||||
}
|
||||
|
||||
# Update scroll region after populating all items
|
||||
self.presets_canvas.configure(scrollregion=self.presets_canvas.bbox("all"))
|
||||
|
||||
def update_preset_card(self, card_id):
|
||||
"""Update a single preset card without refreshing the entire display."""
|
||||
if card_id not in self.preset_cards:
|
||||
return
|
||||
|
||||
card = self.preset_cards[card_id]
|
||||
phrase = card['phrase']
|
||||
|
||||
# Update the favorite button image based on current state
|
||||
fav_img = self.heart_filled_icon if phrase["isFavourite"] else self.heart_icon
|
||||
card['fav_btn'].configure(image=fav_img)
|
||||
|
||||
def get_icon(self, icon_path, size=24):
|
||||
"""
|
||||
Load and resize an icon, caching the result for later use.
|
||||
|
||||
Args:
|
||||
icon_path: Path to the icon file
|
||||
size: Size to resize the icon to (square)
|
||||
|
||||
Returns:
|
||||
A PhotoImage object with the icon
|
||||
"""
|
||||
# Create a cache key based on path and size
|
||||
cache_key = f"{icon_path}_{size}"
|
||||
|
||||
# Check if icon is already in cache
|
||||
if cache_key in self.icon_cache:
|
||||
return self.icon_cache[cache_key]
|
||||
|
||||
# Load and resize the icon
|
||||
try:
|
||||
# Use resource_path to get the correct path
|
||||
full_path = self.parent.resource_path(icon_path)
|
||||
|
||||
# Use PIL to open and resize the image
|
||||
img = Image.open(full_path)
|
||||
img = img.resize((size, size), Image.LANCZOS)
|
||||
|
||||
# Convert to PhotoImage
|
||||
photo_img = ImageTk.PhotoImage(img)
|
||||
|
||||
# Store in cache
|
||||
self.icon_cache[cache_key] = photo_img
|
||||
|
||||
return photo_img
|
||||
except Exception as e:
|
||||
print(f"Error loading icon {icon_path}: {e}")
|
||||
# Return a default empty image
|
||||
return tk.PhotoImage(width=size, height=size)
|
||||
|
||||
def wrap_text(self, text, max_lines=3, max_chars_per_line=20):
|
||||
"""
|
||||
Wrap text to fit within a limited number of lines and characters.
|
||||
|
||||
Args:
|
||||
text: The text to wrap
|
||||
max_lines: Maximum number of lines to display
|
||||
max_chars_per_line: Maximum characters per line
|
||||
|
||||
Returns:
|
||||
The wrapped text with ellipsis if truncated
|
||||
"""
|
||||
words = text.split()
|
||||
wrapped_text = ""
|
||||
line = ""
|
||||
line_count = 0
|
||||
|
||||
for word in words:
|
||||
if len(line + word) <= max_chars_per_line:
|
||||
line += word + " "
|
||||
else:
|
||||
wrapped_text += line.strip() + "\n"
|
||||
line = word + " "
|
||||
line_count += 1
|
||||
if line_count >= max_lines - 1: # Leave space for ellipsis
|
||||
break
|
||||
|
||||
# Add final line and handle overflow with ellipsis
|
||||
wrapped_text += line.strip()
|
||||
if line_count >= max_lines - 1 and len(wrapped_text.splitlines()) >= max_lines:
|
||||
wrapped_text = "\n".join(wrapped_text.splitlines()[:max_lines - 1]) + "\n..."
|
||||
|
||||
return wrapped_text
|
||||
|
||||
def insert_text(self, text):
|
||||
"""Insert preset text into the text area."""
|
||||
self.parent.text_input.delete("1.0", tk.END)
|
||||
self.parent.text_input.insert("1.0", text)
|
||||
|
||||
def play_preset(self, text):
|
||||
"""Insert text and play audio immediately."""
|
||||
self.insert_text(text)
|
||||
self.parent.submit_text()
|
||||
|
||||
def toggle_favourite(self, phrase, card_id=None):
|
||||
"""Toggle the favourite status of a preset."""
|
||||
phrase["isFavourite"] = not phrase["isFavourite"]
|
||||
|
||||
# If we have the card_id, update just that card - much faster than refreshing everything
|
||||
if card_id and card_id in self.preset_cards:
|
||||
self.update_preset_card(card_id)
|
||||
else:
|
||||
# Otherwise, refresh the entire display (fallback, should rarely happen)
|
||||
self.refresh_presets_display()
|
||||
|
||||
# Debounce the save operation to avoid writing to disk on every toggle
|
||||
self.debounced_save()
|
||||
|
||||
def debounced_save(self):
|
||||
"""Save presets with debouncing to avoid frequent disk writes."""
|
||||
# Cancel any pending save
|
||||
if self.save_timer:
|
||||
self.parent.after_cancel(self.save_timer)
|
||||
|
||||
# Schedule a new save operation
|
||||
self.save_timer = self.parent.after(1000, self._perform_save)
|
||||
|
||||
def _perform_save(self):
|
||||
"""Actually perform the save operation after debounce."""
|
||||
# Use threading to avoid blocking the UI
|
||||
threading.Thread(target=self.save_presets, daemon=True).start()
|
||||
self.save_timer = None
|
||||
|
||||
def toggle_presets(self):
|
||||
"""Toggle the visibility of the presets panel."""
|
||||
if self.presets_collapsed:
|
||||
self.presets_frame.grid()
|
||||
# Update button icon to down chevron while preserving text
|
||||
self.presets_button.configure(image=self.chevron_down, text=" Presets")
|
||||
self.parent.geometry(self.parent.default_geometry)
|
||||
else:
|
||||
self.presets_frame.grid_remove()
|
||||
# Update button icon to right chevron while preserving text
|
||||
self.presets_button.configure(image=self.chevron_right, text=" Presets")
|
||||
self.parent.geometry(self.parent.untoggled_geometry)
|
||||
self.presets_collapsed = not self.presets_collapsed
|
||||
|
||||
def save_current_text_as_preset(self):
|
||||
"""Save current text to the selected category as a preset."""
|
||||
text = self.parent.text_input.get("1.0", tk.END).strip()
|
||||
category = self.parent.category_var.get()
|
||||
if text and category != "Select Category":
|
||||
self.add_preset(category, text, is_favourite=False)
|
||||
# Show success message with category information
|
||||
messagebox.showinfo("Save Successful", f"The text has been successfully saved to the category: '{category}'.")
|
||||
else:
|
||||
messagebox.showinfo("Error", "Please enter text and select a category before saving.")
|
||||
|
||||
def load_presets(self):
|
||||
"""
|
||||
Load presets from the JSON file, copying from example if necessary.
|
||||
|
||||
Returns:
|
||||
List of preset categories with their phrases
|
||||
"""
|
||||
presets_path = Path("config/presets.json")
|
||||
example_path = self.parent.resource_path("assets/presets.example.json") # Path for the example file
|
||||
|
||||
# Check if presets.json exists, and if not, copy presets.example.json to config
|
||||
if not presets_path.exists():
|
||||
try:
|
||||
# Ensure config directory exists
|
||||
presets_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Copy example presets to config directory
|
||||
with open(example_path, "r", encoding="utf-8") as example_file:
|
||||
with open(presets_path, "w", encoding="utf-8") as config_file:
|
||||
config_file.write(example_file.read())
|
||||
except Exception as e:
|
||||
messagebox.showerror("Error", f"Failed to copy example presets: {e}")
|
||||
return [] # Return empty if unable to load or copy presets
|
||||
|
||||
# Load presets.json as usual
|
||||
try:
|
||||
with open(presets_path, "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
return data.get("presets", [])
|
||||
except (FileNotFoundError, json.JSONDecodeError) as e:
|
||||
messagebox.showerror("Error", f"Error loading presets: {e}")
|
||||
return [] # Default to empty if load fails
|
||||
|
||||
def save_presets(self):
|
||||
"""Save presets to the JSON file."""
|
||||
data = {"presets": self.presets}
|
||||
with open("config/presets.json", "w") as f:
|
||||
json.dump(data, f, indent=2)
|
||||
|
||||
def add_preset(self, category, text, is_favourite=False):
|
||||
"""
|
||||
Add a new preset and save it.
|
||||
|
||||
Args:
|
||||
category: The category to add the preset to
|
||||
text: The text of the preset
|
||||
is_favourite: Whether the preset is a favorite
|
||||
"""
|
||||
for cat in self.presets:
|
||||
if cat["category"] == category:
|
||||
cat["phrases"].append({"text": text, "isFavourite": is_favourite})
|
||||
break
|
||||
else:
|
||||
# Add a new category if not found
|
||||
self.presets.append({"category": category, "phrases": [{"text": text, "isFavourite": is_favourite}]})
|
||||
|
||||
# Save and refresh
|
||||
self.debounced_save()
|
||||
self.refresh_presets_display()
|
||||
|
||||
def delete_preset(self, category, text):
|
||||
"""
|
||||
Delete a preset by category and text.
|
||||
|
||||
Args:
|
||||
category: The category of the preset
|
||||
text: The text of the preset to delete
|
||||
"""
|
||||
for cat in self.presets:
|
||||
if cat["category"] == category:
|
||||
cat["phrases"] = [p for p in cat["phrases"] if p["text"] != text]
|
||||
break
|
||||
|
||||
# Save and refresh
|
||||
self.debounced_save()
|
||||
self.refresh_presets_display()
|
||||
@@ -9,7 +9,7 @@ import json
|
||||
import sys
|
||||
|
||||
from pystray import Icon as icon, MenuItem as item, Menu as menu
|
||||
from PIL import Image, ImageDraw
|
||||
from PIL import Image, ImageDraw, ImageTk
|
||||
from tkinter import ttk, messagebox, simpledialog, Menu, Frame, Canvas, Scrollbar
|
||||
import customtkinter as ctk
|
||||
from openai import OpenAI
|
||||
@@ -22,6 +22,7 @@ from utils.api_key_manager import APIKeyManager
|
||||
from utils.hotkey_manager import HotkeyManager
|
||||
from utils.resource_utils import ResourceUtils
|
||||
from utils.tone_presets_manager import TonePresetsManager
|
||||
from utils.presets_manager import PresetsManager
|
||||
|
||||
# Modify the load environment variables to load from config/.env
|
||||
def load_env_file():
|
||||
@@ -41,6 +42,9 @@ class TextToMic(tk.Tk):
|
||||
self.available_models = ["gpt-4o-mini", "gpt-4o", "gpt-4-turbo"]
|
||||
self.default_model = "gpt-4o-mini"
|
||||
|
||||
# Cache for icons - will store loaded and resized icon images
|
||||
self.icon_cache = {}
|
||||
|
||||
self.style = ttk.Style(self)
|
||||
if self.tk.call('tk', 'windowingsystem') == 'aqua':
|
||||
self.style.theme_use('aqua')
|
||||
@@ -71,11 +75,6 @@ class TextToMic(tk.Tk):
|
||||
self.style.configure('Recording.TButton', background='red', foreground='white')
|
||||
self.style.configure("Green.TButton", background="green", foreground="white")
|
||||
|
||||
self.presets = self.load_presets()
|
||||
self.current_category = "All"
|
||||
self.presets_collapsed = True
|
||||
|
||||
|
||||
# Ensure that the config directory exists
|
||||
self.ensure_config_directory()
|
||||
load_env_file()
|
||||
@@ -100,7 +99,14 @@ class TextToMic(tk.Tk):
|
||||
# Load tone presets
|
||||
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")
|
||||
|
||||
# Create the presets manager before initializing the GUI
|
||||
self.presets_manager = PresetsManager(self)
|
||||
|
||||
# Create menu and initialize GUI after presets manager is created
|
||||
self.create_menu()
|
||||
self.initialize_gui()
|
||||
|
||||
@@ -301,8 +307,7 @@ class TextToMic(tk.Tk):
|
||||
save_frame.grid(column=1, row=0, sticky=tk.E)
|
||||
|
||||
# Preset Category dropdown
|
||||
self.category_var = tk.StringVar(value="Select Category")
|
||||
categories = [cat["category"] for cat in self.presets]
|
||||
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')
|
||||
@@ -365,8 +370,6 @@ class TextToMic(tk.Tk):
|
||||
)
|
||||
self.submit_button.grid(row=0, column=1, sticky="ew", padx=(10, 0))
|
||||
|
||||
self.create_presets_section()
|
||||
|
||||
#Credits
|
||||
# Banner image that links to Scorchsoft
|
||||
banner_path = self.resource_path("assets/ss-banner-550.png")
|
||||
@@ -384,110 +387,12 @@ class TextToMic(tk.Tk):
|
||||
info_label.grid(column=0, row=7, columnspan=2, pady=(10, 10))
|
||||
info_label.bind("<Button-1>", lambda e: self.open_scorchsoft())
|
||||
|
||||
def create_presets_section(self):
|
||||
# Accordion frame to show/hide presets section
|
||||
self.presets_frame = ttk.Frame(self)
|
||||
self.presets_button = ttk.Button(self, text="▶ Presets", command=self.toggle_presets)
|
||||
self.presets_button.grid(column=0, row=6, columnspan=2, sticky=tk.W)
|
||||
self.presets_frame.grid(column=0, row=7, columnspan=2, sticky=(tk.W, tk.E))
|
||||
|
||||
# Tabs for categories with scrolling arrows
|
||||
self.tab_frame = ttk.Frame(self.presets_frame)
|
||||
self.tab_frame.pack(fill=tk.X)
|
||||
|
||||
# Style for flat buttons
|
||||
bg_color = self.style.lookup('TFrame', 'background')
|
||||
accent_color = "#e0e0e4" # Slightly darker grey for accents
|
||||
|
||||
self.style.configure("Flat.TButton",
|
||||
borderwidth=0,
|
||||
highlightthickness=0,
|
||||
font=("Arial", 12),
|
||||
anchor="center",
|
||||
background=bg_color)
|
||||
|
||||
# Thinner left arrow for tabs
|
||||
self.left_arrow = ttk.Button(self.tab_frame, text="◀", command=self.scroll_left, width=2, style="Flat.TButton")
|
||||
self.left_arrow.pack(side=tk.LEFT, padx=1) # Reduced padding
|
||||
|
||||
# Canvas for scrolling tabs horizontally, removing the horizontal scrollbar
|
||||
self.tabs_canvas = Canvas(self.tab_frame, height=30, bg=bg_color, highlightthickness=0)
|
||||
self.tabs_canvas.pack(side=tk.LEFT, fill=tk.X, expand=True)
|
||||
self.tabs_frame_inner = ttk.Frame(self.tabs_canvas)
|
||||
self.tabs_canvas.create_window((0, 0), window=self.tabs_frame_inner, anchor="nw")
|
||||
|
||||
# Thinner right arrow for tabs
|
||||
self.right_arrow = ttk.Button(self.tab_frame, text="▶", command=self.scroll_right, width=2, style="Flat.TButton")
|
||||
self.right_arrow.pack(side=tk.RIGHT, padx=1) # Reduced padding
|
||||
|
||||
# Presets display area with a fixed height and vertical scrollbar
|
||||
self.presets_canvas = Canvas(self.presets_frame, height=250, width=self.presets_frame.winfo_width(), bg=bg_color, highlightthickness=0)
|
||||
self.presets_scrollbar = Scrollbar(self.presets_frame, orient="vertical", command=self.presets_canvas.yview)
|
||||
self.presets_canvas.configure(yscrollcommand=self.presets_scrollbar.set)
|
||||
|
||||
# Frame inside the canvas to hold presets
|
||||
self.presets_scrollable_frame = ttk.Frame(self.presets_canvas)
|
||||
self.presets_canvas.create_window((0, 0), window=self.presets_scrollable_frame, anchor="nw")
|
||||
|
||||
# Pack the canvas and scrollbar
|
||||
self.presets_canvas.pack(side=tk.LEFT, fill=tk.X, expand=True)
|
||||
self.presets_scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
|
||||
|
||||
# Configure the scroll region to update when the frame changes
|
||||
self.presets_scrollable_frame.bind("<Configure>", lambda e: self.presets_canvas.configure(scrollregion=self.presets_canvas.bbox("all")))
|
||||
|
||||
# Populate tabs and presets
|
||||
self.populate_tabs() # Refresh tabs to show selection
|
||||
self.refresh_presets_display()
|
||||
self.toggle_presets()
|
||||
self.toggle_presets()
|
||||
self.enable_mouse_wheel_scrolling()
|
||||
|
||||
|
||||
|
||||
def scroll_left(self):
|
||||
self.tabs_canvas.xview_scroll(-5, "units")
|
||||
|
||||
def scroll_right(self):
|
||||
self.tabs_canvas.xview_scroll(5, "units")
|
||||
|
||||
|
||||
def enable_mouse_wheel_scrolling(self):
|
||||
"""Enable conditional mouse wheel scrolling for the presets canvas and category tabs canvas."""
|
||||
|
||||
def on_vertical_scroll(event):
|
||||
# Scroll the presets_canvas vertically
|
||||
if event.num == 4: # macOS scroll up
|
||||
self.presets_canvas.yview_scroll(-1, "units")
|
||||
elif event.num == 5: # macOS scroll down
|
||||
self.presets_canvas.yview_scroll(1, "units")
|
||||
else: # Windows and Linux
|
||||
self.presets_canvas.yview_scroll(int(-1 * (event.delta / 120)), "units")
|
||||
|
||||
def on_horizontal_scroll(event):
|
||||
# Scroll the tabs_canvas horizontally
|
||||
if event.num == 4: # macOS scroll left
|
||||
self.tabs_canvas.xview_scroll(-1, "units")
|
||||
elif event.num == 5: # macOS scroll right
|
||||
self.tabs_canvas.xview_scroll(1, "units")
|
||||
else: # Windows and Linux
|
||||
self.tabs_canvas.xview_scroll(int(-1 * (event.delta / 120)), "units")
|
||||
|
||||
# Bind scroll events when mouse enters or leaves the presets canvas area
|
||||
self.presets_canvas.bind("<Enter>", lambda e: self.presets_canvas.bind_all("<MouseWheel>", on_vertical_scroll))
|
||||
self.presets_canvas.bind("<Leave>", lambda e: self.presets_canvas.unbind_all("<MouseWheel>"))
|
||||
|
||||
# Bind scroll events when mouse enters or leaves the tabs canvas area
|
||||
self.tabs_canvas.bind("<Enter>", lambda e: self.tabs_canvas.bind_all("<MouseWheel>", on_horizontal_scroll))
|
||||
self.tabs_canvas.bind("<Leave>", lambda e: self.tabs_canvas.unbind_all("<MouseWheel>"))
|
||||
|
||||
|
||||
|
||||
def open_scorchsoft(self, event=None):
|
||||
webbrowser.open('https://www.scorchsoft.com')
|
||||
|
||||
|
||||
|
||||
def save_current_text_as_preset(self):
|
||||
"""Forward the save request to the presets manager."""
|
||||
self.presets_manager.save_current_text_as_preset()
|
||||
|
||||
def show_instructions(self):
|
||||
instruction_window = tk.Toplevel(self)
|
||||
@@ -534,7 +439,6 @@ Please also make sure you read the Terms of use and licence statement before usi
|
||||
# Add a button to close the window
|
||||
ttk.Button(instruction_window, text="Close", command=instruction_window.destroy).pack(pady=(10, 0))
|
||||
|
||||
|
||||
def show_terms_of_use(self):
|
||||
# Get the path to the LICENSE.md file using the resource_path method
|
||||
license_path = self.resource_path("LICENSE.md")
|
||||
@@ -578,13 +482,6 @@ Please also make sure you read the Terms of use and licence statement before usi
|
||||
# Add a button to close the window
|
||||
ttk.Button(instruction_window, text="Close", command=instruction_window.destroy).pack(pady=(10, 0))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def get_app_support_path_mac(self):
|
||||
home = Path.home()
|
||||
app_support_path = home / 'Library' / 'Application Support' / 'scorchsoft-text-to-mic'
|
||||
@@ -966,262 +863,6 @@ Please also make sure you read the Terms of use and licence statement before usi
|
||||
|
||||
return return_text
|
||||
|
||||
|
||||
|
||||
|
||||
def populate_tabs(self):
|
||||
# Clear current tabs
|
||||
for widget in self.tabs_frame_inner.winfo_children():
|
||||
widget.destroy()
|
||||
|
||||
# Add "All" and "Favourites" tabs along with dynamic categories
|
||||
for category in ["All", "Favourites"] + [cat["category"] for cat in self.presets if cat["category"] not in ["All", "Favourites"]]:
|
||||
btn = ttk.Button(self.tabs_frame_inner, text=category, command=lambda c=category: self.switch_category(c))
|
||||
btn.pack(side=tk.LEFT, padx=2)
|
||||
|
||||
# Style selected category
|
||||
if category == self.current_category:
|
||||
btn.state(['pressed']) # Visual style for selected tab
|
||||
else:
|
||||
btn.state(['!pressed'])
|
||||
|
||||
def switch_category(self, category):
|
||||
"""Switch displayed category."""
|
||||
self.current_category = category
|
||||
self.populate_tabs() # Refresh tabs to show selection
|
||||
self.refresh_presets_display()
|
||||
|
||||
def refresh_presets_display(self):
|
||||
"""Refresh displayed presets based on selected category."""
|
||||
|
||||
# Clear existing items in the scrollable frame
|
||||
for widget in self.presets_scrollable_frame.winfo_children():
|
||||
widget.destroy()
|
||||
|
||||
# Debounce - cancel any previous refresh call if pending
|
||||
if hasattr(self, 'refresh_handle'):
|
||||
self.after_cancel(self.refresh_handle)
|
||||
self.refresh_handle = self.after(100, self._populate_presets)
|
||||
|
||||
def _populate_presets(self):
|
||||
"""Populate presets into grid layout with responsive columns."""
|
||||
# Filter presets based on current category
|
||||
display_phrases = []
|
||||
if self.current_category == "All":
|
||||
for cat in self.presets:
|
||||
display_phrases.extend(cat["phrases"])
|
||||
elif self.current_category == "Favourites":
|
||||
for cat in self.presets:
|
||||
display_phrases.extend([p for p in cat["phrases"] if p["isFavourite"]])
|
||||
else:
|
||||
display_phrases = next((cat["phrases"] for cat in self.presets if cat["category"] == self.current_category), [])
|
||||
|
||||
# Update canvas width to calculate dynamic column width
|
||||
self.presets_canvas.update_idletasks()
|
||||
preset_width = max(self.presets_canvas.winfo_width() // 3, 150) # Minimum width of 100
|
||||
preset_height = 100
|
||||
|
||||
# Get our custom style colors
|
||||
bg_color = self.style.lookup('TFrame', 'background')
|
||||
text_color = self.style.lookup('TLabel', 'foreground')
|
||||
|
||||
# Use a light gray with subtle blue tint for preset buttons - distinct from white but still clean
|
||||
preset_bg_color = "#f0f4f8" # Light blue-gray that contrasts with the main background
|
||||
|
||||
# Configure columns to fill available space
|
||||
for col in range(3):
|
||||
self.presets_scrollable_frame.columnconfigure(col, weight=1)
|
||||
|
||||
# Populate filtered presets in grid layout
|
||||
for i, phrase in enumerate(display_phrases):
|
||||
# Create a frame with no border for cleaner look
|
||||
frame = ttk.Frame(self.presets_scrollable_frame, width=preset_width, height=preset_height)
|
||||
frame.grid(row=i // 4, column=i % 4, padx=3, pady=3, sticky="nsew")
|
||||
frame.grid_propagate(False)
|
||||
|
||||
# Create a unique style name for each card to avoid affecting other widgets
|
||||
card_style_name = f'Card{i}.TFrame'
|
||||
self.style.configure(card_style_name,
|
||||
background=preset_bg_color,
|
||||
borderwidth=0,
|
||||
relief="flat")
|
||||
|
||||
# Create inner frame with distinct background and no border
|
||||
inner_frame = ttk.Frame(frame, style=card_style_name)
|
||||
inner_frame.pack(fill=tk.BOTH, expand=True, padx=1, pady=1)
|
||||
|
||||
self.presets_scrollable_frame.grid_columnconfigure(i % 4, weight=1) # Make columns expandable
|
||||
self.presets_scrollable_frame.grid_rowconfigure(i // 4, weight=1) # Make rows expandable
|
||||
|
||||
# Create a unique style for each label
|
||||
label_style_name = f'CardLabel{i}.TLabel'
|
||||
self.style.configure(label_style_name,
|
||||
background=preset_bg_color,
|
||||
foreground=text_color)
|
||||
|
||||
# Text label with truncation for long text
|
||||
wrapped_text = self.wrap_text(phrase["text"], max_lines=3, max_chars_per_line=20)
|
||||
label = ttk.Label(inner_frame, text=wrapped_text, anchor="center", justify="center",
|
||||
width=20, style=label_style_name)
|
||||
label.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
|
||||
label.bind("<Button-1>", lambda e, t=phrase["text"]: self.insert_text(t))
|
||||
label.bind("<Double-Button-1>", lambda e, t=phrase["text"]: self.play_preset(t))
|
||||
|
||||
# Create a unique style for each bottom frame
|
||||
bottom_frame_style = f'BottomFrame{i}.TFrame'
|
||||
self.style.configure(bottom_frame_style, background=preset_bg_color)
|
||||
|
||||
# Bottom frame for icons
|
||||
bottom_frame = ttk.Frame(inner_frame, style=bottom_frame_style)
|
||||
bottom_frame.pack(side=tk.BOTTOM, fill=tk.X, pady=2)
|
||||
|
||||
# Create a unique style for each button
|
||||
button_style_name = f'FlatButton{i}.TButton'
|
||||
self.style.configure(button_style_name,
|
||||
borderwidth=0,
|
||||
highlightthickness=0,
|
||||
font=("Arial", 12),
|
||||
anchor="center",
|
||||
background=preset_bg_color)
|
||||
|
||||
# Favourite button
|
||||
fav_icon = "❤️" if phrase["isFavourite"] else "♡"
|
||||
fav_btn = ttk.Button(bottom_frame, text=fav_icon,
|
||||
command=lambda p=phrase: self.toggle_favourite(p),
|
||||
width=2, style=button_style_name)
|
||||
fav_btn.pack(side=tk.RIGHT, padx=2)
|
||||
|
||||
# Delete button
|
||||
del_btn = ttk.Button(bottom_frame, text="🗑️",
|
||||
command=lambda t=phrase["text"]: self.delete_preset(self.current_category, t),
|
||||
width=2, style=button_style_name)
|
||||
del_btn.pack(side=tk.RIGHT, padx=2)
|
||||
|
||||
# Update scroll region after populating all items
|
||||
self.presets_canvas.configure(scrollregion=self.presets_canvas.bbox("all"))
|
||||
|
||||
|
||||
def wrap_text(self, text, max_lines=3, max_chars_per_line=20):
|
||||
"""Wrap text to fit within a limited number of lines and characters."""
|
||||
words = text.split()
|
||||
wrapped_text = ""
|
||||
line = ""
|
||||
line_count = 0
|
||||
|
||||
for word in words:
|
||||
if len(line + word) <= max_chars_per_line:
|
||||
line += word + " "
|
||||
else:
|
||||
wrapped_text += line.strip() + "\n"
|
||||
line = word + " "
|
||||
line_count += 1
|
||||
if line_count >= max_lines - 1: # Leave space for ellipsis
|
||||
break
|
||||
|
||||
# Add final line and handle overflow with ellipsis
|
||||
wrapped_text += line.strip()
|
||||
if line_count >= max_lines - 1 and len(wrapped_text.splitlines()) >= max_lines:
|
||||
wrapped_text = "\n".join(wrapped_text.splitlines()[:max_lines - 1]) + "\n..."
|
||||
|
||||
return wrapped_text
|
||||
|
||||
|
||||
|
||||
def insert_text(self, text):
|
||||
"""Insert preset text into the text area."""
|
||||
self.text_input.delete("1.0", tk.END)
|
||||
self.text_input.insert("1.0", text)
|
||||
|
||||
def play_preset(self, text):
|
||||
"""Insert text and play audio immediately."""
|
||||
self.insert_text(text)
|
||||
self.submit_text()
|
||||
|
||||
def toggle_favourite(self, phrase):
|
||||
"""Toggle the favourite status of a preset."""
|
||||
phrase["isFavourite"] = not phrase["isFavourite"]
|
||||
self.save_presets()
|
||||
self.refresh_presets_display()
|
||||
|
||||
def toggle_presets(self):
|
||||
if self.presets_collapsed:
|
||||
self.presets_frame.grid()
|
||||
self.presets_button.config(text="▼ Presets")
|
||||
self.geometry(self.default_geometry)
|
||||
else:
|
||||
self.presets_frame.grid_remove()
|
||||
self.presets_button.config(text="▶ Presets")
|
||||
self.geometry(self.untoggled_geometry)
|
||||
self.presets_collapsed = not self.presets_collapsed
|
||||
|
||||
|
||||
def save_current_text_as_preset(self):
|
||||
"""Save current text to the selected category as a preset."""
|
||||
text = self.text_input.get("1.0", tk.END).strip()
|
||||
category = self.category_var.get()
|
||||
if text and category != "Select Category":
|
||||
self.add_preset(category, text, is_favourite=False)
|
||||
# Show success message with category information
|
||||
messagebox.showinfo("Save Successful", f"The text has been successfully saved to the category: '{category}'.")
|
||||
else:
|
||||
messagebox.showinfo("Error", "Please enter text and select a category before saving.")
|
||||
|
||||
|
||||
def load_presets(self):
|
||||
"""Load presets from the JSON file, copying from example if necessary."""
|
||||
presets_path = Path("config/presets.json")
|
||||
example_path = self.resource_path("assets/presets.example.json") # Path for the example file
|
||||
|
||||
# Check if presets.json exists, and if not, copy presets.example.json to config
|
||||
if not presets_path.exists():
|
||||
try:
|
||||
# Ensure config directory exists
|
||||
presets_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Copy example presets to config directory
|
||||
with open(example_path, "r", encoding="utf-8") as example_file:
|
||||
with open(presets_path, "w", encoding="utf-8") as config_file:
|
||||
config_file.write(example_file.read())
|
||||
except Exception as e:
|
||||
messagebox.showerror("Error", f"Failed to copy example presets: {e}")
|
||||
return [] # Return empty if unable to load or copy presets
|
||||
|
||||
# Load presets.json as usual
|
||||
try:
|
||||
with open(presets_path, "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
return data.get("presets", [])
|
||||
except (FileNotFoundError, json.JSONDecodeError) as e:
|
||||
messagebox.showerror("Error", f"Error loading presets: {e}")
|
||||
return [] # Default to empty if load fails
|
||||
|
||||
def save_presets(self):
|
||||
"""Save presets to the JSON file."""
|
||||
data = {"presets": self.presets}
|
||||
with open("config/presets.json", "w") as f:
|
||||
json.dump(data, f, indent=2)
|
||||
|
||||
def add_preset(self, category, text, is_favourite=False):
|
||||
"""Add a new preset and save it."""
|
||||
for cat in self.presets:
|
||||
if cat["category"] == category:
|
||||
cat["phrases"].append({"text": text, "isFavourite": is_favourite})
|
||||
break
|
||||
else:
|
||||
# Add a new category if not found
|
||||
self.presets.append({"category": category, "phrases": [{"text": text, "isFavourite": is_favourite}]})
|
||||
self.save_presets()
|
||||
self.refresh_presets_display()
|
||||
|
||||
def delete_preset(self, category, text):
|
||||
"""Delete a preset by category and text."""
|
||||
for cat in self.presets:
|
||||
if cat["category"] == category:
|
||||
cat["phrases"] = [p for p in cat["phrases"] if p["text"] != text]
|
||||
break
|
||||
self.save_presets()
|
||||
self.refresh_presets_display()
|
||||
|
||||
def get_device_info(self, device_index):
|
||||
p = pyaudio.PyAudio()
|
||||
try:
|
||||
|
||||