Commit Graph

122 Commits

Author SHA1 Message Date
admin 1b60ca2f22 Drop the in-app unlock key: only the PRO apk opens PRO
The set is two apks and the unlock key was a second, weaker lock on the
same door. A verifier that runs offline has to carry its secret inside
the lite apk, so anyone who unpacked it could mint a key. Nothing in the
lite build can flip the gate now.

- entitlement.ts keeps the gate (which look is PRO, what an export
  carries in lite) and exports IS_PRO straight from the build flag.
- SETTINGS loses the key field; the plan chip still says LITE / PRO
  ACTIVE and the helper text says what lite cannot export.
- The upsell alerts just name the build that has the feature.
- tools/keygen.mjs goes with the verifier it fed.
2026-09-14 20:09:12 +07:00
admin 97949e449d Ship LITE and PRO, and let CLARITY and LIGHT GLOW reach the library
Both flavors build from one JS tree: `lite` hands out the free apk
(com.locphamtran.recipescamera), `pro` sells as-is (same id + .pro, label
"RecipesCam Pro"). Release is signed from android/keystore.properties, both
files gitignored - lose either and no later build can install over this one.
The tier reaches JS through src/provariant.ts, which tools/set-variant.mjs
rewrites before each build (`npm run apk:lite` / `apk:pro`); the gradle flavor
alone would ship a Pro apk that thinks it is Lite.

The dev scaffolding goes: the three src/dev probes and their App effects, the
EXPO_PUBLIC_CAPTURE_MODE [CAPTURE] timing log, and the -PdevtestSuffix
side-by-side install hook. 'balanced' stays the shipped capture mode, which is
what those probes measured on the 12S Ultra.

Looking at the picture: WB now converts kelvin through the Planckian locus into
a luma-normalised gain (unity at 5500K, symmetric tint) instead of the old
one-sided push; LEITZ STREETLIFE splits into CLASSIC and VIVID; selecting a
recipe loads every knob it carries over the defaults and stays editable, and
RESET hands the panel back to the sim's own values. The in-app library picker,
the ultra-wide native module and recipe share/import land here too.

CLARITY and LIGHT GLOW existed only on the camera worklet and the export
engine, so a photo opened from the library ignored both. They now run in all
three library preview branches (libPhoto), and the preview clips them to the
drawn image - the export engine always clipped its bloom to the canvas, the
preview drew it over the whole rect, so the bloom bled past the photo onto the
black letterbox and over the frame's mat.
2026-09-14 19:48:44 +07:00
admin 99405514c6 Library picker: window the grid on the target date instead of walking to it
Dragging the date bar fired a window query every 120ms while one query takes
over a second on a 12S Ultra, so ~20 queries queued behind each other, the
grid froze for the length of the drag and the stale one landed last - the
grid settled on a date the finger had long left.

The window now opens on both sides: a page above the target, so a list parked
mid-library still has content above it and a downward drag does not die on
offset 0, plus the target page and everything older. jumpTo coalesces the
drag into one query and parks the grid on the row the target date sits on;
scrolling back up to the top pages newer photos in and corrects the offset by
hand, because maintainVisibleContentPosition keeps the offset glued to index 0
and walked the grid forward in time as it paged.
2026-09-13 22:14:36 +07:00
admin 1f8b72ec8c Let keygen mint the key for a payload you pick with --from 2026-09-12 13:43:08 +07:00
admin 3e9dcc40fb Rebuild the metering overlay from the peeked values so compare restores the look 2026-09-12 13:43:08 +07:00
admin 59e2719245 Keep LITE exports clean of PRO looks, and gate CREATE, FAVORITED and + SAVE behind PRO
LITE previews every look and feature but export/render is blocked while a PRO
look is in use, and every LITE export carries the RECIPESCAM mark. Keys are
generated offline (FNV-1a checksum over a fixed secret, 16 chars from a
no-ambiguity alphabet) and activate one machine or a thousand: nothing is bound
to a device, so revoking a leaked key needs Play Billing plus server validation.

CREATE, FAVORITED, the star chip and the TopBar + SAVE now answer with a PRO
prompt instead of a silent no-op; CREATE still drafts and applies the look so
LITE can try it, it just cannot persist.

