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,443 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import random
|
|
4
|
+
|
|
5
|
+
from dependency_injector.wiring import Provide
|
|
6
|
+
from dependency_injector.wiring import inject
|
|
7
|
+
from fastapi import Depends
|
|
8
|
+
from nicegui import ui
|
|
9
|
+
|
|
10
|
+
from audex.container import Container
|
|
11
|
+
from audex.service.doctor import DoctorService
|
|
12
|
+
from audex.service.export import ExportService
|
|
13
|
+
from audex.service.session import SessionService
|
|
14
|
+
from audex.view.decorators import handle_errors
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@ui.page("/sessions/export")
|
|
18
|
+
@handle_errors
|
|
19
|
+
@inject
|
|
20
|
+
async def render(
|
|
21
|
+
doctor_service: DoctorService = Depends(Provide[Container.service.doctor]),
|
|
22
|
+
session_service: SessionService = Depends(Provide[Container.service.session]),
|
|
23
|
+
export_service: ExportService = Depends(Provide[Container.service.export]),
|
|
24
|
+
) -> None:
|
|
25
|
+
"""Render export options page."""
|
|
26
|
+
|
|
27
|
+
# Get current doctor
|
|
28
|
+
doctor = await doctor_service.current_doctor()
|
|
29
|
+
|
|
30
|
+
# Add CSS
|
|
31
|
+
ui.add_head_html('<link rel="stylesheet" href="/static/css/sessions/styles.css">')
|
|
32
|
+
|
|
33
|
+
# Fetch sessions
|
|
34
|
+
sessions = await session_service.list(doctor_id=doctor.id, page_size=100)
|
|
35
|
+
|
|
36
|
+
# State
|
|
37
|
+
server_running = {"value": False}
|
|
38
|
+
|
|
39
|
+
# Before unload script
|
|
40
|
+
ui.add_head_html("""
|
|
41
|
+
<script>
|
|
42
|
+
window.addEventListener('beforeunload', function(e) {
|
|
43
|
+
if (window.serverRunning) {
|
|
44
|
+
e.preventDefault();
|
|
45
|
+
e.returnValue = '服务器正在运行,确定要离开吗?';
|
|
46
|
+
return e.returnValue;
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
</script>
|
|
50
|
+
""")
|
|
51
|
+
|
|
52
|
+
# Header
|
|
53
|
+
with (
|
|
54
|
+
ui.header().classes("header-glass items-center justify-between px-6 py-3"),
|
|
55
|
+
ui.row().classes("items-center gap-3"),
|
|
56
|
+
):
|
|
57
|
+
|
|
58
|
+
async def go_back():
|
|
59
|
+
"""返回并检查服务器状态."""
|
|
60
|
+
if server_running["value"]:
|
|
61
|
+
with (
|
|
62
|
+
ui.dialog() as leave_dialog,
|
|
63
|
+
ui.card()
|
|
64
|
+
.classes("dialog-card")
|
|
65
|
+
.style("width: 450px; padding: 28px; border-radius: 16px;"),
|
|
66
|
+
):
|
|
67
|
+
with ui.row().classes("w-full items-center mb-6"):
|
|
68
|
+
ui.icon("warning", size="xl").classes("text-warning")
|
|
69
|
+
ui.label("确认离开").classes("text-h5 font-bold text-grey-9 ml-3 flex-1")
|
|
70
|
+
ui.button(icon="close", on_click=leave_dialog.close).props(
|
|
71
|
+
"flat round dense"
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
ui.label("服务器正在运行,离开将自动关闭服务器").classes(
|
|
75
|
+
"text-body1 text-grey-8 mb-2"
|
|
76
|
+
)
|
|
77
|
+
ui.label("确定要离开吗?").classes("text-body2 text-grey-7 mb-6")
|
|
78
|
+
|
|
79
|
+
with ui.row().classes("w-full gap-3 justify-end"):
|
|
80
|
+
ui.button("取消", on_click=leave_dialog.close).props(
|
|
81
|
+
"outline color=grey-8 no-caps"
|
|
82
|
+
).classes("action-button")
|
|
83
|
+
|
|
84
|
+
async def confirm_leave():
|
|
85
|
+
await export_service.stop_server()
|
|
86
|
+
server_running["value"] = False
|
|
87
|
+
await ui.run_javascript("window.serverRunning = false;")
|
|
88
|
+
leave_dialog.close()
|
|
89
|
+
ui.navigate.to("/sessions")
|
|
90
|
+
|
|
91
|
+
ui.button("确认离开", on_click=confirm_leave).props(
|
|
92
|
+
"unelevated color=negative no-caps"
|
|
93
|
+
).classes("action-button")
|
|
94
|
+
|
|
95
|
+
leave_dialog.open()
|
|
96
|
+
else:
|
|
97
|
+
ui.navigate.to("/sessions")
|
|
98
|
+
|
|
99
|
+
ui.button(icon="arrow_back", on_click=go_back).props("flat round").tooltip("返回历史会话")
|
|
100
|
+
ui.label("导出会话").classes("text-h6 font-semibold text-grey-9")
|
|
101
|
+
|
|
102
|
+
# Main content - 完全垂直居中于整个视口
|
|
103
|
+
with (
|
|
104
|
+
ui.element("div")
|
|
105
|
+
.classes("w-full bg-white")
|
|
106
|
+
.style(
|
|
107
|
+
"position: fixed; "
|
|
108
|
+
"top: 0; "
|
|
109
|
+
"left: 0; "
|
|
110
|
+
"right: 0; "
|
|
111
|
+
"bottom: 0; "
|
|
112
|
+
"display: flex; "
|
|
113
|
+
"align-items: center; "
|
|
114
|
+
"justify-content: center; "
|
|
115
|
+
"padding: 60px 80px; "
|
|
116
|
+
"padding-top: calc(108px + 30px); "
|
|
117
|
+
"box-sizing: border-box; "
|
|
118
|
+
"overflow: auto;"
|
|
119
|
+
),
|
|
120
|
+
ui.element("div").style(
|
|
121
|
+
"display: flex; gap: 60px; align-items: center; max-width: 100%; width: 100%;"
|
|
122
|
+
),
|
|
123
|
+
):
|
|
124
|
+
# Left column
|
|
125
|
+
with ui.column().classes("gap-8").style("width: 360px; flex-shrink: 0;"):
|
|
126
|
+
# Title
|
|
127
|
+
with ui.column().classes("gap-2 mb-6"):
|
|
128
|
+
candidate_words = [":)", ":D", "🚀", "🎉", "😄", "👍"]
|
|
129
|
+
ui.label(random.choice(candidate_words)).classes("text-h3 font-bold text-grey-9")
|
|
130
|
+
ui.label("选择导出方式").classes("text-h2 gradient-text").style("line-height: 1.2;")
|
|
131
|
+
|
|
132
|
+
# Stats
|
|
133
|
+
with ui.card().classes("glass-card p-5 w-full").style("margin-top: 40px;"):
|
|
134
|
+
ui.label("统计").classes("text-subtitle2 font-semibold mb-4 text-grey-8")
|
|
135
|
+
|
|
136
|
+
with ui.column().classes("gap-3 w-full"):
|
|
137
|
+
with ui.row().classes("items-center justify-between w-full"):
|
|
138
|
+
ui.label("会话总数").classes("text-xs text-grey-7")
|
|
139
|
+
ui.label(str(len(sessions))).classes("text-body1 font-bold text-primary")
|
|
140
|
+
|
|
141
|
+
with ui.row().classes("items-center justify-between w-full"):
|
|
142
|
+
ui.label("服务器状态").classes("text-xs text-grey-7")
|
|
143
|
+
status_label = ui.label("未启动").classes(
|
|
144
|
+
"text-body1 font-bold text-grey-6"
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
# Right column - 2x2 grid
|
|
148
|
+
with ui.element("div").style(
|
|
149
|
+
"flex: 1; "
|
|
150
|
+
"display: grid; "
|
|
151
|
+
"grid-template-columns: repeat(2, 1fr); "
|
|
152
|
+
"gap: 20px; "
|
|
153
|
+
"max-width: 850px; "
|
|
154
|
+
"margin-left: auto;"
|
|
155
|
+
):
|
|
156
|
+
# Card 1: Server export
|
|
157
|
+
server_card = (
|
|
158
|
+
ui.card()
|
|
159
|
+
.classes("super-card cursor-pointer")
|
|
160
|
+
.style(
|
|
161
|
+
"height: 220px; "
|
|
162
|
+
"display: flex; "
|
|
163
|
+
"flex-direction: column; "
|
|
164
|
+
"padding: 1.5rem; "
|
|
165
|
+
"box-sizing: border-box;"
|
|
166
|
+
)
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
with server_card:
|
|
170
|
+
server_icon = (
|
|
171
|
+
ui.icon("cloud", size="3em")
|
|
172
|
+
.classes("text-primary rotate-icon")
|
|
173
|
+
.style("flex-shrink: 0; margin-bottom: 0.75rem;")
|
|
174
|
+
)
|
|
175
|
+
with ui.column().classes("gap-2").style("flex: 1;"):
|
|
176
|
+
ui.label("服务器导出").classes("text-h6 font-bold text-grey-9")
|
|
177
|
+
ui.label("通过浏览器访问导出页面").classes("text-sm text-grey-7")
|
|
178
|
+
server_btn = (
|
|
179
|
+
ui.button("启动", icon="arrow_forward")
|
|
180
|
+
.props("color=primary flat dense")
|
|
181
|
+
.classes("press-button")
|
|
182
|
+
.style(
|
|
183
|
+
"align-self: flex-end; "
|
|
184
|
+
"flex-shrink: 0; "
|
|
185
|
+
"background: transparent ! important; "
|
|
186
|
+
"box-shadow: none !important;"
|
|
187
|
+
)
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
async def start_server_export():
|
|
191
|
+
"""Start or stop server export."""
|
|
192
|
+
if server_running["value"]:
|
|
193
|
+
with (
|
|
194
|
+
ui.dialog() as stop_dialog,
|
|
195
|
+
ui.card()
|
|
196
|
+
.classes("dialog-card")
|
|
197
|
+
.style("width: 450px; padding: 28px; border-radius: 16px;"),
|
|
198
|
+
):
|
|
199
|
+
with ui.row().classes("w-full items-center mb-6"):
|
|
200
|
+
ui.icon("warning", size="xl").classes("text-warning")
|
|
201
|
+
ui.label("确认关闭").classes(
|
|
202
|
+
"text-h5 font-bold text-grey-9 ml-3 flex-1"
|
|
203
|
+
)
|
|
204
|
+
ui.button(icon="close", on_click=stop_dialog.close).props(
|
|
205
|
+
"flat round dense"
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
ui.label("确定要停止服务器吗?").classes("text-body1 text-grey-8 mb-2")
|
|
209
|
+
ui.label("其他设备将无法继续访问").classes("text-body2 text-grey-7 mb-6")
|
|
210
|
+
|
|
211
|
+
with ui.row().classes("w-full gap-3 justify-end"):
|
|
212
|
+
ui.button("取消", on_click=stop_dialog.close).props(
|
|
213
|
+
"outline color=grey-8 no-caps"
|
|
214
|
+
).classes("action-button")
|
|
215
|
+
|
|
216
|
+
async def confirm_stop():
|
|
217
|
+
await export_service.stop_server()
|
|
218
|
+
server_running["value"] = False
|
|
219
|
+
await ui.run_javascript("window.serverRunning = false;")
|
|
220
|
+
status_label.text = "未启动"
|
|
221
|
+
status_label.classes(remove="text-positive", add="text-grey-6")
|
|
222
|
+
|
|
223
|
+
# reset card UI
|
|
224
|
+
server_card.classes(remove="super-card-active")
|
|
225
|
+
server_icon.classes(remove="text-negative", add="text-primary")
|
|
226
|
+
server_btn.set_text("启动")
|
|
227
|
+
server_btn.props("icon=arrow_forward color=primary")
|
|
228
|
+
|
|
229
|
+
stop_dialog.close()
|
|
230
|
+
ui.notify("服务器已停止", type="info", position="top")
|
|
231
|
+
|
|
232
|
+
ui.button("确认停止", on_click=confirm_stop).props(
|
|
233
|
+
"unelevated color=negative no-caps"
|
|
234
|
+
).classes("action-button")
|
|
235
|
+
|
|
236
|
+
stop_dialog.open()
|
|
237
|
+
return
|
|
238
|
+
|
|
239
|
+
# Start server
|
|
240
|
+
try:
|
|
241
|
+
info = await export_service.start_server()
|
|
242
|
+
server_running["value"] = True
|
|
243
|
+
await ui.run_javascript("window.serverRunning = true;")
|
|
244
|
+
status_label.text = "运行中"
|
|
245
|
+
status_label.classes(remove="text-grey-6", add="text-positive")
|
|
246
|
+
|
|
247
|
+
server_card.classes(add="super-card-active")
|
|
248
|
+
server_icon.classes(remove="text-primary", add="text-negative")
|
|
249
|
+
server_btn.set_text("停止")
|
|
250
|
+
server_btn.props("icon=stop color=negative")
|
|
251
|
+
|
|
252
|
+
# Show success dialog
|
|
253
|
+
with (
|
|
254
|
+
ui.dialog() as success_dialog,
|
|
255
|
+
ui.card()
|
|
256
|
+
.classes("dialog-card")
|
|
257
|
+
.style("width: 550px; padding: 32px; border-radius: 16px;"),
|
|
258
|
+
):
|
|
259
|
+
with ui.row().classes("w-full items-center mb-6"):
|
|
260
|
+
ui.icon("check_circle", size="xl").classes("text-positive")
|
|
261
|
+
ui.label("服务器已启动").classes(
|
|
262
|
+
"text-h5 font-bold text-grey-9 q-ml-sm"
|
|
263
|
+
)
|
|
264
|
+
ui.space()
|
|
265
|
+
ui.button(icon="close", on_click=success_dialog.close).props(
|
|
266
|
+
"flat round dense"
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
# Server info
|
|
270
|
+
with ui.element("div").classes("info-box"):
|
|
271
|
+
ui.label("访问地址").classes("text-xs text-grey-6 mb-2")
|
|
272
|
+
|
|
273
|
+
with ui.row().classes("items-center gap-2 mb-3"):
|
|
274
|
+
ui.label(info.url).classes("text-body1 font-mono text-primary")
|
|
275
|
+
|
|
276
|
+
async def copy_url():
|
|
277
|
+
await ui.run_javascript(
|
|
278
|
+
f"navigator.clipboard.writeText('{info.url}')"
|
|
279
|
+
)
|
|
280
|
+
ui.notify("已复制", type="positive", position="top")
|
|
281
|
+
|
|
282
|
+
ui.button(icon="content_copy", on_click=copy_url).props(
|
|
283
|
+
"flat dense round size=sm"
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
ui.element("div").classes("divider")
|
|
287
|
+
|
|
288
|
+
ui.label(f"主机: {info.host}").classes("text-sm text-grey-7 mb-1")
|
|
289
|
+
ui.label(f"端口: {info.port}").classes("text-sm text-grey-7")
|
|
290
|
+
|
|
291
|
+
ui.label("在其他设备的浏览器中打开上述地址即可访问").classes(
|
|
292
|
+
"text-body2 text-grey-6 mt-4 mb-6"
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
ui.button("知道了", on_click=success_dialog.close).props(
|
|
296
|
+
"unelevated color=primary no-caps"
|
|
297
|
+
).classes("action-button w-full").style("height: 48px;")
|
|
298
|
+
|
|
299
|
+
success_dialog.open()
|
|
300
|
+
|
|
301
|
+
except Exception:
|
|
302
|
+
ui.notify("启动失败", type="negative", position="top")
|
|
303
|
+
|
|
304
|
+
server_card.on("click", start_server_export)
|
|
305
|
+
|
|
306
|
+
# Card 2: USB export
|
|
307
|
+
async def start_usb_export():
|
|
308
|
+
"""Start USB export."""
|
|
309
|
+
devices = await export_service.list_usb_devices()
|
|
310
|
+
|
|
311
|
+
if not devices:
|
|
312
|
+
with (
|
|
313
|
+
ui.dialog() as no_device_dialog,
|
|
314
|
+
ui.card()
|
|
315
|
+
.classes("dialog-card")
|
|
316
|
+
.style("width: 500px; padding: 32px; border-radius: 16px;"),
|
|
317
|
+
):
|
|
318
|
+
with ui.row().classes("w-full items-center mb-6"):
|
|
319
|
+
ui.icon("usb_off", size="xl").classes("text-grey-4")
|
|
320
|
+
ui.label("未检测到U盘").classes("text-h5 font-bold text-grey-9 q-ml-sm")
|
|
321
|
+
ui.space()
|
|
322
|
+
ui.button(icon="close", on_click=no_device_dialog.close).props(
|
|
323
|
+
"flat round dense"
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
with ui.column().classes("items-center py-4 gap-3"):
|
|
327
|
+
ui.label("请插入U盘后重试").classes("text-body1 text-grey-7")
|
|
328
|
+
|
|
329
|
+
async def refresh():
|
|
330
|
+
no_device_dialog.close()
|
|
331
|
+
await start_usb_export()
|
|
332
|
+
|
|
333
|
+
ui.button("刷新", icon="refresh", on_click=refresh).props(
|
|
334
|
+
"flat no-caps"
|
|
335
|
+
).classes("action-button mt-4")
|
|
336
|
+
|
|
337
|
+
no_device_dialog.open()
|
|
338
|
+
return
|
|
339
|
+
|
|
340
|
+
device = devices[0]
|
|
341
|
+
|
|
342
|
+
with (
|
|
343
|
+
ui.dialog() as usb_dialog,
|
|
344
|
+
ui.card()
|
|
345
|
+
.classes("dialog-card")
|
|
346
|
+
.style("width: 550px; padding: 32px; border-radius: 16px;"),
|
|
347
|
+
):
|
|
348
|
+
with ui.row().classes("w-full items-center mb-6"):
|
|
349
|
+
ui.icon("check_circle", size="xl").classes("text-positive")
|
|
350
|
+
ui.label("检测到U盘").classes("text-h5 font-bold text-grey-9 q-ml-sm")
|
|
351
|
+
ui.space()
|
|
352
|
+
ui.button(icon="close", on_click=usb_dialog.close).props("flat round dense")
|
|
353
|
+
|
|
354
|
+
with ui.element("div").classes("info-box"):
|
|
355
|
+
with ui.row().classes("items-center gap-3 mb-3"):
|
|
356
|
+
ui.icon("usb", size="lg").classes("text-secondary")
|
|
357
|
+
ui.label(device.label or "未命名设备").classes(
|
|
358
|
+
"text-body1 font-semibold"
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
if device.vendor or device.model:
|
|
362
|
+
ui.label(f"{device.vendor or ''} {device.model or ''}").classes(
|
|
363
|
+
"text-sm text-grey-6 mb-2"
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
ui.element("div").classes("divider")
|
|
367
|
+
|
|
368
|
+
ui.label(f"挂载点: {device.mount_point}").classes(
|
|
369
|
+
"text-sm text-grey-7 mb-1"
|
|
370
|
+
)
|
|
371
|
+
if device.size_bytes:
|
|
372
|
+
size_gb = device.size_bytes / (1024**3)
|
|
373
|
+
ui.label(f"容量: {size_gb:.2f} GB").classes("text-sm text-grey-7")
|
|
374
|
+
|
|
375
|
+
with (
|
|
376
|
+
ui.element("div").classes("export-summary-box mt-4"),
|
|
377
|
+
ui.row().classes("items-center justify-between"),
|
|
378
|
+
):
|
|
379
|
+
ui.label("会话数量").classes("text-sm text-grey-6")
|
|
380
|
+
ui.label(f"{len(sessions)} 个").classes("text-body1 font-semibold")
|
|
381
|
+
|
|
382
|
+
async def do_export():
|
|
383
|
+
export_btn.props("loading")
|
|
384
|
+
|
|
385
|
+
try:
|
|
386
|
+
session_ids = [s.id for s in sessions]
|
|
387
|
+
result = await export_service.export_to_usb(session_ids, device)
|
|
388
|
+
|
|
389
|
+
usb_dialog.close()
|
|
390
|
+
|
|
391
|
+
if result.success:
|
|
392
|
+
ui.notify(
|
|
393
|
+
f"成功导出 {result.success_count} 个会话",
|
|
394
|
+
type="positive",
|
|
395
|
+
position="top",
|
|
396
|
+
)
|
|
397
|
+
else:
|
|
398
|
+
ui.notify(
|
|
399
|
+
f"部分失败: 成功 {result.success_count}/{result.total}",
|
|
400
|
+
type="warning",
|
|
401
|
+
position="top",
|
|
402
|
+
)
|
|
403
|
+
|
|
404
|
+
except Exception:
|
|
405
|
+
ui.notify("导出失败", type="negative", position="top")
|
|
406
|
+
finally:
|
|
407
|
+
export_btn.props(remove="loading")
|
|
408
|
+
|
|
409
|
+
export_btn = (
|
|
410
|
+
ui.button("开始导出", on_click=do_export)
|
|
411
|
+
.props("unelevated color=primary size=lg no-caps")
|
|
412
|
+
.classes("action-button w-full mt-6")
|
|
413
|
+
.style("height: 48px;")
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
usb_dialog.open()
|
|
417
|
+
|
|
418
|
+
with (
|
|
419
|
+
ui.card()
|
|
420
|
+
.classes("super-card cursor-pointer")
|
|
421
|
+
.on("click", start_usb_export)
|
|
422
|
+
.style(
|
|
423
|
+
"height: 220px; "
|
|
424
|
+
"display: flex; "
|
|
425
|
+
"flex-direction: column; "
|
|
426
|
+
"padding: 1.5rem; "
|
|
427
|
+
"box-sizing: border-box;"
|
|
428
|
+
)
|
|
429
|
+
):
|
|
430
|
+
ui.icon("usb", size="3em").classes("text-secondary rotate-icon").style(
|
|
431
|
+
"flex-shrink: 0; margin-bottom: 0.75rem;"
|
|
432
|
+
)
|
|
433
|
+
with ui.column().classes("gap-2").style("flex: 1;"):
|
|
434
|
+
ui.label("U盘导出").classes("text-h6 font-bold text-grey-9")
|
|
435
|
+
ui.label("直接导出到U盘设备").classes("text-sm text-grey-7")
|
|
436
|
+
ui.button("开始", icon="arrow_forward").props("color=secondary flat dense").classes(
|
|
437
|
+
"press-button"
|
|
438
|
+
).style(
|
|
439
|
+
"align-self: flex-end; "
|
|
440
|
+
"flex-shrink: 0; "
|
|
441
|
+
"background: transparent !important; "
|
|
442
|
+
"box-shadow: none !important;"
|
|
443
|
+
)
|