@lightningtv/solid 3.0.0-8 → 3.0.0-9

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 (31) hide show
  1. package/dist/src/primitives/Grid.d.ts +2 -2
  2. package/dist/src/primitives/Grid.jsx +27 -17
  3. package/dist/src/primitives/Grid.jsx.map +1 -1
  4. package/dist/src/primitives/Lazy.jsx +1 -1
  5. package/dist/src/primitives/Lazy.jsx.map +1 -1
  6. package/dist/src/primitives/Marquee.jsx +2 -2
  7. package/dist/src/primitives/Marquee.jsx.map +1 -1
  8. package/dist/src/primitives/announcer/announcer.js +0 -1
  9. package/dist/src/primitives/announcer/announcer.js.map +1 -1
  10. package/dist/src/primitives/announcer/speech.d.ts +1 -1
  11. package/dist/src/primitives/announcer/speech.js +51 -21
  12. package/dist/src/primitives/announcer/speech.js.map +1 -1
  13. package/dist/src/primitives/index.d.ts +2 -1
  14. package/dist/src/primitives/index.js +2 -1
  15. package/dist/src/primitives/index.js.map +1 -1
  16. package/dist/src/primitives/types.d.ts +2 -0
  17. package/dist/src/primitives/utils/withScrolling.d.ts +2 -0
  18. package/dist/src/primitives/utils/withScrolling.js +7 -0
  19. package/dist/src/primitives/utils/withScrolling.js.map +1 -1
  20. package/dist/tsconfig.tsbuildinfo +1 -1
  21. package/jsx-runtime.d.ts +2 -1
  22. package/package.json +3 -3
  23. package/src/primitives/Grid.tsx +32 -22
  24. package/src/primitives/Lazy.tsx +1 -1
  25. package/src/primitives/{marquee.tsx → Marquee.tsx} +1 -1
  26. package/src/primitives/announcer/announcer.ts +0 -1
  27. package/src/primitives/announcer/speech.ts +60 -23
  28. package/src/primitives/index.ts +2 -1
  29. package/src/primitives/types.ts +9 -0
  30. package/src/primitives/useHold.ts +69 -0
  31. package/src/primitives/utils/withScrolling.ts +15 -0
@@ -1,4 +1,8 @@
1
- type CoreSpeechType = string | (() => SpeechType) | SpeechType[];
1
+ type CoreSpeechType =
2
+ | string
3
+ | (() => SpeechType)
4
+ | SpeechType[]
5
+ | SpeechSynthesisUtterance;
2
6
  export type SpeechType = CoreSpeechType | Promise<CoreSpeechType>;
3
7
 