The SETTINGS card also lifts above the keyboard now: RN Modal is its own dialog
window, so the IME never resizes or pans it and the UNLOCK row was sitting under
the keys.
2026-09-12 11:21:14 +07:00
admin a662674478 Show the pre-edit values while the finger holds, and pass the FRAME tab flag
Pressing and holding on the picture with the LIGHT/WB/FX panel open now
rewinds the last edit, and the panel rewinds with it: the chip numbers and
the open slider read the same pre-edit snapshot, so before and after can be
compared by eye and not just by looking at the picture. Only the displayed
values move -- the stored adjustments, RECIPE and the export keep the real
ones, and a gap longer than PEEK_GAP_MS starts a new snapshot for the next
gesture.

Also pass frameTabActive, without which the FRAME preview zoom added in
Viewfinder.tsx could never turn on.
2026-09-12 08:51:43 +07:00
admin 98b8021f3b Show the picture with the last edit undone while the finger holds it
Pressing and holding on the photo with a LIGHT/WB/FX panel open asks App
for a preview drawn without the edit that gesture just made; lifting the
finger asks for the edited look back. Pegged to one gesture, not one
pointer-move: App only re-snapshots the 'before' state after a 700ms
pause, so the whole drag compares against its own starting point.

Preview-only - the sliders, RECIPE and the export keep reading the real
adjustments. Moving past TAP_SLOP, a second finger, or a release all end
the peek, and a peek is not a tap, so tap-to-focus and double-tap zoom
are untouched.
2026-09-12 08:13:21 +07:00
admin 93b5b664d4 Save into DCIM/Camera below API 29 too, not just the expo DCIM root 2026-09-12 07:51:12 +07:00
admin 1d84fe8a4a Zoom the FRAME preview instead of the photo, and save to DCIM/Camera
- FRAME tab now pinches the whole view (frame + photo + watermark) in both
  camera and library; one finger pans that view. Preview only: the exported
  file is identical at 1x and while the view is magnified.
- Watermark pinch/drag stay relative and keep priority while the WATERMARK
  row is open, so they still work after returning from the FRAME tab.
- Exports go through a native MediaStore insert into DCIM/Camera, falling
  back to expo-media-library when the native save fails.
2026-09-11 23:09:26 +07:00
admin 40c91d2a75 feat(gps): a Settings GPS switch that reads the location; the watermark chip only prints it
The watermark chip used to be the only GPS control, so turning the watermark
off also stopped the photo from carrying coordinates. Split the two: a GPS
master switch in Settings decides whether the location is read at all, and the
chip decides whether it is drawn on the photo.

0x889d is also written as UTF-8 now — a place name with accents ("TAM KỲ, ĐÀ
NẴNG") was going out latin-1 and reading back as mojibake.
2026-09-11 20:08:59 +07:00
admin ea9ff01a22 feat(exif): name the capture device and place (0x9a00/0x0110/0x889d) for the Gallery frame
The framing tool matches Make/Model against its known-device list and prints
the place name; a CameraX capture only carried the raw model codename
(2203121C) and no 0x9a00/0x889d, which is what a stock camera photo always
has. State the market name plus the geotag locality, and leave a source that
already carries 0x9a00 untouched so a library photo of another device is
never relabelled.
2026-09-11 19:47:51 +07:00
admin 17ee01b62a feat(exif): write the Xiaomi watermark blob (0x889e) so HyperOS Gallery can frame our captures
HyperOS Gallery reads EXIF 0x889e (a JSON blob) to build its watermark
frame; without it a photo fails with "cannot recognize the parameters".
CameraX/HAL never supplies that vendor tag, so a capture from this app
lacked it while a stock-camera photo carried it.

Read back the same props the stock camera derives the blob from
(ro.product.device / ro.product.marketname / Build.MANUFACTURER) via the
native module, synthesize the blob in exifWrite, and write it only when
the source has none and the device is Xiaomi/Redmi/POCO - a source blob
is never overwritten.
2026-09-11 18:47:44 +07:00
admin 33123d47fd fix(exif): write a complete, standard EXIF block so framing tools can read it
Match the layout the sample camera file uses: a fourth Interoperability IFD
(0xa005 = R98/0100) and the baseline tags the emulator HAL omits (ExifVersion
0230, FlashPixVersion 0100, ComponentsConfiguration, ColorSpace), plus the
OffsetTime/OffsetTimeOriginal/OffsetTimeDigitized tags for shots this app
timestamps. Source-owned tags are still kept as-is.

Also fix swapToLittleEndian: Buffer.slice() aliases, so the big-endian swap was
mutating the caller's bytes (a Xiaomi source read back as ISO 36865 instead of
400). Use new Uint8Array(data).
2026-09-11 18:28:02 +07:00
admin 62b9735867 feat(exif): stamp RC_<ts> filenames + EXIF on capture and export 2026-09-11 17:59:44 +07:00
admin 6faef4aef7 feat(crop): two-step crop with APPLY + hidden amber border, persisted in session
- FRAME/CROP: choosing a ratio shows the amber band (border + 0.55 dim);
  APPLY collapses the preview to the crop rect with an opaque mask and
  removes the amber stroke; RESET returns to 'none'.
