@streamplace/components 0.8.0 → 0.8.5

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.
@@ -6,10 +6,19 @@ import {
6
6
  } from "../../lib/metadata-constants";
7
7
 
8
8
  import {
9
+ PlaceStreamMetadataConfiguration,
10
+ PlaceStreamMetadataContentRights,
11
+ PlaceStreamMetadataDistributionPolicy,
12
+ } from "streamplace";
13
+ import {
14
+ useGetBroadcasterDID,
9
15
  useGetContentMetadata,
10
16
  useSaveContentMetadata,
11
17
  } from "../../streamplace-store/content-metadata-actions";
12
- import { useDID } from "../../streamplace-store/streamplace-store";
18
+ import {
19
+ useDID,
20
+ useStreamplaceStore,
21
+ } from "../../streamplace-store/streamplace-store";
13
22
  import { usePDSAgent } from "../../streamplace-store/xrpc";
14
23
  import * as zero from "../../ui";
15
24
  import { Button } from "../ui/button";
@@ -23,29 +32,12 @@ import { Tooltip } from "../ui/tooltip";
23
32
 
24
33
  const { p, r, bg, borders, w, text, layout, gap, flex } = zero;
25
34
 
26
- // Types
27
- export interface DistributionPolicy {
28
- deleteAfter?: number;
29
- }
30
-
31
- export interface Rights {
32
- creator?: string;
33
- copyrightNotice?: string;
34
- copyrightYear?: string | number;
35
- license?: string;
36
- creditLine?: string;
37
- }
38
-
39
- export interface ContentMetadata {
40
- contentWarnings: { warnings: string[] };
41
- distributionPolicy: DistributionPolicy;
42
- contentRights: Rights;
43
- }
44
-
45
35
  export interface ContentMetadataFormProps {
46
36
  showUpdateButton?: boolean;
47
- onMetadataChange?: (metadata: ContentMetadata) => void;
48
- initialMetadata?: ContentMetadata;
37
+ onMetadataChange?: (
38
+ metadata: PlaceStreamMetadataConfiguration.Record,
39
+ ) => void;
40
+ initialMetadata?: PlaceStreamMetadataConfiguration.Record;
49
41
  style?: any;
50
42
  }
51
43
 
