Startup mode default/last-session; GPS watermark default OFF; shutter OFF truly silent
- Settings STARTUP MODE chips (AsyncStorage startup_mode): default = clean camera + PROVIA neutral 4:3, last = restore full session look. - initSession: resolve sim-* recipe ids via FILM_SIMS (fix clobber of session ratio/adjustments to 4:3 canonical); restore via setSelectedRecipe not applyRecipe so user tweaks (frame/geotag/ratio) survive restore. - applyDefaultStartup turns GPS watermark OFF (user-requested default). - Shutter: OFF must not play click on must-play-policy devices. VisionCamera v5.2.3 HybridPhotoOutput.kt plays MediaActionSound itself gated by (enableShutterSound ?: true) || mustPlayShutterSound — on Xiaomi mustPlay=true so OFF still clicked. Both capture paths now gate on enableShutterSound != false. node_modules patch not trackable -> artifacts/patches/vision-camera-shutter-silent.patch. Verified on Xiaomi 12S Ultra: log SND user=false -> play=false; cold-start DEFAULT shows GPS WATERMARK OFF.
This commit is contained in:
@@ -16,9 +16,9 @@ import ToolRail, { TabId } from './src/components/ToolRail';
|
|||||||
import SettingsModal from './src/components/SettingsModal';
|
import SettingsModal from './src/components/SettingsModal';
|
||||||
import PhotoViewerModal from './src/components/PhotoViewerModal';
|
import PhotoViewerModal from './src/components/PhotoViewerModal';
|
||||||
|
|
||||||
import { Recipe, ColorAdjustments, FrameId, GPSInfo, AspectRatio, MeterMode, PhotoEntry, ShutterSound } from './src/types';
|
import { Recipe, ColorAdjustments, FrameId, GPSInfo, AspectRatio, MeterMode, PhotoEntry, ShutterSound, StartupMode } from './src/types';
|
||||||
import { getAllRecipes, saveCustomRecipe, deleteCustomRecipe } from './src/utils/storageUtils';
|
import { getAllRecipes, saveCustomRecipe, deleteCustomRecipe } from './src/utils/storageUtils';
|
||||||
import { DEFAULT_ADJUSTMENTS } from './src/utils/defaultRecipes';
|
import { DEFAULT_ADJUSTMENTS, FILM_SIMS, filmSimToRecipe } from './src/utils/defaultRecipes';
|
||||||
import { getCurrentGPS, requestLocationPermissions } from './src/utils/locationUtils';
|
import { getCurrentGPS, requestLocationPermissions } from './src/utils/locationUtils';
|
||||||
import { resolvePhotoGPS } from './src/utils/photoGps';
|
import { resolvePhotoGPS } from './src/utils/photoGps';
|
||||||
import { processAndExportPhoto } from './src/utils/exportEngine';
|
import { processAndExportPhoto } from './src/utils/exportEngine';
|
||||||
@@ -29,6 +29,7 @@ import { PARAM_DEFS } from './src/utils/paramDefs';
|
|||||||
import './global.css';
|
import './global.css';
|
||||||
|
|
||||||
const SESSION_KEY = 'saved_session';
|
const SESSION_KEY = 'saved_session';
|
||||||
|
const STARTUP_MODE_KEY = 'startup_mode';
|
||||||
|
|
||||||
// P5: native Kotlin export path. EXPO_PUBLIC_NATIVE_EXPORT=1 swaps the Skia
|
// P5: native Kotlin export path. EXPO_PUBLIC_NATIVE_EXPORT=1 swaps the Skia
|
||||||
// render engine for the RecipesCamExport native pipeline (same look contract,
|
// render engine for the RecipesCamExport native pipeline (same look contract,
|
||||||
@@ -100,6 +101,9 @@ export default function App() {
|
|||||||
const [settingsVisible, setSettingsVisible] = useState(false);
|
const [settingsVisible, setSettingsVisible] = useState(false);
|
||||||
// Shutter click: only OFF/ON (no volume API on Android). Applied at capture.
|
// Shutter click: only OFF/ON (no volume API on Android). Applied at capture.
|
||||||
const [shutterSound, setShutterSound] = useState<ShutterSound>('on');
|
const [shutterSound, setShutterSound] = useState<ShutterSound>('on');
|
||||||
|
// Startup behavior: 'default' = PROVIA neutral at 4:3 camera; 'last' = restore
|
||||||
|
// previous session from AsyncStorage. Default is 'default'.
|
||||||
|
const [startupMode, setStartupMode] = useState<StartupMode>('default');
|
||||||
const photoSeq = useRef(0);
|
const photoSeq = useRef(0);
|
||||||
const [isProcessing, setIsProcessing] = useState(false);
|
const [isProcessing, setIsProcessing] = useState(false);
|
||||||
// Short shutter-burst lock: guards the capturePhotoToFile calls from
|
// Short shutter-burst lock: guards the capturePhotoToFile calls from
|
||||||
@@ -195,6 +199,34 @@ export default function App() {
|
|||||||
const list = await getAllRecipes();
|
const list = await getAllRecipes();
|
||||||
setRecipes(list);
|
setRecipes(list);
|
||||||
|
|
||||||
|
// Startup mode: 'default' opens camera + PROVIA film sim (neutral
|
||||||
|
// adjustments, DEFAULT_ADJUSTMENTS) at 4:3. 'last' restores the previous
|
||||||
|
// session from AsyncStorage; with no saved session it falls back to the
|
||||||
|
// same clean default state.
|
||||||
|
let startup: StartupMode = 'default';
|
||||||
|
try {
|
||||||
|
const raw = await AsyncStorage.getItem(STARTUP_MODE_KEY);
|
||||||
|
if (raw === 'last') startup = 'last';
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Failed to read startup mode:', e);
|
||||||
|
}
|
||||||
|
setStartupMode(startup);
|
||||||
|
|
||||||
|
const applyDefaultStartup = () => {
|
||||||
|
setMode('camera');
|
||||||
|
setLibraryImageUri(null);
|
||||||
|
setAspectRatio('4:3');
|
||||||
|
const provia = FILM_SIMS.find((f) => f.id === 'sim-provia');
|
||||||
|
if (provia) applyRecipe(filmSimToRecipe(provia));
|
||||||
|
// Clean camera start: GPS watermark stays OFF until the user opts in.
|
||||||
|
setUseGeotag(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (startup === 'default') {
|
||||||
|
applyDefaultStartup();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let session: any = null;
|
let session: any = null;
|
||||||
try {
|
try {
|
||||||
const raw = await AsyncStorage.getItem(SESSION_KEY);
|
const raw = await AsyncStorage.getItem(SESSION_KEY);
|
||||||
@@ -230,14 +262,22 @@ export default function App() {
|
|||||||
if (session.shutterSound === 'on' || session.shutterSound === 'off') setShutterSound(session.shutterSound);
|
if (session.shutterSound === 'on' || session.shutterSound === 'off') setShutterSound(session.shutterSound);
|
||||||
if (session.gpsInfo) setGpsInfo(session.gpsInfo);
|
if (session.gpsInfo) setGpsInfo(session.gpsInfo);
|
||||||
if (session.selectedRecipeId) {
|
if (session.selectedRecipeId) {
|
||||||
restoredRecipe = list.find((r: Recipe) => r.id === session.selectedRecipeId) || null;
|
// Film sims are never persisted in the recipe list (sims applied on the
|
||||||
|
// fly) — resolve sim ids via FILM_SIMS, real recipes via the list.
|
||||||
|
const sim = FILM_SIMS.find((f) => f.id === session.selectedRecipeId);
|
||||||
|
restoredRecipe = sim
|
||||||
|
? filmSimToRecipe(sim)
|
||||||
|
: list.find((r: Recipe) => r.id === session.selectedRecipeId) || null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (restoredRecipe) {
|
if (restoredRecipe) {
|
||||||
applyRecipe(restoredRecipe);
|
// Recipe identity restored; the look (adjustments/frame/geotag/ratio)
|
||||||
} else if (list.length > 0) {
|
// stays exactly as saved in the session above — applyRecipe would reset
|
||||||
applyRecipe(list[0]);
|
// them to the recipe's canonical values and drop the user's tweaks.
|
||||||
|
setSelectedRecipe(restoredRecipe);
|
||||||
|
} else {
|
||||||
|
applyDefaultStartup();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -315,6 +355,11 @@ export default function App() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleStartupMode = (mode: StartupMode) => {
|
||||||
|
setStartupMode(mode);
|
||||||
|
AsyncStorage.setItem(STARTUP_MODE_KEY, mode).catch(() => {});
|
||||||
|
};
|
||||||
|
|
||||||
// Latest-rawEnabled ref so the stable session-error callback below can read
|
// Latest-rawEnabled ref so the stable session-error callback below can read
|
||||||
// the live toggle without re-subscribing the Camera session on every render.
|
// the live toggle without re-subscribing the Camera session on every render.
|
||||||
const rawEnabledRef = useRef(rawEnabled);
|
const rawEnabledRef = useRef(rawEnabled);
|
||||||
@@ -851,6 +896,8 @@ export default function App() {
|
|||||||
onMeterModeChange={setMeterMode}
|
onMeterModeChange={setMeterMode}
|
||||||
shutterSound={shutterSound}
|
shutterSound={shutterSound}
|
||||||
onShutterSound={setShutterSound}
|
onShutterSound={setShutterSound}
|
||||||
|
startupMode={startupMode}
|
||||||
|
onStartupMode={handleStartupMode}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Session photo viewer (swipe across captured/exported photos) */}
|
{/* Session photo viewer (swipe across captured/exported photos) */}
|
||||||
|
|||||||
@@ -79,7 +79,9 @@ Exit: user QA đạt → mặc định native; legacy chỉ fallback.
|
|||||||
|
|
||||||
### P7 — Dọn & ship
|
### P7 — Dọn & ship
|
||||||
- Xoá probe/dev code, xoá env gate; typecheck; commit + push (KHÔNG stage `.kilo/`, `.expo/`); `assembleRelease`; báo MD5 APK.
|
- Xoá probe/dev code, xoá env gate; typecheck; commit + push (KHÔNG stage `.kilo/`, `.expo/`); `assembleRelease`; báo MD5 APK.
|
||||||
- Nhắc: nếu npm install chạy lại → áp lại patch node_modules vision-camera (`HybridPhotoOutput.kt` "RecipesCam patch").
|
- Nhắc: nếu npm install chạy lại → node_modules vision-camera về pristine, áp lại patch:
|
||||||
|
`cd RecipesCam && git apply patches/vision-camera-shutter-silent.patch`
|
||||||
|
(artifact = diff `HybridPhotoOutput.kt`: shutter OFF thật silent — bỏ OR mustPlayShutterSound).
|
||||||
|
|
||||||
## Rủi ro / ghi chú
|
## Rủi ro / ghi chú
|
||||||
- Local module phải bắt chước đúng cấu trúc expo package (đã đọc expo-media-library: `plugins { id 'com.android.library'; id 'expo-module-gradle-plugin' }`, expo-module.config.json khai báo class Kotlin; autolinking quét `./modules`).
|
- Local module phải bắt chước đúng cấu trúc expo package (đã đọc expo-media-library: `plugins { id 'com.android.library'; id 'expo-module-gradle-plugin' }`, expo-module.config.json khai báo class Kotlin; autolinking quét `./modules`).
|
||||||
@@ -101,3 +103,9 @@ Exit: user QA đạt → mặc định native; legacy chỉ fallback.
|
|||||||
- [x] P5 hook export + flag
|
- [x] P5 hook export + flag
|
||||||
- [ ] P6 parity/perf gate — probe chạy xong trên Xiaomi (1fdf765): classic-neg / retro-amber-frame / cinema-summer trên ảnh real 2304x3072; native off-thread 2.7-2.9s (JS ticks 65-70 vẫn chạy) vs legacy GPU 0.36-0.5s (JS-block); diff meanAbs 7.1/13.1/13.3, gt12% 16.8/42.1/42.7, gt32% 0.14/3.7/4.0 (grain procedural + JPEG encoder + tone approx; retro thấp nhất — mist + frame classic). CHỜ user QA ảnh ghép DCIM/parity_*.jpg → flip default native + quyết GPU
|
- [ ] P6 parity/perf gate — probe chạy xong trên Xiaomi (1fdf765): classic-neg / retro-amber-frame / cinema-summer trên ảnh real 2304x3072; native off-thread 2.7-2.9s (JS ticks 65-70 vẫn chạy) vs legacy GPU 0.36-0.5s (JS-block); diff meanAbs 7.1/13.1/13.3, gt12% 16.8/42.1/42.7, gt32% 0.14/3.7/4.0 (grain procedural + JPEG encoder + tone approx; retro thấp nhất — mist + frame classic). CHỜ user QA ảnh ghép DCIM/parity_*.jpg → flip default native + quyết GPU
|
||||||
- [ ] P7 dọn + ship
|
- [ ] P7 dọn + ship
|
||||||
|
|
||||||
|
## Ngoài P-series (feature branch)
|
||||||
|
- [x] Startup mode DEFAULT/LAST SESSION (Settings chip + AsyncStorage `startup_mode`; restore session qua FILM_SIMS; fix clobber ratio khi sim selected — 3 file LF)
|
||||||
|
- [x] GPS watermark mặc định OFF khi startup DEFAULT (applyDefaultStartup setUseGeotag(false)) — verify UI Xiaomi: cold start DEFAULT → FRAME tab GPS WATERMARK OFF
|
||||||
|
- [x] Shutter OFF thật silent (patch HybridPhotoOutput.kt, 2 path, artifact patches/) — verify log ON/OFF trên Xiaomi
|
||||||
|
- [ ] Zoom camera native: probe physical zoom + zoom UI (đang làm)
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
diff --git a/node_modules/react-native-vision-camera/android/src/main/java/com/margelo/nitro/camera/hybrids/outputs/HybridPhotoOutput.kt b/node_modules/react-native-vision-camera/android/src/main/java/com/margelo/nitro/camera/hybrids/outputs/HybridPhotoOutput.kt
|
||||||
|
index f3bdfe3..883fa50 100644
|
||||||
|
--- a/node_modules/react-native-vision-camera/android/src/main/java/com/margelo/nitro/camera/hybrids/outputs/HybridPhotoOutput.kt
|
||||||
|
+++ b/node_modules/react-native-vision-camera/android/src/main/java/com/margelo/nitro/camera/hybrids/outputs/HybridPhotoOutput.kt
|
||||||
|
@@ -2,7 +2,6 @@ package com.margelo.nitro.camera.hybrids.outputs
|
||||||
|
|
||||||
|
import android.media.MediaActionSound
|
||||||
|
import android.util.Log
|
||||||
|
-import androidx.camera.core.CameraInfo
|
||||||
|
import androidx.camera.core.CameraSelector
|
||||||
|
import androidx.camera.core.ImageCapture
|
||||||
|
import androidx.camera.core.resolutionselector.ResolutionSelector
|
||||||
|
@@ -161,7 +160,11 @@ class HybridPhotoOutput(
|
||||||
|
|
||||||
|
// 1. Get settings
|
||||||
|
val isMirrored = shouldMirror()
|
||||||
|
- val enableShutterSound = (settings.enableShutterSound ?: true) || CameraInfo.mustPlayShutterSound()
|
||||||
|
+ // The click is played by THIS app via MediaActionSound below —
|
||||||
|
+ // CameraInfo.mustPlayShutterSound() only reports device/region policy, it
|
||||||
|
+ // does not make the OS play anything. Honor the user toggle: OFF stays
|
||||||
|
+ // silent even on devices CameraX flags as must-play.
|
||||||
|
+ val enableShutterSound = settings.enableShutterSound != false
|
||||||
|
imageCapture.flashMode = settings.flashMode?.toFlashMode() ?: ImageCapture.FLASH_MODE_OFF
|
||||||
|
val location =
|
||||||
|
if (settings.location != null) {
|
||||||
|
@@ -170,7 +173,6 @@ class HybridPhotoOutput(
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
-
|
||||||
|
// 2. Perform Capture
|
||||||
|
var didFireOnDidCapturePhoto = false
|
||||||
|
val image =
|
||||||
|
@@ -228,8 +230,7 @@ class HybridPhotoOutput(
|
||||||
|
|
||||||
|
// 1. Get settings
|
||||||
|
val isMirrored = shouldMirror()
|
||||||
|
- val enableShutterSound =
|
||||||
|
- (settings.enableShutterSound ?: true) || CameraInfo.mustPlayShutterSound()
|
||||||
|
+ val enableShutterSound = settings.enableShutterSound != false
|
||||||
|
imageCapture.flashMode = settings.flashMode?.toFlashMode() ?: ImageCapture.FLASH_MODE_OFF
|
||||||
|
val location =
|
||||||
|
if (settings.location != null) {
|
||||||
@@ -2,7 +2,7 @@ import React from 'react';
|
|||||||
import { View, Text, Modal, TouchableOpacity } from 'react-native';
|
import { View, Text, Modal, TouchableOpacity } from 'react-native';
|
||||||
import * as Haptics from 'expo-haptics';
|
import * as Haptics from 'expo-haptics';
|
||||||
import { X, Volume2, Settings as SettingsIcon } from 'lucide-react-native';
|
import { X, Volume2, Settings as SettingsIcon } from 'lucide-react-native';
|
||||||
import { AspectRatio, MeterMode, ShutterSound } from '../types';
|
import { AspectRatio, MeterMode, ShutterSound, StartupMode } from '../types';
|
||||||
|
|
||||||
// Gear-sheet (gear button between library picker and shutter). Groups the
|
// Gear-sheet (gear button between library picker and shutter). Groups the
|
||||||
// capture/composition choices that are NOT look-adjustments, so the tool rail
|
// capture/composition choices that are NOT look-adjustments, so the tool rail
|
||||||
@@ -30,6 +30,8 @@ interface SettingsModalProps {
|
|||||||
onMeterModeChange: (mode: MeterMode) => void;
|
onMeterModeChange: (mode: MeterMode) => void;
|
||||||
shutterSound: ShutterSound;
|
shutterSound: ShutterSound;
|
||||||
onShutterSound: (sound: ShutterSound) => void;
|
onShutterSound: (sound: ShutterSound) => void;
|
||||||
|
startupMode: StartupMode;
|
||||||
|
onStartupMode: (mode: StartupMode) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ChipRow({ chips }: { chips: Chip[] }) {
|
function ChipRow({ chips }: { chips: Chip[] }) {
|
||||||
@@ -85,6 +87,8 @@ export default function SettingsModal({
|
|||||||
onMeterModeChange,
|
onMeterModeChange,
|
||||||
shutterSound,
|
shutterSound,
|
||||||
onShutterSound,
|
onShutterSound,
|
||||||
|
startupMode,
|
||||||
|
onStartupMode,
|
||||||
}: SettingsModalProps) {
|
}: SettingsModalProps) {
|
||||||
return (
|
return (
|
||||||
<Modal visible={visible} transparent animationType="fade" onRequestClose={onClose}>
|
<Modal visible={visible} transparent animationType="fade" onRequestClose={onClose}>
|
||||||
@@ -115,6 +119,22 @@ export default function SettingsModal({
|
|||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
{/* Startup mode */}
|
||||||
|
<View>
|
||||||
|
<SectionLabel>STARTUP MODE</SectionLabel>
|
||||||
|
<ChipRow
|
||||||
|
chips={[
|
||||||
|
{ key: 'startup-default', label: 'DEFAULT', active: startupMode === 'default', onPress: () => onStartupMode('default') },
|
||||||
|
{ key: 'startup-last', label: 'LAST SESSION', active: startupMode === 'last', onPress: () => onStartupMode('last') },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<View className="flex-row items-center mt-1.5 ml-1">
|
||||||
|
<Text className="text-zinc-600 font-mono text-[10px] ml-1.5">
|
||||||
|
DEFAULT: camera + PROVIA (neutral adjustments) at 4:3. LAST SESSION: restore your previous look on startup.
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
{/* Shutter sound */}
|
{/* Shutter sound */}
|
||||||
<View>
|
<View>
|
||||||
<SectionLabel>SHUTTER SOUND</SectionLabel>
|
<SectionLabel>SHUTTER SOUND</SectionLabel>
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ export type MeterMode = 'evaluative' | 'highlight';
|
|||||||
// Camera shutter sound. OS policy may force the click on in some regions;
|
// Camera shutter sound. OS policy may force the click on in some regions;
|
||||||
// there is no native volume control (OFF/ON only).
|
// there is no native volume control (OFF/ON only).
|
||||||
export type ShutterSound = 'on' | 'off';
|
export type ShutterSound = 'on' | 'off';
|
||||||
|
export type StartupMode = 'default' | 'last';
|
||||||
// One photo in the in-app session history (recent captures + library exports).
|
// One photo in the in-app session history (recent captures + library exports).
|
||||||
// uri = app-cache JPEG; assetId (when savedToLibrary) = the MediaLibrary asset
|
// uri = app-cache JPEG; assetId (when savedToLibrary) = the MediaLibrary asset
|
||||||
// so the trash button can remove the gallery copy too.
|
// so the trash button can remove the gallery copy too.
|
||||||
|
|||||||
Reference in New Issue
Block a user