- Viewfinder: libCropView (k = min(vw/dw, vh/dh)) + cropScreenPx drive the
  mask/band, libViewMatrix folds the crop transform into the image groups,
  libCropTouchStyle keeps touch mapping aligned.
- Persist cropApplied in the session snapshot and restore it only when it
  still matches cropRatio.
- Add PLAN-2026-09-09.md with the measurements.

tsc --noEmit unchanged at 14 pre-existing errors.
2026-09-11 17:29:51 +07:00
admin 1b632ba78c feat(frame): read AUTO straighten from the photo, not the accelerometer
EXIF Orientation only knows 0/90/180/270, so a library still carries no
record of how the camera was held. The sensor had nothing to offer.

AUTO now measures the dominant line in the picture itself:
src/utils/horizon.ts runs a shear-projection search (coarse 1 deg over
-45..45, then a 0.5 deg refine) on a <=256px thumbnail, bails when no
line's score beats 3x the median, and returns the tilt in degrees.
App applies photoStraighten = -tilt, so preview and export share one
number exactly as the slider did.

Removes expo-sensors wiring, the horizonRoll state, effectiveStraighten
and the bubble-level overlay (autoRoll prop) from App/AdjustmentPanel/
Viewfinder. expo-sensors stays in package.json.
2026-09-11 14:46:28 +07:00
admin d6b3ebda40 fix(frame): take the bubble level off the image when leaving the strip
AUTO turns the amber level line on over the photo, but the line only left
when AUTO itself was switched off: switching to another frame chip, another
parameter or another rail tab kept it painted on the picture.

Gate the line on the ROTATE context instead: AdjustmentPanel reports whether
the ROTATE strip (or its STRAIGHTEN row) is open, App keeps autoRoll non-null
only then, and the frame chips now close the strip like every other chip
does. Verified on emulator-5554: AUTO on with the strip up shows the line
(row 1199-1203, 594 px); tapping LIGHT and coming back leaves it off.
2026-09-11 12:21:09 +07:00
admin da48ceccb5 fix(frame): make the ROTATE strip behave on switching parameters
Four follow-ups on the strip added for the quarter turns / straighten / AUTO.

AUTO is "snap to the sensor horizon": it now drops the manual STRAIGHTEN offset when it is switched on, so enabling it after a hand-set angle really levels the photo instead of leaving the angle untouched (the chip goes from "ROTATE 0 · +22°" to "ROTATE · AUTO").

The "<" back button on a slider row returns to the strip the row was opened from (STRAIGHTEN to ROTATE, COLOR TEMP to TEMP) instead of closing everything, and picking another parameter — a frame chip or the global RESET — closes the open row, so the straighten slider no longer outlives the parameter it belongs to.

Opening another photo from the library resets the turn, the fine straighten angle and AUTO with it: a new photo starts level.
2026-09-11 12:07:18 +07:00
admin 0ce561cb70 feat(frame): level the photo to the horizon with AUTO
The ROTATE strip gains an AUTO chip that reads the device tilt from expo-sensors and feeds it into the straighten angle while the library is open, so the export is levelled to the horizon. It sits right after RESET, combines with the manual STRAIGHTEN offset and with the quarter turns, and both RESET paths clear it.

The accelerator is sampled every 100 ms only while AUTO is on and the library is visible; near-flat and past 45 degrees readings are ignored so the level does not chase noise. AUTO rotates the image by +roll (a +10 degree emulator tilt exports 10 degrees clockwise), which is the sign that matches the preview.

