Ship HDF EFFECT to PRO only, and clean the build for Play

Rename LIGHT GLOW to HDF EFFECT and gate it behind PRO: LITE renders the
chip greyed out with a PRO tag and cannot open its slider; restoration of
older settings that carry hdf > 0 still blocks export in LITE.

Play/licence pass:
- drop unused permissions (ACTIVITY_RECOGNITION, READ_MEDIA_AUDIO/VIDEO,
  SYSTEM_ALERT_WINDOW) and requestLegacyExternalStorage
- rename the Leica sims to LC STREETLIFE CLASSIC/VIVID, drop the brand from
  comments; fix app name text, canvaskit copyright, LICENSE owner
- remove expo-image-picker and expo-sensors (no JS import), delete dead
  exports (computeMatrixTint, cinemaSeasonName, base64 helpers,
  formatCoordinate)
- versionCode 3 / versionName 1.2.1, drop the personal email from About
- add docs/privacy-policy.html for the Play listing

Size: R8 + shrinkResources + bundle compression + ABI trim, 161 MB -> 76 MB.
This commit is contained in:
2026-09-15 11:36:52 +07:00
parent 42756ac7c3
commit 27a9dfb65f
25 changed files with 177 additions and 172 deletions
+1 -1
View File
@@ -1030,7 +1030,7 @@ export default function App() {
// PRO unlocks every look, so a paying user has nothing left in use: only a
// Lite session can trip them: a printed frame it previewed, or a GPS stamp
// that arrived with a restored session.
const proLook = pro ? null : proLookInUse({ frame: selectedFrame, gpsWm: useGeotag });
const proLook = pro ? null : proLookInUse({ frame: selectedFrame, gpsWm: useGeotag, hdf: adjustments.hdf ?? 0 });
// The native Kotlin exporter has no text-watermark draw, so a Lite export
// (which always carries the mark) must take the Skia engine.
const nativeExport = NATIVE_EXPORT && pro;
+1 -1
View File
@@ -1,6 +1,6 @@
The MIT License (MIT)
Copyright (c) 2015-present 650 Industries, Inc. (aka Expo)
Copyright (c) 2026 Lộc Phạm
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
+5 -5
View File
@@ -3,14 +3,14 @@
This app (recipescamera) embeds or depends on the following open-source
software. Full license texts are reproduced below, grouped by license.
Generated 2026-09-09 from node_modules audit — 32 shipped components.
Generated 2026-09-09 from node_modules audit, rev. 2026-09-14 — 31 shipped components.
## Components
| Component | Version | License | Copyright / Author | Project |
|---|---|---|---|---|
| Skia graphics library (bundled native in react-native-skia) | bundled | BSD-3-Clause | Copyright 2011 Google Inc. All rights reserved. | https://skia.org/ |
| canvaskit-wasm | 0.41.0 | BSD-3-Clause | Copyright 2020 Google LLC | https://skia.org/ |
| canvaskit-wasm | 0.41.0 | BSD-3-Clause | Copyright (c) 2011 Google Inc. All rights reserved. | https://skia.org/ |
| lucide-react-native | 1.24.0 | ISC | Copyright (c) 2026 Lucide Icons and Contributors | https://lucide.dev |
| @react-native-async-storage/async-storage | 2.2.0 | MIT | Copyright (c) 2015-present, Facebook, Inc. | https://github.com/react-native-async-storage/async-storage#readme |
| @react-native-community/slider | 5.2.0 | MIT | react-native-community | https://github.com/callstack/react-native-slider#readme |
@@ -18,10 +18,10 @@ Generated 2026-09-09 from node_modules audit — 32 shipped components.
| expo | 57.0.7 | MIT | Copyright (c) 2015-present 650 Industries, Inc. (aka Expo) | https://github.com/expo/expo/tree/main/packages/expo |
| expo-asset | 57.0.6 | MIT | Copyright (c) 2015-present 650 Industries, Inc. (aka Expo) | https://docs.expo.dev/versions/latest/sdk/asset/ |
| expo-dev-client | 57.0.6 | MIT | Copyright (c) 2015-present 650 Industries, Inc. (aka Expo) | https://docs.expo.dev/versions/latest/sdk/dev-client/ |
| expo-document-picker | 57.0.2 | MIT | Copyright (c) 2015-present 650 Industries, Inc. (aka Expo) | https://docs.expo.dev/versions/latest/sdk/document-picker/ |
| expo-file-system | 57.0.1 | MIT | Copyright (c) 2015-present 650 Industries, Inc. (aka Expo) | https://docs.expo.dev/versions/latest/sdk/filesystem/ |
| expo-font | 57.0.1 | MIT | Copyright (c) 2015-present 650 Industries, Inc. (aka Expo) | https://docs.expo.dev/versions/latest/sdk/font/ |
| expo-haptics | 57.0.1 | MIT | Copyright (c) 2015-present 650 Industries, Inc. (aka Expo) | https://docs.expo.dev/versions/latest/sdk/haptics/ |
| expo-image-picker | 57.0.4 | MIT | Copyright (c) 2015-present 650 Industries, Inc. (aka Expo) | https://docs.expo.dev/versions/latest/sdk/imagepicker/ |
| expo-keep-awake | 57.0.1 | MIT | Copyright (c) 2015-present 650 Industries, Inc. (aka Expo) | https://docs.expo.dev/versions/latest/sdk/keep-awake/ |
| expo-location | 57.0.4 | MIT | Copyright (c) 2015-present 650 Industries, Inc. (aka Expo) | https://docs.expo.dev/versions/latest/sdk/location/ |
| expo-media-library | 57.0.2 | MIT | Copyright (c) 2015-present 650 Industries, Inc. (aka Expo) | https://docs.expo.dev/versions/latest/sdk/media-library/ |
@@ -44,7 +44,7 @@ Generated 2026-09-09 from node_modules audit — 32 shipped components.
## BSD-3-Clause
Applied to: Skia graphics library (bundled native in react-native-skia) (Copyright 2011 Google Inc. All rights reserved.), canvaskit-wasm (Copyright 2020 Google LLC)
Applied to: Skia graphics library (bundled native in react-native-skia) (Copyright 2011 Google Inc. All rights reserved.), canvaskit-wasm (Copyright (c) 2011 Google Inc. All rights reserved.)
```text
// Copyright (c) 2011 Google Inc. All rights reserved.
@@ -130,7 +130,7 @@ SOFTWARE.
## MIT
Applied to: @react-native-async-storage/async-storage (Copyright (c) 2015-present, Facebook, Inc.), @react-native-community/slider (react-native-community), @shopify/react-native-skia (Copyright 2021-present, Shopify Inc.), expo (Copyright (c) 2015-present 650 Industries, Inc. (aka Expo)), expo-asset (Copyright (c) 2015-present 650 Industries, Inc. (aka Expo)), expo-dev-client (Copyright (c) 2015-present 650 Industries, Inc. (aka Expo)), expo-file-system (Copyright (c) 2015-present 650 Industries, Inc. (aka Expo)), expo-font (Copyright (c) 2015-present 650 Industries, Inc. (aka Expo)), expo-haptics (Copyright (c) 2015-present 650 Industries, Inc. (aka Expo)), expo-image-picker (Copyright (c) 2015-present 650 Industries, Inc. (aka Expo)), expo-keep-awake (Copyright (c) 2015-present 650 Industries, Inc. (aka Expo)), expo-location (Copyright (c) 2015-present 650 Industries, Inc. (aka Expo)), expo-media-library (Copyright (c) 2015-present 650 Industries, Inc. (aka Expo)), expo-sharing (Copyright (c) 2015-present 650 Industries, Inc. (aka Expo)), expo-status-bar (Copyright (c) 2015-present 650 Industries, Inc. (aka Expo)), nativewind (Copyright (c) 2023 Mark Lawlor), react (Copyright (c) Meta Platforms, Inc. and affiliates.), react-dom (Copyright (c) Meta Platforms, Inc. and affiliates.), react-native (Copyright (c) Meta Platforms, Inc. and affiliates.), react-native-nitro-image (Marc Rousavy <me@mrousavy.com> (https://github.com/mrousavy)), react-native-nitro-modules (Marc Rousavy <me@mrousavy.com> (https://github.com/mrousavy)), react-native-reanimated (Copyright (c) 2016 Software Mansion <swmansion.com>), react-native-safe-area-context (Copyright (c) 2019 Th3rd Wave), react-native-vision-camera (Marc Rousavy <me@mrousavy.com> (https://github.com/mrousavy)), react-native-vision-camera-skia (Marc Rousavy <me@mrousavy.com> (https://github.com/mrousavy)), react-native-vision-camera-worklets (Marc Rousavy <me@mrousavy.com> (https://github.com/mrousavy)), react-native-web (Copyright (c) Nicolas Gallagher.)
Applied to: @react-native-async-storage/async-storage (Copyright (c) 2015-present, Facebook, Inc.), @react-native-community/slider (react-native-community), @shopify/react-native-skia (Copyright 2021-present, Shopify Inc.), expo (Copyright (c) 2015-present 650 Industries, Inc. (aka Expo)), expo-asset (Copyright (c) 2015-present 650 Industries, Inc. (aka Expo)), expo-dev-client (Copyright (c) 2015-present 650 Industries, Inc. (aka Expo)), expo-document-picker (Copyright (c) 2015-present 650 Industries, Inc. (aka Expo)), expo-file-system (Copyright (c) 2015-present 650 Industries, Inc. (aka Expo)), expo-font (Copyright (c) 2015-present 650 Industries, Inc. (aka Expo)), expo-haptics (Copyright (c) 2015-present 650 Industries, Inc. (aka Expo)), expo-keep-awake (Copyright (c) 2015-present 650 Industries, Inc. (aka Expo)), expo-location (Copyright (c) 2015-present 650 Industries, Inc. (aka Expo)), expo-media-library (Copyright (c) 2015-present 650 Industries, Inc. (aka Expo)), expo-sharing (Copyright (c) 2015-present 650 Industries, Inc. (aka Expo)), expo-status-bar (Copyright (c) 2015-present 650 Industries, Inc. (aka Expo)), nativewind (Copyright (c) 2023 Mark Lawlor), react (Copyright (c) Meta Platforms, Inc. and affiliates.), react-dom (Copyright (c) Meta Platforms, Inc. and affiliates.), react-native (Copyright (c) Meta Platforms, Inc. and affiliates.), react-native-nitro-image (Marc Rousavy <me@mrousavy.com> (https://github.com/mrousavy)), react-native-nitro-modules (Marc Rousavy <me@mrousavy.com> (https://github.com/mrousavy)), react-native-reanimated (Copyright (c) 2016 Software Mansion <swmansion.com>), react-native-safe-area-context (Copyright (c) 2019 Th3rd Wave), react-native-vision-camera (Marc Rousavy <me@mrousavy.com> (https://github.com/mrousavy)), react-native-vision-camera-skia (Marc Rousavy <me@mrousavy.com> (https://github.com/mrousavy)), react-native-vision-camera-worklets (Marc Rousavy <me@mrousavy.com> (https://github.com/mrousavy)), react-native-web (Copyright (c) Nicolas Gallagher.)
```text
MIT License
+2 -2
View File
@@ -92,8 +92,8 @@ android {
applicationId 'com.locphamtran.recipescamera'
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 2
versionName "1.2"
versionCode 3
versionName "1.2.1"
buildConfigField "String", "REACT_NATIVE_RELEASE_LEVEL", "\"${findProperty('reactNativeReleaseLevel') ?: 'stable'}\""
+1 -4
View File
@@ -5,12 +5,9 @@
<uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" tools:replace="android:maxSdkVersion"/>
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO"/>
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/>
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO"/>
<uses-permission android:name="android.permission.READ_MEDIA_VISUAL_USER_SELECTED"/>
<uses-permission android:name="android.permission.RECORD_AUDIO" tools:node="remove"/>
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
<uses-permission android:name="android.permission.VIBRATE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="32" tools:replace="android:maxSdkVersion"/>
<queries>
@@ -20,7 +17,7 @@
<data android:scheme="https"/>
</intent>
</queries>
<application android:name=".MainApplication" android:label="@string/app_name" android:icon="@mipmap/ic_launcher" android:roundIcon="@mipmap/ic_launcher_round" android:allowBackup="true" android:theme="@style/AppTheme" android:supportsRtl="true" android:enableOnBackInvokedCallback="false" android:requestLegacyExternalStorage="true">
<application android:name=".MainApplication" android:label="@string/app_name" android:icon="@mipmap/ic_launcher" android:roundIcon="@mipmap/ic_launcher_round" android:allowBackup="true" android:theme="@style/AppTheme" android:supportsRtl="true" android:enableOnBackInvokedCallback="false">
<meta-data android:name="expo.modules.updates.ENABLED" android:value="false"/>
<meta-data android:name="expo.modules.updates.ENABLE_BSDIFF_PATCH_SUPPORT" android:value="true"/>
<meta-data android:name="expo.modules.updates.EXPO_UPDATES_CHECK_ON_LAUNCH" android:value="ALWAYS"/>
+8 -2
View File
@@ -29,7 +29,7 @@ android.enablePngCrunchInReleaseBuilds=true
# Use this property to specify which architecture you want to build.
# You can also override it from the CLI using
# ./gradlew <task> -PreactNativeArchitectures=x86_64
reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64
reactNativeArchitectures=armeabi-v7a,arm64-v8a
# Use this property to enable support to the new architecture.
# This will allow you to use TurboModules and the Fabric render in
@@ -61,4 +61,10 @@ EX_DEV_CLIENT_NETWORK_INSPECTOR=true
# Use legacy packaging to compress native libraries in the resulting APK.
expo.useLegacyPackaging=false
expo.inlineModules.watchedDirectories=[]
expo.inlineModules.watchedDirectories=[]
# Shrink the shipped APK: R8 strips unused Java/Kotlin, shrinkResources drops
# unreferenced resources, bundle compression gzips the embedded JS bundle.
android.enableMinifyInReleaseBuilds=true
android.enableShrinkResourcesInReleaseBuilds=true
android.enableBundleCompression=true
+5 -13
View File
@@ -2,14 +2,14 @@
"expo": {
"name": "RecipesCam",
"slug": "recipescamera",
"version": "1.2",
"version": "1.2.1",
"orientation": "portrait",
"icon": "./RecipesCamIcon.png",
"userInterfaceStyle": "light",
"ios": {
"supportsTablet": true,
"infoPlist": {
"NSCameraUsageDescription": "Allow CamRecipe Pro to access your camera."
"NSCameraUsageDescription": "Allow RecipesCam to access your camera."
}
},
"android": {
@@ -27,8 +27,7 @@
"android.permission.READ_EXTERNAL_STORAGE",
"android.permission.WRITE_EXTERNAL_STORAGE",
"android.permission.READ_MEDIA_VISUAL_USER_SELECTED",
"android.permission.READ_MEDIA_IMAGES",
"android.permission.READ_MEDIA_VIDEO"
"android.permission.READ_MEDIA_IMAGES"
],
"package": "com.locphamtran.recipescamera"
},
@@ -40,23 +39,16 @@
[
"expo-location",
{
"locationAlwaysAndWhenInUsePermission": "Allow CamRecipe Pro to geotag your photos with location watermark."
"locationAlwaysAndWhenInUsePermission": "Allow RecipesCam to geotag your photos with location watermark."
}
],
[
"expo-media-library",
{
"photosPermission": "Allow CamRecipe Pro to save processed photos to your library.",
"photosPermission": "Allow RecipesCam to save processed photos to your library.",
"isAccessMediaLocationEnabled": true
}
],
[
"expo-image-picker",
{
"photosPermission": "Allow CamRecipe Pro to select photos from your library to apply presets.",
"microphonePermission": false
}
],
"expo-sharing"
],
"extra": {
+110
View File
@@ -0,0 +1,110 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>RecipesCam — Privacy Policy</title>
<style>
:root { color-scheme: light dark; }
body { max-width: 46rem; margin: 0 auto; padding: 2rem 1.25rem 4rem;
font: 16px/1.65 -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif; }
h1 { font-size: 1.6rem; margin-bottom: .25rem; }
h2 { font-size: 1.1rem; margin-top: 2rem; }
.meta { color: #666; font-size: .9rem; }
code { background: rgba(127,127,127,.15); padding: .1em .3em; border-radius: 4px; }
</style>
</head>
<body>
<h1>RecipesCam — Privacy Policy</h1>
<p class="meta">Applies to the Android apps <strong>RecipesCam</strong> (com.locphamtran.recipescamera, free/LITE)
and <strong>RecipesCam Pro</strong> (com.locphamtran.recipescamera.pro, paid/PRO).<br>
Effective date: 14 September 2026 · Last updated: 14 September 2026</p>
<p>RecipesCam is a camera app: it applies film-style recipes to photos you take or pick, and saves the
result to your device. It is built and published by an individual developer ("we"). This policy explains
what the app accesses and what happens to that data.</p>
<h2>Summary</h2>
<ul>
<li>The app has <strong>no user accounts</strong> and <strong>no server of ours</strong>.</li>
<li><strong>No photo, location, or usage data is transmitted to us or to any third party.</strong> There is no
analytics, advertising, crash-reporting, or tracking SDK in the app.</li>
<li>Everything the app does — capture, processing, watermarking, saving — happens on your device.</li>
</ul>
<h2>Data the app accesses, and why</h2>
<table>
<tr><th align="left">Access</th><th align="left">Permission</th><th align="left">Purpose</th></tr>
<tr>
<td>Camera</td><td><code>CAMERA</code></td>
<td>To take photos and show the live preview. Photos stay on your device.</td>
</tr>
<tr>
<td>Photos and videos in your library</td>
<td><code>READ_MEDIA_IMAGES</code>, <code>READ_MEDIA_VISUAL_USER_SELECTED</code>,
<code>READ_EXTERNAL_STORAGE</code> (Android 12 and older)</td>
<td>To let you pick an existing photo to edit, and to show your library inside the app's picker.</td>
</tr>
<tr>
<td>Save processed photos</td>
<td><code>WRITE_EXTERNAL_STORAGE</code> (Android 9 and older) and the system media-library APIs</td>
<td>To write the processed photo and its metadata back to your gallery.</td>
</tr>
<tr>
<td>Photo location metadata (EXIF GPS)</td><td><code>ACCESS_MEDIA_LOCATION</code></td>
<td>To let a photo you imported keep its location when you export it again. Read on device only.</td>
</tr>
<tr>
<td>Your location</td>
<td><code>ACCESS_COARSE_LOCATION</code>, <code>ACCESS_FINE_LOCATION</code></td>
<td>Used only when you switch on the optional location watermark, to stamp the place name onto the
photo. Location is never collected in the background, never stored beyond the photo you asked to
geotag, and never sent to us.</td>
</tr>
<tr>
<td>Vibration</td><td><code>VIBRATE</code></td><td>Haptic feedback on shutter and controls.</td>
</tr>
<tr>
<td>Network</td><td><code>INTERNET</code></td>
<td>Present for the app's runtime, which is built on React Native. The app itself makes no network
requests and contacts no server we operate.</td>
</tr>
</table>
<p>The app does not request access to your microphone, contacts, calendar, health data, SMS, call logs,
files outside your photo library, or your physical activity, and it does not use the
"display over other apps" permission.</p>
<h2>Storage on your device</h2>
<p>Your recipes, presets, favourites and app settings are stored locally in the app's private storage on
your device. Removing the app removes them. Photos you take or export live in your own gallery, under
your control; the app keeps no copy of its own.</p>
<h2>Sharing</h2>
<p>When you tap Share, the app hands the chosen photo to the app you pick through Android's standard share
sheet. What that app then does with the photo is governed by its own privacy policy. We receive nothing.</p>
<h2>Third-party software</h2>
<p>The app is built with open-source libraries (React Native, Expo, Skia, VisionCamera, and others). They
run locally inside the app and do not collect data on our behalf. Their licences and copyright notices are
listed in the app under <em>Settings → About → Open-source licences</em> and in the
<code>THIRD_PARTY_NOTICES.md</code> file shipped with the source.</p>
<h2>Children</h2>
<p>The app is not directed at children under 13 and does not knowingly collect personal information from
anyone, including children.</p>
<h2>Your rights and choices</h2>
<p>Because no personal data leaves your device, there is nothing for us to delete or export on your behalf.
You can revoke any permission at any time in Android's <em>Settings → Apps → RecipesCam → Permissions</em>;
the app will keep working minus the feature that needs it. Uninstalling the app deletes all data it stored
locally.</p>
<h2>Changes to this policy</h2>
<p>If this policy changes, the effective date above is updated and the new text is published at the same
URL before it takes effect.</p>
<h2>Contact</h2>
<p>Questions about this policy or the app: <a href="mailto:tranloclqd@gmail.com">tranloclqd@gmail.com</a>.</p>
</body>
</html>
-36
View File
@@ -19,11 +19,9 @@
"expo-file-system": "~57.0.1",
"expo-font": "~57.0.1",
"expo-haptics": "~57.0.1",
"expo-image-picker": "~57.0.4",
"expo-keep-awake": "~57.0.1",
"expo-location": "~57.0.4",
"expo-media-library": "~57.0.2",
"expo-sensors": "~57.0.2",
"expo-sharing": "~57.0.18",
"expo-status-bar": "~57.0.1",
"lucide-react-native": "^1.24.0",
@@ -3489,27 +3487,6 @@
"expo": "*"
}
},
"node_modules/expo-image-loader": {
"version": "57.0.1",
"resolved": "https://registry.npmjs.org/expo-image-loader/-/expo-image-loader-57.0.1.tgz",
"integrity": "sha512-uhrZKLT/cTl2mXyR28kPpVkS5O+PK9N1QA/07IFM4f5T4g0lTW1JHT3NEWwEEsGFldPmVX4j7LwUVVZxE+woug==",
"license": "MIT",
"peerDependencies": {
"expo": "*"
}
},
"node_modules/expo-image-picker": {
"version": "57.0.4",
"resolved": "https://registry.npmjs.org/expo-image-picker/-/expo-image-picker-57.0.4.tgz",
"integrity": "sha512-ypuO99+rMqrjYlTt8EqJ415ff+qIj/gPyEbcOW8g6CFtC6Ljk8OWYyiEFKZg3989Rdljla/fN3IWqXOXKN/2og==",
"license": "MIT",
"dependencies": {
"expo-image-loader": "~57.0.1"
},
"peerDependencies": {
"expo": "*"
}
},
"node_modules/expo-json-utils": {
"version": "57.0.1",
"resolved": "https://registry.npmjs.org/expo-json-utils/-/expo-json-utils-57.0.1.tgz",
@@ -3605,19 +3582,6 @@
"react-native": "*"
}
},
"node_modules/expo-sensors": {
"version": "57.0.2",
"resolved": "https://registry.npmjs.org/expo-sensors/-/expo-sensors-57.0.2.tgz",
"integrity": "sha512-5TjX3b207CZWqMhbV8PMbvRCq+oLLEpnHCzB9TOGqGvcWx6LUx50vuGdPIGMPhuvBzlrPm4jvZik1U712j9BuQ==",
"license": "MIT",
"dependencies": {
"invariant": "^2.2.4"
},
"peerDependencies": {
"expo": "*",
"react-native": "*"
}
},
"node_modules/expo-server": {
"version": "57.0.1",
"resolved": "https://registry.npmjs.org/expo-server/-/expo-server-57.0.1.tgz",
+1 -3
View File
@@ -1,6 +1,6 @@
{
"name": "recipescamera",
"version": "1.2",
"version": "1.2.1",
"main": "index.ts",
"dependencies": {
"@react-native-async-storage/async-storage": "2.2.0",
@@ -14,11 +14,9 @@
"expo-file-system": "~57.0.1",
"expo-font": "~57.0.1",
"expo-haptics": "~57.0.1",
"expo-image-picker": "~57.0.4",
"expo-keep-awake": "~57.0.1",
"expo-location": "~57.0.4",
"expo-media-library": "~57.0.2",
"expo-sensors": "~57.0.2",
"expo-sharing": "~57.0.18",
"expo-status-bar": "~57.0.1",
"lucide-react-native": "^1.24.0",
+6
View File
@@ -168,6 +168,7 @@ interface ParamDef {
step: number;
display: (v: number) => string;
onChange: (v: number) => void;
pro?: boolean; // PRO-only: the LITE build cannot open this row
}
const haptic = () => Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
@@ -420,6 +421,10 @@ export default function AdjustmentPanel({
label: `${p.label.toUpperCase()} ${p.display(p.value)}`,
active: openParam === p.key,
amberValue: openParam !== p.key && p.value !== p.default,
// A PRO-only parameter stays visible in LITE as a greyed chip with its
// corner PRO tag — named where it is, which is the only place it can be.
pro: p.pro,
disabled: !!p.pro && !IS_PRO,
onPress: () => toggleParam(p.key),
}));
@@ -465,6 +470,7 @@ export default function AdjustmentPanel({
label: d.label,
value: d.get(a),
default: d.defaultValue,
pro: d.pro,
min: d.min,
max: d.max,
step: d.step,
+2 -12
View File
@@ -8,8 +8,8 @@ import { CREDITS, LICENSE_TEXTS, CreditEntry } from '../utils/credits';
// each project row links to its homepage, each license group expands to the
// full license text embedded in the app.
const APP_NAME = 'recipescamera';
const APP_VERSION = '1.2'; // keep in sync with app.json
const APP_NAME = 'RecipesCam';
const APP_VERSION = '1.2.1'; // keep in sync with app.json
function Group({
license,
@@ -134,16 +134,6 @@ export default function CreditsModal({
</Text>
<View className="flex-row flex-wrap items-center mt-1">
<Text className="text-zinc-400 font-mono text-[10px]">Author: Lộc Phạm</Text>
<TouchableOpacity
onPress={() => Linking.openURL('mailto:tranloclqd@gmail.com')}
accessibilityLabel="Email author"
className="flex-row items-center ml-3"
hitSlop={{ top: 6, bottom: 6, left: 6, right: 6 }}
>
<Text className="text-amber-500/90 font-mono text-[10px] underline">
tranloclqd@gmail.com
</Text>
</TouchableOpacity>
</View>
<Text className="text-zinc-600 font-mono text-[10px] mt-1">
© 2026 Lộc Phạm · Built with React Native + Expo; icons by Lucide;
+2 -2
View File
@@ -72,13 +72,13 @@ export default function PhotoViewerModal({
if (await Sharing.isAvailableAsync()) {
await Sharing.shareAsync(photo.uri, {
mimeType: 'image/jpeg',
dialogTitle: `Share with CamRecipe Pro - Preset: ${photo.recipeName}`,
dialogTitle: `Share with RecipesCam - Preset: ${photo.recipeName}`,
UTI: 'public.jpeg',
});
} else {
await Share.share({
url: photo.uri,
message: `Captured with CamRecipe Pro - Preset: ${photo.recipeName}`,
message: `Captured with RecipesCam - Preset: ${photo.recipeName}`,
});
}
} catch (error) {
+1 -1
View File
@@ -343,7 +343,7 @@ export default function RecipeCreateModal({
value={name}
onChangeText={setName}
selectTextOnFocus
placeholder="Recipe name (e.g. Fuji Retro)"
placeholder="Recipe name (e.g. Retro Film)"
placeholderTextColor="#71717a"
className="bg-carbon border border-zinc-800 rounded-lg p-2 text-white font-mono text-sm mb-2"
/>
+1 -1
View File
@@ -115,7 +115,7 @@ export default function TopBar({ mode, recipeName, onSaveRecipe, onOverwriteReci
<TextInput
value={newRecipeName}
onChangeText={setNewRecipeName}
placeholder="Recipe name (e.g. Fuji Retro)"
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
+1 -1
View File
@@ -2797,7 +2797,7 @@ const Viewfinder = forwardRef<ViewfinderHandle, ViewfinderProps>(function Viewfi
const cinemaOn = cinemaEffect != null && !!cinemaParams;
const useShaderPass = !!imageFitRect && ((toneEffect != null && toneOn) || cinemaOn);
// ---- CLARITY + LIGHT GLOW on the LIBRARY preview -----------------------
// ---- CLARITY + HDF EFFECT on the LIBRARY preview -----------------------
// The camera worklet and exportEngine both have these two passes; the three
// library branches below draw the photo declaratively and had neither, so a
// photo opened from the library ignored both knobs. Same maths as the camera:
-4
View File
@@ -118,10 +118,6 @@ const KEYS = [
'fogR', 'fogG', 'fogB', 'fogAmt',
] as const;
export function cinemaSeasonName(season: CinemaSeason | null | undefined): string | null {
return season ? SEASONS[season]?.name ?? null : null;
}
export function getCinemaUniforms(season: CinemaSeason | null | undefined): CinemaUniformSet | null {
if (!season) return null;
const s = SEASONS[season];
+3 -23
View File
@@ -139,8 +139,8 @@ export function getSkiaColorMatrix(baseFilter: BaseFilter, adj: ColorAdjustments
0, 0, 0, 1, 0,
];
} else if (baseFilter === 'leica') {
// LEITZ STREETLIFE CLASSIC — the Leica character rather than a saturation
// push. What makes a Leica file read as one is the separation BETWEEN the
// LC STREETLIFE CLASSIC — the rangefinder character rather than a saturation
// push. What makes this file read as one is the separation BETWEEN the
// hues, so the deepening lives in the off-diagonal terms, not in a global
// chroma gain:
// - the three rows sum to 1.00/1.00/0.995, so greys stay exactly grey and
@@ -158,7 +158,7 @@ export function getSkiaColorMatrix(baseFilter: BaseFilter, adj: ColorAdjustments
0, 0, 0, 1, 0,
];
} else if (baseFilter === 'leica-vivid') {
// LEITZ STREETLIFE VIVID — the same Leica channel separation pushed into the
// LC STREETLIFE VIVID — the same slanted channel separation pushed into the
// digital era: ~20% of chroma gain (still well under Velvia's 39%, so it
// sparkles instead of shouting) on the same warm R>G>B bias CLASSIC has, so
// VIVID is a brighter read of one look rather than a second, neutral one.
@@ -285,26 +285,6 @@ export function applyExposureGain(matrix: number[], evStops: number): number[] {
return multiplyMatrices(gainMat, matrix);
}
// Compute a tint color + opacity from the matrix for camera overlay preview
// Applies the matrix to white (1,1,1,1) to see what color cast the filter creates
export function computeMatrixTint(matrix: number[]): { r: number; g: number; b: number; a: number } {
const apply = (r: number, g: number, b: number, a: number) => {
const row = (offset: number) =>
matrix[offset + 0] * r + matrix[offset + 1] * g + matrix[offset + 2] * b + matrix[offset + 3] * a + matrix[offset + 4];
return { r: row(0), g: row(5), b: row(10), a: row(15) };
};
// Apply matrix to neutral white to get the filter's color cast
const white = apply(1, 1, 1, 1);
// Clamp and compute perceived intensity to determine opacity
const clamp = (v: number) => Math.max(0, Math.min(1, v));
const intensity = (white.r + white.g + white.b) / 3;
// Opacity is higher when the filter deviates from neutral
const deviation = Math.abs(white.r - intensity) + Math.abs(white.g - intensity) + Math.abs(white.b - intensity);
const opacity = Math.min(0.35, deviation * 2.5);
return { r: clamp(white.r), g: clamp(white.g), b: clamp(white.b), a: opacity };
}
// 4x5 Matrix multiplication utility: A * B
function multiplyMatrices(a: number[], b: number[]): number[] {
const result = new Array(20).fill(0);
+8 -8
View File
@@ -21,7 +21,7 @@ export const CREDITS: CreditEntry[] = [
name: "canvaskit-wasm",
version: "0.41.0",
license: "BSD-3-Clause",
copyright: "Copyright 2020 Google LLC",
copyright: "Copyright (c) 2011 Google Inc. All rights reserved.",
url: "https://skia.org/",
},
{
@@ -87,6 +87,13 @@ export const CREDITS: CreditEntry[] = [
copyright: "Copyright (c) 2015-present 650 Industries, Inc. (aka Expo)",
url: "https://docs.expo.dev/versions/latest/sdk/font/",
},
{
name: "expo-document-picker",
version: "57.0.2",
license: "MIT",
copyright: "Copyright (c) 2015-present 650 Industries, Inc. (aka Expo)",
url: "https://docs.expo.dev/versions/latest/sdk/document-picker/",
},
{
name: "expo-haptics",
version: "57.0.1",
@@ -94,13 +101,6 @@ export const CREDITS: CreditEntry[] = [
copyright: "Copyright (c) 2015-present 650 Industries, Inc. (aka Expo)",
url: "https://docs.expo.dev/versions/latest/sdk/haptics/",
},
{
name: "expo-image-picker",
version: "57.0.4",
license: "MIT",
copyright: "Copyright (c) 2015-present 650 Industries, Inc. (aka Expo)",
url: "https://docs.expo.dev/versions/latest/sdk/imagepicker/",
},
{
name: "expo-keep-awake",
version: "57.0.1",
+4 -4
View File
@@ -44,19 +44,19 @@ export const FILM_SIMS: FilmSim[] = [
{ id: 'sim-astia', name: 'ASTIPES', baseFilter: 'astia', adjustments: { ...DEFAULT_ADJUSTMENTS } },
{ id: 'sim-eterna', name: 'ETERNIPES', baseFilter: 'eterna', adjustments: { ...DEFAULT_ADJUSTMENTS } },
{ id: 'sim-acros', name: 'ACRIPES', baseFilter: 'monochrome', adjustments: { ...DEFAULT_ADJUSTMENTS } },
// Leica digital look (M/Q/SL) is not a film stock, so it ships as its own
// The rangefinder digital look (M/Q/SL) is not a film stock, so it ships as its own
// pair instead of one look that has to compromise:
// CLASSIC — the stock character: deep solid darks, natural shadows, rich but
// quiet colour. Its micro-contrast is CLARITY (a local unsharp mask in both
// renderers), never a global saturation boost, and a whisker of grain
// keeps it feeling like film rather than digital sharpness.
// VIVID — the same Leica separation opened up for the digital era: lifted
// VIVID — the same channel separation opened up for the digital era: lifted
// exposure, lifted shadows, a little colour. It reads bright and flattering
// straight off the shutter, no editing pass needed.
// Both share the 'leica'/'leica-vivid' matrices; the tonal half is the sim's
// own adjustments, which every apply path now carries into the panel.
{ id: 'sim-leica', name: 'LEITZ STREETLIFE CLASSIC', baseFilter: 'leica', adjustments: { ...DEFAULT_ADJUSTMENTS, clarity: 2, grain: 1 } },
{ id: 'sim-leica-vivid', name: 'LEITZ STREETLIFE VIVID', baseFilter: 'leica-vivid', adjustments: { ...DEFAULT_ADJUSTMENTS, exposure: 2, saturation: 2, clarity: 2, shadow: 1, highlight: -1 } },
{ id: 'sim-leica', name: 'LC STREETLIFE CLASSIC', baseFilter: 'leica', adjustments: { ...DEFAULT_ADJUSTMENTS, clarity: 2, grain: 1 } },
{ id: 'sim-leica-vivid', name: 'LC STREETLIFE VIVID', baseFilter: 'leica-vivid', adjustments: { ...DEFAULT_ADJUSTMENTS, exposure: 2, saturation: 2, clarity: 2, shadow: 1, highlight: -1 } },
];
// Film sim → full Recipe (never persisted; sims are applied on the fly).
+9 -3
View File
@@ -16,9 +16,10 @@ export const IS_PRO = IS_PRO_BUILD;
//
// What LITE owns outright: the eight film sims, the bundled recipes, its own
// recipes up to LITE_RECIPE_LIMIT, its own watermark text (in one face), the
// plain frames, one favorite and every parameter. What needs PRO: the three
// printed frames, the GPS stamp, the platform's other fonts, the next recipe
// past the limit, and the second favorite.
// plain frames, one favorite and every parameter except the HDF effect.
// What needs PRO: the three printed frames, the GPS stamp, the HDF effect,
// the platform's other fonts, the next recipe past the limit, and the second
// favorite.
// Frames on the LITE side: the plain export only. The three printed looks are
// PRO (the artwork/card geometry is the paid half of the FRAME tab).
@@ -47,6 +48,7 @@ export const LITE_MARK = {
export interface Look {
frame: FrameId;
gpsWm: boolean;
hdf: number;
}
// The first PRO feature a look is using, or null when it is LITE-clean. One
@@ -54,5 +56,9 @@ export interface Look {
export function proLookInUse(look: Look): string | null {
if (PRO_FRAMES.includes(look.frame)) return 'A PRO frame';
if (look.gpsWm) return 'The GPS watermark';
// A LITE build can no longer reach the HDF row (paramDefs marks it PRO and
// the panel greys the chip out), but a session or recipe stored before the
// gate existed still carries a value: it is caught here, at the render.
if (look.hdf > 0) return 'The HDF effect';
return null;
}
-35
View File
@@ -8,41 +8,6 @@
// If the JPEG carries neither JFIF nor EXIF, a minimal JFIF APP0 segment is
// inserted right after SOI.
const B64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
export function base64ToBytes(b64: string): Uint8Array {
const clean = b64.replace(/\s+/g, '');
const out: number[] = [];
let buffer = 0;
let bits = 0;
for (const ch of clean) {
if (ch === '=') break;
const v = B64.indexOf(ch);
if (v < 0) continue;
buffer = (buffer << 6) | v;
bits += 6;
if (bits >= 8) {
bits -= 8;
out.push((buffer >> bits) & 0xff);
}
}
return Uint8Array.from(out);
}
export function bytesToBase64(bytes: Uint8Array): string {
let out = '';
for (let i = 0; i < bytes.length; i += 3) {
const b0 = bytes[i];
const b1 = i + 1 < bytes.length ? bytes[i + 1] : 0;
const b2 = i + 2 < bytes.length ? bytes[i + 2] : 0;
out += B64[b0 >> 2];
out += B64[((b0 & 3) << 4) | (b1 >> 4)];
out += i + 1 < bytes.length ? B64[((b1 & 15) << 2) | (b2 >> 6)] : '=';
out += i + 2 < bytes.length ? B64[b2 & 63] : '=';
}
return out;
}
function u16(bytes: Uint8Array, p: number): number {
return (bytes[p] << 8) | bytes[p + 1];
}
-9
View File
@@ -97,12 +97,3 @@ export async function reverseGeocode(latitude: number, longitude: number): Promi
return null;
}
export function formatCoordinate(val: number, type: 'lat' | 'lon'): string {
const absolute = Math.abs(val);
const degrees = Math.floor(absolute);
const minutes = Math.floor((absolute - degrees) * 60);
const seconds = ((absolute - degrees - minutes / 60) * 3600).toFixed(2);
const direction = type === 'lat' ? (val >= 0 ? 'N' : 'S') : (val >= 0 ? 'E' : 'W');
return `${degrees}° ${minutes}' ${seconds}" ${direction}`;
}
+5 -1
View File
@@ -14,6 +14,9 @@ export interface ContinuousParamDef {
display: (v: number) => string;
get: (a: ColorAdjustments) => number;
set: (v: number) => Partial<ColorAdjustments>;
// PRO-only knob: the LITE build greys the chip out (PRO corner tag) and
// cannot open its row, so the value stays at its default there.
pro?: boolean;
}
const sign = (v: number) => (v > 0 ? `+${v}` : String(v));
@@ -174,7 +177,8 @@ export const PARAM_DEFS: {
},
{
key: 'hdf',
label: 'LIGHT GLOW',
label: 'HDF EFFECT',
pro: true,
min: 0,
max: 10,
step: 1,
+1 -1
View File
@@ -106,7 +106,7 @@ vec4 main(vec2 xy) {
}
`;
// Bright Pass Filter for the LIGHT GLOW pass (HDF), SkSL over a child image
// Bright Pass Filter for the HDF EFFECT pass (HDF), SkSL over a child image
// shader — the pattern TONE_SKSL above already proved on device.
//
// Per channel the old 2.5*in-1.5 curve only zeroed a channel that was dark