Bắt đầu sửa lỗi
This commit is contained in:
@@ -0,0 +1,286 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user