Update files
This commit is contained in:
@@ -7,11 +7,10 @@ import {
|
|||||||
} from "react-native";
|
} from "react-native";
|
||||||
import { useState, useEffect, useRef, useCallback, useMemo } from "react";
|
import { useState, useEffect, useRef, useCallback, useMemo } from "react";
|
||||||
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||||
import { ArrowUp, AudioLines, MicOff, Square, Pencil } from "lucide-react-native";
|
import { ArrowUp, Square, Pencil } from "lucide-react-native";
|
||||||
import Animated, { useAnimatedStyle } from "react-native-reanimated";
|
import Animated, { useAnimatedStyle } from "react-native-reanimated";
|
||||||
import { useReanimatedKeyboardAnimation } from "react-native-keyboard-controller";
|
import { useReanimatedKeyboardAnimation } from "react-native-keyboard-controller";
|
||||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
import { useVoice } from "@/contexts/voice-context";
|
|
||||||
import { useIsFocused } from "@react-navigation/native";
|
import { useIsFocused } from "@react-navigation/native";
|
||||||
import { FOOTER_HEIGHT, MAX_CONTENT_WIDTH } from "@/constants/layout";
|
import { FOOTER_HEIGHT, MAX_CONTENT_WIDTH } from "@/constants/layout";
|
||||||
import { generateMessageId } from "@/types/stream";
|
import { generateMessageId } from "@/types/stream";
|
||||||
@@ -94,8 +93,6 @@ export function AgentInputArea({
|
|||||||
const setQueuedMessages = useSessionStore((state) => state.setQueuedMessages);
|
const setQueuedMessages = useSessionStore((state) => state.setQueuedMessages);
|
||||||
const setAgentStreamTail = useSessionStore((state) => state.setAgentStreamTail);
|
const setAgentStreamTail = useSessionStore((state) => state.setAgentStreamTail);
|
||||||
|
|
||||||
const { isVoiceMode, isMuted: isVoiceMuted, toggleMute: toggleVoiceMute } = useVoice();
|
|
||||||
|
|
||||||
const [internalInput, setInternalInput] = useState("");
|
const [internalInput, setInternalInput] = useState("");
|
||||||
const userInput = value ?? internalInput;
|
const userInput = value ?? internalInput;
|
||||||
const setUserInput = onChangeText ?? setInternalInput;
|
const setUserInput = onChangeText ?? setInternalInput;
|
||||||
@@ -573,35 +570,6 @@ export function AgentInputArea({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Voice quick mute indicator */}
|
|
||||||
{isVoiceMode && (
|
|
||||||
<View style={styles.voiceIndicatorRow}>
|
|
||||||
<Pressable
|
|
||||||
onPress={toggleVoiceMute}
|
|
||||||
accessibilityRole="button"
|
|
||||||
accessibilityLabel={isVoiceMuted ? "Unmute voice" : "Mute voice"}
|
|
||||||
style={[
|
|
||||||
styles.voiceIndicatorPill,
|
|
||||||
isVoiceMuted && styles.voiceIndicatorPillMuted,
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
{isVoiceMuted ? (
|
|
||||||
<MicOff size={14} color={theme.colors.surface0} />
|
|
||||||
) : (
|
|
||||||
<AudioLines size={14} color={theme.colors.foreground} />
|
|
||||||
)}
|
|
||||||
<Text
|
|
||||||
style={[
|
|
||||||
styles.voiceIndicatorText,
|
|
||||||
isVoiceMuted && styles.voiceIndicatorTextMuted,
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
{isVoiceMuted ? "Voice muted" : "Voice on"}
|
|
||||||
</Text>
|
|
||||||
</Pressable>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* MessageInput handles everything: text, dictation, attachments, all buttons */}
|
{/* MessageInput handles everything: text, dictation, attachments, all buttons */}
|
||||||
<MessageInput
|
<MessageInput
|
||||||
ref={messageInputRef}
|
ref={messageInputRef}
|
||||||
@@ -666,33 +634,6 @@ const styles = StyleSheet.create(((theme: Theme) => ({
|
|||||||
buttonDisabled: {
|
buttonDisabled: {
|
||||||
opacity: 0.5,
|
opacity: 0.5,
|
||||||
},
|
},
|
||||||
voiceIndicatorRow: {
|
|
||||||
flexDirection: "row",
|
|
||||||
justifyContent: "flex-end",
|
|
||||||
},
|
|
||||||
voiceIndicatorPill: {
|
|
||||||
flexDirection: "row",
|
|
||||||
alignItems: "center",
|
|
||||||
gap: theme.spacing[2],
|
|
||||||
paddingHorizontal: theme.spacing[3],
|
|
||||||
height: 32,
|
|
||||||
borderRadius: theme.borderRadius.full,
|
|
||||||
backgroundColor: theme.colors.surface2,
|
|
||||||
borderWidth: theme.borderWidth[1],
|
|
||||||
borderColor: theme.colors.border,
|
|
||||||
},
|
|
||||||
voiceIndicatorPillMuted: {
|
|
||||||
backgroundColor: theme.colors.palette.red[600],
|
|
||||||
borderColor: theme.colors.palette.red[800],
|
|
||||||
},
|
|
||||||
voiceIndicatorText: {
|
|
||||||
color: theme.colors.foreground,
|
|
||||||
fontSize: theme.fontSize.xs,
|
|
||||||
fontWeight: theme.fontWeight.medium,
|
|
||||||
},
|
|
||||||
voiceIndicatorTextMuted: {
|
|
||||||
color: theme.colors.surface0,
|
|
||||||
},
|
|
||||||
queueContainer: {
|
queueContainer: {
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
gap: theme.spacing[2],
|
gap: theme.spacing[2],
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ import { ToolCallSheetProvider } from "./tool-call-sheet";
|
|||||||
import { createMarkdownStyles } from "@/styles/markdown-styles";
|
import { createMarkdownStyles } from "@/styles/markdown-styles";
|
||||||
import { MAX_CONTENT_WIDTH } from "@/constants/layout";
|
import { MAX_CONTENT_WIDTH } from "@/constants/layout";
|
||||||
import { isPerfLoggingEnabled, measurePayload, perfLog } from "@/utils/perf";
|
import { isPerfLoggingEnabled, measurePayload, perfLog } from "@/utils/perf";
|
||||||
|
import { VoiceCompactIndicator } from "./voice-compact-indicator";
|
||||||
|
|
||||||
const isUserMessageItem = (item?: StreamItem) => item?.kind === "user_message";
|
const isUserMessageItem = (item?: StreamItem) => item?.kind === "user_message";
|
||||||
const isToolSequenceItem = (item?: StreamItem) =>
|
const isToolSequenceItem = (item?: StreamItem) =>
|
||||||
@@ -541,6 +542,7 @@ export function AgentStreamView({
|
|||||||
{showWorkingIndicator ? (
|
{showWorkingIndicator ? (
|
||||||
<View style={stylesheet.workingIndicatorWrapper}>
|
<View style={stylesheet.workingIndicatorWrapper}>
|
||||||
<WorkingIndicator />
|
<WorkingIndicator />
|
||||||
|
<VoiceCompactIndicator />
|
||||||
</View>
|
</View>
|
||||||
) : null}
|
) : null}
|
||||||
</View>
|
</View>
|
||||||
@@ -1179,8 +1181,11 @@ const stylesheet = StyleSheet.create((theme) => ({
|
|||||||
gap: theme.spacing[3],
|
gap: theme.spacing[3],
|
||||||
},
|
},
|
||||||
workingIndicatorWrapper: {
|
workingIndicatorWrapper: {
|
||||||
alignItems: "flex-start",
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "space-between",
|
||||||
paddingLeft: 3,
|
paddingLeft: 3,
|
||||||
|
paddingRight: 3,
|
||||||
paddingTop: theme.spacing[3],
|
paddingTop: theme.spacing[3],
|
||||||
paddingBottom: theme.spacing[2],
|
paddingBottom: theme.spacing[2],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -179,6 +179,7 @@ export function DictationOverlay({
|
|||||||
isDetecting
|
isDetecting
|
||||||
isSpeaking={false}
|
isSpeaking={false}
|
||||||
orientation="horizontal"
|
orientation="horizontal"
|
||||||
|
color={theme.colors.palette.white}
|
||||||
/>
|
/>
|
||||||
<Text
|
<Text
|
||||||
style={[
|
style={[
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useCallback, useMemo, useState, useEffect } from "react";
|
import { useCallback, useMemo, useState, useEffect } from "react";
|
||||||
import { View, Pressable, Text, Platform, Modal, Alert } from "react-native";
|
import { View, Pressable, Text, Platform, Alert } from "react-native";
|
||||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
import Animated, {
|
import Animated, {
|
||||||
useAnimatedStyle,
|
useAnimatedStyle,
|
||||||
@@ -23,6 +23,7 @@ import { useSidebarAgentSections } from "@/hooks/use-sidebar-agent-sections";
|
|||||||
import { useSidebarCollapsedSectionsStore } from "@/stores/sidebar-collapsed-sections-store";
|
import { useSidebarCollapsedSectionsStore } from "@/stores/sidebar-collapsed-sections-store";
|
||||||
import { useKeyboardNavStore } from "@/stores/keyboard-nav-store";
|
import { useKeyboardNavStore } from "@/stores/keyboard-nav-store";
|
||||||
import { deriveSidebarShortcutAgentKeys } from "@/utils/sidebar-shortcuts";
|
import { deriveSidebarShortcutAgentKeys } from "@/utils/sidebar-shortcuts";
|
||||||
|
import { AdaptiveModalSheet } from "@/components/adaptive-modal-sheet";
|
||||||
|
|
||||||
const DESKTOP_SIDEBAR_WIDTH = 320;
|
const DESKTOP_SIDEBAR_WIDTH = 320;
|
||||||
const SIDEBAR_AGENT_LIMIT = 15;
|
const SIDEBAR_AGENT_LIMIT = 15;
|
||||||
@@ -351,31 +352,31 @@ export function SlidingSidebar({ selectedAgentId }: SlidingSidebarProps) {
|
|||||||
</Animated.View>
|
</Animated.View>
|
||||||
</GestureDetector>
|
</GestureDetector>
|
||||||
|
|
||||||
<Modal
|
<AdaptiveModalSheet
|
||||||
|
title="Choose a host"
|
||||||
visible={showVoiceHostPicker}
|
visible={showVoiceHostPicker}
|
||||||
transparent
|
onClose={handleDismissVoiceHostPicker}
|
||||||
animationType="fade"
|
snapPoints={["45%", "70%"]}
|
||||||
onRequestClose={handleDismissVoiceHostPicker}
|
testID="voice-host-picker"
|
||||||
>
|
>
|
||||||
<View style={styles.hostPickerOverlay}>
|
<View style={styles.hostPickerList}>
|
||||||
<Pressable style={styles.hostPickerBackdrop} onPress={handleDismissVoiceHostPicker} />
|
{voiceEligibleHosts.map((entry) => (
|
||||||
<View style={styles.hostPickerContainer}>
|
<Pressable
|
||||||
<Text style={styles.hostPickerTitle}>Choose a host</Text>
|
key={entry.daemon.id}
|
||||||
{voiceEligibleHosts.map((entry) => (
|
style={styles.hostPickerOption}
|
||||||
<Pressable
|
onPress={() => handleSelectVoiceHost(entry.daemon.id)}
|
||||||
key={entry.daemon.id}
|
>
|
||||||
style={styles.hostPickerButton}
|
<Text style={styles.hostPickerOptionText}>{entry.daemon.label}</Text>
|
||||||
onPress={() => handleSelectVoiceHost(entry.daemon.id)}
|
|
||||||
>
|
|
||||||
<Text style={styles.hostPickerButtonText}>{entry.daemon.label}</Text>
|
|
||||||
</Pressable>
|
|
||||||
))}
|
|
||||||
<Pressable style={styles.hostPickerCancel} onPress={handleDismissVoiceHostPicker}>
|
|
||||||
<Text style={styles.hostPickerCancelText}>Cancel</Text>
|
|
||||||
</Pressable>
|
</Pressable>
|
||||||
</View>
|
))}
|
||||||
|
<Pressable
|
||||||
|
style={styles.hostPickerCancel}
|
||||||
|
onPress={handleDismissVoiceHostPicker}
|
||||||
|
>
|
||||||
|
<Text style={styles.hostPickerCancelText}>Cancel</Text>
|
||||||
|
</Pressable>
|
||||||
</View>
|
</View>
|
||||||
</Modal>
|
</AdaptiveModalSheet>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -460,6 +461,32 @@ export function SlidingSidebar({ selectedAgentId }: SlidingSidebarProps) {
|
|||||||
</Pressable>
|
</Pressable>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
<AdaptiveModalSheet
|
||||||
|
title="Choose a host"
|
||||||
|
visible={showVoiceHostPicker}
|
||||||
|
onClose={handleDismissVoiceHostPicker}
|
||||||
|
snapPoints={["45%", "70%"]}
|
||||||
|
testID="voice-host-picker"
|
||||||
|
>
|
||||||
|
<View style={styles.hostPickerList}>
|
||||||
|
{voiceEligibleHosts.map((entry) => (
|
||||||
|
<Pressable
|
||||||
|
key={entry.daemon.id}
|
||||||
|
style={styles.hostPickerOption}
|
||||||
|
onPress={() => handleSelectVoiceHost(entry.daemon.id)}
|
||||||
|
>
|
||||||
|
<Text style={styles.hostPickerOptionText}>{entry.daemon.label}</Text>
|
||||||
|
</Pressable>
|
||||||
|
))}
|
||||||
|
<Pressable
|
||||||
|
style={styles.hostPickerCancel}
|
||||||
|
onPress={handleDismissVoiceHostPicker}
|
||||||
|
>
|
||||||
|
<Text style={styles.hostPickerCancelText}>Cancel</Text>
|
||||||
|
</Pressable>
|
||||||
|
</View>
|
||||||
|
</AdaptiveModalSheet>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -547,33 +574,10 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
footerButtonTextHovered: {
|
footerButtonTextHovered: {
|
||||||
color: theme.colors.foreground,
|
color: theme.colors.foreground,
|
||||||
},
|
},
|
||||||
hostPickerOverlay: {
|
hostPickerList: {
|
||||||
flex: 1,
|
|
||||||
justifyContent: "center",
|
|
||||||
alignItems: "center",
|
|
||||||
backgroundColor: "rgba(0, 0, 0, 0.5)",
|
|
||||||
padding: theme.spacing[4],
|
|
||||||
},
|
|
||||||
hostPickerBackdrop: {
|
|
||||||
...StyleSheet.absoluteFillObject,
|
|
||||||
},
|
|
||||||
hostPickerContainer: {
|
|
||||||
width: "100%",
|
|
||||||
maxWidth: 360,
|
|
||||||
backgroundColor: theme.colors.surface0,
|
|
||||||
borderRadius: theme.borderRadius["2xl"],
|
|
||||||
padding: theme.spacing[4],
|
|
||||||
borderWidth: theme.borderWidth[1],
|
|
||||||
borderColor: theme.colors.border,
|
|
||||||
gap: theme.spacing[2],
|
gap: theme.spacing[2],
|
||||||
},
|
},
|
||||||
hostPickerTitle: {
|
hostPickerOption: {
|
||||||
fontSize: theme.fontSize.base,
|
|
||||||
fontWeight: theme.fontWeight.semibold,
|
|
||||||
color: theme.colors.foreground,
|
|
||||||
marginBottom: theme.spacing[2],
|
|
||||||
},
|
|
||||||
hostPickerButton: {
|
|
||||||
paddingVertical: theme.spacing[3],
|
paddingVertical: theme.spacing[3],
|
||||||
paddingHorizontal: theme.spacing[3],
|
paddingHorizontal: theme.spacing[3],
|
||||||
borderRadius: theme.borderRadius.lg,
|
borderRadius: theme.borderRadius.lg,
|
||||||
@@ -581,12 +585,11 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
borderWidth: theme.borderWidth[1],
|
borderWidth: theme.borderWidth[1],
|
||||||
borderColor: theme.colors.border,
|
borderColor: theme.colors.border,
|
||||||
},
|
},
|
||||||
hostPickerButtonText: {
|
hostPickerOptionText: {
|
||||||
color: theme.colors.foreground,
|
color: theme.colors.foreground,
|
||||||
fontSize: theme.fontSize.sm,
|
fontSize: theme.fontSize.sm,
|
||||||
},
|
},
|
||||||
hostPickerCancel: {
|
hostPickerCancel: {
|
||||||
marginTop: theme.spacing[2],
|
|
||||||
paddingVertical: theme.spacing[3],
|
paddingVertical: theme.spacing[3],
|
||||||
paddingHorizontal: theme.spacing[3],
|
paddingHorizontal: theme.spacing[3],
|
||||||
borderRadius: theme.borderRadius.lg,
|
borderRadius: theme.borderRadius.lg,
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import { Pressable, View } from "react-native";
|
||||||
|
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||||
|
import { AudioLines, MicOff } from "lucide-react-native";
|
||||||
|
import { VolumeMeter } from "@/components/volume-meter";
|
||||||
|
import { useVoice } from "@/contexts/voice-context";
|
||||||
|
|
||||||
|
export function VoiceCompactIndicator() {
|
||||||
|
const { theme } = useUnistyles();
|
||||||
|
const { isVoiceMode, volume, isMuted, isDetecting, isSpeaking, toggleMute } =
|
||||||
|
useVoice();
|
||||||
|
|
||||||
|
if (!isVoiceMode) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.container}>
|
||||||
|
<View style={styles.meterContainer}>
|
||||||
|
<VolumeMeter
|
||||||
|
volume={volume}
|
||||||
|
isMuted={isMuted}
|
||||||
|
isDetecting={isDetecting}
|
||||||
|
isSpeaking={isSpeaking}
|
||||||
|
orientation="horizontal"
|
||||||
|
variant="compact"
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<Pressable
|
||||||
|
onPress={toggleMute}
|
||||||
|
accessibilityRole="button"
|
||||||
|
accessibilityLabel={isMuted ? "Unmute voice" : "Mute voice"}
|
||||||
|
style={[styles.muteButton, isMuted && styles.muteButtonMuted]}
|
||||||
|
>
|
||||||
|
{isMuted ? (
|
||||||
|
<MicOff size={14} color={theme.colors.surface0} />
|
||||||
|
) : (
|
||||||
|
<AudioLines size={14} color={theme.colors.foreground} />
|
||||||
|
)}
|
||||||
|
</Pressable>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create((theme) => ({
|
||||||
|
container: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: theme.spacing[2],
|
||||||
|
paddingLeft: theme.spacing[2],
|
||||||
|
paddingRight: theme.spacing[1],
|
||||||
|
height: 32,
|
||||||
|
borderRadius: theme.borderRadius.full,
|
||||||
|
backgroundColor: theme.colors.surface2,
|
||||||
|
borderWidth: theme.borderWidth[1],
|
||||||
|
borderColor: theme.colors.border,
|
||||||
|
},
|
||||||
|
meterContainer: {
|
||||||
|
justifyContent: "center",
|
||||||
|
},
|
||||||
|
muteButton: {
|
||||||
|
width: 28,
|
||||||
|
height: 28,
|
||||||
|
borderRadius: theme.borderRadius.full,
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
backgroundColor: theme.colors.surface0,
|
||||||
|
borderWidth: theme.borderWidth[1],
|
||||||
|
borderColor: theme.colors.border,
|
||||||
|
},
|
||||||
|
muteButtonMuted: {
|
||||||
|
backgroundColor: theme.colors.palette.red[600],
|
||||||
|
borderColor: theme.colors.palette.red[800],
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
@@ -35,40 +35,42 @@ export function VoicePanel() {
|
|||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View style={styles.meterRow}>
|
<View style={styles.contentRow}>
|
||||||
<VolumeMeter
|
<View style={styles.meterContainer}>
|
||||||
volume={volume}
|
<VolumeMeter
|
||||||
isMuted={isMuted}
|
volume={volume}
|
||||||
isDetecting={isDetecting}
|
isMuted={isMuted}
|
||||||
isSpeaking={isSpeaking}
|
isDetecting={isDetecting}
|
||||||
orientation="horizontal"
|
isSpeaking={isSpeaking}
|
||||||
/>
|
orientation="horizontal"
|
||||||
</View>
|
|
||||||
|
|
||||||
<View style={styles.actionsRow}>
|
|
||||||
<Pressable
|
|
||||||
onPress={toggleMute}
|
|
||||||
accessibilityRole="button"
|
|
||||||
accessibilityLabel={isMuted ? "Unmute voice" : "Mute voice"}
|
|
||||||
style={[
|
|
||||||
styles.iconButton,
|
|
||||||
isMuted && styles.iconButtonMuted,
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<MicOff
|
|
||||||
size={18}
|
|
||||||
color={isMuted ? theme.colors.surface0 : theme.colors.foreground}
|
|
||||||
/>
|
/>
|
||||||
</Pressable>
|
</View>
|
||||||
|
|
||||||
<Pressable
|
<View style={styles.actionsRow}>
|
||||||
onPress={() => void stopVoice()}
|
<Pressable
|
||||||
accessibilityRole="button"
|
onPress={toggleMute}
|
||||||
accessibilityLabel="Stop voice mode"
|
accessibilityRole="button"
|
||||||
style={[styles.iconButton, styles.iconButtonStop]}
|
accessibilityLabel={isMuted ? "Unmute voice" : "Mute voice"}
|
||||||
>
|
style={[
|
||||||
<Square size={16} color="white" fill="white" />
|
styles.iconButton,
|
||||||
</Pressable>
|
isMuted && styles.iconButtonMuted,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<MicOff
|
||||||
|
size={18}
|
||||||
|
color={isMuted ? theme.colors.surface0 : theme.colors.foreground}
|
||||||
|
/>
|
||||||
|
</Pressable>
|
||||||
|
|
||||||
|
<Pressable
|
||||||
|
onPress={() => void stopVoice()}
|
||||||
|
accessibilityRole="button"
|
||||||
|
accessibilityLabel="Stop voice mode"
|
||||||
|
style={[styles.iconButton, styles.iconButtonStop]}
|
||||||
|
>
|
||||||
|
<Square size={16} color="white" fill="white" />
|
||||||
|
</Pressable>
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
@@ -109,7 +111,14 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
color: theme.colors.foregroundMuted,
|
color: theme.colors.foregroundMuted,
|
||||||
fontSize: theme.fontSize.xs,
|
fontSize: theme.fontSize.xs,
|
||||||
},
|
},
|
||||||
meterRow: {
|
contentRow: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
gap: theme.spacing[3],
|
||||||
|
},
|
||||||
|
meterContainer: {
|
||||||
|
flex: 1,
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
},
|
},
|
||||||
actionsRow: {
|
actionsRow: {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useState, useRef } from "react";
|
import { useEffect } from "react";
|
||||||
import { View } from "react-native";
|
import { View } from "react-native";
|
||||||
import ReanimatedAnimated, {
|
import ReanimatedAnimated, {
|
||||||
useSharedValue,
|
useSharedValue,
|
||||||
@@ -17,16 +17,29 @@ interface VolumeMeterProps {
|
|||||||
isDetecting?: boolean;
|
isDetecting?: boolean;
|
||||||
isSpeaking?: boolean;
|
isSpeaking?: boolean;
|
||||||
orientation?: "vertical" | "horizontal";
|
orientation?: "vertical" | "horizontal";
|
||||||
|
variant?: "default" | "compact";
|
||||||
|
color?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function VolumeMeter({ volume, isMuted = false, isDetecting = false, isSpeaking = false, orientation = "vertical" }: VolumeMeterProps) {
|
export function VolumeMeter({
|
||||||
|
volume,
|
||||||
|
isMuted = false,
|
||||||
|
isDetecting = false,
|
||||||
|
isSpeaking = false,
|
||||||
|
orientation = "vertical",
|
||||||
|
variant = "default",
|
||||||
|
color,
|
||||||
|
}: VolumeMeterProps) {
|
||||||
const { theme } = useUnistyles();
|
const { theme } = useUnistyles();
|
||||||
|
const isCompact = variant === "compact";
|
||||||
|
|
||||||
// Base dimensions
|
// Base dimensions
|
||||||
const LINE_SPACING = 8;
|
const LINE_SPACING = isCompact ? 6 : 8;
|
||||||
const LINE_WIDTH = 8;
|
const LINE_WIDTH = isCompact ? 6 : 8;
|
||||||
const MAX_HEIGHT = orientation === "horizontal" ? 30 : 50;
|
const MAX_HEIGHT =
|
||||||
const MIN_HEIGHT = orientation === "horizontal" ? 12 : 20;
|
orientation === "horizontal" ? (isCompact ? 18 : 30) : isCompact ? 32 : 50;
|
||||||
|
const MIN_HEIGHT =
|
||||||
|
orientation === "horizontal" ? (isCompact ? 8 : 12) : isCompact ? 14 : 20;
|
||||||
|
|
||||||
// Create shared values for 3 dots unconditionally
|
// Create shared values for 3 dots unconditionally
|
||||||
const line1Height = useSharedValue(MIN_HEIGHT);
|
const line1Height = useSharedValue(MIN_HEIGHT);
|
||||||
@@ -127,8 +140,9 @@ export function VolumeMeter({ volume, isMuted = false, isDetecting = false, isSp
|
|||||||
}
|
}
|
||||||
}, [volume, isMuted]);
|
}, [volume, isMuted]);
|
||||||
|
|
||||||
const lineColor = "#FFFFFF";
|
const lineColor = color ?? theme.colors.foreground;
|
||||||
const containerHeight = orientation === "horizontal" ? 60 : 100;
|
const containerHeight =
|
||||||
|
orientation === "horizontal" ? (isCompact ? 32 : 60) : isCompact ? 64 : 100;
|
||||||
|
|
||||||
// Create animated styles unconditionally at top level
|
// Create animated styles unconditionally at top level
|
||||||
const line1Style = useAnimatedStyle(() => {
|
const line1Style = useAnimatedStyle(() => {
|
||||||
|
|||||||
Reference in New Issue
Block a user