From 27a9dfb65f9c0d8dab718e7daa9b7d205edbff40 Mon Sep 17 00:00:00 2001 From: locphamtran Date: Tue, 15 Sep 2026 11:36:52 +0700 Subject: [PATCH] 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. --- App.tsx | 2 +- LICENSE | 2 +- THIRD_PARTY_NOTICES.md | 10 +-- android/app/build.gradle | 4 +- android/app/src/main/AndroidManifest.xml | 5 +- android/gradle.properties | 10 ++- app.json | 18 ++-- docs/privacy-policy.html | 110 +++++++++++++++++++++++ package-lock.json | 36 -------- package.json | 4 +- src/components/AdjustmentPanel.tsx | 6 ++ src/components/CreditsModal.tsx | 14 +-- src/components/PhotoViewerModal.tsx | 4 +- src/components/RecipeCreateModal.tsx | 2 +- src/components/TopBar.tsx | 2 +- src/components/Viewfinder.tsx | 2 +- src/utils/cinemaShader.ts | 4 - src/utils/colorUtils.ts | 26 +----- src/utils/credits.ts | 16 ++-- src/utils/defaultRecipes.ts | 8 +- src/utils/entitlement.ts | 12 ++- src/utils/jpegDpi.ts | 35 -------- src/utils/locationUtils.ts | 9 -- src/utils/paramDefs.ts | 6 +- src/utils/toneShader.ts | 2 +- 25 files changed, 177 insertions(+), 172 deletions(-) create mode 100644 docs/privacy-policy.html diff --git a/App.tsx b/App.tsx index d25996f..43259e7 100644 --- a/App.tsx +++ b/App.tsx @@ -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; diff --git a/LICENSE b/LICENSE index 30b20e3..0edd639 100644 --- a/LICENSE +++ b/LICENSE @@ -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 diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index 386364e..5d2c80b 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -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 (https://github.com/mrousavy)), react-native-nitro-modules (Marc Rousavy (https://github.com/mrousavy)), react-native-reanimated (Copyright (c) 2016 Software Mansion ), react-native-safe-area-context (Copyright (c) 2019 Th3rd Wave), react-native-vision-camera (Marc Rousavy (https://github.com/mrousavy)), react-native-vision-camera-skia (Marc Rousavy (https://github.com/mrousavy)), react-native-vision-camera-worklets (Marc Rousavy (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 (https://github.com/mrousavy)), react-native-nitro-modules (Marc Rousavy (https://github.com/mrousavy)), react-native-reanimated (Copyright (c) 2016 Software Mansion ), react-native-safe-area-context (Copyright (c) 2019 Th3rd Wave), react-native-vision-camera (Marc Rousavy (https://github.com/mrousavy)), react-native-vision-camera-skia (Marc Rousavy (https://github.com/mrousavy)), react-native-vision-camera-worklets (Marc Rousavy (https://github.com/mrousavy)), react-native-web (Copyright (c) Nicolas Gallagher.) ```text MIT License diff --git a/android/app/build.gradle b/android/app/build.gradle index 9313b79..2ed88a3 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -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'}\"" diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 4d4274b..7103303 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -5,12 +5,9 @@ - - - @@ -20,7 +17,7 @@ - + diff --git a/android/gradle.properties b/android/gradle.properties index b1849e5..638f5bd 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -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 -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=[] \ No newline at end of file +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 diff --git a/app.json b/app.json index 09e2fd7..193f81e 100644 --- a/app.json +++ b/app.json @@ -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": { diff --git a/docs/privacy-policy.html b/docs/privacy-policy.html new file mode 100644 index 0000000..7953aa7 --- /dev/null +++ b/docs/privacy-policy.html @@ -0,0 +1,110 @@ + + + + + +RecipesCam — Privacy Policy + + + +

RecipesCam — Privacy Policy

+

Applies to the Android apps RecipesCam (com.locphamtran.recipescamera, free/LITE) +and RecipesCam Pro (com.locphamtran.recipescamera.pro, paid/PRO).
+Effective date: 14 September 2026 · Last updated: 14 September 2026

+ +

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.

+ +

Summary

+
    +
  • The app has no user accounts and no server of ours.
  • +
  • No photo, location, or usage data is transmitted to us or to any third party. There is no + analytics, advertising, crash-reporting, or tracking SDK in the app.
  • +
  • Everything the app does — capture, processing, watermarking, saving — happens on your device.
  • +
+ +

Data the app accesses, and why

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
AccessPermissionPurpose
CameraCAMERATo take photos and show the live preview. Photos stay on your device.
Photos and videos in your libraryREAD_MEDIA_IMAGES, READ_MEDIA_VISUAL_USER_SELECTED, + READ_EXTERNAL_STORAGE (Android 12 and older)To let you pick an existing photo to edit, and to show your library inside the app's picker.
Save processed photosWRITE_EXTERNAL_STORAGE (Android 9 and older) and the system media-library APIsTo write the processed photo and its metadata back to your gallery.
Photo location metadata (EXIF GPS)ACCESS_MEDIA_LOCATIONTo let a photo you imported keep its location when you export it again. Read on device only.
Your locationACCESS_COARSE_LOCATION, ACCESS_FINE_LOCATIONUsed 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.
VibrationVIBRATEHaptic feedback on shutter and controls.
NetworkINTERNETPresent for the app's runtime, which is built on React Native. The app itself makes no network + requests and contacts no server we operate.
+ +

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.

+ +

Storage on your device

+

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.

+ +

Sharing

+

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.

+ +

Third-party software

+

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 Settings → About → Open-source licences and in the +THIRD_PARTY_NOTICES.md file shipped with the source.

+ +

Children

+

The app is not directed at children under 13 and does not knowingly collect personal information from +anyone, including children.

+ +

Your rights and choices

+

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 Settings → Apps → RecipesCam → Permissions; +the app will keep working minus the feature that needs it. Uninstalling the app deletes all data it stored +locally.

+ +

Changes to this policy

+

If this policy changes, the effective date above is updated and the new text is published at the same +URL before it takes effect.

+ +

Contact

+

Questions about this policy or the app: tranloclqd@gmail.com.

+ + diff --git a/package-lock.json b/package-lock.json index df15f56..52123bf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 319d18b..1c1c7fa 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/components/AdjustmentPanel.tsx b/src/components/AdjustmentPanel.tsx index 0977a77..3e169a5 100644 --- a/src/components/AdjustmentPanel.tsx +++ b/src/components/AdjustmentPanel.tsx @@ -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, diff --git a/src/components/CreditsModal.tsx b/src/components/CreditsModal.tsx index 25bdda1..d43840f 100644 --- a/src/components/CreditsModal.tsx +++ b/src/components/CreditsModal.tsx @@ -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({ Author: Lộc Phạm - 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 }} - > - - tranloclqd@gmail.com - - © 2026 Lộc Phạm · Built with React Native + Expo; icons by Lucide; diff --git a/src/components/PhotoViewerModal.tsx b/src/components/PhotoViewerModal.tsx index 45aaad5..9d594fc 100644 --- a/src/components/PhotoViewerModal.tsx +++ b/src/components/PhotoViewerModal.tsx @@ -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) { diff --git a/src/components/RecipeCreateModal.tsx b/src/components/RecipeCreateModal.tsx index 6470f9f..51d6d01 100644 --- a/src/components/RecipeCreateModal.tsx +++ b/src/components/RecipeCreateModal.tsx @@ -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" /> diff --git a/src/components/TopBar.tsx b/src/components/TopBar.tsx index dc494f7..d378ac6 100644 --- a/src/components/TopBar.tsx +++ b/src/components/TopBar.tsx @@ -115,7 +115,7 @@ export default function TopBar({ mode, recipeName, onSaveRecipe, onOverwriteReci (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: diff --git a/src/utils/cinemaShader.ts b/src/utils/cinemaShader.ts index d20b960..8efc322 100644 --- a/src/utils/cinemaShader.ts +++ b/src/utils/cinemaShader.ts @@ -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]; diff --git a/src/utils/colorUtils.ts b/src/utils/colorUtils.ts index 3195fab..5288efd 100644 --- a/src/utils/colorUtils.ts +++ b/src/utils/colorUtils.ts @@ -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); diff --git a/src/utils/credits.ts b/src/utils/credits.ts index 5cc27ee..f9784aa 100644 --- a/src/utils/credits.ts +++ b/src/utils/credits.ts @@ -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", diff --git a/src/utils/defaultRecipes.ts b/src/utils/defaultRecipes.ts index 619d76a..f94cbe7 100644 --- a/src/utils/defaultRecipes.ts +++ b/src/utils/defaultRecipes.ts @@ -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). diff --git a/src/utils/entitlement.ts b/src/utils/entitlement.ts index ee40e0d..d596509 100644 --- a/src/utils/entitlement.ts +++ b/src/utils/entitlement.ts @@ -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; } diff --git a/src/utils/jpegDpi.ts b/src/utils/jpegDpi.ts index 060fe26..976b211 100644 --- a/src/utils/jpegDpi.ts +++ b/src/utils/jpegDpi.ts @@ -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]; } diff --git a/src/utils/locationUtils.ts b/src/utils/locationUtils.ts index 9f2c60a..24ef80c 100644 --- a/src/utils/locationUtils.ts +++ b/src/utils/locationUtils.ts @@ -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}`; -} diff --git a/src/utils/paramDefs.ts b/src/utils/paramDefs.ts index 3ba67db..0d16150 100644 --- a/src/utils/paramDefs.ts +++ b/src/utils/paramDefs.ts @@ -14,6 +14,9 @@ export interface ContinuousParamDef { display: (v: number) => string; get: (a: ColorAdjustments) => number; set: (v: number) => Partial; + // 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, diff --git a/src/utils/toneShader.ts b/src/utils/toneShader.ts index 4f1eb9d..b94eea2 100644 --- a/src/utils/toneShader.ts +++ b/src/utils/toneShader.ts @@ -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