1use dioxus::prelude::*;
2use dioxus_icons::lucide::{Mic, Pause, Play, Square, X};
3
4use crate::AudioErrorKind;
5use crate::recorder::{AudioRecorder, RecorderStatus};
6
7#[derive(Clone, Debug, PartialEq, Eq)]
9pub struct RecorderAnnouncementLabels {
10 pub idle: String,
11 pub preparing: String,
12 pub recording: String,
13 pub paused: String,
14 pub stopping: String,
15 pub failed: String,
16}
17
18impl Default for RecorderAnnouncementLabels {
19 fn default() -> Self {
20 Self {
21 idle: "Recorder idle".to_string(),
22 preparing: "Preparing recording".to_string(),
23 recording: "Recording".to_string(),
24 paused: "Recording paused".to_string(),
25 stopping: "Finishing recording".to_string(),
26 failed: "Recording failed".to_string(),
27 }
28 }
29}
30
31#[component]
33pub fn RecorderStatusAnnouncer(
34 recorder: AudioRecorder,
35 #[props(default)] labels: RecorderAnnouncementLabels,
36) -> Element {
37 let status = recorder.status()();
38 let message = match status {
39 RecorderStatus::Idle => labels.idle.as_str(),
40 RecorderStatus::Preparing => labels.preparing.as_str(),
41 RecorderStatus::Recording => labels.recording.as_str(),
42 RecorderStatus::Paused => labels.paused.as_str(),
43 RecorderStatus::Stopping => labels.stopping.as_str(),
44 RecorderStatus::Failed(_) => labels.failed.as_str(),
45 };
46
47 rsx! {
48 div {
49 class: "dioxus-audio dioxus-audio__status-announcer",
50 role: "status",
51 aria_live: "polite",
52 aria_atomic: "true",
53 "{message}"
54 }
55 }
56}
57
58#[component]
60pub fn RecorderStartButton(
61 recorder: AudioRecorder,
62 #[props(default = "Start recording".to_string())] label: String,
63 #[props(default = "Clear recorded audio before starting".to_string())] completed_label: String,
64) -> Element {
65 let status = recorder.status()();
66 let has_completed = recorder.completed().read().is_some();
67 let disabled = has_completed || !can_start(&status);
68 let aria_label = if has_completed {
69 completed_label
70 } else {
71 label
72 };
73
74 rsx! {
75 button {
76 class: "dioxus-audio dioxus-audio__record-action dioxus-audio__record-action--record",
77 r#type: "button",
78 aria_label,
79 disabled,
80 onclick: move |_| { let _ = recorder.start(); },
81 Mic { size: 24 }
82 }
83 }
84}
85
86#[component]
88pub fn RecorderCancelButton(
89 recorder: AudioRecorder,
90 #[props(default = "Cancel recording preparation".to_string())] preparing_label: String,
91 #[props(default = "Cancel recording".to_string())] recording_label: String,
92 #[props(default)] on_cancelled: Option<EventHandler<()>>,
93) -> Element {
94 let status = recorder.status()();
95 let preparing = matches!(status, RecorderStatus::Preparing);
96 let disabled = !can_cancel(&status);
97 let aria_label = if preparing {
98 preparing_label
99 } else {
100 recording_label
101 };
102
103 rsx! {
104 button {
105 class: "dioxus-audio dioxus-audio__record-action",
106 r#type: "button",
107 aria_label,
108 disabled,
109 onclick: move |_| {
110 if recorder.cancel().is_ok()
111 && let Some(on_cancelled) = on_cancelled
112 {
113 on_cancelled.call(());
114 }
115 },
116 X { size: 20 }
117 }
118 }
119}
120
121#[component]
123pub fn RecorderPauseResumeButton(
124 recorder: AudioRecorder,
125 #[props(default = "Pause".to_string())] pause_label: String,
126 #[props(default = "Resume".to_string())] resume_label: String,
127) -> Element {
128 let status = recorder.status()();
129 let paused = matches!(status, RecorderStatus::Paused);
130 let disabled = !can_control_transport(&status);
131 let aria_label = if paused { resume_label } else { pause_label };
132
133 rsx! {
134 button {
135 class: "dioxus-audio dioxus-audio__record-action",
136 r#type: "button",
137 aria_label,
138 disabled,
139 onclick: move |_| {
140 if paused {
141 let _ = recorder.resume();
142 } else {
143 let _ = recorder.pause();
144 }
145 },
146 if paused {
147 Play { size: 20 }
148 } else {
149 Pause { size: 20 }
150 }
151 }
152 }
153}
154
155#[component]
157pub fn RecorderStopButton(
158 recorder: AudioRecorder,
159 #[props(default = "Stop recording".to_string())] stop_label: String,
160 #[props(default = "Finishing recording".to_string())] stopping_label: String,
161) -> Element {
162 let status = recorder.status()();
163 let stopping = matches!(status, RecorderStatus::Stopping);
164 let disabled = !can_control_transport(&status);
165 let aria_label = if stopping { stopping_label } else { stop_label };
166
167 rsx! {
168 button {
169 class: "dioxus-audio dioxus-audio__record-action dioxus-audio__record-action--stop",
170 r#type: "button",
171 aria_label,
172 aria_busy: stopping,
173 disabled,
174 onclick: move |_| { let _ = recorder.stop(); },
175 Square { size: 22 }
176 }
177 }
178}
179
180#[component]
182pub fn RecorderClearButton(
183 recorder: AudioRecorder,
184 #[props(default = "Clear recorded audio".to_string())] label: String,
185) -> Element {
186 let disabled = recorder.completed().read().is_none();
187
188 rsx! {
189 button {
190 class: "dioxus-audio dioxus-audio__record-action",
191 r#type: "button",
192 aria_label: label,
193 disabled,
194 onclick: move |_| recorder.clear_completed(),
195 X { size: 20 }
196 }
197 }
198}
199
200fn can_start(status: &RecorderStatus) -> bool {
201 match status {
202 RecorderStatus::Idle => true,
203 RecorderStatus::Failed(error) => !matches!(
204 error.kind(),
205 AudioErrorKind::UnsupportedPlatform | AudioErrorKind::InvalidConfiguration
206 ),
207 RecorderStatus::Preparing
208 | RecorderStatus::Recording
209 | RecorderStatus::Paused
210 | RecorderStatus::Stopping => false,
211 }
212}
213
214fn can_cancel(status: &RecorderStatus) -> bool {
215 matches!(
216 status,
217 RecorderStatus::Preparing | RecorderStatus::Recording | RecorderStatus::Paused
218 )
219}
220
221fn can_control_transport(status: &RecorderStatus) -> bool {
222 matches!(status, RecorderStatus::Recording | RecorderStatus::Paused)
223}
224
225#[component]
226pub fn RecorderControls(
227 recorder: AudioRecorder,
228 #[props(default)] on_cancelled: Option<EventHandler<()>>,
229) -> Element {
230 let status = recorder.status()();
231 let state_name = recorder_state_name(&status);
232
233 rsx! {
234 div {
235 class: "dioxus-audio dioxus-audio__recorder-controls",
236 "data-state": state_name,
237 match status {
238 RecorderStatus::Idle | RecorderStatus::Failed(_) => rsx! {
239 RecorderStartButton { recorder }
240 },
241 RecorderStatus::Preparing => rsx! {
242 RecorderCancelButton { recorder, on_cancelled }
243 },
244 RecorderStatus::Recording | RecorderStatus::Paused => rsx! {
245 RecorderCancelButton { recorder, on_cancelled }
246 RecorderPauseResumeButton { recorder }
247 RecorderStopButton { recorder }
248 },
249 RecorderStatus::Stopping => rsx! {
250 RecorderStopButton { recorder }
251 },
252 }
253 }
254 }
255}
256
257fn recorder_state_name(status: &RecorderStatus) -> &'static str {
258 match status {
259 RecorderStatus::Idle => "idle",
260 RecorderStatus::Preparing => "preparing",
261 RecorderStatus::Recording => "recording",
262 RecorderStatus::Paused => "paused",
263 RecorderStatus::Stopping => "stopping",
264 RecorderStatus::Failed(_) => "error",
265 }
266}