4
8
  export interface SeriesResult {
@@ -87,11 +91,6 @@ function speakSeries(
87
91
  Array.isArray(series) ? series : [series],
88
92
  );
89
93
  const nestedSeriesResults: SeriesResult[] = [];
90
- /*
91
- We hold this array of SpeechSynthesisUtterances in order to prevent them from being
92
- garbage collected prematurely on STB hardware which can cause the 'onend' events of
93
- utterances to not fire consistently.
94
- */
95
94
  const utterances: SpeechSynthesisUtterance[] = [];
96
95
  let active: boolean = true;
97
96
 
@@ -100,24 +99,61 @@ function speakSeries(
100
99
  while (active && remainingPhrases.length) {
101
100
  const phrase = await Promise.resolve(remainingPhrases.shift());
102
101
  if (!active) {
103
- // Exit
104
- // Need to check this after the await in case it was cancelled in between
105
- break;
106
- } else if (typeof phrase === 'string' && phrase.includes('PAUSE-')) {
107
- // Pause it
108
- let pause = Number(phrase.split('PAUSE-')[1]) * 1000;
109
- if (isNaN(pause)) {
110
- pause = 0;
102
+ break; // Exit if canceled
103
+ }
104
+
105
+ if (typeof phrase === 'string' && phrase.includes('PAUSE-')) {
106
+ // Handle pauses
107
+ const pause = Number(phrase.split('PAUSE-')[1]) * 1000;
108
+ if (!isNaN(pause)) {
109
+ await delay(pause);
110
+ }
111
+ } else if (typeof phrase === 'string') {
112
+ if (!phrase) {
113
+ continue; // Skip empty strings
111
114
  }
112
- await delay(pause);
113
- } else if (typeof phrase === 'string' && phrase.length) {
114
- // Speak it
115
+ // Handle regular strings with retry logic
115
116
  const totalRetries = 3;
116
117
  let retriesLeft = totalRetries;
118
+
117
119
  while (active && retriesLeft > 0) {
118
120
  try {
119
121
  await speak(phrase, utterances, lang, voice);
120
- retriesLeft = 0;
122
+ retriesLeft = 0; // Exit retry loop on success
123
+ } catch (e) {
124
+ if (e instanceof SpeechSynthesisErrorEvent) {
125
+ if (e.error === 'network') {
126
+ retriesLeft--;
127
+ console.warn(
128
+ `Speech synthesis network error. Retries left: ${retriesLeft}`,
129
+ );
130
+ await delay(500 * (totalRetries - retriesLeft));
131
+ } else if (
132
+ e.error === 'canceled' ||
133
+ e.error === 'interrupted'
134
+ ) {
135
+ // Cancel or interrupt error (ignore)
136
+ retriesLeft = 0;
137
+ } else {
138
+ throw new Error(`SpeechSynthesisErrorEvent: ${e.error}`);
139
+ }
140
+ } else {
141
+ throw e;
142
+ }
143
+ }
144
+ }
145
+ } else if (phrase instanceof SpeechSynthesisUtterance) {
146
+ // Handle SpeechSynthesisUtterance objects with retry logic
147
+ const totalRetries = 3;
148
+ let retriesLeft = totalRetries;
149
+ const text = phrase.text;
150
+ const objectLang = phrase?.lang;
151
+ const objectVoice = phrase?.voice;
152
+
153
+ while (active && retriesLeft > 0) {
154
+ try {
155
+ await speak(text, utterances, objectLang, objectVoice?.name);
156
+ retriesLeft = 0; // Exit retry loop on success
121
157
  } catch (e) {
122
158
  if (e instanceof SpeechSynthesisErrorEvent) {
123
159
  if (e.error === 'network') {
@@ -141,11 +177,12 @@ function speakSeries(
141
177
  }
142
178
  }
143
179
  } else if (typeof phrase === 'function') {
180
+ // Handle functions
144
181
  const seriesResult = speakSeries(phrase(), lang, voice, false);
145
182
  nestedSeriesResults.push(seriesResult);
146
183
  await seriesResult.series;
147
184
  } else if (Array.isArray(phrase)) {
148
- // Speak it (recursively)
185
+ // Handle nested arrays
149
186
  const seriesResult = speakSeries(phrase, lang, voice, false);
150
187
  nestedSeriesResults.push(seriesResult);
151
188
  await seriesResult.series;
@@ -155,6 +192,7 @@ function speakSeries(
155
192
  active = false;
156
193
  }
157
194
  })();
195
+
158
196
  return {
159
197
  series: seriesChain,
160
198
  get active() {
@@ -168,16 +206,15 @@ function speakSeries(
168
206
  return;
169
207
  }
170
208
  if (root) {
171
- synth.cancel();
209
+ synth.cancel(); // Cancel all ongoing speech
172
210
  }
173
- nestedSeriesResults.forEach((nestedSeriesResults) => {
174
- nestedSeriesResults.cancel();
211
+ nestedSeriesResults.forEach((nestedSeriesResult) => {
212
+ nestedSeriesResult.cancel();
175
213
  });
176
214
  active = false;
177
215
  },
178
216
  };
179
217
  }
180
-
181
218
  let currentSeries: SeriesResult | undefined;
182
219
  export default function (
183
220
  toSpeak: SpeechType,
@@ -12,7 +12,8 @@ export * from './Grid.jsx';
12
12
  export * from './FPSCounter.jsx';
13
13
  export * from './FadeInOut.jsx';
14
14
  export * from './createFocusStack.jsx';
15
- export * from './marquee.jsx';
15
+ export * from './Marquee.jsx';
16
+ export * from './useHold.js';
16
17
  export { withScrolling } from './utils/withScrolling.js';
17
18
  export {
18
19
  type AnyFunction,
@@ -1,5 +1,6 @@
1
1
  import type { ElementNode, NodeProps, NodeStyles } from '@lightningtv/solid';
2
2
  import type { KeyHandler } from '@lightningtv/core/focusManager';
3
+
3
4
  export type OnSelectedChanged = (
4
5
  this: NavigableElement,
5
6
  selectedIndex: number,
@@ -7,6 +8,7 @@ export type OnSelectedChanged = (
7
8
  active: ElementNode,
8
9
  lastSelectedIndex?: number,
9
10
  ) => void;
11
+
10
12
  export interface NavigableProps extends NodeProps {
11
13
  /** function to be called when the selected of the component changes */
12
14
  onSelectedChanged?: OnSelectedChanged;
@@ -40,6 +42,13 @@ export interface NavigableProps extends NodeProps {
40
42
  * Wrap the row so active goes back to the beginning of the row
41
43
  */
42
44
  wrap?: boolean;
45
+
46
+ /** function to be called when scrolled */
47
+ onScrolled?: (
48
+ elm: NavigableElement,
49
+ offset: number,
50
+ isInitial: boolean,
51
+ ) => void;
43
52
  }
44
53
 
45
54
  // @ts-expect-error animationSettings is not identical - weird
@@ -0,0 +1,69 @@
1
+ import { createMemo } from 'solid-js';
2
+
3
+ export type UseHoldProps = {
4
+ onHold: () => void;
5
+ onEnter: () => void;
6
+ onRelease?: () => void;
7
+ holdThreshold?: number;
8
+ performOnEnterImmediately?: boolean;
9
+ };
10
+
11
+ /**
12
+ * @example
13
+ * const [holdRight, releaseRight] = useHold({
14
+ * onHold: handleHoldRight,
15
+ * onEnter: handleOnRight,
16
+ * onRelease: handleReleaseHold,
17
+ * holdThreshold: 200,
18
+ * performOnEnterImmediately: true
19
+ * });
20
+ *
21
+ * <View
22
+ * onRight={holdRight}
23
+ * onRightRelease={releaseRight}
24
+ * />
25
+ *
26
+ * @param {UseHoldProps} props - The properties for configuring the hold behavior.
27
+ * @returns {[() => boolean, () => boolean]} A tuple containing `startHold` and `releaseHold` functions.
28
+ */
29
+
30
+ export function useHold(props: UseHoldProps) {
31
+ const holdThreshold = createMemo(() => props.holdThreshold ?? 500);
32
+ const performOnEnterImmediately = createMemo(
33
+ () => props.performOnEnterImmediately ?? false,
34
+ );
35
+
36
+ let holdTimeout = -1;
37
+ let wasHeld = false;
38
+
39
+ const startHold = () => {
40
+ if (holdTimeout === -1) {
41
+ if (performOnEnterImmediately()) {
42
+ props.onEnter();
43
+ }
44
+ holdTimeout = setTimeout(() => {
45
+ wasHeld = true;
46
+ props.onHold();
47
+ }, holdThreshold()) as unknown as number;
48
+ }
49
+ return true;
50
+ };
51
+
52
+ const releaseHold = () => {
53
+ if (holdTimeout !== -1) {
54
+ clearTimeout(holdTimeout);
55
+ holdTimeout = -1;
56
+ if (!wasHeld) {
57
+ if (!performOnEnterImmediately()) props.onEnter();
58
+ return;
59
+ }
60
+ props.onRelease?.();
61
+ wasHeld = false;
62
+ }
63
+ return true;
64
+ };
65
+
66
+ return [startHold, releaseHold];
67
+ }
68
+
69
+ export default useHold;
@@ -11,8 +11,14 @@ export interface ScrollableElement extends ElementNode {
11
11
  selected: number;
12
12
  offset?: number;
13
13
  endOffset?: number;
14
+ onScrolled?: (
15
+ elm: ScrollableElement,
16
+ offset: number,
17
+ isInitial: boolean,
18
+ ) => void;
14
19
  _targetPosition?: number;
15
20
  _screenOffset?: number;
21
+ _initialPosition?: number;
16
22
  }
17
23
 
18
24
  // From the renderer, not exported
@@ -48,6 +54,10 @@ export function withScrolling(isRow: boolean) {
48
54
  )
49
55
  return;
50
56
 
57
+ if (componentRef._initialPosition === undefined) {
58
+ componentRef._initialPosition = componentRef[axis];
59
+ }
60
+
51
61
  const lng = componentRef.lng as INode;
52
62
  const screenSize = isRow ? lng.stage.root.width : lng.stage.root.height;
53
63
  // Determine if movement is incremental or decremental
@@ -157,6 +167,11 @@ export function withScrolling(isRow: boolean) {
157
167
 
158
168
  // Update position if it has changed
159
169
  if (componentRef[axis] !== nextPosition) {
170
+ if (componentRef.onScrolled) {
171
+ const isInitial = nextPosition === componentRef._initialPosition;
172
+ componentRef.onScrolled(componentRef, nextPosition, isInitial);
173
+ }
174
+
160
175
  componentRef[axis] = nextPosition;
161
176
  // Store the new position to keep track during animations
162
177
  componentRef._targetPosition = nextPosition;