a85ebf0a81
- One free Play app (com.locphamtran.recipescamera) with an expo-iap PRO unlock, replacing the paid apk for the store channel - Android SEND/SEND_MULTIPLE share target feeds photos into the editor, on cold start and on resume - BACK button in the TopBar and press-and-hold PEEK replace the chip row - NOISE REDUCTION and SHARPENING accept -10: a negative SHARPENING softens the frame before the grain pass, a negative NOISE REDUCTION re-grains it, in the Skia engine, the live preview and the Kotlin export path alike
186 lines
7.1 KiB
TypeScript
186 lines
7.1 KiB
TypeScript
import React, { useState } from 'react';
|
|
import { View, Text, TouchableOpacity, TextInput, Modal, KeyboardAvoidingView, Platform, Alert } from 'react-native';
|
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
import { Plus, Pencil, Undo2 } from 'lucide-react-native';
|
|
import * as Haptics from 'expo-haptics';
|
|
|
|
interface TopBarProps {
|
|
mode: 'camera' | 'library';
|
|
recipeName: string;
|
|
onSaveRecipe: (name: string) => void;
|
|
// Present only when the active recipe is user-created: enables Save (overwrite).
|
|
onOverwriteRecipe?: (name: string) => void;
|
|
// Also custom-only: opens the recipe editor — the CREATE form prefilled
|
|
// with this recipe's parameters and name.
|
|
onEditRecipe?: () => void;
|
|
// SAVE & SHARE -> EXPORT: write this look to an encrypted recipe file and
|
|
// hand it to the system share sheet so it can reach another phone.
|
|
onExportShare: (name: string) => void;
|
|
// BACK (undo): one step back through the look's history. PRO only — App
|
|
// refuses and offers the paywall when a LITE build taps it. No count is
|
|
// drawn: the icon turns amber while there is a step to go back to.
|
|
onUndo: () => void;
|
|
undoDepth: number;
|
|
}
|
|
|
|
export default function TopBar({
|
|
mode,
|
|
recipeName,
|
|
onSaveRecipe,
|
|
onOverwriteRecipe,
|
|
onEditRecipe,
|
|
onExportShare,
|
|
onUndo,
|
|
undoDepth,
|
|
}: TopBarProps) {
|
|
const insets = useSafeAreaInsets();
|
|
const [modalVisible, setModalVisible] = useState(false);
|
|
const [newRecipeName, setNewRecipeName] = useState('');
|
|
|
|
const openModal = () => {
|
|
// Bundled recipes are read-only -> Save As only, blank name. Custom ones
|
|
// prefill with the current name so Save (overwrite) is one tap.
|
|
setNewRecipeName(onOverwriteRecipe ? recipeName : '');
|
|
setModalVisible(true);
|
|
};
|
|
|
|
// SAVE & SHARE: two ways out of the same look — keep it as a recipe on this
|
|
// phone, or export it as a file another phone can import.
|
|
const openSaveMenu = () => {
|
|
Alert.alert(
|
|
'SAVE & SHARE',
|
|
'Save the look on this screen as a recipe, or export it as a file to share.',
|
|
[
|
|
{ text: 'CANCEL', style: 'cancel' },
|
|
{ text: 'EXPORT & SHARE', onPress: () => onExportShare(recipeName) },
|
|
{ text: 'SAVE RECIPE', onPress: openModal },
|
|
],
|
|
);
|
|
};
|
|
|
|
const run = (fn: (name: string) => void) => {
|
|
const name = newRecipeName.trim();
|
|
if (!name) return;
|
|
fn(name);
|
|
setNewRecipeName('');
|
|
setModalVisible(false);
|
|
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
|
};
|
|
|
|
const handleSavePress = () => run(onSaveRecipe);
|
|
const handleOverwritePress = () => onOverwriteRecipe && run(onOverwriteRecipe);
|
|
|
|
return (
|
|
<>
|
|
<View
|
|
style={{ paddingTop: insets.top + 6 }}
|
|
className="absolute top-0 left-0 right-0 z-20 flex-row items-start justify-between px-4"
|
|
>
|
|
<View className="max-w-[62%] rounded-xl border border-zinc-800/80 bg-black/45 px-3 py-2">
|
|
<Text className="text-amber-500 font-mono text-[9px] font-bold tracking-[0.2em]">
|
|
{mode === 'camera' ? 'LIVE' : 'LIBRARY'}
|
|
</Text>
|
|
<Text
|
|
numberOfLines={1}
|
|
className="text-white font-mono text-sm font-bold tracking-wide"
|
|
>
|
|
{recipeName}
|
|
</Text>
|
|
</View>
|
|
|
|
<View className="flex-row items-center space-x-2">
|
|
{/* BACK: the history control the chip row used to carry, sitting
|
|
between the status block and SAVE & SHARE. Lucide (ISC) vector
|
|
— no icon font to ship or license. */}
|
|
<TouchableOpacity
|
|
onPress={onUndo}
|
|
accessibilityLabel="BACK"
|
|
className="rounded-full border border-zinc-700/80 bg-black/45 p-2.5"
|
|
activeOpacity={0.7}
|
|
>
|
|
<Undo2 size={13} color={undoDepth > 0 ? '#f59e0b' : '#52525b'} />
|
|
</TouchableOpacity>
|
|
{onEditRecipe && (
|
|
<TouchableOpacity
|
|
onPress={onEditRecipe}
|
|
className="rounded-full border border-zinc-700/80 bg-black/45 p-2.5"
|
|
activeOpacity={0.7}
|
|
>
|
|
<Pencil size={13} color="#f59e0b" />
|
|
</TouchableOpacity>
|
|
)}
|
|
<TouchableOpacity
|
|
onPress={openSaveMenu}
|
|
className="flex-row items-center rounded-full border border-zinc-700/80 bg-black/45 px-3 py-2.5"
|
|
activeOpacity={0.7}
|
|
>
|
|
<Plus size={14} color="#f59e0b" />
|
|
<Text className="ml-1 font-mono text-[10px] font-bold tracking-wider text-amber-500">
|
|
SAVE & SHARE
|
|
</Text>
|
|
</TouchableOpacity>
|
|
</View>
|
|
</View>
|
|
|
|
<Modal
|
|
transparent
|
|
animationType="slide"
|
|
visible={modalVisible}
|
|
onRequestClose={() => setModalVisible(false)}
|
|
>
|
|
<View className="absolute inset-0 bg-black/60" />
|
|
<KeyboardAvoidingView
|
|
style={{ flex: 1 }}
|
|
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
|
>
|
|
<View style={{ paddingBottom: insets.bottom + 16 }} className="mt-auto bg-titan border-t border-zinc-800 rounded-t-3xl p-6">
|
|
<Text className="text-white font-mono text-sm font-bold tracking-wider mb-4">
|
|
{onOverwriteRecipe ? 'SAVE RECIPE' : 'SAVE CURRENT AS RECIPE'}
|
|
</Text>
|
|
<TextInput
|
|
value={newRecipeName}
|
|
onChangeText={setNewRecipeName}
|
|
placeholder="Recipe name (e.g. Retro Film)"
|
|
placeholderTextColor="#71717a"
|
|
className="bg-carbon border border-zinc-800 rounded-lg p-3 text-white font-mono text-sm mb-4"
|
|
autoFocus
|
|
onSubmitEditing={handleSavePress}
|
|
/>
|
|
<View className="flex-row space-x-3">
|
|
<TouchableOpacity
|
|
onPress={() => setModalVisible(false)}
|
|
className="flex-1 bg-zinc-800 py-3 rounded-lg items-center"
|
|
>
|
|
<Text className="text-zinc-300 font-mono text-xs font-semibold">CANCEL</Text>
|
|
</TouchableOpacity>
|
|
{onOverwriteRecipe ? (
|
|
<>
|
|
<TouchableOpacity
|
|
onPress={handleSavePress}
|
|
className="flex-1 border border-amber-500 py-3 rounded-lg items-center"
|
|
>
|
|
<Text className="text-amber-500 font-mono text-xs font-bold">SAVE AS</Text>
|
|
</TouchableOpacity>
|
|
<TouchableOpacity
|
|
onPress={handleOverwritePress}
|
|
className="flex-1 bg-amber-500 py-3 rounded-lg items-center"
|
|
>
|
|
<Text className="text-black font-mono text-xs font-bold">SAVE</Text>
|
|
</TouchableOpacity>
|
|
</>
|
|
) : (
|
|
<TouchableOpacity
|
|
onPress={handleSavePress}
|
|
className="flex-1 bg-amber-500 py-3 rounded-lg items-center"
|
|
>
|
|
<Text className="text-black font-mono text-xs font-bold">SAVE RECIPE</Text>
|
|
</TouchableOpacity>
|
|
)}
|
|
</View>
|
|
</View>
|
|
</KeyboardAvoidingView>
|
|
</Modal>
|
|
</>
|
|
);
|
|
}
|