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.
Files changed (192) hide show
  1. audex/__init__.py +9 -0
  2. audex/__main__.py +7 -0
  3. audex/cli/__init__.py +189 -0
  4. audex/cli/apis/__init__.py +12 -0
  5. audex/cli/apis/init/__init__.py +34 -0
  6. audex/cli/apis/init/gencfg.py +130 -0
  7. audex/cli/apis/init/setup.py +330 -0
  8. audex/cli/apis/init/vprgroup.py +125 -0
  9. audex/cli/apis/serve.py +141 -0
  10. audex/cli/args.py +356 -0
  11. audex/cli/exceptions.py +44 -0
  12. audex/cli/helper/__init__.py +0 -0
  13. audex/cli/helper/ansi.py +193 -0
  14. audex/cli/helper/display.py +288 -0
  15. audex/config/__init__.py +64 -0
  16. audex/config/core/__init__.py +30 -0
  17. audex/config/core/app.py +29 -0
  18. audex/config/core/audio.py +45 -0
  19. audex/config/core/logging.py +163 -0
  20. audex/config/core/session.py +11 -0
  21. audex/config/helper/__init__.py +1 -0
  22. audex/config/helper/client/__init__.py +1 -0
  23. audex/config/helper/client/http.py +28 -0
  24. audex/config/helper/client/websocket.py +21 -0
  25. audex/config/helper/provider/__init__.py +1 -0
  26. audex/config/helper/provider/dashscope.py +13 -0
  27. audex/config/helper/provider/unisound.py +18 -0
  28. audex/config/helper/provider/xfyun.py +23 -0
  29. audex/config/infrastructure/__init__.py +31 -0
  30. audex/config/infrastructure/cache.py +51 -0
  31. audex/config/infrastructure/database.py +48 -0
  32. audex/config/infrastructure/recorder.py +32 -0
  33. audex/config/infrastructure/store.py +19 -0
  34. audex/config/provider/__init__.py +18 -0
  35. audex/config/provider/transcription.py +109 -0
  36. audex/config/provider/vpr.py +99 -0
  37. audex/container.py +40 -0
  38. audex/entity/__init__.py +468 -0
  39. audex/entity/doctor.py +109 -0
  40. audex/entity/doctor.pyi +51 -0
  41. audex/entity/fields.py +401 -0
  42. audex/entity/segment.py +115 -0
  43. audex/entity/segment.pyi +38 -0
  44. audex/entity/session.py +133 -0
  45. audex/entity/session.pyi +47 -0
  46. audex/entity/utterance.py +142 -0
  47. audex/entity/utterance.pyi +48 -0
  48. audex/entity/vp.py +68 -0
  49. audex/entity/vp.pyi +35 -0
  50. audex/exceptions.py +157 -0
  51. audex/filters/__init__.py +692 -0
  52. audex/filters/generated/__init__.py +21 -0
  53. audex/filters/generated/doctor.py +987 -0
  54. audex/filters/generated/segment.py +723 -0
  55. audex/filters/generated/session.py +978 -0
  56. audex/filters/generated/utterance.py +939 -0
  57. audex/filters/generated/vp.py +815 -0
  58. audex/helper/__init__.py +1 -0
  59. audex/helper/hash.py +33 -0
  60. audex/helper/mixin.py +65 -0
  61. audex/helper/net.py +19 -0
  62. audex/helper/settings/__init__.py +830 -0
  63. audex/helper/settings/fields.py +317 -0
  64. audex/helper/stream.py +153 -0
  65. audex/injectors/__init__.py +1 -0
  66. audex/injectors/config.py +12 -0
  67. audex/injectors/lifespan.py +7 -0
  68. audex/lib/__init__.py +1 -0
  69. audex/lib/cache/__init__.py +383 -0
  70. audex/lib/cache/inmemory.py +513 -0
  71. audex/lib/database/__init__.py +83 -0
  72. audex/lib/database/sqlite.py +406 -0
  73. audex/lib/exporter.py +189 -0
  74. audex/lib/injectors/__init__.py +1 -0
  75. audex/lib/injectors/cache.py +25 -0
  76. audex/lib/injectors/container.py +47 -0
  77. audex/lib/injectors/exporter.py +26 -0
  78. audex/lib/injectors/recorder.py +33 -0
  79. audex/lib/injectors/server.py +17 -0
  80. audex/lib/injectors/session.py +18 -0
  81. audex/lib/injectors/sqlite.py +24 -0
  82. audex/lib/injectors/store.py +13 -0
  83. audex/lib/injectors/transcription.py +42 -0
  84. audex/lib/injectors/usb.py +12 -0
  85. audex/lib/injectors/vpr.py +65 -0
  86. audex/lib/injectors/wifi.py +7 -0
  87. audex/lib/recorder.py +844 -0
  88. audex/lib/repos/__init__.py +149 -0
  89. audex/lib/repos/container.py +23 -0
  90. audex/lib/repos/database/__init__.py +1 -0
  91. audex/lib/repos/database/sqlite.py +672 -0
  92. audex/lib/repos/decorators.py +74 -0
  93. audex/lib/repos/doctor.py +286 -0
  94. audex/lib/repos/segment.py +302 -0
  95. audex/lib/repos/session.py +285 -0
  96. audex/lib/repos/tables/__init__.py +70 -0
  97. audex/lib/repos/tables/doctor.py +137 -0
  98. audex/lib/repos/tables/segment.py +113 -0
  99. audex/lib/repos/tables/session.py +140 -0
  100. audex/lib/repos/tables/utterance.py +131 -0
  101. audex/lib/repos/tables/vp.py +102 -0
  102. audex/lib/repos/utterance.py +288 -0
  103. audex/lib/repos/vp.py +286 -0
  104. audex/lib/restful.py +251 -0
  105. audex/lib/server/__init__.py +97 -0
  106. audex/lib/server/auth.py +98 -0
  107. audex/lib/server/handlers.py +248 -0
  108. audex/lib/server/templates/index.html.j2 +226 -0
  109. audex/lib/server/templates/login.html.j2 +111 -0
  110. audex/lib/server/templates/static/script.js +68 -0
  111. audex/lib/server/templates/static/style.css +579 -0
  112. audex/lib/server/types.py +123 -0
  113. audex/lib/session.py +503 -0
  114. audex/lib/store/__init__.py +238 -0
  115. audex/lib/store/localfile.py +411 -0
  116. audex/lib/transcription/__init__.py +33 -0
  117. audex/lib/transcription/dashscope.py +525 -0
  118. audex/lib/transcription/events.py +62 -0
  119. audex/lib/usb.py +554 -0
  120. audex/lib/vpr/__init__.py +38 -0
  121. audex/lib/vpr/unisound/__init__.py +185 -0
  122. audex/lib/vpr/unisound/types.py +469 -0
  123. audex/lib/vpr/xfyun/__init__.py +483 -0
  124. audex/lib/vpr/xfyun/types.py +679 -0
  125. audex/lib/websocket/__init__.py +8 -0
  126. audex/lib/websocket/connection.py +485 -0
  127. audex/lib/websocket/pool.py +991 -0
  128. audex/lib/wifi.py +1146 -0
  129. audex/lifespan.py +75 -0
  130. audex/service/__init__.py +27 -0
  131. audex/service/decorators.py +73 -0
  132. audex/service/doctor/__init__.py +652 -0
  133. audex/service/doctor/const.py +36 -0
  134. audex/service/doctor/exceptions.py +96 -0
  135. audex/service/doctor/types.py +54 -0
  136. audex/service/export/__init__.py +236 -0
  137. audex/service/export/const.py +17 -0
  138. audex/service/export/exceptions.py +34 -0
  139. audex/service/export/types.py +21 -0
  140. audex/service/injectors/__init__.py +1 -0
  141. audex/service/injectors/container.py +53 -0
  142. audex/service/injectors/doctor.py +34 -0
  143. audex/service/injectors/export.py +27 -0
  144. audex/service/injectors/session.py +49 -0
  145. audex/service/session/__init__.py +754 -0
  146. audex/service/session/const.py +34 -0
  147. audex/service/session/exceptions.py +67 -0
  148. audex/service/session/types.py +91 -0
  149. audex/types.py +39 -0
  150. audex/utils.py +287 -0
  151. audex/valueobj/__init__.py +81 -0
  152. audex/valueobj/common/__init__.py +1 -0
  153. audex/valueobj/common/auth.py +84 -0
  154. audex/valueobj/common/email.py +16 -0
  155. audex/valueobj/common/ops.py +22 -0
  156. audex/valueobj/common/phone.py +84 -0
  157. audex/valueobj/common/version.py +72 -0
  158. audex/valueobj/session.py +19 -0
  159. audex/valueobj/utterance.py +15 -0
  160. audex/view/__init__.py +51 -0
  161. audex/view/container.py +17 -0
  162. audex/view/decorators.py +303 -0
  163. audex/view/pages/__init__.py +1 -0
  164. audex/view/pages/dashboard/__init__.py +286 -0
  165. audex/view/pages/dashboard/wifi.py +407 -0
  166. audex/view/pages/login.py +110 -0
  167. audex/view/pages/recording.py +348 -0
  168. audex/view/pages/register.py +202 -0
  169. audex/view/pages/sessions/__init__.py +196 -0
  170. audex/view/pages/sessions/details.py +224 -0
  171. audex/view/pages/sessions/export.py +443 -0
  172. audex/view/pages/settings.py +374 -0
  173. audex/view/pages/voiceprint/__init__.py +1 -0
  174. audex/view/pages/voiceprint/enroll.py +195 -0
  175. audex/view/pages/voiceprint/update.py +195 -0
  176. audex/view/static/css/dashboard.css +452 -0
  177. audex/view/static/css/glass.css +22 -0
  178. audex/view/static/css/global.css +541 -0
  179. audex/view/static/css/login.css +386 -0
  180. audex/view/static/css/recording.css +439 -0
  181. audex/view/static/css/register.css +293 -0
  182. audex/view/static/css/sessions/styles.css +501 -0
  183. audex/view/static/css/settings.css +186 -0
  184. audex/view/static/css/voiceprint/enroll.css +43 -0
  185. audex/view/static/css/voiceprint/styles.css +209 -0
  186. audex/view/static/css/voiceprint/update.css +44 -0
  187. audex/view/static/images/logo.svg +95 -0
  188. audex/view/static/js/recording.js +42 -0
  189. audex-1.0.7a3.dist-info/METADATA +361 -0
  190. audex-1.0.7a3.dist-info/RECORD +192 -0
  191. audex-1.0.7a3.dist-info/WHEEL +4 -0
  192. audex-1.0.7a3.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,195 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import typing as t
