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
meeting_noter/__init__.py
CHANGED
meeting_noter/cli.py
CHANGED
|
@@ -1051,6 +1051,47 @@ def ui():
|
|
|
1051
1051
|
app.run()
|
|
1052
1052
|
|
|
1053
1053
|
|
|
1054
|
+
@cli.command()
|
|
1055
|
+
@click.option("--foreground", "-f", is_flag=True, help="Run in foreground (block terminal)")
|
|
1056
|
+
def gui(foreground: bool):
|
|
1057
|
+
"""Launch the Desktop GUI.
|
|
1058
|
+
|
|
1059
|
+
Opens the Meeting Noter desktop application with a modern dark theme.
|
|
1060
|
+
Runs in background by default so your terminal stays free.
|
|
1061
|
+
Requires PySide6: pip install meeting-noter[gui]
|
|
1062
|
+
|
|
1063
|
+
\b
|
|
1064
|
+
Examples:
|
|
1065
|
+
meeting-noter gui # Launch GUI in background
|
|
1066
|
+
meeting-noter gui -f # Launch GUI in foreground (blocks terminal)
|
|
1067
|
+
mn gui # Same with short alias
|
|
1068
|
+
"""
|
|
1069
|
+
try:
|
|
1070
|
+
# Check if PySide6 is available
|
|
1071
|
+
import PySide6 # noqa: F401
|
|
1072
|
+
except ImportError:
|
|
1073
|
+
click.echo(click.style("Error: ", fg="red") + "PySide6 not installed.")
|
|
1074
|
+
click.echo("Try reinstalling: pipx install meeting-noter --force")
|
|
1075
|
+
raise SystemExit(1)
|
|
1076
|
+
|
|
1077
|
+
if foreground:
|
|
1078
|
+
# Run in foreground (blocks terminal)
|
|
1079
|
+
from meeting_noter.gui import run_gui
|
|
1080
|
+
run_gui()
|
|
1081
|
+
else:
|
|
1082
|
+
# Run in background
|
|
1083
|
+
import subprocess
|
|
1084
|
+
import sys
|
|
1085
|
+
|
|
1086
|
+
subprocess.Popen(
|
|
1087
|
+
[sys.executable, "-m", "meeting_noter.cli", "gui", "-f"],
|
|
1088
|
+
stdout=subprocess.DEVNULL,
|
|
1089
|
+
stderr=subprocess.DEVNULL,
|
|
1090
|
+
start_new_session=True,
|
|
1091
|
+
)
|
|
1092
|
+
click.echo("Meeting Noter GUI launched.")
|
|
1093
|
+
|
|
1094
|
+
|
|
1054
1095
|
@cli.command()
|
|
1055
1096
|
@click.option("--shell", type=click.Choice(["zsh", "bash", "fish"]), default="zsh")
|
|
1056
1097
|
def completion(shell: str):
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Meeting Noter Desktop GUI - Modern PySide6 interface."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def run_gui():
|
|
7
|
+
"""Launch the GUI application with system tray icon."""
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from PySide6.QtCore import Qt
|
|
12
|
+
from PySide6.QtGui import QIcon
|
|
13
|
+
from PySide6.QtWidgets import QApplication
|
|
14
|
+
|
|
15
|
+
from meeting_noter.gui.main_window import MainWindow
|
|
16
|
+
from meeting_noter.gui.menubar import MenuBarIcon
|
|
17
|
+
|
|
18
|
+
# Enable High DPI scaling
|
|
19
|
+
QApplication.setHighDpiScaleFactorRoundingPolicy(
|
|
20
|
+
Qt.HighDpiScaleFactorRoundingPolicy.PassThrough
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
app = QApplication(sys.argv)
|
|
24
|
+
app.setApplicationName("Meeting Noter")
|
|
25
|
+
app.setOrganizationName("Meeting Noter")
|
|
26
|
+
|
|
27
|
+
# Set application icon (for dock)
|
|
28
|
+
resources_dir = Path(__file__).parent.parent / "resources"
|
|
29
|
+
icon_path = resources_dir / "icon.icns"
|
|
30
|
+
if not icon_path.exists():
|
|
31
|
+
icon_path = resources_dir / "icon.png"
|
|
32
|
+
if icon_path.exists():
|
|
33
|
+
app.setWindowIcon(QIcon(str(icon_path)))
|
|
34
|
+
|
|
35
|
+
# Don't quit when last window closes (we have tray icon)
|
|
36
|
+
app.setQuitOnLastWindowClosed(False)
|
|
37
|
+
|
|
38
|
+
# Create main window
|
|
39
|
+
window = MainWindow()
|
|
40
|
+
|
|
41
|
+
# Create system tray icon
|
|
42
|
+
tray_icon = MenuBarIcon(window)
|
|
43
|
+
tray_icon.show()
|
|
44
|
+
|
|
45
|
+
# Give window reference to tray icon
|
|
46
|
+
window.set_tray_icon(tray_icon)
|
|
47
|
+
|
|
48
|
+
# Show window
|
|
49
|
+
window.show()
|
|
50
|
+
|
|
51
|
+
sys.exit(app.exec())
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
"""Main window for Meeting Noter GUI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import platform
|
|
6
|
+
|
|
7
|
+
from PySide6.QtCore import Qt
|
|
8
|
+
from PySide6.QtWidgets import (
|
|
9
|
+
QApplication,
|
|
10
|
+
QHBoxLayout,
|
|
11
|
+
QMainWindow,
|
|
12
|
+
QStackedWidget,
|
|
13
|
+
QWidget,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
# macOS dock visibility control
|
|
17
|
+
if platform.system() == "Darwin":
|
|
18
|
+
try:
|
|
19
|
+
from AppKit import NSApp, NSApplicationActivationPolicyAccessory, NSApplicationActivationPolicyRegular
|
|
20
|
+
HAS_APPKIT = True
|
|
21
|
+
except ImportError:
|
|
22
|
+
HAS_APPKIT = False
|
|
23
|
+
else:
|
|
24
|
+
HAS_APPKIT = False
|
|
25
|
+
|
|
26
|
+
from meeting_noter.gui.screens import (
|
|
27
|
+
DashboardScreen,
|
|
28
|
+
LogsScreen,
|
|
29
|
+
RecordingsScreen,
|
|
30
|
+
SearchScreen,
|
|
31
|
+
SettingsScreen,
|
|
32
|
+
ViewerScreen,
|
|
33
|
+
)
|
|
34
|
+
from meeting_noter.gui.theme import apply_theme
|
|
35
|
+
from meeting_noter.gui.utils.signals import get_app_state
|
|
36
|
+
from meeting_noter.gui.utils.workers import StatusPollingWorker
|
|
37
|
+
from meeting_noter.gui.widgets import Sidebar
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class MainWindow(QMainWindow):
|
|
41
|
+
"""Main application window with sidebar navigation."""
|
|
42
|
+
|
|
43
|
+
def __init__(self, tray_icon=None):
|
|
44
|
+
super().__init__()
|
|
45
|
+
self.setWindowTitle("Meeting Noter")
|
|
46
|
+
self.setMinimumSize(1000, 700)
|
|
47
|
+
self.resize(1200, 800)
|
|
48
|
+
|
|
49
|
+
self._tray_icon = tray_icon
|
|
50
|
+
self._force_quit = False
|
|
51
|
+
|
|
52
|
+
# Apply dark theme
|
|
53
|
+
apply_theme(self.app())
|
|
54
|
+
|
|
55
|
+
self._setup_ui()
|
|
56
|
+
self._setup_workers()
|
|
57
|
+
self._connect_signals()
|
|
58
|
+
|
|
59
|
+
# Select dashboard initially
|
|
60
|
+
self._show_screen("dashboard")
|
|
61
|
+
|
|
62
|
+
def app(self):
|
|
63
|
+
"""Get the QApplication instance."""
|
|
64
|
+
return QApplication.instance()
|
|
65
|
+
|
|
66
|
+
def set_tray_icon(self, tray_icon):
|
|
67
|
+
"""Set the tray icon reference."""
|
|
68
|
+
self._tray_icon = tray_icon
|
|
69
|
+
|
|
70
|
+
def _setup_ui(self):
|
|
71
|
+
"""Set up the main UI."""
|
|
72
|
+
# Central widget
|
|
73
|
+
central = QWidget()
|
|
74
|
+
self.setCentralWidget(central)
|
|
75
|
+
|
|
76
|
+
# Main layout (horizontal: sidebar + content)
|
|
77
|
+
layout = QHBoxLayout(central)
|
|
78
|
+
layout.setContentsMargins(0, 0, 0, 0)
|
|
79
|
+
layout.setSpacing(0)
|
|
80
|
+
|
|
81
|
+
# Sidebar
|
|
82
|
+
self._sidebar = Sidebar()
|
|
83
|
+
self._sidebar.quit_requested.connect(self._quit_app)
|
|
84
|
+
layout.addWidget(self._sidebar)
|
|
85
|
+
|
|
86
|
+
# Content area (stacked widget)
|
|
87
|
+
self._content = QStackedWidget()
|
|
88
|
+
self._content.setObjectName("content-area")
|
|
89
|
+
layout.addWidget(self._content, 1) # Stretch factor 1
|
|
90
|
+
|
|
91
|
+
# Create screens
|
|
92
|
+
self._screens: dict[str, QWidget] = {}
|
|
93
|
+
self._create_screens()
|
|
94
|
+
|
|
95
|
+
def _create_screens(self):
|
|
96
|
+
"""Create all screen widgets."""
|
|
97
|
+
# Dashboard
|
|
98
|
+
self._screens["dashboard"] = DashboardScreen()
|
|
99
|
+
self._content.addWidget(self._screens["dashboard"])
|
|
100
|
+
|
|
101
|
+
# Recordings
|
|
102
|
+
self._screens["recordings"] = RecordingsScreen()
|
|
103
|
+
self._content.addWidget(self._screens["recordings"])
|
|
104
|
+
|
|
105
|
+
# Search
|
|
106
|
+
self._screens["search"] = SearchScreen()
|
|
107
|
+
self._content.addWidget(self._screens["search"])
|
|
108
|
+
|
|
109
|
+
# Settings
|
|
110
|
+
self._screens["settings"] = SettingsScreen()
|
|
111
|
+
self._content.addWidget(self._screens["settings"])
|
|
112
|
+
|
|
113
|
+
# Logs
|
|
114
|
+
self._screens["logs"] = LogsScreen()
|
|
115
|
+
self._content.addWidget(self._screens["logs"])
|
|
116
|
+
|
|
117
|
+
# Viewer (for transcripts)
|
|
118
|
+
self._screens["viewer"] = ViewerScreen()
|
|
119
|
+
self._content.addWidget(self._screens["viewer"])
|
|
120
|
+
|
|
121
|
+
def _setup_workers(self):
|
|
122
|
+
"""Set up background workers."""
|
|
123
|
+
# Status polling
|
|
124
|
+
self._status_worker = StatusPollingWorker()
|
|
125
|
+
self._status_worker.status_updated.connect(self._on_status_updated)
|
|
126
|
+
self._status_worker.start()
|
|
127
|
+
|
|
128
|
+
def _connect_signals(self):
|
|
129
|
+
"""Connect signals."""
|
|
130
|
+
# Sidebar navigation
|
|
131
|
+
self._sidebar.screen_selected.connect(self._show_screen)
|
|
132
|
+
|
|
133
|
+
# App state signals
|
|
134
|
+
app_state = get_app_state()
|
|
135
|
+
app_state.navigate_to.connect(self._show_screen)
|
|
136
|
+
app_state.open_transcript.connect(self._open_transcript)
|
|
137
|
+
|
|
138
|
+
def _show_screen(self, screen_id: str):
|
|
139
|
+
"""Show a screen by ID."""
|
|
140
|
+
if screen_id in self._screens:
|
|
141
|
+
self._content.setCurrentWidget(self._screens[screen_id])
|
|
142
|
+
self._sidebar.select_screen(screen_id)
|
|
143
|
+
|
|
144
|
+
# Refresh screen if it has a refresh method
|
|
145
|
+
screen = self._screens[screen_id]
|
|
146
|
+
if hasattr(screen, "refresh"):
|
|
147
|
+
screen.refresh()
|
|
148
|
+
|
|
149
|
+
def _open_transcript(self, filepath: str):
|
|
150
|
+
"""Open a transcript in the viewer."""
|
|
151
|
+
viewer = self._screens.get("viewer")
|
|
152
|
+
if viewer and hasattr(viewer, "load_transcript"):
|
|
153
|
+
viewer.load_transcript(filepath)
|
|
154
|
+
self._show_screen("viewer")
|
|
155
|
+
|
|
156
|
+
def _on_status_updated(self, watcher_running: bool, daemon_running: bool, meeting_name: str):
|
|
157
|
+
"""Handle status update from worker."""
|
|
158
|
+
# Update sidebar status
|
|
159
|
+
self._sidebar.update_status(daemon_running, meeting_name)
|
|
160
|
+
|
|
161
|
+
# Update dashboard status indicator
|
|
162
|
+
dashboard = self._screens.get("dashboard")
|
|
163
|
+
if dashboard and hasattr(dashboard, "update_status"):
|
|
164
|
+
dashboard.update_status(watcher_running, daemon_running, meeting_name)
|
|
165
|
+
|
|
166
|
+
# Update tray icon
|
|
167
|
+
if self._tray_icon:
|
|
168
|
+
self._tray_icon.update_status(watcher_running, daemon_running, meeting_name)
|
|
169
|
+
|
|
170
|
+
# Update app state
|
|
171
|
+
app_state = get_app_state()
|
|
172
|
+
app_state._is_recording = daemon_running
|
|
173
|
+
app_state._current_meeting = meeting_name
|
|
174
|
+
app_state._watcher_running = watcher_running
|
|
175
|
+
|
|
176
|
+
def _quit_app(self):
|
|
177
|
+
"""Quit the application completely."""
|
|
178
|
+
self._force_quit = True
|
|
179
|
+
self._cleanup()
|
|
180
|
+
QApplication.instance().quit()
|
|
181
|
+
|
|
182
|
+
def _cleanup(self):
|
|
183
|
+
"""Clean up workers and resources."""
|
|
184
|
+
# Stop workers
|
|
185
|
+
if hasattr(self, "_status_worker"):
|
|
186
|
+
self._status_worker.stop()
|
|
187
|
+
|
|
188
|
+
# Stop screen workers
|
|
189
|
+
for screen in self._screens.values():
|
|
190
|
+
if hasattr(screen, "stop_workers"):
|
|
191
|
+
screen.stop_workers()
|
|
192
|
+
|
|
193
|
+
def closeEvent(self, event):
|
|
194
|
+
"""Handle window close - hide to tray instead of quitting."""
|
|
195
|
+
if self._force_quit or self._tray_icon is None:
|
|
196
|
+
# Actually quit
|
|
197
|
+
self._cleanup()
|
|
198
|
+
event.accept()
|
|
199
|
+
else:
|
|
200
|
+
# Hide to tray instead of closing
|
|
201
|
+
event.ignore()
|
|
202
|
+
self.hide()
|
|
203
|
+
self._hide_from_dock()
|
|
204
|
+
|
|
205
|
+
def show(self):
|
|
206
|
+
"""Show window and add to dock."""
|
|
207
|
+
super().show()
|
|
208
|
+
self._show_in_dock()
|
|
209
|
+
|
|
210
|
+
def _hide_from_dock(self):
|
|
211
|
+
"""Hide app from macOS dock."""
|
|
212
|
+
if HAS_APPKIT:
|
|
213
|
+
NSApp.setActivationPolicy_(NSApplicationActivationPolicyAccessory)
|
|
214
|
+
|
|
215
|
+
def _show_in_dock(self):
|
|
216
|
+
"""Show app in macOS dock."""
|
|
217
|
+
if HAS_APPKIT:
|
|
218
|
+
NSApp.setActivationPolicy_(NSApplicationActivationPolicyRegular)
|
|
219
|
+
NSApp.activateIgnoringOtherApps_(True)
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
"""Menu bar (system tray) integration for Meeting Noter GUI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
from PySide6.QtCore import QTimer
|
|
9
|
+
from PySide6.QtGui import QAction, QIcon, QPixmap, QPainter, QColor, QFont
|
|
10
|
+
from PySide6.QtWidgets import QMenu, QSystemTrayIcon
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from meeting_noter.gui.main_window import MainWindow
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class MenuBarIcon(QSystemTrayIcon):
|
|
17
|
+
"""System tray / menu bar icon for Meeting Noter."""
|
|
18
|
+
|
|
19
|
+
# Status states
|
|
20
|
+
STATUS_STOPPED = "stopped"
|
|
21
|
+
STATUS_READY = "ready"
|
|
22
|
+
STATUS_RECORDING = "recording"
|
|
23
|
+
|
|
24
|
+
def __init__(self, main_window: "MainWindow"):
|
|
25
|
+
super().__init__()
|
|
26
|
+
self._main_window = main_window
|
|
27
|
+
self._current_status = self.STATUS_STOPPED
|
|
28
|
+
self._recording_name = ""
|
|
29
|
+
|
|
30
|
+
self._setup_icons()
|
|
31
|
+
self._setup_menu()
|
|
32
|
+
self._setup_polling()
|
|
33
|
+
|
|
34
|
+
# Set initial icon
|
|
35
|
+
self._update_icon()
|
|
36
|
+
|
|
37
|
+
# Connect signals
|
|
38
|
+
self.activated.connect(self._on_activated)
|
|
39
|
+
|
|
40
|
+
def _setup_icons(self):
|
|
41
|
+
"""Create status icons programmatically."""
|
|
42
|
+
self._icons = {}
|
|
43
|
+
|
|
44
|
+
# Create icons for each status
|
|
45
|
+
for status, color in [
|
|
46
|
+
(self.STATUS_STOPPED, "#5a5a5a"), # Gray
|
|
47
|
+
(self.STATUS_READY, "#4ec9b0"), # Green/Cyan
|
|
48
|
+
(self.STATUS_RECORDING, "#f14c4c"), # Red
|
|
49
|
+
]:
|
|
50
|
+
self._icons[status] = self._create_icon(color)
|
|
51
|
+
|
|
52
|
+
def _create_icon(self, color: str) -> QIcon:
|
|
53
|
+
"""Create a simple colored circle icon."""
|
|
54
|
+
size = 22
|
|
55
|
+
pixmap = QPixmap(size, size)
|
|
56
|
+
pixmap.fill(QColor(0, 0, 0, 0)) # Transparent background
|
|
57
|
+
|
|
58
|
+
painter = QPainter(pixmap)
|
|
59
|
+
painter.setRenderHint(QPainter.Antialiasing)
|
|
60
|
+
|
|
61
|
+
# Draw filled circle
|
|
62
|
+
painter.setBrush(QColor(color))
|
|
63
|
+
painter.setPen(QColor(color))
|
|
64
|
+
margin = 3
|
|
65
|
+
painter.drawEllipse(margin, margin, size - margin * 2, size - margin * 2)
|
|
66
|
+
|
|
67
|
+
painter.end()
|
|
68
|
+
return QIcon(pixmap)
|
|
69
|
+
|
|
70
|
+
def _setup_menu(self):
|
|
71
|
+
"""Set up the context menu."""
|
|
72
|
+
self._menu = QMenu()
|
|
73
|
+
self._menu.setStyleSheet("""
|
|
74
|
+
QMenu {
|
|
75
|
+
background-color: #1a1a1a;
|
|
76
|
+
border: 1px solid #2a2a2a;
|
|
77
|
+
border-radius: 6px;
|
|
78
|
+
padding: 5px;
|
|
79
|
+
}
|
|
80
|
+
QMenu::item {
|
|
81
|
+
color: #d4d4d4;
|
|
82
|
+
padding: 8px 20px;
|
|
83
|
+
border-radius: 4px;
|
|
84
|
+
}
|
|
85
|
+
QMenu::item:selected {
|
|
86
|
+
background-color: #2a2a2a;
|
|
87
|
+
}
|
|
88
|
+
QMenu::separator {
|
|
89
|
+
height: 1px;
|
|
90
|
+
background-color: #2a2a2a;
|
|
91
|
+
margin: 5px 10px;
|
|
92
|
+
}
|
|
93
|
+
""")
|
|
94
|
+
|
|
95
|
+
# Status label (non-clickable)
|
|
96
|
+
self._status_action = QAction("● Stopped")
|
|
97
|
+
self._status_action.setEnabled(False)
|
|
98
|
+
self._menu.addAction(self._status_action)
|
|
99
|
+
|
|
100
|
+
self._menu.addSeparator()
|
|
101
|
+
|
|
102
|
+
# Open GUI
|
|
103
|
+
self._open_action = QAction("Open Meeting Noter")
|
|
104
|
+
self._open_action.triggered.connect(self._show_main_window)
|
|
105
|
+
self._menu.addAction(self._open_action)
|
|
106
|
+
|
|
107
|
+
self._menu.addSeparator()
|
|
108
|
+
|
|
109
|
+
# Quit
|
|
110
|
+
self._quit_action = QAction("Quit Meeting Noter")
|
|
111
|
+
self._quit_action.triggered.connect(self._quit_app)
|
|
112
|
+
self._menu.addAction(self._quit_action)
|
|
113
|
+
|
|
114
|
+
self.setContextMenu(self._menu)
|
|
115
|
+
|
|
116
|
+
def _setup_polling(self):
|
|
117
|
+
"""Set up status polling timer."""
|
|
118
|
+
self._poll_timer = QTimer()
|
|
119
|
+
self._poll_timer.timeout.connect(self._poll_status)
|
|
120
|
+
self._poll_timer.start(2000) # Poll every 2 seconds
|
|
121
|
+
|
|
122
|
+
def _poll_status(self):
|
|
123
|
+
"""Poll for status updates."""
|
|
124
|
+
import os
|
|
125
|
+
from meeting_noter.daemon import is_process_running, read_pid_file
|
|
126
|
+
|
|
127
|
+
DEFAULT_PID_FILE = Path.home() / ".meeting-noter.pid"
|
|
128
|
+
WATCHER_PID_FILE = Path.home() / ".meeting-noter-watcher.pid"
|
|
129
|
+
|
|
130
|
+
# Check watcher
|
|
131
|
+
watcher_running = False
|
|
132
|
+
if WATCHER_PID_FILE.exists():
|
|
133
|
+
try:
|
|
134
|
+
pid = int(WATCHER_PID_FILE.read_text().strip())
|
|
135
|
+
os.kill(pid, 0)
|
|
136
|
+
watcher_running = True
|
|
137
|
+
except (ProcessLookupError, ValueError, FileNotFoundError, PermissionError):
|
|
138
|
+
pass
|
|
139
|
+
|
|
140
|
+
# Check daemon
|
|
141
|
+
daemon_running = False
|
|
142
|
+
daemon_pid = read_pid_file(DEFAULT_PID_FILE)
|
|
143
|
+
if daemon_pid and is_process_running(daemon_pid):
|
|
144
|
+
daemon_running = True
|
|
145
|
+
|
|
146
|
+
# Determine status
|
|
147
|
+
if daemon_running:
|
|
148
|
+
new_status = self.STATUS_RECORDING
|
|
149
|
+
# Get recording name
|
|
150
|
+
recording_name = self._get_recording_name()
|
|
151
|
+
self._recording_name = recording_name or "Recording..."
|
|
152
|
+
elif watcher_running:
|
|
153
|
+
new_status = self.STATUS_READY
|
|
154
|
+
self._recording_name = ""
|
|
155
|
+
else:
|
|
156
|
+
new_status = self.STATUS_STOPPED
|
|
157
|
+
self._recording_name = ""
|
|
158
|
+
|
|
159
|
+
if new_status != self._current_status:
|
|
160
|
+
self._current_status = new_status
|
|
161
|
+
self._update_icon()
|
|
162
|
+
self._update_status_text()
|
|
163
|
+
|
|
164
|
+
def _get_recording_name(self) -> str:
|
|
165
|
+
"""Get the current recording name from logs."""
|
|
166
|
+
log_path = Path.home() / ".meeting-noter.log"
|
|
167
|
+
if not log_path.exists():
|
|
168
|
+
return ""
|
|
169
|
+
|
|
170
|
+
try:
|
|
171
|
+
with open(log_path, "r") as f:
|
|
172
|
+
lines = f.readlines()
|
|
173
|
+
|
|
174
|
+
for line in reversed(lines[-50:]):
|
|
175
|
+
if "Recording started:" in line:
|
|
176
|
+
parts = line.split("Recording started:")
|
|
177
|
+
if len(parts) > 1:
|
|
178
|
+
filename = parts[1].strip().replace(".mp3", "")
|
|
179
|
+
name_parts = filename.split("_", 2)
|
|
180
|
+
if len(name_parts) >= 3:
|
|
181
|
+
return name_parts[2]
|
|
182
|
+
return filename
|
|
183
|
+
elif "Recording saved:" in line or "Recording discarded" in line:
|
|
184
|
+
break
|
|
185
|
+
return ""
|
|
186
|
+
except Exception:
|
|
187
|
+
return ""
|
|
188
|
+
|
|
189
|
+
def _update_icon(self):
|
|
190
|
+
"""Update the tray icon based on status."""
|
|
191
|
+
self.setIcon(self._icons[self._current_status])
|
|
192
|
+
|
|
193
|
+
def _update_status_text(self):
|
|
194
|
+
"""Update the status text in menu."""
|
|
195
|
+
if self._current_status == self.STATUS_RECORDING:
|
|
196
|
+
text = f"● Recording: {self._recording_name}"
|
|
197
|
+
elif self._current_status == self.STATUS_READY:
|
|
198
|
+
text = "● Ready"
|
|
199
|
+
else:
|
|
200
|
+
text = "● Stopped"
|
|
201
|
+
|
|
202
|
+
self._status_action.setText(text)
|
|
203
|
+
|
|
204
|
+
# Update tooltip
|
|
205
|
+
self.setToolTip(f"Meeting Noter - {text}")
|
|
206
|
+
|
|
207
|
+
def _on_activated(self, reason):
|
|
208
|
+
"""Handle tray icon activation."""
|
|
209
|
+
if reason == QSystemTrayIcon.ActivationReason.Trigger:
|
|
210
|
+
# Single click - toggle window visibility
|
|
211
|
+
self._toggle_main_window()
|
|
212
|
+
elif reason == QSystemTrayIcon.ActivationReason.DoubleClick:
|
|
213
|
+
# Double click - show window
|
|
214
|
+
self._show_main_window()
|
|
215
|
+
|
|
216
|
+
def _toggle_main_window(self):
|
|
217
|
+
"""Toggle main window visibility."""
|
|
218
|
+
if self._main_window.isVisible():
|
|
219
|
+
self._main_window.hide()
|
|
220
|
+
else:
|
|
221
|
+
self._show_main_window()
|
|
222
|
+
|
|
223
|
+
def _show_main_window(self):
|
|
224
|
+
"""Show and raise the main window."""
|
|
225
|
+
self._main_window.show() # This also shows in dock
|
|
226
|
+
self._main_window.raise_()
|
|
227
|
+
self._main_window.activateWindow()
|
|
228
|
+
|
|
229
|
+
def _quit_app(self):
|
|
230
|
+
"""Quit the application completely."""
|
|
231
|
+
self._poll_timer.stop()
|
|
232
|
+
from PySide6.QtWidgets import QApplication
|
|
233
|
+
QApplication.instance().quit()
|
|
234
|
+
|
|
235
|
+
def update_status(self, watcher_running: bool, daemon_running: bool, meeting_name: str):
|
|
236
|
+
"""Update status from external source."""
|
|
237
|
+
if daemon_running:
|
|
238
|
+
self._current_status = self.STATUS_RECORDING
|
|
239
|
+
self._recording_name = meeting_name or "Recording..."
|
|
240
|
+
elif watcher_running:
|
|
241
|
+
self._current_status = self.STATUS_READY
|
|
242
|
+
self._recording_name = ""
|
|
243
|
+
else:
|
|
244
|
+
self._current_status = self.STATUS_STOPPED
|
|
245
|
+
self._recording_name = ""
|
|
246
|
+
|
|
247
|
+
self._update_icon()
|
|
248
|
+
self._update_status_text()
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Screen modules for Meeting Noter GUI."""
|
|
2
|
+
|
|
3
|
+
from meeting_noter.gui.screens.dashboard import DashboardScreen
|
|
4
|
+
from meeting_noter.gui.screens.logs import LogsScreen
|
|
5
|
+
from meeting_noter.gui.screens.recordings import RecordingsScreen
|
|
6
|
+
from meeting_noter.gui.screens.search import SearchScreen
|
|
7
|
+
from meeting_noter.gui.screens.settings import SettingsScreen
|
|
8
|
+
from meeting_noter.gui.screens.viewer import ViewerScreen
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"DashboardScreen",
|
|
12
|
+
"RecordingsScreen",
|
|
13
|
+
"SearchScreen",
|
|
14
|
+
"ViewerScreen",
|
|
15
|
+
"SettingsScreen",
|
|
16
|
+
"LogsScreen",
|
|
17
|
+
]
|