Files

1 line
68 KiB
JSON

{"repo": "bialobrzeskid/hold-to-speak", "files": {"whisper_ui.py": {"a53d992524703b97ec43cc53838b6ff712492d5d": {"t": 1785674612.4868546, "n": 66654, "h": "6ecea4826de90909", "c": "import sys\nimport os\nimport json\nimport time\nfrom datetime import timedelta\nfrom PyQt6.QtWidgets import (QApplication, QMainWindow, QPushButton, QVBoxLayout, QHBoxLayout, \n QWidget, QLabel, QTextEdit, QFrame, QTabWidget, QLineEdit,\n QGridLayout, QComboBox, QDialog, QDialogButtonBox, QMessageBox,\n QSpacerItem, QSizePolicy, QGroupBox, QFormLayout, QSystemTrayIcon, QMenu,\n QToolButton, QScrollArea, QFileDialog, QCheckBox)\nfrom PyQt6.QtCore import Qt, pyqtSignal, QSettings, QEvent, QSize, QPoint\nfrom PyQt6.QtGui import QFont, QIcon, QPixmap, QPainter, QColor, QAction, QPen\nimport math\n\nclass StatsManager:\n \"\"\"Klasa do zarządzania statystykami użytkownika\"\"\"\n \n def __init__(self):\n self.settings_file = \"whisper_stats.json\"\n self.stats = self._load_stats()\n \n def _load_stats(self):\n default_stats = {\n \"total_recordings\": 0,\n \"total_seconds\": 0,\n \"total_characters\": 0,\n \"api_calls\": 0,\n \"last_used\": None\n }\n \n if os.path.exists(self.settings_file):\n try:\n with open(self.settings_file, 'r') as f:\n return json.load(f)\n except:\n return default_stats\n return default_stats\n \n def save_stats(self):\n with open(self.settings_file, 'w') as f:\n json.dump(self.stats, f)\n \n def update_recording_stats(self, duration_seconds, text_length):\n self.stats[\"total_recordings\"] += 1\n self.stats[\"total_seconds\"] += duration_seconds\n self.stats[\"total_characters\"] += text_length\n self.stats[\"api_calls\"] += 1\n self.stats[\"last_used\"] = time.strftime(\"%Y-%m-%d %H:%M:%S\")\n self.save_stats()\n \n def get_time_saved(self):\n # Zakładamy, że mówimy 3x szybciej niż piszemy\n # Przyjmijmy, że przeciętna prędkość pisania to 40 WPM (200 znaków/min)\n # Więc czas zaoszczędzony to (znaki / 200) - (czas nagrywania w min)\n chars = self.stats[\"total_characters\"]\n recording_minutes = self.stats[\"total_seconds\"] / 60\n typing_minutes = chars / 200\n return typing_minutes - recording_minutes\n \n def get_formatted_stats(self):\n total_time = timedelta(seconds=self.stats[\"total_seconds\"])\n total_time_str = str(total_time).split('.')[0] # Bez mikrosekund\n \n time_saved = timedelta(minutes=self.get_time_saved())\n time_saved_str = str(time_saved).split('.')[0]\n \n return {\n \"total_recordings\": self.stats[\"total_recordings\"],\n \"total_time\": total_time_str,\n \"total_characters\": self.stats[\"total_characters\"],\n \"api_calls\": self.stats[\"api_calls\"],\n \"time_saved\": time_saved_str,\n \"last_used\": self.stats[\"last_used\"] or \"Nigdy\"\n }\n \n def clear_stats(self):\n \"\"\"Czyści wszystkie statystyki użytkownika\"\"\"\n default_stats = {\n \"total_recordings\": 0,\n \"total_seconds\": 0,\n \"total_characters\": 0,\n \"api_calls\": 0,\n \"last_used\": None\n }\n self.stats = default_stats\n self.save_stats()\n\n def update_last_recording_stats(self, duration_seconds, text):\n \"\"\"Updates statistics after recording completion\"\"\"\n chars = len(text)\n \n # Update general statistics\n self.stats_manager.update_recording_stats(duration_seconds, chars)\n \n # If we're in the main view, refresh the statistics section\n if hasattr(self, 'main_content') and self.main_content.isVisible():\n # Remove the old statistics section\n for i in reversed(range(self.main_content.layout().count())):\n item = self.main_content.layout().itemAt(i)\n if isinstance(item.widget(), QWidget) and item.widget().height() == 80:\n item.widget().deleteLater()\n break\n \n # Add the new statistics section\n self.main_content.layout().addWidget(self.create_statistics_section())\n\nclass ApiKeyDialog(QDialog):\n \"\"\"Dialog for entering API key\"\"\"\n \n def __init__(self, parent=None, current_openai_key=\"\", current_deepinfra_key=\"\", selected_provider=\"openai\"):\n super().__init__(parent)\n self.setWindowTitle(\"API Settings\")\n self.setMinimumWidth(400)\n \n # Set dialog style\n self.setStyleSheet(\"\"\"\n QDialog {\n background-color: #F8F9FC;\n }\n QLabel {\n color: #212529;\n }\n QComboBox, QLineEdit {\n background-color: #FFFFFF;\n color: #212529;\n border: 1px solid #E9ECEF;\n border-radius: 8px;\n padding: 8px;\n }\n QPushButton {\n background-color: #007BFF;\n color: white;\n border: none;\n padding: 10px;\n border-radius: 8px;\n font-size: 14px;\n min-width: 120px;\n }\n QPushButton:hover {\n background-color: #0056b3;\n }\n QPushButton:disabled {\n background-color: #6C757D;\n }\n \"\"\")\n \n layout = QVBoxLayout()\n layout.setSpacing(15)\n \n # API provider selection\n provider_layout = QFormLayout()\n self.provider_combo = QComboBox()\n self.provider_combo.addItems([\"OpenAI\", \"DeepInfra\"])\n self.provider_combo.setCurrentText(\"OpenAI\" if selected_provider == \"openai\" else \"DeepInfra\")\n self.provider_combo.currentTextChanged.connect(self.on_provider_changed)\n provider_layout.addRow(\"API Provider:\", self.provider_combo)\n layout.addLayout(provider_layout)\n \n # OpenAI information\n self.openai_info_label = QLabel(\"Enter your OpenAI API key:\")\n self.openai_info_label.setWordWrap(True)\n \n # OpenAI key input\n self.openai_key_input = QLineEdit()\n self.openai_key_input.setText(current_openai_key)\n self.openai_key_input.setPlaceholderText(\"sk-...\")\n self.openai_key_input.setEchoMode(QLineEdit.EchoMode.Password)\n \n # DeepInfra information\n self.deepinfra_info_label = QLabel(\"Enter your DeepInfra API key:\")\n self.deepinfra_info_label.setWordWrap(True)\n \n # DeepInfra key input\n self.deepinfra_key_input = QLineEdit()\n self.deepinfra_key_input.setText(current_deepinfra_key)\n self.deepinfra_key_input.setPlaceholderText(\"udhgGPY...\")\n self.deepinfra_key_input.setEchoMode(QLineEdit.EchoMode.Password)\n \n # API fields container\n self.api_container = QVBoxLayout()\n self.api_container.addWidget(self.openai_info_label)\n self.api_container.addWidget(self.openai_key_input)\n self.api_container.addWidget(self.deepinfra_info_label)\n self.api_container.addWidget(self.deepinfra_key_input)\n layout.addLayout(self.api_container)\n \n # Show/hide key button\n self.show_key_button = QPushButton(\"Show Keys\")\n self.show_key_button.setCheckable(True)\n self.show_key_button.clicked.connect(self.toggle_key_visibility)\n layout.addWidget(self.show_key_button, alignment=Qt.AlignmentFlag.AlignCenter)\n \n # OK/Cancel buttons\n button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | \n QDialogButtonBox.StandardButton.Cancel)\n button_box.accepted.connect(self.accept)\n button_box.rejected.connect(self.reject)\n layout.addWidget(button_box)\n \n self.setLayout(layout)\n \n # Set initial field visibility\n self.on_provider_changed(self.provider_combo.currentText())\n \n def on_provider_changed(self, provider):\n \"\"\"Updates interface based on selected API provider\"\"\"\n is_openai = provider == \"OpenAI\"\n self.openai_info_label.setVisible(is_openai)\n self.openai_key_input.setVisible(is_openai)\n self.deepinfra_info_label.setVisible(not is_openai)\n self.deepinfra_key_input.setVisible(not is_openai)\n \n def toggle_key_visibility(self):\n \"\"\"Toggles API key visibility\"\"\"\n mode = QLineEdit.EchoMode.Normal if self.show_key_button.isChecked() else QLineEdit.EchoMode.Password\n self.openai_key_input.setEchoMode(mode)\n self.deepinfra_key_input.setEchoMode(mode)\n self.show_key_button.setText(\"Hide Keys\" if self.show_key_button.isChecked() else \"Show Keys\")\n \n def get_api_settings(self):\n \"\"\"Returns API settings\"\"\"\n provider = \"openai\" if self.provider_combo.currentText() == \"OpenAI\" else \"deepinfra\"\n openai_key = self.openai_key_input.text().strip()\n deepinfra_key = self.deepinfra_key_input.text().strip()\n return {\n \"provider\": provider,\n \"openai_key\": openai_key,\n \"deepinfra_key\": deepinfra_key\n }\n\nclass HotkeyDialog(QDialog):\n \"\"\"Dialog for setting hotkeys\"\"\"\n \n def __init__(self, parent=None, current_hotkeys=None):\n super().__init__(parent)\n self.setWindowTitle(\"Hotkey Settings\")\n self.setMinimumWidth(400)\n \n # Set dialog style\n self.setStyleSheet(\"\"\"\n QDialog {\n background-color: #F8F9FC;\n }\n QLabel {\n color: #212529;\n }\n QLineEdit {\n background-color: #FFFFFF;\n color: #212529;\n border: 1px solid #E9ECEF;\n border-radius: 8px;\n padding: 8px;\n }\n QPushButton {\n background-color: #007BFF;\n color: white;\n border: none;\n padding: 10px;\n border-radius: 8px;\n font-size: 14px;\n min-width: 120px;\n }\n QPushButton:hover {\n background-color: #0056b3;\n }\n QPushButton:disabled {\n background-color: #6C757D;\n }\n QFrame {\n background-color: #FFFFFF;\n border: 1px solid #E9ECEF;\n border-radius: 8px;\n }\n \"\"\")\n \n self.current_hotkeys = current_hotkeys or [\"Ctrl\", \"Shift\"]\n self.new_hotkeys = []\n self.listening = False\n \n layout = QVBoxLayout()\n layout.setSpacing(15)\n \n # Instructions\n info_label = QLabel(\"Press the key combination you want to use as a hotkey.\")\n info_label.setWordWrap(True)\n layout.addWidget(info_label)\n \n # Current hotkey display\n current_hotkey_text = \" + \".join(self.current_hotkeys)\n self.current_hotkey_label = QLabel(f\"Current hotkey: {current_hotkey_text}\")\n layout.addWidget(self.current_hotkey_label)\n \n # Hotkey input frame\n hotkey_frame = QFrame()\n hotkey_frame.setMinimumHeight(60)\n hotkey_layout = QVBoxLayout(hotkey_frame)\n \n self.hotkey_display = QLineEdit()\n self.hotkey_display.setReadOnly(True)\n self.hotkey_display.setPlaceholderText(\"Click 'Start Listening' and press keys...\")\n self.hotkey_display.setAlignment(Qt.AlignmentFlag.AlignCenter)\n hotkey_layout.addWidget(self.hotkey_display)\n \n layout.addWidget(hotkey_frame)\n \n # Listen button\n self.listen_button = QPushButton(\"Start Listening\")\n self.listen_button.clicked.connect(self.toggle_listening)\n layout.addWidget(self.listen_button, alignment=Qt.AlignmentFlag.AlignCenter)\n \n # OK/Cancel buttons\n button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | \n QDialogButtonBox.StandardButton.Cancel)\n button_box.accepted.connect(self.accept)\n button_box.rejected.connect(self.reject)\n layout.addWidget(button_box)\n \n self.setLayout(layout)\n \n # Install event filter for key press events\n self.installEventFilter(self)\n \n def toggle_listening(self):\n \"\"\"Toggles key listening mode\"\"\"\n if self.listening:\n self.stop_listening()\n else:\n self.start_listening()\n \n def start_listening(self):\n \"\"\"Starts listening for key presses\"\"\"\n self.listening = True\n self.new_hotkeys = []\n self.hotkey_display.clear()\n self.hotkey_display.setPlaceholderText(\"Press keys now...\")\n self.listen_button.setText(\"Stop Listening\")\n self.listen_button.setStyleSheet(\"\"\"\n QPushButton {\n background-color: #DC3545;\n color: white;\n }\n QPushButton:hover {\n background-color: #C82333;\n }\n \"\"\")\n \n def stop_listening(self):\n \"\"\"Stops listening for key presses\"\"\"\n self.listening = False\n self.listen_button.setText(\"Start Listening\")\n self.listen_button.setStyleSheet(\"\")\n \n if not self.new_hotkeys:\n self.hotkey_display.setPlaceholderText(\"Click 'Start Listening' and press keys...\")\n else:\n self.hotkey_display.setText(\" + \".join(self.new_hotkeys))\n \n def eventFilter(self, obj, event):\n \"\"\"Filters key press events\"\"\"\n if self.listening and event.type() == QEvent.Type.KeyPress:\n key_text = self._get_key_text(event)\n \n if key_text and key_text not in self.new_hotkeys:\n self.new_hotkeys.append(key_text)\n self.hotkey_display.setText(\" + \".join(self.new_hotkeys))\n \n return True\n \n return super().eventFilter(obj, event)\n \n def _get_key_text(self, event):\n \"\"\"Converts key event to standardized key name\"\"\"\n # Special keys mapping - simplified to only include the keys we use\n special_keys = {\n Qt.Key.Key_Control: \"Ctrl\",\n Qt.Key.Key_Shift: \"Shift\",\n Qt.Key.Key_Alt: \"Alt\",\n }\n \n key = event.key()\n \n # Handle special keys\n if key in special_keys:\n return special_keys[key]\n \n # Handle regular keys (letters, numbers, etc.)\n text = event.text().upper()\n if text and text.isprintable():\n return text\n \n # If we can't determine the key, return None\n return None\n \n def get_hotkey(self):\n \"\"\"Returns the new hotkey combination\"\"\"\n return self.new_hotkeys if self.new_hotkeys else None\n\nclass WhisperMainWindow(QMainWindow):\n \"\"\"Główne okno aplikacji Whisper\"\"\"\n \n api_settings_changed = pyqtSignal(dict)\n hotkey_changed = pyqtSignal(list)\n option_changed = pyqtSignal(str, bool)\n microphone_changed = pyqtSignal(str) # New signal for microphone changes\n \n def __init__(self):\n super().__init__()\n self.setWindowTitle(\"Whisper Transcriber\")\n self.setMinimumSize(800, 600)\n \n # Ustawienia\n self.settings = QSettings(\"WhisperApp\", \"TranscriberSettings\")\n self.api_provider = self.settings.value(\"api_provider\", \"openai\")\n self.openai_key = self.settings.value(\"openai_key\", \"\")\n self.deepinfra_key = self.settings.value(\"deepinfra_key\", \"\")\n self.hotkey = self.settings.value(\"hotkey\", [\"Ctrl\", \"Shift\"])\n self.selected_microphone = self.settings.value(\"selected_microphone\", \"\")\n \n # Opcje dodatkowe\n self.auto_paste_enabled = self.settings.value(\"auto_paste_enabled\", True, type=bool)\n self.tray_notifications_enabled = self.settings.value(\"tray_notifications_enabled\", True, type=bool)\n self.startup_enabled = self.settings.value(\"startup_enabled\", False, type=bool)\n \n # Statystyki\n self.stats_manager = StatsManager()\n \n # UI\n self.init_ui()\n \n # Show main view by default\n self.show_main_view()\n \n # System Tray\n self.setup_system_tray()\n \n def init_ui(self):\n # Główny widget i layout\n central_widget = QWidget()\n self.setCentralWidget(central_widget)\n self.main_layout = QVBoxLayout(central_widget)\n self.main_layout.setContentsMargins(0, 0, 0, 0)\n self.main_layout.setSpacing(0)\n \n # Create top navigation bar\n self.create_top_navigation()\n \n # Create content container that will hold either main or settings view\n self.content_container = QWidget()\n self.content_layout = QVBoxLayout(self.content_container)\n self.content_layout.setContentsMargins(0, 0, 0, 0)\n self.content_layout.setSpacing(0)\n \n # Add content container to main layout\n self.main_layout.addWidget(self.content_container)\n \n # Style\n self.setStyleSheet(\"\"\"\n QMainWindow {\n background-color: #F8F9FC;\n }\n QPushButton {\n background-color: #007BFF;\n color: white;\n border: none;\n padding: 10px;\n border-radius: 8px;\n font-size: 14px;\n min-width: 120px;\n }\n QPushButton:hover {\n background-color: #0056b3;\n }\n QLabel {\n color: #212529;\n font-size: 14px;\n }\n QTextEdit, QLineEdit {\n background-color: #FFFFFF;\n color: #212529;\n border: 1px solid #E9ECEF;\n border-radius: 8px;\n padding: 10px;\n font-size: 13px;\n }\n QTextEdit::placeholder, QLineEdit::placeholder {\n color: #6C757D;\n }\n QComboBox {\n background-color: #FFFFFF;\n color: #212529;\n border: 1px solid #E9ECEF;\n border-radius: 8px;\n padding: 8px;\n }\n QComboBox::drop-down {\n border: 0px;\n }\n QComboBox::down-arrow {\n width: 14px;\n height: 14px;\n }\n QComboBox QAbstractItemView {\n background-color: #FFFFFF;\n color: #212529;\n selection-background-color: #007BFF;\n }\n QScrollArea {\n border: none;\n background-color: transparent;\n }\n QToolButton {\n background-color: transparent;\n border: none;\n }\n QToolButton:hover {\n background-color: rgba(0, 123, 255, 0.1);\n border-radius: 4px;\n }\n \"\"\")\n \n def create_top_navigation(self):\n \"\"\"Creates the top navigation bar\"\"\"\n # Create top navigation bar\n nav_bar = QWidget(self)\n nav_bar.setFixedHeight(60)\n nav_bar.setStyleSheet(\"\"\"\n QWidget {\n background-color: #FFFFFF;\n border-bottom: 1px solid #E9ECEF;\n }\n \"\"\")\n \n # Set the nav bar to be at the top of the window\n nav_bar.setGeometry(0, 0, self.width(), 60)\n \n # Layout for navigation bar\n nav_layout = QHBoxLayout(nav_bar)\n nav_layout.setContentsMargins(20, 0, 20, 0)\n \n # Application title with microphone icon\n title_layout = QHBoxLayout()\n \n # Create microphone icon\n mic_icon = QLabel()\n mic_pixmap = QPixmap(24, 24)\n mic_pixmap.fill(Qt.GlobalColor.transparent)\n painter = QPainter(mic_pixmap)\n painter.setRenderHint(QPainter.RenderHint.Antialiasing)\n \n # Draw microphone icon\n painter.setPen(QPen(QColor(\"#212529\"), 2))\n painter.setBrush(QColor(\"#212529\"))\n # Base of microphone\n painter.drawRoundedRect(8, 4, 8, 12, 2, 2)\n # Stand of microphone\n painter.drawLine(12, 16, 12, 20)\n painter.drawLine(8, 20, 16, 20)\n painter.end()\n \n mic_icon.setPixmap(mic_pixmap)\n title_layout.addWidget(mic_icon)\n \n # Application title\n app_title = QLabel(\"Whisper Transcriber\")\n app_title.setStyleSheet(\"\"\"\n font-size: 20px;\n font-weight: bold;\n color: #212529;\n \"\"\")\n title_layout.addWidget(app_title)\n \n nav_layout.addLayout(title_layout)\n nav_layout.addStretch()\n \n # Navigation buttons\n nav_buttons_layout = QHBoxLayout()\n \n # Main button (active)\n self.main_button = QPushButton(\"Main\")\n self.main_button.setStyleSheet(\"\"\"\n QPushButton {\n background-color: #007BFF;\n color: #FFFFFF;\n border-radius: 8px;\n padding: 8px 16px;\n font-weight: bold;\n }\n \"\"\")\n self.main_button.clicked.connect(self.show_main_view)\n nav_buttons_layout.addWidget(self.main_button)\n \n # Settings button (inactive)\n self.settings_button = QPushButton(\"Settings\")\n self.settings_button.setStyleSheet(\"\"\"\n QPushButton {\n background-color: #F8F9FC;\n color: #212529;\n border: 1px solid #E9ECEF;\n border-radius: 8px;\n padding: 8px 16px;\n }\n QPushButton:hover {\n background-color: #E9ECEF;\n }\n \"\"\")\n \n # Create gear icon\n gear_icon = QPixmap(16, 16)\n gear_icon.fill(Qt.GlobalColor.transparent)\n painter = QPainter(gear_icon)\n painter.setRenderHint(QPainter.RenderHint.Antialiasing)\n painter.setPen(QPen(QColor(\"#212529\"), 1))\n painter.setBrush(Qt.BrushStyle.NoBrush)\n \n # Draw a simple gear\n center = QPoint(8, 8)\n painter.drawEllipse(center, 3, 3)\n painter.drawEllipse(center, 6, 6)\n \n # Draw gear teeth\n for i in range(8):\n angle = i * 45\n x1 = center.x() + 6 * math.cos(math.radians(angle))\n y1 = center.y() + 6 * math.sin(math.radians(angle))\n x2 = center.x() + 8 * math.cos(math.radians(angle))\n y2 = center.y() + 8 * math.sin(math.radians(angle))\n painter.drawLine(int(x1), int(y1), int(x2), int(y2))\n \n painter.end()\n \n self.settings_button.setIcon(QIcon(gear_icon))\n self.settings_button.clicked.connect(self.show_settings_view)\n nav_buttons_layout.addWidget(self.settings_button)\n \n nav_layout.addLayout(nav_buttons_layout)\n \n # Make sure the nav bar stays at the top when window is resized\n self.resizeEvent = lambda event: nav_bar.setGeometry(0, 0, self.width(), 60)\n \n # Store references to content widgets\n self.main_content = None\n self.settings_content = None\n \n def create_recording_section(self):\n \"\"\"Creates the centered recording section with microphone button\"\"\"\n section = QWidget()\n section.setMinimumHeight(200)\n section.setStyleSheet(\"\"\"\n QWidget {\n background-color: #FFFFFF;\n border-radius: 8px;\n border: 1px solid #E9ECEF;\n }\n \"\"\")\n \n layout = QVBoxLayout(section)\n layout.setAlignment(Qt.AlignmentFlag.AlignCenter)\n layout.setSpacing(15)\n \n # Create large microphone button\n self.record_button = QToolButton()\n self.record_button.setFixedSize(80, 80)\n \n # Create microphone icon\n record_pixmap = QPixmap(80, 80)\n record_pixmap.fill(Qt.GlobalColor.transparent)\n painter = QPainter(record_pixmap)\n painter.setRenderHint(QPainter.RenderHint.Antialiasing)\n \n # Draw blue circle background\n painter.setBrush(QColor(\"#007BFF\"))\n painter.setPen(Qt.PenStyle.NoPen)\n painter.drawEllipse(0, 0, 80, 80)\n \n # Draw microphone icon\n painter.setPen(QPen(Qt.GlobalColor.white, 3))\n painter.setBrush(Qt.GlobalColor.white)\n # Base of microphone\n painter.drawRoundedRect(32, 20, 16, 24, 4, 4)\n # Stand of microphone\n painter.drawLine(40, 44, 40, 52)\n painter.drawLine(30, 52, 50, 52)\n painter.end()\n \n self.record_icon = QIcon(record_pixmap)\n \n # Create stop icon\n stop_pixmap = QPixmap(80, 80)\n stop_pixmap.fill(Qt.GlobalColor.transparent)\n painter = QPainter(stop_pixmap)\n painter.setRenderHint(QPainter.RenderHint.Antialiasing)\n \n # Draw blue circle background\n painter.setBrush(QColor(\"#007BFF\"))\n painter.setPen(Qt.PenStyle.NoPen)\n painter.drawEllipse(0, 0, 80, 80)\n \n # Draw stop icon (square)\n painter.setBrush(Qt.GlobalColor.white)\n painter.drawRect(26, 26, 28, 28)\n painter.end()\n \n self.stop_icon = QIcon(stop_pixmap)\n \n # Set initial icon\n self.record_button.setIcon(self.record_icon)\n self.record_button.setIconSize(QSize(80, 80))\n self.record_button.setToolTip(\"Press to start recording\")\n \n # Style the button\n self.record_button.setStyleSheet(\"\"\"\n QToolButton {\n background-color: transparent;\n border: none;\n }\n QToolButton:hover {\n background-color: rgba(0, 123, 255, 0.1);\n border-radius: 40px;\n }\n \"\"\")\n \n layout.addWidget(self.record_button, alignment=Qt.AlignmentFlag.AlignCenter)\n \n # Instructional text\n instruction_label = QLabel(\"Press to start recording\")\n instruction_label.setAlignment(Qt.AlignmentFlag.AlignCenter)\n instruction_label.setStyleSheet(\"\"\"\n color: #6C757D;\n font-size: 14px;\n \"\"\")\n layout.addWidget(instruction_label)\n \n # Hotkey container - increase width to fit longer text\n hotkey_container = QFrame()\n hotkey_container.setFixedHeight(35)\n hotkey_container.setMinimumWidth(120) # Increase minimum width\n hotkey_container.setStyleSheet(\"\"\"\n QFrame {\n background-color: #E9ECEF;\n border-radius: 15px;\n padding: 5px 10px;\n }\n \"\"\")\n \n hotkey_layout = QHBoxLayout(hotkey_container)\n hotkey_layout.setContentsMargins(10, 0, 10, 0)\n \n # Set hotkey text with proper width\n hotkey_text = QLabel(\" + \".join(self.hotkey))\n hotkey_text.setAlignment(Qt.AlignmentFlag.AlignCenter)\n hotkey_text.setStyleSheet(\"\"\"\n color: #6C757D;\n font-size: 12px;\n \"\"\")\n \n # Ensure the label will expand horizontally\n hotkey_text.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred)\n hotkey_text.setMinimumWidth(100)\n \n hotkey_layout.addWidget(hotkey_text)\n layout.addWidget(hotkey_container, alignment=Qt.AlignmentFlag.AlignCenter)\n \n # Connect the button to the recording function\n self.record_button.clicked.connect(self.toggle_recording_from_ui)\n \n return section\n \n def toggle_recording_from_ui(self):\n \"\"\"Handles recording button click from the UI\"\"\"\n # This will be connected to the transcriber in whisper_app.py\n # For now, just toggle the button appearance\n if self.record_button.icon().cacheKey() == self.record_icon.cacheKey():\n self.record_button.setIcon(self.stop_icon)\n self.record_button.setToolTip(\"Press to stop recording\")\n else:\n self.record_button.setIcon(self.record_icon)\n self.record_button.setToolTip(\"Press to start recording\")\n \n # This will be properly connected in whisper_app.py\n self.toggle_recording_from_tray()\n \n def toggle_recording_icon(self, is_recording):\n \"\"\"Toggles the recording button icon based on recording state\"\"\"\n if is_recording:\n self.record_button.setIcon(self.stop_icon)\n self.record_button.setToolTip(\"Press to stop recording\")\n else:\n self.record_button.setIcon(self.record_icon)\n self.record_button.setToolTip(\"Press to start recording\")\n \n def create_transcription_section(self):\n \"\"\"Creates the transcription section\"\"\"\n section = QWidget()\n section.setMinimumHeight(150)\n section.setStyleSheet(\"\"\"\n QWidget {\n background-color: #FFFFFF;\n border-radius: 8px;\n border: 1px solid #E9ECEF;\n }\n \"\"\")\n \n layout = QVBoxLayout(section)\n layout.setContentsMargins(15, 15, 15, 15)\n layout.setSpacing(10)\n \n # Header with title and action buttons\n header_layout = QHBoxLayout()\n \n # Title\n title_label = QLabel(\"Transcription\")\n title_label.setStyleSheet(\"\"\"\n font-size: 16px;\n font-weight: bold;\n color: #212529;\n \"\"\")\n header_layout.addWidget(title_label)\n \n header_layout.addStretch()\n \n # Action buttons\n buttons_layout = QHBoxLayout()\n buttons_layout.setSpacing(8)\n \n # Clear button (trash icon)\n clear_button = QToolButton()\n clear_button.setFixedSize(32, 32)\n clear_pixmap = QPixmap(16, 16)\n clear_pixmap.fill(Qt.GlobalColor.transparent)\n painter = QPainter(clear_pixmap)\n painter.setRenderHint(QPainter.RenderHint.Antialiasing)\n \n # Draw trash icon\n painter.setPen(QPen(QColor(\"#6C757D\"), 1.5))\n painter.setBrush(Qt.BrushStyle.NoBrush)\n # Trash can body\n painter.drawRect(3, 5, 10, 10)\n # Trash can lid\n painter.drawLine(2, 5, 14, 5)\n painter.drawLine(6, 2, 10, 2)\n painter.drawLine(6, 2, 6, 5)\n painter.drawLine(10, 2, 10, 5)\n # Trash can lines\n painter.drawLine(6, 8, 6, 12)\n painter.drawLine(10, 8, 10, 12)\n painter.end()\n \n clear_button.setIcon(QIcon(clear_pixmap))\n clear_button.setIconSize(QSize(16, 16))\n clear_button.setToolTip(\"Clear transcription\")\n clear_button.setStyleSheet(\"\"\"\n QToolButton {\n background-color: transparent;\n border: none;\n border-radius: 4px;\n padding: 4px;\n }\n QToolButton:hover {\n background-color: rgba(220, 53, 69, 0.1);\n }\n \"\"\")\n clear_button.clicked.connect(self.clear_transcript)\n \n # Copy button (clipboard icon)\n copy_button = QToolButton()\n copy_button.setFixedSize(32, 32)\n copy_pixmap = QPixmap(16, 16)\n copy_pixmap.fill(Qt.GlobalColor.transparent)\n painter = QPainter(copy_pixmap)\n painter.setRenderHint(QPainter.RenderHint.Antialiasing)\n \n # Draw clipboard icon\n painter.setPen(QPen(QColor(\"#007BFF\"), 1.5))\n painter.setBrush(Qt.BrushStyle.NoBrush)\n # Clipboard body\n painter.drawRect(3, 3, 10, 12)\n # Clipboard top\n painter.drawRect(5, 1, 6, 3)\n # Clipboard lines\n painter.drawLine(6, 7, 10, 7)\n painter.drawLine(6, 9, 10, 9)\n painter.drawLine(6, 11, 10, 11)\n painter.end()\n \n copy_button.setIcon(QIcon(copy_pixmap))\n copy_button.setIconSize(QSize(16, 16))\n copy_button.setToolTip(\"Copy to clipboard\")\n copy_button.setStyleSheet(\"\"\"\n QToolButton {\n background-color: transparent;\n border: none;\n border-radius: 4px;\n padding: 4px;\n }\n QToolButton:hover {\n background-color: rgba(0, 123, 255, 0.1);\n }\n \"\"\")\n copy_button.clicked.connect(self.copy_transcript)\n \n # Save button (save icon)\n save_button = QToolButton()\n save_button.setFixedSize(32, 32)\n save_pixmap = QPixmap(16, 16)\n save_pixmap.fill(Qt.GlobalColor.transparent)\n painter = QPainter(save_pixmap)\n painter.setRenderHint(QPainter.RenderHint.Antialiasing)\n \n # Draw save icon\n painter.setPen(QPen(QColor(\"#28A745\"), 1.5))\n painter.setBrush(Qt.BrushStyle.NoBrush)\n # Floppy disk body\n painter.drawRect(2, 2, 12, 12)\n # Floppy disk top\n painter.drawRect(4, 3, 8, 4)\n # Floppy disk hole\n painter.drawRect(10, 4, 2, 2)\n # Floppy disk bottom lines\n painter.drawLine(4, 10, 12, 10)\n painter.drawLine(4, 12, 12, 12)\n painter.end()\n \n save_button.setIcon(QIcon(save_pixmap))\n save_button.setIconSize(QSize(16, 16))\n save_button.setToolTip(\"Save transcription\")\n save_button.setStyleSheet(\"\"\"\n QToolButton {\n background-color: transparent;\n border: none;\n border-radius: 4px;\n padding: 4px;\n }\n QToolButton:hover {\n background-color: rgba(40, 167, 69, 0.1);\n }\n \"\"\")\n save_button.clicked.connect(self.save_transcript)\n \n # Add buttons to layout\n buttons_layout.addWidget(clear_button)\n buttons_layout.addWidget(copy_button)\n buttons_layout.addWidget(save_button)\n \n header_layout.addLayout(buttons_layout)\n \n layout.addLayout(header_layout)\n \n # Create transcript text area\n self.transcript_text = QTextEdit()\n self.transcript_text.setPlaceholderText(\"Transcriptions will appear here...\")\n self.transcript_text.setReadOnly(True)\n self.transcript_text.setMinimumHeight(100)\n self.transcript_text.setStyleSheet(\"\"\"\n QTextEdit {\n background-color: #F8F9FA;\n border: 1px solid #E9ECEF;\n border-radius: 8px;\n padding: 10px;\n font-size: 14px;\n line-height: 1.5;\n }\n \"\"\")\n \n layout.addWidget(self.transcript_text)\n \n return section\n \n def save_transcript(self):\n \"\"\"Saves the transcription to a file\"\"\"\n if not self.transcript_text.toPlainText():\n QMessageBox.information(self, \"Save Transcription\", \"There is no transcription to save.\")\n return\n \n file_path, _ = QFileDialog.getSaveFileName(\n self,\n \"Save Transcription\",\n \"\",\n \"Text Files (*.txt);;All Files (*)\"\n )\n \n if file_path:\n try:\n with open(file_path, 'w', encoding='utf-8') as file:\n file.write(self.transcript_text.toPlainText())\n QMessageBox.information(self, \"Save Transcription\", \"Transcription saved successfully.\")\n except Exception as e:\n QMessageBox.critical(self, \"Error\", f\"Failed to save transcription: {str(e)}\")\n \n def create_statistics_section(self):\n \"\"\"Creates the statistics section at the bottom of the window\"\"\"\n section = QWidget()\n section.setFixedHeight(80)\n section.setStyleSheet(\"\"\"\n QWidget {\n background-color: #FFFFFF;\n border-radius: 8px;\n border: 1px solid #E9ECEF;\n }\n \"\"\")\n \n layout = QHBoxLayout(section)\n layout.setContentsMargins(15, 10, 15, 10)\n layout.setSpacing(15)\n \n # Load statistics\n stats = self.stats_manager.get_formatted_stats()\n \n # Create stat widgets\n self.create_stat_widget(layout, \"🎤\", \"#007BFF\", \"Recordings\", str(stats[\"total_recordings\"]))\n self.create_stat_widget(layout, \"⏳\", \"#FFC107\", \"Total Time\", stats[\"total_time\"])\n self.create_stat_widget(layout, \"📄\", \"#28A745\", \"Characters\", f\"{stats['total_characters']:,}\")\n \n # For the last session, we'll use the time saved instead\n self.create_stat_widget(layout, \"⏱\", \"#6F42C1\", \"Time Saved\", stats[\"time_saved\"])\n \n return section\n \n def create_stat_widget(self, parent_layout, icon, color, label, value):\n \"\"\"Creates a single statistic widget\"\"\"\n container = QFrame()\n container.setStyleSheet(f\"\"\"\n QFrame {{\n background-color: #FFFFFF;\n border-radius: 8px;\n }}\n \"\"\")\n \n layout = QHBoxLayout(container)\n layout.setContentsMargins(10, 5, 10, 5)\n layout.setSpacing(10)\n \n # Icon\n icon_label = QLabel(icon)\n icon_label.setStyleSheet(f\"\"\"\n font-size: 24px;\n color: {color};\n padding: 5px;\n \"\"\")\n layout.addWidget(icon_label)\n \n # Text content\n text_container = QVBoxLayout()\n text_container.setSpacing(2)\n \n # Label\n label_widget = QLabel(label)\n label_widget.setStyleSheet(\"\"\"\n color: #6C757D;\n font-size: 12px;\n \"\"\")\n text_container.addWidget(label_widget)\n \n # Value\n value_widget = QLabel(value)\n value_widget.setStyleSheet(\"\"\"\n color: #212529;\n font-size: 16px;\n font-weight: bold;\n \"\"\")\n text_container.addWidget(value_widget)\n \n layout.addLayout(text_container)\n parent_layout.addWidget(container)\n \n return container\n \n def clear_transcript(self):\n \"\"\"Czyści pole transkrypcji\"\"\"\n # Create custom styled message box\n msg_box = QMessageBox(self)\n msg_box.setWindowTitle(\"Potwierdzenie\")\n msg_box.setText(\"Czy na pewno chcesz wyczyścić historię transkrypcji?\")\n msg_box.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)\n msg_box.setDefaultButton(QMessageBox.StandardButton.No)\n \n # Apply styling to match the main UI\n msg_box.setStyleSheet(\"\"\"\n QMessageBox {\n background-color: #FFFFFF;\n }\n QLabel {\n color: #212529;\n font-size: 14px;\n }\n QPushButton {\n background-color: #007BFF;\n color: white;\n border: none;\n padding: 8px 16px;\n border-radius: 6px;\n min-width: 80px;\n }\n QPushButton:hover {\n background-color: #0056b3;\n }\n \"\"\")\n \n reply = msg_box.exec()\n \n if reply == QMessageBox.StandardButton.Yes:\n self.transcript_text.clear()\n \n def clear_stats(self):\n \"\"\"Czyści wszystkie statystyki\"\"\"\n self.stats_manager.clear_stats()\n \n def create_settings_tab(self):\n \"\"\"Creates the settings tab with improved layout\"\"\"\n scroll_area = QScrollArea()\n scroll_area.setWidgetResizable(True)\n scroll_area.setStyleSheet(\"\"\"\n QScrollArea {\n border: none;\n background-color: transparent;\n }\n QWidget {\n background-color: transparent;\n }\n \"\"\")\n \n tab = QWidget()\n layout = QVBoxLayout(tab)\n layout.setContentsMargins(20, 20, 20, 20)\n layout.setSpacing(20)\n \n # Common style for all group boxes\n group_box_style = \"\"\"\n QGroupBox {\n background-color: #FFFFFF;\n border-radius: 8px;\n border: 1px solid #E9ECEF;\n margin-top: 1em;\n padding: 15px;\n }\n QGroupBox::title {\n subcontrol-origin: margin;\n left: 10px;\n padding: 0 5px;\n color: #212529;\n font-weight: bold;\n }\n QLabel {\n color: #212529;\n }\n QCheckBox {\n color: #212529;\n }\n QComboBox {\n background-color: #FFFFFF;\n color: #212529;\n border: 1px solid #E9ECEF;\n border-radius: 8px;\n padding: 8px;\n }\n QPushButton {\n background-color: #007BFF;\n color: white;\n border: none;\n padding: 10px;\n border-radius: 8px;\n font-size: 14px;\n min-width: 120px;\n }\n QPushButton:hover {\n background-color: #0056b3;\n }\n \"\"\"\n \n # Microphone Settings Section\n mic_section = QGroupBox(\"Microphone Settings\")\n mic_section.setStyleSheet(group_box_style)\n mic_layout = QVBoxLayout(mic_section)\n \n # Microphone Selection\n mic_label = QLabel(\"Select Input Device:\")\n self.mic_combo = QComboBox()\n \n # Get available audio input devices\n import pyaudio\n p = pyaudio.PyAudio()\n for i in range(p.get_device_count()):\n device_info = p.get_device_info_by_index(i)\n if device_info['maxInputChannels'] > 0: # Only add input devices\n name = device_info['name']\n # Add device info to the display text\n display_text = f\"{name} (Channels: {int(device_info['maxInputChannels'])})\"\n self.mic_combo.addItem(display_text, device_info['index'])\n p.terminate()\n \n # Set current microphone if previously selected\n if self.selected_microphone:\n # Find the item that contains the selected microphone name\n for i in range(self.mic_combo.count()):\n if self.selected_microphone in self.mic_combo.itemText(i):\n self.mic_combo.setCurrentIndex(i)\n break\n \n # Create a horizontal layout for microphone selection and refresh button\n mic_selection_layout = QHBoxLayout()\n mic_selection_layout.addWidget(self.mic_combo)\n \n # Add refresh button\n refresh_mic_button = QPushButton(\"Refresh\")\n refresh_mic_button.setToolTip(\"Scan for new microphones\")\n refresh_mic_button.clicked.connect(self.refresh_microphone_list)\n refresh_mic_button.setFixedWidth(80)\n mic_selection_layout.addWidget(refresh_mic_button)\n \n mic_layout.addWidget(mic_label)\n mic_layout.addLayout(mic_selection_layout)\n \n # Save Microphone Button\n save_mic_button = QPushButton(\"Save Microphone Settings\")\n save_mic_button.clicked.connect(self.save_microphone_settings)\n mic_layout.addWidget(save_mic_button)\n \n layout.addWidget(mic_section)\n \n # API Settings Section\n api_section = QGroupBox(\"API Settings\")\n api_section.setStyleSheet(group_box_style)\n api_layout = QVBoxLayout(api_section)\n \n # API Provider Selection\n provider_layout = QHBoxLayout()\n provider_label = QLabel(\"API Provider:\")\n self.provider_combo = QComboBox()\n self.provider_combo.addItems([\"OpenAI\", \"DeepInfra\"])\n self.provider_combo.setCurrentText(\"OpenAI\" if self.api_provider == \"openai\" else \"DeepInfra\")\n self.provider_combo.currentTextChanged.connect(self.on_provider_changed)\n provider_layout.addWidget(provider_label)\n provider_layout.addWidget(self.provider_combo)\n api_layout.addLayout(provider_layout)\n \n # API Key Input\n key_layout = QHBoxLayout()\n key_label = QLabel(\"API Key:\")\n self.api_key_input = QLineEdit()\n self.api_key_input.setEchoMode(QLineEdit.EchoMode.Password)\n self.api_key_input.setText(self.openai_key if self.api_provider == \"openai\" else self.deepinfra_key)\n key_layout.addWidget(key_label)\n key_layout.addWidget(self.api_key_input)\n api_layout.addLayout(key_layout)\n \n # Save API Settings Button\n save_api_button = QPushButton(\"Save API Settings\")\n save_api_button.clicked.connect(self.save_api_settings)\n api_layout.addWidget(save_api_button)\n \n layout.addWidget(api_section)\n \n # Hotkey Settings Section\n hotkey_section = QGroupBox(\"Hotkey Settings\")\n hotkey_section.setStyleSheet(group_box_style)\n hotkey_layout = QVBoxLayout(hotkey_section)\n \n # Checkboxes for modifier keys\n self.ctrl_check = QCheckBox(\"Ctrl\")\n self.shift_check = QCheckBox(\"Shift\")\n self.alt_check = QCheckBox(\"Alt\")\n \n # Set initial state based on current hotkey\n self.ctrl_check.setChecked(\"Ctrl\" in self.hotkey)\n self.shift_check.setChecked(\"Shift\" in self.hotkey)\n self.alt_check.setChecked(\"Alt\" in self.hotkey)\n \n hotkey_layout.addWidget(QLabel(\"Select at least 2 modifier keys:\"))\n hotkey_layout.addWidget(self.ctrl_check)\n hotkey_layout.addWidget(self.shift_check)\n hotkey_layout.addWidget(self.alt_check)\n \n # Save Hotkey Button\n save_hotkey_button = QPushButton(\"Save Hotkey\")\n save_hotkey_button.clicked.connect(self.save_hotkey_settings)\n hotkey_layout.addWidget(save_hotkey_button)\n \n layout.addWidget(hotkey_section)\n \n # Additional Options Section\n options_section = QGroupBox(\"Additional Options\")\n options_section.setStyleSheet(group_box_style)\n options_layout = QVBoxLayout(options_section)\n \n # Auto-paste option\n self.auto_paste_check = QCheckBox(\"Enable automatic pasting\")\n self.auto_paste_check.setChecked(self.auto_paste_enabled)\n options_layout.addWidget(self.auto_paste_check)\n \n # Tray notifications option\n self.tray_notif_check = QCheckBox(\"Enable tray notifications\")\n self.tray_notif_check.setChecked(self.tray_notifications_enabled)\n options_layout.addWidget(self.tray_notif_check)\n \n # Startup option\n self.startup_check = QCheckBox(\"Start with system\")\n self.startup_check.setChecked(self.startup_enabled)\n options_layout.addWidget(self.startup_check)\n \n # Save Options Button\n save_options_button = QPushButton(\"Save Options\")\n save_options_button.clicked.connect(self.save_additional_options)\n options_layout.addWidget(save_options_button)\n \n layout.addWidget(options_section)\n \n # Add a spacer at the bottom\n layout.addStretch()\n \n scroll_area.setWidget(tab)\n return scroll_area\n \n def on_provider_changed(self, provider):\n \"\"\"Updates the API key input when provider is changed\"\"\"\n self.api_key_input.setText(self.openai_key if provider == \"OpenAI\" else self.deepinfra_key)\n \n def save_api_settings(self):\n \"\"\"Saves the API settings\"\"\"\n provider = self.provider_combo.currentText()\n api_key = self.api_key_input.text().strip()\n \n if not api_key:\n QMessageBox.warning(self, \"API Settings\", \"API key cannot be empty.\")\n return\n \n self.api_provider = provider.lower()\n if self.api_provider == \"openai\":\n self.openai_key = api_key\n self.settings.setValue(\"openai_key\", api_key)\n else:\n self.deepinfra_key = api_key\n self.settings.setValue(\"deepinfra_key\", api_key)\n \n self.settings.setValue(\"api_provider\", self.api_provider)\n \n self.api_settings_changed.emit({\n \"provider\": self.api_provider,\n \"key\": api_key\n })\n \n QMessageBox.information(self, \"API Settings\", f\"API settings for {provider} have been saved.\")\n \n def save_hotkey_settings(self):\n \"\"\"Saves the hotkey settings\"\"\"\n selected_keys = []\n if self.ctrl_check.isChecked():\n selected_keys.append(\"Ctrl\")\n if self.shift_check.isChecked():\n selected_keys.append(\"Shift\")\n if self.alt_check.isChecked():\n selected_keys.append(\"Alt\")\n \n if len(selected_keys) < 2:\n QMessageBox.warning(self, \"Hotkey Settings\", \"Please select at least 2 modifier keys.\")\n return\n \n self.hotkey = selected_keys\n self.settings.setValue(\"hotkey\", selected_keys)\n self.hotkey_changed.emit(selected_keys)\n \n QMessageBox.information(self, \"Hotkey Settings\", f\"Hotkey has been changed to {' + '.join(selected_keys)}\")\n \n def save_additional_options(self):\n \"\"\"Saves the additional options\"\"\"\n # Update auto-paste setting\n auto_paste = self.auto_paste_check.isChecked()\n self.auto_paste_enabled = auto_paste\n self.settings.setValue(\"auto_paste_enabled\", auto_paste)\n self.option_changed.emit(\"auto_paste\", auto_paste)\n \n # Update tray notifications setting\n tray_notif = self.tray_notif_check.isChecked()\n self.tray_notifications_enabled = tray_notif\n self.settings.setValue(\"tray_notifications_enabled\", tray_notif)\n self.option_changed.emit(\"tray_notifications\", tray_notif)\n \n # Update startup setting\n startup = self.startup_check.isChecked()\n self.startup_enabled = startup\n self.settings.setValue(\"startup_enabled\", startup)\n self.option_changed.emit(\"startup\", startup)\n self.set_startup_registry(startup)\n \n QMessageBox.information(self, \"Options\", \"Additional options have been saved.\")\n\n def set_startup_registry(self, enable):\n \"\"\"Sets or removes registry entry for autostart\"\"\"\n try:\n import winreg as reg\n \n # Registry key path for startup programs\n key_path = r\"Software\\Microsoft\\Windows\\CurrentVersion\\Run\"\n \n # Open or create key\n registry_key = reg.OpenKey(reg.HKEY_CURRENT_USER, key_path, 0, reg.KEY_WRITE)\n \n app_path = sys.argv[0]\n # If launched by Python, find the proper path\n if app_path.endswith('.py'):\n app_path = f'pythonw \"{app_path}\"'\n elif not app_path.endswith('.exe'):\n # If not exe or py, probably a script run by Python\n app_path = f'pythonw \"{app_path}\"'\n \n if enable:\n # Add program to autostart\n reg.SetValueEx(registry_key, \"WhisperTranscriber\", 0, reg.REG_SZ, app_path)\n print(f\"Added application to autostart: {app_path}\")\n else:\n try:\n # Remove program from autostart\n reg.DeleteValue(registry_key, \"WhisperTranscriber\")\n print(\"Removed application from autostart\")\n except FileNotFoundError:\n # Key doesn't exist, nothing to remove\n pass\n \n reg.CloseKey(registry_key)\n return True\n except Exception as e:\n print(f\"Error configuring autostart: {e}\")\n QMessageBox.warning(\n self,\n \"Autostart Configuration Error\",\n f\"An error occurred while configuring autostart: {e}\"\n )\n return False\n \n def setup_system_tray(self):\n \"\"\"Configures the system tray icon\"\"\"\n # Create the tray icon\n self.tray_icon = QSystemTrayIcon(self)\n \n # Create a default icon if the icon file doesn't exist\n if os.path.exists(\"whisper_icon.png\"):\n icon = QIcon(\"whisper_icon.png\")\n else:\n # Create a simple blue icon\n pixmap = QPixmap(64, 64)\n pixmap.fill(Qt.GlobalColor.transparent)\n painter = QPainter(pixmap)\n painter.setRenderHint(QPainter.RenderHint.Antialiasing)\n painter.setBrush(QColor(\"#007BFF\")) # Blue color from the UI\n painter.setPen(Qt.PenStyle.NoPen)\n painter.drawRoundedRect(8, 8, 48, 48, 10, 10)\n \n # Add a microphone icon\n painter.setPen(QPen(Qt.GlobalColor.white, 2))\n painter.setBrush(Qt.GlobalColor.white)\n # Base of microphone\n painter.drawRoundedRect(24, 18, 16, 20, 4, 4)\n # Stand of microphone\n painter.drawLine(32, 38, 32, 44)\n painter.drawLine(24, 44, 40, 44)\n painter.end()\n \n icon = QIcon(pixmap)\n \n self.tray_icon.setIcon(icon)\n self.setWindowIcon(icon) # Set the same icon for the application window\n \n # Create context menu for the tray icon\n tray_menu = QMenu()\n \n # Show/hide action\n self.show_action = QAction(\"Show\", self)\n self.show_action.triggered.connect(self.show_window)\n tray_menu.addAction(self.show_action)\n \n # Recording action\n self.record_action = QAction(\"Start Recording\", self)\n self.record_action.triggered.connect(self.toggle_recording_from_tray)\n tray_menu.addAction(self.record_action)\n \n # Separator\n tray_menu.addSeparator()\n \n # Exit action\n exit_action = QAction(\"Exit\", self)\n exit_action.triggered.connect(self.close_from_tray)\n tray_menu.addAction(exit_action)\n \n # Set the menu for the tray icon\n self.tray_icon.setContextMenu(tray_menu)\n \n # Handle tray icon clicks\n self.tray_icon.activated.connect(self.tray_icon_activated)\n \n # Show the tray icon\n self.tray_icon.show()\n \n # Set tooltip\n self.tray_icon.setToolTip(\"Whisper Transcriber\")\n \n def show_window(self):\n \"\"\"Shows the application window\"\"\"\n self.showNormal()\n self.activateWindow()\n self.show_action.setText(\"Hide\")\n \n def tray_icon_activated(self, reason):\n \"\"\"Handles tray icon activation\"\"\"\n if reason == QSystemTrayIcon.ActivationReason.Trigger:\n # Single click\n if self.isVisible():\n self.hide()\n self.show_action.setText(\"Show\")\n else:\n self.show_window()\n \n def closeEvent(self, event):\n \"\"\"Handles window close event\"\"\"\n # Minimize to tray instead of closing\n event.ignore()\n self.hide()\n self.show_action.setText(\"Show\")\n \n if self.tray_notifications_enabled:\n self.tray_icon.showMessage(\n \"Whisper Transcriber\",\n \"Application is running in the background. Click the icon to show.\",\n QSystemTrayIcon.MessageIcon.Information,\n 3000\n )\n \n def close_from_tray(self):\n \"\"\"Closes the application from the system tray\"\"\"\n self.tray_icon.hide() # Hide the icon before closing\n QApplication.quit()\n \n def copy_transcript(self):\n clipboard = QApplication.clipboard()\n clipboard.setText(self.transcript_text.toPlainText())\n\n def update_last_recording_stats(self, duration_seconds, text):\n \"\"\"Updates statistics after recording completion\"\"\"\n chars = len(text)\n \n # Update general statistics\n self.stats_manager.update_recording_stats(duration_seconds, chars)\n \n # If we're in the main view, refresh the statistics section\n if hasattr(self, 'content_layout') and self.content_layout.count() > 0:\n # Find the main content widget if it exists\n main_content = None\n for i in range(self.content_layout.count()):\n widget = self.content_layout.itemAt(i).widget()\n if widget:\n main_content = widget\n break\n \n if main_content and main_content.layout():\n # Remove any existing statistics section (usually the last widget)\n for i in reversed(range(main_content.layout().count())):\n widget = main_content.layout().itemAt(i).widget()\n if widget and widget.height() == 80: # Stats section has fixed height of 80\n widget.deleteLater()\n break\n \n # Add the new statistics section\n main_content.layout().addWidget(self.create_statistics_section())\n \n def show_main_view(self):\n \"\"\"Shows the main view\"\"\"\n # Update button styles\n self.main_button.setStyleSheet(\"\"\"\n QPushButton {\n background-color: #007BFF;\n color: #FFFFFF;\n border-radius: 8px;\n padding: 8px 16px;\n font-weight: bold;\n }\n \"\"\")\n \n self.settings_button.setStyleSheet(\"\"\"\n QPushButton {\n background-color: #F8F9FC;\n color: #212529;\n border: 1px solid #E9ECEF;\n border-radius: 8px;\n padding: 8px 16px;\n }\n QPushButton:hover {\n background-color: #E9ECEF;\n }\n \"\"\")\n \n # Clear existing content\n for i in reversed(range(self.content_layout.count())):\n widget = self.content_layout.itemAt(i).widget()\n if widget:\n widget.deleteLater()\n \n # Create main content widget\n main_content = QWidget()\n main_content_layout = QVBoxLayout(main_content)\n main_content_layout.setContentsMargins(20, 80, 20, 20) # Extra top margin for nav bar\n main_content_layout.setSpacing(20)\n \n # Add sections\n main_content_layout.addWidget(self.create_recording_section())\n main_content_layout.addWidget(self.create_transcription_section())\n main_content_layout.addWidget(self.create_statistics_section())\n \n # Add to content container\n self.content_layout.addWidget(main_content)\n \n def show_settings_view(self):\n \"\"\"Shows the settings view\"\"\"\n # Update button styles\n self.settings_button.setStyleSheet(\"\"\"\n QPushButton {\n background-color: #007BFF;\n color: #FFFFFF;\n border-radius: 8px;\n padding: 8px 16px;\n font-weight: bold;\n }\n \"\"\")\n \n self.main_button.setStyleSheet(\"\"\"\n QPushButton {\n background-color: #F8F9FC;\n color: #212529;\n border: 1px solid #E9ECEF;\n border-radius: 8px;\n padding: 8px 16px;\n }\n QPushButton:hover {\n background-color: #E9ECEF;\n }\n \"\"\")\n \n # Clear existing content\n for i in reversed(range(self.content_layout.count())):\n widget = self.content_layout.itemAt(i).widget()\n if widget:\n widget.deleteLater()\n \n # Create settings content\n settings_content = QWidget()\n settings_layout = QVBoxLayout(settings_content)\n settings_layout.setContentsMargins(20, 80, 20, 20) # Extra top margin for nav bar\n settings_layout.setSpacing(20)\n \n # Add settings content\n settings_layout.addWidget(self.create_settings_tab())\n \n # Add to content container\n self.content_layout.addWidget(settings_content)\n \n def toggle_recording_from_tray(self):\n \"\"\"Toggles recording from the system tray\"\"\"\n # This function will be connected to the transcriber object in whisper_app.py\n pass\n \n def on_tray_notifications_changed(self, index):\n \"\"\"Handles change in tray notification settings\"\"\"\n enabled = (index == 0)\n self.tray_notifications_enabled = enabled\n self.settings.setValue(\"tray_notifications_enabled\", enabled)\n self.option_changed.emit(\"tray_notifications\", enabled)\n \n def on_auto_paste_changed(self, index):\n \"\"\"Handles change in automatic pasting settings\"\"\"\n enabled = (index == 0)\n self.auto_paste_enabled = enabled\n self.settings.setValue(\"auto_paste_enabled\", enabled)\n self.option_changed.emit(\"auto_paste\", enabled)\n \n def on_startup_changed(self, index):\n \"\"\"Handles change in startup settings\"\"\"\n enabled = (index == 0)\n self.startup_enabled = enabled\n self.settings.setValue(\"startup_enabled\", enabled)\n self.option_changed.emit(\"startup\", enabled)\n \n if enabled:\n self.set_startup_registry(True)\n else:\n self.set_startup_registry(False)\n\n def save_microphone_settings(self):\n \"\"\"Saves the selected microphone settings\"\"\"\n if self.mic_combo.currentText() == \"No microphones found\":\n QMessageBox.warning(self, \"No Microphone Selected\", \"No valid microphone is available to select.\")\n return\n \n # Extract just the microphone name without the additional info\n full_text = self.mic_combo.currentText()\n mic_name = full_text.split(\" (Channels:\")[0]\n \n # Store the microphone name with proper encoding\n self.selected_microphone = mic_name\n self.settings.setValue(\"selected_microphone\", mic_name)\n self.microphone_changed.emit(mic_name)\n QMessageBox.information(self, \"Microphone Settings\", f\"Microphone has been set to: {mic_name}\")\n\n def refresh_microphone_list(self):\n \"\"\"Refreshes the list of available microphones\"\"\"\n self.mic_combo.clear()\n \n # Get available audio input devices\n import pyaudio\n p = pyaudio.PyAudio()\n found_devices = False\n \n for i in range(p.get_device_count()):\n device_info = p.get_device_info_by_index(i)\n if device_info['maxInputChannels'] > 0: # Only add input devices\n found_devices = True\n name = device_info['name']\n \n # Fix encoding of device name for display\n try:\n name_utf8 = name.encode('latin1').decode('utf-8')\n except (UnicodeDecodeError, UnicodeEncodeError):\n name_utf8 = name\n \n # Add device info to the display text\n display_text = f\"{name_utf8} (Channels: {int(device_info['maxInputChannels'])})\"\n self.mic_combo.addItem(display_text, device_info['index'])\n p.terminate()\n \n if not found_devices:\n self.mic_combo.addItem(\"No microphones found\")\n QMessageBox.warning(self, \"No Microphones\", \"No microphone devices were found on your system.\")\n else:\n # Set current microphone if previously selected\n if self.selected_microphone:\n # Find the item that contains the selected microphone name\n for i in range(self.mic_combo.count()):\n if self.selected_microphone in self.mic_combo.itemText(i):\n self.mic_combo.setCurrentIndex(i)\n break\n \n QMessageBox.information(self, \"Microphones Refreshed\", f\"Found {self.mic_combo.count()} microphone device(s).\")\n\n def get_api_settings(self):\n \"\"\"Returns current API settings\"\"\"\n return {\n \"provider\": self.api_provider,\n \"key\": self.get_active_api_key()\n }\n \n def get_active_api_key(self):\n \"\"\"Returns active API key based on selected provider\"\"\"\n if self.api_provider == \"openai\":\n return self.openai_key\n elif self.api_provider == \"deepinfra\":\n return self.deepinfra_key\n return \"\"\n \n def get_selected_microphone(self):\n \"\"\"Returns the currently selected microphone\"\"\"\n return self.selected_microphone\n\n def get_options(self):\n \"\"\"Returns current application options\"\"\"\n return {\n \"auto_paste_enabled\": self.auto_paste_enabled,\n \"sound_notifications_enabled\": self.settings.value(\"sound_notifications_enabled\", True, type=bool),\n \"tray_notifications_enabled\": self.tray_notifications_enabled,\n \"startup_enabled\": self.startup_enabled\n }\n\n def get_hotkey(self):\n \"\"\"Returns the current hotkey combination\"\"\"\n return self.hotkey\n \n def refresh_statistics(self):\n \"\"\"Refreshes the statistics section\"\"\"\n # Find the main content widget\n main_content = None\n for i in range(self.content_layout.count()):\n widget = self.content_layout.itemAt(i).widget()\n if widget and hasattr(widget, 'layout'):\n main_content = widget\n break\n \n if not main_content:\n return\n \n # Find and remove the statistics section if it exists\n main_layout = main_content.layout()\n if not main_layout:\n return\n \n for i in reversed(range(main_layout.count())):\n widget = main_layout.itemAt(i).widget()\n if widget and widget.height() == 80: # Stats section has fixed height\n widget.deleteLater()\n break\n \n # Add the new statistics section\n main_layout.addWidget(self.create_statistics_section())\n\nif __name__ == \"__main__\":\n app = QApplication(sys.argv)\n window = WhisperMainWindow()\n window.show()\n sys.exit(app.exec())\n"}}}}