fix(camera): matrix-based zoom for pinch/double-tap, tap-to-focus AE/AF lock, library letterbox smear fix, export raw JPEG bytes q95, resume isActive fixes, dark theme polish
This commit is contained in:
@@ -1,21 +1,21 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { View, StatusBar, Alert, ActivityIndicator, Text, AppState } from 'react-native';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
import { useCameraPermission, type Camera } from 'react-native-vision-camera';
|
||||
import { useCameraPermission, usePhotoOutput } from 'react-native-vision-camera';
|
||||
import * as ImagePicker from 'expo-image-picker';
|
||||
import * as FileSystem from 'expo-file-system/legacy';
|
||||
import * as Haptics from 'expo-haptics';
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
|
||||
import Header from './src/components/Header';
|
||||
import TopBar from './src/components/TopBar';
|
||||
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 ToolRail, { TabId } from './src/components/ToolRail';
|
||||
import PreviewModal from './src/components/PreviewModal';
|
||||
|
||||
import { Recipe, ColorAdjustments, FrameId, GPSInfo } from './src/types';
|
||||
import { getAllRecipes, saveCustomRecipe, deleteCustomRecipe } from './src/utils/storageUtils';
|
||||
import { DEFAULT_ADJUSTMENTS } from './src/utils/defaultRecipes';
|
||||
import { getCurrentGPS, requestLocationPermissions } from './src/utils/locationUtils';
|
||||
import { resolvePhotoGPS } from './src/utils/photoGps';
|
||||
import { processAndExportPhoto } from './src/utils/exportEngine';
|
||||
@@ -31,24 +31,13 @@ export default function App() {
|
||||
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 [adjustments, setAdjustments] = useState<ColorAdjustments>({ ...DEFAULT_ADJUSTMENTS });
|
||||
|
||||
const [selectedFrame, setSelectedFrame] = useState<FrameId>('none');
|
||||
const [useGeotag, setUseGeotag] = useState(true);
|
||||
const [gpsInfo, setGpsInfo] = useState<GPSInfo | null>(null);
|
||||
const [activeTab, setActiveTab] = useState<TabId>('recipes');
|
||||
// Progressive disclosure: a tool chip opens its panel; null = photo hero only.
|
||||
const [openTab, setOpenTab] = useState<TabId | null>(null);
|
||||
|
||||
const [libraryImageUri, setLibraryImageUri] = useState<string | null>(null);
|
||||
const [lastPhotoUri, setLastPhotoUri] = useState<string | null>(null);
|
||||
@@ -58,7 +47,11 @@ export default function App() {
|
||||
|
||||
// Hardware Permissions (vision-camera)
|
||||
const { hasPermission: cameraPermissionGranted, requestPermission: requestCameraPermission } = useCameraPermission();
|
||||
const cameraRef = useRef<Camera>(null);
|
||||
// Full-resolution photo output — attached to the live <Camera> in Viewfinder.
|
||||
// Photo capture is resolution-independent from the Skia preview output.
|
||||
const photoOutput = usePhotoOutput({
|
||||
qualityPrioritization: 'quality',
|
||||
});
|
||||
|
||||
// vision-camera may return a raw absolute path on Android; surface a file:// URI.
|
||||
const toUri = (path: string) => (/^(file|content|ph):\/\//.test(path) ? path : `file://${path}`);
|
||||
@@ -123,7 +116,8 @@ export default function App() {
|
||||
} else {
|
||||
setMode(session.mode || 'camera');
|
||||
}
|
||||
if (session.adjustments) setAdjustments(session.adjustments);
|
||||
if (session.adjustments)
|
||||
setAdjustments({ ...DEFAULT_ADJUSTMENTS, ...session.adjustments });
|
||||
if (session.selectedFrame) setSelectedFrame(session.selectedFrame);
|
||||
if (session.useGeotag !== undefined) setUseGeotag(session.useGeotag);
|
||||
if (session.gpsInfo) setGpsInfo(session.gpsInfo);
|
||||
@@ -267,7 +261,7 @@ export default function App() {
|
||||
Alert.alert('Permission Needed', 'Grant camera permission to take photos.');
|
||||
return;
|
||||
}
|
||||
if (!cameraRef.current) {
|
||||
if (!photoOutput) {
|
||||
Alert.alert('Camera Not Ready', 'Please wait for the camera to start, then try again.');
|
||||
return;
|
||||
}
|
||||
@@ -285,17 +279,23 @@ export default function App() {
|
||||
}
|
||||
}
|
||||
|
||||
const photo = await cameraRef.current.takePhoto({});
|
||||
const sourceUri = toUri(photo.path);
|
||||
const tShot = Date.now();
|
||||
const { filePath } = await photoOutput.capturePhotoToFile({}, {});
|
||||
console.error(`[capture] shot-took ${Date.now() - tShot}ms`);
|
||||
const sourceUri = toUri(filePath);
|
||||
|
||||
const tExp = Date.now();
|
||||
const result = await processAndExportPhoto(
|
||||
sourceUri,
|
||||
{ ...activeRecipe, adjustments, frameId: selectedFrame, useGeotag },
|
||||
selectedFrame,
|
||||
useGeotag,
|
||||
captureGps,
|
||||
{ dpi: 300, sharpen: true } // 300dpi header + full-res screen sharpen
|
||||
// 300dpi header + full-res screen sharpen. evFromCamera: the AE bias
|
||||
// already applied exposure compensation at capture — don't add 2^EV.
|
||||
{ dpi: 300, sharpen: true, evFromCamera: true }
|
||||
);
|
||||
console.error(`[capture] export-took ${Date.now() - tExp}ms (pipeline total ${Date.now() - tShot}ms)`);
|
||||
|
||||
if (result) {
|
||||
setLastPhotoUri(result.uri);
|
||||
@@ -358,14 +358,11 @@ export default function App() {
|
||||
};
|
||||
|
||||
return (
|
||||
<SafeAreaView className="flex-1 bg-carbon">
|
||||
<StatusBar barStyle="light-content" backgroundColor="#08080a" />
|
||||
|
||||
{/* Top Header */}
|
||||
<Header mode={mode} setMode={handleSetMode} title="CamRecipe Pro" />
|
||||
<View className="flex-1 bg-black">
|
||||
<StatusBar barStyle="light-content" translucent backgroundColor="transparent" />
|
||||
|
||||
{/* Main Viewfinder Frame */}
|
||||
<View className="flex-1 justify-center items-center px-4 py-2 bg-carbon">
|
||||
{/* Fullscreen hero: photo or live preview fills the whole screen */}
|
||||
<View className="absolute inset-0 bg-black">
|
||||
<Viewfinder
|
||||
mode={mode}
|
||||
recipe={
|
||||
@@ -384,41 +381,53 @@ export default function App() {
|
||||
libraryImageUri={libraryImageUri}
|
||||
cameraPermissionGranted={cameraPermissionGranted}
|
||||
onRequestCameraPermission={requestCameraPermission}
|
||||
cameraRef={cameraRef}
|
||||
photoOutput={photoOutput}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* Selector Tabs */}
|
||||
<TabSelector activeTab={activeTab} setActiveTab={setActiveTab} />
|
||||
{/* Top overlay: mode label + recipe name (left), Save current (right) */}
|
||||
<TopBar mode={mode} recipeName={activeRecipe.name} onSaveRecipe={handleSaveRecipe} />
|
||||
|
||||
{/* 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}
|
||||
{/* Bottom deck: chip panel (progressive disclosure) -> tool rail -> shutter */}
|
||||
<View className="absolute left-0 right-0 bottom-0 z-10">
|
||||
{openTab !== null && (
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: 'rgba(8,8,10,0.96)',
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: 'rgba(63,63,70,0.7)',
|
||||
}}
|
||||
className="rounded-t-2xl overflow-hidden border-l border-r border-zinc-800/60"
|
||||
>
|
||||
<AdjustmentPanel
|
||||
activeTab={openTab}
|
||||
recipes={recipes}
|
||||
currentRecipeId={selectedRecipe?.id || ''}
|
||||
adjustments={adjustments}
|
||||
selectedFrame={selectedFrame}
|
||||
useGeotag={useGeotag}
|
||||
onSelectRecipe={applyRecipe}
|
||||
onUpdateAdjustments={handleUpdateAdjustments}
|
||||
onUpdateFrame={handleUpdateFrame}
|
||||
onToggleGeotag={handleToggleGeotag}
|
||||
onSaveRecipe={handleSaveRecipe}
|
||||
onDeleteRecipe={handleDeleteRecipe}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
|
||||
<ToolRail activeTab={openTab} onToggle={(tab) => setOpenTab(openTab === tab ? null : tab)} />
|
||||
|
||||
<CameraControls
|
||||
lastPhotoUri={lastPhotoUri}
|
||||
onCapture={handleCapture}
|
||||
onPickImage={handlePickImage}
|
||||
onOpenPreview={() => setPreviewVisible(true)}
|
||||
isLibraryMode={mode === 'library'}
|
||||
onSwitchToCamera={() => handleSetMode('camera')}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* Bottom Camera Buttons */}
|
||||
<CameraControls
|
||||
lastPhotoUri={lastPhotoUri}
|
||||
onCapture={handleCapture}
|
||||
onPickImage={handlePickImage}
|
||||
onOpenPreview={() => setPreviewVisible(true)}
|
||||
isLibraryMode={mode === 'library'}
|
||||
onSwitchToCamera={() => handleSetMode('camera')}
|
||||
/>
|
||||
|
||||
{/* Preview Modal */}
|
||||
<PreviewModal
|
||||
visible={previewVisible}
|
||||
@@ -437,6 +446,6 @@ export default function App() {
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</SafeAreaView>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user