Files
recipescamera/App.tsx
T
2026-07-16 12:22:02 +07:00

287 lines
8.9 KiB
TypeScript

import React, { useState, useEffect } from 'react';
import { SafeAreaView, View, StatusBar, Alert, ActivityIndicator, Text } from 'react-native';
import { useCameraPermissions } from 'expo-camera';
import * as ImagePicker from 'expo-image-picker';
import * as Haptics from 'expo-haptics';
import Header from './src/components/Header';
import Viewfinder from './src/components/Viewfinder';
import TabSelector, { TabId } from './src/components/TabSelector';
import AdjustmentPanel from './src/components/AdjustmentPanel';
import CameraControls from './src/components/CameraControls';
import PreviewModal from './src/components/PreviewModal';
import { Recipe, ColorAdjustments, FrameId, GPSInfo } from './src/types';
import { getAllRecipes, saveCustomRecipe, deleteCustomRecipe } from './src/utils/storageUtils';
import { getCurrentGPS, requestLocationPermissions } from './src/utils/locationUtils';
import { processAndExportPhoto } from './src/utils/exportEngine';
// @ts-ignore
import './global.css';
export default function App() {
const [mode, setMode] = useState<'camera' | 'library'>('camera');
const [recipes, setRecipes] = useState<Recipe[]>([]);
const [selectedRecipe, setSelectedRecipe] = useState<Recipe | null>(null);
// Custom temporary overrides for the sliders
const [adjustments, setAdjustments] = useState<ColorAdjustments>({
exposure: 0,
contrast: 0,
saturation: 0,
temperature: 5500,
tint: 0,
highlight: 0,
shadow: 0,
denoise: 0,
clarity: 0,
grain: 0,
colorChrome: 'none',
});
const [selectedFrame, setSelectedFrame] = useState<FrameId>('none');
const [useGeotag, setUseGeotag] = useState(true);
const [gpsInfo, setGpsInfo] = useState<GPSInfo | null>(null);
const [activeTab, setActiveTab] = useState<TabId>('recipes');
const [libraryImageUri, setLibraryImageUri] = useState<string | null>(null);
const [lastPhotoUri, setLastPhotoUri] = useState<string | null>(null);
const [previewVisible, setPreviewVisible] = useState(false);
const [isProcessing, setIsProcessing] = useState(false);
// Hardware Permissions
const [cameraPermission, requestCameraPermission] = useCameraPermissions();
useEffect(() => {
loadRecipes();
loadGPS();
}, []);
const loadRecipes = async () => {
const list = await getAllRecipes();
setRecipes(list);
if (list.length > 0) {
applyRecipe(list[0]);
}
};
const loadGPS = async () => {
const info = await getCurrentGPS();
if (info) {
setGpsInfo(info);
}
};
const applyRecipe = (recipe: Recipe) => {
setSelectedRecipe(recipe);
setAdjustments(recipe.adjustments);
setSelectedFrame(recipe.frameId || 'none');
setUseGeotag(recipe.useGeotag);
};
const handleUpdateAdjustments = (updates: Partial<ColorAdjustments>) => {
setAdjustments((prev) => {
const next = { ...prev, ...updates };
if (selectedRecipe) {
setSelectedRecipe({
...selectedRecipe,
adjustments: next,
});
}
return next;
});
};
const handleUpdateFrame = (frameId: FrameId) => {
setSelectedFrame(frameId);
if (selectedRecipe) {
setSelectedRecipe({
...selectedRecipe,
frameId,
});
}
};
const handleToggleGeotag = async (enabled: boolean) => {
setUseGeotag(enabled);
if (selectedRecipe) {
setSelectedRecipe({
...selectedRecipe,
useGeotag: enabled,
});
}
if (enabled && !gpsInfo) {
const locationGranted = await requestLocationPermissions();
if (locationGranted) {
loadGPS();
} else {
Alert.alert('Permission Denied', 'Enable location services to use GPS Watermark.');
setUseGeotag(false);
}
}
};
const handleSaveRecipe = async (name: string) => {
if (!selectedRecipe) return;
const newRecipe = await saveCustomRecipe({
name,
baseFilter: selectedRecipe.baseFilter,
adjustments,
frameId: selectedFrame,
useGeotag,
});
const list = await getAllRecipes();
setRecipes(list);
applyRecipe(newRecipe);
Alert.alert('Recipe Saved', `"${name}" has been created.`);
};
const handleDeleteRecipe = async (id: string) => {
await deleteCustomRecipe(id);
const list = await getAllRecipes();
setRecipes(list);
if (selectedRecipe?.id === id && list.length > 0) {
applyRecipe(list[0]);
}
};
const handlePickImage = async () => {
const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ImagePicker.MediaTypeOptions.Images,
allowsEditing: false,
quality: 1,
});
if (!result.canceled && result.assets && result.assets.length > 0) {
setLibraryImageUri(result.assets[0].uri);
setMode('library');
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
}
};
const handleCapture = async () => {
if (mode === 'camera') {
// For simulator & simplicity, if camera view isn't fully mocked, we use a placeholder or trigger camera picture.
// In this setup, we simulate taking a photo or request library image if running in simulator.
// Let's create a beautiful simulation that pulls a mock asset or lets you select one if in simulator.
Alert.alert(
'Capture Mode',
'Camera capture requires native device hardware. For testing on emulators, please use the LIBRARY mode to load a photo and apply filters.',
[{ text: 'OK' }]
);
} else {
// Library mode capture/export: processes selected image with recipe, overlays, text
if (!libraryImageUri || !selectedRecipe) {
Alert.alert('Error', 'Please select an image from the library first.');
return;
}
setIsProcessing(true);
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
const exportedUri = await processAndExportPhoto(
libraryImageUri,
{
...selectedRecipe,
adjustments,
frameId: selectedFrame,
useGeotag,
},
selectedFrame,
useGeotag,
gpsInfo
);
setIsProcessing(false);
if (exportedUri) {
setLastPhotoUri(exportedUri);
setPreviewVisible(true);
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
} else {
Alert.alert('Failed', 'Error occurred while processing image.');
}
}
};
return (
<SafeAreaView className="flex-1 bg-carbon">
<StatusBar barStyle="light-content" backgroundColor="#08080a" />
{/* Top Header */}
<Header mode={mode} setMode={setMode} title="CamRecipe Pro" />
{/* Main Viewfinder Frame */}
<View className="flex-1 justify-center items-center px-4 py-2 bg-carbon">
<Viewfinder
mode={mode}
recipe={
selectedRecipe || {
id: 'temp',
name: 'Custom',
baseFilter: 'none',
adjustments,
frameId: selectedFrame,
useGeotag,
}
}
selectedFrame={selectedFrame}
useGeotag={useGeotag}
gpsInfo={gpsInfo}
libraryImageUri={libraryImageUri}
cameraPermissionGranted={!!cameraPermission?.granted}
onRequestCameraPermission={requestCameraPermission}
/>
</View>
{/* Selector Tabs */}
<TabSelector activeTab={activeTab} setActiveTab={setActiveTab} />
{/* Adjustments Panel */}
<View style={{ height: 250 }} className="bg-carbon">
<AdjustmentPanel
activeTab={activeTab}
recipes={recipes}
currentRecipeId={selectedRecipe?.id || ''}
adjustments={adjustments}
selectedFrame={selectedFrame}
useGeotag={useGeotag}
onSelectRecipe={applyRecipe}
onUpdateAdjustments={handleUpdateAdjustments}
onUpdateFrame={handleUpdateFrame}
onToggleGeotag={handleToggleGeotag}
onSaveRecipe={handleSaveRecipe}
onDeleteRecipe={handleDeleteRecipe}
/>
</View>
{/* Bottom Camera Buttons */}
<CameraControls
lastPhotoUri={lastPhotoUri}
onCapture={handleCapture}
onPickImage={handlePickImage}
onOpenPreview={() => setPreviewVisible(true)}
isLibraryMode={mode === 'library' && !libraryImageUri}
/>
{/* Preview Modal */}
<PreviewModal
visible={previewVisible}
onClose={() => setPreviewVisible(false)}
photoUri={lastPhotoUri}
recipeName={selectedRecipe?.name || 'Custom'}
/>
{/* Global Processing Loader Overlay */}
{isProcessing && (
<View className="absolute inset-0 bg-black/75 flex items-center justify-center space-y-4">
<ActivityIndicator size="large" color="#f59e0b" />
<Text className="text-amber-500 font-mono text-sm tracking-widest font-bold">
RENDERING PHOTO...
</Text>
</View>
)}
</SafeAreaView>
);
}