audex 1.0.7a3__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.
- audex/__init__.py +9 -0
- audex/__main__.py +7 -0
- audex/cli/__init__.py +189 -0
- audex/cli/apis/__init__.py +12 -0
- audex/cli/apis/init/__init__.py +34 -0
- audex/cli/apis/init/gencfg.py +130 -0
- audex/cli/apis/init/setup.py +330 -0
- audex/cli/apis/init/vprgroup.py +125 -0
- audex/cli/apis/serve.py +141 -0
- audex/cli/args.py +356 -0
- audex/cli/exceptions.py +44 -0
- audex/cli/helper/__init__.py +0 -0
- audex/cli/helper/ansi.py +193 -0
- audex/cli/helper/display.py +288 -0
- audex/config/__init__.py +64 -0
- audex/config/core/__init__.py +30 -0
- audex/config/core/app.py +29 -0
- audex/config/core/audio.py +45 -0
- audex/config/core/logging.py +163 -0
- audex/config/core/session.py +11 -0
- audex/config/helper/__init__.py +1 -0
- audex/config/helper/client/__init__.py +1 -0
- audex/config/helper/client/http.py +28 -0
- audex/config/helper/client/websocket.py +21 -0
- audex/config/helper/provider/__init__.py +1 -0
- audex/config/helper/provider/dashscope.py +13 -0
- audex/config/helper/provider/unisound.py +18 -0
- audex/config/helper/provider/xfyun.py +23 -0
- audex/config/infrastructure/__init__.py +31 -0
- audex/config/infrastructure/cache.py +51 -0
- audex/config/infrastructure/database.py +48 -0
- audex/config/infrastructure/recorder.py +32 -0
- audex/config/infrastructure/store.py +19 -0
- audex/config/provider/__init__.py +18 -0
- audex/config/provider/transcription.py +109 -0
- audex/config/provider/vpr.py +99 -0
- audex/container.py +40 -0
- audex/entity/__init__.py +468 -0
- audex/entity/doctor.py +109 -0
- audex/entity/doctor.pyi +51 -0
- audex/entity/fields.py +401 -0
- audex/entity/segment.py +115 -0
- audex/entity/segment.pyi +38 -0
- audex/entity/session.py +133 -0
- audex/entity/session.pyi +47 -0
- audex/entity/utterance.py +142 -0
- audex/entity/utterance.pyi +48 -0
- audex/entity/vp.py +68 -0
- audex/entity/vp.pyi +35 -0
- audex/exceptions.py +157 -0
- audex/filters/__init__.py +692 -0
- audex/filters/generated/__init__.py +21 -0
- audex/filters/generated/doctor.py +987 -0
- audex/filters/generated/segment.py +723 -0
- audex/filters/generated/session.py +978 -0
- audex/filters/generated/utterance.py +939 -0
- audex/filters/generated/vp.py +815 -0
- audex/helper/__init__.py +1 -0
- audex/helper/hash.py +33 -0
- audex/helper/mixin.py +65 -0
- audex/helper/net.py +19 -0
- audex/helper/settings/__init__.py +830 -0
- audex/helper/settings/fields.py +317 -0
- audex/helper/stream.py +153 -0
- audex/injectors/__init__.py +1 -0
- audex/injectors/config.py +12 -0
- audex/injectors/lifespan.py +7 -0
- audex/lib/__init__.py +1 -0
- audex/lib/cache/__init__.py +383 -0
- audex/lib/cache/inmemory.py +513 -0
- audex/lib/database/__init__.py +83 -0
- audex/lib/database/sqlite.py +406 -0
- audex/lib/exporter.py +189 -0
- audex/lib/injectors/__init__.py +1 -0
- audex/lib/injectors/cache.py +25 -0
- audex/lib/injectors/container.py +47 -0
- audex/lib/injectors/exporter.py +26 -0
- audex/lib/injectors/recorder.py +33 -0
- audex/lib/injectors/server.py +17 -0
- audex/lib/injectors/session.py +18 -0
- audex/lib/injectors/sqlite.py +24 -0
- audex/lib/injectors/store.py +13 -0
- audex/lib/injectors/transcription.py +42 -0
- audex/lib/injectors/usb.py +12 -0
- audex/lib/injectors/vpr.py +65 -0
- audex/lib/injectors/wifi.py +7 -0
- audex/lib/recorder.py +844 -0
- audex/lib/repos/__init__.py +149 -0
- audex/lib/repos/container.py +23 -0
- audex/lib/repos/database/__init__.py +1 -0
- audex/lib/repos/database/sqlite.py +672 -0
- audex/lib/repos/decorators.py +74 -0
- audex/lib/repos/doctor.py +286 -0
- audex/lib/repos/segment.py +302 -0
- audex/lib/repos/session.py +285 -0
- audex/lib/repos/tables/__init__.py +70 -0
- audex/lib/repos/tables/doctor.py +137 -0
- audex/lib/repos/tables/segment.py +113 -0
- audex/lib/repos/tables/session.py +140 -0
- audex/lib/repos/tables/utterance.py +131 -0
- audex/lib/repos/tables/vp.py +102 -0
- audex/lib/repos/utterance.py +288 -0
- audex/lib/repos/vp.py +286 -0
- audex/lib/restful.py +251 -0
- audex/lib/server/__init__.py +97 -0
- audex/lib/server/auth.py +98 -0
- audex/lib/server/handlers.py +248 -0
- audex/lib/server/templates/index.html.j2 +226 -0
- audex/lib/server/templates/login.html.j2 +111 -0
- audex/lib/server/templates/static/script.js +68 -0
- audex/lib/server/templates/static/style.css +579 -0
- audex/lib/server/types.py +123 -0
- audex/lib/session.py +503 -0
- audex/lib/store/__init__.py +238 -0
- audex/lib/store/localfile.py +411 -0
- audex/lib/transcription/__init__.py +33 -0
- audex/lib/transcription/dashscope.py +525 -0
- audex/lib/transcription/events.py +62 -0
- audex/lib/usb.py +554 -0
- audex/lib/vpr/__init__.py +38 -0
- audex/lib/vpr/unisound/__init__.py +185 -0
- audex/lib/vpr/unisound/types.py +469 -0
- audex/lib/vpr/xfyun/__init__.py +483 -0
- audex/lib/vpr/xfyun/types.py +679 -0
- audex/lib/websocket/__init__.py +8 -0
- audex/lib/websocket/connection.py +485 -0
- audex/lib/websocket/pool.py +991 -0
- audex/lib/wifi.py +1146 -0
- audex/lifespan.py +75 -0
- audex/service/__init__.py +27 -0
- audex/service/decorators.py +73 -0
- audex/service/doctor/__init__.py +652 -0
- audex/service/doctor/const.py +36 -0
- audex/service/doctor/exceptions.py +96 -0
- audex/service/doctor/types.py +54 -0
- audex/service/export/__init__.py +236 -0
- audex/service/export/const.py +17 -0
- audex/service/export/exceptions.py +34 -0
- audex/service/export/types.py +21 -0
- audex/service/injectors/__init__.py +1 -0
- audex/service/injectors/container.py +53 -0
- audex/service/injectors/doctor.py +34 -0
- audex/service/injectors/export.py +27 -0
- audex/service/injectors/session.py +49 -0
- audex/service/session/__init__.py +754 -0
- audex/service/session/const.py +34 -0
- audex/service/session/exceptions.py +67 -0
- audex/service/session/types.py +91 -0
- audex/types.py +39 -0
- audex/utils.py +287 -0
- audex/valueobj/__init__.py +81 -0
- audex/valueobj/common/__init__.py +1 -0
- audex/valueobj/common/auth.py +84 -0
- audex/valueobj/common/email.py +16 -0
- audex/valueobj/common/ops.py +22 -0
- audex/valueobj/common/phone.py +84 -0
- audex/valueobj/common/version.py +72 -0
- audex/valueobj/session.py +19 -0
- audex/valueobj/utterance.py +15 -0
- audex/view/__init__.py +51 -0
- audex/view/container.py +17 -0
- audex/view/decorators.py +303 -0
- audex/view/pages/__init__.py +1 -0
- audex/view/pages/dashboard/__init__.py +286 -0
- audex/view/pages/dashboard/wifi.py +407 -0
- audex/view/pages/login.py +110 -0
- audex/view/pages/recording.py +348 -0
- audex/view/pages/register.py +202 -0
- audex/view/pages/sessions/__init__.py +196 -0
- audex/view/pages/sessions/details.py +224 -0
- audex/view/pages/sessions/export.py +443 -0
- audex/view/pages/settings.py +374 -0
- audex/view/pages/voiceprint/__init__.py +1 -0
- audex/view/pages/voiceprint/enroll.py +195 -0
- audex/view/pages/voiceprint/update.py +195 -0
- audex/view/static/css/dashboard.css +452 -0
- audex/view/static/css/glass.css +22 -0
- audex/view/static/css/global.css +541 -0
- audex/view/static/css/login.css +386 -0
- audex/view/static/css/recording.css +439 -0
- audex/view/static/css/register.css +293 -0
- audex/view/static/css/sessions/styles.css +501 -0
- audex/view/static/css/settings.css +186 -0
- audex/view/static/css/voiceprint/enroll.css +43 -0
- audex/view/static/css/voiceprint/styles.css +209 -0
- audex/view/static/css/voiceprint/update.css +44 -0
- audex/view/static/images/logo.svg +95 -0
- audex/view/static/js/recording.js +42 -0
- audex-1.0.7a3.dist-info/METADATA +361 -0
- audex-1.0.7a3.dist-info/RECORD +192 -0
- audex-1.0.7a3.dist-info/WHEEL +4 -0
- audex-1.0.7a3.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
|
|
5
|
+
from nicegui import ui
|
|
6
|
+
|
|
7
|
+
from audex.lib.wifi import WiFiManager
|
|
8
|
+
from audex.lib.wifi import WiFiNetwork
|
|
9
|
+
from audex.lib.wifi import WiFiSecurity
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _get_wifi_icon_and_color(signal_quality: int, is_connected: bool) -> tuple[str, str]:
|
|
13
|
+
"""Get WiFi icon and color based on signal quality."""
|
|
14
|
+
if is_connected:
|
|
15
|
+
if signal_quality >= 75:
|
|
16
|
+
return "signal_wifi_4_bar", "text-positive"
|
|
17
|
+
if signal_quality >= 50:
|
|
18
|
+
return "network_wifi_3_bar", "text-positive"
|
|
19
|
+
if signal_quality >= 25:
|
|
20
|
+
return "network_wifi_2_bar", "text-warning"
|
|
21
|
+
return "network_wifi_1_bar", "text-warning"
|
|
22
|
+
return "signal_wifi_off", "text-grey-6"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _get_security_label(security: WiFiSecurity) -> str:
|
|
26
|
+
"""Get human-readable security label."""
|
|
27
|
+
security_map = {
|
|
28
|
+
WiFiSecurity.OPEN: "开放",
|
|
29
|
+
WiFiSecurity.WEP: "WEP",
|
|
30
|
+
WiFiSecurity.WPA: "WPA",
|
|
31
|
+
WiFiSecurity.WPA2: "WPA2",
|
|
32
|
+
WiFiSecurity.WPA3: "WPA3",
|
|
33
|
+
WiFiSecurity.UNKNOWN: "未知",
|
|
34
|
+
}
|
|
35
|
+
return security_map.get(security, "未知")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class WiFiIndicator:
|
|
39
|
+
"""WiFi status indicator component."""
|
|
40
|
+
|
|
41
|
+
def __init__(self, wifi_manager: WiFiManager) -> None:
|
|
42
|
+
self.wifi_manager = wifi_manager
|
|
43
|
+
self.icon_display: ui.button | None = None
|
|
44
|
+
|
|
45
|
+
# Dialog state
|
|
46
|
+
self.dialog: ui.dialog | None = None
|
|
47
|
+
self.current_conn_container: ui.column | None = None
|
|
48
|
+
self.networks_container: ui.column | None = None
|
|
49
|
+
self.scanning = False
|
|
50
|
+
self.disconnecting = False
|
|
51
|
+
|
|
52
|
+
# Track expanded state
|
|
53
|
+
self.expanded_ssids: set[str] = set()
|
|
54
|
+
|
|
55
|
+
def render(self) -> ui.button:
|
|
56
|
+
"""Render WiFi indicator as a simple icon button."""
|
|
57
|
+
self.icon_display = (
|
|
58
|
+
ui.button(icon="signal_wifi_off", on_click=self._show_dialog)
|
|
59
|
+
.props("flat round")
|
|
60
|
+
.classes("wifi-indicator-btn text-grey-6")
|
|
61
|
+
.tooltip("WiFi 设置")
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
# Start status polling
|
|
65
|
+
asyncio.create_task(self._initial_update()) # noqa
|
|
66
|
+
ui.timer(5.0, self._update_status)
|
|
67
|
+
|
|
68
|
+
return self.icon_display
|
|
69
|
+
|
|
70
|
+
async def _initial_update(self) -> None:
|
|
71
|
+
"""Initial status update."""
|
|
72
|
+
await asyncio.sleep(0.1)
|
|
73
|
+
await self._update_status()
|
|
74
|
+
|
|
75
|
+
async def _update_status(self) -> None:
|
|
76
|
+
"""Update WiFi status display."""
|
|
77
|
+
if not self.icon_display:
|
|
78
|
+
return
|
|
79
|
+
|
|
80
|
+
try:
|
|
81
|
+
is_available = getattr(self.wifi_manager, "is_available", False)
|
|
82
|
+
if not is_available:
|
|
83
|
+
self.icon_display.props('icon="signal_wifi_off"')
|
|
84
|
+
self.icon_display.classes(
|
|
85
|
+
"text-grey-4", remove="text-positive text-warning text-grey-6"
|
|
86
|
+
)
|
|
87
|
+
return
|
|
88
|
+
|
|
89
|
+
conn_info = await self.wifi_manager.get_connection_info()
|
|
90
|
+
if conn_info:
|
|
91
|
+
icon, color = _get_wifi_icon_and_color(conn_info.signal_quality, True)
|
|
92
|
+
self.icon_display.props(f'icon="{icon}"')
|
|
93
|
+
self.icon_display.classes(
|
|
94
|
+
color, remove="text-positive text-warning text-grey-6 text-grey-4"
|
|
95
|
+
)
|
|
96
|
+
else:
|
|
97
|
+
self.icon_display.props('icon="signal_wifi_off"')
|
|
98
|
+
self.icon_display.classes(
|
|
99
|
+
"text-grey-6", remove="text-positive text-warning text-grey-4"
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
except Exception as e:
|
|
103
|
+
print(f"[WiFi] Update error: {e}")
|
|
104
|
+
if self.icon_display:
|
|
105
|
+
self.icon_display.props('icon="signal_wifi_off"')
|
|
106
|
+
self.icon_display.classes(
|
|
107
|
+
"text-grey-4", remove="text-positive text-warning text-grey-6"
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
def _show_dialog(self) -> None:
|
|
111
|
+
"""Show WiFi management dialog."""
|
|
112
|
+
with (
|
|
113
|
+
ui.dialog() as dialog,
|
|
114
|
+
ui.card()
|
|
115
|
+
.classes("wifi-dialog-card")
|
|
116
|
+
.style("width: 540px; max-width: 90vw; padding: 28px;"),
|
|
117
|
+
):
|
|
118
|
+
self.dialog = dialog
|
|
119
|
+
|
|
120
|
+
# Header
|
|
121
|
+
with ui.row().classes("items-center justify-between w-full mb-4"):
|
|
122
|
+
with ui.row().classes("items-center gap-2"):
|
|
123
|
+
ui.icon("wifi", size="md").classes("text-primary")
|
|
124
|
+
ui.label("WiFi 设置").classes("text-h6 font-bold text-grey-9")
|
|
125
|
+
ui.button(icon="close", on_click=dialog.close).props("flat round dense").classes(
|
|
126
|
+
"press-button"
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
# Loading overlay
|
|
130
|
+
loading_container = ui.column().classes("w-full items-center gap-3 py-8")
|
|
131
|
+
with loading_container:
|
|
132
|
+
ui.spinner(size="lg").classes("text-primary")
|
|
133
|
+
ui.label("正在扫描网络...").classes("text-body2 text-grey-7")
|
|
134
|
+
|
|
135
|
+
# Current connection - fixed at top
|
|
136
|
+
self.current_conn_container = ui.column().classes("w-full").style("display: none;")
|
|
137
|
+
|
|
138
|
+
# Scan button
|
|
139
|
+
with (
|
|
140
|
+
ui.row()
|
|
141
|
+
.classes("items-center justify-between w-full mb-3")
|
|
142
|
+
.style("margin-top: 16px;")
|
|
143
|
+
):
|
|
144
|
+
ui.label("可用网络").classes("text-subtitle2 font-semibold text-grey-8")
|
|
145
|
+
|
|
146
|
+
async def do_rescan():
|
|
147
|
+
if self.scanning:
|
|
148
|
+
return
|
|
149
|
+
self.scanning = True
|
|
150
|
+
scan_btn.props("loading")
|
|
151
|
+
# Clear expanded state on rescan
|
|
152
|
+
self.expanded_ssids.clear()
|
|
153
|
+
await self._scan_networks()
|
|
154
|
+
scan_btn.props(remove="loading")
|
|
155
|
+
self.scanning = False
|
|
156
|
+
|
|
157
|
+
scan_btn = (
|
|
158
|
+
ui.button(icon="refresh", on_click=do_rescan)
|
|
159
|
+
.props("flat round dense")
|
|
160
|
+
.classes("wifi-scan-btn")
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
# Networks container - scrollable
|
|
164
|
+
self.networks_container = (
|
|
165
|
+
ui.column()
|
|
166
|
+
.classes("w-full gap-2")
|
|
167
|
+
.style("max-height: 380px; overflow-y: auto; display: none; scrollbar-with: none;")
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
# Scan in background
|
|
171
|
+
async def load_networks():
|
|
172
|
+
await self._scan_networks()
|
|
173
|
+
loading_container.style("display: none;")
|
|
174
|
+
if self.current_conn_container:
|
|
175
|
+
self.current_conn_container.style("display: flex;")
|
|
176
|
+
if self.networks_container:
|
|
177
|
+
self.networks_container.style("display: flex;")
|
|
178
|
+
|
|
179
|
+
asyncio.create_task(load_networks()) # noqa
|
|
180
|
+
|
|
181
|
+
dialog.open()
|
|
182
|
+
|
|
183
|
+
async def _scan_networks(self) -> None:
|
|
184
|
+
"""Scan and display networks."""
|
|
185
|
+
if not self.current_conn_container or not self.networks_container:
|
|
186
|
+
return
|
|
187
|
+
|
|
188
|
+
self.current_conn_container.clear()
|
|
189
|
+
self.networks_container.clear()
|
|
190
|
+
|
|
191
|
+
try:
|
|
192
|
+
conn_info = await self.wifi_manager.get_connection_info()
|
|
193
|
+
current_ssid = conn_info.ssid if conn_info else None
|
|
194
|
+
|
|
195
|
+
# Current connection card - fixed at top
|
|
196
|
+
if conn_info:
|
|
197
|
+
with (
|
|
198
|
+
self.current_conn_container,
|
|
199
|
+
ui.card().classes("w-full wifi-current-card"),
|
|
200
|
+
ui.row().classes("items-center justify-between w-full"),
|
|
201
|
+
):
|
|
202
|
+
with ui.row().classes("items-center gap-3"):
|
|
203
|
+
icon, color = _get_wifi_icon_and_color(conn_info.signal_quality, True)
|
|
204
|
+
ui.icon(icon, size="md").classes(color)
|
|
205
|
+
with ui.column().classes("gap-0"):
|
|
206
|
+
ui.label(conn_info.ssid).classes("text-body1 font-semibold text-grey-9")
|
|
207
|
+
status_parts = [f"已连接 · {conn_info.signal_quality}%"]
|
|
208
|
+
if conn_info.ip_address:
|
|
209
|
+
status_parts.append(conn_info.ip_address)
|
|
210
|
+
ui.label(" · ".join(status_parts)).classes("text-xs text-grey-7")
|
|
211
|
+
|
|
212
|
+
async def do_disconnect():
|
|
213
|
+
if self.disconnecting:
|
|
214
|
+
return
|
|
215
|
+
self.disconnecting = True
|
|
216
|
+
disconnect_btn.props("loading")
|
|
217
|
+
|
|
218
|
+
success = await self.wifi_manager.disconnect()
|
|
219
|
+
|
|
220
|
+
disconnect_btn.props(remove="loading")
|
|
221
|
+
self.disconnecting = False
|
|
222
|
+
|
|
223
|
+
if success:
|
|
224
|
+
ui.notify("已断开连接", type="positive")
|
|
225
|
+
await self._update_status()
|
|
226
|
+
await self._scan_networks()
|
|
227
|
+
else:
|
|
228
|
+
ui.notify("断开失败", type="negative")
|
|
229
|
+
|
|
230
|
+
disconnect_btn = (
|
|
231
|
+
ui.button(icon="link_off", on_click=do_disconnect)
|
|
232
|
+
.props("flat")
|
|
233
|
+
.classes("wifi-disconnect-btn")
|
|
234
|
+
.tooltip("断开连接")
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
# Scan networks
|
|
238
|
+
networks = await self.wifi_manager.scan()
|
|
239
|
+
|
|
240
|
+
if not networks:
|
|
241
|
+
with self.networks_container, ui.column().classes("items-center gap-3 py-8"):
|
|
242
|
+
ui.icon("wifi_off", size="xl").classes("text-grey-4")
|
|
243
|
+
ui.label("未找到网络").classes("text-body2 text-grey-6")
|
|
244
|
+
return
|
|
245
|
+
|
|
246
|
+
# Filter out current network from list
|
|
247
|
+
available_networks = [n for n in networks if n.ssid != current_ssid]
|
|
248
|
+
|
|
249
|
+
# Display available networks
|
|
250
|
+
with self.networks_container:
|
|
251
|
+
for network in available_networks:
|
|
252
|
+
self._render_network_card(network)
|
|
253
|
+
|
|
254
|
+
except Exception as e:
|
|
255
|
+
if self.networks_container:
|
|
256
|
+
self.networks_container.clear()
|
|
257
|
+
with self.networks_container, ui.column().classes("items-center gap-3 py-8"):
|
|
258
|
+
ui.icon("error_outline", size="xl").classes("text-negative")
|
|
259
|
+
ui.label(f"扫描失败: {e!s}").classes("text-body2 text-negative")
|
|
260
|
+
|
|
261
|
+
def _render_network_card(self, network: WiFiNetwork) -> None:
|
|
262
|
+
"""Render a single network card with expandable connect form."""
|
|
263
|
+
is_expanded = network.ssid in self.expanded_ssids
|
|
264
|
+
|
|
265
|
+
card_container = ui.column().classes("w-full").style("gap: 8px;")
|
|
266
|
+
|
|
267
|
+
with card_container:
|
|
268
|
+
# Store form container reference
|
|
269
|
+
form_container = None
|
|
270
|
+
expand_icon = None
|
|
271
|
+
|
|
272
|
+
# Toggle function - only one expanded at a time
|
|
273
|
+
def make_toggle_handler(ssid: str):
|
|
274
|
+
def toggle():
|
|
275
|
+
# If clicking already expanded, just collapse it
|
|
276
|
+
if ssid in self.expanded_ssids:
|
|
277
|
+
self.expanded_ssids.remove(ssid)
|
|
278
|
+
if form_container:
|
|
279
|
+
# Collapse animation
|
|
280
|
+
form_container.style(
|
|
281
|
+
"display: flex; max-height: 0; opacity: 0; padding-top: 0; padding-bottom: 0;"
|
|
282
|
+
)
|
|
283
|
+
ui.timer(0.4, lambda: form_container.style("display: none;"), once=True)
|
|
284
|
+
if expand_icon:
|
|
285
|
+
expand_icon.props('name="expand_more"')
|
|
286
|
+
else:
|
|
287
|
+
# Collapse all other cards first
|
|
288
|
+
self.expanded_ssids.clear()
|
|
289
|
+
|
|
290
|
+
# Re-render to collapse others (we need to track all form containers)
|
|
291
|
+
# For now, just clear and add this one
|
|
292
|
+
self.expanded_ssids.add(ssid)
|
|
293
|
+
|
|
294
|
+
if form_container:
|
|
295
|
+
# Expand animation
|
|
296
|
+
form_container.style("display: flex; max-height: 0; opacity: 0;")
|
|
297
|
+
ui.timer(
|
|
298
|
+
0.01,
|
|
299
|
+
lambda: form_container.style(
|
|
300
|
+
"display: flex; max-height: 80px; opacity: 1; padding-top: 12px; padding-bottom: 12px;"
|
|
301
|
+
),
|
|
302
|
+
once=True,
|
|
303
|
+
)
|
|
304
|
+
if expand_icon:
|
|
305
|
+
expand_icon.props('name="expand_less"')
|
|
306
|
+
|
|
307
|
+
return toggle
|
|
308
|
+
|
|
309
|
+
# Network info card
|
|
310
|
+
with (
|
|
311
|
+
(
|
|
312
|
+
ui.card()
|
|
313
|
+
.classes("w-full wifi-network-card")
|
|
314
|
+
.on("click", make_toggle_handler(network.ssid))
|
|
315
|
+
),
|
|
316
|
+
ui.row().classes("items-center justify-between w-full"),
|
|
317
|
+
):
|
|
318
|
+
with ui.row().classes("items-center gap-3 flex-1"):
|
|
319
|
+
icon, color = _get_wifi_icon_and_color(
|
|
320
|
+
network.signal_quality, network.is_connected
|
|
321
|
+
)
|
|
322
|
+
ui.icon(icon, size="md").classes(color)
|
|
323
|
+
|
|
324
|
+
with ui.column().classes("gap-0 flex-1"):
|
|
325
|
+
ui.label(network.ssid).classes("text-body1 font-semibold text-grey-9")
|
|
326
|
+
|
|
327
|
+
info_parts = [
|
|
328
|
+
f"{network.signal_quality}%",
|
|
329
|
+
_get_security_label(network.security),
|
|
330
|
+
]
|
|
331
|
+
if network.channel:
|
|
332
|
+
info_parts.append(f"信道 {network.channel}")
|
|
333
|
+
ui.label(" · ".join(info_parts)).classes("text-xs text-grey-7")
|
|
334
|
+
|
|
335
|
+
with ui.row().classes("items-center gap-2"):
|
|
336
|
+
if network.security != WiFiSecurity.OPEN:
|
|
337
|
+
ui.icon("lock", size="sm").classes("text-grey-6")
|
|
338
|
+
expand_icon = ui.icon(
|
|
339
|
+
"expand_less" if is_expanded else "expand_more", size="sm"
|
|
340
|
+
).classes("text-grey-6")
|
|
341
|
+
|
|
342
|
+
# Expandable connect form
|
|
343
|
+
initial_style = (
|
|
344
|
+
"display: flex; max-height: 80px; opacity: 1; padding: 12px;"
|
|
345
|
+
if is_expanded
|
|
346
|
+
else "display: none; max-height: 0; opacity: 0; padding: 0;"
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
form_container = (
|
|
350
|
+
ui.column().classes("w-full wifi-connect-form").style(f"{initial_style} gap: 0;")
|
|
351
|
+
)
|
|
352
|
+
# Row layout: password input + connect button
|
|
353
|
+
with form_container, ui.row().classes("items-center w-full").style("gap: 12px;"):
|
|
354
|
+
if network.security != WiFiSecurity.OPEN:
|
|
355
|
+
password_input = (
|
|
356
|
+
ui.input("密码", password=True, password_toggle_button=True)
|
|
357
|
+
.classes("flex-1 clean-input")
|
|
358
|
+
.props("standout dense outlined")
|
|
359
|
+
.style("margin: 0;")
|
|
360
|
+
)
|
|
361
|
+
else:
|
|
362
|
+
password_input = None
|
|
363
|
+
ui.label("此网络无需密码").classes("text-body2 text-grey-6 flex-1")
|
|
364
|
+
|
|
365
|
+
def make_connect_handler(net: WiFiNetwork, pwd_input):
|
|
366
|
+
async def do_connect():
|
|
367
|
+
# Get password if needed
|
|
368
|
+
password = None
|
|
369
|
+
if pwd_input:
|
|
370
|
+
password = pwd_input.value.strip()
|
|
371
|
+
if not password:
|
|
372
|
+
ui.notify("请输入密码", type="warning")
|
|
373
|
+
return
|
|
374
|
+
|
|
375
|
+
connect_btn.props("loading")
|
|
376
|
+
|
|
377
|
+
# Disconnect current connection first
|
|
378
|
+
conn_info = await self.wifi_manager.get_connection_info()
|
|
379
|
+
if conn_info:
|
|
380
|
+
await self.wifi_manager.disconnect()
|
|
381
|
+
await asyncio.sleep(1)
|
|
382
|
+
|
|
383
|
+
# Connect to new network
|
|
384
|
+
success = await self.wifi_manager.connect(net.ssid, password)
|
|
385
|
+
|
|
386
|
+
connect_btn.props(remove="loading")
|
|
387
|
+
|
|
388
|
+
if success:
|
|
389
|
+
ui.notify(f"已连接到 {net.ssid}", type="positive")
|
|
390
|
+
await self._update_status()
|
|
391
|
+
if self.dialog:
|
|
392
|
+
self.dialog.close()
|
|
393
|
+
else:
|
|
394
|
+
ui.notify("连接失败,请检查密码", type="negative")
|
|
395
|
+
|
|
396
|
+
return do_connect
|
|
397
|
+
|
|
398
|
+
# Connect button with arrow_forward icon
|
|
399
|
+
connect_btn = (
|
|
400
|
+
ui.button(
|
|
401
|
+
icon="arrow_forward",
|
|
402
|
+
on_click=make_connect_handler(network, password_input),
|
|
403
|
+
)
|
|
404
|
+
.props("flat")
|
|
405
|
+
.classes("wifi-connect-btn")
|
|
406
|
+
.tooltip("连接到此网络")
|
|
407
|
+
)
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dependency_injector.wiring import Provide
|
|
4
|
+
from dependency_injector.wiring import inject
|
|
5
|
+
from fastapi import Depends
|
|
6
|
+
from nicegui import ui
|
|
7
|
+
|
|
8
|
+
from audex.config import Config
|
|
9
|
+
from audex.container import Container
|
|
10
|
+
from audex.exceptions import PermissionDeniedError
|
|
11
|
+
from audex.service.doctor import DoctorService
|
|
12
|
+
from audex.service.doctor.exceptions import InvalidCredentialsError
|
|
13
|
+
from audex.service.doctor.types import LoginCommand
|
|
14
|
+
from audex.valueobj.common.auth import Password
|
|
15
|
+
from audex.view.decorators import handle_errors
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@ui.page("/login")
|
|
19
|
+
@handle_errors
|
|
20
|
+
@inject
|
|
21
|
+
async def render(
|
|
22
|
+
doctor_service: DoctorService = Depends(Provide[Container.service.doctor]),
|
|
23
|
+
config: Config = Depends(Provide[Container.config]),
|
|
24
|
+
) -> None:
|
|
25
|
+
"""Render login page with clean design."""
|
|
26
|
+
|
|
27
|
+
# Check if already logged in
|
|
28
|
+
try:
|
|
29
|
+
await doctor_service.current_doctor()
|
|
30
|
+
ui.navigate.to("/")
|
|
31
|
+
return
|
|
32
|
+
except PermissionDeniedError:
|
|
33
|
+
pass # Not logged in, continue
|
|
34
|
+
|
|
35
|
+
# Add consistent CSS
|
|
36
|
+
ui.add_head_html('<link rel="stylesheet" href="/static/css/login.css">')
|
|
37
|
+
|
|
38
|
+
# Full screen container
|
|
39
|
+
with (
|
|
40
|
+
(
|
|
41
|
+
ui.element("div")
|
|
42
|
+
.classes("w-full bg-white")
|
|
43
|
+
.style(
|
|
44
|
+
"min-height: 100vh; display: flex; align-items: center; justify-content: center; padding: 20px 0; overflow-y: auto;"
|
|
45
|
+
)
|
|
46
|
+
),
|
|
47
|
+
ui.card().classes("login-card").style("width: 420px; max-width: 90vw; padding: 32px 36px;"),
|
|
48
|
+
):
|
|
49
|
+
# Logo
|
|
50
|
+
ui.image("/static/images/logo.svg").classes("mx-auto mb-3 login-logo")
|
|
51
|
+
|
|
52
|
+
# Title
|
|
53
|
+
ui.label("欢迎回来").classes("gradient-title text-h5 text-center w-full mb-1")
|
|
54
|
+
ui.label(f"登录 {config.core.app.app_name}").classes(
|
|
55
|
+
"text-sm text-grey-7 text-center w-full mb-4"
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
# Input fields - only bottom border
|
|
59
|
+
eid_input = (
|
|
60
|
+
ui.input("", placeholder="工号")
|
|
61
|
+
.classes("w-full mb-3 clean-input")
|
|
62
|
+
.props("standout dense hide-bottom-space")
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
password_input = (
|
|
66
|
+
ui.input(
|
|
67
|
+
"",
|
|
68
|
+
placeholder="密码",
|
|
69
|
+
password=True,
|
|
70
|
+
password_toggle_button=True,
|
|
71
|
+
)
|
|
72
|
+
.classes("w-full mb-2 clean-input")
|
|
73
|
+
.props("standout dense hide-bottom-space")
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
@handle_errors
|
|
77
|
+
async def do_login() -> None:
|
|
78
|
+
"""Handle login action."""
|
|
79
|
+
eid = eid_input.value.strip()
|
|
80
|
+
pwd = password_input.value
|
|
81
|
+
|
|
82
|
+
if not eid or not pwd:
|
|
83
|
+
ui.notify("请输入工号和密码", type="warning", position="top")
|
|
84
|
+
return
|
|
85
|
+
|
|
86
|
+
try:
|
|
87
|
+
await doctor_service.login(LoginCommand(eid=eid, password=Password.parse(pwd)))
|
|
88
|
+
ui.notify("登录成功", type="positive", position="top")
|
|
89
|
+
ui.navigate.to("/")
|
|
90
|
+
except InvalidCredentialsError as e:
|
|
91
|
+
ui.notify(e.message, type="negative", position="top")
|
|
92
|
+
|
|
93
|
+
# Enter key to login
|
|
94
|
+
password_input.on("keydown.enter", do_login)
|
|
95
|
+
|
|
96
|
+
# Login button
|
|
97
|
+
ui.button("登录", on_click=do_login).props(
|
|
98
|
+
"unelevated color=primary size=lg no-caps"
|
|
99
|
+
).classes("w-full login-button mt-4").style("height: 48px;")
|
|
100
|
+
|
|
101
|
+
# Divider
|
|
102
|
+
with ui.row().classes("w-full items-center gap-4 my-3"):
|
|
103
|
+
ui.separator().classes("flex-1")
|
|
104
|
+
ui.label("或").classes("text-xs text-grey-6")
|
|
105
|
+
ui.separator().classes("flex-1")
|
|
106
|
+
|
|
107
|
+
# Register button
|
|
108
|
+
ui.button("注册新账号", on_click=lambda: ui.navigate.to("/register")).props(
|
|
109
|
+
"flat size=lg no-caps"
|
|
110
|
+
).classes("w-full register-button").style("height: 48px;")
|