@@ -105,11 +97,11 @@ export const ContentMetadataForm = forwardRef<any, ContentMetadataFormProps>(
105
97
  // Local state for metadata
106
98
  const [contentWarnings, setContentWarnings] = useState<string[]>([]);
107
99
  const [distributionPolicy, setDistributionPolicy] =
108
- useState<DistributionPolicy>({});
109
- const [contentRights, setContentRights] = useState<Rights>({});
100
+ useState<PlaceStreamMetadataDistributionPolicy.Main>({});
101
+ const [contentRights, setContentRights] =
102
+ useState<PlaceStreamMetadataContentRights.Main>({});
110
103
  const [selectedLicense, setSelectedLicense] = useState<string>("");
111
104
  const [customLicenseText, setCustomLicenseText] = useState<string>("");
112
- const [customDateTime, setCustomDateTime] = useState<string>("");
113
105
  const [loading, setLoading] = useState(false);
114
106
  const [hasMetadata, setHasMetadata] = useState(false);
115
107
 
@@ -119,6 +111,13 @@ export const ContentMetadataForm = forwardRef<any, ContentMetadataFormProps>(
119
111
 
120
112
  const currentYear = new Date().getFullYear();
121
113
 
114
+ const getBroadcasterDID = useGetBroadcasterDID();
115
+ useEffect(() => {
116
+ getBroadcasterDID();
117
+ }, [getBroadcasterDID]);
118
+
119
+ const broadcasterDID = useStreamplaceStore((state) => state.broadcasterDID);
120
+
122
121
  // Load existing metadata on mount or from initialMetadata prop
123
122
  useEffect(() => {
124
123
  if (initialMetadata) {
@@ -128,11 +127,6 @@ export const ContentMetadataForm = forwardRef<any, ContentMetadataFormProps>(
128
127
  }
129
128
  if (initialMetadata.distributionPolicy) {
130
129
  setDistributionPolicy(initialMetadata.distributionPolicy);
131
- setCustomDateTime(
132
- initialMetadata.distributionPolicy.deleteAfter
133
- ? String(initialMetadata.distributionPolicy.deleteAfter)
134
- : "",
135
- );
136
130
  }
137
131
  if (initialMetadata.contentRights) {
138
132
  setContentRights(initialMetadata.contentRights);
@@ -155,11 +149,6 @@ export const ContentMetadataForm = forwardRef<any, ContentMetadataFormProps>(
155
149
  }
156
150
  if (metadata.record.distributionPolicy) {
157
151
  setDistributionPolicy(metadata.record.distributionPolicy);
158
- setCustomDateTime(
159
- metadata.record.distributionPolicy.deleteAfter
160
- ? String(metadata.record.distributionPolicy.deleteAfter)
161
- : "",
162
- );
163
152
  }
164
153
  if (metadata.record.contentRights) {
165
154
  setContentRights(metadata.record.contentRights);
@@ -185,6 +174,7 @@ export const ContentMetadataForm = forwardRef<any, ContentMetadataFormProps>(
185
174
 
186
175
  if (onMetadataChange) {
187
176
  onMetadataChange({
177
+ $type: "place.stream.metadata.configuration",
188
178
  contentWarnings: { warnings: newWarnings },
189
179
  distributionPolicy,
190
180
  contentRights,
@@ -198,6 +188,7 @@ export const ContentMetadataForm = forwardRef<any, ContentMetadataFormProps>(
198
188
  useEffect(() => {
199
189
  if (onMetadataChange) {
200
190
  onMetadataChange({
191
+ $type: "place.stream.metadata.configuration",
201
192
  contentWarnings: { warnings: contentWarnings },
202
193
  distributionPolicy,
203
194
  contentRights,
@@ -207,19 +198,40 @@ export const ContentMetadataForm = forwardRef<any, ContentMetadataFormProps>(
207
198
 
208
199
  // Handle distribution policy changes
209
200
  const handleDistributionPolicyChange = useCallback(
210
- (deleteAfter: string) => {
211
- let duration = parseInt(deleteAfter, 10);
212
- if (isNaN(duration) || duration < 0) {
213
- duration = 0;
201
+ ({
202
+ deleteAfter,
203
+ allowedBroadcasters,
204
+ }: {
205
+ deleteAfter?: string;
206
+ allowedBroadcasters?: string;
207
+ }) => {
208
+ let newDistributionPolicy: PlaceStreamMetadataDistributionPolicy.Main =
209
+ {
210
+ ...distributionPolicy,
211
+ };
212
+ if (typeof deleteAfter === "string") {
213
+ let duration = parseInt(deleteAfter, 10);
214
+ if (isNaN(duration)) {
215
+ newDistributionPolicy.deleteAfter = undefined;
216
+ } else {
217
+ if (isNaN(duration) || duration < 0) {
218
+ duration = -1;
219
+ }
220
+ newDistributionPolicy.deleteAfter =
221
+ duration === 0 ? undefined : duration;
222
+ }
214
223
  }
215
- const newPolicy = duration > 0 ? { deleteAfter: duration } : {};
216
- console.log("newPolicy", newPolicy);
217
- setDistributionPolicy(newPolicy);
224
+ if (typeof allowedBroadcasters === "string") {
225
+ newDistributionPolicy.allowedBroadcasters =
226
+ allowedBroadcasters.split("\n");
227
+ }
228
+ setDistributionPolicy(newDistributionPolicy);
218
229
 
219
230
  if (onMetadataChange) {
220
231
  onMetadataChange({
232
+ $type: "place.stream.metadata.configuration",
221
233
  contentWarnings: { warnings: contentWarnings },
222
- distributionPolicy: newPolicy,
234
+ distributionPolicy: newDistributionPolicy,
223
235
  contentRights,
224
236
  });
225
237
  }
@@ -241,6 +253,7 @@ export const ContentMetadataForm = forwardRef<any, ContentMetadataFormProps>(
241
253
 
242
254
  if (onMetadataChange) {
243
255
  onMetadataChange({
256
+ $type: "place.stream.metadata.configuration",
244
257
  contentWarnings: { warnings: contentWarnings },
245
258
  distributionPolicy,
246
259
  contentRights: newRights,
@@ -254,20 +267,13 @@ export const ContentMetadataForm = forwardRef<any, ContentMetadataFormProps>(
254
267
  setLoading(true);
255
268
  try {
256
269
  // Build the metadata object, only including non-empty fields
257
- const metadata: any = {};
270
+ const metadata: PlaceStreamMetadataConfiguration.Record = {
271
+ $type: "place.stream.metadata.configuration",
272
+ };
258
273
 
259
274
  // Only include contentWarnings if it has values
260
275
  if (contentWarnings && contentWarnings.length > 0) {
261
- metadata.contentWarnings = contentWarnings;
262
- }
263
-
264
- // Only include distributionPolicy if it has a deleteAfter value
265
- const duration = parseInt(customDateTime, 10);
266
- if (!isNaN(duration) && duration > 0) {
267
- metadata.distributionPolicy = { deleteAfter: duration };
268
- setCustomDateTime(`${duration}`);
269
- } else {
270
- setCustomDateTime("");
276
+ metadata.contentWarnings = { warnings: contentWarnings };
271
277
  }
272
278
 
273
279
  // Only include contentRights if it has actual values
@@ -301,6 +307,21 @@ export const ContentMetadataForm = forwardRef<any, ContentMetadataFormProps>(
301
307
  metadata.contentRights = filteredRights;
302
308
  }
303
309
 
310
+ metadata.distributionPolicy = {
311
+ ...distributionPolicy,
312
+ };
313
+
314
+ if (distributionPolicy?.allowedBroadcasters) {
315
+ const filteredBs = distributionPolicy.allowedBroadcasters.filter(
316
+ (broadcaster) => broadcaster !== "",
317
+ );
318
+ metadata.distributionPolicy.allowedBroadcasters = filteredBs;
319
+ setDistributionPolicy({
320
+ ...distributionPolicy,
321
+ allowedBroadcasters: filteredBs,
322
+ });
323
+ }
324
+
304
325
  await saveContentMetadata(metadata);
305
326
  setHasMetadata(true);
306
327
  // Show success toast
@@ -320,7 +341,6 @@ export const ContentMetadataForm = forwardRef<any, ContentMetadataFormProps>(
320
341
  contentRights,
321
342
  selectedLicense,
322
343
  customLicenseText,
323
- customDateTime,
324
344
  hasMetadata,
325
345
  saveContentMetadata,
326
346
  ]);
@@ -670,58 +690,170 @@ export const ContentMetadataForm = forwardRef<any, ContentMetadataFormProps>(
670
690
  optional
671
691
  </Text>
672
692
  </View>
673
- <View style={[gap.all[3], w.percent[100]]}>
674
- <View
675
- style={[
676
- layout.flex.row,
677
- layout.flex.alignCenter,
678
- w.percent[100],
679
- ]}
680
- >
681
- <Text
693
+
694
+ {/* allow everyone to distribute your content */}
695
+ <Tooltip
696
+ content="Distribution of your content is unlimited, but they still have to respect the deleteAfter policy below."
697
+ position="top"
698
+ >
699
+ <Checkbox
700
+ checked={
701
+ distributionPolicy.allowedBroadcasters?.includes("*") ||
702
+ false
703
+ }
704
+ onCheckedChange={(checked) =>
705
+ handleDistributionPolicyChange({
706
+ allowedBroadcasters: checked
707
+ ? "*"
708
+ : broadcasterDID || "",
709
+ })
710
+ }
711
+ label={"Allow everyone to distribute your content"}
712
+ style={[{ fontSize: 12 }]}
713
+ />
714
+ </Tooltip>
715
+
716
+ {/* allowedBroadcasters */}
717
+ {!distributionPolicy.allowedBroadcasters?.includes("*") && (
718
+ <View style={[gap.all[3], w.percent[100]]}>
719
+ <View
682
720
  style={[
683
- text.neutral[300],
684
- {
685
- minWidth: 100,
686
- textAlign: "left",
687
- paddingBottom: 8,
688
- fontSize: 14,
689
- },
721
+ layout.flex.row,
722
+ layout.flex.alignCenter,
723
+ w.percent[100],
690
724
  ]}
691
725
  >
692
- Delete After
693
- </Text>
694
- <View style={[flex.values[1]]}>
695
726
  <Text
696
727
  style={[
697
- text.gray[500],
698
- { fontSize: 12, paddingBottom: 4 },
728
+ text.neutral[300],
729
+ {
730
+ minWidth: 100,
731
+ textAlign: "left",
732
+ paddingBottom: 8,
733
+ fontSize: 14,
734
+ },
699
735
  ]}
700
736
  >
701
- Duration in seconds (e.g., 300 for 5 minutes) or 0 to
702
- allow archiving your stream
737
+ Allowed<br></br>Broadcasters
703
738
  </Text>
704
- <Input
705
- value={customDateTime}
706
- onChange={(value) => {
707
- setCustomDateTime(value);
708
- handleDistributionPolicyChange(value);
709
- }}
710
- keyboardType="numeric"
711
- variant="filled"
712
- inputStyle={[
713
- p[3],
714
- r.md,
715
- bg.neutral[800],
716
- text.white,
717
- borders.width.thin,
718
- borders.color.neutral[600],
719
- w.percent[100],
739
+ <View style={[flex.values[1]]}>
740
+ <Text
741
+ style={[
742
+ text.gray[500],
743
+ { fontSize: 12, paddingBottom: 4 },
744
+ ]}
745
+ >
746
+ Enter the did:webs of the broadcasters you want to
747
+ allow to distribute your content, one per line.
748
+ </Text>
749
+ <Input
750
+ multiline={true}
751
+ numberOfLines={4}
752
+ value={
753
+ distributionPolicy.allowedBroadcasters?.join(
754
+ "\n",
755
+ ) || ""
756
+ }
757
+ onChange={(value) => {
758
+ handleDistributionPolicyChange({
759
+ allowedBroadcasters: value,
760
+ });
761
+ }}
762
+ variant="filled"
763
+ inputStyle={[
764
+ p[3],
765
+ r.md,
766
+ bg.neutral[800],
767
+ text.white,
768
+ borders.width.thin,
769
+ borders.color.neutral[600],
770
+ w.percent[100],
771
+ ]}
772
+ />
773
+ </View>
774
+ </View>
775
+ </View>
776
+ )}
777
+
778
+ <Tooltip
779
+ content="Anyone may archive your content indefinitely."
780
+ position="top"
781
+ >
782
+ <Checkbox
783
+ checked={distributionPolicy.deleteAfter === -1}
784
+ onCheckedChange={(checked) => {
785
+ if (checked) {
786
+ handleDistributionPolicyChange({
787
+ deleteAfter: "-1",
788
+ });
789
+ } else {
790
+ handleDistributionPolicyChange({
791
+ deleteAfter: "300",
792
+ });
793
+ }
794
+ }}
795
+ label={"Allow everyone to archive your content"}
796
+ style={[{ fontSize: 12 }]}
797
+ />
798
+ </Tooltip>
799
+
800
+ {/* deleteAfter */}
801
+ {distributionPolicy.deleteAfter !== -1 && (
802
+ <View style={[gap.all[3], w.percent[100]]}>
803
+ <View
804
+ style={[
805
+ layout.flex.row,
806
+ layout.flex.alignCenter,
807
+ w.percent[100],
808
+ ]}
809
+ >
810
+ <Text
811
+ style={[
812
+ text.neutral[300],
813
+ {
814
+ minWidth: 100,
815
+ textAlign: "left",
816
+ paddingBottom: 8,
817
+ fontSize: 14,
818
+ },
720
819
  ]}
721
- />
820
+ >
821
+ Delete After
822
+ </Text>
823
+ <View style={[flex.values[1]]}>
824
+ <Text
825
+ style={[
826
+ text.gray[500],
827
+ { fontSize: 12, paddingBottom: 4 },
828
+ ]}
829
+ >
830
+ Duration in seconds (e.g., 300 for 5 minutes)
831
+ </Text>
832
+ <Input
833
+ value={
834
+ distributionPolicy.deleteAfter?.toString() || ""
835
+ }
836
+ onChange={(value) => {
837
+ handleDistributionPolicyChange({
838
+ deleteAfter: value,
839
+ });
840
+ }}
841
+ keyboardType="numeric"
842
+ variant="filled"
843
+ inputStyle={[
844
+ p[3],
845
+ r.md,
846
+ bg.neutral[800],
847
+ text.white,
848
+ borders.width.thin,
849
+ borders.color.neutral[600],
850
+ w.percent[100],
851
+ ]}
852
+ />
853
+ </View>
722
854
  </View>
723
855
  </View>
724
- </View>
856
+ )}
725
857
  </View>
726
858
  )}
727
859
 
@@ -5,14 +5,6 @@ export { ContentMetadataForm } from "./content-metadata-form";
5
5
  export { ContentRights } from "./content-rights";
6
6
  export { ContentWarnings } from "./content-warnings";
7
7
 
8
- // Types
9
- export type {
10
- ContentMetadata,
11
- ContentMetadataFormProps,
12
- DistributionPolicy,
13
- Rights,
14
- } from "./content-metadata-form";
15
-
16
8
  export type { ContentRightsProps } from "./content-rights";
17
9
 
18
10
  export type { ContentWarningsProps } from "./content-warnings";
@@ -0,0 +1,15 @@
1
+ let ScreenOrientation: typeof import("expo-screen-orientation") | null = null;
2
+
3
+ try {
4
+ ScreenOrientation = require("expo-screen-orientation");
5
+ } catch {
6
+ // expo-screen-orientation not available
7
+ if (__DEV__) {
8
+ console.warn(
9
+ "expo-screen-orientation not installed, rotation features disabled",
10
+ );
11
+ }
12
+ }
13
+
14
+ export const isRotationAvailable = ScreenOrientation != null;
15
+ export { ScreenOrientation };
@@ -0,0 +1,200 @@
1
+ import type { Orientation } from "expo-screen-orientation";
2
+ import React, {
3
+ createContext,
4
+ useContext,
5
+ useEffect,
6
+ useMemo,
7
+ useState,
8
+ } from "react";
9
+ import {
10
+ isRotationAvailable,
11
+ ScreenOrientation,
12
+ } from "./rotation-async.native";
13
+
14
+ export interface RotationContextValue {
15
+ currentOrientation: Orientation;
16
+ isLocked: boolean;
17
+ isActive: boolean;
18
+ rotateToLandscape: () => Promise<void>;
19
+ rotateToPortrait: () => Promise<void>;
20
+ toggleRotation: () => Promise<void>;
21
+ canRotate: boolean;
22
+ }
23
+
24
+ export interface RotationProviderProps {
25
+ children: React.ReactNode;
26
+ enabled?: boolean;
27
+ }
28
+
29
+ const RotationContext = createContext<RotationContextValue | null>(null);
30
+
31
+ export const RotationProvider: React.FC<RotationProviderProps> = ({
32
+ children,
33
+ enabled = true,
34
+ }) => {
35
+ const [isLocked, setIsLocked] = useState(false);
36
+ const [canRotate, setCanRotate] = useState(isRotationAvailable);
37
+ const [currentOrientation, setCurrentOrientation] = useState<Orientation>(
38
+ ScreenOrientation?.Orientation.PORTRAIT_UP ?? 1,
39
+ );
40
+
41
+ // If module not available, provide disabled context
42
+ if (!isRotationAvailable || !ScreenOrientation) {
43
+ const disabledContextValue: RotationContextValue = {
44
+ currentOrientation: 1, // Orientation.PORTRAIT_UP
45
+ isLocked: false,
46
+ isActive: false,
47
+ rotateToLandscape: async () => {},
48
+ rotateToPortrait: async () => {},
49
+ toggleRotation: async () => {},
50
+ canRotate: false,
51
+ };
52
+
53
+ return (
54
+ <RotationContext.Provider value={disabledContextValue}>
55
+ {children}
56
+ </RotationContext.Provider>
57
+ );
58
+ }
59
+
60
+ // Manual rotation functions
61
+ const rotateToLandscape = async () => {
62
+ if (!enabled || !canRotate || !ScreenOrientation) return;
63
+
64
+ try {
65
+ await ScreenOrientation.unlockAsync();
66
+ await ScreenOrientation.lockAsync(
67
+ ScreenOrientation.OrientationLock.LANDSCAPE_RIGHT,
68
+ );
69
+ setIsLocked(true);
70
+
71
+ // set current orientation to landscape right
72
+ setCurrentOrientation(ScreenOrientation.Orientation.LANDSCAPE_RIGHT);
73
+
74
+ if (__DEV__) {
75
+ console.log("📲 Manual landscape");
76
+ }
77
+ } catch (error) {
78
+ console.warn("Failed to rotate to landscape:", error);
79
+ }
80
+ };
81
+
82
+ const rotateToPortrait = async () => {
83
+ if (!enabled || !canRotate || !ScreenOrientation) return;
84
+
85
+ try {
86
+ await ScreenOrientation.unlockAsync();
87
+ await ScreenOrientation.lockAsync(
88
+ ScreenOrientation.OrientationLock.PORTRAIT_UP,
89
+ );
90
+ setIsLocked(true);
91
+
92
+ setCurrentOrientation(ScreenOrientation.Orientation.PORTRAIT_UP);
93
+
94
+ if (__DEV__) {
95
+ console.log("📲 Manual portrait");
96
+ }
97
+ } catch (error) {
98
+ console.warn("Failed to rotate to portrait:", error);
99
+ }
100
+ };
101
+
102
+ const toggleRotation = async () => {
103
+ if (!ScreenOrientation) return;
104
+
105
+ const isLandscape =
106
+ currentOrientation === ScreenOrientation.Orientation.LANDSCAPE_LEFT ||
107
+ currentOrientation === ScreenOrientation.Orientation.LANDSCAPE_RIGHT;
108
+
109
+ if (__DEV__) {
110
+ console.log(
111
+ `🔄 Toggle: current=${currentOrientation}, isLandscape=${isLandscape}`,
112
+ );
113
+ }
114
+
115
+ if (isLandscape) {
116
+ await rotateToPortrait();
117
+ } else {
118
+ await rotateToLandscape();
119
+ }
120
+ };
121
+
122
+ // Track orientation changes
123
+ useEffect(() => {
124
+ if (!enabled) return;
125
+
126
+ const getCurrentOrientation = async () => {
127
+ if (!ScreenOrientation) return;
128
+ try {
129
+ const orient = await ScreenOrientation.getOrientationAsync();
130
+ setCurrentOrientation(orient);
131
+
132
+ if (__DEV__) {
133
+ console.log(`📲 Orientation on load: ${orient}`);
134
+ }
135
+ } catch (error) {
136
+ console.warn("Failed to get orientation:", error);
137
+ }
138
+ };
139
+
140
+ getCurrentOrientation();
141
+
142
+ if (!ScreenOrientation) return;
143
+
144
+ const subscription = ScreenOrientation.addOrientationChangeListener(
145
+ (event) => {
146
+ const newOrientation = event.orientationInfo.orientation;
147
+ setCurrentOrientation(newOrientation);
148
+
149
+ if (__DEV__) {
150
+ console.log(
151
+ `🔄 Orientation: ${newOrientation} (locked: ${isLocked})`,
152
+ );
153
+ }
154
+ },
155
+ );
156
+
157
+ return () => {
158
+ subscription?.remove();
159
+ if (ScreenOrientation) {
160
+ ScreenOrientation.removeOrientationChangeListeners();
161
+ ScreenOrientation.unlockAsync().catch(() => {});
162
+ }
163
+ };
164
+ }, [enabled]);
165
+
166
+ const contextValue = useMemo(
167
+ () => ({
168
+ currentOrientation,
169
+ isLocked,
170
+ isActive: enabled,
171
+ rotateToLandscape,
172
+ rotateToPortrait,
173
+ toggleRotation,
174
+ canRotate,
175
+ }),
176
+ [
177
+ currentOrientation,
178
+ isLocked,
179
+ enabled,
180
+ canRotate,
181
+ rotateToLandscape,
182
+ rotateToPortrait,
183
+ toggleRotation,
184
+ ],
185
+ );
186
+
187
+ return (
188
+ <RotationContext.Provider value={contextValue}>
189
+ {children}
190
+ </RotationContext.Provider>
191
+ );
192
+ };
193
+
194
+ export const useRotation = (): RotationContextValue => {
195
+ const context = useContext(RotationContext);
196
+ if (!context) {
197
+ throw new Error("useRotation must be used within a RotationProvider");
198
+ }
199
+ return context;
200
+ };
package/src/index.tsx CHANGED
@@ -33,6 +33,16 @@ export * from "./components/chat/system-message";
33
33
  export { default as VideoRetry } from "./components/mobile-player/video-retry";
34
34
  export * from "./lib/system-messages";
35
35
 
36
+ // Rotation lock system exports
37
+ export {
38
+ RotationProvider,
39
+ useRotation,
40
+ } from "./components/mobile-player/rotation-lock";
41
+ export type {
42
+ RotationContextValue,
43
+ RotationProviderProps,
44
+ } from "./components/mobile-player/rotation-lock";
45
+
36
46
  export * from "./components/share/sharesheet";
37
47
 
38
48
  export * from "./components/keep-awake";