A level line overlay is drawn in the preview: amber within one degree of level, white otherwise. Adds the expo-sensors dependency, so a native rebuild is required.
2026-09-11 11:35:48 +07:00
admin 9ddaeac260 feat(frame): give ROTATE quarter turns plus a fine straighten 2026-09-11 11:17:39 +07:00
admin 6788d51fb4 feat(frame): rename the RETRO POLAROID chip to RETRO INSTANT 2026-09-11 10:39:30 +07:00
admin dcb74a7243 feat(frame): pinch and drag the framed window on the camera
Two fingers scale the live feed inside a polaroid card / wall opening and
one finger drags it, both clamped to the window, and the shutter passes the
result to the export as frameWindowZoom so the printed crop matches what the
viewfinder showed. The crop centre is mapped through the same out-space ->
raw transform as the window rect; feeding it straight to the raw plane
swapped the axes on a rotated sensor.
2026-09-11 10:36:50 +07:00
admin 4131c9f401 feat(frame): move the watermark controls into a FRAME sub-panel 2026-09-11 10:00:10 +07:00
admin ba971f136c feat(panel): add a FAVORITED tab with a star on every recipe 2026-09-11 09:32:21 +07:00
admin ba7e579986 feat(panel): RESET shows a star while the look differs from a clean start 2026-09-11 09:08:37 +07:00
admin 7ed4301055 feat(panel): gather the white balance presets and COLOR TEMP into a TEMP chip 2026-09-11 08:59:49 +07:00
admin d66ae3dc59 feat(panel): collapse the film simulations into a PHOTO STYLE chip 2026-09-11 08:54:38 +07:00
admin 6adf0b5933 feat(sims): rename film simulations and bundled recipes
Sims: PROVIA->PROVIPES, VELVIA->VELVIPES, CLASSIC CHROME->CLASSIC CHRIPES, CLASSIC NEG->CLASSIC NEGIPES, ASTIA->ASTIPES, ETERNA->ETERNIPES, ACROS->ACRIPES, LEICA->LEITZ STREETLIFE.

Bundled recipes follow: PROVIPES STD, VELVIPES VIVID, ACRIPES MONO, CLASSIC NEGIPES. Ids and baseFilters untouched, so saved sessions keep resolving.
2026-09-11 08:44:58 +07:00
admin 7454ea1218 feat(panel): one RESET on every tab, back to the app's defaults
RESET was look-relative: it only put the sliders back on the values the
active recipe ships with, so the look itself (a saved recipe, a film sim)
survived the tap. It now restores the state a clean start leaves — the
PROVIA startup look, every parameter neutral, frame off, the custom and
GPS watermarks back to their defaults — and it sits on every tab, pinned
outside the scrolling chip row so it can never scroll out of reach.

Mode and aspect ratio stay put: they frame the shot being worked on, not
the look.
2026-09-10 22:18:43 +07:00
admin 02bc3aa4da feat(panel): editor keeps the values when the sim changes; one-tap RESET back to the look 2026-09-10 22:06:36 +07:00
admin 868ebfe6e2 feat(recipe): edit opens the prefilled create form; fields select their value on tap 2026-09-10 21:34:32 +07:00
admin b154e50191 feat(recipe): edit button on the top bar, with overwrite-or-save-as on save 2026-09-10 21:16:50 +07:00
admin 5186b2aab1 feat(sim): add a CLASSIC NEG chip to the film-sim presets 2026-09-10 21:16:50 +07:00
admin 91109cba54 feat(recipe): Save and Save As for recipes
Custom recipes can be overwritten in place (TopBar SAVE now offers CANCEL /
SAVE AS / SAVE and prefills the current name); bundled recipes stay read-only
and only offer Save As. Adds updateCustomRecipe to storageUtils.
2026-09-10 20:57:19 +07:00
admin 4bb2bda5c8 fix(sim): rebuild the six film-sim color matrices to match the stock looks
Provia/Velvia/Astia/Classic Chrome/Classic Neg/Acros each get their own
primaries+cross-talk matrix measured against the pipeline, plus a split-tone
stage (shadow/highlight tint uniforms) for Classic Chrome's teal and Classic
Neg's green-cyan shadows vs warm highlights.
2026-09-10 20:57:19 +07:00
admin 93f48537f8 feat(frame): CROP ratios in the library editor, and the framed-photo fixes
CROP chip on the FRAME tab for the library's plain frames: NONE, FREE
(drag the box) and the fixed ratios 1:1, 2:3, 3:2, 3:4, 4:3, 16:9. The
preview draws the keep-rectangle over the photo and the export honours it
pixel-for-pixel, pinching and dragging inside the band to pick the framing.

- Viewfinder: build frameRect from the normalised rect so the Skia canvas
  never sees undefined width/height (nothing to commit until now), keep
  the watermark drag target across the WATERMARK tab so a zoomed photo
  no longer resets, and derive the crop band + export rect from the photo
  fit rect.
- exportEngine: crop by fraction rect (or the ratio fallback) before the
  frame is composited.
- Leaving a plain frame clears the crop, and the ratio strip closes with
  the CROP chip it belongs to.
