1use leptos::html;
4use leptos::prelude::*;
5use ui_storybook::components::{
6 DropZone, DropZoneState, PlayState, PlayerControls, RecordingState, RecordingToolbar,
7 StatusBar, StatusKind,
8};
9use wasm_bindgen::JsCast;
10use wasm_bindgen::prelude::*;
11use web_sys::{CustomEvent, HtmlVideoElement};
12
13use crate::player_ipc::{
14 self, PlayerStatus, SessionState, convert_file_src, install_player_status_listener,
15 screen_open, screen_pause, screen_play,
16};
17
18#[component]
21pub fn App() -> impl IntoView {
22 let (loaded, set_loaded) = signal::<Option<String>>(None);
29
30 let (player_status, set_player_status) = signal::<PlayerStatus>(PlayerStatus::default());
33
34 let (is_dragging, set_dragging) = signal::<bool>(false);
38
39 install_file_drop_listener(set_loaded);
40 install_player_status_listener(set_player_status);
41 install_drag_state_listeners(set_dragging);
42
43 let on_demo_load = move |_| {
44 set_loaded.set(Some("Recording 01.mp4 (demo)".into()));
45 };
46
47 view! {
48 <div class="shell">
49 <RecordingToolbar
50 state=RecordingState::Idle
51 elapsed_seconds=0.0
52 source="Built-in Display"
53 />
54
55 <main class="shell-main">
56 <Show
57 when=move || loaded.get().is_some()
58 fallback=move || {
59 let drop_state = move || if is_dragging.get() {
60 DropZoneState::Active
61 } else {
62 DropZoneState::Idle
63 };
64 view! {
65 <div class="shell-drop-wrap" on:click=on_demo_load>
66 {move || view! {
67 <DropZone
68 state=drop_state()
69 hint="Drop an MP4 here, or click for a demo"
70 />
71 }}
72 </div>
73 }
74 }
75 >
76 <PlayerView loaded=loaded player_status=player_status />
77 </Show>
78 </main>
79
80 <StatusBar
81 fps=60.0
82 encoder="H.264 · idle"
83 file_bytes=0
84 kind=StatusKind::Ready
85 />
86 </div>
87 }
88}
89
90#[component]
91fn PlayerView(
92 loaded: ReadSignal<Option<String>>,
93 player_status: ReadSignal<PlayerStatus>,
94) -> impl IntoView {
95 let video_ref: NodeRef<html::Video> = NodeRef::new();
96
97 let video_src = move || -> Option<String> {
101 let path = loaded.get()?;
102 convert_file_src(&path)
103 };
104 let path_label = move || loaded.get().unwrap_or_default();
105
106 Effect::new(move |_| {
110 let status = player_status.get();
111 let Some(video) = video_ref.get() else {
112 return;
113 };
114 let video: HtmlVideoElement = video;
115 let should_play = status.state == SessionState::Playing;
116 let is_paused = video.paused();
117 if should_play && is_paused {
118 let _ = video.play();
119 } else if !should_play && !is_paused {
120 let _ = video.pause();
121 }
122 });
123
124 let controls = move || {
125 let status = player_status.get();
126 let play_state = match status.state {
127 SessionState::Playing => PlayState::Playing,
128 _ => PlayState::Paused,
129 };
130 let position = player_ipc::position(&status);
131 let duration = player_ipc::duration_seconds(&status);
132 let toggle = Callback::new(move |()| {
133 let cur = player_status.get_untracked();
134 let want_playing = cur.state != SessionState::Playing;
135
136 if let Some(video) = video_ref.get_untracked() {
140 let video: HtmlVideoElement = video;
141 let _ = if want_playing {
142 let _ = video.play();
143 Ok::<(), JsValue>(())
144 } else {
145 video.pause()
146 };
147 }
148
149 let _ = if want_playing {
152 screen_play()
153 } else {
154 screen_pause()
155 };
156 });
157 view! {
158 <PlayerControls
159 state=play_state
160 position=position
161 duration_seconds=duration
162 on_toggle=toggle
163 />
164 }
165 };
166
167 view! {
168 <div class="player-view">
169 <div class="player-surface">
170 {move || video_src().map(|src| view! {
171 <video
172 node_ref=video_ref
173 class="player-video"
174 src=src
175 preload="auto"
176 />
177 })}
178 <div class="player-surface-label">
179 "Preview surface · " {path_label}
180 </div>
181 </div>
182 {controls}
183 </div>
184 }
185}
186
187fn install_file_drop_listener(set_loaded: WriteSignal<Option<String>>) {
198 let Some(window) = web_sys::window() else {
199 return;
200 };
201 let closure = Closure::wrap(Box::new(move |event: web_sys::Event| {
202 if let Ok(ce) = event.dyn_into::<CustomEvent>()
203 && let Some(path) = ce.detail().as_string()
204 {
205 let _ = screen_open(&path);
209 set_loaded.set(Some(path));
210 }
211 }) as Box<dyn FnMut(_)>);
212 let _ =
213 window.add_event_listener_with_callback("file-dropped", closure.as_ref().unchecked_ref());
214 closure.forget();
215}
216
217fn install_drag_state_listeners(set_dragging: WriteSignal<bool>) {
222 let Some(window) = web_sys::window() else {
223 return;
224 };
225 let enter = Closure::wrap(Box::new(move |_: web_sys::Event| {
226 set_dragging.set(true);
227 }) as Box<dyn FnMut(_)>);
228 let _ =
229 window.add_event_listener_with_callback("file-drag-enter", enter.as_ref().unchecked_ref());
230 enter.forget();
231
232 let leave = Closure::wrap(Box::new(move |_: web_sys::Event| {
233 set_dragging.set(false);
234 }) as Box<dyn FnMut(_)>);
235 let _ =
236 window.add_event_listener_with_callback("file-drag-leave", leave.as_ref().unchecked_ref());
237 leave.forget();
238}