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:
@@ -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,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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'}\""
|
||||
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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>
|
||||
Generated
-36
@@ -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
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
@@ -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}`;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user