Bắt đầu sửa lỗi
@@ -0,0 +1,14 @@
|
|||||||
|
> Why do I have a folder named ".expo" in my project?
|
||||||
|
|
||||||
|
The ".expo" folder is created when an Expo project is started using "expo start" command.
|
||||||
|
|
||||||
|
> What do the files contain?
|
||||||
|
|
||||||
|
- "devices.json": contains information about devices that have recently opened this project. This is used to populate the "Development sessions" list in your development builds.
|
||||||
|
- "settings.json": contains the server configuration that is used to serve the application manifest.
|
||||||
|
- "dev/logs/": contains structured JSONL event logs from CLI commands (e.g. start.log, export.log). These are truncated on each run.
|
||||||
|
|
||||||
|
> Should I commit the ".expo" folder?
|
||||||
|
|
||||||
|
No, you should not share the ".expo" folder. It does not contain any information that is relevant for other developers working on the project, it is specific to your machine.
|
||||||
|
Upon project creation, the ".expo" folder is already added to your ".gitignore" file.
|
||||||
@@ -0,0 +1,177 @@
|
|||||||
|
{"_e":"root:init","_t":1784254504055,"format":"v0-jsonl","version":"57.0.8"}
|
||||||
|
{"_e":"env:mode","_t":1784254504056,"nodeEnv":"development","babelEnv":"development","mode":"development"}
|
||||||
|
{"_e":"env:load","_t":1784254504058,"mode":"development","files":[],"env":{}}
|
||||||
|
{"_e":"devserver:start","_t":1784254508949,"mode":"development","web":true,"baseUrl":"","asyncRoutes":false,"routerRoot":"../../../app","serverComponents":false,"serverActions":false,"serverRendering":false,"apiRoutes":false,"exporting":false}
|
||||||
|
{"_e":"metro:config","_t":1784254509422,"serverRoot":"../../..","projectRoot":"../../..","exporting":false,"flags":{"autolinkingModuleResolution":false,"serverActions":false,"serverComponents":false,"reactCompiler":false,"optimizeGraph":false,"treeshaking":false,"logbox":false}}
|
||||||
|
{"_e":"expo:dev-tools-plugin:load","_t":1784254509956,"plugins":[]}
|
||||||
|
{"_e":"metro:instantiate","_t":1784254516842,"atlas":false,"workers":3,"host":"::","port":8086}
|
||||||
|
{"_e":"metro:bundling:started","_t":1784254564715,"id":"1","platform":"android","environment":null,"entry":"index.ts"}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254564715,"id":"1","progress":0,"total":1,"current":0}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254564729,"id":"1","progress":0,"total":1,"current":0}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254565214,"id":"1","progress":0.010000000000000002,"total":1,"current":1}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254565318,"id":"1","progress":0.09,"total":9,"current":3}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254565418,"id":"1","progress":0.09,"total":41,"current":7}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254565518,"id":"1","progress":0.09,"total":46,"current":11}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254565577,"id":"1","progress":0.09,"total":46,"current":12}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254565690,"id":"1","progress":0.09,"total":147,"current":14}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254565794,"id":"1","progress":0.09,"total":156,"current":24}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254565894,"id":"1","progress":0.09,"total":157,"current":31}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254565998,"id":"1","progress":0.09,"total":185,"current":45}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254566099,"id":"1","progress":0.09,"total":197,"current":52}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254566211,"id":"1","progress":0.09,"total":210,"current":59}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254566311,"id":"1","progress":0.09,"total":223,"current":66}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254566419,"id":"1","progress":0.09782840730462457,"total":227,"current":71}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254566519,"id":"1","progress":0.11201999167013746,"total":245,"current":82}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254566623,"id":"1","progress":0.15123456790123457,"total":270,"current":105}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254566723,"id":"1","progress":0.18367346938775508,"total":287,"current":123}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254566823,"id":"1","progress":0.2352733413538482,"total":301,"current":146}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254566923,"id":"1","progress":0.259462890625,"total":320,"current":163}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254567024,"id":"1","progress":0.27952464836940155,"total":331,"current":175}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254567124,"id":"1","progress":0.33271161520688924,"total":345,"current":199}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254567225,"id":"1","progress":0.35676406135865596,"total":370,"current":221}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254567326,"id":"1","progress":0.37024645369120834,"total":401,"current":244}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254567428,"id":"1","progress":0.37472705039741466,"total":428,"current":262}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254567529,"id":"1","progress":0.3978698224852071,"total":455,"current":287}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254567628,"id":"1","progress":0.41077662667090886,"total":479,"current":307}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254567730,"id":"1","progress":0.42145226326742974,"total":496,"current":322}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254567834,"id":"1","progress":0.42145226326742974,"total":536,"current":333}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254567938,"id":"1","progress":0.42145226326742974,"total":540,"current":339}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254568057,"id":"1","progress":0.42145226326742974,"total":543,"current":345}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254568158,"id":"1","progress":0.42145226326742974,"total":556,"current":354}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254568257,"id":"1","progress":0.42145226326742974,"total":556,"current":356}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254568357,"id":"1","progress":0.42472014414228576,"total":557,"current":363}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254568458,"id":"1","progress":0.42472014414228576,"total":571,"current":370}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254568559,"id":"1","progress":0.44212055590581395,"total":573,"current":381}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254568663,"id":"1","progress":0.46466158211707237,"total":578,"current":394}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254568764,"id":"1","progress":0.48831470460153875,"total":581,"current":406}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254568909,"id":"1","progress":0.500828402366864,"total":585,"current":414}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254569015,"id":"1","progress":0.500828402366864,"total":2334,"current":415}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254569115,"id":"1","progress":0.500828402366864,"total":2338,"current":435}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254569222,"id":"1","progress":0.500828402366864,"total":2343,"current":440}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254569326,"id":"1","progress":0.500828402366864,"total":2346,"current":459}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254569467,"id":"1","progress":0.500828402366864,"total":2347,"current":467}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254569567,"id":"1","progress":0.500828402366864,"total":2348,"current":472}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254569667,"id":"1","progress":0.500828402366864,"total":2367,"current":491}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254569771,"id":"1","progress":0.500828402366864,"total":2428,"current":523}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254569871,"id":"1","progress":0.500828402366864,"total":2454,"current":542}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254569971,"id":"1","progress":0.500828402366864,"total":2465,"current":563}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254570071,"id":"1","progress":0.500828402366864,"total":2477,"current":583}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254570173,"id":"1","progress":0.500828402366864,"total":2478,"current":625}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254570273,"id":"1","progress":0.500828402366864,"total":2478,"current":677}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254570374,"id":"1","progress":0.500828402366864,"total":2478,"current":727}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254570474,"id":"1","progress":0.500828402366864,"total":2478,"current":772}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254570574,"id":"1","progress":0.500828402366864,"total":2478,"current":818}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254570674,"id":"1","progress":0.500828402366864,"total":2478,"current":863}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254570774,"id":"1","progress":0.500828402366864,"total":2478,"current":912}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254570874,"id":"1","progress":0.500828402366864,"total":2478,"current":965}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254570974,"id":"1","progress":0.500828402366864,"total":2478,"current":1008}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254571076,"id":"1","progress":0.500828402366864,"total":2478,"current":1056}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254571176,"id":"1","progress":0.500828402366864,"total":2478,"current":1105}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254571277,"id":"1","progress":0.500828402366864,"total":2478,"current":1160}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254571377,"id":"1","progress":0.500828402366864,"total":2478,"current":1209}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254571478,"id":"1","progress":0.500828402366864,"total":2478,"current":1264}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254571578,"id":"1","progress":0.500828402366864,"total":2478,"current":1311}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254571678,"id":"1","progress":0.500828402366864,"total":2478,"current":1357}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254571778,"id":"1","progress":0.500828402366864,"total":2478,"current":1410}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254571878,"id":"1","progress":0.500828402366864,"total":2478,"current":1461}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254571978,"id":"1","progress":0.500828402366864,"total":2478,"current":1507}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254572078,"id":"1","progress":0.500828402366864,"total":2478,"current":1560}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254572178,"id":"1","progress":0.500828402366864,"total":2478,"current":1611}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254572279,"id":"1","progress":0.500828402366864,"total":2478,"current":1668}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254572379,"id":"1","progress":0.500828402366864,"total":2478,"current":1721}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254572480,"id":"1","progress":0.5136689550856253,"total":2478,"current":1776}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254572580,"id":"1","progress":0.5394363050209071,"total":2478,"current":1820}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254572681,"id":"1","progress":0.5737542838642687,"total":2478,"current":1877}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254572781,"id":"1","progress":0.6047290409029646,"total":2478,"current":1927}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254572881,"id":"1","progress":0.6397417532559323,"total":2478,"current":1982}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254572981,"id":"1","progress":0.6737507987969679,"total":2478,"current":2034}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254573081,"id":"1","progress":0.7106802981654216,"total":2478,"current":2089}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254573181,"id":"1","progress":0.7506914764373623,"total":2478,"current":2147}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254573281,"id":"1","progress":0.782489458485683,"total":2478,"current":2192}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254573382,"id":"1","progress":0.822981380620811,"total":2478,"current":2248}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254573483,"id":"1","progress":0.8637444540202369,"total":2478,"current":2303}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254573583,"id":"1","progress":0.8846124232278917,"total":2489,"current":2341}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254573683,"id":"1","progress":0.8880743173619988,"total":2499,"current":2355}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254573786,"id":"1","progress":0.8880743173619988,"total":2589,"current":2373}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254573886,"id":"1","progress":0.8880743173619988,"total":2601,"current":2399}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254573987,"id":"1","progress":0.8880743173619988,"total":2602,"current":2410}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254574087,"id":"1","progress":0.8880743173619988,"total":2614,"current":2424}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254574188,"id":"1","progress":0.8880743173619988,"total":2617,"current":2444}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254574288,"id":"1","progress":0.8880743173619988,"total":2619,"current":2451}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254574388,"id":"1","progress":0.8880743173619988,"total":2621,"current":2455}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254574488,"id":"1","progress":0.8880743173619988,"total":2625,"current":2462}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254574588,"id":"1","progress":0.8880743173619988,"total":2631,"current":2469}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254574688,"id":"1","progress":0.8880743173619988,"total":2636,"current":2474}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254574844,"id":"1","progress":0.8880743173619988,"total":2640,"current":2481}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254574944,"id":"1","progress":0.8880743173619988,"total":2649,"current":2493}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254575044,"id":"1","progress":0.8880743173619988,"total":2700,"current":2508}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254575144,"id":"1","progress":0.8880743173619988,"total":2707,"current":2526}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254575245,"id":"1","progress":0.8880743173619988,"total":2713,"current":2534}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254575345,"id":"1","progress":0.8880743173619988,"total":2778,"current":2556}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254575447,"id":"1","progress":0.8880743173619988,"total":2786,"current":2569}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254575547,"id":"1","progress":0.8880743173619988,"total":2787,"current":2572}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254575647,"id":"1","progress":0.8880743173619988,"total":2787,"current":2574}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254575778,"id":"1","progress":0.8880743173619988,"total":2787,"current":2575}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254575878,"id":"1","progress":0.8880743173619988,"total":2789,"current":2582}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254575980,"id":"1","progress":0.8880743173619988,"total":2789,"current":2593}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254576082,"id":"1","progress":0.8880743173619988,"total":2799,"current":2612}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254576182,"id":"1","progress":0.8880743173619988,"total":2813,"current":2637}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254576282,"id":"1","progress":0.8880743173619988,"total":2824,"current":2661}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254576384,"id":"1","progress":0.9156393512217658,"total":2830,"current":2708}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254576484,"id":"1","progress":0.9156393512217658,"total":2848,"current":2724}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254576584,"id":"1","progress":0.9156393512217658,"total":2849,"current":2726}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254576684,"id":"1","progress":0.9156393512217658,"total":2868,"current":2734}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254576827,"id":"1","progress":0.9156393512217658,"total":2868,"current":2735}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254576927,"id":"1","progress":0.9156393512217658,"total":2885,"current":2738}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254577029,"id":"1","progress":0.9156393512217658,"total":2885,"current":2741}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254577129,"id":"1","progress":0.9156393512217658,"total":2900,"current":2760}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254577229,"id":"1","progress":0.9242757760075363,"total":2901,"current":2789}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254577332,"id":"1","progress":0.936287891094404,"total":2903,"current":2809}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254577435,"id":"1","progress":0.936287891094404,"total":2938,"current":2819}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254577540,"id":"1","progress":0.936287891094404,"total":2986,"current":2825}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254577642,"id":"1","progress":0.936287891094404,"total":3015,"current":2842}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254577740,"id":"1","progress":0.936287891094404,"total":3030,"current":2856}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254577840,"id":"1","progress":0.936287891094404,"total":3035,"current":2860}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254577940,"id":"1","progress":0.936287891094404,"total":3044,"current":2871}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254578041,"id":"1","progress":0.936287891094404,"total":3047,"current":2879}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254578141,"id":"1","progress":0.936287891094404,"total":3065,"current":2890}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254578241,"id":"1","progress":0.936287891094404,"total":3080,"current":2900}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254578342,"id":"1","progress":0.936287891094404,"total":3082,"current":2913}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254578442,"id":"1","progress":0.936287891094404,"total":3082,"current":2928}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254578543,"id":"1","progress":0.936287891094404,"total":3089,"current":2949}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254578642,"id":"1","progress":0.936287891094404,"total":3096,"current":2973}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254578742,"id":"1","progress":0.936287891094404,"total":3114,"current":2999}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254578843,"id":"1","progress":0.936287891094404,"total":3114,"current":3002}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254578943,"id":"1","progress":0.936287891094404,"total":3123,"current":3009}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254579045,"id":"1","progress":0.936287891094404,"total":3123,"current":3011}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254579064,"id":"1","progress":0.936287891094404,"total":3123,"current":3012}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254579164,"id":"1","progress":0.936287891094404,"total":3131,"current":3025}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254579265,"id":"1","progress":0.9365842200881371,"total":3134,"current":3033}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254579367,"id":"1","progress":0.9368419128824654,"total":3147,"current":3046}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254579468,"id":"1","progress":0.9387456790123457,"total":3150,"current":3052}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254579568,"id":"1","progress":0.9393609473418997,"total":3150,"current":3053}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254579682,"id":"1","progress":0.9412079617031998,"total":3150,"current":3056}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254579782,"id":"1","progress":0.9430567901234569,"total":3150,"current":3059}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254579883,"id":"1","progress":0.9473777777777779,"total":3150,"current":3066}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254579989,"id":"1","progress":0.9529773417570666,"total":3152,"current":3077}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254580089,"id":"1","progress":0.9549354307189066,"total":3159,"current":3087}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254580189,"id":"1","progress":0.9686047908988945,"total":3160,"current":3110}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254580292,"id":"1","progress":0.9717926782024794,"total":3168,"current":3123}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254580392,"id":"1","progress":0.9767778200247423,"total":3168,"current":3131}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254580502,"id":"1","progress":0.9768938323842832,"total":3184,"current":3147}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254580608,"id":"1","progress":0.9769943965134558,"total":3198,"current":3161}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254580709,"id":"1","progress":0.9769943965134558,"total":3220,"current":3182}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254580809,"id":"1","progress":0.9791352776121244,"total":3242,"current":3208}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254580909,"id":"1","progress":0.9791352776121244,"total":3272,"current":3232}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254581010,"id":"1","progress":0.9848326177487721,"total":3284,"current":3259}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254581111,"id":"1","progress":0.999,"total":3291,"current":3290}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254581211,"id":"1","progress":0.999,"total":3291,"current":3291}
|
||||||
|
{"_e":"metro:bundling:done","_t":1784254581468,"id":"1","total":3291,"ms":16753.428873}
|
||||||
|
{"_e":"metro:client_log","_t":1784254596888,"level":"warn","data":["SafeAreaView has been deprecated and will be removed in a future release. Please use 'react-native-safe-area-context' instead. See https://github.com/AppAndFlow/react-native-safe-area-context"]}
|
||||||
|
{"_e":"metro:bundling:started","_t":1784254612886,"id":"3","platform":"android","environment":null,"entry":"index.ts"}
|
||||||
|
{"_e":"metro:bundling:progress","_t":1784254612886,"id":"3","progress":0,"total":1,"current":0}
|
||||||
|
{"_e":"metro:bundling:done","_t":1784254612993,"id":"3","total":1,"ms":107.061183}
|
||||||
|
{"_e":"metro:client_log","_t":1784254630240,"level":"warn","data":["SafeAreaView has been deprecated and will be removed in a future release. Please use 'react-native-safe-area-context' instead. See https://github.com/AppAndFlow/react-native-safe-area-context"]}
|
||||||
|
{"_e":"metro:client_log","_t":1784254721520,"level":"warn","data":["[expo-image-picker] `ImagePicker.MediaTypeOptions` have been deprecated. Use `ImagePicker.MediaType` or an array of `ImagePicker.MediaType` instead."]}
|
||||||
|
{"_e":"metro:client_log","_t":1784254774528,"level":"warn","data":["[expo-image-picker] `ImagePicker.MediaTypeOptions` have been deprecated. Use `ImagePicker.MediaType` or an array of `ImagePicker.MediaType` instead."]}
|
||||||
|
{"_e":"metro:client_log","_t":1784254928412,"level":"warn","data":["[expo-image-picker] `ImagePicker.MediaTypeOptions` have been deprecated. Use `ImagePicker.MediaType` or an array of `ImagePicker.MediaType` instead."]}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"devices": [
|
||||||
|
{
|
||||||
|
"installationId": "3ac39ffb-519e-4fcd-b686-c20d0a63c449",
|
||||||
|
"lastUsed": 1784254612714
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 5.8 KiB |
|
After Width: | Height: | Size: 8.6 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 5.5 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 66 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 5.2 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 6.4 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 5.7 KiB |
|
After Width: | Height: | Size: 9.0 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
@@ -0,0 +1 @@
|
|||||||
|
node_modules
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
Dưới đây là toàn bộ nội dung kế hoạch đã được chuyển đổi sang định dạng Markdown chuẩn, tối ưu hóa cấu trúc scannable với tiêu đề phân cấp, bảng biểu, danh sách và các khối code block được định dạng rõ ràng.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Kế Hoạch Sửa Lỗi & Tối Ưu Hóa Ứng Dụng Recipes Camera
|
||||||
|
|
||||||
|
Tài liệu này tập trung giải quyết triệt để **4 vấn đề kỹ thuật lớn** được phát hiện trên phiên bản di động hiện tại của ứng dụng **Recipes Camera** (dựa trên giao diện thực tế trong tệp tin `Screenshot_2026-07-17-09-22-29-895_com.locphamtran.recipescamera.jpg`). Phương án sửa lỗi ưu tiên sử dụng các bộ thiết lập có sẵn, tối ưu hóa hiệu năng GPU và cải thiện trải nghiệm người dùng (UX).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Khắc phục lỗi: Kính ngắm (Viewfinder) Camera Native không áp dụng hiệu ứng Recipes
|
||||||
|
|
||||||
|
### Nguyên nhân lỗi
|
||||||
|
|
||||||
|
Trên thiết bị di động, luồng preview từ phần cứng camera (`Camera` của Expo hoặc `VisionCamera`) được render trực tiếp lên một bề mặt native riêng (**Native Surface**). Do đó, các bộ lọc CSS hoặc Canvas thông thường trong JavaScript không thể tác động lên Live Viewfinder này, dẫn đến việc kính ngắm vẫn hiển thị màu gốc dù người dùng đã chọn recipe khác.
|
||||||
|
|
||||||
|
### Giải pháp kỹ thuật (Sử dụng Skia Frame Processor có sẵn)
|
||||||
|
|
||||||
|
Thay vì dùng luồng preview thô, chúng ta sẽ định tuyến luồng frame của camera đi qua GPU của thiết bị bằng cách kết hợp **React Native Vision Camera (v3/v4)** và **React Native Skia**:
|
||||||
|
|
||||||
|
* **Sử dụng Frame Processor:** Kích hoạt Frame Processor từ `react-native-vision-camera`.
|
||||||
|
* **Vẽ trực tiếp lên Skia Canvas:** Truyền các Frame từ camera vào làm texture cho Skia Canvas thời gian thực.
|
||||||
|
* **Áp bộ lọc phần cứng của Skia:** Sử dụng cấu trúc lọc màu ma trận có sẵn của Skia (`Skia.ColorFilter.Matrix`) đè trực tiếp lên Canvas ngắm.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Trích đoạn giải pháp xử lý luồng Frame thời gian thực trên GPU di động
|
||||||
|
import { Camera, useFrameProcessor } from 'react-native-vision-camera';
|
||||||
|
import { Canvas, Image, ColorMatrix } from '@shopify/react-native-skia';
|
||||||
|
|
||||||
|
// Trong Component hiển thị Viewfinder
|
||||||
|
const frameProcessor = useFrameProcessor((frame) => {
|
||||||
|
'worklet';
|
||||||
|
// Đưa frame camera trực tiếp vào luồng xử lý của Skia GPU
|
||||||
|
const skiaImage = frame.toSkiaImage();
|
||||||
|
// Render và áp dụng ColorMatrix tức thì ở tốc độ 60 FPS
|
||||||
|
}, [currentMatrix]);
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Nâng cấp UX: Thay đổi nút Shutter thành nút Lưu ảnh khi ở chế độ Thư viện
|
||||||
|
|
||||||
|
### Vấn đề UX
|
||||||
|
|
||||||
|
Khi người dùng tải ảnh từ thư viện lên ứng dụng để chỉnh sửa (**Library Mode**), nút bấm chính ở giữa dưới cùng vẫn là nút chụp ảnh (nút Shutter trắng tròn lớn), gây bối rối vì người dùng không biết nhấn vào đâu để lưu bức ảnh đã áp công thức màu mới.
|
||||||
|
|
||||||
|
### Giải pháp kỹ thuật (Giao diện động)
|
||||||
|
|
||||||
|
Chúng ta sẽ chuyển đổi trạng thái của nút điều khiển chính dựa vào giá trị của biến trạng thái nguồn ảnh `currentInputSource`:
|
||||||
|
|
||||||
|
| Trạng thái `currentInputSource` | Giao diện nút bấm hiển thị | Hành vi khi kích hoạt (`onPress`) |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| **`'camera'`** | Nút Shutter trắng tròn cổ điển để chụp ảnh native. | Chạy hàm chụp ảnh `handleTakePhoto` |
|
||||||
|
| **`'library'`** | Nút Lưu ảnh (Save/Export Button) với icon tải xuống nổi bật (`download`/`save`). | Chạy trực tiếp hàm kết xuất và lưu ảnh `captureAndSave()` |
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Logic kết xuất giao diện động tại thanh điều khiển camera
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress={currentInputSource === 'camera' ? handleTakePhoto : captureAndSave}
|
||||||
|
className="relative items-center justify-center"
|
||||||
|
>
|
||||||
|
{currentInputSource === 'camera' ? (
|
||||||
|
// Nút Shutter chụp ảnh
|
||||||
|
<View className="w-16 h-16 rounded-full bg-white border-4 border-black" />
|
||||||
|
) : (
|
||||||
|
// Nút Lưu ảnh khi chỉnh sửa ảnh từ thư viện
|
||||||
|
<View className="w-16 h-16 rounded-full bg-amber-500 items-center justify-center">
|
||||||
|
<DownloadIcon stroke="#000" size={28} />
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Khắc phục lỗi: GPS Geotag Watermark không đọc được tọa độ từ ảnh thư viện
|
||||||
|
|
||||||
|
### Nguyên nhân lỗi
|
||||||
|
|
||||||
|
Mã nguồn hiện tại chỉ mô phỏng việc sinh GPS giả lập dựa trên tên tệp tin ảnh hoặc lấy GPS thời gian thực từ cảm biến điện thoại. Khi người dùng nạp một ảnh cũ có chứa dữ liệu GPS từ thư viện, ứng dụng không trích xuất được tọa độ thực tế này.
|
||||||
|
|
||||||
|
### Giải pháp kỹ thuật (Sử dụng API Native có sẵn)
|
||||||
|
|
||||||
|
Khi chọn ảnh từ thư viện bằng gói Media Library, chúng ta sẽ không dùng trực tiếp URI tạm thời, mà dùng mã định danh tài sản (`assetId`) để yêu cầu hệ thống cung cấp siêu dữ liệu đầy đủ bao gồm dữ liệu vị trí:
|
||||||
|
|
||||||
|
* **Sử dụng `expo-media-library`:** Gọi hàm `MediaLibrary.getAssetInfoAsync(assetId)` có sẵn của Expo.
|
||||||
|
* **Đọc thuộc tính Location:** API native này tự động trả về đối tượng chứa vị trí chính xác:
|
||||||
|
```json
|
||||||
|
"location": {
|
||||||
|
"latitude": 15.9000,
|
||||||
|
"longitude": 108.1500
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
* **Mã hóa địa danh ngược (Reverse Geocoding):** Gửi tọa độ này lên OpenStreetMap API để hiển thị tên địa danh hoài cổ trên ảnh (Ví dụ: `📍 PHỐ CỔ HỘI AN, VIETNAM`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Khắc phục lỗi: Ứng dụng tự reset về chế độ Camera khi thu nhỏ (App Lifecycle State)
|
||||||
|
|
||||||
|
### Nguyên nhân lỗi
|
||||||
|
|
||||||
|
Khi ứng dụng bị đẩy vào trạng thái chạy ngầm (**Background**) và bật lại (**Active**), hệ điều hành có thể giải phóng bộ nhớ tạm thời hoặc cơ chế khởi tạo camera bị kích hoạt lại từ đầu, khiến ứng dụng tự động reset về chế độ Native Camera và xóa sạch bức ảnh người dùng đang chỉnh sửa dở.
|
||||||
|
|
||||||
|
### Giải pháp kỹ thuật (Lưu giữ trạng thái bằng AppState & AsyncStorage)
|
||||||
|
|
||||||
|
Sử dụng API `AppState` có sẵn của React Native kết hợp với cơ chế lưu trữ để đóng băng trạng thái làm việc hiện tại:
|
||||||
|
|
||||||
|
1. **Lắng nghe Lifecycle:** Theo dõi trạng thái ứng dụng bằng `AppState.addEventListener`.
|
||||||
|
2. **Đóng băng khi thu nhỏ (Pause / Background):**
|
||||||
|
Lưu trữ các biến `currentInputSource`, đường dẫn ảnh tạm `currentImageUri`, Recipe đang chọn và các thông số tùy chỉnh IQ hiện tại vào bộ nhớ lưu trữ `AsyncStorage`.
|
||||||
|
3. **Khôi phục khi khởi động lại (Active):**
|
||||||
|
Trong hàm `useEffect` khởi tạo ứng dụng, tiến hành đọc lại các dữ liệu đã đóng băng từ `AsyncStorage`. Nếu người dùng đang chỉnh sửa dở ảnh thư viện, ứng dụng sẽ khôi phục chính xác bức ảnh cùng các thanh slider ở nguyên vị trí cũ.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
useEffect(() => {
|
||||||
|
const subscription = AppState.addEventListener('change', async (nextAppState) => {
|
||||||
|
if (nextAppState === 'background' || nextAppState === 'inactive') {
|
||||||
|
// Đóng băng và lưu trữ trạng thái hiện tại xuống bộ nhớ tạm
|
||||||
|
await AsyncStorage.setItem('saved_session', JSON.stringify({
|
||||||
|
source: currentInputSource,
|
||||||
|
imageUri: currentImageUri,
|
||||||
|
adjustments: adjustments
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return () => subscription.remove();
|
||||||
|
}, [currentInputSource, currentImageUri, adjustments]);
|
||||||
|
|
||||||
|
```
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# Expo HAS CHANGED
|
||||||
|
|
||||||
|
Read the exact versioned docs at https://docs.expo.dev/versions/v57.0.0/ before writing any code.
|
||||||
@@ -0,0 +1,286 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { SafeAreaView, View, StatusBar, Alert, ActivityIndicator, Text } from 'react-native';
|
||||||
|
import { useCameraPermissions } from 'expo-camera';
|
||||||
|
import * as ImagePicker from 'expo-image-picker';
|
||||||
|
import * as Haptics from 'expo-haptics';
|
||||||
|
|
||||||
|
import Header from './src/components/Header';
|
||||||
|
import Viewfinder from './src/components/Viewfinder';
|
||||||
|
import TabSelector, { TabId } from './src/components/TabSelector';
|
||||||
|
import AdjustmentPanel from './src/components/AdjustmentPanel';
|
||||||
|
import CameraControls from './src/components/CameraControls';
|
||||||
|
import PreviewModal from './src/components/PreviewModal';
|
||||||
|
|
||||||
|
import { Recipe, ColorAdjustments, FrameId, GPSInfo } from './src/types';
|
||||||
|
import { getAllRecipes, saveCustomRecipe, deleteCustomRecipe } from './src/utils/storageUtils';
|
||||||
|
import { getCurrentGPS, requestLocationPermissions } from './src/utils/locationUtils';
|
||||||
|
import { processAndExportPhoto } from './src/utils/exportEngine';
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
import './global.css';
|
||||||
|
|
||||||
|
export default function App() {
|
||||||
|
const [mode, setMode] = useState<'camera' | 'library'>('camera');
|
||||||
|
const [recipes, setRecipes] = useState<Recipe[]>([]);
|
||||||
|
const [selectedRecipe, setSelectedRecipe] = useState<Recipe | null>(null);
|
||||||
|
|
||||||
|
// Custom temporary overrides for the sliders
|
||||||
|
const [adjustments, setAdjustments] = useState<ColorAdjustments>({
|
||||||
|
exposure: 0,
|
||||||
|
contrast: 0,
|
||||||
|
saturation: 0,
|
||||||
|
temperature: 5500,
|
||||||
|
tint: 0,
|
||||||
|
highlight: 0,
|
||||||
|
shadow: 0,
|
||||||
|
denoise: 0,
|
||||||
|
clarity: 0,
|
||||||
|
grain: 0,
|
||||||
|
colorChrome: 'none',
|
||||||
|
});
|
||||||
|
|
||||||
|
const [selectedFrame, setSelectedFrame] = useState<FrameId>('none');
|
||||||
|
const [useGeotag, setUseGeotag] = useState(true);
|
||||||
|
const [gpsInfo, setGpsInfo] = useState<GPSInfo | null>(null);
|
||||||
|
const [activeTab, setActiveTab] = useState<TabId>('recipes');
|
||||||
|
|
||||||
|
const [libraryImageUri, setLibraryImageUri] = useState<string | null>(null);
|
||||||
|
const [lastPhotoUri, setLastPhotoUri] = useState<string | null>(null);
|
||||||
|
const [previewVisible, setPreviewVisible] = useState(false);
|
||||||
|
const [isProcessing, setIsProcessing] = useState(false);
|
||||||
|
|
||||||
|
// Hardware Permissions
|
||||||
|
const [cameraPermission, requestCameraPermission] = useCameraPermissions();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadRecipes();
|
||||||
|
loadGPS();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const loadRecipes = async () => {
|
||||||
|
const list = await getAllRecipes();
|
||||||
|
setRecipes(list);
|
||||||
|
if (list.length > 0) {
|
||||||
|
applyRecipe(list[0]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadGPS = async () => {
|
||||||
|
const info = await getCurrentGPS();
|
||||||
|
if (info) {
|
||||||
|
setGpsInfo(info);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const applyRecipe = (recipe: Recipe) => {
|
||||||
|
setSelectedRecipe(recipe);
|
||||||
|
setAdjustments(recipe.adjustments);
|
||||||
|
setSelectedFrame(recipe.frameId || 'none');
|
||||||
|
setUseGeotag(recipe.useGeotag);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUpdateAdjustments = (updates: Partial<ColorAdjustments>) => {
|
||||||
|
setAdjustments((prev) => {
|
||||||
|
const next = { ...prev, ...updates };
|
||||||
|
if (selectedRecipe) {
|
||||||
|
setSelectedRecipe({
|
||||||
|
...selectedRecipe,
|
||||||
|
adjustments: next,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUpdateFrame = (frameId: FrameId) => {
|
||||||
|
setSelectedFrame(frameId);
|
||||||
|
if (selectedRecipe) {
|
||||||
|
setSelectedRecipe({
|
||||||
|
...selectedRecipe,
|
||||||
|
frameId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleToggleGeotag = async (enabled: boolean) => {
|
||||||
|
setUseGeotag(enabled);
|
||||||
|
if (selectedRecipe) {
|
||||||
|
setSelectedRecipe({
|
||||||
|
...selectedRecipe,
|
||||||
|
useGeotag: enabled,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (enabled && !gpsInfo) {
|
||||||
|
const locationGranted = await requestLocationPermissions();
|
||||||
|
if (locationGranted) {
|
||||||
|
loadGPS();
|
||||||
|
} else {
|
||||||
|
Alert.alert('Permission Denied', 'Enable location services to use GPS Watermark.');
|
||||||
|
setUseGeotag(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSaveRecipe = async (name: string) => {
|
||||||
|
if (!selectedRecipe) return;
|
||||||
|
const newRecipe = await saveCustomRecipe({
|
||||||
|
name,
|
||||||
|
baseFilter: selectedRecipe.baseFilter,
|
||||||
|
adjustments,
|
||||||
|
frameId: selectedFrame,
|
||||||
|
useGeotag,
|
||||||
|
});
|
||||||
|
const list = await getAllRecipes();
|
||||||
|
setRecipes(list);
|
||||||
|
applyRecipe(newRecipe);
|
||||||
|
Alert.alert('Recipe Saved', `"${name}" has been created.`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteRecipe = async (id: string) => {
|
||||||
|
await deleteCustomRecipe(id);
|
||||||
|
const list = await getAllRecipes();
|
||||||
|
setRecipes(list);
|
||||||
|
if (selectedRecipe?.id === id && list.length > 0) {
|
||||||
|
applyRecipe(list[0]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePickImage = async () => {
|
||||||
|
const result = await ImagePicker.launchImageLibraryAsync({
|
||||||
|
mediaTypes: ImagePicker.MediaTypeOptions.Images,
|
||||||
|
allowsEditing: false,
|
||||||
|
quality: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!result.canceled && result.assets && result.assets.length > 0) {
|
||||||
|
setLibraryImageUri(result.assets[0].uri);
|
||||||
|
setMode('library');
|
||||||
|
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCapture = async () => {
|
||||||
|
if (mode === 'camera') {
|
||||||
|
// For simulator & simplicity, if camera view isn't fully mocked, we use a placeholder or trigger camera picture.
|
||||||
|
// In this setup, we simulate taking a photo or request library image if running in simulator.
|
||||||
|
// Let's create a beautiful simulation that pulls a mock asset or lets you select one if in simulator.
|
||||||
|
Alert.alert(
|
||||||
|
'Capture Mode',
|
||||||
|
'Camera capture requires native device hardware. For testing on emulators, please use the LIBRARY mode to load a photo and apply filters.',
|
||||||
|
[{ text: 'OK' }]
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Library mode capture/export: processes selected image with recipe, overlays, text
|
||||||
|
if (!libraryImageUri || !selectedRecipe) {
|
||||||
|
Alert.alert('Error', 'Please select an image from the library first.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsProcessing(true);
|
||||||
|
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
|
||||||
|
|
||||||
|
const exportedUri = await processAndExportPhoto(
|
||||||
|
libraryImageUri,
|
||||||
|
{
|
||||||
|
...selectedRecipe,
|
||||||
|
adjustments,
|
||||||
|
frameId: selectedFrame,
|
||||||
|
useGeotag,
|
||||||
|
},
|
||||||
|
selectedFrame,
|
||||||
|
useGeotag,
|
||||||
|
gpsInfo
|
||||||
|
);
|
||||||
|
|
||||||
|
setIsProcessing(false);
|
||||||
|
|
||||||
|
if (exportedUri) {
|
||||||
|
setLastPhotoUri(exportedUri);
|
||||||
|
setPreviewVisible(true);
|
||||||
|
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
|
||||||
|
} else {
|
||||||
|
Alert.alert('Failed', 'Error occurred while processing image.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SafeAreaView className="flex-1 bg-carbon">
|
||||||
|
<StatusBar barStyle="light-content" backgroundColor="#08080a" />
|
||||||
|
|
||||||
|
{/* Top Header */}
|
||||||
|
<Header mode={mode} setMode={setMode} title="CamRecipe Pro" />
|
||||||
|
|
||||||
|
{/* Main Viewfinder Frame */}
|
||||||
|
<View className="flex-1 justify-center items-center px-4 py-2 bg-carbon">
|
||||||
|
<Viewfinder
|
||||||
|
mode={mode}
|
||||||
|
recipe={
|
||||||
|
selectedRecipe || {
|
||||||
|
id: 'temp',
|
||||||
|
name: 'Custom',
|
||||||
|
baseFilter: 'none',
|
||||||
|
adjustments,
|
||||||
|
frameId: selectedFrame,
|
||||||
|
useGeotag,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
selectedFrame={selectedFrame}
|
||||||
|
useGeotag={useGeotag}
|
||||||
|
gpsInfo={gpsInfo}
|
||||||
|
libraryImageUri={libraryImageUri}
|
||||||
|
cameraPermissionGranted={!!cameraPermission?.granted}
|
||||||
|
onRequestCameraPermission={requestCameraPermission}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Selector Tabs */}
|
||||||
|
<TabSelector activeTab={activeTab} setActiveTab={setActiveTab} />
|
||||||
|
|
||||||
|
{/* Adjustments Panel */}
|
||||||
|
<View style={{ height: 250 }} className="bg-carbon">
|
||||||
|
<AdjustmentPanel
|
||||||
|
activeTab={activeTab}
|
||||||
|
recipes={recipes}
|
||||||
|
currentRecipeId={selectedRecipe?.id || ''}
|
||||||
|
adjustments={adjustments}
|
||||||
|
selectedFrame={selectedFrame}
|
||||||
|
useGeotag={useGeotag}
|
||||||
|
onSelectRecipe={applyRecipe}
|
||||||
|
onUpdateAdjustments={handleUpdateAdjustments}
|
||||||
|
onUpdateFrame={handleUpdateFrame}
|
||||||
|
onToggleGeotag={handleToggleGeotag}
|
||||||
|
onSaveRecipe={handleSaveRecipe}
|
||||||
|
onDeleteRecipe={handleDeleteRecipe}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Bottom Camera Buttons */}
|
||||||
|
<CameraControls
|
||||||
|
lastPhotoUri={lastPhotoUri}
|
||||||
|
onCapture={handleCapture}
|
||||||
|
onPickImage={handlePickImage}
|
||||||
|
onOpenPreview={() => setPreviewVisible(true)}
|
||||||
|
isLibraryMode={mode === 'library' && !libraryImageUri}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Preview Modal */}
|
||||||
|
<PreviewModal
|
||||||
|
visible={previewVisible}
|
||||||
|
onClose={() => setPreviewVisible(false)}
|
||||||
|
photoUri={lastPhotoUri}
|
||||||
|
recipeName={selectedRecipe?.name || 'Custom'}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Global Processing Loader Overlay */}
|
||||||
|
{isProcessing && (
|
||||||
|
<View className="absolute inset-0 bg-black/75 flex items-center justify-center space-y-4">
|
||||||
|
<ActivityIndicator size="large" color="#f59e0b" />
|
||||||
|
<Text className="text-amber-500 font-mono text-sm tracking-widest font-bold">
|
||||||
|
RENDERING PHOTO...
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</SafeAreaView>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
# Kế Hoạch Triển Khai Chi Tiết (Implementation Plan) - CamRecipe Pro Mobile
|
||||||
|
|
||||||
|
Dựa trên tài liệu **PROJECT_OVERVIEW.md**, kế hoạch triển khai chi tiết cho dự án **CamRecipe Pro** trên nền tảng di động iOS & Android sử dụng **React Native (Expo)** được thiết lập cụ thể dưới đây.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Thiết Lập Môi Trường & Cấu Hình Nền Móng (Mốc 1: Tuần 1)
|
||||||
|
Mục tiêu là thiết lập khung ứng dụng (UI Shell), cài đặt và cấp quyền cho các module phần cứng cơ bản.
|
||||||
|
|
||||||
|
### 1.1 Khởi Tạo Dự Án Expo
|
||||||
|
* **Lệnh thực hiện:** Khởi tạo dự án Expo mới với template TypeScript:
|
||||||
|
```bash
|
||||||
|
npx create-expo-app@latest CamRecipePro --template blank-typescript
|
||||||
|
```
|
||||||
|
* **Cài đặt thư viện giao diện:**
|
||||||
|
* `NativeWind` (v4 trở lên hỗ trợ Expo tốt nhất) và `tailwindcss`.
|
||||||
|
* `lucide-react-native` để hiển thị bộ icons SVG từ bản web.
|
||||||
|
* `expo-font` để nạp phông chữ Retro `Courier Prime.ttf`.
|
||||||
|
|
||||||
|
### 1.2 Cài Đặt Thư Viện Phần Cứng & Quyền Truy Cập (Permissions)
|
||||||
|
Cài đặt các gói Expo SDK để làm việc với phần cứng:
|
||||||
|
```bash
|
||||||
|
npx expo install expo-camera expo-location expo-media-library expo-haptics expo-image-picker @react-native-async-storage/async-storage
|
||||||
|
```
|
||||||
|
Cấu hình các quyền trong tệp `app.json` (hoặc `app.config.js`):
|
||||||
|
* **Camera:** `plugins: [["expo-camera", { "cameraPermission": "Cho phép ứng dụng sử dụng Camera để chụp ảnh với bộ lọc màu." }]]`
|
||||||
|
* **Vị trí (GPS):** `plugins: [["expo-location", { "locationAlwaysPermission": "Cho phép ứng dụng lấy tọa độ để chèn watermark GPS lên hình ảnh." }]]`
|
||||||
|
* **Thư viện ảnh:** `plugins: [["expo-media-library", { "photosPermission": "Cho phép ứng dụng lưu ảnh và truy xuất ảnh để áp bộ lọc." }]]`
|
||||||
|
|
||||||
|
### 1.3 Thiết Kế UI Shell & Navigation
|
||||||
|
* Khung bao quanh: Dùng `<SafeAreaView>` kết hợp cấu hình `StatusBar` ẩn/hiện hoặc tối để giả lập Notch/Dynamic Island.
|
||||||
|
* Bố cục chính: Cấu trúc dọc (Vertical Layout) theo khung tỉ lệ 9:19.5 (như wireframe).
|
||||||
|
* Header: Chuyển đổi giữa chế độ `Camera` và `Library` sử dụng `<TouchableOpacity>`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Phát Triển Engine Xử Lý Hình Ảnh GPU Thời Gian Thực (Mốc 2: Tuần 2)
|
||||||
|
Mục tiêu là tích hợp luồng camera native với canvas vẽ của Skia để áp dụng các bộ lọc thời gian thực.
|
||||||
|
|
||||||
|
### 2.1 Cài Đặt Thư Viện Đồ Họa & Camera Nâng Cao
|
||||||
|
```bash
|
||||||
|
npx expo install @shopify/react-native-skia react-native-vision-camera
|
||||||
|
```
|
||||||
|
*Lưu ý: Đối với `react-native-vision-camera`, cần cấu hình Babel plugin và cài đặt Expo Config Plugin tương thích.*
|
||||||
|
|
||||||
|
### 2.2 Xây Dựng Viewfinder Pipeline
|
||||||
|
* **Tích hợp Camera Frame làm Texture:** Kết nối `react-native-vision-camera` với `React Native Skia` thông qua cơ chế ghi nhận frame (Frame Processor) hoặc sử dụng các bindings sẵn có để truyền luồng video trực tiếp làm Texture cho `<Canvas>`.
|
||||||
|
* **Bộ lọc màu (Color Matrix Filter):**
|
||||||
|
* Định nghĩa cấu trúc ma trận màu $4 \times 5$ (hoặc $5 \times 5$ của Skia) cho các bộ lọc kinh điển (Classic Negative, Provia, Velvia).
|
||||||
|
* Quy đổi nhiệt độ màu Kelvin ($2500K - 10000K$) sang các hệ số RGB và đưa vào ma trận màu để tinh chỉnh White Balance mà không làm lệch Tint.
|
||||||
|
* Sử dụng component `<ColorMatrix>` của Skia để xử lý hiệu năng cao trên GPU.
|
||||||
|
* **Đường Cong Ánh Sáng (Highlight & Shadow Contrast):**
|
||||||
|
* Sử dụng `<ComponentTransfer>` của Skia để cấu hình mảng bảng màu `tableValues` riêng biệt cho Highlight và Shadow.
|
||||||
|
* **Tạo Hạt Film Monochrome (Monochrome Grain):**
|
||||||
|
* Sử dụng `<Turbulence>` tạo cấu hình nhiễu hạt ngẫu nhiên dựa trên các tham số tần số (`baseFrequency`) và hạt giống (`seed`).
|
||||||
|
* Áp dụng `<ColorMatrix>` xám hóa (Grayscale) lên lớp nhiễu này và trộn vào luồng camera gốc.
|
||||||
|
* **Khử Nhiễu (Denoise) & Độ Rõ Nét (Clarity):**
|
||||||
|
* *Denoise:* Áp dụng `Skia.ImageFilter.MakeBlur` với tham số `sigma` siêu nhỏ từ $0.3$ đến $0.5$ để mịn da/ảnh nhẹ nhàng.
|
||||||
|
* *Clarity:* Áp dụng `Skia.ImageFilter.MakeMatrixConvolution` cho độ rõ nét dương (tăng độ tương phản cạnh sắc) hoặc kết hợp hiệu ứng Blur + BlendMode Screen cho độ rõ nét âm (tạo sương mù mượt mà).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Phát Triển Quản Lý Recipes & Bộ Nhớ Tạm (Mốc 3: Tuần 3)
|
||||||
|
Mục tiêu là quản lý các công thức màu sắc cá nhân hóa và hiển thị danh sách trực quan.
|
||||||
|
|
||||||
|
### 3.1 Cài Đặt AsyncStorage & Định Dạng JSON
|
||||||
|
Mỗi Recipe sẽ có cấu trúc JSON như sau:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "uuid-v4-string",
|
||||||
|
"name": "Tên Công Thức (Hỗ trợ Tiếng Việt & Emoji)",
|
||||||
|
"matrix": [...],
|
||||||
|
"wbKelvin": 5500,
|
||||||
|
"contrastHighlight": 1.0,
|
||||||
|
"contrastShadow": 1.0,
|
||||||
|
"denoiseSigma": 0.4,
|
||||||
|
"clarity": 0,
|
||||||
|
"grainFrequency": 0.05,
|
||||||
|
"framePath": "optional-frame-id"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 Giao Diện Danh Sách Recipes 2 Cột
|
||||||
|
* Sử dụng `<FlatList numColumns={2}>` để tối ưu hóa hiệu năng render danh sách.
|
||||||
|
* Cột bên trái hiển thị hình ảnh mẫu nhỏ (thumbnail) đã được nạp động hoặc áp sẵn bộ lọc tương ứng để người dùng xem trước.
|
||||||
|
* Chạm vào thẻ công thức để kích hoạt ngay (`applyRecipe()`) mà không cần nút áp dụng phụ, tích hợp rung phản hồi xúc giác nhẹ (`expo-haptics`).
|
||||||
|
* Cửa sổ Modal lưu công thức (`#save-recipe-modal`) hiển thị dưới dạng Bottom Sheet để người dùng đặt tên và lưu lại vào AsyncStorage.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Chụp Ảnh Độ Phân Giải Cao & Xuất File (Mốc 4: Tuần 4)
|
||||||
|
Mục tiêu là lưu giữ khoảnh khắc ở chất lượng cao nhất, kết xuất đầy đủ hiệu ứng nghệ thuật và siêu dữ liệu.
|
||||||
|
|
||||||
|
### 4.1 Quy Trình Xử Lý Khi Nhấn Shutter
|
||||||
|
1. **Chụp Ảnh Gốc:** Gọi hàm chụp ảnh tĩnh độ phân giải cao từ `Vision Camera` hoặc `Expo Camera`.
|
||||||
|
2. **Canvas Ẩn (Offscreen Canvas):** Khởi tạo một đối tượng Skia Canvas ẩn có độ phân giải bằng đúng độ phân giải ảnh gốc.
|
||||||
|
3. **Áp Bộ Lọc Chất Lượng Cao:** Thực thi lại toàn bộ chuỗi bộ lọc đồ họa (WB, Exposure, Curves, Grain, Denoise, Clarity) trên canvas ẩn để giữ nguyên chất lượng chi tiết.
|
||||||
|
4. **Vẽ Khung & Watermark GPS:**
|
||||||
|
* Tải font chữ TrueType `Courier Prime.ttf` thông qua `Skia.Typeface.MakeFromFile` hoặc `useFont` từ Expo.
|
||||||
|
* Lấy tọa độ hiện tại từ `expo-location` (hoặc phân tích EXIF gốc của ảnh nếu ở chế độ Library) để render chuỗi text (📍 Địa điểm, Tọa độ, Thông số ISO/Shutter Speed).
|
||||||
|
* Vẽ đè hình ảnh PNG Frame trang trí (nếu chọn) lên trên cùng.
|
||||||
|
5. **Đóng Gói & Xuất Bản:**
|
||||||
|
* Xuất canvas thành mảng byte JPEG: `image.encodeToBytes(ImageFormat.JPEG, 95)`.
|
||||||
|
* Sử dụng `expo-media-library` để tạo tệp tin ảnh mới trong thư viện thiết bị.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Kế Hoạch Kiểm Thử & Kiểm Soát Chất Lượng (Testing & QA)
|
||||||
|
* **Kiểm thử Giả lập (Emulators):**
|
||||||
|
* Giả lập GPS thông qua các công cụ của Xcode Simulator (Location menu) hoặc Android Emulator (Extended Controls).
|
||||||
|
* Sử dụng luồng dữ liệu camera ảo (Virtual Camera) để kiểm thử bộ lọc Skia Shader mà không cần thiết bị thật.
|
||||||
|
* **Kiểm thử Thiết bị Thật:**
|
||||||
|
* Thử nghiệm nhanh bằng ứng dụng Expo Go qua mã QR.
|
||||||
|
* Tạo bản build phát triển (`eas build --profile development`) để test hiệu năng bộ nhớ, độ mượt Haptic Feedback, tốc độ render ảnh độ phân giải cao và tính ổn định của luồng xử lý GPU.
|
||||||
|
* Phân phối thử nghiệm qua TestFlight (iOS) và Google Play Console Internal (Android).
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2015-present 650 Industries, Inc. (aka Expo)
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
@@ -0,0 +1,185 @@
|
|||||||
|
|
||||||
|
# [SYSTEM PROMPT / WORKFLOW] Kế Hoạch Chuyển Đổi Dự Án CamRecipe Pro Lên iOS & Android
|
||||||
|
|
||||||
|
> **VAI TRÒ CỦA AI:** Bạn là một Chuyên gia Phát triển Di động Cấp cao (Senior Mobile Engineer) chuyên về React Native (Expo) và Đồ họa Máy tính (GPU Image Processing). Bạn có nhiệm vụ đọc hiểu cấu trúc dưới đây và thực thi/phát triển mã nguồn theo đúng đặc tả kỹ thuật, không tự ý thay đổi stack công nghệ hoặc lược bỏ tính năng trừ khi có yêu cầu.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. TỔNG QUAN DỰ ÁN (PROJECT OVERVIEW)
|
||||||
|
Tài liệu này phác thảo toàn bộ cấu trúc thư mục, stack công nghệ, engine xử lý hình ảnh, đặc tả thiết kế giao diện, kế hoạch debug và kiểm thử để chuyển đổi ứng dụng **CamRecipe Pro** (từ phiên bản Web nguyên mẫu hiện tại) thành một ứng dụng di động native hoàn chỉnh chạy mượt mà trên cả hai nền tảng iOS và Android.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. STACK CÔNG NGHỆ & THƯ VIỆN SỬ DỤNG (TECHNOLOGY STACK)
|
||||||
|
Để tối ưu hóa hiệu năng (đạt mức 60 FPS) và rút ngắn thời gian phát triển, dự án sẽ áp dụng triệt để nguyên tắc tái sử dụng các thư viện chuẩn hóa (industry-standard):
|
||||||
|
|
||||||
|
| Thành phần | Công nghệ / Thư viện | Vai trò & Giải pháp tái sử dụng |
|
||||||
|
| :--- | :--- | :--- |
|
||||||
|
| **Framework chính** | `React Native` (Expo SDK mới nhất) | Nền tảng phát triển đa nền tảng (Cross-platform). |
|
||||||
|
| **Truy cập phần cứng** | `expo-camera`, `expo-location`, `expo-media-library` | Tái sử dụng API có sẵn của Expo để truy cập Camera, Định vị (GPS) và Thư viện ảnh mà không cần viết Native Bridge. |
|
||||||
|
| **Engine đồ họa** | `@shopify/react-native-skia` + `react-native-vision-camera` | Xử lý Viewfinder & Render thời gian thực. Tận dụng bộ lọc ma trận màu (`Skia.ColorFilter.Matrix`), bộ lọc tích chập (`feConvolveMatrix`) và bộ lọc nhòe (`feGaussianBlur`) trực tiếp trên GPU. |
|
||||||
|
| **Tạo kiểu giao diện** | `NativeWind` (Tailwind CSS cho React Native) | Ánh xạ 1-1 toàn bộ class Tailwind CSS từ bản Web sang Native Components. |
|
||||||
|
| **Hệ thống Icons** | `lucide-react-native` | Sử dụng trực tiếp bộ icon SVG chất lượng cao từ bản Web. |
|
||||||
|
| **Thanh trượt thông số** | `@react-native-community/slider` | Hỗ trợ vuốt chạm vật lý và Phản hồi xúc giác (Haptic Feedback) native. |
|
||||||
|
| **Bộ nhớ tạm (Local)** | `@react-native-async-storage/async-storage` | Lưu trữ cấu trúc JSON Recipes cá nhân xuống thiết bị. |
|
||||||
|
| **Trích xuất Metadata**| `exif-reader` | Giải mã dữ liệu EXIF & GPS từ ảnh trong thư viện. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. ĐẶC TẢ THIẾT KẾ GIAO DIỆN NGƯỜI DÙNG (UI/UX SPECIFICATION)
|
||||||
|
|
||||||
|
### A. Triết lý Thiết kế (Design System)
|
||||||
|
* **Phong cách:** Retro-Modern tối giản, cao cấp.
|
||||||
|
* **Bảng màu chủ đạo:**
|
||||||
|
* Đen carbon: `#08080a`
|
||||||
|
* Xám titan: `#1f1f23`
|
||||||
|
* Vàng hổ phách: `#f59e0b`
|
||||||
|
|
||||||
|
### B. Sơ đồ bố cục màn hình (Wireframe Layout - Tỉ lệ 9:19.5)
|
||||||
|
|
||||||
|
```text
|
||||||
|
+-------------------------------------------------+
|
||||||
|
| [X] 9:41 StatusBar [=] | -> Giả lập vùng khuyết notch/Dynamic Island
|
||||||
|
+-------------------------------------------------+
|
||||||
|
| (•) X-TRANS PRO EMULATOR [CAMERA] [LIBRARY] | -> Header: Chế độ chụp & tải ảnh thư viện
|
||||||
|
+-------------------------------------------------+
|
||||||
|
| +-------------------------------------------+ |
|
||||||
|
| | [CLASSIC NEG] [DR400] [SIM LIVE OK] | | -> Viewfinder: Kính ngắm trực tiếp (Skia Canvas)
|
||||||
|
| | | | - Lớp 1: Video/Ảnh mẫu gốc
|
||||||
|
| | (Grid) | | - Lớp 2: Skia Shader (Màu + Khử nhiễu + Hạt)
|
||||||
|
| | | | - Lớp 3: PNG Frame (Trong suốt đè lên)
|
||||||
|
| | | | - Lớp 4: GPS Watermark (Chữ Courier Prime)
|
||||||
|
| | [SS 1/125 f/2.8 +0.7 EV] [ISO 3200] | |
|
||||||
|
| | | |
|
||||||
|
| | 📍 PHỐ CỔ HỘI AN | |
|
||||||
|
| | 15.9000° N, 108.1500° E | |
|
||||||
|
| +-------------------------------------------+ |
|
||||||
|
+-------------------------------------------------+
|
||||||
|
| [ Recipes ] [ Thông số IQ ] [ Màu & WB ]... | -> Tab Selectors: Chuyển đổi các bảng thông số
|
||||||
|
+-------------------------------------------------+
|
||||||
|
| [Dynamic Panels - Chiều cao cố định 250dp] | -> Khu vực nội dung Tab đang chọn
|
||||||
|
| - Tab Recipes: Danh sách 2 cột kèm ảnh mẫu |
|
||||||
|
| - Tab IQ: Thước phơi sáng, Highlight/Shadow |
|
||||||
|
| - Tab Màu & WB: Kelvin, Chrome Blue, Grain... |
|
||||||
|
+-------------------------------------------------+
|
||||||
|
| [x] Đính kèm vị trí (GPS) [Sửa vị trí] | -> Thanh tiện ích nhanh (Quick Utility Bar)
|
||||||
|
+-------------------------------------------------+
|
||||||
|
| [🎨 Thư viện] (( 🔘 )) [📷 Preview] | -> Camera Controls: Nút Chụp & Phím tắt nhanh
|
||||||
|
+-------------------------------------------------+
|
||||||
|
| === | -> Home Indicator ảo trên thiết bị di động
|
||||||
|
+-------------------------------------------------+
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
### C. Bản Đồ Ánh Xạ Linh Kiện (Web to React Native Mapping)
|
||||||
|
|
||||||
|
| Giao diện Web (HTML / Tailwind CSS) | Thành phần di động (React Native / NativeWind) | Vai trò & Trải nghiệm người dùng |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Main Wrapper (`max-w-md bg-black border-zinc-800 rounded-[48px]`) | `<SafeAreaView className="flex-1 bg-black">` | Khung bao bọc toàn màn hình, tự động tránh Notch tai thỏ và bo cong viền. |
|
||||||
|
| Mode Toggles (`#btn-mode-camera`, `#btn-mode-library`) | `<View className="flex-row">` + `<TouchableOpacity>` | Chuyển đổi tức thì nguồn cấp giữa Camera vật lý và Thư viện ảnh. |
|
||||||
|
| Viewfinder Wrapper (`aspect-[3/4] overflow-hidden`) | `<View style={{ aspectRatio: 3/4 }} className="relative overflow-hidden">` | Kính ngắm tỷ lệ nhiếp ảnh hoài cổ 3:4. |
|
||||||
|
| Camera Live Stream (`<video id="webcam">`) | `<Camera>` từ `react-native-vision-camera` | Kết nối camera vật lý, hỗ trợ lấy nét và đảo chiều camera. |
|
||||||
|
| GPU Sim Processing (`filter: url(#fuji-live-filter)`) | `<Canvas>` + `<Shader>` từ `@shopify/react-native-skia` | Nhận texture camera gốc, áp công thức màu GLSL/Skia Shaders (Exposure, WB, Grain) trực tiếp. |
|
||||||
|
| EXIF Geotag Watermark (`#geotag-watermark`) | `<Text style={{ fontFamily: 'Courier Prime' }} className="absolute bottom-4 left-4 text-amber-500">` | Đóng mốc địa danh và tọa độ địa lý màu hổ phách phong cách retro. |
|
||||||
|
| Tab Selectors (`#tab-recipes`, `#tab-adjust`) | `<FlatList horizontal={true}>` hoặc dãy nút bấm `<TouchableOpacity>` | Cuộn ngang mượt mà để chuyển đổi nhanh các nhóm thông số. |
|
||||||
|
| Thước kéo Ruler (`input[type="range"]`) | `@react-native-community/slider` | Thanh trượt mịn kết hợp phản hồi rung Haptic khi kéo. |
|
||||||
|
| Thư viện Recipes 2 cột (`#recipes-list-container`) | `<FlatList numColumns={2}>` | Bảng hiển thị các công thức màu. Cột trái là ảnh mẫu nạp động, cột phải là thông tin chi tiết. Chạm để kích hoạt. |
|
||||||
|
| Cửa sổ lưu (`#save-recipe-modal`) | `<Modal transparent={true} animationType="slide">` | Cửa sổ nhỏ (bottom sheet) trượt từ dưới lên cho phép nhập tên Tiếng Việt để lưu công thức. |
|
||||||
|
| Phím chụp Shutter (`button active:scale-90`) | `<TouchableOpacity className="w-16 h-16 rounded-full bg-white border-4 border-black">` | Nút chụp giả lập vật lý, có hiệu ứng nén nhẹ (scale down) khi nhấn, kích hoạt chuỗi render ảnh độ phân giải cao. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. KIẾN TRÚC CORE IMAGE PIPELINE ENGINE (GPU REAL-TIME)
|
||||||
|
|
||||||
|
Hệ thống xử lý hình ảnh được chia làm hai luồng hoạt động song song sử dụng React Native Skia:
|
||||||
|
|
||||||
|
### A. Luồng Kính Ngắm Live Viewfinder (Real-time GPU)
|
||||||
|
|
||||||
|
1. **Nhận Luồng Camera:** Luồng ghi trực tiếp (Camera Frame) được nạp làm texture cho Skia Canvas thông qua Native Bridge tích hợp sẵn của `react-native-vision-camera`.
|
||||||
|
2. **Bộ lọc Màu Ma Trận (Color Filter Matrix):** Tái sử dụng bộ lọc màu tích hợp của Skia nhằm tránh việc tự viết mã GLSL Shader phức tạp:
|
||||||
|
```javascript
|
||||||
|
// Áp dụng trực tiếp ma trận màu sắc có sẵn mà không cần biên dịch shader thủ công
|
||||||
|
const colorFilter = Skia.ColorFilter.Matrix(finalMatrix);
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
3. **Đồ thị Curves (Highlight/Shadow):** Thực hiện bằng bộ lọc `Skia.ColorFilter.ComponentTransfer` có sẵn, nạp các mảng giá trị `tableValues` đã tính toán trước để thay đổi cường độ điểm ảnh mà không cần can thiệp bằng CPU.
|
||||||
|
4. **Hạt Film Monochrome (Monochrome Grain):**
|
||||||
|
* Tận dụng bộ sinh nhiễu có sẵn: `Skia.Shader.MakeTurbulence(baseFrequencyX, baseFrequencyY, octaves, seed)` tạo hạt nhiễu trực tiếp trên GPU.
|
||||||
|
* Chuyển đổi màu hạt sang xám đơn sắc bằng bộ lọc `ColorFilter.Matrix` tích hợp.
|
||||||
|
|
||||||
|
|
||||||
|
5. **Clarity & Denoise (Khử Nhiễu):**
|
||||||
|
* *Denoise (Khử nhiễu):* Sử dụng bộ lọc `Skia.ImageFilter.MakeBlur(sigmaX, sigmaY, tileMode, input)` với giá trị `sigma` siêu nhỏ ($0.3 - 0.5$) để làm mịn các hạt nhiễu thô.
|
||||||
|
* *Clarity (Độ rõ nét):* Khi giá trị dương, sử dụng bộ lọc tích chập `Skia.ImageFilter.MakeMatrixConvolution` tăng độ sắc nét vùng biên. Khi giá trị âm, sử dụng bộ lọc Bloom kết hợp chế độ hòa trộn `Skia.BlendMode.Screen`.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### B. Cơ chế xuất ảnh chất lượng cao (High-Quality Export)
|
||||||
|
|
||||||
|
Khi người dùng bấm nút Shutter (Chụp):
|
||||||
|
|
||||||
|
1. Gọi API chụp ảnh tĩnh độ phân giải cao của camera vật lý.
|
||||||
|
2. Nạp ảnh tĩnh vào một `Canvas` ẩn của Skia.
|
||||||
|
3. Áp dụng chuỗi filter tương tự Viewfinder (WB, Exp, Highlight/Shadow, Grain, Denoise, Clarity).
|
||||||
|
4. Vẽ đè file PNG Frame (Khung ảnh nghệ thuật) và vẽ text chứa tọa độ GPS (Sử dụng phông chữ TrueType `Courier Prime.ttf` nạp qua `Skia.Typeface`).
|
||||||
|
5. Kết xuất ảnh sang định dạng JPEG: `image.encodeToBytes(ImageFormat.JPEG, 95)`.
|
||||||
|
6. Lưu trực tiếp vào thư viện thiết bị bằng `expo-media-library`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. ĐÁNH GIÁ VÀ GIẢI PHÁP CHO CÁC YÊU CẦU TỪ NGƯỜI DÙNG
|
||||||
|
|
||||||
|
| Yêu cầu từ người dùng | Giải pháp tối ưu trên Mobile bằng thư viện có sẵn |
|
||||||
|
| --- | --- |
|
||||||
|
| **Bỏ nút áp dụng, chạm thẻ kích hoạt ngay** | Sử dụng `<TouchableOpacity>` trong danh sách `FlatList` 2 cột. Kích hoạt hàm `applyRecipe()` ngay khi chạm đồng thời kích hoạt rung nhẹ (`expo-haptics`). |
|
||||||
|
| **Nhiệt độ màu WB: 2500K (Lạnh) -> 10000K (Ấm)** | Sử dụng thuật toán quy đổi Kelvin sang ma trận màu sắc (RGB), nạp trực tiếp vào component `<ColorMatrix>` tích hợp sẵn của Skia. |
|
||||||
|
| **Khử nhiễu (Denoise) giá trị âm không làm mờ tịt** | Sử dụng `Skia.ImageFilter.MakeBlur` với mức giới hạn `sigma` cực nhỏ ($0.3 - 0.5\text{px}$) kết hợp bù sáng nhẹ để triệt tiêu nhiễu hạt mịn mà không làm nhòe chi tiết ảnh. |
|
||||||
|
| **Chỉnh Đậm màu (Color) không bị lệch Tint** | Sử dụng thuộc tính bão hòa (saturation) thuần túy của bộ lọc `<ColorMatrix values={...} />` cấu hình độc lập cho từng hệ màu (Provia/Velvia) để tăng/giảm sắc độ mà không ảnh hưởng tới cân bằng trắng (Tint). |
|
||||||
|
| **Thêm thuộc tính Clarity (-10 đến +10)** | Sử dụng `Skia.ImageFilter.MakeMatrixConvolution` (tăng độ sắc nét biên khi dương) và kết hợp `MakeBlur` + `Skia.BlendMode.Screen` (tạo hiệu ứng sương mờ mượt mà khi âm). |
|
||||||
|
| **Highlight & Shadow Contrast độc lập** | Sử dụng component điều chế `<ComponentTransfer>` của Skia, nạp các dải bảng màu `tableValues` để tách biệt tương phản vùng tối và vùng sáng. |
|
||||||
|
| **Hỗ trợ tên có ký tự đặc biệt, Tiếng Việt, Emoji** | Quản lý danh sách Recipes dưới dạng mảng JSON lưu trong `AsyncStorage`. Định danh mỗi công thức bằng UUID (`uuid`) để tối ưu hóa quá trình render của `FlatList` không bị lỗi font hoặc trùng lặp định danh. |
|
||||||
|
| **Hạt đơn sắc (Monochrome Grain)** | Kết hợp bộ sinh nhiễu `<Turbulence>` của Skia cùng bộ lọc xám của `<ColorMatrix>` để sinh cấu trúc hạt xám hoài cổ trực tiếp trên GPU. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. KẾ HOẠCH PHÁT TRIỂN & KIỂM THỬ (MILESTONES & TESTING)
|
||||||
|
|
||||||
|
### A. Kế hoạch phát triển (4 Tuần)
|
||||||
|
|
||||||
|
* **Tuần 1: Thiết lập nền móng (Shell)**
|
||||||
|
* Khởi tạo dự án Expo SDK mới nhất. Cấu hình cấp quyền truy cập Camera, Thư viện ảnh, GPS (`Location`).
|
||||||
|
* Xây dựng giao diện Khung (UI Shell) bằng `NativeWind` và `lucide-react-native`.
|
||||||
|
|
||||||
|
|
||||||
|
* **Tuần 2: Core Image Engine (Viewfinder)**
|
||||||
|
* Tích hợp `React Native Skia` và `react-native-vision-camera`.
|
||||||
|
* Thiết lập luồng xử lý ảnh Real-time trên GPU (Color Matrix, WB, Highlight/Shadow, Denoise, Grain) hiển thị trực tiếp lên Viewfinder.
|
||||||
|
|
||||||
|
|
||||||
|
* **Tuần 3: Quản lý Recipe & Bộ nhớ tạm**
|
||||||
|
* Tích hợp `AsyncStorage` quản lý lưu/trích xuất công thức màu dưới dạng JSON.
|
||||||
|
* Xây dựng màn hình danh sách Recipes 2 cột, hiển thị Thumbnail nạp động đã áp bộ lọc tương ứng.
|
||||||
|
|
||||||
|
|
||||||
|
* **Tuần 4: Chụp ảnh độ phân giải cao & Đóng gói**
|
||||||
|
* Xây dựng mô-đun chụp ảnh tĩnh, áp bộ lọc chất lượng cao, đè PNG Frame và ghi đè Watermark GPS bằng font `Courier Prime`.
|
||||||
|
* Xuất bản thử nghiệm qua TestFlight (iOS) và Google Play Console Internal Testing (Android).
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### B. Phương án Kiểm thử (Testing Plan)
|
||||||
|
|
||||||
|
* **Trên Trình giả lập (Emulators):**
|
||||||
|
* *iOS Simulator:* Kích hoạt "Simulated Camera" (vòng lặp video mẫu) để kiểm tra Skia Shader. Giả lập GPS qua `Features -> Location`.
|
||||||
|
* *Android Emulator:* Liên kết webcam máy tính làm camera đầu vào, tùy chỉnh tọa độ GPS trong Settings nâng cao của máy ảo.
|
||||||
|
|
||||||
|
|
||||||
|
* **Trên Thiết bị thật (Khuyến nghị cao nhất):**
|
||||||
|
* *Giai đoạn Alpha (Expo Go):* Quét mã QR nội bộ qua mạng WiFi để kiểm tra tốc độ phản hồi kéo thanh trượt, độ nhạy của bộ chọn công thức 2 cột, và cảm nhận phản hồi rung (`Haptics`).
|
||||||
|
* *Giai đoạn Beta (Development Builds):* Tạo build native bằng lệnh `eas build --profile development` để kiểm tra toàn diện tốc độ ghi đè tệp tin của `Media Library` và hiệu năng kết xuất ảnh gốc độ phân giải cao ($12\text{MP} - 48\text{MP}$) trực tiếp trên thiết bị thực tế.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# OSX
|
||||||
|
#
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Android/IntelliJ
|
||||||
|
#
|
||||||
|
build/
|
||||||
|
.idea
|
||||||
|
.gradle
|
||||||
|
local.properties
|
||||||
|
*.iml
|
||||||
|
*.hprof
|
||||||
|
.cxx/
|
||||||
|
|
||||||
|
# generated inline modules
|
||||||
|
app/src/main/java/inline/
|
||||||
|
|
||||||
|
# Bundle artifacts
|
||||||
|
*.jsbundle
|
||||||
@@ -0,0 +1,182 @@
|
|||||||
|
apply plugin: "com.android.application"
|
||||||
|
apply plugin: "org.jetbrains.kotlin.android"
|
||||||
|
apply plugin: "com.facebook.react"
|
||||||
|
|
||||||
|
def projectRoot = rootDir.getAbsoluteFile().getParentFile().getAbsolutePath()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is the configuration block to customize your React Native Android app.
|
||||||
|
* By default you don't need to apply any configuration, just uncomment the lines you need.
|
||||||
|
*/
|
||||||
|
react {
|
||||||
|
entryFile = file(["node", "-e", "require('expo/scripts/resolveAppEntry')", projectRoot, "android", "absolute"].execute(null, rootDir).text.trim())
|
||||||
|
reactNativeDir = new File(["node", "--print", "require.resolve('react-native/package.json')"].execute(null, rootDir).text.trim()).getParentFile().getAbsoluteFile()
|
||||||
|
hermesCommand = new File(["node", "--print", "require.resolve('hermes-compiler/package.json', { paths: [require.resolve('react-native/package.json')] })"].execute(null, rootDir).text.trim()).getParentFile().getAbsolutePath() + "/hermesc/%OS-BIN%/hermesc"
|
||||||
|
codegenDir = new File(["node", "--print", "require.resolve('@react-native/codegen/package.json', { paths: [require.resolve('react-native/package.json')] })"].execute(null, rootDir).text.trim()).getParentFile().getAbsoluteFile()
|
||||||
|
|
||||||
|
enableBundleCompression = (findProperty('android.enableBundleCompression') ?: false).toBoolean()
|
||||||
|
// Use Expo CLI to bundle the app, this ensures the Metro config
|
||||||
|
// works correctly with Expo projects.
|
||||||
|
cliFile = new File(["node", "--print", "require.resolve('@expo/cli', { paths: [require.resolve('expo/package.json')] })"].execute(null, rootDir).text.trim())
|
||||||
|
bundleCommand = "export:embed"
|
||||||
|
|
||||||
|
/* Folders */
|
||||||
|
// The root of your project, i.e. where "package.json" lives. Default is '../..'
|
||||||
|
// root = file("../../")
|
||||||
|
// The folder where the react-native NPM package is. Default is ../../node_modules/react-native
|
||||||
|
// reactNativeDir = file("../../node_modules/react-native")
|
||||||
|
// The folder where the react-native Codegen package is. Default is ../../node_modules/@react-native/codegen
|
||||||
|
// codegenDir = file("../../node_modules/@react-native/codegen")
|
||||||
|
|
||||||
|
/* Variants */
|
||||||
|
// The list of variants to that are debuggable. For those we're going to
|
||||||
|
// skip the bundling of the JS bundle and the assets. By default is just 'debug'.
|
||||||
|
// If you add flavors like lite, prod, etc. you'll have to list your debuggableVariants.
|
||||||
|
// debuggableVariants = ["liteDebug", "prodDebug"]
|
||||||
|
|
||||||
|
/* Bundling */
|
||||||
|
// A list containing the node command and its flags. Default is just 'node'.
|
||||||
|
// nodeExecutableAndArgs = ["node"]
|
||||||
|
|
||||||
|
//
|
||||||
|
// The path to the CLI configuration file. Default is empty.
|
||||||
|
// bundleConfig = file(../rn-cli.config.js)
|
||||||
|
//
|
||||||
|
// The name of the generated asset file containing your JS bundle
|
||||||
|
// bundleAssetName = "MyApplication.android.bundle"
|
||||||
|
//
|
||||||
|
// The entry file for bundle generation. Default is 'index.android.js' or 'index.js'
|
||||||
|
// entryFile = file("../js/MyApplication.android.js")
|
||||||
|
//
|
||||||
|
// A list of extra flags to pass to the 'bundle' commands.
|
||||||
|
// See https://github.com/react-native-community/cli/blob/main/docs/commands.md#bundle
|
||||||
|
// extraPackagerArgs = []
|
||||||
|
|
||||||
|
/* Hermes Commands */
|
||||||
|
// The hermes compiler command to run. By default it is 'hermesc'
|
||||||
|
// hermesCommand = "$rootDir/my-custom-hermesc/bin/hermesc"
|
||||||
|
//
|
||||||
|
// The list of flags to pass to the Hermes compiler. By default is "-O", "-output-source-map"
|
||||||
|
// hermesFlags = ["-O", "-output-source-map"]
|
||||||
|
|
||||||
|
/* Autolinking */
|
||||||
|
autolinkLibrariesWithApp()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set this to true in release builds to optimize the app using [R8](https://developer.android.com/topic/performance/app-optimization/enable-app-optimization).
|
||||||
|
*/
|
||||||
|
def enableMinifyInReleaseBuilds = (findProperty('android.enableMinifyInReleaseBuilds') ?: false).toBoolean()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The preferred build flavor of JavaScriptCore (JSC)
|
||||||
|
*
|
||||||
|
* For example, to use the international variant, you can use:
|
||||||
|
* `def jscFlavor = 'org.webkit:android-jsc-intl:+'`
|
||||||
|
*
|
||||||
|
* The international variant includes ICU i18n library and necessary data
|
||||||
|
* allowing to use e.g. `Date.toLocaleString` and `String.localeCompare` that
|
||||||
|
* give correct results when using with locales other than en-US. Note that
|
||||||
|
* this variant is about 6MiB larger per architecture than default.
|
||||||
|
*/
|
||||||
|
def jscFlavor = 'io.github.react-native-community:jsc-android:2026004.+'
|
||||||
|
|
||||||
|
android {
|
||||||
|
ndkVersion rootProject.ext.ndkVersion
|
||||||
|
|
||||||
|
buildToolsVersion rootProject.ext.buildToolsVersion
|
||||||
|
compileSdk rootProject.ext.compileSdkVersion
|
||||||
|
|
||||||
|
namespace 'com.locphamtran.recipescamera'
|
||||||
|
defaultConfig {
|
||||||
|
applicationId 'com.locphamtran.recipescamera'
|
||||||
|
minSdkVersion rootProject.ext.minSdkVersion
|
||||||
|
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||||
|
versionCode 1
|
||||||
|
versionName "1.0.0"
|
||||||
|
|
||||||
|
buildConfigField "String", "REACT_NATIVE_RELEASE_LEVEL", "\"${findProperty('reactNativeReleaseLevel') ?: 'stable'}\""
|
||||||
|
}
|
||||||
|
signingConfigs {
|
||||||
|
debug {
|
||||||
|
storeFile file('debug.keystore')
|
||||||
|
storePassword 'android'
|
||||||
|
keyAlias 'androiddebugkey'
|
||||||
|
keyPassword 'android'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
buildTypes {
|
||||||
|
debug {
|
||||||
|
signingConfig signingConfigs.debug
|
||||||
|
}
|
||||||
|
release {
|
||||||
|
// Caution! In production, you need to generate your own keystore file.
|
||||||
|
// see https://reactnative.dev/docs/signed-apk-android.
|
||||||
|
signingConfig signingConfigs.debug
|
||||||
|
def enableShrinkResources = findProperty('android.enableShrinkResourcesInReleaseBuilds') ?: 'false'
|
||||||
|
shrinkResources enableShrinkResources.toBoolean()
|
||||||
|
minifyEnabled enableMinifyInReleaseBuilds
|
||||||
|
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
|
||||||
|
def enablePngCrunchInRelease = findProperty('android.enablePngCrunchInReleaseBuilds') ?: 'true'
|
||||||
|
crunchPngs enablePngCrunchInRelease.toBoolean()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
packagingOptions {
|
||||||
|
jniLibs {
|
||||||
|
def enableLegacyPackaging = findProperty('expo.useLegacyPackaging') ?: 'false'
|
||||||
|
useLegacyPackaging enableLegacyPackaging.toBoolean()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
androidResources {
|
||||||
|
ignoreAssetsPattern '!.svn:!.git:!.ds_store:!*.scc:!CVS:!thumbs.db:!picasa.ini:!*~'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply static values from `gradle.properties` to the `android.packagingOptions`
|
||||||
|
// Accepts values in comma delimited lists, example:
|
||||||
|
// android.packagingOptions.pickFirsts=/LICENSE,**/picasa.ini
|
||||||
|
["pickFirsts", "excludes", "merges", "doNotStrip"].each { prop ->
|
||||||
|
// Split option: 'foo,bar' -> ['foo', 'bar']
|
||||||
|
def options = (findProperty("android.packagingOptions.$prop") ?: "").split(",");
|
||||||
|
// Trim all elements in place.
|
||||||
|
for (i in 0..<options.size()) options[i] = options[i].trim();
|
||||||
|
// `[] - ""` is essentially `[""].filter(Boolean)` removing all empty strings.
|
||||||
|
options -= ""
|
||||||
|
|
||||||
|
if (options.length > 0) {
|
||||||
|
println "android.packagingOptions.$prop += $options ($options.length)"
|
||||||
|
// Ex: android.packagingOptions.pickFirsts += '**/SCCS/**'
|
||||||
|
options.each {
|
||||||
|
android.packagingOptions[prop] += it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
// The version of react-native is set by the React Native Gradle Plugin
|
||||||
|
implementation("com.facebook.react:react-android")
|
||||||
|
|
||||||
|
def isGifEnabled = (findProperty('expo.gif.enabled') ?: "") == "true";
|
||||||
|
def isWebpEnabled = (findProperty('expo.webp.enabled') ?: "") == "true";
|
||||||
|
def isWebpAnimatedEnabled = (findProperty('expo.webp.animated') ?: "") == "true";
|
||||||
|
|
||||||
|
if (isGifEnabled) {
|
||||||
|
// For animated gif support
|
||||||
|
implementation("com.facebook.fresco:animated-gif:${expoLibs.versions.fresco.get()}")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isWebpEnabled) {
|
||||||
|
// For webp support
|
||||||
|
implementation("com.facebook.fresco:webpsupport:${expoLibs.versions.fresco.get()}")
|
||||||
|
if (isWebpAnimatedEnabled) {
|
||||||
|
// Animated webp support
|
||||||
|
implementation("com.facebook.fresco:animated-webp:${expoLibs.versions.fresco.get()}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hermesEnabled.toBoolean()) {
|
||||||
|
implementation("com.facebook.react:hermes-android")
|
||||||
|
} else {
|
||||||
|
implementation jscFlavor
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
# Add project specific ProGuard rules here.
|
||||||
|
# By default, the flags in this file are appended to flags specified
|
||||||
|
# in /usr/local/Cellar/android-sdk/24.3.3/tools/proguard/proguard-android.txt
|
||||||
|
# You can edit the include path and order by changing the proguardFiles
|
||||||
|
# directive in build.gradle.
|
||||||
|
#
|
||||||
|
# For more details, see
|
||||||
|
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||||
|
|
||||||
|
# react-native-reanimated
|
||||||
|
-keep class com.swmansion.reanimated.** { *; }
|
||||||
|
-keep class com.facebook.react.turbomodule.** { *; }
|
||||||
|
|
||||||
|
# Add any project specific keep options here:
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
|
||||||
|
|
||||||
|
<application android:usesCleartextTraffic="true" tools:targetApi="28" tools:ignore="GoogleAppIndexingWarning" tools:replace="android:usesCleartextTraffic" />
|
||||||
|
</manifest>
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
|
||||||
|
|
||||||
|
<application android:usesCleartextTraffic="true" tools:targetApi="28" tools:ignore="GoogleAppIndexingWarning" tools:replace="android:usesCleartextTraffic" />
|
||||||
|
</manifest>
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
|
||||||
|
<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"/>
|
||||||
|
<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>
|
||||||
|
<intent>
|
||||||
|
<action android:name="android.intent.action.VIEW"/>
|
||||||
|
<category android:name="android.intent.category.BROWSABLE"/>
|
||||||
|
<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">
|
||||||
|
<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"/>
|
||||||
|
<meta-data android:name="expo.modules.updates.EXPO_UPDATES_LAUNCH_WAIT_MS" android:value="0"/>
|
||||||
|
<activity android:name=".MainActivity" android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|uiMode|smallestScreenSize|assetsPaths" android:launchMode="singleTask" android:windowSoftInputMode="adjustResize" android:theme="@style/Theme.App.SplashScreen" android:exported="true" android:screenOrientation="portrait">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN"/>
|
||||||
|
<category android:name="android.intent.category.LAUNCHER"/>
|
||||||
|
</intent-filter>
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.VIEW"/>
|
||||||
|
<category android:name="android.intent.category.DEFAULT"/>
|
||||||
|
<category android:name="android.intent.category.BROWSABLE"/>
|
||||||
|
<data android:scheme="exp+recipescamera"/>
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
</application>
|
||||||
|
</manifest>
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
package com.locphamtran.recipescamera
|
||||||
|
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
|
|
||||||
|
import com.facebook.react.ReactActivity
|
||||||
|
import com.facebook.react.ReactActivityDelegate
|
||||||
|
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled
|
||||||
|
import com.facebook.react.defaults.DefaultReactActivityDelegate
|
||||||
|
|
||||||
|
import expo.modules.ReactActivityDelegateWrapper
|
||||||
|
|
||||||
|
class MainActivity : ReactActivity() {
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
// Set the theme to AppTheme BEFORE onCreate to support
|
||||||
|
// coloring the background, status bar, and navigation bar.
|
||||||
|
// This is required for expo-splash-screen.
|
||||||
|
setTheme(R.style.AppTheme);
|
||||||
|
super.onCreate(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the name of the main component registered from JavaScript. This is used to schedule
|
||||||
|
* rendering of the component.
|
||||||
|
*/
|
||||||
|
override fun getMainComponentName(): String = "main"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the instance of the [ReactActivityDelegate]. We use [DefaultReactActivityDelegate]
|
||||||
|
* which allows you to enable New Architecture with a single boolean flags [fabricEnabled]
|
||||||
|
*/
|
||||||
|
override fun createReactActivityDelegate(): ReactActivityDelegate {
|
||||||
|
return ReactActivityDelegateWrapper(
|
||||||
|
this,
|
||||||
|
BuildConfig.IS_NEW_ARCHITECTURE_ENABLED,
|
||||||
|
object : DefaultReactActivityDelegate(
|
||||||
|
this,
|
||||||
|
mainComponentName,
|
||||||
|
fabricEnabled
|
||||||
|
){})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Align the back button behavior with Android S
|
||||||
|
* where moving root activities to background instead of finishing activities.
|
||||||
|
* @see <a href="https://developer.android.com/reference/android/app/Activity#onBackPressed()">onBackPressed</a>
|
||||||
|
*/
|
||||||
|
override fun invokeDefaultOnBackPressed() {
|
||||||
|
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) {
|
||||||
|
if (!moveTaskToBack(false)) {
|
||||||
|
// For non-root activities, use the default implementation to finish them.
|
||||||
|
super.invokeDefaultOnBackPressed()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use the default back button implementation on Android S
|
||||||
|
// because it's doing more than [Activity.moveTaskToBack] in fact.
|
||||||
|
super.invokeDefaultOnBackPressed()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
package com.locphamtran.recipescamera
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import android.content.res.Configuration
|
||||||
|
|
||||||
|
import com.facebook.react.PackageList
|
||||||
|
import com.facebook.react.ReactApplication
|
||||||
|
import com.facebook.react.ReactNativeApplicationEntryPoint.loadReactNative
|
||||||
|
import com.facebook.react.ReactPackage
|
||||||
|
import com.facebook.react.ReactHost
|
||||||
|
import com.facebook.react.common.ReleaseLevel
|
||||||
|
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint
|
||||||
|
|
||||||
|
import expo.modules.ApplicationLifecycleDispatcher
|
||||||
|
import expo.modules.ExpoReactHostFactory
|
||||||
|
|
||||||
|
class MainApplication : Application(), ReactApplication {
|
||||||
|
|
||||||
|
override val reactHost: ReactHost by lazy {
|
||||||
|
ExpoReactHostFactory.getDefaultReactHost(
|
||||||
|
context = applicationContext,
|
||||||
|
packageList =
|
||||||
|
PackageList(this).packages.apply {
|
||||||
|
// Packages that cannot be autolinked yet can be added manually here, for example:
|
||||||
|
// add(MyReactNativePackage())
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
|
DefaultNewArchitectureEntryPoint.releaseLevel = try {
|
||||||
|
ReleaseLevel.valueOf(BuildConfig.REACT_NATIVE_RELEASE_LEVEL.uppercase())
|
||||||
|
} catch (e: IllegalArgumentException) {
|
||||||
|
ReleaseLevel.STABLE
|
||||||
|
}
|
||||||
|
loadReactNative(this)
|
||||||
|
ApplicationLifecycleDispatcher.onApplicationCreate(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||||
|
super.onConfigurationChanged(newConfig)
|
||||||
|
ApplicationLifecycleDispatcher.onConfigurationChanged(this, newConfig)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 65 KiB |
@@ -0,0 +1,6 @@
|
|||||||
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:drawable="@color/splashscreen_background"/>
|
||||||
|
<item>
|
||||||
|
<bitmap android:gravity="center" android:src="@drawable/splashscreen_logo"/>
|
||||||
|
</item>
|
||||||
|
</layer-list>
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Copyright (C) 2014 The Android Open Source Project
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
-->
|
||||||
|
<inset xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:insetLeft="@dimen/abc_edit_text_inset_horizontal_material"
|
||||||
|
android:insetRight="@dimen/abc_edit_text_inset_horizontal_material"
|
||||||
|
android:insetTop="@dimen/abc_edit_text_inset_top_material"
|
||||||
|
android:insetBottom="@dimen/abc_edit_text_inset_bottom_material"
|
||||||
|
>
|
||||||
|
|
||||||
|
<selector>
|
||||||
|
<!--
|
||||||
|
This file is a copy of abc_edit_text_material (https://bit.ly/3k8fX7I).
|
||||||
|
The item below with state_pressed="false" and state_focused="false" causes a NullPointerException.
|
||||||
|
NullPointerException:tempt to invoke virtual method 'android.graphics.drawable.Drawable android.graphics.drawable.Drawable$ConstantState.newDrawable(android.content.res.Resources)'
|
||||||
|
|
||||||
|
<item android:state_pressed="false" android:state_focused="false" android:drawable="@drawable/abc_textfield_default_mtrl_alpha"/>
|
||||||
|
|
||||||
|
For more info, see https://bit.ly/3CdLStv (react-native/pull/29452) and https://bit.ly/3nxOMoR.
|
||||||
|
-->
|
||||||
|
<item android:state_enabled="false" android:drawable="@drawable/abc_textfield_default_mtrl_alpha"/>
|
||||||
|
<item android:drawable="@drawable/abc_textfield_activated_mtrl_alpha"/>
|
||||||
|
</selector>
|
||||||
|
|
||||||
|
</inset>
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@mipmap/ic_launcher_background"/>
|
||||||
|
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||||
|
<monochrome android:drawable="@mipmap/ic_launcher_monochrome"/>
|
||||||
|
</adaptive-icon>
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@mipmap/ic_launcher_background"/>
|
||||||
|
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||||
|
<monochrome android:drawable="@mipmap/ic_launcher_monochrome"/>
|
||||||
|
</adaptive-icon>
|
||||||
|
After Width: | Height: | Size: 5.7 KiB |
|
After Width: | Height: | Size: 5.8 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 6.4 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 5.5 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 9.0 KiB |
|
After Width: | Height: | Size: 8.6 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 66 KiB |
|
After Width: | Height: | Size: 5.2 KiB |
|
After Width: | Height: | Size: 31 KiB |
@@ -0,0 +1 @@
|
|||||||
|
<resources/>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<resources>
|
||||||
|
<color name="splashscreen_background">#FFFFFF</color>
|
||||||
|
<color name="iconBackground">#E6F4FE</color>
|
||||||
|
<color name="colorPrimary">#023c69</color>
|
||||||
|
</resources>
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
<resources>
|
||||||
|
<string name="app_name">recipescamera</string>
|
||||||
|
</resources>
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
<style name="AppTheme" parent="Theme.AppCompat.DayNight.NoActionBar">
|
||||||
|
<item name="android:editTextBackground">@drawable/rn_edit_text_material</item>
|
||||||
|
<item name="colorPrimary">@color/colorPrimary</item>
|
||||||
|
<item name="android:statusBarColor">@android:color/transparent</item>
|
||||||
|
<item name="android:navigationBarColor">@android:color/transparent</item>
|
||||||
|
</style>
|
||||||
|
<style name="Theme.App.SplashScreen" parent="AppTheme">
|
||||||
|
<item name="android:windowBackground">@drawable/splashscreen_logo</item>
|
||||||
|
</style>
|
||||||
|
</resources>
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||||
|
|
||||||
|
buildscript {
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
dependencies {
|
||||||
|
classpath('com.android.tools.build:gradle')
|
||||||
|
classpath('com.facebook.react:react-native-gradle-plugin')
|
||||||
|
classpath('org.jetbrains.kotlin:kotlin-gradle-plugin')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
allprojects {
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
maven { url 'https://www.jitpack.io' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
apply plugin: "expo-root-project"
|
||||||
|
apply plugin: "com.facebook.react.rootproject"
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
# Project-wide Gradle settings.
|
||||||
|
|
||||||
|
# IDE (e.g. Android Studio) users:
|
||||||
|
# Gradle settings configured through the IDE *will override*
|
||||||
|
# any settings specified in this file.
|
||||||
|
|
||||||
|
# For more details on how to configure your build environment visit
|
||||||
|
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
||||||
|
|
||||||
|
# Specifies the JVM arguments used for the daemon process.
|
||||||
|
# The setting is particularly useful for tweaking memory settings.
|
||||||
|
# Default value: -Xmx512m -XX:MaxMetaspaceSize=256m
|
||||||
|
org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m
|
||||||
|
|
||||||
|
# When configured, Gradle will run in incubating parallel mode.
|
||||||
|
# This option should only be used with decoupled projects. More details, visit
|
||||||
|
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
||||||
|
org.gradle.parallel=true
|
||||||
|
|
||||||
|
# AndroidX package structure to make it clearer which packages are bundled with the
|
||||||
|
# Android operating system, and which are packaged with your app's APK
|
||||||
|
# https://developer.android.com/topic/libraries/support-library/androidx-rn
|
||||||
|
android.useAndroidX=true
|
||||||
|
|
||||||
|
# Enable AAPT2 PNG crunching
|
||||||
|
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
|
||||||
|
|
||||||
|
# Use this property to enable support to the new architecture.
|
||||||
|
# This will allow you to use TurboModules and the Fabric render in
|
||||||
|
# your application. You should enable this flag either if you want
|
||||||
|
# to write custom TurboModules/Fabric components OR use libraries that
|
||||||
|
# are providing them.
|
||||||
|
newArchEnabled=true
|
||||||
|
|
||||||
|
# Use this property to enable or disable the Hermes JS engine.
|
||||||
|
# If set to false, you will be using JSC instead.
|
||||||
|
hermesEnabled=true
|
||||||
|
|
||||||
|
# Use this property to enable edge-to-edge display support.
|
||||||
|
# This allows your app to draw behind system bars for an immersive UI.
|
||||||
|
# Note: Only works with ReactActivity and should not be used with custom Activity.
|
||||||
|
edgeToEdgeEnabled=true
|
||||||
|
|
||||||
|
# Enable GIF support in React Native images (~200 B increase)
|
||||||
|
expo.gif.enabled=true
|
||||||
|
# Enable webp support in React Native images (~85 KB increase)
|
||||||
|
expo.webp.enabled=true
|
||||||
|
# Enable animated webp support (~3.4 MB increase)
|
||||||
|
# Disabled by default because iOS doesn't support animated webp
|
||||||
|
expo.webp.animated=false
|
||||||
|
|
||||||
|
# Enable network inspector
|
||||||
|
EX_DEV_CLIENT_NETWORK_INSPECTOR=true
|
||||||
|
|
||||||
|
# Use legacy packaging to compress native libraries in the resulting APK.
|
||||||
|
expo.useLegacyPackaging=false
|
||||||
|
|
||||||
|
expo.inlineModules.watchedDirectories=[]
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
distributionBase=GRADLE_USER_HOME
|
||||||
|
distributionPath=wrapper/dists
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip
|
||||||
|
networkTimeout=10000
|
||||||
|
validateDistributionUrl=true
|
||||||
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
zipStorePath=wrapper/dists
|
||||||
@@ -0,0 +1,248 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
#
|
||||||
|
# Copyright © 2015 the original authors.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
#
|
||||||
|
|
||||||
|
##############################################################################
|
||||||
|
#
|
||||||
|
# Gradle start up script for POSIX generated by Gradle.
|
||||||
|
#
|
||||||
|
# Important for running:
|
||||||
|
#
|
||||||
|
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
||||||
|
# noncompliant, but you have some other compliant shell such as ksh or
|
||||||
|
# bash, then to run this script, type that shell name before the whole
|
||||||
|
# command line, like:
|
||||||
|
#
|
||||||
|
# ksh Gradle
|
||||||
|
#
|
||||||
|
# Busybox and similar reduced shells will NOT work, because this script
|
||||||
|
# requires all of these POSIX shell features:
|
||||||
|
# * functions;
|
||||||
|
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
||||||
|
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
||||||
|
# * compound commands having a testable exit status, especially «case»;
|
||||||
|
# * various built-in commands including «command», «set», and «ulimit».
|
||||||
|
#
|
||||||
|
# Important for patching:
|
||||||
|
#
|
||||||
|
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
||||||
|
# by Bash, Ksh, etc; in particular arrays are avoided.
|
||||||
|
#
|
||||||
|
# The "traditional" practice of packing multiple parameters into a
|
||||||
|
# space-separated string is a well documented source of bugs and security
|
||||||
|
# problems, so this is (mostly) avoided, by progressively accumulating
|
||||||
|
# options in "$@", and eventually passing that to Java.
|
||||||
|
#
|
||||||
|
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
||||||
|
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
||||||
|
# see the in-line comments for details.
|
||||||
|
#
|
||||||
|
# There are tweaks for specific operating systems such as AIX, CygWin,
|
||||||
|
# Darwin, MinGW, and NonStop.
|
||||||
|
#
|
||||||
|
# (3) This script is generated from the Groovy template
|
||||||
|
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||||
|
# within the Gradle project.
|
||||||
|
#
|
||||||
|
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||||
|
#
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
# Attempt to set APP_HOME
|
||||||
|
|
||||||
|
# Resolve links: $0 may be a link
|
||||||
|
app_path=$0
|
||||||
|
|
||||||
|
# Need this for daisy-chained symlinks.
|
||||||
|
while
|
||||||
|
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
||||||
|
[ -h "$app_path" ]
|
||||||
|
do
|
||||||
|
ls=$( ls -ld "$app_path" )
|
||||||
|
link=${ls#*' -> '}
|
||||||
|
case $link in #(
|
||||||
|
/*) app_path=$link ;; #(
|
||||||
|
*) app_path=$APP_HOME$link ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# This is normally unused
|
||||||
|
# shellcheck disable=SC2034
|
||||||
|
APP_BASE_NAME=${0##*/}
|
||||||
|
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||||
|
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
|
||||||
|
|
||||||
|
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||||
|
MAX_FD=maximum
|
||||||
|
|
||||||
|
warn () {
|
||||||
|
echo "$*"
|
||||||
|
} >&2
|
||||||
|
|
||||||
|
die () {
|
||||||
|
echo
|
||||||
|
echo "$*"
|
||||||
|
echo
|
||||||
|
exit 1
|
||||||
|
} >&2
|
||||||
|
|
||||||
|
# OS specific support (must be 'true' or 'false').
|
||||||
|
cygwin=false
|
||||||
|
msys=false
|
||||||
|
darwin=false
|
||||||
|
nonstop=false
|
||||||
|
case "$( uname )" in #(
|
||||||
|
CYGWIN* ) cygwin=true ;; #(
|
||||||
|
Darwin* ) darwin=true ;; #(
|
||||||
|
MSYS* | MINGW* ) msys=true ;; #(
|
||||||
|
NONSTOP* ) nonstop=true ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Determine the Java command to use to start the JVM.
|
||||||
|
if [ -n "$JAVA_HOME" ] ; then
|
||||||
|
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||||
|
# IBM's JDK on AIX uses strange locations for the executables
|
||||||
|
JAVACMD=$JAVA_HOME/jre/sh/java
|
||||||
|
else
|
||||||
|
JAVACMD=$JAVA_HOME/bin/java
|
||||||
|
fi
|
||||||
|
if [ ! -x "$JAVACMD" ] ; then
|
||||||
|
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
location of your Java installation."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
JAVACMD=java
|
||||||
|
if ! command -v java >/dev/null 2>&1
|
||||||
|
then
|
||||||
|
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
location of your Java installation."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Increase the maximum file descriptors if we can.
|
||||||
|
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||||
|
case $MAX_FD in #(
|
||||||
|
max*)
|
||||||
|
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||||
|
# shellcheck disable=SC2039,SC3045
|
||||||
|
MAX_FD=$( ulimit -H -n ) ||
|
||||||
|
warn "Could not query maximum file descriptor limit"
|
||||||
|
esac
|
||||||
|
case $MAX_FD in #(
|
||||||
|
'' | soft) :;; #(
|
||||||
|
*)
|
||||||
|
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||||
|
# shellcheck disable=SC2039,SC3045
|
||||||
|
ulimit -n "$MAX_FD" ||
|
||||||
|
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Collect all arguments for the java command, stacking in reverse order:
|
||||||
|
# * args from the command line
|
||||||
|
# * the main class name
|
||||||
|
# * -classpath
|
||||||
|
# * -D...appname settings
|
||||||
|
# * --module-path (only if needed)
|
||||||
|
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
||||||
|
|
||||||
|
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||||
|
if "$cygwin" || "$msys" ; then
|
||||||
|
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||||
|
|
||||||
|
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||||
|
|
||||||
|
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||||
|
for arg do
|
||||||
|
if
|
||||||
|
case $arg in #(
|
||||||
|
-*) false ;; # don't mess with options #(
|
||||||
|
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
||||||
|
[ -e "$t" ] ;; #(
|
||||||
|
*) false ;;
|
||||||
|
esac
|
||||||
|
then
|
||||||
|
arg=$( cygpath --path --ignore --mixed "$arg" )
|
||||||
|
fi
|
||||||
|
# Roll the args list around exactly as many times as the number of
|
||||||
|
# args, so each arg winds up back in the position where it started, but
|
||||||
|
# possibly modified.
|
||||||
|
#
|
||||||
|
# NB: a `for` loop captures its iteration list before it begins, so
|
||||||
|
# changing the positional parameters here affects neither the number of
|
||||||
|
# iterations, nor the values presented in `arg`.
|
||||||
|
shift # remove old arg
|
||||||
|
set -- "$@" "$arg" # push replacement arg
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
|
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||||
|
|
||||||
|
# Collect all arguments for the java command:
|
||||||
|
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||||
|
# and any embedded shellness will be escaped.
|
||||||
|
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||||
|
# treated as '${Hostname}' itself on the command line.
|
||||||
|
|
||||||
|
set -- \
|
||||||
|
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||||
|
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
|
||||||
|
"$@"
|
||||||
|
|
||||||
|
# Stop when "xargs" is not available.
|
||||||
|
if ! command -v xargs >/dev/null 2>&1
|
||||||
|
then
|
||||||
|
die "xargs is not available"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Use "xargs" to parse quoted args.
|
||||||
|
#
|
||||||
|
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||||
|
#
|
||||||
|
# In Bash we could simply go:
|
||||||
|
#
|
||||||
|
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
||||||
|
# set -- "${ARGS[@]}" "$@"
|
||||||
|
#
|
||||||
|
# but POSIX shell has neither arrays nor command substitution, so instead we
|
||||||
|
# post-process each arg (as a line of input to sed) to backslash-escape any
|
||||||
|
# character that might be a shell metacharacter, then use eval to reverse
|
||||||
|
# that process (while maintaining the separation between arguments), and wrap
|
||||||
|
# the whole thing up as a single "set" statement.
|
||||||
|
#
|
||||||
|
# This will of course break if any of these variables contains a newline or
|
||||||
|
# an unmatched quote.
|
||||||
|
#
|
||||||
|
|
||||||
|
eval "set -- $(
|
||||||
|
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
||||||
|
xargs -n1 |
|
||||||
|
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
||||||
|
tr '\n' ' '
|
||||||
|
)" '"$@"'
|
||||||
|
|
||||||
|
exec "$JAVACMD" "$@"
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
@REM Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||||
|
@REM
|
||||||
|
@REM This source code is licensed under the MIT license found in the
|
||||||
|
@REM LICENSE file in the root directory of this source tree.
|
||||||
|
|
||||||
|
@rem
|
||||||
|
@rem Copyright 2015 the original author or authors.
|
||||||
|
@rem
|
||||||
|
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
@rem you may not use this file except in compliance with the License.
|
||||||
|
@rem You may obtain a copy of the License at
|
||||||
|
@rem
|
||||||
|
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
@rem
|
||||||
|
@rem Unless required by applicable law or agreed to in writing, software
|
||||||
|
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
@rem See the License for the specific language governing permissions and
|
||||||
|
@rem limitations under the License.
|
||||||
|
@rem
|
||||||
|
@rem SPDX-License-Identifier: Apache-2.0
|
||||||
|
@rem
|
||||||
|
|
||||||
|
@if "%DEBUG%"=="" @echo off
|
||||||
|
@rem ##########################################################################
|
||||||
|
@rem
|
||||||
|
@rem Gradle startup script for Windows
|
||||||
|
@rem
|
||||||
|
@rem ##########################################################################
|
||||||
|
|
||||||
|
@rem Set local scope for the variables with windows NT shell
|
||||||
|
if "%OS%"=="Windows_NT" setlocal
|
||||||
|
|
||||||
|
set DIRNAME=%~dp0
|
||||||
|
if "%DIRNAME%"=="" set DIRNAME=.
|
||||||
|
@rem This is normally unused
|
||||||
|
set APP_BASE_NAME=%~n0
|
||||||
|
set APP_HOME=%DIRNAME%
|
||||||
|
|
||||||
|
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||||
|
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||||
|
|
||||||
|
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
|
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||||
|
|
||||||
|
@rem Find java.exe
|
||||||
|
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||||
|
|
||||||
|
set JAVA_EXE=java.exe
|
||||||
|
%JAVA_EXE% -version >NUL 2>&1
|
||||||
|
if %ERRORLEVEL% equ 0 goto execute
|
||||||
|
|
||||||
|
echo. 1>&2
|
||||||
|
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
||||||
|
echo. 1>&2
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||||
|
echo location of your Java installation. 1>&2
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:findJavaFromJavaHome
|
||||||
|
set JAVA_HOME=%JAVA_HOME:"=%
|
||||||
|
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||||
|
|
||||||
|
if exist "%JAVA_EXE%" goto execute
|
||||||
|
|
||||||
|
echo. 1>&2
|
||||||
|
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||||
|
echo. 1>&2
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||||
|
echo location of your Java installation. 1>&2
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:execute
|
||||||
|
@rem Setup the command line
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@rem Execute Gradle
|
||||||
|
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
|
||||||
|
|
||||||
|
:end
|
||||||
|
@rem End local scope for the variables with windows NT shell
|
||||||
|
if %ERRORLEVEL% equ 0 goto mainEnd
|
||||||
|
|
||||||
|
:fail
|
||||||
|
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||||
|
rem the _cmd.exe /c_ return code!
|
||||||
|
set EXIT_CODE=%ERRORLEVEL%
|
||||||
|
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||||
|
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||||
|
exit /b %EXIT_CODE%
|
||||||
|
|
||||||
|
:mainEnd
|
||||||
|
if "%OS%"=="Windows_NT" endlocal
|
||||||
|
|
||||||
|
:omega
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
pluginManagement {
|
||||||
|
def reactNativeGradlePlugin = new File(
|
||||||
|
providers.exec {
|
||||||
|
workingDir(rootDir)
|
||||||
|
commandLine("node", "--print", "require.resolve('@react-native/gradle-plugin/package.json', { paths: [require.resolve('react-native/package.json')] })")
|
||||||
|
}.standardOutput.asText.get().trim()
|
||||||
|
).getParentFile().absolutePath
|
||||||
|
includeBuild(reactNativeGradlePlugin)
|
||||||
|
|
||||||
|
def expoPluginsPath = new File(
|
||||||
|
providers.exec {
|
||||||
|
workingDir(rootDir)
|
||||||
|
commandLine("node", "--print", "require.resolve('expo-modules-autolinking/package.json', { paths: [require.resolve('expo/package.json')] })")
|
||||||
|
}.standardOutput.asText.get().trim(),
|
||||||
|
"../android/expo-gradle-plugin"
|
||||||
|
).absolutePath
|
||||||
|
includeBuild(expoPluginsPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
id("com.facebook.react.settings")
|
||||||
|
id("expo-autolinking-settings")
|
||||||
|
}
|
||||||
|
|
||||||
|
extensions.configure(com.facebook.react.ReactSettingsExtension) { ex ->
|
||||||
|
if (System.getenv('EXPO_USE_COMMUNITY_AUTOLINKING') == '1') {
|
||||||
|
ex.autolinkLibrariesFromCommand()
|
||||||
|
} else {
|
||||||
|
ex.autolinkLibrariesFromCommand(expoAutolinking.rnConfigCommand)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
expoAutolinking.useExpoModules()
|
||||||
|
|
||||||
|
rootProject.name = 'recipescamera'
|
||||||
|
|
||||||
|
expoAutolinking.useExpoVersionCatalog()
|
||||||
|
|
||||||
|
include ':app'
|
||||||
|
includeBuild(expoAutolinking.reactNativeGradlePlugin)
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
{
|
||||||
|
"expo": {
|
||||||
|
"name": "recipescamera",
|
||||||
|
"slug": "recipescamera",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"orientation": "portrait",
|
||||||
|
"icon": "./assets/icon.png",
|
||||||
|
"userInterfaceStyle": "light",
|
||||||
|
"ios": {
|
||||||
|
"supportsTablet": true
|
||||||
|
},
|
||||||
|
"android": {
|
||||||
|
"adaptiveIcon": {
|
||||||
|
"backgroundColor": "#E6F4FE",
|
||||||
|
"foregroundImage": "./assets/android-icon-foreground.png",
|
||||||
|
"backgroundImage": "./assets/android-icon-background.png",
|
||||||
|
"monochromeImage": "./assets/android-icon-monochrome.png"
|
||||||
|
},
|
||||||
|
"predictiveBackGestureEnabled": false,
|
||||||
|
"permissions": [
|
||||||
|
"android.permission.CAMERA",
|
||||||
|
"android.permission.RECORD_AUDIO",
|
||||||
|
"android.permission.ACCESS_COARSE_LOCATION",
|
||||||
|
"android.permission.ACCESS_FINE_LOCATION",
|
||||||
|
"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_AUDIO"
|
||||||
|
],
|
||||||
|
"package": "com.locphamtran.recipescamera"
|
||||||
|
},
|
||||||
|
"web": {
|
||||||
|
"favicon": "./assets/favicon.png"
|
||||||
|
},
|
||||||
|
"plugins": [
|
||||||
|
"expo-asset",
|
||||||
|
[
|
||||||
|
"expo-camera",
|
||||||
|
{
|
||||||
|
"cameraPermission": "Allow CamRecipe Pro to access your camera to take photos."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"expo-location",
|
||||||
|
{
|
||||||
|
"locationAlwaysAndWhenInUsePermission": "Allow CamRecipe Pro to geotag your photos with location watermark."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"expo-media-library",
|
||||||
|
{
|
||||||
|
"photosPermission": "Allow CamRecipe Pro to save processed photos to your library."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"expo-image-picker",
|
||||||
|
{
|
||||||
|
"photosPermission": "Allow CamRecipe Pro to select photos from your library to apply presets."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"extra": {
|
||||||
|
"eas": {
|
||||||
|
"projectId": "31b3c7c4-c00c-447d-aa77-a087f78480ef"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 77 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 384 KiB |