mirror of
https://github.com/jjbrunton/Sauci.git
synced 2026-10-31 18:19:43 +00:00
Update
This commit is contained in:
1 parent
b4c8a2f303
commit
0fa7971cb9
5 files changed
+266
-22
No files matched your search
@@ -1,6 +1,7 @@
|
|||||||
apply plugin: "com.android.application"
|
apply plugin: "com.android.application"
|
||||||
apply plugin: "org.jetbrains.kotlin.android"
|
apply plugin: "org.jetbrains.kotlin.android"
|
||||||
apply plugin: "com.facebook.react"
|
apply plugin: "com.facebook.react"
|
||||||
|
apply plugin: "com.google.gms.google-services"
|
||||||
|
|
||||||
def projectRoot = rootDir.getAbsoluteFile().getParentFile().getAbsolutePath()
|
def projectRoot = rootDir.getAbsoluteFile().getParentFile().getAbsolutePath()
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
buildscript {
|
buildscript {
|
||||||
ext {
|
ext {
|
||||||
buildToolsVersion = findProperty('android.buildToolsVersion') ?: '35.0.0'
|
buildToolsVersion = findProperty('android.buildToolsVersion') ?: '34.0.0'
|
||||||
minSdkVersion = Integer.parseInt(findProperty('android.minSdkVersion') ?: '24')
|
minSdkVersion = Integer.parseInt(findProperty('android.minSdkVersion') ?: '24')
|
||||||
compileSdkVersion = Integer.parseInt(findProperty('android.compileSdkVersion') ?: '35')
|
compileSdkVersion = Integer.parseInt(findProperty('android.compileSdkVersion') ?: '35')
|
||||||
targetSdkVersion = Integer.parseInt(findProperty('android.targetSdkVersion') ?: '35')
|
targetSdkVersion = Integer.parseInt(findProperty('android.targetSdkVersion') ?: '35')
|
||||||
@@ -18,6 +18,7 @@ buildscript {
|
|||||||
classpath('com.android.tools.build:gradle')
|
classpath('com.android.tools.build:gradle')
|
||||||
classpath('com.facebook.react:react-native-gradle-plugin')
|
classpath('com.facebook.react:react-native-gradle-plugin')
|
||||||
classpath('org.jetbrains.kotlin:kotlin-gradle-plugin')
|
classpath('org.jetbrains.kotlin:kotlin-gradle-plugin')
|
||||||
|
classpath('com.google.gms:google-services:4.4.2')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +30,7 @@
|
|||||||
"backgroundColor": "#1a1a2e"
|
"backgroundColor": "#1a1a2e"
|
||||||
},
|
},
|
||||||
"package": "com.sauci.app",
|
"package": "com.sauci.app",
|
||||||
|
"googleServicesFile": "./google-services.json",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
"RECEIVE_BOOT_COMPLETED",
|
"RECEIVE_BOOT_COMPLETED",
|
||||||
"VIBRATE",
|
"VIBRATE",
|
||||||
|
|||||||
@@ -35,35 +35,65 @@ interface PlayTabButtonProps {
|
|||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
onPress?: (e: any) => void;
|
onPress?: (e: any) => void;
|
||||||
accessibilityState?: { selected?: boolean };
|
accessibilityState?: { selected?: boolean };
|
||||||
|
isMenuOpen?: boolean;
|
||||||
|
onToggleMenu?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function PlayTabButton({ onPress, accessibilityState }: PlayTabButtonProps) {
|
interface RadialMenuItem {
|
||||||
|
id: string;
|
||||||
|
icon: keyof typeof Ionicons.glyphMap;
|
||||||
|
label: string;
|
||||||
|
angle: number; // degrees from top (0 = top, -45 = left, 45 = right)
|
||||||
|
color?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const RADIAL_MENU_ITEMS: RadialMenuItem[] = [
|
||||||
|
{ id: 'dares', icon: 'flash', label: 'Dares', angle: -50, color: '#FF6B35' },
|
||||||
|
{ id: 'match', icon: 'flame', label: 'Match', angle: 0 },
|
||||||
|
{ id: 'quiz', icon: 'help-circle', label: 'Quiz', angle: 50, color: '#7B68EE' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const RADIAL_DISTANCE = 100; // Distance from center button
|
||||||
|
|
||||||
|
// Store for sharing state between PlayTabButton and AppLayout
|
||||||
|
let globalMenuToggle: (() => void) | null = null;
|
||||||
|
let globalMenuOpen = false;
|
||||||
|
|
||||||
|
function PlayTabButton({ accessibilityState }: PlayTabButtonProps) {
|
||||||
const isSelected = accessibilityState?.selected;
|
const isSelected = accessibilityState?.selected;
|
||||||
|
const [, forceUpdate] = useState({});
|
||||||
|
|
||||||
|
// Subscribe to global state changes
|
||||||
|
useEffect(() => {
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
forceUpdate({});
|
||||||
|
}, 50);
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Pressable
|
<View style={styles.playButtonContainer}>
|
||||||
onPress={onPress}
|
|
||||||
style={styles.playButtonContainer}
|
|
||||||
>
|
|
||||||
{/* Outer glow effect */}
|
{/* Outer glow effect */}
|
||||||
<View style={[styles.playButtonGlow, isSelected && styles.playButtonGlowActive]} />
|
<View style={[styles.playButtonGlow, isSelected && styles.playButtonGlowActive]} />
|
||||||
|
|
||||||
{/* Circle container */}
|
{/* Main circle button */}
|
||||||
<View style={styles.playButtonCircle}>
|
<Pressable onPress={() => globalMenuToggle?.()}>
|
||||||
<LinearGradient
|
<Animated.View style={styles.playButtonCircle}>
|
||||||
colors={gradients.primary as [string, string]}
|
<LinearGradient
|
||||||
start={{ x: 0, y: 0 }}
|
colors={gradients.primary as [string, string]}
|
||||||
end={{ x: 1, y: 1 }}
|
start={{ x: 0, y: 0 }}
|
||||||
style={styles.playButtonGradient}
|
end={{ x: 1, y: 1 }}
|
||||||
>
|
style={styles.playButtonGradient}
|
||||||
<Ionicons
|
>
|
||||||
name="flame"
|
<Ionicons
|
||||||
size={32}
|
name={globalMenuOpen ? "close" : "flame"}
|
||||||
color={colors.text}
|
size={32}
|
||||||
/>
|
color={colors.text}
|
||||||
</LinearGradient>
|
/>
|
||||||
</View>
|
</LinearGradient>
|
||||||
</Pressable>
|
</Animated.View>
|
||||||
|
</Pressable>
|
||||||
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,6 +113,60 @@ export default function AppLayout() {
|
|||||||
const hasCheckedInitialBiometric = useRef(false);
|
const hasCheckedInitialBiometric = useRef(false);
|
||||||
const wentToBackgroundAt = useRef<number | null>(null);
|
const wentToBackgroundAt = useRef<number | null>(null);
|
||||||
|
|
||||||
|
// Radial menu state
|
||||||
|
const [isRadialMenuOpen, setIsRadialMenuOpen] = useState(false);
|
||||||
|
const radialMenuAnim = useRef(new Animated.Value(0)).current;
|
||||||
|
|
||||||
|
const toggleRadialMenu = useCallback(() => {
|
||||||
|
const toValue = isRadialMenuOpen ? 0 : 1;
|
||||||
|
setIsRadialMenuOpen(!isRadialMenuOpen);
|
||||||
|
globalMenuOpen = !isRadialMenuOpen;
|
||||||
|
|
||||||
|
Animated.spring(radialMenuAnim, {
|
||||||
|
toValue,
|
||||||
|
useNativeDriver: true,
|
||||||
|
tension: 100,
|
||||||
|
friction: 8,
|
||||||
|
}).start();
|
||||||
|
}, [isRadialMenuOpen, radialMenuAnim]);
|
||||||
|
|
||||||
|
const closeRadialMenu = useCallback(() => {
|
||||||
|
setIsRadialMenuOpen(false);
|
||||||
|
globalMenuOpen = false;
|
||||||
|
Animated.timing(radialMenuAnim, {
|
||||||
|
toValue: 0,
|
||||||
|
duration: 150,
|
||||||
|
useNativeDriver: true,
|
||||||
|
}).start();
|
||||||
|
}, [radialMenuAnim]);
|
||||||
|
|
||||||
|
const handleRadialMenuItemPress = useCallback((itemId: string) => {
|
||||||
|
closeRadialMenu();
|
||||||
|
|
||||||
|
// Navigate based on item
|
||||||
|
switch (itemId) {
|
||||||
|
case 'match':
|
||||||
|
router.push('/(app)/swipe');
|
||||||
|
break;
|
||||||
|
case 'dares':
|
||||||
|
// TODO: Navigate to dares screen
|
||||||
|
console.log('Dares pressed - not yet implemented');
|
||||||
|
break;
|
||||||
|
case 'quiz':
|
||||||
|
// TODO: Navigate to quiz screen
|
||||||
|
console.log('Quiz pressed - not yet implemented');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}, [router, closeRadialMenu]);
|
||||||
|
|
||||||
|
// Register global menu toggle for PlayTabButton
|
||||||
|
useEffect(() => {
|
||||||
|
globalMenuToggle = toggleRadialMenu;
|
||||||
|
return () => {
|
||||||
|
globalMenuToggle = null;
|
||||||
|
};
|
||||||
|
}, [toggleRadialMenu]);
|
||||||
|
|
||||||
// Check if we're on screens that should hide the tab bar
|
// Check if we're on screens that should hide the tab bar
|
||||||
const segmentStrings = segments as string[];
|
const segmentStrings = segments as string[];
|
||||||
const isOnOnboarding = segmentStrings.includes("onboarding");
|
const isOnOnboarding = segmentStrings.includes("onboarding");
|
||||||
@@ -527,6 +611,78 @@ export default function AppLayout() {
|
|||||||
/>
|
/>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
|
{/* Radial Menu Overlay */}
|
||||||
|
{isRadialMenuOpen && (
|
||||||
|
<Pressable
|
||||||
|
style={radialStyles.overlay}
|
||||||
|
onPress={closeRadialMenu}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Radial Menu Items - positioned above the center button */}
|
||||||
|
<Animated.View
|
||||||
|
style={[
|
||||||
|
radialStyles.menuContainer,
|
||||||
|
{ opacity: radialMenuAnim },
|
||||||
|
]}
|
||||||
|
pointerEvents={isRadialMenuOpen ? 'auto' : 'none'}
|
||||||
|
>
|
||||||
|
{RADIAL_MENU_ITEMS.map((item) => {
|
||||||
|
// Convert angle to radians and calculate position
|
||||||
|
const angleRad = (item.angle - 90) * (Math.PI / 180); // -90 to make 0 = top
|
||||||
|
const translateX = Math.cos(angleRad) * RADIAL_DISTANCE;
|
||||||
|
const translateY = Math.sin(angleRad) * RADIAL_DISTANCE;
|
||||||
|
|
||||||
|
const itemScale = radialMenuAnim.interpolate({
|
||||||
|
inputRange: [0, 1],
|
||||||
|
outputRange: [0.3, 1],
|
||||||
|
});
|
||||||
|
|
||||||
|
const itemTranslateX = radialMenuAnim.interpolate({
|
||||||
|
inputRange: [0, 1],
|
||||||
|
outputRange: [0, translateX],
|
||||||
|
});
|
||||||
|
|
||||||
|
const itemTranslateY = radialMenuAnim.interpolate({
|
||||||
|
inputRange: [0, 1],
|
||||||
|
outputRange: [0, translateY],
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Animated.View
|
||||||
|
key={item.id}
|
||||||
|
style={[
|
||||||
|
radialStyles.menuItem,
|
||||||
|
{
|
||||||
|
transform: [
|
||||||
|
{ translateX: itemTranslateX },
|
||||||
|
{ translateY: itemTranslateY },
|
||||||
|
{ scale: itemScale },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Pressable
|
||||||
|
onPress={() => handleRadialMenuItemPress(item.id)}
|
||||||
|
style={radialStyles.menuItemButton}
|
||||||
|
>
|
||||||
|
<View style={[
|
||||||
|
radialStyles.menuItemCircle,
|
||||||
|
item.color ? { backgroundColor: item.color } : null,
|
||||||
|
]}>
|
||||||
|
<Ionicons
|
||||||
|
name={item.icon}
|
||||||
|
size={22}
|
||||||
|
color={colors.text}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
<Text style={radialStyles.menuItemLabel}>{item.label}</Text>
|
||||||
|
</Pressable>
|
||||||
|
</Animated.View>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Animated.View>
|
||||||
|
|
||||||
{/* Match Notification Overlay - Using Animated.View instead of Modal for Android compatibility */}
|
{/* Match Notification Overlay - Using Animated.View instead of Modal for Android compatibility */}
|
||||||
{matchNotification && (
|
{matchNotification && (
|
||||||
<Animated.View
|
<Animated.View
|
||||||
@@ -756,3 +912,49 @@ const styles = StyleSheet.create({
|
|||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Radial menu styles
|
||||||
|
const TAB_BAR_HEIGHT = Platform.OS === 'ios' ? 88 : 64;
|
||||||
|
|
||||||
|
const radialStyles = StyleSheet.create({
|
||||||
|
overlay: {
|
||||||
|
...StyleSheet.absoluteFillObject,
|
||||||
|
backgroundColor: 'rgba(0, 0, 0, 0.6)',
|
||||||
|
zIndex: 998,
|
||||||
|
},
|
||||||
|
menuContainer: {
|
||||||
|
position: 'absolute',
|
||||||
|
bottom: TAB_BAR_HEIGHT + 30, // Position above tab bar, centered on button
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
alignItems: 'center',
|
||||||
|
zIndex: 999,
|
||||||
|
},
|
||||||
|
menuItem: {
|
||||||
|
position: 'absolute',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
menuItemButton: {
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
menuItemCircle: {
|
||||||
|
width: 52,
|
||||||
|
height: 52,
|
||||||
|
borderRadius: 26,
|
||||||
|
backgroundColor: colors.primary,
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
...shadows.lg,
|
||||||
|
borderWidth: 2,
|
||||||
|
borderColor: 'rgba(255, 255, 255, 0.2)',
|
||||||
|
},
|
||||||
|
menuItemLabel: {
|
||||||
|
...typography.caption1,
|
||||||
|
color: colors.text,
|
||||||
|
marginTop: 6,
|
||||||
|
fontWeight: '600',
|
||||||
|
textShadowColor: 'rgba(0, 0, 0, 0.5)',
|
||||||
|
textShadowOffset: { width: 0, height: 1 },
|
||||||
|
textShadowRadius: 2,
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
{
|
||||||
|
"project_info": {
|
||||||
|
"project_number": "764866133492",
|
||||||
|
"project_id": "sauci-6840e",
|
||||||
|
"storage_bucket": "sauci-6840e.firebasestorage.app"
|
||||||
|
},
|
||||||
|
"client": [
|
||||||
|
{
|
||||||
|
"client_info": {
|
||||||
|
"mobilesdk_app_id": "1:764866133492:android:eabdb78009a18d2ffe4da1",
|
||||||
|
"android_client_info": {
|
||||||
|
"package_name": "com.sauci.app"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"oauth_client": [
|
||||||
|
{
|
||||||
|
"client_id": "764866133492-e78o2rh3rdjc4rfj2j6r2j77b065kggm.apps.googleusercontent.com",
|
||||||
|
"client_type": 3
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"api_key": [
|
||||||
|
{
|
||||||
|
"current_key": "AIzaSyDOAqIXAd_dy7L9ZflPvNkJdNXWMaGZ1O4"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"services": {
|
||||||
|
"appinvite_service": {
|
||||||
|
"other_platform_oauth_client": [
|
||||||
|
{
|
||||||
|
"client_id": "764866133492-e78o2rh3rdjc4rfj2j6r2j77b065kggm.apps.googleusercontent.com",
|
||||||
|
"client_type": 3
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"configuration_version": "1"
|
||||||
|
}
|
||||||
Reference in new issue
Block a user