diff --git a/apps/common-app/src/App.tsx b/apps/common-app/src/App.tsx index 4204e65ad..c9b6eb581 100644 --- a/apps/common-app/src/App.tsx +++ b/apps/common-app/src/App.tsx @@ -30,8 +30,7 @@ const HomeScreen: FC = () => { style={({ pressed }) => [ styles.button, { borderStyle: pressed ? 'solid' : 'dashed' }, - ]} - > + ]}> {item.title} {item.subtitle} @@ -64,8 +63,7 @@ const App: FC = () => { headerTintColor: colors.white, headerBackTitle: ' ', headerBackAccessibilityLabel: 'Go back', - }} - > + }}> = (props) => { return ( + style={[ + styles.basic, + centered && styles.centered, + !disablePadding && styles.padding, + style, + ]}> {children} diff --git a/apps/common-app/src/components/VerticalSlider.tsx b/apps/common-app/src/components/VerticalSlider.tsx new file mode 100644 index 000000000..889ba92b6 --- /dev/null +++ b/apps/common-app/src/components/VerticalSlider.tsx @@ -0,0 +1,119 @@ +import React, { useEffect } from 'react'; +import { View, Text, StyleSheet } from 'react-native'; +import { GestureDetector, Gesture } from 'react-native-gesture-handler'; +import Animated, { + useSharedValue, + useAnimatedStyle, +} from 'react-native-reanimated'; +import { scheduleOnRN } from 'react-native-worklets'; + +const SLIDER_HEIGHT = 150; +const THUMB_SIZE = 30; +const TRACK_HEIGHT = SLIDER_HEIGHT - THUMB_SIZE; + +interface VerticalSliderProps { + label: string; + value: number; + onValueChange: (val: number) => void; +} + +const VerticalSlider: React.FC = ({ + label, + value, + onValueChange, +}) => { + const progress = useSharedValue(value); + const startValue = useSharedValue(0); + + useEffect(() => { + progress.value = value; + }, [value, progress]); + + const gesture = Gesture.Pan() + .onStart(() => { + 'worklet'; + startValue.value = progress.value; + }) + .onUpdate((e) => { + 'worklet'; + const change = -e.translationY / TRACK_HEIGHT; + const newValue = startValue.value + change; + progress.value = Math.min(Math.max(newValue, 0), 1); + scheduleOnRN(onValueChange, progress.value); + }); + + const thumbStyle = useAnimatedStyle(() => { + const translateY = (1 - progress.value) * TRACK_HEIGHT; + return { + transform: [{ translateY }], + }; + }); + + return ( + + {label} + + + + + + + + + {(value * 100).toFixed(0)} + + ); +}; + +const styles = StyleSheet.create({ + sliderContainer: { + alignItems: 'center', + gap: 5, + height: SLIDER_HEIGHT + 40, + }, + sliderLabel: { + fontWeight: 'bold', + fontSize: 12, + color: '#333', + }, + sliderTrackContainer: { + width: 40, + height: SLIDER_HEIGHT, + justifyContent: 'center', + alignItems: 'center', + }, + sliderTrack: { + position: 'absolute', + width: 4, + height: '100%', + backgroundColor: '#111', + borderRadius: 2, + }, + sliderThumbHitArea: { + position: 'absolute', + top: 0, + width: 40, + height: THUMB_SIZE, + justifyContent: 'center', + alignItems: 'center', + }, + sliderThumb: { + width: 30, + height: 15, + backgroundColor: '#222', + borderWidth: 1, + borderColor: '#fff', + borderRadius: 2, + shadowColor: '#000', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.5, + shadowRadius: 2, + }, + sliderValue: { + fontSize: 10, + color: '#555', + fontVariant: ['tabular-nums'], + }, +}); + +export default VerticalSlider; diff --git a/apps/common-app/src/components/index.ts b/apps/common-app/src/components/index.ts index 95b235bb7..c3e93660e 100644 --- a/apps/common-app/src/components/index.ts +++ b/apps/common-app/src/components/index.ts @@ -5,3 +5,4 @@ export { default as Switch } from './Switch'; export { default as Select } from './Select'; export { default as Container } from './Container'; export { default as BGGradient } from './BGGradient'; +export { default as VerticalSlider } from './VerticalSlider'; diff --git a/apps/common-app/src/examples/AudioFile/AudioPlayer.ts b/apps/common-app/src/examples/AudioFile/AudioPlayer.ts index 91ef3695b..34a127013 100644 --- a/apps/common-app/src/examples/AudioFile/AudioPlayer.ts +++ b/apps/common-app/src/examples/AudioFile/AudioPlayer.ts @@ -1,4 +1,7 @@ -import { AudioContext, PlaybackNotificationManager } from 'react-native-audio-api'; +import { + AudioContext, + PlaybackNotificationManager, +} from 'react-native-audio-api'; import type { AudioBufferSourceNode, AudioBuffer, diff --git a/apps/common-app/src/examples/Distorted/Distorted.tsx b/apps/common-app/src/examples/Distorted/Distorted.tsx new file mode 100644 index 000000000..579508c72 --- /dev/null +++ b/apps/common-app/src/examples/Distorted/Distorted.tsx @@ -0,0 +1,103 @@ +import React, { useCallback, useEffect, useState, FC } from 'react'; +import { ActivityIndicator } from 'react-native'; +import { + AudioContext, + AudioNode, + AudioBuffer, + AudioBufferSourceNode, +} from 'react-native-audio-api'; +import { Container, Button } from '../../components'; +import { presetEffects } from '../../utils/effects'; + +const URL = 'https://files.catbox.moe/s2i1wn.flac'; + +const Distorted: FC = () => { + const [isPlaying, setIsPlaying] = useState(false); + const [isLoading, setIsLoading] = useState(false); + const [buffer, setBuffer] = useState(null); + + const aCtxRef = React.useRef(null); + const effectsMap = React.useRef | null>(null); + const sourceNodeRef = React.useRef(null); + + const fetchAudioBuffer = useCallback(async () => { + setIsLoading(true); + + if (!aCtxRef.current) { + aCtxRef.current = new AudioContext(); + } + const audioContext = aCtxRef.current; + + effectsMap.current = presetEffects.distorted(audioContext); + + const audioBuffer = await fetch(URL, { + headers: { + 'User-Agent': + 'Mozilla/5.0 (Android; Mobile; rv:122.0) Gecko/122.0 Firefox/122.0', + }, + }) + .then((response) => response.arrayBuffer()) + .then((arrayBuffer) => audioContext.decodeAudioData(arrayBuffer)) + .catch((error) => { + console.error('Error decoding audio data source:', error); + return null; + }); + + setBuffer(audioBuffer); + + setIsLoading(false); + }, []); + + const togglePlayPause = useCallback(async () => { + if (!aCtxRef.current) { + return; + } + + if (buffer === null) { + fetchAudioBuffer(); + return; + } + + if (isPlaying) { + sourceNodeRef.current?.stop(); + } else { + await aCtxRef.current.resume(); + sourceNodeRef.current = aCtxRef.current.createBufferSource(); + sourceNodeRef.current.buffer = buffer; + + let previousNode: AudioNode = sourceNodeRef.current; + effectsMap.current?.forEach((node) => { + previousNode.connect(node); + previousNode = node; + }); + + previousNode.connect(aCtxRef.current.destination); + + sourceNodeRef.current.start(); + } + + setIsPlaying((prev) => !prev); + }, [isPlaying, buffer, fetchAudioBuffer]); + + useEffect(() => { + fetchAudioBuffer(); + + return () => { + aCtxRef.current?.close(); + aCtxRef.current = null; + }; + }, [fetchAudioBuffer]); + + return ( + + {isLoading && } +