vibefast-cli 0.1.1
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.
- package/FINAL-STATUS.md +144 -0
- package/HOW-IT-WORKS.md +559 -0
- package/PLAN.md +453 -0
- package/README.md +129 -0
- package/RECIPES-READY.md +172 -0
- package/STATUS.md +199 -0
- package/SUCCESS.md +259 -0
- package/TESTING-CHECKLIST.md +450 -0
- package/cloudflare-worker/.wrangler/state/v3/kv/64907821e2634080acce34618d2f3d4c/blobs/11f2769953c717e188062bc644da97c1fd1e4d6d0813a226ce7567dba759afab0000019a736fb8d4 +1 -0
- package/cloudflare-worker/.wrangler/state/v3/kv/miniflare-KVNamespaceObject/0b03767237c0408301af51ca35d4b09470cbc479c7e5f23cc9de774749d23c59.sqlite +0 -0
- package/cloudflare-worker/.wrangler/state/v3/kv/miniflare-KVNamespaceObject/0b03767237c0408301af51ca35d4b09470cbc479c7e5f23cc9de774749d23c59.sqlite-shm +0 -0
- package/cloudflare-worker/.wrangler/state/v3/kv/miniflare-KVNamespaceObject/0b03767237c0408301af51ca35d4b09470cbc479c7e5f23cc9de774749d23c59.sqlite-wal +0 -0
- package/cloudflare-worker/.wrangler/state/v3/r2/miniflare-R2BucketObject/d1cc388a1a0ef44dd5669fd1a165d168b61362136c8b5fa50aefd96c72688e54.sqlite +0 -0
- package/cloudflare-worker/.wrangler/state/v3/r2/miniflare-R2BucketObject/d1cc388a1a0ef44dd5669fd1a165d168b61362136c8b5fa50aefd96c72688e54.sqlite-shm +0 -0
- package/cloudflare-worker/.wrangler/state/v3/r2/miniflare-R2BucketObject/d1cc388a1a0ef44dd5669fd1a165d168b61362136c8b5fa50aefd96c72688e54.sqlite-wal +0 -0
- package/cloudflare-worker/.wrangler/state/v3/r2/vibefast-recipes/blobs/620e8cf7c35d9806da25dee237e1d7e8b2432bd98f755b60e2c7f08a48d2c7b90000019a73736484 +0 -0
- package/cloudflare-worker/MIGRATION.md +160 -0
- package/cloudflare-worker/QUICKSTART.md +200 -0
- package/cloudflare-worker/README.md +242 -0
- package/cloudflare-worker/generate-token.js +32 -0
- package/cloudflare-worker/mini-native@latest.zip +0 -0
- package/cloudflare-worker/setup.sh +143 -0
- package/cloudflare-worker/test-recipe/apps/native/src/app/mini/index.tsx +15 -0
- package/cloudflare-worker/test-recipe/recipe.json +16 -0
- package/cloudflare-worker/worker.js +308 -0
- package/cloudflare-worker/wrangler.toml +13 -0
- package/dist/commands/add.d.ts +3 -0
- package/dist/commands/add.d.ts.map +1 -0
- package/dist/commands/add.js +149 -0
- package/dist/commands/add.js.map +1 -0
- package/dist/commands/devices.d.ts +3 -0
- package/dist/commands/devices.d.ts.map +1 -0
- package/dist/commands/devices.js +35 -0
- package/dist/commands/devices.js.map +1 -0
- package/dist/commands/doctor.d.ts +3 -0
- package/dist/commands/doctor.d.ts.map +1 -0
- package/dist/commands/doctor.js +67 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/list.d.ts +3 -0
- package/dist/commands/list.d.ts.map +1 -0
- package/dist/commands/list.js +40 -0
- package/dist/commands/list.js.map +1 -0
- package/dist/commands/login.d.ts +3 -0
- package/dist/commands/login.d.ts.map +1 -0
- package/dist/commands/login.js +23 -0
- package/dist/commands/login.js.map +1 -0
- package/dist/commands/logout.d.ts +3 -0
- package/dist/commands/logout.d.ts.map +1 -0
- package/dist/commands/logout.js +16 -0
- package/dist/commands/logout.js.map +1 -0
- package/dist/commands/remove.d.ts +3 -0
- package/dist/commands/remove.d.ts.map +1 -0
- package/dist/commands/remove.js +67 -0
- package/dist/commands/remove.js.map +1 -0
- package/dist/core/__tests__/journal.test.d.ts +2 -0
- package/dist/core/__tests__/journal.test.d.ts.map +1 -0
- package/dist/core/__tests__/journal.test.js +101 -0
- package/dist/core/__tests__/journal.test.js.map +1 -0
- package/dist/core/__tests__/validate.test.d.ts +2 -0
- package/dist/core/__tests__/validate.test.d.ts.map +1 -0
- package/dist/core/__tests__/validate.test.js +53 -0
- package/dist/core/__tests__/validate.test.js.map +1 -0
- package/dist/core/archive.d.ts +2 -0
- package/dist/core/archive.d.ts.map +1 -0
- package/dist/core/archive.js +59 -0
- package/dist/core/archive.js.map +1 -0
- package/dist/core/auth.d.ts +15 -0
- package/dist/core/auth.d.ts.map +1 -0
- package/dist/core/auth.js +76 -0
- package/dist/core/auth.js.map +1 -0
- package/dist/core/codemod.d.ts +20 -0
- package/dist/core/codemod.d.ts.map +1 -0
- package/dist/core/codemod.js +150 -0
- package/dist/core/codemod.js.map +1 -0
- package/dist/core/fsx.d.ts +12 -0
- package/dist/core/fsx.d.ts.map +1 -0
- package/dist/core/fsx.js +70 -0
- package/dist/core/fsx.js.map +1 -0
- package/dist/core/http.d.ts +30 -0
- package/dist/core/http.d.ts.map +1 -0
- package/dist/core/http.js +95 -0
- package/dist/core/http.js.map +1 -0
- package/dist/core/journal.d.ts +18 -0
- package/dist/core/journal.d.ts.map +1 -0
- package/dist/core/journal.js +34 -0
- package/dist/core/journal.js.map +1 -0
- package/dist/core/log.d.ts +8 -0
- package/dist/core/log.d.ts.map +1 -0
- package/dist/core/log.js +9 -0
- package/dist/core/log.js.map +1 -0
- package/dist/core/pathGuard.d.ts +3 -0
- package/dist/core/pathGuard.d.ts.map +1 -0
- package/dist/core/pathGuard.js +18 -0
- package/dist/core/pathGuard.js.map +1 -0
- package/dist/core/paths.d.ts +11 -0
- package/dist/core/paths.d.ts.map +1 -0
- package/dist/core/paths.js +22 -0
- package/dist/core/paths.js.map +1 -0
- package/dist/core/validate.d.ts +8 -0
- package/dist/core/validate.d.ts.map +1 -0
- package/dist/core/validate.js +27 -0
- package/dist/core/validate.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +23 -0
- package/dist/index.js.map +1 -0
- package/docs/decisions.md +55 -0
- package/package.json +39 -0
- package/recipes/audio-recorder/apps/native/src/app/audio-recorder/index.tsx +5 -0
- package/recipes/audio-recorder/apps/native/src/features/audio-recorder/components/audio-player.tsx +301 -0
- package/recipes/audio-recorder/apps/native/src/features/audio-recorder/components/audio-recorder.tsx +373 -0
- package/recipes/audio-recorder/apps/native/src/features/audio-recorder/components/audio-waveform.tsx +270 -0
- package/recipes/audio-recorder/apps/native/src/features/audio-recorder/components/index.ts +4 -0
- package/recipes/audio-recorder/apps/native/src/features/audio-recorder/components/recording-list.tsx +89 -0
- package/recipes/audio-recorder/apps/native/src/features/audio-recorder/demo/audio-player-demo.tsx +66 -0
- package/recipes/audio-recorder/apps/native/src/features/audio-recorder/demo/audio-recorder-cloud.tsx +68 -0
- package/recipes/audio-recorder/apps/native/src/features/audio-recorder/demo/audio-recorder-interview.tsx +102 -0
- package/recipes/audio-recorder/apps/native/src/features/audio-recorder/demo/basic.tsx +27 -0
- package/recipes/audio-recorder/apps/native/src/features/audio-recorder/demo/index.ts +5 -0
- package/recipes/audio-recorder/apps/native/src/features/audio-recorder/demo/with-recording-list-demo.tsx +82 -0
- package/recipes/audio-recorder/recipe.json +22 -0
- package/recipes/audio-recorder@latest.zip +0 -0
- package/recipes/charts/apps/native/src/app/charts/index.tsx +3 -0
- package/recipes/charts/apps/native/src/features/charts/README.md +185 -0
- package/recipes/charts/apps/native/src/features/charts/app/preview.tsx +223 -0
- package/recipes/charts/apps/native/src/features/charts/components/area-chart.tsx +40 -0
- package/recipes/charts/apps/native/src/features/charts/components/bar-chart.tsx +143 -0
- package/recipes/charts/apps/native/src/features/charts/components/candlestick-chart.tsx +196 -0
- package/recipes/charts/apps/native/src/features/charts/components/chart-card.tsx +65 -0
- package/recipes/charts/apps/native/src/features/charts/components/column-chart.tsx +143 -0
- package/recipes/charts/apps/native/src/features/charts/components/doughnut-chart.tsx +246 -0
- package/recipes/charts/apps/native/src/features/charts/components/index.ts +10 -0
- package/recipes/charts/apps/native/src/features/charts/components/line-chart.tsx +308 -0
- package/recipes/charts/apps/native/src/features/charts/components/radar-chart.tsx +180 -0
- package/recipes/charts/apps/native/src/features/charts/components/radial-bar-chart.tsx +188 -0
- package/recipes/charts/apps/native/src/features/charts/components/stacked-area-chart.tsx +265 -0
- package/recipes/charts/apps/native/src/features/charts/components/stacked-bar-chart.tsx +322 -0
- package/recipes/charts/apps/native/src/features/charts/data/mock-data.ts +183 -0
- package/recipes/charts/apps/native/src/features/charts/types/index.ts +66 -0
- package/recipes/charts/recipe.json +22 -0
- package/recipes/charts@latest.zip +0 -0
- package/recipes/chatbot/apps/native/src/app/chatbot/index.tsx +1 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/app/index.tsx +302 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/components/chat-header-buttons.tsx +59 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/components/chat-input-bar.tsx +469 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/components/chat-markdown.tsx +575 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/components/chat-message-bubble.tsx +246 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/components/chat-settings-modal.tsx +161 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/components/image-preview-list.tsx +115 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/components/markdown/code-block.tsx +165 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/components/markdown/index.ts +10 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/components/markdown/table-renderer.tsx +129 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/components/message-error-boundary.tsx +78 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/components/message-list.tsx +173 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/components/model-selector.tsx +283 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/components/report-content-modal.tsx +188 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/components/suggested-messages.tsx +67 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/constants/models.ts +20 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/constants/report-reasons.ts +9 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/hooks/use-attachment-cache.ts +143 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/hooks/use-chat-config.ts +664 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/hooks/use-chat-handlers.ts +359 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/hooks/use-chatbot-settings.ts +89 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/hooks/use-conversation.ts +79 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/hooks/use-image-picker.ts +122 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/hooks/use-keyboard-coordinator.ts +161 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/hooks/use-smart-scroll-manager.ts +207 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/models/index.ts +86 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/models/models.ts +162 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/models/providers.ts +62 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/models/types.ts +40 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/services/file-uploader.ts +238 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/services/message-handler-service.ts +180 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/types/index.ts +60 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/utils/chat-telemetry.ts +91 -0
- package/recipes/chatbot/recipe.json +22 -0
- package/recipes/chatbot@latest.zip +0 -0
- package/recipes/image-generator/apps/native/src/app/image-generator/gallery.tsx +3 -0
- package/recipes/image-generator/apps/native/src/app/image-generator/index.tsx +3 -0
- package/recipes/image-generator/apps/native/src/features/image-generator/app/_layout.tsx +25 -0
- package/recipes/image-generator/apps/native/src/features/image-generator/app/gallery.tsx +217 -0
- package/recipes/image-generator/apps/native/src/features/image-generator/app/index.tsx +237 -0
- package/recipes/image-generator/apps/native/src/features/image-generator/components/gallery-image.tsx +26 -0
- package/recipes/image-generator/apps/native/src/features/image-generator/components/image-detail-modal.tsx +215 -0
- package/recipes/image-generator/apps/native/src/features/image-generator/components/image-model-selector.tsx +210 -0
- package/recipes/image-generator/apps/native/src/features/image-generator/components/image-placeholder.tsx +26 -0
- package/recipes/image-generator/apps/native/src/features/image-generator/hooks/use-image-gallery.ts +71 -0
- package/recipes/image-generator/apps/native/src/features/image-generator/hooks/use-image-generator-settings.ts +152 -0
- package/recipes/image-generator/apps/native/src/features/image-generator/hooks/use-image-generator.ts +93 -0
- package/recipes/image-generator/apps/native/src/features/image-generator/models/models.ts +66 -0
- package/recipes/image-generator/apps/native/src/features/image-generator/services/image-gallery-service.ts +98 -0
- package/recipes/image-generator/apps/native/src/features/image-generator/services/image-save-service.ts +121 -0
- package/recipes/image-generator/recipe.json +22 -0
- package/recipes/image-generator@latest.zip +0 -0
- package/recipes/quiz/apps/native/src/app/quiz/index.tsx +47 -0
- package/recipes/quiz/apps/native/src/features/quiz/components/question.tsx +67 -0
- package/recipes/quiz/apps/native/src/features/quiz/config.ts +11 -0
- package/recipes/quiz/apps/native/src/features/quiz/index.tsx +133 -0
- package/recipes/quiz/recipe.json +22 -0
- package/recipes/quiz@latest.zip +0 -0
- package/recipes/tracker-app/apps/native/src/app/tracker-app/index.tsx +1 -0
- package/recipes/tracker-app/apps/native/src/features/tracker-app/app/index.tsx +108 -0
- package/recipes/tracker-app/apps/native/src/features/tracker-app/components/animated-number.tsx +102 -0
- package/recipes/tracker-app/apps/native/src/features/tracker-app/components/calorie-card.tsx +66 -0
- package/recipes/tracker-app/apps/native/src/features/tracker-app/components/circular-progress.tsx +97 -0
- package/recipes/tracker-app/apps/native/src/features/tracker-app/components/floating-add-button.tsx +27 -0
- package/recipes/tracker-app/apps/native/src/features/tracker-app/components/macro-card.tsx +80 -0
- package/recipes/tracker-app/apps/native/src/features/tracker-app/components/promo-banner.tsx +98 -0
- package/recipes/tracker-app/apps/native/src/features/tracker-app/components/recently-logged.tsx +64 -0
- package/recipes/tracker-app/apps/native/src/features/tracker-app/components/week-calendar.tsx +68 -0
- package/recipes/tracker-app/recipe.json +22 -0
- package/recipes/tracker-app@latest.zip +0 -0
- package/recipes/upload-all.sh +32 -0
- package/recipes/voice-bot/apps/native/src/app/voice-bot/index.tsx +27 -0
- package/recipes/voice-bot/apps/native/src/features/voice-bot/README.md +185 -0
- package/recipes/voice-bot/apps/native/src/features/voice-bot/components/conversation-status.tsx +76 -0
- package/recipes/voice-bot/apps/native/src/features/voice-bot/components/index.ts +4 -0
- package/recipes/voice-bot/apps/native/src/features/voice-bot/components/message-input.tsx +98 -0
- package/recipes/voice-bot/apps/native/src/features/voice-bot/components/voice-bot-screen.tsx +173 -0
- package/recipes/voice-bot/apps/native/src/features/voice-bot/components/voice-controls.tsx +73 -0
- package/recipes/voice-bot/apps/native/src/features/voice-bot/index.ts +3 -0
- package/recipes/voice-bot/apps/native/src/features/voice-bot/services/index.ts +1 -0
- package/recipes/voice-bot/apps/native/src/features/voice-bot/services/use-voice-bot.ts +161 -0
- package/recipes/voice-bot/apps/native/src/features/voice-bot/types.ts +29 -0
- package/recipes/voice-bot/recipe.json +22 -0
- package/recipes/voice-bot@latest.zip +0 -0
- package/scripts/create-recipes.mjs +189 -0
- package/src/commands/add.ts +183 -0
- package/src/commands/devices.ts +38 -0
- package/src/commands/doctor.ts +67 -0
- package/src/commands/list.ts +45 -0
- package/src/commands/login.ts +24 -0
- package/src/commands/logout.ts +15 -0
- package/src/commands/remove.ts +78 -0
- package/src/core/__tests__/journal.test.ts +119 -0
- package/src/core/__tests__/validate.test.ts +64 -0
- package/src/core/archive.ts +69 -0
- package/src/core/auth.ts +103 -0
- package/src/core/codemod.ts +211 -0
- package/src/core/fsx.ts +80 -0
- package/src/core/http.ts +136 -0
- package/src/core/journal.ts +64 -0
- package/src/core/log.ts +9 -0
- package/src/core/pathGuard.ts +22 -0
- package/src/core/paths.ts +33 -0
- package/src/core/validate.ts +44 -0
- package/src/index.ts +27 -0
- package/test-critical-cases.mjs +258 -0
- package/tsconfig.json +21 -0
- package/vitest.config.mts +12 -0
package/recipes/audio-recorder/apps/native/src/features/audio-recorder/components/audio-recorder.tsx
ADDED
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AudioModule,
|
|
3
|
+
type RecordingOptions,
|
|
4
|
+
RecordingPresets,
|
|
5
|
+
setAudioModeAsync,
|
|
6
|
+
useAudioRecorder,
|
|
7
|
+
useAudioRecorderState,
|
|
8
|
+
} from 'expo-audio';
|
|
9
|
+
import { getInfoAsync } from 'expo-file-system/legacy';
|
|
10
|
+
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
|
11
|
+
import { Alert, Animated, View, type ViewStyle } from 'react-native';
|
|
12
|
+
|
|
13
|
+
import { Button, Icon, Text } from '@/components/ui';
|
|
14
|
+
import colors from '@/components/ui/colors';
|
|
15
|
+
import { useThemeConfig } from '@/lib/use-theme-config';
|
|
16
|
+
|
|
17
|
+
import { AudioPlayer } from './audio-player';
|
|
18
|
+
import { AudioWaveform } from './audio-waveform';
|
|
19
|
+
|
|
20
|
+
export interface AudioRecorderProps {
|
|
21
|
+
style?: ViewStyle;
|
|
22
|
+
quality?: 'high' | 'low';
|
|
23
|
+
showWaveform?: boolean;
|
|
24
|
+
showTimer?: boolean;
|
|
25
|
+
maxDuration?: number; // in seconds
|
|
26
|
+
onRecordingComplete?: (uri: string) => void;
|
|
27
|
+
onRecordingStart?: () => void;
|
|
28
|
+
onRecordingStop?: () => void;
|
|
29
|
+
customRecordingOptions?: RecordingOptions;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function AudioRecorder({
|
|
33
|
+
style,
|
|
34
|
+
quality = 'high',
|
|
35
|
+
showWaveform = true,
|
|
36
|
+
showTimer = true,
|
|
37
|
+
maxDuration,
|
|
38
|
+
onRecordingComplete,
|
|
39
|
+
onRecordingStart,
|
|
40
|
+
onRecordingStop,
|
|
41
|
+
customRecordingOptions,
|
|
42
|
+
}: AudioRecorderProps) {
|
|
43
|
+
const theme = useThemeConfig();
|
|
44
|
+
const recordingOptions =
|
|
45
|
+
customRecordingOptions ||
|
|
46
|
+
(quality === 'high'
|
|
47
|
+
? RecordingPresets.HIGH_QUALITY
|
|
48
|
+
: RecordingPresets.LOW_QUALITY);
|
|
49
|
+
|
|
50
|
+
const recorder = useAudioRecorder(recordingOptions);
|
|
51
|
+
const [permissionGranted, setPermissionGranted] = useState(false);
|
|
52
|
+
// Use recorder state duration instead of custom interval
|
|
53
|
+
const [recordingUri, setRecordingUri] = useState<string | null>(null);
|
|
54
|
+
const [isRecording, setIsRecording] = useState(false);
|
|
55
|
+
|
|
56
|
+
// Waveform data for real-time visualization
|
|
57
|
+
const [waveformData, setWaveformData] = useState<number[]>(
|
|
58
|
+
Array.from({ length: 30 }, () => 0.2),
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
// Animation values
|
|
62
|
+
const recordingPulse = useRef(new Animated.Value(1)).current;
|
|
63
|
+
|
|
64
|
+
// Request permissions on mount
|
|
65
|
+
useEffect(() => {
|
|
66
|
+
(async () => {
|
|
67
|
+
try {
|
|
68
|
+
const status = await AudioModule.requestRecordingPermissionsAsync();
|
|
69
|
+
setPermissionGranted(status.granted);
|
|
70
|
+
|
|
71
|
+
if (!status.granted) {
|
|
72
|
+
Alert.alert(
|
|
73
|
+
'Permission Required',
|
|
74
|
+
'Please grant microphone permission to record audio.',
|
|
75
|
+
[{ text: 'OK' }],
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
// Make sure the OS session is configured for recording & playback in silent mode
|
|
79
|
+
await setAudioModeAsync({
|
|
80
|
+
allowsRecording: true,
|
|
81
|
+
playsInSilentMode: true,
|
|
82
|
+
});
|
|
83
|
+
} catch (error) {
|
|
84
|
+
console.error('Error requesting permissions:', error);
|
|
85
|
+
setPermissionGranted(false);
|
|
86
|
+
}
|
|
87
|
+
})();
|
|
88
|
+
}, []);
|
|
89
|
+
|
|
90
|
+
// Recording pulse animation
|
|
91
|
+
useEffect(() => {
|
|
92
|
+
if (isRecording) {
|
|
93
|
+
const pulse = Animated.loop(
|
|
94
|
+
Animated.sequence([
|
|
95
|
+
Animated.timing(recordingPulse, {
|
|
96
|
+
toValue: 1.2,
|
|
97
|
+
duration: 600,
|
|
98
|
+
useNativeDriver: true,
|
|
99
|
+
}),
|
|
100
|
+
Animated.timing(recordingPulse, {
|
|
101
|
+
toValue: 1,
|
|
102
|
+
duration: 600,
|
|
103
|
+
useNativeDriver: true,
|
|
104
|
+
}),
|
|
105
|
+
]),
|
|
106
|
+
);
|
|
107
|
+
pulse.start();
|
|
108
|
+
|
|
109
|
+
return () => pulse.stop();
|
|
110
|
+
}
|
|
111
|
+
recordingPulse.setValue(1);
|
|
112
|
+
}, [isRecording, recordingPulse]);
|
|
113
|
+
|
|
114
|
+
const recState = useAudioRecorderState(recorder, 100); // updates ~every 100ms, includes time & metering
|
|
115
|
+
const durationSeconds = recState.durationMillis / 1000;
|
|
116
|
+
|
|
117
|
+
// Real-time waveform updates during recording
|
|
118
|
+
useEffect(() => {
|
|
119
|
+
if (!isRecording) {
|
|
120
|
+
setWaveformData(Array.from({ length: 30 }, () => 0.2));
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
// Use metering from recorder state when available, otherwise simulate
|
|
124
|
+
const db = typeof recState.metering === 'number' ? recState.metering : null;
|
|
125
|
+
const level =
|
|
126
|
+
db !== null
|
|
127
|
+
? Math.max(0.1, Math.min(1.0, (db + 50) / 50))
|
|
128
|
+
: 0.35 + (Math.random() - 0.5) * 0.25; // slightly more varied fallback
|
|
129
|
+
|
|
130
|
+
setWaveformData((prev) => [...prev.slice(1), level]);
|
|
131
|
+
}, [
|
|
132
|
+
isRecording,
|
|
133
|
+
recState.metering,
|
|
134
|
+
recState.canRecord,
|
|
135
|
+
recState.durationMillis,
|
|
136
|
+
recState.isRecording,
|
|
137
|
+
]);
|
|
138
|
+
|
|
139
|
+
// No custom duration interval; rely on recState.durationMillis
|
|
140
|
+
|
|
141
|
+
const handleStopRecording = useCallback(async () => {
|
|
142
|
+
try {
|
|
143
|
+
console.log('Stopping recording...');
|
|
144
|
+
setIsRecording(false);
|
|
145
|
+
|
|
146
|
+
await recorder.stop();
|
|
147
|
+
const uri = recorder.uri;
|
|
148
|
+
console.log('Recording stopped, URI:', uri);
|
|
149
|
+
|
|
150
|
+
if (uri) {
|
|
151
|
+
// === START DEBUG CODE ===
|
|
152
|
+
try {
|
|
153
|
+
const fileInfo = await getInfoAsync(uri);
|
|
154
|
+
console.log('Recorded File Info:', fileInfo);
|
|
155
|
+
if (fileInfo.exists && fileInfo.size === 0) {
|
|
156
|
+
Alert.alert(
|
|
157
|
+
'Debug Info',
|
|
158
|
+
'The recording was created but is empty (0 bytes). This is likely still a permissions issue.',
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
} catch (e) {
|
|
162
|
+
console.error("Couldn't get file info", e);
|
|
163
|
+
}
|
|
164
|
+
// === END DEBUG CODE ===
|
|
165
|
+
|
|
166
|
+
setRecordingUri(uri);
|
|
167
|
+
onRecordingComplete?.(uri);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
onRecordingStop?.();
|
|
171
|
+
} catch (error) {
|
|
172
|
+
console.error('Error stopping recording:', error);
|
|
173
|
+
Alert.alert('Error', 'Failed to stop recording. Please try again.');
|
|
174
|
+
}
|
|
175
|
+
}, [recorder, onRecordingComplete, onRecordingStop]);
|
|
176
|
+
|
|
177
|
+
// Auto-stop recording when max duration is reached
|
|
178
|
+
useEffect(() => {
|
|
179
|
+
if (maxDuration && durationSeconds >= maxDuration && isRecording) {
|
|
180
|
+
handleStopRecording();
|
|
181
|
+
}
|
|
182
|
+
}, [durationSeconds, maxDuration, isRecording, handleStopRecording]);
|
|
183
|
+
|
|
184
|
+
const handleStartRecording = async () => {
|
|
185
|
+
if (!permissionGranted) {
|
|
186
|
+
Alert.alert(
|
|
187
|
+
'Permission Required',
|
|
188
|
+
'Microphone permission is required to record audio.',
|
|
189
|
+
);
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
try {
|
|
194
|
+
console.log('Starting recording...');
|
|
195
|
+
setRecordingUri(null);
|
|
196
|
+
setIsRecording(true);
|
|
197
|
+
|
|
198
|
+
// Enable metering in recording options
|
|
199
|
+
const meteringOptions = {
|
|
200
|
+
...recordingOptions,
|
|
201
|
+
isMeteringEnabled: true,
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
await recorder.prepareToRecordAsync(meteringOptions);
|
|
205
|
+
recorder.record();
|
|
206
|
+
|
|
207
|
+
onRecordingStart?.();
|
|
208
|
+
console.log('Recording started successfully');
|
|
209
|
+
} catch (error) {
|
|
210
|
+
console.error('Error starting recording:', error);
|
|
211
|
+
setIsRecording(false);
|
|
212
|
+
// nothing to stop here; using recState
|
|
213
|
+
Alert.alert('Error', 'Failed to start recording. Please try again.');
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
const handleDeleteRecording = () => {
|
|
218
|
+
Alert.alert(
|
|
219
|
+
'Delete Recording',
|
|
220
|
+
'Are you sure you want to delete this recording?',
|
|
221
|
+
[
|
|
222
|
+
{ text: 'Cancel', style: 'cancel' },
|
|
223
|
+
{
|
|
224
|
+
text: 'Delete',
|
|
225
|
+
style: 'destructive',
|
|
226
|
+
onPress: () => {
|
|
227
|
+
setRecordingUri(null);
|
|
228
|
+
},
|
|
229
|
+
},
|
|
230
|
+
],
|
|
231
|
+
);
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
const handleSaveRecording = () => {
|
|
235
|
+
if (recordingUri && onRecordingComplete) {
|
|
236
|
+
onRecordingComplete(recordingUri);
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
const formatTime = (seconds: number) => {
|
|
241
|
+
const mins = Math.floor(seconds / 60);
|
|
242
|
+
const secs = Math.floor(seconds % 60);
|
|
243
|
+
const centisecs = Math.floor((seconds % 1) * 100);
|
|
244
|
+
return `${mins}:${secs.toString().padStart(2, '0')}.${centisecs
|
|
245
|
+
.toString()
|
|
246
|
+
.padStart(2, '0')}`;
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
if (!permissionGranted) {
|
|
250
|
+
return (
|
|
251
|
+
<View
|
|
252
|
+
className="items-center rounded-lg bg-secondary p-2 pt-28"
|
|
253
|
+
style={style}
|
|
254
|
+
>
|
|
255
|
+
<Text className="text-text text-center">
|
|
256
|
+
Microphone permission is required to record audio.
|
|
257
|
+
</Text>
|
|
258
|
+
</View>
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return (
|
|
263
|
+
<View
|
|
264
|
+
className="items-center rounded-lg bg-secondary p-2 pt-28"
|
|
265
|
+
style={style}
|
|
266
|
+
>
|
|
267
|
+
{recordingUri && !isRecording ? (
|
|
268
|
+
<View className="items-center">
|
|
269
|
+
<AudioPlayer
|
|
270
|
+
key={recordingUri} // Force fresh mount on new recording
|
|
271
|
+
source={{ uri: recordingUri }}
|
|
272
|
+
showControls={true}
|
|
273
|
+
showWaveform={true}
|
|
274
|
+
showTimer={true}
|
|
275
|
+
autoPlay={false}
|
|
276
|
+
onPlaybackStatusUpdate={(status) => {
|
|
277
|
+
console.log('Playback status:', status);
|
|
278
|
+
}}
|
|
279
|
+
/>
|
|
280
|
+
<View className="flex-row items-center gap-3">
|
|
281
|
+
<Button
|
|
282
|
+
variant="outline"
|
|
283
|
+
size="icon"
|
|
284
|
+
onPress={handleDeleteRecording}
|
|
285
|
+
className="size-12"
|
|
286
|
+
>
|
|
287
|
+
<Icon name="trash" size={20} color={theme.colors.foreground} />
|
|
288
|
+
</Button>
|
|
289
|
+
|
|
290
|
+
<Button
|
|
291
|
+
variant="default"
|
|
292
|
+
size="slim"
|
|
293
|
+
onPress={handleSaveRecording}
|
|
294
|
+
className="rounded-2xl bg-green-600 px-8 dark:bg-green-600"
|
|
295
|
+
>
|
|
296
|
+
<Text className="text-white">Save</Text>
|
|
297
|
+
</Button>
|
|
298
|
+
</View>
|
|
299
|
+
</View>
|
|
300
|
+
) : (
|
|
301
|
+
<View className="w-full">
|
|
302
|
+
{/* Recording Status */}
|
|
303
|
+
<View className="h-9 flex-row items-center justify-center">
|
|
304
|
+
{isRecording && (
|
|
305
|
+
<View className="flex-row items-center">
|
|
306
|
+
<Icon name="ellipse" size={8} color={colors.danger[500]} />
|
|
307
|
+
<Text className="ml-2 text-danger-500">Recording</Text>
|
|
308
|
+
</View>
|
|
309
|
+
)}
|
|
310
|
+
</View>
|
|
311
|
+
|
|
312
|
+
{/* Waveform Visualization */}
|
|
313
|
+
{showWaveform && (
|
|
314
|
+
<View className="mb-2 items-center">
|
|
315
|
+
<AudioWaveform
|
|
316
|
+
data={waveformData}
|
|
317
|
+
isPlaying={false}
|
|
318
|
+
progress={0}
|
|
319
|
+
height={80}
|
|
320
|
+
barCount={30}
|
|
321
|
+
barWidth={6}
|
|
322
|
+
barGap={2}
|
|
323
|
+
activeColor={isRecording ? '#EF4444' : '#6b7280'} //
|
|
324
|
+
inactiveColor="#444444"
|
|
325
|
+
animated={false}
|
|
326
|
+
/>
|
|
327
|
+
</View>
|
|
328
|
+
)}
|
|
329
|
+
{/* Timer */}
|
|
330
|
+
{showTimer && (
|
|
331
|
+
<View className="mb-2 items-center">
|
|
332
|
+
<Text
|
|
333
|
+
className={`font-mono ${isRecording ? 'text-danger-500' : 'text-text'}`}
|
|
334
|
+
>
|
|
335
|
+
{formatTime(durationSeconds)}
|
|
336
|
+
</Text>
|
|
337
|
+
{maxDuration && (
|
|
338
|
+
<Text className="text-muted">
|
|
339
|
+
Max: {formatTime(maxDuration)}
|
|
340
|
+
</Text>
|
|
341
|
+
)}
|
|
342
|
+
</View>
|
|
343
|
+
)}
|
|
344
|
+
|
|
345
|
+
{/* Controls */}
|
|
346
|
+
<View className="mb-3 items-center">
|
|
347
|
+
{!isRecording && !recordingUri && (
|
|
348
|
+
<Animated.View style={{ transform: [{ scale: recordingPulse }] }}>
|
|
349
|
+
<Button
|
|
350
|
+
variant="default"
|
|
351
|
+
onPress={handleStartRecording}
|
|
352
|
+
className="size-20 rounded-full bg-danger-500"
|
|
353
|
+
>
|
|
354
|
+
<Icon name="mic" size={36} color={theme.colors.background} />
|
|
355
|
+
</Button>
|
|
356
|
+
</Animated.View>
|
|
357
|
+
)}
|
|
358
|
+
|
|
359
|
+
{isRecording && (
|
|
360
|
+
<Button
|
|
361
|
+
variant="default"
|
|
362
|
+
onPress={handleStopRecording}
|
|
363
|
+
className="size-20 rounded-full bg-danger-500"
|
|
364
|
+
>
|
|
365
|
+
<Icon name="stop" size={36} color={theme.colors.background} />
|
|
366
|
+
</Button>
|
|
367
|
+
)}
|
|
368
|
+
</View>
|
|
369
|
+
</View>
|
|
370
|
+
)}
|
|
371
|
+
</View>
|
|
372
|
+
);
|
|
373
|
+
}
|
package/recipes/audio-recorder/apps/native/src/features/audio-recorder/components/audio-waveform.tsx
ADDED
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import { Canvas, RoundedRect } from '@shopify/react-native-skia';
|
|
2
|
+
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
|
3
|
+
import { PanResponder, StyleSheet, View, type ViewStyle } from 'react-native';
|
|
4
|
+
|
|
5
|
+
export interface AudioWaveformProps {
|
|
6
|
+
data?: number[]; // Audio amplitude data
|
|
7
|
+
isPlaying?: boolean;
|
|
8
|
+
progress?: number; // 0-100
|
|
9
|
+
onSeek?: (position: number) => void;
|
|
10
|
+
onSeekStart?: () => void;
|
|
11
|
+
onSeekEnd?: () => void;
|
|
12
|
+
style?: ViewStyle & { className?: string };
|
|
13
|
+
height?: number;
|
|
14
|
+
barCount?: number;
|
|
15
|
+
barWidth?: number;
|
|
16
|
+
barGap?: number;
|
|
17
|
+
activeColor?: string;
|
|
18
|
+
inactiveColor?: string;
|
|
19
|
+
animated?: boolean;
|
|
20
|
+
showProgress?: boolean;
|
|
21
|
+
interactive?: boolean; // New prop to enable seeking
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function AudioWaveform({
|
|
25
|
+
data,
|
|
26
|
+
isPlaying = false,
|
|
27
|
+
progress = 0,
|
|
28
|
+
onSeek,
|
|
29
|
+
onSeekStart,
|
|
30
|
+
onSeekEnd,
|
|
31
|
+
style,
|
|
32
|
+
height = 60,
|
|
33
|
+
barCount = 50,
|
|
34
|
+
barWidth = 3,
|
|
35
|
+
barGap = 2,
|
|
36
|
+
activeColor,
|
|
37
|
+
inactiveColor,
|
|
38
|
+
animated = true,
|
|
39
|
+
showProgress = false,
|
|
40
|
+
interactive = false,
|
|
41
|
+
}: AudioWaveformProps) {
|
|
42
|
+
const finalActiveColor = activeColor || 'hsl(0, 84.2%, 60.2%)'; // destructive
|
|
43
|
+
const finalInactiveColor = inactiveColor || 'hsl(215.4, 16.3%, 56.9%)'; // textMuted
|
|
44
|
+
|
|
45
|
+
// Generate sample data if none provided
|
|
46
|
+
const waveformData = useMemo(
|
|
47
|
+
() => data || generateSampleWaveform(barCount),
|
|
48
|
+
[data, barCount],
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
// Internal display data used for drawing (allows lightweight animation without many Animated.Values)
|
|
52
|
+
const [displayData, setDisplayData] = useState<number[]>(() => {
|
|
53
|
+
const base = waveformData;
|
|
54
|
+
return Array.from({ length: barCount }, (_, i) => base[i] || 0.2);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// Container ref for measuring dimensions
|
|
58
|
+
const containerRef = useRef<View>(null);
|
|
59
|
+
const containerWidth = useRef(0);
|
|
60
|
+
|
|
61
|
+
// Calculate total width including gaps
|
|
62
|
+
const totalWidth = barCount * barWidth + (barCount - 1) * barGap;
|
|
63
|
+
|
|
64
|
+
// Calculate progress line position more accurately
|
|
65
|
+
// const getProgressLinePosition = () => {
|
|
66
|
+
// const progressRatio = Math.max(0, Math.min(100, progress)) / 100;
|
|
67
|
+
|
|
68
|
+
// if (progressRatio === 0) return 0;
|
|
69
|
+
// if (progressRatio === 1) return totalWidth - 1; // Slight offset to keep within bounds
|
|
70
|
+
|
|
71
|
+
// // Calculate which bar the progress falls on
|
|
72
|
+
// const exactBarPosition = progressRatio * barCount;
|
|
73
|
+
// const barIndex = Math.floor(exactBarPosition);
|
|
74
|
+
// const barProgress = exactBarPosition - barIndex;
|
|
75
|
+
|
|
76
|
+
// // Calculate position accounting for bars and gaps
|
|
77
|
+
// let position = barIndex * (barWidth + barGap);
|
|
78
|
+
// position += barProgress * barWidth;
|
|
79
|
+
|
|
80
|
+
// // Ensure we don't exceed the waveform width
|
|
81
|
+
// return Math.min(position, totalWidth - 1);
|
|
82
|
+
// };
|
|
83
|
+
|
|
84
|
+
// Update display data when incoming data changes (real-time recorder case)
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
if (data && !animated) {
|
|
87
|
+
setDisplayData(() =>
|
|
88
|
+
Array.from({ length: barCount }, (_, i) => data[i] || 0.2),
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
}, [data, animated, barCount]);
|
|
92
|
+
|
|
93
|
+
// Enhanced animation system for smooth playback
|
|
94
|
+
useEffect(() => {
|
|
95
|
+
if (isPlaying && animated && !showProgress) {
|
|
96
|
+
let raf: number;
|
|
97
|
+
const tick = () => {
|
|
98
|
+
const t = Date.now() / 600; // slower phase
|
|
99
|
+
setDisplayData((prev) =>
|
|
100
|
+
prev.map((_, i) => {
|
|
101
|
+
const base = waveformData[i] || 0.2;
|
|
102
|
+
const variation = 0.9 + Math.sin(t + i * 0.15) * 0.1;
|
|
103
|
+
return Math.max(0.1, Math.min(1, base * variation));
|
|
104
|
+
}),
|
|
105
|
+
);
|
|
106
|
+
raf = requestAnimationFrame(tick);
|
|
107
|
+
};
|
|
108
|
+
raf = requestAnimationFrame(tick);
|
|
109
|
+
return () => cancelAnimationFrame(raf);
|
|
110
|
+
}
|
|
111
|
+
}, [isPlaying, animated, showProgress, waveformData]);
|
|
112
|
+
|
|
113
|
+
// Pan responder for seeking
|
|
114
|
+
const panResponder = useRef(
|
|
115
|
+
PanResponder.create({
|
|
116
|
+
onStartShouldSetPanResponder: () => {
|
|
117
|
+
console.log(
|
|
118
|
+
'AudioWaveform: onStartShouldSetPanResponder, interactive:',
|
|
119
|
+
interactive,
|
|
120
|
+
);
|
|
121
|
+
return interactive;
|
|
122
|
+
},
|
|
123
|
+
onMoveShouldSetPanResponder: () => {
|
|
124
|
+
console.log(
|
|
125
|
+
'AudioWaveform: onMoveShouldSetPanResponder, interactive:',
|
|
126
|
+
interactive,
|
|
127
|
+
);
|
|
128
|
+
return interactive;
|
|
129
|
+
},
|
|
130
|
+
onMoveShouldSetPanResponderCapture: () => interactive,
|
|
131
|
+
onPanResponderGrant: (evt) => {
|
|
132
|
+
if (!interactive) return;
|
|
133
|
+
console.log(
|
|
134
|
+
'AudioWaveform: PanResponder grant, locationX:',
|
|
135
|
+
evt.nativeEvent.locationX,
|
|
136
|
+
);
|
|
137
|
+
onSeekStart?.();
|
|
138
|
+
handleSeek(evt.nativeEvent.locationX);
|
|
139
|
+
},
|
|
140
|
+
onPanResponderMove: (evt) => {
|
|
141
|
+
if (!interactive) return;
|
|
142
|
+
handleSeek(evt.nativeEvent.locationX);
|
|
143
|
+
},
|
|
144
|
+
onPanResponderRelease: () => {
|
|
145
|
+
if (!interactive) return;
|
|
146
|
+
console.log('AudioWaveform: PanResponder release');
|
|
147
|
+
onSeekEnd?.();
|
|
148
|
+
},
|
|
149
|
+
onPanResponderTerminate: () => {
|
|
150
|
+
if (!interactive) return;
|
|
151
|
+
onSeekEnd?.();
|
|
152
|
+
},
|
|
153
|
+
}),
|
|
154
|
+
).current;
|
|
155
|
+
|
|
156
|
+
const handleSeek = (x: number) => {
|
|
157
|
+
if (!interactive || !onSeek) return;
|
|
158
|
+
|
|
159
|
+
console.log(
|
|
160
|
+
'AudioWaveform: handleSeek called with x:',
|
|
161
|
+
x,
|
|
162
|
+
'totalWidth:',
|
|
163
|
+
totalWidth,
|
|
164
|
+
);
|
|
165
|
+
const clampedX = Math.max(0, Math.min(totalWidth, x));
|
|
166
|
+
const seekPercentage = (clampedX / totalWidth) * 100;
|
|
167
|
+
console.log('AudioWaveform: seeking to percentage:', seekPercentage);
|
|
168
|
+
onSeek(seekPercentage);
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
// Removed unused handleBarPress helper to satisfy lint rules
|
|
172
|
+
|
|
173
|
+
const onLayout = (event: any) => {
|
|
174
|
+
containerWidth.current = event.nativeEvent.layout.width;
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
const bars = useMemo(() => {
|
|
178
|
+
const progressRatio = progress / 100;
|
|
179
|
+
return Array.from({ length: barCount }, (_, index) => {
|
|
180
|
+
const barProgress = (index + 0.5) / barCount;
|
|
181
|
+
const isActive = showProgress ? barProgress <= progressRatio : true;
|
|
182
|
+
const isPast = showProgress ? barProgress > progressRatio : false;
|
|
183
|
+
let opacity = 1;
|
|
184
|
+
if (showProgress && isPast) {
|
|
185
|
+
const distance = barProgress - progressRatio;
|
|
186
|
+
opacity = Math.max(0.3, 1 - distance * 2);
|
|
187
|
+
}
|
|
188
|
+
const heightPx = 4 + (height * 0.9 - 4) * (displayData[index] || 0.2);
|
|
189
|
+
const x = index * (barWidth + barGap);
|
|
190
|
+
const y = (height - heightPx) / 2;
|
|
191
|
+
const color = isActive ? finalActiveColor : finalInactiveColor;
|
|
192
|
+
return {
|
|
193
|
+
x,
|
|
194
|
+
y,
|
|
195
|
+
width: barWidth,
|
|
196
|
+
height: heightPx,
|
|
197
|
+
color,
|
|
198
|
+
opacity,
|
|
199
|
+
} as const;
|
|
200
|
+
});
|
|
201
|
+
}, [
|
|
202
|
+
barCount,
|
|
203
|
+
barGap,
|
|
204
|
+
barWidth,
|
|
205
|
+
height,
|
|
206
|
+
displayData,
|
|
207
|
+
finalActiveColor,
|
|
208
|
+
finalInactiveColor,
|
|
209
|
+
showProgress,
|
|
210
|
+
progress,
|
|
211
|
+
]);
|
|
212
|
+
|
|
213
|
+
return (
|
|
214
|
+
<View
|
|
215
|
+
style={[styles.container, { height }, style]}
|
|
216
|
+
onLayout={onLayout}
|
|
217
|
+
ref={containerRef}
|
|
218
|
+
>
|
|
219
|
+
<View
|
|
220
|
+
style={[styles.waveform, { width: totalWidth }]}
|
|
221
|
+
{...(interactive ? panResponder.panHandlers : {})}
|
|
222
|
+
>
|
|
223
|
+
<Canvas style={{ width: totalWidth, height }}>
|
|
224
|
+
{bars.map((b, idx) => (
|
|
225
|
+
<RoundedRect
|
|
226
|
+
key={idx}
|
|
227
|
+
x={b.x}
|
|
228
|
+
y={b.y}
|
|
229
|
+
width={b.width}
|
|
230
|
+
height={b.height}
|
|
231
|
+
color={b.color}
|
|
232
|
+
opacity={b.opacity}
|
|
233
|
+
r={b.width / 2}
|
|
234
|
+
/>
|
|
235
|
+
))}
|
|
236
|
+
</Canvas>
|
|
237
|
+
</View>
|
|
238
|
+
</View>
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const styles = StyleSheet.create({
|
|
243
|
+
container: {
|
|
244
|
+
justifyContent: 'center',
|
|
245
|
+
alignItems: 'center',
|
|
246
|
+
position: 'relative',
|
|
247
|
+
},
|
|
248
|
+
waveform: {
|
|
249
|
+
flexDirection: 'row',
|
|
250
|
+
alignItems: 'center',
|
|
251
|
+
justifyContent: 'center',
|
|
252
|
+
position: 'relative',
|
|
253
|
+
},
|
|
254
|
+
barContainer: {},
|
|
255
|
+
bar: {},
|
|
256
|
+
progressLine: {},
|
|
257
|
+
touchOverlay: {
|
|
258
|
+
position: 'absolute',
|
|
259
|
+
backgroundColor: 'transparent',
|
|
260
|
+
},
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
// Helper to generate sample data
|
|
264
|
+
function generateSampleWaveform(count: number): number[] {
|
|
265
|
+
return Array.from({ length: count }, (_, i) => {
|
|
266
|
+
const base = Math.sin((i / count) * Math.PI * 4) * 0.3 + 0.4;
|
|
267
|
+
const noise = (Math.random() - 0.5) * 0.2;
|
|
268
|
+
return Math.max(0.1, Math.min(1, base + noise));
|
|
269
|
+
});
|
|
270
|
+
}
|