ui_storybook/components/recorder/
device_picker.rs1use leptos::prelude::*;
6
7use crate::components::menus::{
8 MenuFooter, MenuList, MenuSection, PopoverPlacement, PopoverSurface,
9};
10use crate::components::primitives::{Badge, BadgeKind, Meter};
11
12use super::capture_source_row::CaptureSourceKind;
13
14#[derive(Debug, Clone, PartialEq, Eq)]
17pub struct DeviceThumb {
18 pub background: String,
20 pub glyph: String,
22}
23
24#[derive(Debug, Clone, PartialEq)]
26pub struct DeviceOptionView {
27 pub id: String,
29 pub name: String,
31 pub detail: String,
33 pub badge: Option<String>,
35 pub selected: bool,
37 pub level: Option<f32>,
39 pub thumbnail: Option<DeviceThumb>,
41}
42
43#[derive(Clone, Copy, Debug, PartialEq, Eq)]
45pub enum DevicePickerState {
46 Populated,
48 Empty,
50 PermissionNeeded,
52}
53
54#[component]
55pub fn DevicePickerMenu(
56 kind: CaptureSourceKind,
58 devices: Vec<DeviceOptionView>,
61 #[prop(optional, default = DevicePickerState::Populated)]
63 state: DevicePickerState,
64) -> impl IntoView {
65 let title = match kind {
66 CaptureSourceKind::Camera => "Cameras",
67 CaptureSourceKind::Microphone => "Microphones",
68 };
69 let footer_action = match kind {
70 CaptureSourceKind::Camera => "Connect new camera",
71 CaptureSourceKind::Microphone => "Pair Bluetooth microphone",
72 };
73 let rows: Vec<_> = if matches!(state, DevicePickerState::Populated) {
74 devices.into_iter().map(render_row).collect()
75 } else {
76 Vec::new()
77 };
78 let body = match state {
79 DevicePickerState::Populated => view! {
80 <MenuList label=title.to_string()>
81 <MenuSection heading="Available".to_string()>{rows}</MenuSection>
82 </MenuList>
83 }
84 .into_any(),
85 DevicePickerState::Empty => view! {
86 <div class="device-picker-state">
87 <div class="device-picker-state-glyph" aria-hidden="true">"⚠"</div>
88 <div class="device-picker-state-title">"No devices detected"</div>
89 <div class="device-picker-state-subtitle">
90 "Plug in or pair a device, then re-open this menu."
91 </div>
92 </div>
93 }
94 .into_any(),
95 DevicePickerState::PermissionNeeded => view! {
96 <div class="device-picker-state">
97 <div class="device-picker-state-glyph device-picker-state-glyph-warn" aria-hidden="true">"⚠"</div>
98 <div class="device-picker-state-title">
99 {match kind {
100 CaptureSourceKind::Camera => "Camera access required",
101 CaptureSourceKind::Microphone => "Microphone access required",
102 }}
103 </div>
104 <div class="device-picker-state-subtitle">
105 "Grant access in System Settings → Privacy & Security, then re-open this menu."
106 </div>
107 </div>
108 }
109 .into_any(),
110 };
111 view! {
112 <PopoverSurface
113 placement=PopoverPlacement::BottomLeft
114 width_px=320_u16
115 title=title.to_string()
116 footer=ToChildren::to_children(move || view! {
117 <MenuFooter>
118 <span style="font-size:11px;color:var(--text-tertiary)">"Devices update live."</span>
119 <button class="btn btn-default btn-sm">{footer_action}</button>
120 </MenuFooter>
121 })
122 >
123 {body}
124 </PopoverSurface>
125 }
126}
127
128fn render_row(opt: DeviceOptionView) -> impl IntoView {
129 let mut row_class = String::from("device-picker-row");
130 if opt.selected {
131 row_class.push_str(" device-picker-row-selected");
132 }
133 let badge = opt
134 .badge
135 .map(|b| view! { <Badge kind=BadgeKind::Plan>{b}</Badge> });
136 let level_view = opt.level.map(|l| view! { <Meter level=l bar_count=10 /> });
137 let thumb_view = opt.thumbnail.as_ref().map(|t| {
138 let style = format!("background:{}", t.background);
139 let glyph = t.glyph.clone();
140 view! {
141 <span class="device-picker-thumb" style=style aria-hidden="true">
142 <span class="device-picker-thumb-glyph">{glyph}</span>
143 </span>
144 }
145 });
146 view! {
147 <li class="device-picker-row-item" role="none">
148 <button class=row_class role="menuitem" aria-pressed=opt.selected>
149 {thumb_view}
150 <span class="device-picker-row-text">
151 <span class="device-picker-row-name">{opt.name}</span>
152 <span class="device-picker-row-detail">{opt.detail}</span>
153 </span>
154 {badge}
155 {level_view.map(|l| view! { <span class="device-picker-row-meter">{l}</span> })}
156 {opt.selected.then(|| view! {
157 <span class="device-picker-row-check" aria-hidden="true">"✓"</span>
158 })}
159 </button>
160 </li>
161 }
162}