meeting-noter 2.0.0__py3-none-any.whl → 3.0.0__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.
@@ -0,0 +1,258 @@
1
+ """Background worker threads for Meeting Noter GUI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import time
7
+ from pathlib import Path
8
+ from typing import Optional
9
+
10
+ from PySide6.QtCore import QThread, Signal
11
+
12
+ from meeting_noter.config import get_config
13
+ from meeting_noter.daemon import is_process_running, read_pid_file
14
+
15
+
16
+ DEFAULT_PID_FILE = Path.home() / ".meeting-noter.pid"
17
+ WATCHER_PID_FILE = Path.home() / ".meeting-noter-watcher.pid"
18
+ LOG_FILE = Path.home() / ".meeting-noter.log"
19
+
20
+
21
+ class StatusPollingWorker(QThread):
22
+ """Polls daemon/watcher status periodically."""
23
+
24
+ status_updated = Signal(bool, bool, str) # watcher_running, daemon_running, meeting_name
25
+
26
+ def __init__(self, interval_ms: int = 2000):
27
+ super().__init__()
28
+ self._stop_flag = False
29
+ self._interval = interval_ms
30
+
31
+ def run(self):
32
+ """Poll status until stopped."""
33
+ while not self._stop_flag:
34
+ watcher = self._check_watcher()
35
+ daemon, name = self._check_daemon()
36
+ self.status_updated.emit(watcher, daemon, name or "")
37
+ self.msleep(self._interval)
38
+
39
+ def stop(self):
40
+ """Stop the worker."""
41
+ self._stop_flag = True
42
+ self.wait()
43
+
44
+ def _check_watcher(self) -> bool:
45
+ """Check if watcher is running."""
46
+ if WATCHER_PID_FILE.exists():
47
+ try:
48
+ pid = int(WATCHER_PID_FILE.read_text().strip())
49
+ os.kill(pid, 0)
50
+ return True
51
+ except (ProcessLookupError, ValueError, FileNotFoundError, PermissionError):
52
+ pass
53
+ return False
54
+
55
+ def _check_daemon(self) -> tuple[bool, Optional[str]]:
56
+ """Check if daemon is running and get recording name."""
57
+ daemon_pid = read_pid_file(DEFAULT_PID_FILE)
58
+ if daemon_pid and is_process_running(daemon_pid):
59
+ recording_name = self._get_current_recording_name()
60
+ return True, recording_name
61
+ return False, None
62
+
63
+ def _get_current_recording_name(self) -> Optional[str]:
64
+ """Get the name of the current recording from the log file."""
65
+ if not LOG_FILE.exists():
66
+ return None
67
+
68
+ try:
69
+ with open(LOG_FILE, "r") as f:
70
+ lines = f.readlines()
71
+
72
+ for line in reversed(lines[-50:]):
73
+ if "Recording started:" in line:
74
+ parts = line.split("Recording started:")
75
+ if len(parts) > 1:
76
+ filename = parts[1].strip().replace(".mp3", "")
77
+ name_parts = filename.split("_", 2)
78
+ if len(name_parts) >= 3:
79
+ return name_parts[2]
80
+ return filename
81
+ elif "Recording saved:" in line or "Recording discarded" in line:
82
+ break
83
+ return None
84
+ except Exception:
85
+ return None
86
+
87
+
88
+ class LiveTranscriptWatcher(QThread):
89
+ """Watches live transcript file for changes."""
90
+
91
+ new_content = Signal(str) # new text content
92
+ file_changed = Signal(str) # new file name
93
+ recording_ended = Signal()
94
+
95
+ def __init__(self, interval_ms: int = 500):
96
+ super().__init__()
97
+ self._stop_flag = False
98
+ self._interval = interval_ms
99
+ self._current_file: Optional[Path] = None
100
+ self._last_content = ""
101
+ self._last_mtime = 0.0
102
+
103
+ def run(self):
104
+ """Watch for live transcript updates."""
105
+ while not self._stop_flag:
106
+ self._check_live_transcript()
107
+ self.msleep(self._interval)
108
+
109
+ def stop(self):
110
+ """Stop the worker."""
111
+ self._stop_flag = True
112
+ self.wait()
113
+
114
+ def _check_live_transcript(self) -> None:
115
+ """Check for live transcript updates."""
116
+ config = get_config()
117
+ live_dir = config.recordings_dir / "live"
118
+
119
+ if not live_dir.exists():
120
+ return
121
+
122
+ # Find the most recent live file
123
+ try:
124
+ live_files = sorted(
125
+ live_dir.glob("*.live.txt"),
126
+ key=lambda p: p.stat().st_mtime,
127
+ reverse=True,
128
+ )
129
+ except Exception:
130
+ return
131
+
132
+ if not live_files:
133
+ if self._current_file is not None:
134
+ self.recording_ended.emit()
135
+ self._current_file = None
136
+ self._last_content = ""
137
+ return
138
+
139
+ live_file = live_files[0]
140
+
141
+ # Check if file is recent (active within last 30 seconds)
142
+ try:
143
+ file_mtime = live_file.stat().st_mtime
144
+ if time.time() - file_mtime > 30:
145
+ if self._current_file is not None:
146
+ self.recording_ended.emit()
147
+ self._current_file = None
148
+ self._last_content = ""
149
+ return
150
+ except Exception:
151
+ return
152
+
153
+ # Detect new file (new recording started)
154
+ if self._current_file != live_file:
155
+ self._current_file = live_file
156
+ self._last_content = ""
157
+ self._last_mtime = file_mtime
158
+ meeting_name = live_file.stem.replace(".live", "")
159
+ self.file_changed.emit(meeting_name)
160
+
161
+ # Read and emit new content
162
+ try:
163
+ content = live_file.read_text()
164
+ if len(content) > len(self._last_content):
165
+ new_content = content[len(self._last_content) :]
166
+ self.new_content.emit(new_content)
167
+ self._last_content = content
168
+ except Exception:
169
+ pass
170
+
171
+
172
+ class TranscriptionWorker(QThread):
173
+ """Runs transcription in background."""
174
+
175
+ progress = Signal(str) # status message
176
+ finished = Signal(str) # transcript path
177
+ error = Signal(str) # error message
178
+
179
+ def __init__(self, audio_path: Path, model: str = ""):
180
+ super().__init__()
181
+ self._audio_path = audio_path
182
+ self._model = model
183
+
184
+ def run(self):
185
+ """Run transcription."""
186
+ import subprocess
187
+ import sys
188
+
189
+ try:
190
+ self.progress.emit("Starting transcription...")
191
+
192
+ cmd = [sys.executable, "-m", "meeting_noter.cli", "transcribe", str(self._audio_path)]
193
+ if self._model:
194
+ cmd.extend(["--model", self._model])
195
+
196
+ result = subprocess.run(cmd, capture_output=True, text=True)
197
+
198
+ if result.returncode == 0:
199
+ # Extract transcript path from output
200
+ output = result.stdout
201
+ for line in output.splitlines():
202
+ if "Transcript saved:" in line:
203
+ path = line.split("Transcript saved:")[-1].strip()
204
+ self.finished.emit(path)
205
+ return
206
+ self.finished.emit("")
207
+ else:
208
+ self.error.emit(result.stderr or "Transcription failed")
209
+
210
+ except Exception as e:
211
+ self.error.emit(str(e))
212
+
213
+
214
+ class LogWatcher(QThread):
215
+ """Watches log file for new content."""
216
+
217
+ new_content = Signal(str) # new log lines
218
+
219
+ def __init__(self, interval_ms: int = 1000):
220
+ super().__init__()
221
+ self._stop_flag = False
222
+ self._interval = interval_ms
223
+ self._last_size = 0
224
+
225
+ def run(self):
226
+ """Watch log file for changes."""
227
+ while not self._stop_flag:
228
+ self._check_log_file()
229
+ self.msleep(self._interval)
230
+
231
+ def stop(self):
232
+ """Stop the worker."""
233
+ self._stop_flag = True
234
+ self.wait()
235
+
236
+ def _check_log_file(self) -> None:
237
+ """Check for new log content."""
238
+ if not LOG_FILE.exists():
239
+ return
240
+
241
+ try:
242
+ current_size = LOG_FILE.stat().st_size
243
+ if current_size > self._last_size:
244
+ with open(LOG_FILE, "r") as f:
245
+ f.seek(self._last_size)
246
+ new_content = f.read()
247
+ if new_content:
248
+ self.new_content.emit(new_content)
249
+ self._last_size = current_size
250
+ elif current_size < self._last_size:
251
+ # File was truncated, reset
252
+ self._last_size = 0
253
+ except Exception:
254
+ pass
255
+
256
+ def reset(self):
257
+ """Reset the file position."""
258
+ self._last_size = 0
@@ -0,0 +1,6 @@
1
+ """Reusable widgets for Meeting Noter GUI."""
2
+
3
+ from meeting_noter.gui.widgets.sidebar import Sidebar
4
+ from meeting_noter.gui.widgets.status_indicator import StatusIndicator
5
+
6
+ __all__ = ["Sidebar", "StatusIndicator"]
@@ -0,0 +1,210 @@
1
+ """Navigation sidebar widget for Meeting Noter GUI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from PySide6.QtCore import Qt, Signal
6
+ from PySide6.QtGui import QFont
7
+ from PySide6.QtWidgets import (
8
+ QButtonGroup,
9
+ QFrame,
10
+ QLabel,
11
+ QPushButton,
12
+ QSizePolicy,
13
+ QSpacerItem,
14
+ QVBoxLayout,
15
+ QWidget,
16
+ )
17
+
18
+
19
+ class SidebarButton(QPushButton):
20
+ """A navigation button for the sidebar."""
21
+
22
+ def __init__(self, text: str, icon: str = "", parent=None):
23
+ super().__init__(parent)
24
+ display_text = f" {icon} {text}" if icon else f" {text}"
25
+ self.setText(display_text)
26
+ self.setCheckable(True)
27
+ self.setMinimumHeight(50)
28
+ self.setCursor(Qt.PointingHandCursor)
29
+
30
+
31
+ class Sidebar(QWidget):
32
+ """Navigation sidebar with icon buttons."""
33
+
34
+ screen_selected = Signal(str) # Emits screen name when clicked
35
+ quit_requested = Signal() # Emits when quit button clicked
36
+
37
+ # Navigation items with better Unicode icons
38
+ NAV_ITEMS = [
39
+ ("dashboard", "Dashboard", "⌂"), # Home
40
+ ("recordings", "Recordings", "◉"), # Record
41
+ ("search", "Search", "⌕"), # Search
42
+ ("settings", "Settings", "⚙"), # Gear
43
+ ("logs", "Logs", "☰"), # Menu/logs
44
+ ]
45
+
46
+ def __init__(self, parent=None):
47
+ super().__init__(parent)
48
+ self.setObjectName("sidebar")
49
+ self.setMinimumWidth(220)
50
+ self.setMaximumWidth(220)
51
+
52
+ self._setup_ui()
53
+ self._connect_signals()
54
+
55
+ def _setup_ui(self):
56
+ """Set up the sidebar UI."""
57
+ layout = QVBoxLayout(self)
58
+ layout.setContentsMargins(0, 0, 0, 0)
59
+ layout.setSpacing(0)
60
+
61
+ # Title/Logo area
62
+ title_frame = QFrame()
63
+ title_frame.setObjectName("sidebar-title-frame")
64
+ title_layout = QVBoxLayout(title_frame)
65
+ title_layout.setContentsMargins(20, 20, 20, 20)
66
+
67
+ title = QLabel("Meeting Noter")
68
+ title.setObjectName("sidebar-title")
69
+ title_font = QFont("Menlo, Monaco, Courier New, monospace", 18)
70
+ title_font.setBold(True)
71
+ title.setFont(title_font)
72
+ title.setAlignment(Qt.AlignCenter)
73
+ title_layout.addWidget(title)
74
+
75
+ subtitle = QLabel("v2.0")
76
+ subtitle.setObjectName("sidebar-subtitle")
77
+ subtitle.setAlignment(Qt.AlignCenter)
78
+ subtitle.setStyleSheet("color: #5a5a5a; font-size: 12px;")
79
+ title_layout.addWidget(subtitle)
80
+
81
+ layout.addWidget(title_frame)
82
+
83
+ # Separator
84
+ separator = QFrame()
85
+ separator.setFrameShape(QFrame.HLine)
86
+ separator.setStyleSheet("background-color: #3c3c3c; max-height: 1px;")
87
+ layout.addWidget(separator)
88
+
89
+ # Navigation section label
90
+ nav_label = QLabel(" NAVIGATION")
91
+ nav_label.setObjectName("sidebar-section-label")
92
+ nav_label.setStyleSheet(
93
+ "color: #5a5a5a; font-size: 11px; font-weight: 600; "
94
+ "letter-spacing: 1px; padding: 15px 0 5px 15px;"
95
+ )
96
+ layout.addWidget(nav_label)
97
+
98
+ # Navigation buttons
99
+ self._button_group = QButtonGroup(self)
100
+ self._button_group.setExclusive(True)
101
+ self._buttons: dict[str, SidebarButton] = {}
102
+
103
+ button_font = QFont("Menlo, Monaco, Courier New, monospace", 14)
104
+
105
+ for screen_id, label, icon in self.NAV_ITEMS:
106
+ btn = SidebarButton(label, icon)
107
+ btn.setFont(button_font)
108
+ btn.setProperty("screen_id", screen_id)
109
+ self._buttons[screen_id] = btn
110
+ self._button_group.addButton(btn)
111
+ layout.addWidget(btn)
112
+
113
+ # Spacer
114
+ layout.addItem(QSpacerItem(20, 40, QSizePolicy.Minimum, QSizePolicy.Expanding))
115
+
116
+ # Status area at bottom
117
+ separator2 = QFrame()
118
+ separator2.setFrameShape(QFrame.HLine)
119
+ separator2.setStyleSheet("background-color: #3c3c3c; max-height: 1px;")
120
+ layout.addWidget(separator2)
121
+
122
+ self._status_frame = QFrame()
123
+ self._status_frame.setObjectName("sidebar-status")
124
+ status_layout = QVBoxLayout(self._status_frame)
125
+ status_layout.setContentsMargins(15, 15, 15, 15)
126
+
127
+ status_label = QLabel("STATUS")
128
+ status_label.setStyleSheet(
129
+ "color: #5a5a5a; font-size: 11px; font-weight: 600; letter-spacing: 1px;"
130
+ )
131
+ status_layout.addWidget(status_label)
132
+
133
+ self._status_text = QLabel("● Idle")
134
+ self._status_text.setObjectName("sidebar-status-text")
135
+ status_font = QFont("Menlo, Monaco, Courier New, monospace", 13)
136
+ self._status_text.setFont(status_font)
137
+ self._status_text.setStyleSheet("color: #5a5a5a; padding-top: 5px;")
138
+ status_layout.addWidget(self._status_text)
139
+
140
+ layout.addWidget(self._status_frame)
141
+
142
+ # Quit button at bottom
143
+ separator3 = QFrame()
144
+ separator3.setFrameShape(QFrame.HLine)
145
+ separator3.setStyleSheet("background-color: #3c3c3c; max-height: 1px;")
146
+ layout.addWidget(separator3)
147
+
148
+ self._quit_btn = QPushButton(" ⏻ Quit")
149
+ self._quit_btn.setFont(button_font)
150
+ self._quit_btn.setMinimumHeight(50)
151
+ self._quit_btn.setCursor(Qt.PointingHandCursor)
152
+ self._quit_btn.setStyleSheet("""
153
+ QPushButton {
154
+ background-color: transparent;
155
+ border: none;
156
+ border-radius: 6px;
157
+ padding: 12px 15px;
158
+ text-align: left;
159
+ color: #808080;
160
+ font-size: 14px;
161
+ margin: 3px 10px;
162
+ }
163
+ QPushButton:hover {
164
+ background-color: #2a1a1a;
165
+ color: #f14c4c;
166
+ }
167
+ """)
168
+ layout.addWidget(self._quit_btn)
169
+
170
+ # Bottom padding
171
+ layout.addSpacing(10)
172
+
173
+ # Select dashboard by default
174
+ self._buttons["dashboard"].setChecked(True)
175
+
176
+ def _connect_signals(self):
177
+ """Connect button signals."""
178
+ for btn in self._buttons.values():
179
+ btn.clicked.connect(self._on_button_clicked)
180
+ self._quit_btn.clicked.connect(self._on_quit_clicked)
181
+
182
+ def _on_button_clicked(self):
183
+ """Handle navigation button click."""
184
+ btn = self.sender()
185
+ if btn:
186
+ screen_id = btn.property("screen_id")
187
+ self.screen_selected.emit(screen_id)
188
+
189
+ def _on_quit_clicked(self):
190
+ """Handle quit button click."""
191
+ self.quit_requested.emit()
192
+
193
+ def select_screen(self, screen_id: str):
194
+ """Programmatically select a screen."""
195
+ if screen_id in self._buttons:
196
+ self._buttons[screen_id].setChecked(True)
197
+
198
+ def update_status(self, is_recording: bool, meeting_name: str = ""):
199
+ """Update the status display."""
200
+ if is_recording:
201
+ if meeting_name:
202
+ display_name = meeting_name[:18] + "..." if len(meeting_name) > 18 else meeting_name
203
+ status = f"● {display_name}"
204
+ else:
205
+ status = "● Recording..."
206
+ self._status_text.setStyleSheet("color: #f14c4c; padding-top: 5px;")
207
+ else:
208
+ status = "● Idle"
209
+ self._status_text.setStyleSheet("color: #5a5a5a; padding-top: 5px;")
210
+ self._status_text.setText(status)
@@ -0,0 +1,108 @@
1
+ """Status indicator widget for Meeting Noter GUI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from PySide6.QtCore import Qt
6
+ from PySide6.QtGui import QFont
7
+ from PySide6.QtWidgets import QHBoxLayout, QLabel, QVBoxLayout, QWidget
8
+
9
+ from meeting_noter.gui.theme.dark_theme import COLORS
10
+
11
+
12
+ class StatusIndicator(QWidget):
13
+ """Widget to display current recording status."""
14
+
15
+ def __init__(self, parent=None):
16
+ super().__init__(parent)
17
+ self.setObjectName("status-indicator")
18
+ self._setup_ui()
19
+ self._set_idle_state()
20
+
21
+ def _setup_ui(self):
22
+ """Set up the status indicator UI."""
23
+ self.setStyleSheet(
24
+ "background-color: #0d0d0d; border: 1px solid #2a2a2a; border-radius: 8px;"
25
+ )
26
+
27
+ layout = QHBoxLayout(self)
28
+ layout.setContentsMargins(20, 20, 20, 20)
29
+ layout.setSpacing(20)
30
+
31
+ # Status dot
32
+ self._dot = QLabel("●")
33
+ self._dot.setObjectName("status-dot")
34
+ self._dot.setAlignment(Qt.AlignCenter)
35
+ dot_font = QFont("Menlo, Monaco, Courier New, monospace", 32)
36
+ self._dot.setFont(dot_font)
37
+ layout.addWidget(self._dot)
38
+
39
+ # Status text container
40
+ text_container = QWidget()
41
+ text_container.setStyleSheet("background-color: transparent; border: none;")
42
+ text_layout = QVBoxLayout(text_container)
43
+ text_layout.setContentsMargins(0, 0, 0, 0)
44
+ text_layout.setSpacing(4)
45
+
46
+ self._status_text = QLabel("Stopped")
47
+ self._status_text.setObjectName("status-text")
48
+ status_font = QFont("Menlo, Monaco, Courier New, monospace", 18)
49
+ status_font.setBold(True)
50
+ self._status_text.setFont(status_font)
51
+ text_layout.addWidget(self._status_text)
52
+
53
+ self._details_text = QLabel("Watcher: stopped | Recorder: idle")
54
+ self._details_text.setObjectName("status-details")
55
+ details_font = QFont("Menlo, Monaco, Courier New, monospace", 12)
56
+ self._details_text.setFont(details_font)
57
+ self._details_text.setStyleSheet("color: #808080; background-color: transparent;")
58
+ text_layout.addWidget(self._details_text)
59
+
60
+ layout.addWidget(text_container)
61
+ layout.addStretch()
62
+
63
+ def _set_idle_state(self):
64
+ """Set the idle/stopped state."""
65
+ self._dot.setStyleSheet(f"color: #5a5a5a; background-color: transparent;")
66
+ self._status_text.setText("Stopped")
67
+ self._status_text.setStyleSheet("color: #808080; background-color: transparent;")
68
+ self._details_text.setText("Watcher: stopped | Recorder: idle")
69
+
70
+ def update_status(
71
+ self, watcher_running: bool, daemon_running: bool, meeting_name: str = ""
72
+ ):
73
+ """Update the status display."""
74
+ if daemon_running:
75
+ # Recording state - red
76
+ self._dot.setStyleSheet("color: #f14c4c; background-color: transparent;")
77
+ self._status_text.setStyleSheet("color: #f14c4c; background-color: transparent;")
78
+ if meeting_name:
79
+ self._status_text.setText(f"Recording: {meeting_name}")
80
+ else:
81
+ self._status_text.setText("Recording...")
82
+ elif watcher_running:
83
+ # Ready state - green
84
+ self._dot.setStyleSheet("color: #4ec9b0; background-color: transparent;")
85
+ self._status_text.setStyleSheet("color: #4ec9b0; background-color: transparent;")
86
+ self._status_text.setText("Ready (watcher active)")
87
+ else:
88
+ # Stopped state - gray
89
+ self._dot.setStyleSheet("color: #5a5a5a; background-color: transparent;")
90
+ self._status_text.setStyleSheet("color: #808080; background-color: transparent;")
91
+ self._status_text.setText("Stopped")
92
+
93
+ # Update details
94
+ watcher_status = "running" if watcher_running else "stopped"
95
+ recorder_status = "recording" if daemon_running else "idle"
96
+ self._details_text.setText(f"Watcher: {watcher_status} | Recorder: {recorder_status}")
97
+
98
+ def set_recording(self, meeting_name: str = ""):
99
+ """Set recording state."""
100
+ self.update_status(False, True, meeting_name)
101
+
102
+ def set_ready(self):
103
+ """Set ready (watcher active) state."""
104
+ self.update_status(True, False)
105
+
106
+ def set_stopped(self):
107
+ """Set stopped state."""
108
+ self.update_status(False, False)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: meeting-noter
3
- Version: 2.0.0
3
+ Version: 3.0.0
4
4
  Summary: Offline meeting transcription for macOS with automatic meeting detection
5
5
  Author: Victor
6
6
  License: MIT
@@ -36,6 +36,8 @@ Requires-Dist: pyobjc-framework-CoreMedia>=9.0; sys_platform == "darwin"
36
36
  Requires-Dist: pyobjc-framework-libdispatch>=9.0; sys_platform == "darwin"
37
37
  Provides-Extra: offline
38
38
  Requires-Dist: meeting-noter-models>=0.1.0; extra == "offline"
39
+ Provides-Extra: gui
40
+ Requires-Dist: PySide6>=6.5.0; extra == "gui"
39
41
  Provides-Extra: dev
40
42
  Requires-Dist: pytest>=7.0; extra == "dev"
41
43
  Requires-Dist: pytest-cov; extra == "dev"
@@ -1,6 +1,6 @@
1
- meeting_noter/__init__.py,sha256=UpvJiLNpsDsoEbxhb6uqsVi1Sa0aUY4BvxWHfaYcBXg,103
1
+ meeting_noter/__init__.py,sha256=IUXWaiwZS1L2077809O-HLyrSjQQUYQ30Xjw7j_HDiI,103
2
2
  meeting_noter/__main__.py,sha256=6sSOqH1o3jvgvkVzsVKmF6-xVGcUAbNVQkRl2CrygdE,120
3
- meeting_noter/cli.py,sha256=m9oHXqRLM8nilFRNaPcVgNOyflmBV9qQ6YHQZmGMG0I,36790
3
+ meeting_noter/cli.py,sha256=_nAbOnqO1eZprWO74zvHKrCfKDqn7YoFs1SsdEOY-WQ,38200
4
4
  meeting_noter/config.py,sha256=vdnTh6W6-DUPJCj0ekFu1Q1m87O7gx0QD3E5DrRGpXk,7537
5
5
  meeting_noter/daemon.py,sha256=i81IXgEnsoAoMewknySO5_ECa1nCGOsVLfXtJXJf0rw,21611
6
6
  meeting_noter/meeting_detector.py,sha256=St0qoMkvUERP4BaxnXO1M6fZDJpWqBf9In7z2SgWcWg,10564
@@ -10,6 +10,25 @@ meeting_noter/audio/__init__.py,sha256=O7PU8CxHSHxMeHbc9Jdwt9kePLQzsPh81GQU7VHCt
10
10
  meeting_noter/audio/capture.py,sha256=fDrT5oXfva8vdFlht9cv60NviKbksw2QeJ8eOtI19uE,6469
11
11
  meeting_noter/audio/encoder.py,sha256=OBsgUmlZPz-YZQZ7Rp8MAlMRaQxTsccjuTgCtvRebmc,6573
12
12
  meeting_noter/audio/system_audio.py,sha256=jbHGjNCerI19weXap0a90Ik17lVTCT1hCEgRKYke-p8,13016
13
+ meeting_noter/gui/__init__.py,sha256=XdjSYNGeiLyGb4wRfDk9mnJuYD_PC-a-tw096Zf0DNk,1401
14
+ meeting_noter/gui/main_window.py,sha256=NXl4JgPbF7MvuLrwmId6i6speYUoiwhMwubYX3C1MLA,6902
15
+ meeting_noter/gui/menubar.py,sha256=hyqm74Vp1I0eB57yzvYEvtzk4y-Y5GRCZau-erGYr_I,8261
16
+ meeting_noter/gui/screens/__init__.py,sha256=_ETRs61OD0xm_FkKTvhv286hpSEsmeZ8v15bKUSUs8Y,549
17
+ meeting_noter/gui/screens/dashboard.py,sha256=_5VzZuh4lUDKoW9iGdM9YZZAqliZtK8azYagI6zFimg,9492
18
+ meeting_noter/gui/screens/logs.py,sha256=acqNhx0H2emVDQ4uCrhkVxbExG58Z99mUrMySF321xI,5746
19
+ meeting_noter/gui/screens/recordings.py,sha256=rQ7_hLv2uq-qyFTlmnR1NuH9h0Yp5u7rh0o6Gv1Vfw8,9500
20
+ meeting_noter/gui/screens/search.py,sha256=emdfAGQR3rhtrbj5rfCYjgqPY8i6gzUO4z1wZ6naFsA,7345
21
+ meeting_noter/gui/screens/settings.py,sha256=u-L0l6I6h25Tp2PIBQW8KipKEBc2VYGsxrXwEENLJxU,8016
22
+ meeting_noter/gui/screens/viewer.py,sha256=TEVCO1tcYVO1li1pt846fKx5VxTxk-lWywnoBV2XuUc,4383
23
+ meeting_noter/gui/theme/__init__.py,sha256=UMcYNuIxJSuLH9iFerklxShJd9BGqyFdeEHYwzWYk8E,148
24
+ meeting_noter/gui/theme/dark_theme.py,sha256=UAgrJDjPgzkjBg7ucg1NC0jOUylh1-n0BcQL9mBZNMA,1795
25
+ meeting_noter/gui/theme/styles.qss,sha256=zY0bYZALFE5Mu93oKYix1s0IzhQCk9AQbu1ChWe-NSQ,9299
26
+ meeting_noter/gui/utils/__init__.py,sha256=IG3pknZCt9P3vEM-1NbARiwoeLN8vT0B5gLOL2Aecik,338
27
+ meeting_noter/gui/utils/signals.py,sha256=voOqXteRWGbrgnltrz_Q4_hjwtUK71r9n16ojAhQLAU,2362
28
+ meeting_noter/gui/utils/workers.py,sha256=xYdpdgUHJcYJkThWoluT0QKKyfTXKzscyi_u3a91GuE,8283
29
+ meeting_noter/gui/widgets/__init__.py,sha256=lbcVJ23SYzVro0VLucsXaBn_X9L1vviUtuwS-H_0YPA,214
30
+ meeting_noter/gui/widgets/sidebar.py,sha256=-VDnWMk5gSonjHmpSIv4MgcVDmsrmPAbnafLk5AHHK0,7232
31
+ meeting_noter/gui/widgets/status_indicator.py,sha256=4gHhOrX1gmP_00V3dXRCg5EkR_k8r_MP_BHNll25ido,4347
13
32
  meeting_noter/install/__init__.py,sha256=SX5vLFMrV8aBDEGW18jhaqBqJqnRXaeo0Ct7QVGDgvE,38
14
33
  meeting_noter/install/macos.py,sha256=dO-86zbNKRtt0l4D8naVn7kFWjzI8TufWLWE3FRLHQ8,3400
15
34
  meeting_noter/output/__init__.py,sha256=F7xPlOrqweZcPbZtDrhved1stBI59vnWnLYfGwdu6oY,31
@@ -39,8 +58,8 @@ meeting_noter/ui/screens/settings.py,sha256=Zb9-1rkPap1oUNcu4SJcOIemI8DmVC_5uqMd
39
58
  meeting_noter/ui/screens/viewer.py,sha256=5LVhghDcpshlisCrygk9H3uwOh48vmonMd8d4NH77hU,3991
40
59
  meeting_noter/ui/styles/app.tcss,sha256=-uE-1mE3oqKBZW1-pySmOZhK9oxjEQqcG7LDprwxBg4,3396
41
60
  meeting_noter/ui/widgets/__init__.py,sha256=6aVXLDGFXL9PnX5bCnMAqRqOvsv4VuzK_T2UTySQlTg,36
42
- meeting_noter-2.0.0.dist-info/METADATA,sha256=_t77Osf039ZI_VOKMgOsk-OGcBe0SX950SUpsHvVzmY,6970
43
- meeting_noter-2.0.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
44
- meeting_noter-2.0.0.dist-info/entry_points.txt,sha256=osZoOmm-UBPCJ4b6DGH6JOAm7mofM2fK06eK6blplmg,83
45
- meeting_noter-2.0.0.dist-info/top_level.txt,sha256=9Tuq04_0SXM0OXOHVbOHkHkB5tG3fqkrMrfzCMpbLpY,14
46
- meeting_noter-2.0.0.dist-info/RECORD,,
61
+ meeting_noter-3.0.0.dist-info/METADATA,sha256=sROgsYboliy4ehnWYiZ54Jzs-tWePYweba1EoP8Z-90,7036
62
+ meeting_noter-3.0.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
63
+ meeting_noter-3.0.0.dist-info/entry_points.txt,sha256=osZoOmm-UBPCJ4b6DGH6JOAm7mofM2fK06eK6blplmg,83
64
+ meeting_noter-3.0.0.dist-info/top_level.txt,sha256=9Tuq04_0SXM0OXOHVbOHkHkB5tG3fqkrMrfzCMpbLpY,14
65
+ meeting_noter-3.0.0.dist-info/RECORD,,