meeting-noter 2.0.0__py3-none-any.whl → 3.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of meeting-noter might be problematic. Click here for more details.
- meeting_noter/__init__.py +1 -1
- meeting_noter/cli.py +41 -0
- meeting_noter/gui/__init__.py +51 -0
- meeting_noter/gui/main_window.py +219 -0
- meeting_noter/gui/menubar.py +248 -0
- meeting_noter/gui/screens/__init__.py +17 -0
- meeting_noter/gui/screens/dashboard.py +262 -0
- meeting_noter/gui/screens/logs.py +184 -0
- meeting_noter/gui/screens/recordings.py +279 -0
- meeting_noter/gui/screens/search.py +229 -0
- meeting_noter/gui/screens/settings.py +232 -0
- meeting_noter/gui/screens/viewer.py +140 -0
- meeting_noter/gui/theme/__init__.py +5 -0
- meeting_noter/gui/theme/dark_theme.py +53 -0
- meeting_noter/gui/theme/styles.qss +504 -0
- meeting_noter/gui/utils/__init__.py +15 -0
- meeting_noter/gui/utils/signals.py +82 -0
- meeting_noter/gui/utils/workers.py +258 -0
- meeting_noter/gui/widgets/__init__.py +6 -0
- meeting_noter/gui/widgets/sidebar.py +210 -0
- meeting_noter/gui/widgets/status_indicator.py +108 -0
- {meeting_noter-2.0.0.dist-info → meeting_noter-3.0.1.dist-info}/METADATA +2 -1
- {meeting_noter-2.0.0.dist-info → meeting_noter-3.0.1.dist-info}/RECORD +26 -7
- {meeting_noter-2.0.0.dist-info → meeting_noter-3.0.1.dist-info}/WHEEL +0 -0
- {meeting_noter-2.0.0.dist-info → meeting_noter-3.0.1.dist-info}/entry_points.txt +0 -0
- {meeting_noter-2.0.0.dist-info → meeting_noter-3.0.1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
"""Dashboard screen for Meeting Noter GUI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import signal
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from PySide6.QtCore import Qt
|
|
12
|
+
from PySide6.QtGui import QFont
|
|
13
|
+
from PySide6.QtWidgets import (
|
|
14
|
+
QCheckBox,
|
|
15
|
+
QFrame,
|
|
16
|
+
QHBoxLayout,
|
|
17
|
+
QLabel,
|
|
18
|
+
QLineEdit,
|
|
19
|
+
QPushButton,
|
|
20
|
+
QTextEdit,
|
|
21
|
+
QVBoxLayout,
|
|
22
|
+
QWidget,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
from meeting_noter.config import generate_meeting_name, get_config
|
|
26
|
+
from meeting_noter.daemon import is_process_running, read_pid_file
|
|
27
|
+
from meeting_noter.gui.utils.workers import LiveTranscriptWatcher
|
|
28
|
+
from meeting_noter.gui.widgets.status_indicator import StatusIndicator
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
DEFAULT_PID_FILE = Path.home() / ".meeting-noter.pid"
|
|
32
|
+
WATCHER_PID_FILE = Path.home() / ".meeting-noter-watcher.pid"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class DashboardScreen(QWidget):
|
|
36
|
+
"""Main dashboard screen with recording controls."""
|
|
37
|
+
|
|
38
|
+
def __init__(self, parent=None):
|
|
39
|
+
super().__init__(parent)
|
|
40
|
+
self._setup_ui()
|
|
41
|
+
self._setup_workers()
|
|
42
|
+
self._connect_signals()
|
|
43
|
+
|
|
44
|
+
def _setup_ui(self):
|
|
45
|
+
"""Set up the dashboard UI."""
|
|
46
|
+
mono_font = QFont("Menlo, Monaco, Courier New, monospace")
|
|
47
|
+
|
|
48
|
+
layout = QVBoxLayout(self)
|
|
49
|
+
layout.setContentsMargins(30, 30, 30, 30)
|
|
50
|
+
layout.setSpacing(20)
|
|
51
|
+
|
|
52
|
+
# Title
|
|
53
|
+
title = QLabel("Dashboard")
|
|
54
|
+
title.setProperty("class", "heading")
|
|
55
|
+
title_font = QFont("Menlo, Monaco, Courier New, monospace", 22)
|
|
56
|
+
title_font.setBold(True)
|
|
57
|
+
title.setFont(title_font)
|
|
58
|
+
title.setStyleSheet("color: #ffffff;")
|
|
59
|
+
layout.addWidget(title)
|
|
60
|
+
|
|
61
|
+
# Status indicator
|
|
62
|
+
self._status_indicator = StatusIndicator()
|
|
63
|
+
layout.addWidget(self._status_indicator)
|
|
64
|
+
|
|
65
|
+
# Recording controls panel
|
|
66
|
+
controls_panel = QFrame()
|
|
67
|
+
controls_panel.setProperty("class", "panel")
|
|
68
|
+
controls_panel.setStyleSheet(
|
|
69
|
+
"background-color: #0d0d0d; border: 1px solid #2a2a2a; border-radius: 8px; padding: 20px;"
|
|
70
|
+
)
|
|
71
|
+
controls_layout = QVBoxLayout(controls_panel)
|
|
72
|
+
controls_layout.setSpacing(15)
|
|
73
|
+
|
|
74
|
+
# Meeting name input
|
|
75
|
+
name_row = QHBoxLayout()
|
|
76
|
+
name_label = QLabel("Meeting name:")
|
|
77
|
+
name_label.setFont(mono_font)
|
|
78
|
+
name_label.setFixedWidth(140)
|
|
79
|
+
self._name_input = QLineEdit()
|
|
80
|
+
self._name_input.setPlaceholderText(generate_meeting_name())
|
|
81
|
+
self._name_input.setFont(mono_font)
|
|
82
|
+
name_row.addWidget(name_label)
|
|
83
|
+
name_row.addWidget(self._name_input)
|
|
84
|
+
controls_layout.addLayout(name_row)
|
|
85
|
+
|
|
86
|
+
# Live transcription toggle
|
|
87
|
+
toggle_row = QHBoxLayout()
|
|
88
|
+
toggle_label = QLabel("Live transcription:")
|
|
89
|
+
toggle_label.setFont(mono_font)
|
|
90
|
+
toggle_label.setFixedWidth(140)
|
|
91
|
+
self._live_toggle = QCheckBox("Show live transcript")
|
|
92
|
+
self._live_toggle.setFont(mono_font)
|
|
93
|
+
self._live_toggle.setChecked(True)
|
|
94
|
+
toggle_row.addWidget(toggle_label)
|
|
95
|
+
toggle_row.addWidget(self._live_toggle)
|
|
96
|
+
toggle_row.addStretch()
|
|
97
|
+
controls_layout.addLayout(toggle_row)
|
|
98
|
+
|
|
99
|
+
# Buttons
|
|
100
|
+
button_row = QHBoxLayout()
|
|
101
|
+
button_row.setSpacing(12)
|
|
102
|
+
|
|
103
|
+
self._start_btn = QPushButton("▶ Start Recording")
|
|
104
|
+
self._start_btn.setProperty("class", "success")
|
|
105
|
+
self._start_btn.setFont(mono_font)
|
|
106
|
+
self._start_btn.setStyleSheet(
|
|
107
|
+
"background-color: #4ec9b0; color: #0d0d0d; min-width: 160px; padding: 12px 24px; font-weight: 600;"
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
self._stop_btn = QPushButton("■ Stop")
|
|
111
|
+
self._stop_btn.setProperty("class", "danger")
|
|
112
|
+
self._stop_btn.setFont(mono_font)
|
|
113
|
+
self._stop_btn.setStyleSheet(
|
|
114
|
+
"background-color: #f14c4c; color: #ffffff; min-width: 100px; padding: 12px 24px; font-weight: 600;"
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
button_row.addWidget(self._start_btn)
|
|
118
|
+
button_row.addWidget(self._stop_btn)
|
|
119
|
+
button_row.addStretch()
|
|
120
|
+
controls_layout.addLayout(button_row)
|
|
121
|
+
|
|
122
|
+
layout.addWidget(controls_panel)
|
|
123
|
+
|
|
124
|
+
# Live transcription display
|
|
125
|
+
transcript_label = QLabel("LIVE TRANSCRIPTION")
|
|
126
|
+
transcript_label.setFont(mono_font)
|
|
127
|
+
transcript_label.setStyleSheet("font-size: 11px; font-weight: 600; color: #5a5a5a; letter-spacing: 2px;")
|
|
128
|
+
layout.addWidget(transcript_label)
|
|
129
|
+
|
|
130
|
+
self._transcript_display = QTextEdit()
|
|
131
|
+
self._transcript_display.setReadOnly(True)
|
|
132
|
+
self._transcript_display.setPlaceholderText("Waiting for transcription...")
|
|
133
|
+
self._transcript_display.setMinimumHeight(200)
|
|
134
|
+
self._transcript_display.setFont(mono_font)
|
|
135
|
+
self._transcript_display.setStyleSheet(
|
|
136
|
+
"background-color: #0d0d0d; border: 1px solid #2a2a2a; border-radius: 6px; "
|
|
137
|
+
"color: #4ec9b0; padding: 12px; font-size: 13px;"
|
|
138
|
+
)
|
|
139
|
+
layout.addWidget(self._transcript_display, 1) # Stretch factor
|
|
140
|
+
|
|
141
|
+
def _setup_workers(self):
|
|
142
|
+
"""Set up background workers."""
|
|
143
|
+
self._transcript_watcher = LiveTranscriptWatcher()
|
|
144
|
+
self._transcript_watcher.new_content.connect(self._on_new_transcript)
|
|
145
|
+
self._transcript_watcher.file_changed.connect(self._on_transcript_file_changed)
|
|
146
|
+
self._transcript_watcher.recording_ended.connect(self._on_recording_ended)
|
|
147
|
+
self._transcript_watcher.start()
|
|
148
|
+
|
|
149
|
+
def _connect_signals(self):
|
|
150
|
+
"""Connect button signals."""
|
|
151
|
+
self._start_btn.clicked.connect(self._start_recording)
|
|
152
|
+
self._stop_btn.clicked.connect(self._stop_recording)
|
|
153
|
+
self._live_toggle.toggled.connect(self._on_live_toggle)
|
|
154
|
+
|
|
155
|
+
def _start_recording(self):
|
|
156
|
+
"""Start a new recording."""
|
|
157
|
+
# Check if already recording
|
|
158
|
+
daemon_pid = read_pid_file(DEFAULT_PID_FILE)
|
|
159
|
+
if daemon_pid and is_process_running(daemon_pid):
|
|
160
|
+
self._show_message("Already recording. Stop first.")
|
|
161
|
+
return
|
|
162
|
+
|
|
163
|
+
# Get meeting name
|
|
164
|
+
meeting_name = self._name_input.text().strip() or generate_meeting_name()
|
|
165
|
+
|
|
166
|
+
# Start recording daemon
|
|
167
|
+
subprocess.Popen(
|
|
168
|
+
[sys.executable, "-m", "meeting_noter.cli", "daemon", "--name", meeting_name],
|
|
169
|
+
stdout=subprocess.DEVNULL,
|
|
170
|
+
stderr=subprocess.DEVNULL,
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
# Clear input and transcript
|
|
174
|
+
self._name_input.clear()
|
|
175
|
+
self._name_input.setPlaceholderText(generate_meeting_name())
|
|
176
|
+
self._transcript_display.clear()
|
|
177
|
+
self._transcript_display.append(f"Starting recording: {meeting_name}\n")
|
|
178
|
+
|
|
179
|
+
def _stop_recording(self):
|
|
180
|
+
"""Stop recording."""
|
|
181
|
+
daemon_pid = read_pid_file(DEFAULT_PID_FILE)
|
|
182
|
+
watcher_pid = None
|
|
183
|
+
|
|
184
|
+
if WATCHER_PID_FILE.exists():
|
|
185
|
+
try:
|
|
186
|
+
watcher_pid = int(WATCHER_PID_FILE.read_text().strip())
|
|
187
|
+
except (ValueError, FileNotFoundError):
|
|
188
|
+
pass
|
|
189
|
+
|
|
190
|
+
stopped = []
|
|
191
|
+
|
|
192
|
+
# Stop daemon gracefully
|
|
193
|
+
if daemon_pid and is_process_running(daemon_pid):
|
|
194
|
+
try:
|
|
195
|
+
os.kill(daemon_pid, signal.SIGTERM)
|
|
196
|
+
stopped.append("recording")
|
|
197
|
+
except ProcessLookupError:
|
|
198
|
+
pass
|
|
199
|
+
|
|
200
|
+
# Stop watcher
|
|
201
|
+
if watcher_pid:
|
|
202
|
+
try:
|
|
203
|
+
os.kill(watcher_pid, signal.SIGTERM)
|
|
204
|
+
stopped.append("watcher")
|
|
205
|
+
except ProcessLookupError:
|
|
206
|
+
pass
|
|
207
|
+
WATCHER_PID_FILE.unlink(missing_ok=True)
|
|
208
|
+
|
|
209
|
+
if not stopped:
|
|
210
|
+
self._show_message("Nothing running")
|
|
211
|
+
else:
|
|
212
|
+
self._show_message(f"Stopped: {', '.join(stopped)}")
|
|
213
|
+
|
|
214
|
+
def _on_live_toggle(self, checked: bool):
|
|
215
|
+
"""Handle live transcription toggle."""
|
|
216
|
+
if checked:
|
|
217
|
+
self._transcript_watcher.start()
|
|
218
|
+
else:
|
|
219
|
+
self._transcript_watcher.stop()
|
|
220
|
+
|
|
221
|
+
def _on_new_transcript(self, text: str):
|
|
222
|
+
"""Handle new transcript content."""
|
|
223
|
+
if self._live_toggle.isChecked():
|
|
224
|
+
# Format timestamps in cyan
|
|
225
|
+
lines = text.splitlines()
|
|
226
|
+
for line in lines:
|
|
227
|
+
if line.strip() and line.startswith("["):
|
|
228
|
+
self._transcript_display.append(line)
|
|
229
|
+
# Auto-scroll to bottom
|
|
230
|
+
scrollbar = self._transcript_display.verticalScrollBar()
|
|
231
|
+
scrollbar.setValue(scrollbar.maximum())
|
|
232
|
+
|
|
233
|
+
def _on_transcript_file_changed(self, meeting_name: str):
|
|
234
|
+
"""Handle new transcript file."""
|
|
235
|
+
self._transcript_display.clear()
|
|
236
|
+
self._transcript_display.append(f"Transcribing: {meeting_name}\n")
|
|
237
|
+
|
|
238
|
+
def _on_recording_ended(self):
|
|
239
|
+
"""Handle recording end."""
|
|
240
|
+
self._transcript_display.append("\n--- Recording ended ---")
|
|
241
|
+
|
|
242
|
+
def _show_message(self, message: str):
|
|
243
|
+
"""Show a message in the transcript area."""
|
|
244
|
+
self._transcript_display.append(f"\n{message}")
|
|
245
|
+
|
|
246
|
+
def update_status(self, watcher_running: bool, daemon_running: bool, meeting_name: str):
|
|
247
|
+
"""Update status indicator."""
|
|
248
|
+
self._status_indicator.update_status(watcher_running, daemon_running, meeting_name)
|
|
249
|
+
|
|
250
|
+
# Update button states
|
|
251
|
+
self._start_btn.setEnabled(not daemon_running)
|
|
252
|
+
self._stop_btn.setEnabled(daemon_running or watcher_running)
|
|
253
|
+
|
|
254
|
+
def refresh(self):
|
|
255
|
+
"""Refresh the dashboard."""
|
|
256
|
+
# Update placeholder with new timestamp
|
|
257
|
+
self._name_input.setPlaceholderText(generate_meeting_name())
|
|
258
|
+
|
|
259
|
+
def stop_workers(self):
|
|
260
|
+
"""Stop background workers."""
|
|
261
|
+
if hasattr(self, "_transcript_watcher"):
|
|
262
|
+
self._transcript_watcher.stop()
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"""Logs viewer screen for Meeting Noter GUI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from PySide6.QtGui import QColor, QTextCharFormat, QTextCursor
|
|
8
|
+
from PySide6.QtWidgets import (
|
|
9
|
+
QCheckBox,
|
|
10
|
+
QHBoxLayout,
|
|
11
|
+
QLabel,
|
|
12
|
+
QPushButton,
|
|
13
|
+
QTextEdit,
|
|
14
|
+
QVBoxLayout,
|
|
15
|
+
QWidget,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
from meeting_noter.gui.utils.workers import LogWatcher
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
LOG_FILE = Path.home() / ".meeting-noter.log"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class LogsScreen(QWidget):
|
|
25
|
+
"""Screen for viewing application logs."""
|
|
26
|
+
|
|
27
|
+
def __init__(self, parent=None):
|
|
28
|
+
super().__init__(parent)
|
|
29
|
+
self._setup_ui()
|
|
30
|
+
self._setup_workers()
|
|
31
|
+
self._connect_signals()
|
|
32
|
+
|
|
33
|
+
def _setup_ui(self):
|
|
34
|
+
"""Set up the logs UI."""
|
|
35
|
+
layout = QVBoxLayout(self)
|
|
36
|
+
layout.setContentsMargins(30, 30, 30, 30)
|
|
37
|
+
layout.setSpacing(20)
|
|
38
|
+
|
|
39
|
+
# Title
|
|
40
|
+
title = QLabel("Logs")
|
|
41
|
+
title.setStyleSheet("font-size: 24px; font-weight: 600; color: #ffffff;")
|
|
42
|
+
layout.addWidget(title)
|
|
43
|
+
|
|
44
|
+
# Log path
|
|
45
|
+
path_label = QLabel(f"Log file: {LOG_FILE}")
|
|
46
|
+
path_label.setStyleSheet("color: #858585;")
|
|
47
|
+
layout.addWidget(path_label)
|
|
48
|
+
|
|
49
|
+
# Log content
|
|
50
|
+
self._log_view = QTextEdit()
|
|
51
|
+
self._log_view.setReadOnly(True)
|
|
52
|
+
self._log_view.setStyleSheet(
|
|
53
|
+
"font-family: 'Menlo', 'Monaco', 'Courier New', monospace; font-size: 12px;"
|
|
54
|
+
)
|
|
55
|
+
layout.addWidget(self._log_view, 1)
|
|
56
|
+
|
|
57
|
+
# Controls
|
|
58
|
+
controls_row = QHBoxLayout()
|
|
59
|
+
controls_row.setSpacing(10)
|
|
60
|
+
|
|
61
|
+
self._follow_toggle = QCheckBox("Follow")
|
|
62
|
+
self._follow_toggle.setChecked(True)
|
|
63
|
+
controls_row.addWidget(self._follow_toggle)
|
|
64
|
+
|
|
65
|
+
self._refresh_btn = QPushButton("Refresh")
|
|
66
|
+
self._refresh_btn.setProperty("class", "secondary")
|
|
67
|
+
self._refresh_btn.setStyleSheet("background-color: #3c3c3c;")
|
|
68
|
+
controls_row.addWidget(self._refresh_btn)
|
|
69
|
+
|
|
70
|
+
self._clear_btn = QPushButton("Clear")
|
|
71
|
+
self._clear_btn.setProperty("class", "secondary")
|
|
72
|
+
self._clear_btn.setStyleSheet("background-color: #3c3c3c;")
|
|
73
|
+
controls_row.addWidget(self._clear_btn)
|
|
74
|
+
|
|
75
|
+
controls_row.addStretch()
|
|
76
|
+
layout.addLayout(controls_row)
|
|
77
|
+
|
|
78
|
+
def _setup_workers(self):
|
|
79
|
+
"""Set up background workers."""
|
|
80
|
+
self._log_watcher = LogWatcher()
|
|
81
|
+
self._log_watcher.new_content.connect(self._on_new_content)
|
|
82
|
+
|
|
83
|
+
def _connect_signals(self):
|
|
84
|
+
"""Connect button signals."""
|
|
85
|
+
self._refresh_btn.clicked.connect(self._refresh_logs)
|
|
86
|
+
self._clear_btn.clicked.connect(self._clear_logs)
|
|
87
|
+
self._follow_toggle.toggled.connect(self._on_follow_toggled)
|
|
88
|
+
|
|
89
|
+
def _on_follow_toggled(self, checked: bool):
|
|
90
|
+
"""Handle follow toggle."""
|
|
91
|
+
if checked:
|
|
92
|
+
self._log_watcher.start()
|
|
93
|
+
else:
|
|
94
|
+
self._log_watcher.stop()
|
|
95
|
+
|
|
96
|
+
def _refresh_logs(self):
|
|
97
|
+
"""Refresh log content."""
|
|
98
|
+
self._log_view.clear()
|
|
99
|
+
self._load_logs()
|
|
100
|
+
|
|
101
|
+
# Reset watcher position
|
|
102
|
+
if self._follow_toggle.isChecked():
|
|
103
|
+
self._log_watcher.reset()
|
|
104
|
+
if not self._log_watcher.isRunning():
|
|
105
|
+
self._log_watcher.start()
|
|
106
|
+
|
|
107
|
+
def _clear_logs(self):
|
|
108
|
+
"""Clear the log display."""
|
|
109
|
+
self._log_view.clear()
|
|
110
|
+
|
|
111
|
+
def _load_logs(self):
|
|
112
|
+
"""Load the last 100 lines from the log file."""
|
|
113
|
+
if not LOG_FILE.exists():
|
|
114
|
+
self._log_view.setPlainText("No log file found.")
|
|
115
|
+
return
|
|
116
|
+
|
|
117
|
+
try:
|
|
118
|
+
with open(LOG_FILE, "r") as f:
|
|
119
|
+
lines = f.readlines()[-100:]
|
|
120
|
+
|
|
121
|
+
for line in lines:
|
|
122
|
+
self._append_colored_line(line.rstrip())
|
|
123
|
+
|
|
124
|
+
# Scroll to bottom
|
|
125
|
+
scrollbar = self._log_view.verticalScrollBar()
|
|
126
|
+
scrollbar.setValue(scrollbar.maximum())
|
|
127
|
+
|
|
128
|
+
except Exception as e:
|
|
129
|
+
self._log_view.setPlainText(f"Error reading log file: {e}")
|
|
130
|
+
|
|
131
|
+
def _on_new_content(self, content: str):
|
|
132
|
+
"""Handle new log content."""
|
|
133
|
+
if not self._follow_toggle.isChecked():
|
|
134
|
+
return
|
|
135
|
+
|
|
136
|
+
for line in content.splitlines():
|
|
137
|
+
if line.strip():
|
|
138
|
+
self._append_colored_line(line)
|
|
139
|
+
|
|
140
|
+
# Auto-scroll if following
|
|
141
|
+
scrollbar = self._log_view.verticalScrollBar()
|
|
142
|
+
scrollbar.setValue(scrollbar.maximum())
|
|
143
|
+
|
|
144
|
+
def _append_colored_line(self, line: str):
|
|
145
|
+
"""Append a line with appropriate color."""
|
|
146
|
+
cursor = self._log_view.textCursor()
|
|
147
|
+
cursor.movePosition(QTextCursor.MoveOperation.End)
|
|
148
|
+
|
|
149
|
+
fmt = QTextCharFormat()
|
|
150
|
+
|
|
151
|
+
# Color based on content
|
|
152
|
+
line_lower = line.lower()
|
|
153
|
+
if "error" in line_lower:
|
|
154
|
+
fmt.setForeground(QColor("#f14c4c")) # Red
|
|
155
|
+
elif "warning" in line_lower:
|
|
156
|
+
fmt.setForeground(QColor("#dcdcaa")) # Yellow
|
|
157
|
+
elif "recording started" in line_lower:
|
|
158
|
+
fmt.setForeground(QColor("#4ec9b0")) # Green
|
|
159
|
+
elif "recording saved" in line_lower or "transcription" in line_lower:
|
|
160
|
+
fmt.setForeground(QColor("#9cdcfe")) # Cyan
|
|
161
|
+
else:
|
|
162
|
+
fmt.setForeground(QColor("#cccccc")) # Default gray
|
|
163
|
+
|
|
164
|
+
cursor.insertText(line + "\n", fmt)
|
|
165
|
+
|
|
166
|
+
def refresh(self):
|
|
167
|
+
"""Refresh the logs view."""
|
|
168
|
+
self._refresh_logs()
|
|
169
|
+
|
|
170
|
+
def stop_workers(self):
|
|
171
|
+
"""Stop background workers."""
|
|
172
|
+
if hasattr(self, "_log_watcher"):
|
|
173
|
+
self._log_watcher.stop()
|
|
174
|
+
|
|
175
|
+
def showEvent(self, event):
|
|
176
|
+
"""Handle show event."""
|
|
177
|
+
super().showEvent(event)
|
|
178
|
+
self._refresh_logs()
|
|
179
|
+
|
|
180
|
+
def hideEvent(self, event):
|
|
181
|
+
"""Handle hide event."""
|
|
182
|
+
super().hideEvent(event)
|
|
183
|
+
if hasattr(self, "_log_watcher") and self._log_watcher.isRunning():
|
|
184
|
+
self._log_watcher.stop()
|