Skip to content
Source
Sign inSign up

VideoPreviewModal.android.tsx

186 lines6.2 KB

1import { videoMimeType } from "@t3tools/shared/video";
2import { Modal, Pressable, View } from "react-native";
3import { useSafeAreaInsets } from "react-native-safe-area-context";
4​
5import { AppActivityIndicator } from "./AppActivityIndicator";
6import { useLocalAttachmentPreview } from "../../lib/localAttachmentPreview";
7import { usePreviewPresentation, videoPreviewKey } from "../../lib/previewPresentation";
8import { useMediaActions } from "../../state/mediaActions";
9import { type MediaActionsSource } from "../../lib/mediaActionsSource";
10import {
11 mediaVideoPreviewUri,
12 mediaVideoThumbnailKey,
13 type LocalVideoPreviewSource,
14 type MediaVideoPreviewSource,
15 type VideoPreviewSource,
16} from "../../lib/videoPreviewSource";
17import { useAssetUrlState, useRefreshAssetUrl } from "../../state/assets";
18import { usePreparedConnection } from "../../state/session";
19import { AppText } from "./AppText";
20import { MediaPreviewHeader } from "../MediaPreviewHeader";
21import { MediaVideoPlayer } from "../MediaVideoPlayer";
22​
23export type { VideoPreviewSource } from "../../lib/videoPreviewSource";
24​
25interface PlaybackState {
26 readonly uri: string | null;
27 readonly resolvePlaybackUri?: () => Promise<string | null>;
28 readonly unavailable: boolean;
29 readonly error: string | null;
30 readonly actionsSource: MediaActionsSource | undefined;
32​
33function useMediaPlayback(source: MediaVideoPreviewSource): PlaybackState {
34 const environmentId = "environmentId" in source ? source.environmentId : null;
35 const resource = "resource" in source ? source.resource : null;
36 const connection = usePreparedConnection(environmentId);
37 const asset = useAssetUrlState(environmentId, resource);
38 const refreshAssetUrl = useRefreshAssetUrl(environmentId, resource);
39 const uri = mediaVideoPreviewUri(source, asset._tag === "Success" ? asset.url : null);
40 return {
41 uri,
42 ...(resource !== null
43 ? { resolvePlaybackUri: async () => mediaVideoPreviewUri(source, await refreshAssetUrl()) }
44 : {}),
45 unavailable:
46 uri === null &&
47 environmentId !== null &&
48 (connection._tag === "None" || asset._tag === "Failure"),
49 error: null,
50 actionsSource: source.actionsSource,
51 };
53​
54function useLocalPlayback(source: LocalVideoPreviewSource): PlaybackState {
55 const { attachment } = source;
56 const preview = useLocalAttachmentPreview(attachment);
57 const { uri } = preview;
58 const error = preview.error ? preview.error.message || "Could not load this video." : null;
59 const mimeType = videoMimeType(attachment) ?? attachment.mimeType;
60 return {
61 uri,
62 unavailable: error !== null,
63 error,
64 actionsSource: uri === null ? undefined : { name: attachment.name, mimeType, uri },
65 };
67​
68function MediaPreviewModal(props: {
69 readonly source: MediaVideoPreviewSource;
70 readonly onRequestClose: () => void;
71}) {
72 return (
73 <OpenVideoPreviewModal
74 name={props.source.name}
75 thumbnailKey={mediaVideoThumbnailKey(props.source)}
76 playback={useMediaPlayback(props.source)}
77 onRequestClose={props.onRequestClose}
78 />
79 );
81​
82function LocalPreviewModal(props: {
83 readonly source: LocalVideoPreviewSource;
84 readonly onRequestClose: () => void;
85}) {
86 return (
87 <OpenVideoPreviewModal
88 name={props.source.attachment.name}
89 thumbnailKey={`local:${props.source.attachment.id}`}
90 playback={useLocalPlayback(props.source)}
91 onRequestClose={props.onRequestClose}
92 />
93 );
95​
96function OpenVideoPreviewModal(props: {
97 readonly name: string;
98 readonly thumbnailKey: string;
99 readonly playback: PlaybackState;
100 readonly onRequestClose: () => void;
101}) {
102 const { playback } = props;
103 const insets = useSafeAreaInsets();
104 const mediaActions = useMediaActions(playback.actionsSource, props.onRequestClose);
105​
106 return (
107 <Modal
108 visible
109 animationType="fade"
110 presentationStyle="fullScreen"
111 onRequestClose={props.onRequestClose}
112 statusBarTranslucent
113 navigationBarTranslucent
114 >
115 <View
116 className="flex-1 bg-black"
117 style={{ paddingTop: insets.top, paddingBottom: insets.bottom }}
118 >
119 <MediaPreviewHeader
120 title={props.name}
121 media={mediaActions}
122 closeAccessibilityLabel="Close video"
123 onClose={props.onRequestClose}
124 />
125 {playback.uri === null && !playback.unavailable ? (
126 <View className="flex-1 items-center justify-center gap-3 px-6">
127 <AppActivityIndicator color="#ffffff" />
128 <AppText className="text-sm text-white/80">Loading video...</AppText>
129 </View>
130 ) : (
131 <MediaVideoPlayer
132 uri={playback.uri}
133 resolvePlaybackUri={playback.resolvePlaybackUri}
134 name={props.name}
135 thumbnailKey={props.thumbnailKey}
136 unavailable={playback.unavailable}
137 paused={mediaActions.sharing}
138 expanded
139 autoPlay
140 />
141 )}
142 {playback.error ? (
143 <AppText accessibilityRole="alert" className="px-4 pb-3 text-sm text-white/80">
144 {playback.error}
145 </AppText>
146 ) : null}
147 <Pressable
148 accessibilityRole="button"
149 accessibilityLabel="Save or share video"
150 disabled={playback.uri === null || mediaActions.sharing}
151 onPress={mediaActions.share}
152 className="mx-4 my-3 min-h-12 items-center justify-center rounded-xl bg-white/15 px-4"
153 >
154 <AppText className="font-t3-medium text-base text-white">
155 {mediaActions.sharing ? "Opening share sheet..." : "Save or share video"}
156 </AppText>
157 </Pressable>
158 </View>
159 </Modal>
160 );
162​
163export function VideoPreviewModal(props: {
164 readonly source: VideoPreviewSource | null;
165 readonly onRequestClose: () => void;
166}) {
167 usePreviewPresentation(
168 props.source === null ? null : videoPreviewKey(props.source),
169 props.onRequestClose,
170 );
171 const { source } = props;
172 if (source === null) return null;
173 return source.type === "local" ? (
174 <LocalPreviewModal
175 key={videoPreviewKey(source)}
176 source={source}
177 onRequestClose={props.onRequestClose}
178 />
179 ) : (
180 <MediaPreviewModal
181 key={videoPreviewKey(source)}
182 source={source}
183 onRequestClose={props.onRequestClose}
184 />
185 );