Merge remote-tracking branch 'origin/main' into Improve-style-consistency
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
import * as PlatformInfo from './src/PlatformInfo'
|
import * as PlatformInfo from './src/PlatformInfo'
|
||||||
|
import {AudioCategory} from './src/PlatformInfo/types'
|
||||||
import * as Referrer from './src/Referrer'
|
import * as Referrer from './src/Referrer'
|
||||||
import * as SharedPrefs from './src/SharedPrefs'
|
import * as SharedPrefs from './src/SharedPrefs'
|
||||||
import VisibilityView from './src/VisibilityView'
|
import VisibilityView from './src/VisibilityView'
|
||||||
|
|
||||||
export {PlatformInfo, Referrer, SharedPrefs, VisibilityView}
|
export {AudioCategory, PlatformInfo, Referrer, SharedPrefs, VisibilityView}
|
||||||
|
|||||||
@@ -7,5 +7,36 @@ public class ExpoPlatformInfoModule: Module {
|
|||||||
Function("getIsReducedMotionEnabled") {
|
Function("getIsReducedMotionEnabled") {
|
||||||
return UIAccessibility.isReduceMotionEnabled
|
return UIAccessibility.isReduceMotionEnabled
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Function("setAudioCategory") { (audioCategoryString: String) in
|
||||||
|
let audioCategory = AVAudioSession.Category(rawValue: audioCategoryString)
|
||||||
|
try? AVAudioSession.sharedInstance().setCategory(audioCategory)
|
||||||
|
}
|
||||||
|
|
||||||
|
Function("setAudioActive") { (active: Bool) in
|
||||||
|
var categoryOptions: AVAudioSession.CategoryOptions
|
||||||
|
let currentCategory = AVAudioSession.sharedInstance().category
|
||||||
|
|
||||||
|
if active {
|
||||||
|
categoryOptions = [.mixWithOthers]
|
||||||
|
try? AVAudioSession.sharedInstance().setActive(true)
|
||||||
|
} else {
|
||||||
|
categoryOptions = [.duckOthers]
|
||||||
|
try? AVAudioSession
|
||||||
|
.sharedInstance()
|
||||||
|
.setActive(
|
||||||
|
false,
|
||||||
|
options: [.notifyOthersOnDeactivation]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
try? AVAudioSession
|
||||||
|
.sharedInstance()
|
||||||
|
.setCategory(
|
||||||
|
currentCategory,
|
||||||
|
mode: .default,
|
||||||
|
options: categoryOptions
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,20 @@
|
|||||||
|
import {Platform} from 'react-native'
|
||||||
import {requireNativeModule} from 'expo-modules-core'
|
import {requireNativeModule} from 'expo-modules-core'
|
||||||
|
|
||||||
|
import {AudioCategory} from './types'
|
||||||
|
|
||||||
const NativeModule = requireNativeModule('ExpoPlatformInfo')
|
const NativeModule = requireNativeModule('ExpoPlatformInfo')
|
||||||
|
|
||||||
export function getIsReducedMotionEnabled(): boolean {
|
export function getIsReducedMotionEnabled(): boolean {
|
||||||
return NativeModule.getIsReducedMotionEnabled()
|
return NativeModule.getIsReducedMotionEnabled()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function setAudioActive(active: boolean): void {
|
||||||
|
if (Platform.OS !== 'ios') return
|
||||||
|
NativeModule.setAudioActive(active)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setAudioCategory(audioCategory: AudioCategory): void {
|
||||||
|
if (Platform.OS !== 'ios') return
|
||||||
|
NativeModule.setAudioCategory(audioCategory)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,25 @@
|
|||||||
import {NotImplementedError} from '../NotImplemented'
|
import {NotImplementedError} from '../NotImplemented'
|
||||||
|
import {AudioCategory} from './types'
|
||||||
|
|
||||||
export function getIsReducedMotionEnabled(): boolean {
|
export function getIsReducedMotionEnabled(): boolean {
|
||||||
throw new NotImplementedError()
|
throw new NotImplementedError()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set whether the app's audio should mix with other apps' audio. Will also resume background music playback when `false`
|
||||||
|
* if it was previously playing.
|
||||||
|
* @param mixWithOthers
|
||||||
|
* @see https://developer.apple.com/documentation/avfaudio/avaudiosession/setactiveoptions/1616603-notifyothersondeactivation
|
||||||
|
*/
|
||||||
|
export function setAudioActive(active: boolean): void {
|
||||||
|
throw new NotImplementedError({active})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the audio category for the app.
|
||||||
|
* @param audioCategory
|
||||||
|
* @platform ios
|
||||||
|
*/
|
||||||
|
export function setAudioCategory(audioCategory: AudioCategory): void {
|
||||||
|
throw new NotImplementedError({audioCategory})
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,17 @@
|
|||||||
|
import {NotImplementedError} from '../NotImplemented'
|
||||||
|
import {AudioCategory} from './types'
|
||||||
|
|
||||||
export function getIsReducedMotionEnabled(): boolean {
|
export function getIsReducedMotionEnabled(): boolean {
|
||||||
if (typeof window === 'undefined') {
|
if (typeof window === 'undefined') {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return window.matchMedia('(prefers-reduced-motion: reduce)').matches
|
return window.matchMedia('(prefers-reduced-motion: reduce)').matches
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function setAudioActive(active: boolean): void {
|
||||||
|
throw new NotImplementedError({active})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setAudioCategory(audioCategory: AudioCategory): void {
|
||||||
|
throw new NotImplementedError({audioCategory})
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
/**
|
||||||
|
* Sets the audio session category on iOS. In general, we should only need to use this for the `playback` and `ambient`
|
||||||
|
* categories. This enum however includes other categories that are available in the native API for clarity and
|
||||||
|
* potential future use.
|
||||||
|
* @see https://developer.apple.com/documentation/avfoundation/avaudiosession/category
|
||||||
|
* @platform ios
|
||||||
|
*/
|
||||||
|
export enum AudioCategory {
|
||||||
|
Ambient = 'AVAudioSessionCategoryAmbient',
|
||||||
|
Playback = 'AVAudioSessionCategoryPlayback',
|
||||||
|
_SoloAmbient = 'AVAudioSessionCategorySoloAmbient',
|
||||||
|
_Record = 'AVAudioSessionCategoryRecord',
|
||||||
|
_PlayAndRecord = 'AVAudioSessionCategoryPlayAndRecord',
|
||||||
|
_MultiRoute = 'AVAudioSessionCategoryMultiRoute',
|
||||||
|
}
|
||||||
+1
-1
@@ -139,7 +139,7 @@
|
|||||||
"expo-system-ui": "~3.0.4",
|
"expo-system-ui": "~3.0.4",
|
||||||
"expo-task-manager": "~11.8.1",
|
"expo-task-manager": "~11.8.1",
|
||||||
"expo-updates": "~0.25.14",
|
"expo-updates": "~0.25.14",
|
||||||
"expo-video": "^1.1.10",
|
"expo-video": "^1.2.4",
|
||||||
"expo-web-browser": "~13.0.3",
|
"expo-web-browser": "~13.0.3",
|
||||||
"fast-text-encoding": "^1.0.6",
|
"fast-text-encoding": "^1.0.6",
|
||||||
"history": "^5.3.0",
|
"history": "^5.3.0",
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
--- a/node_modules/expo-video/android/src/main/java/expo/modules/video/PlayerViewExtension.kt
|
|
||||||
+++ b/node_modules/expo-video/android/src/main/java/expo/modules/video/PlayerViewExtension.kt
|
|
||||||
@@ -11,6 +11,7 @@ internal fun PlayerView.applyRequiresLinearPlayback(requireLinearPlayback: Boole
|
|
||||||
setShowPreviousButton(!requireLinearPlayback)
|
|
||||||
setShowNextButton(!requireLinearPlayback)
|
|
||||||
setTimeBarInteractive(requireLinearPlayback)
|
|
||||||
+ setShowSubtitleButton(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
|
|
||||||
@@ -27,7 +28,8 @@ internal fun PlayerView.setTimeBarInteractive(interactive: Boolean) {
|
|
||||||
|
|
||||||
@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
|
|
||||||
internal fun PlayerView.setFullscreenButtonVisibility(visible: Boolean) {
|
|
||||||
- val fullscreenButton = findViewById<android.widget.ImageButton>(androidx.media3.ui.R.id.exo_fullscreen)
|
|
||||||
+ val fullscreenButton =
|
|
||||||
+ findViewById<android.widget.ImageButton>(androidx.media3.ui.R.id.exo_fullscreen)
|
|
||||||
fullscreenButton?.visibility = if (visible) {
|
|
||||||
android.view.View.VISIBLE
|
|
||||||
} else {
|
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
diff --git a/node_modules/expo-video/android/src/main/java/expo/modules/video/PlayerViewExtension.kt b/node_modules/expo-video/android/src/main/java/expo/modules/video/PlayerViewExtension.kt
|
||||||
|
index 9905e13..47342ff 100644
|
||||||
|
--- a/node_modules/expo-video/android/src/main/java/expo/modules/video/PlayerViewExtension.kt
|
||||||
|
+++ b/node_modules/expo-video/android/src/main/java/expo/modules/video/PlayerViewExtension.kt
|
||||||
|
@@ -11,6 +11,7 @@ internal fun PlayerView.applyRequiresLinearPlayback(requireLinearPlayback: Boole
|
||||||
|
setShowPreviousButton(!requireLinearPlayback)
|
||||||
|
setShowNextButton(!requireLinearPlayback)
|
||||||
|
setTimeBarInteractive(requireLinearPlayback)
|
||||||
|
+ setShowSubtitleButton(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
|
||||||
|
@@ -27,7 +28,8 @@ internal fun PlayerView.setTimeBarInteractive(interactive: Boolean) {
|
||||||
|
|
||||||
|
@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
|
||||||
|
internal fun PlayerView.setFullscreenButtonVisibility(visible: Boolean) {
|
||||||
|
- val fullscreenButton = findViewById<android.widget.ImageButton>(androidx.media3.ui.R.id.exo_fullscreen)
|
||||||
|
+ val fullscreenButton =
|
||||||
|
+ findViewById<android.widget.ImageButton>(androidx.media3.ui.R.id.exo_fullscreen)
|
||||||
|
fullscreenButton?.visibility = if (visible) {
|
||||||
|
android.view.View.VISIBLE
|
||||||
|
} else {
|
||||||
|
diff --git a/node_modules/expo-video/android/src/main/java/expo/modules/video/VideoModule.kt b/node_modules/expo-video/android/src/main/java/expo/modules/video/VideoModule.kt
|
||||||
|
index ec3da2a..5a1397a 100644
|
||||||
|
--- a/node_modules/expo-video/android/src/main/java/expo/modules/video/VideoModule.kt
|
||||||
|
+++ b/node_modules/expo-video/android/src/main/java/expo/modules/video/VideoModule.kt
|
||||||
|
@@ -43,7 +43,9 @@ class VideoModule : Module() {
|
||||||
|
View(VideoView::class) {
|
||||||
|
Events(
|
||||||
|
"onPictureInPictureStart",
|
||||||
|
- "onPictureInPictureStop"
|
||||||
|
+ "onPictureInPictureStop",
|
||||||
|
+ "onEnterFullscreen",
|
||||||
|
+ "onExitFullscreen"
|
||||||
|
)
|
||||||
|
|
||||||
|
Prop("player") { view: VideoView, player: VideoPlayer ->
|
||||||
|
diff --git a/node_modules/expo-video/android/src/main/java/expo/modules/video/VideoView.kt b/node_modules/expo-video/android/src/main/java/expo/modules/video/VideoView.kt
|
||||||
|
index a951d80..3932535 100644
|
||||||
|
--- a/node_modules/expo-video/android/src/main/java/expo/modules/video/VideoView.kt
|
||||||
|
+++ b/node_modules/expo-video/android/src/main/java/expo/modules/video/VideoView.kt
|
||||||
|
@@ -36,6 +36,8 @@ class VideoView(context: Context, appContext: AppContext) : ExpoView(context, ap
|
||||||
|
val playerView: PlayerView = PlayerView(context.applicationContext)
|
||||||
|
val onPictureInPictureStart by EventDispatcher<Unit>()
|
||||||
|
val onPictureInPictureStop by EventDispatcher<Unit>()
|
||||||
|
+ val onEnterFullscreen by EventDispatcher()
|
||||||
|
+ val onExitFullscreen by EventDispatcher()
|
||||||
|
|
||||||
|
var willEnterPiP: Boolean = false
|
||||||
|
var isInFullscreen: Boolean = false
|
||||||
|
@@ -154,6 +156,7 @@ class VideoView(context: Context, appContext: AppContext) : ExpoView(context, ap
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
currentActivity.overridePendingTransition(0, 0)
|
||||||
|
}
|
||||||
|
+ onEnterFullscreen(mapOf())
|
||||||
|
isInFullscreen = true
|
||||||
|
}
|
||||||
|
|
||||||
|
@@ -162,6 +165,7 @@ class VideoView(context: Context, appContext: AppContext) : ExpoView(context, ap
|
||||||
|
val fullScreenButton: ImageButton = playerView.findViewById(androidx.media3.ui.R.id.exo_fullscreen)
|
||||||
|
fullScreenButton.setImageResource(androidx.media3.ui.R.drawable.exo_icon_fullscreen_enter)
|
||||||
|
videoPlayer?.changePlayerView(playerView)
|
||||||
|
+ this.onExitFullscreen(mapOf())
|
||||||
|
isInFullscreen = false
|
||||||
|
}
|
||||||
|
|
||||||
|
diff --git a/node_modules/expo-video/build/VideoView.types.d.ts b/node_modules/expo-video/build/VideoView.types.d.ts
|
||||||
|
index cb9ca6d..60e9f4e 100644
|
||||||
|
--- a/node_modules/expo-video/build/VideoView.types.d.ts
|
||||||
|
+++ b/node_modules/expo-video/build/VideoView.types.d.ts
|
||||||
|
@@ -89,5 +89,8 @@ export interface VideoViewProps extends ViewProps {
|
||||||
|
* @platform ios 16.0+
|
||||||
|
*/
|
||||||
|
allowsVideoFrameAnalysis?: boolean;
|
||||||
|
+
|
||||||
|
+ onEnterFullscreen?: () => void;
|
||||||
|
+ onExitFullscreen?: () => void;
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=VideoView.types.d.ts.map
|
||||||
|
diff --git a/node_modules/expo-video/ios/VideoModule.swift b/node_modules/expo-video/ios/VideoModule.swift
|
||||||
|
index c537a12..e4a918f 100644
|
||||||
|
--- a/node_modules/expo-video/ios/VideoModule.swift
|
||||||
|
+++ b/node_modules/expo-video/ios/VideoModule.swift
|
||||||
|
@@ -16,7 +16,9 @@ public final class VideoModule: Module {
|
||||||
|
View(VideoView.self) {
|
||||||
|
Events(
|
||||||
|
"onPictureInPictureStart",
|
||||||
|
- "onPictureInPictureStop"
|
||||||
|
+ "onPictureInPictureStop",
|
||||||
|
+ "onEnterFullscreen",
|
||||||
|
+ "onExitFullscreen"
|
||||||
|
)
|
||||||
|
|
||||||
|
Prop("player") { (view, player: VideoPlayer?) in
|
||||||
|
diff --git a/node_modules/expo-video/ios/VideoView.swift b/node_modules/expo-video/ios/VideoView.swift
|
||||||
|
index f4579e4..10c5908 100644
|
||||||
|
--- a/node_modules/expo-video/ios/VideoView.swift
|
||||||
|
+++ b/node_modules/expo-video/ios/VideoView.swift
|
||||||
|
@@ -41,6 +41,8 @@ public final class VideoView: ExpoView, AVPlayerViewControllerDelegate {
|
||||||
|
|
||||||
|
let onPictureInPictureStart = EventDispatcher()
|
||||||
|
let onPictureInPictureStop = EventDispatcher()
|
||||||
|
+ let onEnterFullscreen = EventDispatcher()
|
||||||
|
+ let onExitFullscreen = EventDispatcher()
|
||||||
|
|
||||||
|
public override var bounds: CGRect {
|
||||||
|
didSet {
|
||||||
|
@@ -163,6 +165,7 @@ public final class VideoView: ExpoView, AVPlayerViewControllerDelegate {
|
||||||
|
_ playerViewController: AVPlayerViewController,
|
||||||
|
willBeginFullScreenPresentationWithAnimationCoordinator coordinator: UIViewControllerTransitionCoordinator
|
||||||
|
) {
|
||||||
|
+ onEnterFullscreen()
|
||||||
|
isFullscreen = true
|
||||||
|
}
|
||||||
|
|
||||||
|
@@ -179,6 +182,7 @@ public final class VideoView: ExpoView, AVPlayerViewControllerDelegate {
|
||||||
|
if wasPlaying {
|
||||||
|
self.player?.pointer.play()
|
||||||
|
}
|
||||||
|
+ self.onExitFullscreen()
|
||||||
|
self.isFullscreen = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
diff --git a/node_modules/expo-video/src/VideoView.types.ts b/node_modules/expo-video/src/VideoView.types.ts
|
||||||
|
index 29fe5db..e1fbf59 100644
|
||||||
|
--- a/node_modules/expo-video/src/VideoView.types.ts
|
||||||
|
+++ b/node_modules/expo-video/src/VideoView.types.ts
|
||||||
|
@@ -100,4 +100,7 @@ export interface VideoViewProps extends ViewProps {
|
||||||
|
* @platform ios 16.0+
|
||||||
|
*/
|
||||||
|
allowsVideoFrameAnalysis?: boolean;
|
||||||
|
+
|
||||||
|
+ onEnterFullscreen?: () => void;
|
||||||
|
+ onExitFullscreen?: () => void;
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
## uwu woad beawing, do not wemove
|
||||||
|
|
||||||
|
## `expo-video` Patch
|
||||||
|
|
||||||
|
This patch adds two props to `VideoView`: `onEnterFullscreen` and `onExitFullscreen` which do exactly what they say on
|
||||||
|
the tin.
|
||||||
@@ -61,6 +61,7 @@ import {Provider as PortalProvider} from '#/components/Portal'
|
|||||||
import {Splash} from '#/Splash'
|
import {Splash} from '#/Splash'
|
||||||
import {Provider as TourProvider} from '#/tours'
|
import {Provider as TourProvider} from '#/tours'
|
||||||
import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider'
|
import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider'
|
||||||
|
import {AudioCategory, PlatformInfo} from '../modules/expo-bluesky-swiss-army'
|
||||||
|
|
||||||
SplashScreen.preventAutoHideAsync()
|
SplashScreen.preventAutoHideAsync()
|
||||||
|
|
||||||
@@ -157,6 +158,8 @@ function App() {
|
|||||||
const [isReady, setReady] = useState(false)
|
const [isReady, setReady] = useState(false)
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
|
PlatformInfo.setAudioCategory(AudioCategory.Ambient)
|
||||||
|
PlatformInfo.setAudioActive(true)
|
||||||
initPersistedState().then(() => setReady(true))
|
initPersistedState().then(() => setReady(true))
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
|||||||
@@ -249,7 +249,7 @@ function DialogError({details}: {details?: string}) {
|
|||||||
const control = Dialog.useDialogContext()
|
const control = Dialog.useDialogContext()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog.ScrollableInner style={a.gap_md} label={_(msg`An error occured`)}>
|
<Dialog.ScrollableInner style={a.gap_md} label={_(msg`An error has occurred`)}>
|
||||||
<Dialog.Close />
|
<Dialog.Close />
|
||||||
<ErrorScreen
|
<ErrorScreen
|
||||||
title={_(msg`Oh no!`)}
|
title={_(msg`Oh no!`)}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import {View} from 'react-native'
|
|||||||
// @ts-expect-error no type definition
|
// @ts-expect-error no type definition
|
||||||
import ProgressPie from 'react-native-progress/Pie'
|
import ProgressPie from 'react-native-progress/Pie'
|
||||||
import {ImagePickerAsset} from 'expo-image-picker'
|
import {ImagePickerAsset} from 'expo-image-picker'
|
||||||
|
import {Trans} from '@lingui/macro'
|
||||||
|
|
||||||
import {atoms as a, useTheme} from '#/alf'
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
@@ -46,7 +47,9 @@ export function VideoTranscodeProgress({
|
|||||||
color={t.atoms.text.color}
|
color={t.atoms.text.color}
|
||||||
progress={progress}
|
progress={progress}
|
||||||
/>
|
/>
|
||||||
<Text>Compressing...</Text>
|
<Text>
|
||||||
|
<Trans>Compressing...</Trans>
|
||||||
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, {memo, useMemo, useState} from 'react'
|
import React, {memo, useId, useMemo, useState} from 'react'
|
||||||
import {StyleSheet, View} from 'react-native'
|
import {StyleSheet, View} from 'react-native'
|
||||||
import {
|
import {
|
||||||
AppBskyActorDefs,
|
AppBskyActorDefs,
|
||||||
@@ -137,7 +137,6 @@ let FeedItemInner = ({
|
|||||||
const {openComposer} = useComposerControls()
|
const {openComposer} = useComposerControls()
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const gate = useGate()
|
|
||||||
|
|
||||||
const href = useMemo(() => {
|
const href = useMemo(() => {
|
||||||
const urip = new AtUri(post.uri)
|
const urip = new AtUri(post.uri)
|
||||||
@@ -356,9 +355,7 @@ let FeedItemInner = ({
|
|||||||
postAuthor={post.author}
|
postAuthor={post.author}
|
||||||
onOpenEmbed={onOpenEmbed}
|
onOpenEmbed={onOpenEmbed}
|
||||||
/>
|
/>
|
||||||
{gate('video_debug') && (
|
<VideoDebug />
|
||||||
<VideoEmbed source="https://lumi.jazco.dev/watch/did:plc:q6gjnaw2blty4crticxkmujt/Qmc8w93UpTa2adJHg4ZhnDPrBs1EsbzrekzPcqF5SwusuZ/playlist.m3u8" />
|
|
||||||
)}
|
|
||||||
<PostCtrls
|
<PostCtrls
|
||||||
post={post}
|
post={post}
|
||||||
record={record}
|
record={record}
|
||||||
@@ -501,6 +498,19 @@ function ReplyToLabel({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function VideoDebug() {
|
||||||
|
const gate = useGate()
|
||||||
|
const id = useId()
|
||||||
|
|
||||||
|
if (!gate('video_debug')) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<VideoEmbed
|
||||||
|
source={`https://lumi.jazco.dev/watch/did:plc:q6gjnaw2blty4crticxkmujt/Qmc8w93UpTa2adJHg4ZhnDPrBs1EsbzrekzPcqF5SwusuZ/playlist.m3u8?ignore_me_just_testing_frontend_stuff=${id}`}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
outer: {
|
outer: {
|
||||||
paddingLeft: 10,
|
paddingLeft: 10,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React from 'react'
|
import React, {useCallback, useState} from 'react'
|
||||||
import {View} from 'react-native'
|
import {View} from 'react-native'
|
||||||
import {msg} from '@lingui/macro'
|
import {msg, Trans} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
import {VideoEmbedInnerNative} from 'view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative'
|
import {VideoEmbedInnerNative} from 'view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative'
|
||||||
@@ -8,13 +8,23 @@ import {atoms as a, useTheme} from '#/alf'
|
|||||||
import {Button, ButtonIcon} from '#/components/Button'
|
import {Button, ButtonIcon} from '#/components/Button'
|
||||||
import {Play_Filled_Corner2_Rounded as PlayIcon} from '#/components/icons/Play'
|
import {Play_Filled_Corner2_Rounded as PlayIcon} from '#/components/icons/Play'
|
||||||
import {VisibilityView} from '../../../../../modules/expo-bluesky-swiss-army'
|
import {VisibilityView} from '../../../../../modules/expo-bluesky-swiss-army'
|
||||||
|
import {ErrorBoundary} from '../ErrorBoundary'
|
||||||
import {useActiveVideoView} from './ActiveVideoContext'
|
import {useActiveVideoView} from './ActiveVideoContext'
|
||||||
|
import * as VideoFallback from './VideoEmbedInner/VideoFallback'
|
||||||
|
|
||||||
export function VideoEmbed({source}: {source: string}) {
|
export function VideoEmbed({source}: {source: string}) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {active, setActive} = useActiveVideoView({source})
|
const {active, setActive} = useActiveVideoView({source})
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
|
|
||||||
|
const [key, setKey] = useState(0)
|
||||||
|
const renderError = useCallback(
|
||||||
|
(error: unknown) => (
|
||||||
|
<VideoError error={error} retry={() => setKey(key + 1)} />
|
||||||
|
),
|
||||||
|
[key],
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
@@ -25,27 +35,42 @@ export function VideoEmbed({source}: {source: string}) {
|
|||||||
t.atoms.bg_contrast_25,
|
t.atoms.bg_contrast_25,
|
||||||
a.my_xs,
|
a.my_xs,
|
||||||
]}>
|
]}>
|
||||||
<VisibilityView
|
<ErrorBoundary renderError={renderError} key={key}>
|
||||||
enabled={true}
|
<VisibilityView
|
||||||
onChangeStatus={isActive => {
|
enabled={true}
|
||||||
if (isActive) {
|
onChangeStatus={isActive => {
|
||||||
setActive()
|
if (isActive) {
|
||||||
}
|
setActive()
|
||||||
}}>
|
}
|
||||||
{active ? (
|
}}>
|
||||||
<VideoEmbedInnerNative />
|
{active ? (
|
||||||
) : (
|
<VideoEmbedInnerNative />
|
||||||
<Button
|
) : (
|
||||||
style={[a.flex_1, t.atoms.bg_contrast_25]}
|
<Button
|
||||||
onPress={setActive}
|
style={[a.flex_1, t.atoms.bg_contrast_25]}
|
||||||
label={_(msg`Play video`)}
|
onPress={setActive}
|
||||||
variant="ghost"
|
label={_(msg`Play video`)}
|
||||||
color="secondary"
|
variant="ghost"
|
||||||
size="large">
|
color="secondary"
|
||||||
<ButtonIcon icon={PlayIcon} />
|
size="large">
|
||||||
</Button>
|
<ButtonIcon icon={PlayIcon} />
|
||||||
)}
|
</Button>
|
||||||
</VisibilityView>
|
)}
|
||||||
|
</VisibilityView>
|
||||||
|
</ErrorBoundary>
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function VideoError({retry}: {error: unknown; retry: () => void}) {
|
||||||
|
return (
|
||||||
|
<VideoFallback.Container>
|
||||||
|
<VideoFallback.Text>
|
||||||
|
<Trans>
|
||||||
|
An error occurred while loading the video. Please try again later.
|
||||||
|
</Trans>
|
||||||
|
</VideoFallback.Text>
|
||||||
|
<VideoFallback.RetryButton onPress={retry} />
|
||||||
|
</VideoFallback.Container>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,17 +1,15 @@
|
|||||||
import React, {useCallback, useEffect, useRef, useState} from 'react'
|
import React, {useCallback, useEffect, useRef, useState} from 'react'
|
||||||
import {View} from 'react-native'
|
import {View} from 'react-native'
|
||||||
import {msg, Trans} from '@lingui/macro'
|
import {Trans} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
HLSUnsupportedError,
|
HLSUnsupportedError,
|
||||||
VideoEmbedInnerWeb,
|
VideoEmbedInnerWeb,
|
||||||
} from 'view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerWeb'
|
} from 'view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerWeb'
|
||||||
import {atoms as a, useTheme} from '#/alf'
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
import {Button, ButtonText} from '#/components/Button'
|
|
||||||
import {Text} from '#/components/Typography'
|
|
||||||
import {ErrorBoundary} from '../ErrorBoundary'
|
import {ErrorBoundary} from '../ErrorBoundary'
|
||||||
import {useActiveVideoView} from './ActiveVideoContext'
|
import {useActiveVideoView} from './ActiveVideoContext'
|
||||||
|
import * as VideoFallback from './VideoEmbedInner/VideoFallback'
|
||||||
|
|
||||||
export function VideoEmbed({source}: {source: string}) {
|
export function VideoEmbed({source}: {source: string}) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
@@ -138,32 +136,11 @@ function ViewportObserver({
|
|||||||
}
|
}
|
||||||
|
|
||||||
function VideoError({error, retry}: {error: unknown; retry: () => void}) {
|
function VideoError({error, retry}: {error: unknown; retry: () => void}) {
|
||||||
const t = useTheme()
|
|
||||||
const {_} = useLingui()
|
|
||||||
|
|
||||||
const isHLS = error instanceof HLSUnsupportedError
|
const isHLS = error instanceof HLSUnsupportedError
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View
|
<VideoFallback.Container>
|
||||||
style={[
|
<VideoFallback.Text>
|
||||||
a.flex_1,
|
|
||||||
t.atoms.bg_contrast_25,
|
|
||||||
a.justify_center,
|
|
||||||
a.align_center,
|
|
||||||
a.px_lg,
|
|
||||||
a.border,
|
|
||||||
t.atoms.border_contrast_low,
|
|
||||||
a.rounded_sm,
|
|
||||||
a.gap_lg,
|
|
||||||
]}>
|
|
||||||
<Text
|
|
||||||
style={[
|
|
||||||
a.text_center,
|
|
||||||
t.atoms.text_contrast_high,
|
|
||||||
a.text_md,
|
|
||||||
a.leading_snug,
|
|
||||||
{maxWidth: 300},
|
|
||||||
]}>
|
|
||||||
{isHLS ? (
|
{isHLS ? (
|
||||||
<Trans>
|
<Trans>
|
||||||
Your browser does not support the video format. Please try a
|
Your browser does not support the video format. Please try a
|
||||||
@@ -174,19 +151,8 @@ function VideoError({error, retry}: {error: unknown; retry: () => void}) {
|
|||||||
An error occurred while loading the video. Please try again later.
|
An error occurred while loading the video. Please try again later.
|
||||||
</Trans>
|
</Trans>
|
||||||
)}
|
)}
|
||||||
</Text>
|
</VideoFallback.Text>
|
||||||
{!isHLS && (
|
{!isHLS && <VideoFallback.RetryButton onPress={retry} />}
|
||||||
<Button
|
</VideoFallback.Container>
|
||||||
onPress={retry}
|
|
||||||
size="small"
|
|
||||||
color="secondary_inverted"
|
|
||||||
variant="solid"
|
|
||||||
label={_(msg`Retry`)}>
|
|
||||||
<ButtonText>
|
|
||||||
<Trans>Retry</Trans>
|
|
||||||
</ButtonText>
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,27 +1,76 @@
|
|||||||
import React, {useEffect, useRef, useState} from 'react'
|
import React, {useCallback, useEffect, useRef, useState} from 'react'
|
||||||
import {Pressable, View} from 'react-native'
|
import {Pressable, View} from 'react-native'
|
||||||
|
import Animated, {FadeInDown, FadeOutDown} from 'react-native-reanimated'
|
||||||
import {VideoPlayer, VideoView} from 'expo-video'
|
import {VideoPlayer, VideoView} from 'expo-video'
|
||||||
|
import {msg} from '@lingui/macro'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
|
import {useIsFocused} from '@react-navigation/native'
|
||||||
|
|
||||||
import {useVideoPlayer} from 'view/com/util/post-embeds/VideoPlayerContext'
|
import {HITSLOP_30} from '#/lib/constants'
|
||||||
import {android, atoms as a} from '#/alf'
|
import {useAppState} from '#/lib/hooks/useAppState'
|
||||||
|
import {useVideoPlayer} from '#/view/com/util/post-embeds/VideoPlayerContext'
|
||||||
|
import {android, atoms as a, useTheme} from '#/alf'
|
||||||
|
import {Mute_Stroke2_Corner0_Rounded as MuteIcon} from '#/components/icons/Mute'
|
||||||
|
import {SpeakerVolumeFull_Stroke2_Corner0_Rounded as UnmuteIcon} from '#/components/icons/Speaker'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
|
import {
|
||||||
|
AudioCategory,
|
||||||
|
PlatformInfo,
|
||||||
|
} from '../../../../../../modules/expo-bluesky-swiss-army'
|
||||||
|
|
||||||
export function VideoEmbedInnerNative() {
|
export function VideoEmbedInnerNative() {
|
||||||
const player = useVideoPlayer()
|
const player = useVideoPlayer()
|
||||||
const ref = useRef<VideoView>(null)
|
const ref = useRef<VideoView>(null)
|
||||||
|
const isScreenFocused = useIsFocused()
|
||||||
|
const isAppFocused = useAppState()
|
||||||
|
const prevFocusedRef = useRef(isAppFocused)
|
||||||
|
|
||||||
|
// resume video when coming back from background
|
||||||
|
useEffect(() => {
|
||||||
|
if (isAppFocused !== prevFocusedRef.current) {
|
||||||
|
prevFocusedRef.current = isAppFocused
|
||||||
|
if (isAppFocused === 'active') {
|
||||||
|
player.play()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [isAppFocused, player])
|
||||||
|
|
||||||
|
// pause the video when the screen is not focused
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isScreenFocused) {
|
||||||
|
let wasPlaying = player.playing
|
||||||
|
player.pause()
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (wasPlaying) player.play()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [isScreenFocused, player])
|
||||||
|
|
||||||
|
const enterFullscreen = useCallback(() => {
|
||||||
|
ref.current?.enterFullscreen()
|
||||||
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={[a.flex_1, a.relative]} collapsable={false}>
|
<View style={[a.flex_1, a.relative]}>
|
||||||
<VideoView
|
<VideoView
|
||||||
ref={ref}
|
ref={ref}
|
||||||
player={player}
|
player={player}
|
||||||
style={a.flex_1}
|
style={[a.flex_1, a.rounded_sm]}
|
||||||
nativeControls={true}
|
nativeControls={true}
|
||||||
|
onEnterFullscreen={() => {
|
||||||
|
PlatformInfo.setAudioCategory(AudioCategory.Playback)
|
||||||
|
PlatformInfo.setAudioActive(false)
|
||||||
|
player.muted = false
|
||||||
|
}}
|
||||||
|
onExitFullscreen={() => {
|
||||||
|
PlatformInfo.setAudioCategory(AudioCategory.Ambient)
|
||||||
|
PlatformInfo.setAudioActive(true)
|
||||||
|
player.muted = true
|
||||||
|
if (!player.playing) player.play()
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<Controls
|
<Controls player={player} enterFullscreen={enterFullscreen} />
|
||||||
player={player}
|
|
||||||
enterFullscreen={() => ref.current?.enterFullscreen()}
|
|
||||||
/>
|
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -33,6 +82,9 @@ function Controls({
|
|||||||
player: VideoPlayer
|
player: VideoPlayer
|
||||||
enterFullscreen: () => void
|
enterFullscreen: () => void
|
||||||
}) {
|
}) {
|
||||||
|
const {_} = useLingui()
|
||||||
|
const t = useTheme()
|
||||||
|
const [isMuted, setIsMuted] = useState(player.muted)
|
||||||
const [duration, setDuration] = useState(() => Math.floor(player.duration))
|
const [duration, setDuration] = useState(() => Math.floor(player.duration))
|
||||||
const [currentTime, setCurrentTime] = useState(() =>
|
const [currentTime, setCurrentTime] = useState(() =>
|
||||||
Math.floor(player.currentTime),
|
Math.floor(player.currentTime),
|
||||||
@@ -47,50 +99,121 @@ function Controls({
|
|||||||
// duration gets reset to 0 on loop
|
// duration gets reset to 0 on loop
|
||||||
if (player.duration) setDuration(Math.floor(player.duration))
|
if (player.duration) setDuration(Math.floor(player.duration))
|
||||||
setCurrentTime(Math.floor(player.currentTime))
|
setCurrentTime(Math.floor(player.currentTime))
|
||||||
|
|
||||||
// how often should we update the time?
|
// how often should we update the time?
|
||||||
// 1000 gets out of sync with the video time
|
// 1000 gets out of sync with the video time
|
||||||
}, 250)
|
}, 250)
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-shadow
|
||||||
|
const sub = player.addListener('volumeChange', ({isMuted}) => {
|
||||||
|
setIsMuted(isMuted)
|
||||||
|
})
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
clearInterval(interval)
|
clearInterval(interval)
|
||||||
|
sub.remove()
|
||||||
}
|
}
|
||||||
}, [player])
|
}, [player])
|
||||||
|
|
||||||
if (isNaN(timeRemaining)) {
|
const onPressFullscreen = useCallback(() => {
|
||||||
return null
|
switch (player.status) {
|
||||||
}
|
case 'idle':
|
||||||
|
case 'loading':
|
||||||
|
case 'readyToPlay': {
|
||||||
|
if (!player.playing) player.play()
|
||||||
|
enterFullscreen()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'error': {
|
||||||
|
player.replay()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [player, enterFullscreen])
|
||||||
|
|
||||||
|
const toggleMuted = useCallback(() => {
|
||||||
|
const muted = !player.muted
|
||||||
|
// We want to set this to the _inverse_ of the new value, because we actually want for the audio to be mixed when
|
||||||
|
// the video is muted, and vice versa.
|
||||||
|
const mix = !muted
|
||||||
|
const category = muted ? AudioCategory.Ambient : AudioCategory.Playback
|
||||||
|
|
||||||
|
PlatformInfo.setAudioCategory(category)
|
||||||
|
PlatformInfo.setAudioActive(mix)
|
||||||
|
player.muted = muted
|
||||||
|
}, [player])
|
||||||
|
|
||||||
|
// show countdown when:
|
||||||
|
// 1. timeRemaining is a number - was seeing NaNs
|
||||||
|
// 2. duration is greater than 0 - means metadata has loaded
|
||||||
|
// 3. we're less than 5 second into the video
|
||||||
|
const showTime = !isNaN(timeRemaining) && duration > 0 && currentTime <= 5
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={[a.absolute, a.inset_0]}>
|
<View style={[a.absolute, a.inset_0]}>
|
||||||
<View
|
{showTime && (
|
||||||
style={[
|
<Animated.View
|
||||||
{
|
entering={FadeInDown.duration(300)}
|
||||||
backgroundColor: 'rgba(0, 0, 0, 0.75',
|
exiting={FadeOutDown.duration(500)}
|
||||||
|
style={[
|
||||||
|
{
|
||||||
|
backgroundColor: 'rgba(0, 0, 0, 0.75)',
|
||||||
|
borderRadius: 6,
|
||||||
|
paddingHorizontal: 6,
|
||||||
|
paddingVertical: 3,
|
||||||
|
position: 'absolute',
|
||||||
|
left: 5,
|
||||||
|
bottom: 5,
|
||||||
|
minHeight: 20,
|
||||||
|
justifyContent: 'center',
|
||||||
|
},
|
||||||
|
]}>
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
{color: t.palette.white, fontSize: 12},
|
||||||
|
a.font_bold,
|
||||||
|
android({lineHeight: 1.25}),
|
||||||
|
]}>
|
||||||
|
{minutes}:{seconds}
|
||||||
|
</Text>
|
||||||
|
</Animated.View>
|
||||||
|
)}
|
||||||
|
<Pressable
|
||||||
|
onPress={onPressFullscreen}
|
||||||
|
style={a.flex_1}
|
||||||
|
accessibilityLabel={_(msg`Video`)}
|
||||||
|
accessibilityHint={_(msg`Tap to enter full screen`)}
|
||||||
|
accessibilityRole="button"
|
||||||
|
/>
|
||||||
|
{duration > 0 && (
|
||||||
|
<Animated.View
|
||||||
|
entering={FadeInDown.duration(300)}
|
||||||
|
style={{
|
||||||
|
backgroundColor: 'rgba(0, 0, 0, 0.75)',
|
||||||
borderRadius: 6,
|
borderRadius: 6,
|
||||||
paddingHorizontal: 6,
|
paddingHorizontal: 6,
|
||||||
paddingVertical: 3,
|
paddingVertical: 3,
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
left: 5,
|
|
||||||
bottom: 5,
|
bottom: 5,
|
||||||
},
|
right: 5,
|
||||||
]}
|
minHeight: 20,
|
||||||
pointerEvents="none">
|
justifyContent: 'center',
|
||||||
<Text
|
}}>
|
||||||
style={[
|
<Pressable
|
||||||
{color: 'white', fontSize: 12},
|
onPress={toggleMuted}
|
||||||
a.font_bold,
|
style={a.flex_1}
|
||||||
android({lineHeight: 1.25}),
|
accessibilityLabel={isMuted ? _(msg`Muted`) : _(msg`Unmuted`)}
|
||||||
]}>
|
accessibilityHint={_(msg`Tap to toggle sound`)}
|
||||||
{minutes}:{seconds}
|
accessibilityRole="button"
|
||||||
</Text>
|
hitSlop={HITSLOP_30}>
|
||||||
</View>
|
{isMuted ? (
|
||||||
<Pressable
|
<MuteIcon width={14} fill={t.palette.white} />
|
||||||
onPress={enterFullscreen}
|
) : (
|
||||||
style={a.flex_1}
|
<UnmuteIcon width={14} fill={t.palette.white} />
|
||||||
accessibilityLabel="Video"
|
)}
|
||||||
accessibilityHint="Tap to enter full screen"
|
</Pressable>
|
||||||
accessibilityRole="button"
|
</Animated.View>
|
||||||
/>
|
)}
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import {View} from 'react-native'
|
||||||
|
import {msg, Trans} from '@lingui/macro'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
|
import {Button, ButtonText} from '#/components/Button'
|
||||||
|
import {Text as TypoText} from '#/components/Typography'
|
||||||
|
|
||||||
|
export function Container({children}: {children: React.ReactNode}) {
|
||||||
|
const t = useTheme()
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.flex_1,
|
||||||
|
t.atoms.bg_contrast_25,
|
||||||
|
a.justify_center,
|
||||||
|
a.align_center,
|
||||||
|
a.px_lg,
|
||||||
|
a.border,
|
||||||
|
t.atoms.border_contrast_low,
|
||||||
|
a.rounded_sm,
|
||||||
|
a.gap_lg,
|
||||||
|
]}>
|
||||||
|
{children}
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Text({children}: {children: React.ReactNode}) {
|
||||||
|
const t = useTheme()
|
||||||
|
return (
|
||||||
|
<TypoText
|
||||||
|
style={[
|
||||||
|
a.text_center,
|
||||||
|
t.atoms.text_contrast_high,
|
||||||
|
a.text_md,
|
||||||
|
a.leading_snug,
|
||||||
|
{maxWidth: 300},
|
||||||
|
]}>
|
||||||
|
{children}
|
||||||
|
</TypoText>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RetryButton({onPress}: {onPress: () => void}) {
|
||||||
|
const {_} = useLingui()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
onPress={onPress}
|
||||||
|
size="small"
|
||||||
|
color="secondary_inverted"
|
||||||
|
variant="solid"
|
||||||
|
label={_(msg`Retry`)}>
|
||||||
|
<ButtonText>
|
||||||
|
<Trans>Retry</Trans>
|
||||||
|
</ButtonText>
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -14,6 +14,7 @@ export function VideoPlayerProvider({
|
|||||||
// eslint-disable-next-line @typescript-eslint/no-shadow
|
// eslint-disable-next-line @typescript-eslint/no-shadow
|
||||||
const player = useExpoVideoPlayer(source, player => {
|
const player = useExpoVideoPlayer(source, player => {
|
||||||
player.loop = true
|
player.loop = true
|
||||||
|
player.muted = true
|
||||||
player.play()
|
player.play()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -12302,10 +12302,10 @@ expo-updates@~0.25.14:
|
|||||||
ignore "^5.3.1"
|
ignore "^5.3.1"
|
||||||
resolve-from "^5.0.0"
|
resolve-from "^5.0.0"
|
||||||
|
|
||||||
expo-video@^1.1.10:
|
expo-video@^1.2.4:
|
||||||
version "1.1.10"
|
version "1.2.4"
|
||||||
resolved "https://registry.yarnpkg.com/expo-video/-/expo-video-1.1.10.tgz#b47c0d40c21f401236639424bd25d70c09316b7b"
|
resolved "https://registry.yarnpkg.com/expo-video/-/expo-video-1.2.4.tgz#787342aded4295a1b6864f59227d178b93e1bb53"
|
||||||
integrity sha512-k9ecpgtwAK8Ut8enm8Jv398XkB/uVOyLLqk80M/d8pH9EN5CVrBQ7iEzWlR3quvVUFM7Uf5wRukJ4hk3mZ8NCg==
|
integrity sha512-pBK9mt7vYAbuPQjCSQxHQ7xrNjbmRheJep7JIStEg57O183/JRfP2blKuXniiSt1HBdZYPdoQnGRa3jGMXB9pg==
|
||||||
|
|
||||||
expo-web-browser@~13.0.3:
|
expo-web-browser@~13.0.3:
|
||||||
version "13.0.3"
|
version "13.0.3"
|
||||||
|
|||||||
Reference in New Issue
Block a user