2026-09-10 20:12:03 +07:00
admin 299d81bade feat(wm): GPS watermark color + place-name/time toggles, "commune, city" locality 2026-09-10 18:05:57 +07:00
admin f13fd8e0f3 fix(library): drop the leftover zoom in the mark editor, re-anchor the framed crop
Two zoom-interaction bugs in the library viewer:

- Opening the WATERMARK editor now resets a photo zoom left over from the
  panel-closed pass. The zoom had shifted the mark's hit box along with the
  matrix and tripped the `libZoom.s <= 1.01` guards, so after visiting another
  tab and zooming, the mark could neither be dragged nor pinched.
- A polaroid/wall photo window that MOVES (deck grows: a chip slider or the
  watermark text field) now re-anchors the photo transform by the same u/v the
  export uses, so the crop inside the frame no longer slides away from the card
  (at 2.5x the content used to drift 2.5x the card's move).

Verified on device (9a6a7277): the mark drags again after tab-switch + zoom;
opening WATERMARK returns the photo to the pre-zoom framing; the framed crop
tracks the card within ~2 px over a 62 px deck-driven move.
2026-09-10 17:40:50 +07:00
admin f4a5b0c683 fix(wm): let the library editor pinch a watermark mark
onLibTouchStart only recorded the pinch baseline when both fingers arrived in
the same touch-start, so a second finger landing later (the normal case) left
wmPinchRef null and the move handler scaled nothing -- pinch-the-mark worked
on the camera viewfinder but not on a library photo. Seed the baseline in the
move handler, as the camera path already does.
2026-09-10 17:15:36 +07:00
admin 614ebb170b fix(gps): stop stamping the phone's current location on a picked photo
The library flow ended in a device-GPS fallback: whenever a photo's own
coordinates could not be read, getCurrentGPS() supplied the phone's present
spot, so the mark named wherever the user was standing instead of where the
photo was taken. Drop that tier entirely -- a photo with no readable location
gets no GPS mark. Also keep the legacy Android picker unconditionally so the
picked file keeps its EXIF GPS whatever the chip state, and prefer the city
over subAdminArea (county/district) as the displayed place name.
2026-09-10 17:08:09 +07:00
admin 423bf533f8 fix(gps): resolve place name when the watermark is switched on
A photo picked while the GPS chip was off kept its coordinates but no
locality (the geocode only ran when the chip was already on), and
handleToggleGeotag skipped GPS loading entirely when coordinates already
existed -- so switching the chip on showed STREET VIEW. Request location
permission inside reverseGeocode (expo-location's Android geocoder throws
LocationUnauthorizedException without it) and re-resolve the name from an
effect whenever the mark has coordinates but no place name. Both paths now
share localityName() so camera and photo stamps cannot disagree.
2026-09-10 17:01:48 +07:00
admin 67a2a52a6f Bump the app to version 1.2
versionName 1.2 / versionCode 2, kept in sync with app.json, package.json and the credits screen.
2026-09-10 16:43:03 +07:00
admin 11ed786758 Let the GPS watermark be placed, sized and hung landscape
Three WATERMARK/FRAME behaviours that share the stamp geometry:

* GPS mark: drag it anywhere on the frame/photo area and pinch it bigger or
  smaller. The 0..1 anchor plus the size multiplier ride along in the export
  options, so a framed file (whose canvas IS the card/frame) burns the mark in
  at exactly the fraction the preview showed.
* The photo's own pinch zoom stays available while the WATERMARK editor is
  closed, so a two-finger zoom is never swallowed by the mark while its panel
  is open.
* WALL FRAME gains a WALL PORTRAIT / WALL LANDSCAPE chip: the artwork hangs
  in its own 4000x3117 orientation instead of the default 90-degree rotation.
2026-09-10 16:35:35 +07:00
admin 47e424789d Revert "Zoom the finished photo with a double tap only"
This reverts commit 29b75ec3d3.
2026-09-10 16:06:23 +07:00
admin 29b75ec3d3 Zoom the finished photo with a double tap only 2026-09-10 15:59:15 +07:00
admin 6595e5cbc1 Let the GPS and custom watermarks be rotated 2026-09-10 15:56:06 +07:00
admin 2911dd57f0 Let the wall-frame photo be dragged inside its opening 2026-09-10 15:36:51 +07:00
admin 2835f46c37 Keep the dialled-in look when swapping film simulations 2026-09-10 15:16:21 +07:00
admin 045f5f2048 Keep a thumbnail of the finished export in library mode 2026-09-10 12:31:54 +07:00