reflex 0.8.12a1__py3-none-any.whl → 0.8.13a1__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.

Potentially problematic release.


This version of reflex might be problematic. Click here for more details.

@@ -531,6 +531,7 @@ export const connect = async (
531
531
  ) => {
532
532
  // Get backend URL object from the endpoint.
533
533
  const endpoint = getBackendURL(EVENTURL);
534
+ const on_hydrated_queue = [];
534
535
 
535
536
  // Create the socket.
536
537
  socket.current = io(endpoint.href, {
@@ -552,7 +553,17 @@ export const connect = async (
552
553
 
553
554
  function checkVisibility() {
554
555
  if (document.visibilityState === "visible") {
555
- if (!socket.current.connected) {
556
+ if (!socket.current) {
557
+ connect(
558
+ socket,
559
+ dispatch,
560
+ transports,
561
+ setConnectErrors,
562
+ client_storage,
563
+ navigate,
564
+ params,
565
+ );
566
+ } else if (!socket.current.connected) {
556
567
  console.log("Socket is disconnected, attempting to reconnect ");
557
568
  socket.current.connect();
558
569
  } else {
@@ -593,6 +604,7 @@ export const connect = async (
593
604
 
594
605
  // When the socket disconnects reset the event_processing flag
595
606
  socket.current.on("disconnect", () => {
607
+ socket.current = null; // allow reconnect to occur automatically
596
608
  event_processing = false;
597
609
  window.removeEventListener("unload", disconnectTrigger);
598
610
  window.removeEventListener("beforeunload", disconnectTrigger);
@@ -603,6 +615,14 @@ export const connect = async (
603
615
  socket.current.on("event", async (update) => {
604
616
  for (const substate in update.delta) {
605
617
  dispatch[substate](update.delta[substate]);
618
+ // handle events waiting for `is_hydrated`
619
+ if (
620
+ substate === state_name &&
621
+ update.delta[substate]?.is_hydrated_rx_state_
622
+ ) {
623
+ queueEvents(on_hydrated_queue, socket, false, navigate, params);
624
+ on_hydrated_queue.length = 0;
625
+ }
606
626
  }
607
627
  applyClientStorageDelta(client_storage, update.delta);
608
628
  event_processing = !update.final;
@@ -612,7 +632,8 @@ export const connect = async (
612
632
  });
613
633
  socket.current.on("reload", async (event) => {
614
634
  event_processing = false;
615
- queueEvents([...initialEvents(), event], socket, true, navigate, params);
635
+ on_hydrated_queue.push(event);
636
+ queueEvents(initialEvents(), socket, true, navigate, params);
616
637
  });
617
638
  socket.current.on("new_token", async (new_token) => {
618
639
  token = new_token;
@@ -774,10 +795,32 @@ export const useEventLoop = (
774
795
  }
775
796
  }, [paramsR]);
776
797
 
798
+ const ensureSocketConnected = useCallback(async () => {
799
+ // only use websockets if state is present and backend is not disabled (reflex cloud).
800
+ if (
801
+ Object.keys(initialState).length > 1 &&
802
+ !isBackendDisabled() &&
803
+ !socket.current
804
+ ) {
805
+ // Initialize the websocket connection.
806
+ await connect(
807
+ socket,
808
+ dispatch,
809
+ ["websocket"],
810
+ setConnectErrors,
811
+ client_storage,
812
+ navigate,
813
+ () => params.current,
814
+ );
815
+ }
816
+ }, [socket, dispatch, setConnectErrors, client_storage, navigate, params]);
817
+
777
818
  // Function to add new events to the event queue.
778
819
  const addEvents = useCallback((events, args, event_actions) => {
779
820
  const _events = events.filter((e) => e !== undefined && e !== null);
780
821
 
822
+ ensureSocketConnected();
823
+
781
824
  if (!(args instanceof Array)) {
782
825
  args = [args];
783
826
  }
@@ -870,21 +913,8 @@ export const useEventLoop = (
870
913
 
871
914
  // Handle socket connect/disconnect.
872
915
  useEffect(() => {
873
- // only use websockets if state is present and backend is not disabled (reflex cloud).
874
- if (Object.keys(initialState).length > 1 && !isBackendDisabled()) {
875
- // Initialize the websocket connection.
876
- if (!socket.current) {
877
- connect(
878
- socket,
879
- dispatch,
880
- ["websocket"],
881
- setConnectErrors,
882
- client_storage,
883
- navigate,
884
- () => params.current,
885
- );
886
- }
887
- }
916
+ // Initialize the websocket connection.
917
+ ensureSocketConnected();
888
918
 
889
919
  // Cleanup function.
890
920
  return () => {
@@ -903,6 +933,7 @@ export const useEventLoop = (
903
933
  (async () => {
904
934
  // Process all outstanding events.
905
935
  while (event_queue.length > 0 && !event_processing) {
936
+ await ensureSocketConnected();
906
937
  await processEvent(socket.current, navigate, () => params.current);
907
938
  }
908
939
  })();
reflex/app.py CHANGED
@@ -97,6 +97,7 @@ from reflex.state import (
97
97
  State,
98
98
  StateManager,
99
99
  StateUpdate,
100
+ _split_substate_key,
100
101
  _substate_key,
101
102
  all_base_state_classes,
102
103
  code_uses_state_contexts,
@@ -112,7 +113,12 @@ from reflex.utils import (
112
113
  prerequisites,
113
114
  types,
114
115
  )
115
- from reflex.utils.exec import get_compile_context, is_prod_mode, is_testing_env
116
+ from reflex.utils.exec import (
117
+ get_compile_context,
118
+ is_prod_mode,
119
+ is_testing_env,
120
+ should_prerender_routes,
121
+ )
116
122
  from reflex.utils.imports import ImportVar
117
123
  from reflex.utils.token_manager import TokenManager
118
124
  from reflex.utils.types import ASGIApp, Message, Receive, Scope, Send
@@ -606,7 +612,7 @@ class App(MiddlewareMixin, LifespanMixin):
606
612
  """
607
613
  from reflex.vars.base import GLOBAL_CACHE
608
614
 
609
- self._compile(prerender_routes=is_prod_mode())
615
+ self._compile(prerender_routes=should_prerender_routes())
610
616
 
611
617
  config = get_config()
612
618
 
@@ -1559,7 +1565,7 @@ class App(MiddlewareMixin, LifespanMixin):
1559
1565
  state._clean()
1560
1566
  await self.event_namespace.emit_update(
1561
1567
  update=StateUpdate(delta=delta),
1562
- sid=state.router.session.session_id,
1568
+ token=token,
1563
1569
  )
1564
1570
 
1565
1571
  def _process_background(
@@ -1599,7 +1605,7 @@ class App(MiddlewareMixin, LifespanMixin):
1599
1605
  # Send the update to the client.
1600
1606
  await self.event_namespace.emit_update(
1601
1607
  update=update,
1602
- sid=state.router.session.session_id,
1608
+ token=event.token,
1603
1609
  )
1604
1610
 
1605
1611
  task = asyncio.create_task(
@@ -2061,20 +2067,19 @@ class EventNamespace(AsyncNamespace):
2061
2067
  and console.error(f"Token cleanup error: {t.exception()}")
2062
2068
  )
2063
2069
 
2064
- async def emit_update(self, update: StateUpdate, sid: str) -> None:
2070
+ async def emit_update(self, update: StateUpdate, token: str) -> None:
2065
2071
  """Emit an update to the client.
2066
2072
 
2067
2073
  Args:
2068
2074
  update: The state update to send.
2069
- sid: The Socket.IO session id.
2075
+ token: The client token (tab) associated with the event.
2070
2076
  """
2071
- if not sid:
2077
+ client_token, _ = _split_substate_key(token)
2078
+ sid = self.token_to_sid.get(client_token)
2079
+ if sid is None:
2072
2080
  # If the sid is None, we are not connected to a client. Prevent sending
2073
2081
  # updates to all clients.
2074
- return
2075
- token = self.sid_to_token.get(sid)
2076
- if token is None:
2077
- console.warn(f"Attempting to send delta to disconnected websocket {sid}")
2082
+ console.warn(f"Attempting to send delta to disconnected client {token!r}")
2078
2083
  return
2079
2084
  # Creating a task prevents the update from being blocked behind other coroutines.
2080
2085
  await asyncio.create_task(
@@ -2165,7 +2170,7 @@ class EventNamespace(AsyncNamespace):
2165
2170
  # Process the events.
2166
2171
  async for update in updates_gen:
2167
2172
  # Emit the update from processing the event.
2168
- await self.emit_update(update=update, sid=sid)
2173
+ await self.emit_update(update=update, token=event.token)
2169
2174
 
2170
2175
  async def on_ping(self, sid: str):
2171
2176
  """Event for testing the API endpoint.
@@ -60,6 +60,17 @@ class LifespanMixin(AppMixin):
60
60
  for task in running_tasks:
61
61
  console.debug(f"Canceling lifespan task: {task}")
62
62
  task.cancel(msg="lifespan_cleanup")
63
+ # Disassociate sid / token pairings so they can be reconnected properly.
64
+ try:
65
+ event_namespace = self.event_namespace # pyright: ignore[reportAttributeAccessIssue]
66
+ except AttributeError:
67
+ pass
68
+ else:
69
+ try:
70
+ if event_namespace:
71
+ await event_namespace._token_manager.disconnect_all()
72
+ except Exception as e:
73
+ console.error(f"Error during lifespan cleanup: {e}")
63
74
 
64
75
  def register_lifespan_task(self, task: Callable | asyncio.Task, **task_kwargs):
65
76
  """Register a task to run during the lifespan of the app.
@@ -621,7 +621,10 @@ def purge_web_pages_dir():
621
621
  return
622
622
 
623
623
  # Empty out the web pages directory.
624
- utils.empty_dir(get_web_dir() / constants.Dirs.PAGES, keep_files=["routes.js"])
624
+ utils.empty_dir(
625
+ get_web_dir() / constants.Dirs.PAGES,
626
+ keep_files=["routes.js", "entry.client.js"],
627
+ )
625
628
 
626
629
 
627
630
  if TYPE_CHECKING:
@@ -17,6 +17,7 @@ from reflex.components.component import (
17
17
  from reflex.components.core.cond import cond
18
18
  from reflex.components.el.elements.forms import Input
19
19
  from reflex.components.radix.themes.layout.box import Box
20
+ from reflex.components.sonner.toast import toast
20
21
  from reflex.constants import Dirs
21
22
  from reflex.constants.compiler import Hooks, Imports
22
23
  from reflex.environment import environment
@@ -36,7 +37,8 @@ from reflex.utils.imports import ImportVar
36
37
  from reflex.vars import VarData
37
38
  from reflex.vars.base import Var, get_unique_variable_name
38
39
  from reflex.vars.function import FunctionVar
39
- from reflex.vars.sequence import LiteralStringVar
40
+ from reflex.vars.object import ObjectVar
41
+ from reflex.vars.sequence import ArrayVar, LiteralStringVar
40
42
 
41
43
  DEFAULT_UPLOAD_ID: str = "default"
42
44
 
@@ -178,6 +180,34 @@ def _on_drop_spec(files: Var) -> tuple[Var[Any]]:
178
180
  return (files,)
179
181
 
180
182
 
183
+ def _default_drop_rejected(rejected_files: ArrayVar[list[dict[str, Any]]]) -> EventSpec:
184
+ """Event handler for showing a toast with rejected file info.
185
+
186
+ Args:
187
+ rejected_files: The files that were rejected.
188
+
189
+ Returns:
190
+ An event spec that shows a toast with the rejected file info when triggered.
191
+ """
192
+
193
+ def _format_rejected_file_record(rf: ObjectVar[dict[str, Any]]) -> str:
194
+ rf = rf.to(ObjectVar, dict[str, dict[str, Any]])
195
+ file = rf["file"].to(ObjectVar, dict[str, Any])
196
+ errors = rf["errors"].to(ArrayVar, list[dict[str, Any]])
197
+ return (
198
+ f"{file['path']}: {errors.foreach(lambda err: err['message']).join(', ')}"
199
+ )
200
+
201
+ return toast.error(
202
+ title="Files not Accepted",
203
+ description=rejected_files.to(ArrayVar)
204
+ .foreach(_format_rejected_file_record)
205
+ .join("\n\n"),
206
+ close_button=True,
207
+ style={"white_space": "pre-line"},
208
+ )
209
+
210
+
181
211
  class UploadFilesProvider(Component):
182
212
  """AppWrap component that provides a dict of selected files by ID via useContext."""
183
213
 
@@ -191,6 +221,9 @@ class GhostUpload(Fragment):
191
221
  # Fired when files are dropped.
192
222
  on_drop: EventHandler[_on_drop_spec]
193
223
 
224
+ # Fired when dropped files do not meet the specified criteria.
225
+ on_drop_rejected: EventHandler[_on_drop_spec]
226
+
194
227
 
195
228
  class Upload(MemoizationLeaf):
196
229
  """A file upload component."""
@@ -234,6 +267,9 @@ class Upload(MemoizationLeaf):
234
267
  # Fired when files are dropped.
235
268
  on_drop: EventHandler[_on_drop_spec]
236
269
 
270
+ # Fired when dropped files do not meet the specified criteria.
271
+ on_drop_rejected: EventHandler[_on_drop_spec]
272
+
237
273
  # Style rules to apply when actively dragging.
238
274
  drag_active_style: Style | None = field(default=None, is_javascript_property=False)
239
275
 
@@ -295,6 +331,10 @@ class Upload(MemoizationLeaf):
295
331
  on_drop[ix] = event
296
332
  upload_props["on_drop"] = on_drop
297
333
 
334
+ if upload_props.get("on_drop_rejected") is None:
335
+ # If on_drop_rejected is not provided, show an error toast.
336
+ upload_props["on_drop_rejected"] = _default_drop_rejected
337
+
298
338
  input_props_unique_name = get_unique_variable_name()
299
339
  root_props_unique_name = get_unique_variable_name()
300
340
  is_drag_active_unique_name = get_unique_variable_name()
@@ -313,22 +353,22 @@ class Upload(MemoizationLeaf):
313
353
  ),
314
354
  )
315
355
 
316
- event_var, callback_str = StatefulComponent._get_memoized_event_triggers(
317
- GhostUpload.create(on_drop=upload_props["on_drop"])
318
- )["on_drop"]
319
-
320
- upload_props["on_drop"] = event_var
356
+ event_triggers = StatefulComponent._get_memoized_event_triggers(
357
+ GhostUpload.create(
358
+ on_drop=upload_props["on_drop"],
359
+ on_drop_rejected=upload_props["on_drop_rejected"],
360
+ )
361
+ )
362
+ callback_hooks = []
363
+ for trigger_name, (event_var, callback_str) in event_triggers.items():
364
+ upload_props[trigger_name] = event_var
365
+ callback_hooks.append(callback_str)
321
366
 
322
367
  upload_props = {
323
368
  format.to_camel_case(key): value for key, value in upload_props.items()
324
369
  }
325
370
 
326
- use_dropzone_arguments = Var.create(
327
- {
328
- "onDrop": event_var,
329
- **upload_props,
330
- }
331
- )
371
+ use_dropzone_arguments = Var.create(upload_props)
332
372
 
333
373
  left_side = (
334
374
  "const { "
@@ -344,11 +384,10 @@ class Upload(MemoizationLeaf):
344
384
  imports=Imports.EVENTS,
345
385
  hooks={Hooks.EVENTS: None},
346
386
  ),
347
- event_var._get_all_var_data(),
348
387
  use_dropzone_arguments._get_all_var_data(),
349
388
  VarData(
350
389
  hooks={
351
- callback_str: None,
390
+ **dict.fromkeys(callback_hooks, None),
352
391
  f"{left_side} = {right_side};": None,
353
392
  },
354
393
  imports={
@@ -108,6 +108,7 @@ class GhostUpload(Fragment):
108
108
  on_context_menu: EventType[()] | EventType[PointerEventInfo] | None = None,
109
109
  on_double_click: EventType[()] | EventType[PointerEventInfo] | None = None,
110
110
  on_drop: EventType[()] | EventType[Any] | None = None,
111
+ on_drop_rejected: EventType[()] | EventType[Any] | None = None,
111
112
  on_focus: EventType[()] | None = None,
112
113
  on_mount: EventType[()] | None = None,
113
114
  on_mouse_down: EventType[()] | None = None,
@@ -127,6 +128,7 @@ class GhostUpload(Fragment):
127
128
  Args:
128
129
  *children: The children of the component.
129
130
  on_drop: Fired when files are dropped.
131
+ on_drop_rejected: Fired when dropped files do not meet the specified criteria.
130
132
  style: The style of the component.
131
133
  key: A unique key for the component.
132
134
  id: The id for the component.
@@ -171,6 +173,7 @@ class Upload(MemoizationLeaf):
171
173
  on_context_menu: EventType[()] | EventType[PointerEventInfo] | None = None,
172
174
  on_double_click: EventType[()] | EventType[PointerEventInfo] | None = None,
173
175
  on_drop: EventType[()] | EventType[Any] | None = None,
176
+ on_drop_rejected: EventType[()] | EventType[Any] | None = None,
174
177
  on_focus: EventType[()] | None = None,
175
178
  on_mount: EventType[()] | None = None,
176
179
  on_mouse_down: EventType[()] | None = None,
@@ -199,6 +202,7 @@ class Upload(MemoizationLeaf):
199
202
  no_drag: Whether to disable drag and drop.
200
203
  no_keyboard: Whether to disable using the space/enter keys to upload.
201
204
  on_drop: Fired when files are dropped.
205
+ on_drop_rejected: Fired when dropped files do not meet the specified criteria.
202
206
  drag_active_style: Style rules to apply when actively dragging.
203
207
  style: The style of the component.
204
208
  key: A unique key for the component.
@@ -242,6 +246,7 @@ class StyledUpload(Upload):
242
246
  on_context_menu: EventType[()] | EventType[PointerEventInfo] | None = None,
243
247
  on_double_click: EventType[()] | EventType[PointerEventInfo] | None = None,
244
248
  on_drop: EventType[()] | EventType[Any] | None = None,
249
+ on_drop_rejected: EventType[()] | EventType[Any] | None = None,
245
250
  on_focus: EventType[()] | None = None,
246
251
  on_mount: EventType[()] | None = None,
247
252
  on_mouse_down: EventType[()] | None = None,
@@ -270,6 +275,7 @@ class StyledUpload(Upload):
270
275
  no_drag: Whether to disable drag and drop.
271
276
  no_keyboard: Whether to disable using the space/enter keys to upload.
272
277
  on_drop: Fired when files are dropped.
278
+ on_drop_rejected: Fired when dropped files do not meet the specified criteria.
273
279
  drag_active_style: Style rules to apply when actively dragging.
274
280
  style: The style of the component.
275
281
  key: A unique key for the component.
@@ -314,6 +320,7 @@ class UploadNamespace(ComponentNamespace):
314
320
  on_context_menu: EventType[()] | EventType[PointerEventInfo] | None = None,
315
321
  on_double_click: EventType[()] | EventType[PointerEventInfo] | None = None,
316
322
  on_drop: EventType[()] | EventType[Any] | None = None,
323
+ on_drop_rejected: EventType[()] | EventType[Any] | None = None,
317
324
  on_focus: EventType[()] | None = None,
318
325
  on_mount: EventType[()] | None = None,
319
326
  on_mouse_down: EventType[()] | None = None,
@@ -342,6 +349,7 @@ class UploadNamespace(ComponentNamespace):
342
349
  no_drag: Whether to disable drag and drop.
343
350
  no_keyboard: Whether to disable using the space/enter keys to upload.
344
351
  on_drop: Fired when files are dropped.
352
+ on_drop_rejected: Fired when dropped files do not meet the specified criteria.
345
353
  drag_active_style: Style rules to apply when actively dragging.
346
354
  style: The style of the component.
347
355
  key: A unique key for the component.
@@ -4,9 +4,14 @@ from __future__ import annotations
4
4
 
5
5
  from typing import Any, TypedDict
6
6
 
7
- from reflex.components.component import NoSSRComponent
8
- from reflex.event import EventHandler, no_args_event_spec, passthrough_event_spec
7
+ from reflex.components.component import Component
8
+ from reflex.components.core.cond import cond
9
+ from reflex.event import EventHandler, no_args_event_spec
10
+ from reflex.utils import console
9
11
  from reflex.vars.base import Var
12
+ from reflex.vars.object import ObjectVar
13
+
14
+ ReactPlayerEvent = ObjectVar[dict[str, dict[str, dict[str, Any]]]]
10
15
 
11
16
 
12
17
  class Progress(TypedDict):
@@ -16,21 +21,123 @@ class Progress(TypedDict):
16
21
  playedSeconds: float
17
22
  loaded: float
18
23
  loadedSeconds: float
24
+ duration: float
25
+
26
+
27
+ def _on_progress_signature(event: ReactPlayerEvent) -> list[Var[Progress]]:
28
+ """Type signature for on_progress event.
29
+
30
+ Args:
31
+ event: The event variable.
32
+
33
+ Returns:
34
+ The progress information extracted from the event.
35
+ """
36
+ player_info = event["target"]["api"]["playerInfo"].to(dict)
37
+ progress_state = player_info["progressState"].to(dict)
38
+ current = progress_state["current"].to(float)
39
+ loaded = progress_state["loaded"].to(float)
40
+ duration = progress_state["duration"].to(float)
41
+ return [
42
+ cond(
43
+ progress_state,
44
+ {
45
+ "played": cond(duration, current / duration, 0.0),
46
+ "playedSeconds": current,
47
+ "loaded": cond(duration, loaded / duration, 0.0),
48
+ "loadedSeconds": loaded,
49
+ "duration": duration,
50
+ },
51
+ {
52
+ "played": 0.0,
53
+ "playedSeconds": 0.0,
54
+ "loaded": 0.0,
55
+ "loadedSeconds": 0.0,
56
+ "duration": 0.0,
57
+ },
58
+ ).to(Progress)
59
+ ]
60
+
61
+
62
+ def _player_info_key_or_zero(event: ReactPlayerEvent, key: str) -> Var[float]:
63
+ """Helper to extract a value from playerInfo or return 0.0 if not available.
64
+
65
+ Args:
66
+ event: The event variable.
67
+ key: The key to extract from playerInfo.
68
+
69
+ Returns:
70
+ The extracted value or 0.0 if not available.
71
+ """
72
+ player_info = event["target"]["api"]["playerInfo"].to(dict)
73
+ return cond(
74
+ player_info[key],
75
+ player_info[key],
76
+ 0.0,
77
+ ).to(float)
78
+
79
+
80
+ def _on_time_update_signature(event: ReactPlayerEvent) -> list[Var[float]]:
81
+ """Type signature for on_time_update event.
82
+
83
+ Args:
84
+ event: The event variable.
85
+
86
+ Returns:
87
+ The current timestamp in seconds.
88
+ """
89
+ return [_player_info_key_or_zero(event, "currentTime")]
90
+
91
+
92
+ def _on_duration_change_signature(event: ReactPlayerEvent) -> list[Var[float]]:
93
+ """Type signature for on_duration_change event.
94
+
95
+ Args:
96
+ event: The event variable.
97
+
98
+ Returns:
99
+ The active media's duration in seconds.
100
+ """
101
+ return [_player_info_key_or_zero(event, "duration")]
102
+
103
+
104
+ def _on_rate_change_signature(event: ReactPlayerEvent) -> list[Var[float]]:
105
+ """Type signature for on_rate_change event.
19
106
 
107
+ Args:
108
+ event: The event variable.
20
109
 
21
- class ReactPlayer(NoSSRComponent):
110
+ Returns:
111
+ The current playback rate.
112
+ """
113
+ return [_player_info_key_or_zero(event, "playbackRate")]
114
+
115
+
116
+ _DEPRECATED_PROP_MAP = {
117
+ "url": "src",
118
+ "on_duration": "on_duration_change",
119
+ "on_playback_rate_change": "on_rate_change",
120
+ "on_seek": "on_seeked",
121
+ "on_buffer": "on_waiting",
122
+ "on_buffer_end": "on_playing",
123
+ "on_enable_pip": "on_enter_picture_in_picture",
124
+ "on_disable_pip": "on_leave_picture_in_picture",
125
+ }
126
+
127
+
128
+ class ReactPlayer(Component):
22
129
  """Using react-player and not implement all props and callback yet.
23
130
  reference: https://github.com/cookpete/react-player.
24
131
  """
25
132
 
26
- library = "react-player@2.16.0"
133
+ library = "react-player@3.3.3"
27
134
 
28
135
  tag = "ReactPlayer"
29
136
 
30
137
  is_default = True
31
138
 
32
139
  # The url of a video or song to play
33
- url: Var[str]
140
+ src: Var[str | list[str] | list[dict[str, str]]]
34
141
 
35
142
  # Set to true or false to pause or play the media
36
143
  playing: Var[bool]
@@ -50,38 +157,44 @@ class ReactPlayer(NoSSRComponent):
50
157
  # Mutes the player
51
158
  muted: Var[bool]
52
159
 
160
+ # Player-specific configuration parameters.
161
+ config: Var[dict[str, Any]]
162
+
53
163
  # Called when media is loaded and ready to play. If playing is set to true, media will play immediately.
54
164
  on_ready: EventHandler[no_args_event_spec]
55
165
 
56
166
  # Called when media starts playing.
57
167
  on_start: EventHandler[no_args_event_spec]
58
168
 
59
- # Called when media starts or resumes playing after pausing or buffering.
169
+ # Called when playing is set to true.
60
170
  on_play: EventHandler[no_args_event_spec]
61
171
 
62
- # Callback containing played and loaded progress as a fraction, and playedSeconds and loadedSeconds in seconds. eg { played: 0.12, playedSeconds: 11.3, loaded: 0.34, loadedSeconds: 16.7 }
63
- on_progress: EventHandler[passthrough_event_spec(Progress)]
172
+ # Called when media starts or resumes playing after pausing or buffering.
173
+ on_playing: EventHandler[no_args_event_spec]
174
+
175
+ # Called while the video is loading only. Contains played and loaded progress as a fraction, and playedSeconds and loadedSeconds in seconds. eg { played: 0.12, playedSeconds: 11.3, loaded: 0.34, loadedSeconds: 16.7 }
176
+ on_progress: EventHandler[_on_progress_signature]
177
+
178
+ # Called when the media's current time changes (~4Hz, use .throttle to limit calls to backend).
179
+ on_time_update: EventHandler[_on_time_update_signature]
64
180
 
65
181
  # Callback containing duration of the media, in seconds.
66
- on_duration: EventHandler[passthrough_event_spec(float)]
182
+ on_duration_change: EventHandler[_on_duration_change_signature]
67
183
 
68
184
  # Called when media is paused.
69
185
  on_pause: EventHandler[no_args_event_spec]
70
186
 
71
187
  # Called when media starts buffering.
72
- on_buffer: EventHandler[no_args_event_spec]
188
+ on_waiting: EventHandler[no_args_event_spec]
73
189
 
74
- # Called when media has finished buffering. Works for files, YouTube and Facebook.
75
- on_buffer_end: EventHandler[no_args_event_spec]
190
+ # Called when the media is seeking.
191
+ on_seeking: EventHandler[no_args_event_spec]
76
192
 
77
193
  # Called when media seeks with seconds parameter.
78
- on_seek: EventHandler[passthrough_event_spec(float)]
194
+ on_seeked: EventHandler[_on_time_update_signature]
79
195
 
80
196
  # Called when playback rate of the player changed. Only supported by YouTube, Vimeo (if enabled), Wistia, and file paths.
81
- on_playback_rate_change: EventHandler[no_args_event_spec]
82
-
83
- # Called when playback quality of the player changed. Only supported by YouTube (if enabled).
84
- on_playback_quality_change: EventHandler[no_args_event_spec]
197
+ on_rate_change: EventHandler[_on_rate_change_signature]
85
198
 
86
199
  # Called when media finishes playing. Does not fire when loop is set to true.
87
200
  on_ended: EventHandler[no_args_event_spec]
@@ -93,10 +206,37 @@ class ReactPlayer(NoSSRComponent):
93
206
  on_click_preview: EventHandler[no_args_event_spec]
94
207
 
95
208
  # Called when picture-in-picture mode is enabled.
96
- on_enable_pip: EventHandler[no_args_event_spec]
209
+ on_enter_picture_in_picture: EventHandler[no_args_event_spec]
97
210
 
98
211
  # Called when picture-in-picture mode is disabled.
99
- on_disable_pip: EventHandler[no_args_event_spec]
212
+ on_leave_picture_in_picture: EventHandler[no_args_event_spec]
213
+
214
+ @classmethod
215
+ def create(cls, *children, **props) -> ReactPlayer:
216
+ """Create a component.
217
+
218
+ Args:
219
+ children: The children of the component.
220
+ props: The props of the component.
221
+
222
+ Returns:
223
+ The created component.
224
+
225
+ Raises:
226
+ ValueError: If both a deprecated prop and its replacement are both passed.
227
+ """
228
+ for prop, new_prop in _DEPRECATED_PROP_MAP.items():
229
+ if prop in props:
230
+ if new_prop in props:
231
+ msg = (
232
+ f"The prop {prop!r} is deprecated, but the replacement {new_prop!r} is also passed. Please remove {prop!r}.",
233
+ )
234
+ raise ValueError(msg)
235
+ console.warn(
236
+ f"The prop {prop!r} has been replaced by {new_prop!r}, please update your code.",
237
+ )
238
+ props[new_prop] = props.pop(prop)
239
+ return super().create(*children, **props) # type: ignore[return-value]
100
240
 
101
241
  def _render(self, props: dict[str, Any] | None = None):
102
242
  """Render the component. Adds width and height set to None because