5
+
6
+ from dependency_injector.wiring import Provide
7
+ from dependency_injector.wiring import inject
8
+ from fastapi import Depends
9
+ from nicegui import ui
10
+
11
+ from audex.container import Container
12
+ from audex.service.doctor import DoctorService
13
+ from audex.view.decorators import handle_errors
14
+
15
+
16
+ @ui.page("/voiceprint/update")
17
+ @handle_errors
18
+ @inject
19
+ async def render(
20
+ doctor_service: DoctorService = Depends(Provide[Container.service.doctor]),
21
+ ) -> None:
22
+ """Render voiceprint update page."""
23
+
24
+ # Check if has voiceprint
25
+ has_vp = await doctor_service.has_voiceprint()
26
+ if not has_vp:
27
+ ui.notify("您还未注册声纹,请先注册", type="warning", position="top")
28
+ ui.navigate.to("/voiceprint/enroll")
29
+ return
30
+
31
+ # Add CSS
32
+ ui.add_head_html('<link rel="stylesheet" href="/static/css/voiceprint/update.css">')
33
+
34
+ # State
35
+ is_recording = {"value": False}
36
+ update_context: dict[str, t.Any] = {"value": None}
37
+ elapsed_time = {"value": 0}
38
+ timer_task: dict[str, asyncio.Task[t.Any] | None] = {"value": None}
39
+
40
+ # Header
41
+ with (
42
+ ui.header().classes("header-glass items-center justify-between px-6 py-3"),
43
+ ui.row().classes("items-center gap-3"),
44
+ ):
45
+ ui.button(icon="arrow_back", on_click=lambda: ui.navigate.to("/")).props(
46
+ "flat round"
47
+ ).tooltip("返回主面板")
48
+ ui.label("更新声纹").classes("text-h6 font-semibold text-grey-9")
49
+
50
+ # Main container
51
+ with (
52
+ ui.element("div").classes("voiceprint-container"),
53
+ ui.element("div").classes("voiceprint-content"),
54
+ ):
55
+ # Left side: Steps
56
+ with ui.column().classes("voiceprint-steps"):
57
+ ui.label("操作流程").classes("text-h5 font-bold text-grey-9 mb-2")
58
+
59
+ with ui.column().classes("gap-4"):
60
+ with ui.row().classes("items-start gap-3"):
61
+ ui.label("1").classes(
62
+ "text-sm font-bold text-white w-6 h-6 flex items-center justify-center"
63
+ ).style("background: #8b5cf6; border-radius: 50%;")
64
+ with ui.column().classes("gap-1"):
65
+ ui.label("点击按钮开始").classes("text-sm font-medium text-grey-9")
66
+ ui.label("启动录音功能").classes("text-xs text-grey-6")
67
+
68
+ with ui.row().classes("items-start gap-3"):
69
+ ui.label("2").classes(
70
+ "text-sm font-bold text-white w-6 h-6 flex items-center justify-center"
71
+ ).style("background: #8b5cf6; border-radius: 50%;")
72
+ with ui.column().classes("gap-1"):
73
+ ui.label("朗读右侧文字").classes("text-sm font-medium text-grey-9")
74
+ ui.label("清晰完整朗读").classes("text-xs text-grey-6")
75
+
76
+ with ui.row().classes("items-start gap-3"):
77
+ ui.label("3").classes(
78
+ "text-sm font-bold text-white w-6 h-6 flex items-center justify-center"
79
+ ).style("background: #8b5cf6; border-radius: 50%;")
80
+ with ui.column().classes("gap-1"):
81
+ ui.label("点击停止完成").classes("text-sm font-medium text-grey-9")
82
+ ui.label("时长 5-20 秒").classes("text-xs text-grey-6")
83
+
84
+ # Center: Text to read
85
+ with ui.column().classes("voiceprint-text"):
86
+ ui.label("请朗读:").classes("text-body1 text-grey-6")
87
+ ui.label(doctor_service.config.vpr_text_content).classes(
88
+ "text-h4 text-grey-9 font-semibold leading-relaxed"
89
+ ).style(
90
+ "line-height: 1.8; "
91
+ "word-break: keep-all; "
92
+ "overflow-wrap: break-word; "
93
+ "white-space: normal;"
94
+ )
95
+
96
+ # Right side: Recording button
97
+ with ui.column().classes("voiceprint-button"):
98
+ # Timer
99
+ timer_label = ui.label("00:00").classes("timer")
100
+
101
+ # Button container
102
+ button_container = ui.element("div").style(
103
+ "position: relative; display: flex; align-items: center; justify-content: center;"
104
+ )
105
+
106
+ with button_container:
107
+ # Rings
108
+ ring1 = ui.element("div").classes("recording-ring")
109
+ ring1.visible = False
110
+ ring2 = ui.element("div").classes("recording-ring").style("animation-delay: 0.8s;")
111
+ ring2.visible = False
112
+ ring3 = ui.element("div").classes("recording-ring").style("animation-delay: 1.6s;")
113
+ ring3.visible = False
114
+
115
+ @handle_errors
116
+ async def toggle_recording():
117
+ """Toggle recording."""
118
+ if not is_recording["value"]:
119
+ # Start
120
+ ctx = await doctor_service.update_vp()
121
+ update_context["value"] = ctx
122
+ await ctx.start()
123
+
124
+ is_recording["value"] = True
125
+ elapsed_time["value"] = 0
126
+
127
+ record_btn.props("icon=stop color=negative")
128
+
129
+ ring1.visible = True
130
+ ring2.visible = True
131
+ ring3.visible = True
132
+
133
+ ui.notify("开始录音", type="info")
134
+ timer_task["value"] = asyncio.create_task(update_timer())
135
+
136
+ else:
137
+ # Stop
138
+ if elapsed_time["value"] < 5:
139
+ ui.notify("录音时间不足 5 秒,请继续", type="warning")
140
+ return
141
+
142
+ if timer_task["value"]:
143
+ timer_task["value"].cancel()
144
+
145
+ record_btn.props("loading icon=mic color=grey")
146
+
147
+ ring1.visible = False
148
+ ring2.visible = False
149
+ ring3.visible = False
150
+
151
+ try:
152
+ ctx = update_context["value"]
153
+ result = await ctx.close()
154
+
155
+ is_recording["value"] = False
156
+
157
+ ui.notify(
158
+ f"声纹更新成功!录音时长: {result.duration_ms / 1000:.1f}秒",
159
+ type="positive",
160
+ )
161
+
162
+ await asyncio.sleep(2)
163
+ ui.navigate.to("/")
164
+
165
+ except Exception:
166
+ record_btn.props(remove="loading")
167
+ record_btn.props("icon=mic color=purple")
168
+ is_recording["value"] = False
169
+ raise
170
+
171
+ async def update_timer():
172
+ try:
173
+ while is_recording["value"]:
174
+ await asyncio.sleep(1)
175
+ elapsed_time["value"] += 1
176
+
177
+ minutes = elapsed_time["value"] // 60
178
+ seconds = elapsed_time["value"] % 60
179
+ timer_label.text = f"{minutes:02d}:{seconds:02d}"
180
+
181
+ if elapsed_time["value"] >= 20:
182
+ await toggle_recording()
183
+ break
184
+ except asyncio.CancelledError:
185
+ pass
186
+
187
+ record_btn = (
188
+ ui.button(icon="mic", on_click=toggle_recording)
189
+ .props("round unelevated color=purple size=xl")
190
+ .classes("record-button")
191
+ .style("font-size: 3em ! important;")
192
+ )
193
+
194
+ # Hint
195
+ ui.label("点击按钮开始录音").classes("text-sm text-grey-6")
@@ -0,0 +1,452 @@
1
+ @import "global.css";
2
+ @import "glass.css";
3
+
4
+
5
+ /* ==================== Utility ==================== */
6
+ .cursor-pointer {
7
+ cursor: pointer;
8
+ }
9
+
10
+ /* ==================== Animations ==================== */
11
+
12
+ /* Gradient text animation */
13
+ .gradient-text {
14
+ background: linear-gradient(-45deg, #ee7752, #e73c7e, #23a6d5, #23d5ab, #f093fb, #4facfe);
15
+ background-size: 400% 400%;
16
+ -webkit-background-clip: text;
17
+ -webkit-text-fill-color: transparent;
18
+ background-clip: text;
19
+ animation: gradient-shift 8s ease infinite;
20
+ font-weight: 700;
21
+ }
22
+
23
+ @keyframes gradient-shift {
24
+ 0% {
25
+ background-position: 0 50%;
26
+ }
27
+ 50% {
28
+ background-position: 100% 50%;
29
+ }
30
+ 100% {
31
+ background-position: 0 50%;
32
+ }
33
+ }
34
+
35
+ /* ==================== Cards ==================== */
36
+
37
+ /* Super card with hover and active states */
38
+ .super-card {
39
+ border-radius: 28px !important;
40
+ background: rgba(255, 255, 255, 0.9) !important;
41
+ backdrop-filter: blur(20px) !important;
42
+ -webkit-backdrop-filter: blur(20px) !important;
43
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.06), 0 2px 8px rgba(0, 0, 0, 0.04) !important;
44
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important;
45
+ }
46
+
47
+ .super-card:hover {
48
+ transform: translateY(-6px) scale(1.02);
49
+ box-shadow: 0 12px 28px rgba(0, 0, 0, 0.12), 0 6px 14px rgba(0, 0, 0, 0.08) !important;
50
+ }
51
+
52
+ .super-card:active {
53
+ transform: translateY(-3px) scale(1.01);
54
+ transition: all 0.1s cubic-bezier(0.4, 0, 0.2, 1) !important;
55
+ }
56
+
57
+ /* Active card state (for server running) */
58
+ .super-card-active {
59
+ border: 2px solid #c10015 !important;
60
+ box-shadow: 0 4px 20px rgba(193, 0, 21, 0.15) !important;
61
+ }
62
+
63
+ .super-card-active:hover {
64
+ box-shadow: 0 12px 32px rgba(193, 0, 21, 0.25) !important;
65
+ }
66
+
67
+ /* Button press */
68
+ .press-button:active {
69
+ transform: scale(0.95);
70
+ }
71
+
72
+ /* Icon rotate on hover */
73
+ .rotate-icon {
74
+ transition: transform 0.4s cubic-bezier(0.4, 0, 0.2, 1);
75
+ }
76
+
77
+ .super-card:hover .rotate-icon {
78
+ transform: rotate(8deg) scale(1.15);
79
+ }
80
+
81
+ /* Pure white background */
82
+ .bg-white {
83
+ background: #ffffff !important;
84
+ padding: 40px 60px !important;
85
+ margin: 0 !important;
86
+ margin-top: 120px !important;
87
+ box-sizing: border-box !important;
88
+ overflow: visible !important;
89
+ min-height: auto !important;
90
+ height: auto !important;
91
+ }
92
+
93
+ /* ==================== Buttons (dashboard 特有) ==================== */
94
+
95
+ /* Header buttons */
96
+ .header-btn {
97
+ border-radius: 8px !important;
98
+ min-width: 100px !important;
99
+ height: 40px !important;
100
+ font-size: 14px !important;
101
+ font-weight: 500 !important;
102
+ transition: all 0.2s ease !important;
103
+ }
104
+
105
+ .header-btn:hover {
106
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15) !important;
107
+ transform: translateY(-1px);
108
+ }
109
+
110
+ .export-btn {
111
+ background: white !important;
112
+ color: #374151 !important;
113
+ border: 1px solid #d1d5db !important;
114
+ }
115
+
116
+ .export-btn:hover {
117
+ border-color: #9ca3af !important;
118
+ }
119
+
120
+ /* Action buttons */
121
+ .action-btn {
122
+ transition: all 0.2s ease !important;
123
+ border-radius: 8px !important;
124
+ min-width: 70px !important;
125
+ height: 32px !important;
126
+ padding: 0 12px !important;
127
+ font-size: 13px !important;
128
+ font-weight: 500 !important;
129
+ }
130
+
131
+ .action-btn:hover {
132
+ transform: translateY(-1px);
133
+ box-shadow: 0 3px 8px rgba(0, 0, 0, 0.12);
134
+ }
135
+
136
+ .btn-primary {
137
+ background: #3b82f6 !important;
138
+ color: white !important;
139
+ border: none !important;
140
+ }
141
+
142
+ .btn-secondary {
143
+ background: white !important;
144
+ color: #374151 !important;
145
+ border: 1px solid #d1d5db !important;
146
+ }
147
+
148
+ .btn-secondary:hover {
149
+ border-color: #9ca3af !important;
150
+ }
151
+
152
+ /* Delete button */
153
+ .btn-delete {
154
+ background: white !important;
155
+ border: none !important;
156
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1) !important;
157
+ color: #6b7280 !important;
158
+ border-radius: 50% !important;
159
+ width: 36px !important;
160
+ height: 36px !important;
161
+ min-width: 36px !important;
162
+ transition: all 0.2s ease !important;
163
+ }
164
+
165
+ .btn-delete:hover {
166
+ background: #f3f4f6 !important;
167
+ color: #374151 !important;
168
+ box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15) !important;
169
+ }
170
+
171
+ .btn-delete .q-btn__content {
172
+ min-width: unset !important;
173
+ width: 100% !important;
174
+ padding: 0 !important;
175
+ }
176
+
177
+ .press-button {
178
+ background: transparent !important;
179
+ box-shadow: none !important;
180
+ }
181
+
182
+ .press-button:active {
183
+ transform: scale(0.95);
184
+ }
185
+
186
+ /* Button layout */
187
+ .button-layout {
188
+ display: flex !important;
189
+ justify-content: space-between !important;
190
+ align-items: center !important;
191
+ width: 100% !important;
192
+ margin-top: auto !important;
193
+ gap: 16px !important;
194
+ }
195
+
196
+ .right-buttons {
197
+ display: flex !important;
198
+ gap: 12px !important;
199
+ }
200
+
201
+ /* ==================== Status Badges ==================== */
202
+
203
+ .status-badge {
204
+ padding: 4px 8px;
205
+ border-radius: 12px;
206
+ font-size: 0.75rem;
207
+ font-weight: 500;
208
+ display: inline-block;
209
+ }
210
+
211
+ .status-completed {
212
+ background: #dcfce7;
213
+ color: #166534;
214
+ }
215
+
216
+ .status-in-progress {
217
+ background: #fef3c7;
218
+ color: #92400e;
219
+ }
220
+
221
+ .status-cancelled {
222
+ background: #fee2e2;
223
+ color: #991b1b;
224
+ }
225
+
226
+ /* ==================== Empty State ==================== */
227
+
228
+ .empty-state {
229
+ background: transparent;
230
+ border: none;
231
+ padding: 80px 40px;
232
+ text-align: center;
233
+ display: flex;
234
+ flex-direction: column;
235
+ align-items: center;
236
+ }
237
+
238
+ .empty-content {
239
+ display: flex !important;
240
+ flex-direction: column !important;
241
+ align-items: center !important;
242
+ justify-content: center !important;
243
+ text-align: center !important;
244
+ width: 100% !important;
245
+ height: 100% !important;
246
+ }
247
+
248
+ /* Empty state button */
249
+ .empty-state .q-btn {
250
+ border-radius: 12px !important;
251
+ font-size: 16px !important;
252
+ font-weight: 500 !important;
253
+ letter-spacing: 0.02em !important;
254
+ transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1) !important;
255
+ min-width: 140px !important;
256
+ height: 48px !important;
257
+ text-transform: none !important;
258
+ }
259
+
260
+ .empty-state .q-btn:hover {
261
+ transform: translateY(-2px);
262
+ box-shadow: 0 8px 20px rgba(25, 118, 210, 0.25) !important;
263
+ }
264
+
265
+ .empty-state .q-btn:active {
266
+ transform: translateY(0);
267
+ }
268
+
269
+ /* ==================== Dialog ==================== */
270
+
271
+ .dialog-card {
272
+ background: white !important;
273
+ border-radius: 16px !important;
274
+ box-shadow: 0 10px 40px rgba(0, 0, 0, 0.15) !important;
275
+ }
276
+
277
+ /* Info box */
278
+ .info-box {
279
+ background: #f9fafb;
280
+ border: 1px solid #e5e7eb;
281
+ border-radius: 12px;
282
+ padding: 20px;
283
+ }
284
+
285
+ .export-summary-box {
286
+ background: white;
287
+ border: 1px solid #e5e7eb;
288
+ border-radius: 12px;
289
+ padding: 16px;
290
+ }
291
+
292
+ .divider {
293
+ height: 1px;
294
+ background: rgba(0, 0, 0, 0.08);
295
+ margin: 12px 0;
296
+ }
297
+
298
+ /* Info field */
299
+ .info-field {
300
+ padding: 12px 0;
301
+ border-bottom: 1px solid rgba(0, 0, 0, 0.06);
302
+ }
303
+
304
+ /* ==================== Responsive ==================== */
305
+
306
+ @media (max-width: 1400px) {
307
+ .bg-white {
308
+ padding: 40px 60px !important;
309
+ }
310
+
311
+ .px-6 {
312
+ padding-left: 2.5rem !important;
313
+ padding-right: 2.5rem !important;
314
+ }
315
+ }
316
+
317
+ @media (max-width: 1024px) {
318
+ .bg-white {
319
+ padding: 30px 40px !important;
320
+ }
321
+
322
+ .px-6 {
323
+ padding-left: 2rem !important;
324
+ padding-right: 2rem !important;
325
+ }
326
+ }
327
+
328
+ /* ==================== WiFi Indicator ==================== */
329
+
330
+ .wifi-indicator-btn {
331
+ border-radius: 50% !important;
332
+ width: 44px !important;
333
+ height: 44px !important;
334
+ min-width: 44px !important;
335
+ background: transparent !important;
336
+ transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1) !important;
337
+ }
338
+
339
+ .wifi-indicator-btn:hover {
340
+ background: rgba(0, 0, 0, 0.04) !important;
341
+ }
342
+
343
+ .wifi-indicator-btn:active {
344
+ background: rgba(0, 0, 0, 0.08) !important;
345
+ transform: scale(0.95);
346
+ }
347
+
348
+ /* WiFi Dialog */
349
+ .wifi-dialog-card {
350
+ border-radius: 20px;
351
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.15), 0 8px 24px rgba(0, 0, 0, 0.1);
352
+ animation: slideUp 0.3s cubic-bezier(0.4, 0, 0.2, 1);
353
+ }
354
+
355
+ /* Networks container - scrollable but hide scrollbar */
356
+ .wifi-dialog-card .q-scrollarea__thumb {
357
+ display: none !important;
358
+ }
359
+
360
+ /* Alternative: hide native scrollbar */
361
+ .wifi-dialog-card *::-webkit-scrollbar {
362
+ display: none !important;
363
+ }
364
+
365
+ .wifi-dialog-card * {
366
+ -ms-overflow-style: none !important;
367
+ scrollbar-width: none !important;
368
+ }
369
+
370
+ /* Current Connection Card - Clean, no border, no background */
371
+ .wifi-current-card {
372
+ background: transparent !important;
373
+ border: none !important;
374
+ border-radius: 12px !important;
375
+ padding: 16px !important;
376
+ margin-bottom: 16px !important;
377
+ box-shadow: none !important;
378
+ transition: all 0.2s ease !important;
379
+ }
380
+
381
+ .wifi-current-card:hover {
382
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08) !important;
383
+ }
384
+
385
+ /* Network Cards - No border, shadow on hover */
386
+ .wifi-network-card {
387
+ border-radius: 12px !important;
388
+ padding: 14px !important;
389
+ transition: all 0.2s ease !important;
390
+ background: white !important;
391
+ border: none !important;
392
+ box-shadow: none !important;
393
+ cursor: pointer !important;
394
+ }
395
+
396
+ .wifi-network-card:hover {
397
+ box-shadow: 0 4px 12px rgba(102, 126, 234, 0.15) !important;
398
+ transform: translateY(-2px);
399
+ }
400
+
401
+ /* Connect Form - Compact, single row height */
402
+ .wifi-connect-form {
403
+ background: rgba(248, 249, 250, 0.6) !important;
404
+ border: none !important;
405
+ border-radius: 12px !important;
406
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06) !important;
407
+ overflow: hidden !important;
408
+ transition: max-height 0.4s cubic-bezier(0.4, 0, 0.2, 1),
409
+ opacity 0.3s ease,
410
+ padding 0.3s ease !important;
411
+ }
412
+
413
+ /* Input row - force input to take remaining space */
414
+ .wifi-connect-form .q-field {
415
+ flex: 1 !important;
416
+ min-width: 0 !important;
417
+ }
418
+
419
+ /* Connect Button - Fixed width, always on the right */
420
+ .wifi-connect-btn {
421
+ border-radius: 50% !important;
422
+ width: 40px !important;
423
+ height: 40px !important;
424
+ min-width: 40px !important;
425
+ max-width: 40px !important;
426
+ transition: all 0.2s ease !important;
427
+ background: white !important;
428
+ border: none !important;
429
+ color: #3b82f6 !important;
430
+ box-shadow: none !important;
431
+ flex-shrink: 0 ! important;
432
+ margin-left: auto !important;
433
+ }
434
+
435
+ .wifi-connect-btn:hover {
436
+ background: rgba(59, 130, 246, 0.1) !important;
437
+ transform: scale(1.1);
438
+ }
439
+
440
+ .wifi-connect-btn:active {
441
+ transform: scale(0.95);
442
+ }
443
+
444
+ /* Scan Button */
445
+ .wifi-scan-btn {
446
+ border-radius: 50% !important;
447
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important;
448
+ }
449
+
450
+ .wifi-scan-btn:hover {
451
+ transform: rotate(180deg) scale(1.1);
452
+ }
@@ -0,0 +1,22 @@
1
+ .header-glass {
2
+ backdrop-filter: blur(80px) saturate(200%) !important;
3
+ -webkit-backdrop-filter: blur(80px) saturate(200%) !important;
4
+ background: rgba(255, 255, 255, 0.5) !important;
5
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.03) !important;
6
+ padding-top: 1.5rem !important;
7
+ padding-bottom: 1.5rem !important;
8
+ margin-top: 1.5rem !important;
9
+ min-height: 60px !important;
10
+ position: fixed !important;
11
+ top: 0 !important;
12
+ left: 0 !important;
13
+ right: 0 !important;
14
+ z-index: 1000 !important;
15
+ }
16
+
17
+ .glass-card {
18
+ border-radius: 24px !important;
19
+ background: transparent !important;
20
+ box-shadow: none !important;
21
+ border: 1px solid rgba(0, 0, 0, 0.08) !important;
22
+ }