From 9955fd4a6262d26336c9e62444ec923592db2e71 Mon Sep 17 00:00:00 2001 From: joesphchang Date: Wed, 9 Jul 2025 15:11:33 -0500 Subject: [PATCH 01/16] docs(component): added missing components --- docs/components.md | 63 ++++++++++++------ static/icons/component-action-sheet-icon.png | Bin 0 -> 2081 bytes static/icons/component-breadcrumbs-icon.png | Bin 0 -> 1219 bytes static/icons/component-icons-icon.png | Bin 0 -> 1146 bytes static/icons/component-input-otp-icon.png | Bin 0 -> 1614 bytes static/icons/component-media-icon.png | Bin 1521 -> 3028 bytes static/icons/component-navigation-icon.png | Bin 0 -> 1666 bytes static/icons/component-searchbar-icon.png | Bin 0 -> 1547 bytes static/icons/component-typography-icon.png | Bin 1543 -> 1522 bytes .../feature-component-accordion-icon.png | Bin 0 -> 6395 bytes .../icons/feature-component-datetime-icon.png | Bin 0 -> 9244 bytes static/icons/feature-component-item-icon.png | Bin 0 -> 6044 bytes .../feature-component-refresher-icon.png | Bin 0 -> 9168 bytes 13 files changed, 43 insertions(+), 20 deletions(-) create mode 100644 static/icons/component-action-sheet-icon.png create mode 100644 static/icons/component-breadcrumbs-icon.png create mode 100644 static/icons/component-icons-icon.png create mode 100644 static/icons/component-input-otp-icon.png create mode 100644 static/icons/component-navigation-icon.png create mode 100644 static/icons/component-searchbar-icon.png create mode 100644 static/icons/feature-component-accordion-icon.png create mode 100644 static/icons/feature-component-datetime-icon.png create mode 100644 static/icons/feature-component-item-icon.png create mode 100644 static/icons/feature-component-refresher-icon.png diff --git a/docs/components.md b/docs/components.md index d951640a809..6eb76357568 100644 --- a/docs/components.md +++ b/docs/components.md @@ -24,10 +24,14 @@ Ionic apps are made of high-level building blocks called Components, which allow - -

Action Sheets display a set of options with the ability to confirm or cancel an action.

+ +

Accordions provide collapsible sections in your content.

+ +

Action Sheets display a set of options with the ability to confirm or cancel an action.

+
+

Alerts are a great way to offer the user the ability to choose a specific action or list of actions.

@@ -36,6 +40,10 @@ Ionic apps are made of high-level building blocks called Components, which allow

Badges are a small component that typically communicate a numerical value to the user.

+ +

Breadcrumbs are navigation items that are used to indicate where a user is on an app.

+
+

Buttons let your users take action. They're an essential way to interact with and navigate through an app.

@@ -57,22 +65,22 @@ Ionic apps are made of high-level building blocks called Components, which allow

Content is the quintessential way to interact with and navigate through an app.

- -

Date & time pickers are used to present an interface that makes it easy for users to select dates and times.

+ +

An interface which makes it easy for users to select dates and time.

Floating action buttons are circular buttons that perform a primary action on a screen.

- -

Beautifully designed icons for use in web, iOS, and Android apps.

-
-

The grid is a powerful mobile-first system for building custom layouts.

+ +

Ionicons is Ionic's Icon library for use in web, iOS, Android, and desktop apps.

+
+

Infinite scroll allows you to load new data as the user scrolls through your app.

@@ -81,16 +89,19 @@ Ionic apps are made of high-level building blocks called Components, which allow

Inputs provides a way for users to enter data in your app.

- -

Items are an all-purpose UI container that can be used as part of a list.

+ +

A common UI paradigm that serves as an entry point to more detailed information.

Lists can display rows of information, such as a contact list, playlist, or menu.

- -

Navigation is how users move between different pages in your app.

+ +

+ Media refers to a collection of Ionic's media-related components like ion-avatar, ion-img, ion-icon, and + ion-thumbnail. +

@@ -101,6 +112,14 @@ Ionic apps are made of high-level building blocks called Components, which allow

Modals slide in and off screen to display a temporary UI and are often used for login or sign-up pages.

+ +

Navigation is how users move between different pages in your app.

+
+ + +

Input OTP component simplifies entering one-time passwords with a customizable, multi-box interface.

+
+

Popover provides an easy way to present information or options without changing contexts.

@@ -113,12 +132,8 @@ Ionic apps are made of high-level building blocks called Components, which allow

Radio inputs allow you to present a set of exclusive options.

- -

Refresher provides pull-to-refresh functionality on a content component.

-
- - -

Searchbar is used to search or filter items, usually from a toolbar.

+ +

A floating action button (FAB) is a circular button that offers an action on a screen.

@@ -129,6 +144,10 @@ Ionic apps are made of high-level building blocks called Components, which allow

Routing allows navigation based on the current path.

+ +

Searchbar is used to search or filter items, usually from a toolbar.

+
+

Segments provide a set of exclusive buttons that can be used as a filter or view switcher.

@@ -149,7 +168,11 @@ Ionic apps are made of high-level building blocks called Components, which allow

Toggles are an input for binary options, often used for options and switches.

- -

Toolbars are used to house information and actions relating to your app.

+ +

Toolbars are used to house information and actions relating to your app.

+
+ + +

Text is used to style or change the color of text within an application.

diff --git a/static/icons/component-action-sheet-icon.png b/static/icons/component-action-sheet-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..50dee9de006ab6c695da206e8df8d793786734d3 GIT binary patch literal 2081 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 literal 0 HcmV?d00001 diff --git a/static/icons/component-icons-icon.png b/static/icons/component-icons-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..1e444bc4e3e3d5590f8d5accb8e8d9c6a0c1022f GIT binary patch literal 1146 zcmV-=1cm#FP)K~#7F&01Yj z6G0IEW|K%jP*5yNpF9HI0B;~C5Klk_if7=NpU4T28+Zfp1|ES|${(;0LI}y&-|jSQ zg#>nI7TB+<3zD78_Se(X(>*f=#UK)?i_}Ca?k~SHsWx3F9VZtemk5M`Kd9hSkr5;c zpF71=$(|EM71!jRxW9aUiU2Y`1XS@cr!=JvKal*W>#HSlf}BM15}0r=J40f*9*RN5 z^~?TGoMb{)0v!G+5i4?p#7*u4BtjA#ncv7T8Wr3PT4oZ7#s4B9abIBHbV(ZaITAw! zO>|mg2FZkbXjI(0C*TQHKFcWG7raT-@%YLsl*CxpA5>8uc>QwE2Fi6Q)BLCnY z+wTJm*PPgNrL**degYG&appMufZ%FPH>$Y*V??P293EXr$_lPQBypv8dIT7EH8m(J zxK2=c=2Gz}C=*(rImsEy4=$)zqo5O2#@ba_ zJ1ZY2jWIuZ*6xfhFMo;O*lbS9crRLO7o^Qw`RW+bMn3Opo1CDV$Me-VHZMgE;emFu zxoUg8wIPoWE~99^*6r?EV`c|6Fj2HN(rBzBKVFL*k^udQF7yXE(c0QtkPdbc2=R$e z9wWg7^MVtBzT~xNiK{H407yR{ksr)bYf8W`WlelO9M;YuKbUG)NPv;ZH|IV^0Wg03 za2=>A0lx}MA_)qB_3MY>)I@=KjZ-4WNBwVpamX_YZ51hNxw0s;-5r7e6 z=3`gL18Lvm@BG0GqN@ag%E#E1jCr4#gtTM(5Z6Is^bQGhP{6hcmB~b+8086We#Hcs z+0{`1sFb)9efsOb2FC33c>&53bfLCxElwjTB?~L$TN@i|l4-8$XUSOCM^{$XPz-4O zXCgn90CQ`OhIdUc2<^?y4^~J>sBDPx`zQ|Fr=_v4Do83n1JCu~fkD@K0~dHS`rIg% zOAMY(aLtO#-?YWGSgxYHxUuzG=!Wy46v8@T(aca))H#?hbk~F-zSIA3SPsO7bS3)SW0Fwi&oE?O-j>8MoIWNA zy09ug)bH=gLP>&(Pr1(>JPbHtX*eo=dRxP+$E!00000 M07*qoM6N<$f=N{aL;wH) literal 0 HcmV?d00001 diff --git a/static/icons/component-input-otp-icon.png b/static/icons/component-input-otp-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..416847abf39fa1694b740f2bafe63b6f5883ee8c GIT binary patch literal 1614 zcmV-U2C?~xP);FXK~#7F)mrUR z6K51Y`|d{)z_>ApP=-#J(O98V)(+}8(-!ih!_Z%N1==e>uK>IQxPsa%z!|4wX@3CL z8AoeN>=YI7BUKThq%1*5vYXxQIjb9!01F#1gl8s`?7L^rp69&h{W>Qc>;Xo`CgoaQ z@3VmG05Bt9aS8_@TlgsuOK=<{P$tfS^}sO1@!r_!1nd$VlnFArPRR9JmetKMp}Q`4 z7$ko!NTLsQ0~C5rLaycOs1OBj{=6SaU0gk`F!xh{%77uc=28rYXjD(i__n7`=V} zBD1)m@Udp}`Z1^k)Z_vZnNtnj`o1UDdEHIcmB1*5j3`KhO)ba47pQ?sf_mdsVj9ex z<{DC45^#)fj-32xA+sHJLSj<_qqpw&iJ~~NpT;|INaQsAQcqXskEQoF5TF4o2-4)I z$J^!oHr_!N*N(a8k5ye~r%MMmD;37sW{d`GwHQzE`D@@vL$l17K2mzGhycZXP_B0)>y{4;0-@udl;o{4YqQ zQg{&2i|#cxHp1uaryx+X{ocr{9xw8Bhh(r;l6`_{fl_!NAte*HZo`*t5m4GX!6$v# z^JldjJbLodzTfrDSFR+y>hUTs1!}=mlM0?6pu_L~V94K758=FW0?sMXA|(X6R@z$a zmyoC2;8l-Tc{;k%`J3VN84(I*pEqw7;MDOJSK~+-b+a41>hUU1Vb1~Upjjv)ipA4s zsRv5olfFL>B>G2}ZzdO##4E6xi(=+z>%*+f_3bY=;UJ*EX@`U{z zjh=u?L%`jfY8r<8o)HM5Xpefg?#yb%Q(QC}wLe9QJ?bgvy#$8YGu1G2`G&*6AjBU& z12;T+`pTY{-QZP^S9#i*A`%SO6@2mJ9cfyxpa7}NFL2eWWzau(X#+gxAf zRgYKsoUTzaEW!Oeac}5oQ%ip+{K2E^>uH#pnzE<1Lf|EdhW{niziSGz2`+dB~eh4)*$(0(| zG+-&_b=|l^(cmWQrOY};wF4|Pe6-%6kfCWD_d0PLuow2E%vuNvDNG~{9KkevP(He6 zL}&{`yMUGC0(w0+-0KoDrSSY$gLl2b2Jz90l+mW7aG*8_<$!j;w253*mu$l`APqT% zi=CqEKYI`n%+7T44EixKrck1Zi1_iF5@m(CCh{PKO$3nKd+g1rH>-al^1C z$}xP`1U4hXF;FN~N!dPZjugB^rQ`wQthixu|1dmBt$}#GT%!x|Ki^e8=B?xDBme*a M07*qoM6N<$f;=+=H2?qr literal 0 HcmV?d00001 diff --git a/static/icons/component-media-icon.png b/static/icons/component-media-icon.png index 3c21044bfad6f9f8b0408b0f943a3ccaab4124be..7534e9f6944b197607c439342c5e5de4d2e57885 100644 GIT binary patch literal 3028 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 QTL1t607*qoM6N<$g30m{DF6Tf diff --git a/static/icons/component-navigation-icon.png b/static/icons/component-navigation-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..e0c808a709c3448f67d9fbfebcd62349322fd96d GIT binary patch literal 1666 zcmV-|27UR7P)jQFO*6pm33}xY^eN(8%~ZqfJ)Pd-5+4-Ofs1ixLn!+ zhS=fZAw80VH$L~p8z4VLs~;U5saFMvQ2EW()zwL91$5{3_O^;xRj(QhD}LK;NL?b& ziaG_)BIvqKlW!tosu|Zn6U(RZ-a;ysO0=u7x&_CKy}dmJW6RUVh8=7y4DxgR{r%U5 zJi&o67KA3p!h0d(A;`agn_41YI1~Bp4W=9a!XS$Nn;6EFE)fxVG$QgeEuKMc@I(S( z{v1DNcu(^{tJXJ&|N7>fv>7Pmi^txDfT!&2?9{xVc>u1NVBEVU?}oSm1@pG={}B0k z$(s<)a3hgO%?m~dP2;BHO1uznL@Po#1IB#m8LtBjAM~$U_&E6}Ke&(+-(iH-SW3UP z0|qxK6bi`@!v|fbWno%*Zb`RzyI^>PvBvQuy{xu;d*2EHPW z<5T?^E!>B(G8wfePOz6@`wZ5 zeYpC`9X+;HRa_CL#WU}t7rJ?wJYh9Wr}dfHW`;uR03qNt1WcGs>ex-1^)7~xLQ?lg z9%4^T$}%iQle*w$q%YKKk?>|V8=Pl(tvR1z1oB)HB&%iO1-HTJ$nqfknNxus1`wq6^6v#dnZ+G^`U3TC^!< zuW`H@9Uc8vd(U`)xO#~Uv_geuV1J&7>J2aAZBL^qaaueT*T)De^5jMV+z6??Z(LLx z^Q@vMVh2Bju#WzSGvWqdVD|kNc5yDfhEQ6F$Kx3{**S(5!-@^KT(Kuki@Oz0^4P`6 zA#}#-?_ZjR!98E0-~!Tk3A>fr1yW(16nh{FDV6ocZ#-uY#OGlT9;=3q!d)9jFRZVx zf5VhEMrgIuAO~`By<4-kwl>pg5e9j35ijVZ2ySb&MBV^h*QrfDhO!+$$J?x^caD1p z%+ptMGQNY2mEAb3pJiv_`=&SD$<1hPO8XgB+%#vMw7T1naXk^HH}Jt3tcIqT5a!X! zUk#Aol;h)L1$EBC122t@3#ahBB&iDFA3dhA9L;ddpb#Ehp4Kl>nKXc*waq$)3D2Cq zPEscl2_Y02!mEWUuAm4c5az}S%7tq{;+}HtrQyK6;S7TRfF65G3kRff zsDp|`gh<@H<0fIrOYtLKe!ubBtk<)~&vp8fCcBxPo&9}}nfc}$8`1_WFE2+!q0lwl zP6Yyi7_M>4vZA;~OKl9pNa2zy6beaP7kYYn?v9U-r=&#~(j-W_udi|^VyfacuV+OpuQmfxUT@!O5M zR26x4)GahE1Wng%@){wgbzBXN=3{v8!RY8{v{Qq%QE<)J+1W9%wp?)!?0~y5$j=WA z4V@YC1Q*7^!NDkJLie)@0qASz&Yg>4fs(U^fq?-om~pHXv(Eam3)yr1^4QpzoF(l$ z!F3{?PMeub=6S$mUD*O4e-mapv*ZbKgC(M{#Pw30vcQv|{Sv@awzs#-R!|mzX%hYY z{WZyBa90#(o$h(7#S4sZ!L@9Kpj|&h0lbLcU-(lN{#cSsHZOi4+#8fvCojmmZ(OVh z$xC1ajCkpN-Wzlu%&*FQ+#va%?*A^i{HeTm^%6P`i67WMJXT7J^oQl`_$z8};RY5r zIXO8Q4$yr#&$>Y)(%%-B*5s|JmwY2n3=Q-vh5fdeQZtVZv;ud5v`?J7DK{uHU(V$6 zszbj)g=OKgUSongKOhbl3ruM(z?W(aru4q}0di8=!CHUj|QXx zknmIIi{uGv{wO0|fjzy}7QpR(Bz&Ct&;K8;6$R=hI0j5imr>n

aL=A|Wz&r#Q`C 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 Date: Thu, 10 Jul 2025 12:22:00 -0500 Subject: [PATCH 02/16] docs(components): updated descriptions on components --- docs/components.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/components.md b/docs/components.md index 6eb76357568..d58e9493e25 100644 --- a/docs/components.md +++ b/docs/components.md @@ -66,7 +66,7 @@ Ionic apps are made of high-level building blocks called Components, which allow -

An interface which makes it easy for users to select dates and time.

+

Date & time pickers are used to present an interface that makes it easy for users to select dates and times.

@@ -78,7 +78,7 @@ Ionic apps are made of high-level building blocks called Components, which allow -

Ionicons is Ionic's Icon library for use in web, iOS, Android, and desktop apps.

+

Beautifully designed icons for use in web, iOS, and Android apps.

@@ -90,7 +90,7 @@ Ionic apps are made of high-level building blocks called Components, which allow -

A common UI paradigm that serves as an entry point to more detailed information.

+

Items are an all-purpose UI container that can be used as part of a list.

@@ -133,7 +133,7 @@ Ionic apps are made of high-level building blocks called Components, which allow -

A floating action button (FAB) is a circular button that offers an action on a screen.

+

Refresher provides pull-to-refresh functionality on a content component.

From aeead466f0bd5ede23d7c6b039d972234f19a1ab Mon Sep 17 00:00:00 2001 From: joesphchang Date: Wed, 16 Jul 2025 15:14:41 -0500 Subject: [PATCH 03/16] docs(react): show complete code context in the "Your FirstApp" tutorial --- docs/react/your-first-app.md | 100 ++++- docs/react/your-first-app/2-taking-photos.md | 224 +++++++--- docs/react/your-first-app/3-saving-photos.md | 186 ++++++-- docs/react/your-first-app/4-loading-photos.md | 174 +++++++- docs/react/your-first-app/5-adding-mobile.md | 244 ++++++++--- docs/react/your-first-app/7-live-reload.md | 400 +++++++++++++++--- 6 files changed, 1104 insertions(+), 224 deletions(-) diff --git a/docs/react/your-first-app.md b/docs/react/your-first-app.md index b1fe24b6ec5..311e7bd3728 100644 --- a/docs/react/your-first-app.md +++ b/docs/react/your-first-app.md @@ -102,10 +102,22 @@ After installation, open up the project in your code editor of choice. Next, import `@ionic/pwa-elements` by editing `src/main.tsx`. ```tsx +import React from 'react'; +import { createRoot } from 'react-dom/client'; +import App from './App'; +// CHANGE: Add the following import. import { defineCustomElements } from '@ionic/pwa-elements/loader'; // Call the element loader before the render call defineCustomElements(window); + +const container = document.getElementById('root'); +const root = createRoot(container!); +root.render( + + + +); ``` That’s it! Now for the fun part - let’s see the app in action. @@ -147,10 +159,12 @@ Open `/src/pages/Tab2.tsx`. We see: Photo Gallery ``` -We put the visual aspects of our app into ``. In this case, it’s where we’ll add a button that opens the device’s camera as well as displays the image captured by the camera. Start by adding a [floating action button](https://ionicframework.com/docs/api/fab) (FAB). First, update the imports at the top of the page to include the Camera icon as well as some of the Ionic components we'll use shortly: +We put the visual aspects of our app into . In this case, it’s where we’ll add a button that opens the device’s camera as well as displays the image captured by the camera. Start by adding a [floating action button](https://ionicframework.com/docs/api/fab) (FAB) to the bottom of the page and set the camera image as the icon. ```tsx +// CHANGE: Add the following import. import { camera, trash, close } from 'ionicons/icons'; +// CHANGE: Add the following import. import { IonContent, IonHeader, @@ -166,22 +180,33 @@ import { IonImg, IonActionSheet, } from '@ionic/react'; +import ExploreContainer from '../components/ExploreContainer'; +import './Tab2.css'; + +const Tab2: React.FC = () => { + return ( + + + + Tab 2 + + + + + + takePhoto()}> + + + + + + + ); +}; + +export default Tab2; ``` -Then, add the FAB to the bottom of the page. Use the camera image as the icon, and call the `takePhoto()` function when this button is clicked (to be implemented soon): - -```tsx - - - takePhoto()}> - - - - -``` - -We’ll be creating the `takePhoto` method and the logic to use the Camera and other native features in a moment. - Next, open `src/App.tsx`, remove the `ellipse` icon from the import and import the `images` icon instead: ```tsx @@ -191,10 +216,47 @@ import { images, square, triangle } from 'ionicons/icons'; Within the tab bar (``), change the label to “Photos” and the `ellipse` icon to `images` for the middle tab button: ```tsx - - - Photos - +// Keep other imports +// CHANGE: Add the following import. +import { images, square, triangle } from 'ionicons/icons'; + +const App: React.FC = () => ( + + + + + + + + + + + + + + + + + + + + + + + + Photos + + + + + + + +); ``` :::note diff --git a/docs/react/your-first-app/2-taking-photos.md b/docs/react/your-first-app/2-taking-photos.md index 64b402d6aec..68270ff90c6 100644 --- a/docs/react/your-first-app/2-taking-photos.md +++ b/docs/react/your-first-app/2-taking-photos.md @@ -24,30 +24,54 @@ Create a new file at `src/hooks/usePhotoGallery.ts` and open it up. A custom hook is just a function that uses other React hooks. And that's what we will be doing! We will start by importing the various hooks and utilities we will be using from React core, the Ionic React Hooks project, and Capacitor: ```tsx +// CHANGE: Add the following imports import { useState, useEffect } from 'react'; import { isPlatform } from '@ionic/react'; -import { Camera, CameraResultType, CameraSource, Photo } from '@capacitor/camera'; +// CHANGE: Add the following imports +import { + Camera, + CameraResultType, + CameraSource, + Photo, +} from '@capacitor/camera'; import { Filesystem, Directory } from '@capacitor/filesystem'; import { Preferences } from '@capacitor/preferences'; import { Capacitor } from '@capacitor/core'; + +export function usePhotoGallery() {} ``` Next, create a function named usePhotoGallery: ```tsx +import { useState, useEffect } from 'react'; +import { isPlatform } from '@ionic/react'; + +import { + Camera, + CameraResultType, + CameraSource, + Photo, +} from '@capacitor/camera'; +import { Filesystem, Directory } from '@capacitor/filesystem'; +import { Preferences } from '@capacitor/preferences'; +import { Capacitor } from '@capacitor/core'; + export function usePhotoGallery() { - const takePhoto = async () => { - const photo = await Camera.getPhoto({ - resultType: CameraResultType.Uri, - source: CameraSource.Camera, - quality: 100, - }); - }; - - return { - takePhoto, - }; + // CHANGE: ADd the usePhotoGallery function. + const takePhoto = async () => { + // Take a photo + const photo = await Camera.getPhoto({ + resultType: CameraResultType.Uri, + source: CameraSource.Camera, + quality: 100, + }); + }; + + return { + takePhoto, + }; } ``` @@ -58,16 +82,34 @@ Notice the magic here: there's no platform-specific code (web, iOS, or Android)! The last step we need to take is to use the new hook from the Tab2 page. Go back to Tab2.tsx and import the hook: ```tsx -import { usePhotoGallery } from '../hooks/usePhotoGallery'; -``` +// Keep the other imports -And right before the return statement in the functional component, get access to the `takePhoto` method by using the hook: +// CHANGE: Import the usePhotoGallery hook +import { usePhotoGallery } from '../hooks/usePhotoGallery'; -```tsx const Tab2: React.FC = () => { - const { takePhoto } = usePhotoGallery(); + // CHANGE: Get access to `takePhoto` method by using the hook + const { takePhoto } = usePhotoGallery(); + + return ( + + + + Tab 2 + + + + + takePhoto()}> + + + + + + ); +}; - // snip - rest of code +export default Tab2; ``` Save the file, and if you’re not already, restart the development server in your browser by running `ionic serve`. On the Photo Gallery tab, click the Camera button. If your computer has a webcam of any sort, a modal window appears. Take a selfie! @@ -83,6 +125,11 @@ After taking a photo, it disappears. We still need to display it within our app First we will create a new type to define our Photo, which will hold specific metadata. Add the following UserPhoto interface to the `usePhotoGallery.ts` file, somewhere outside of the main function: ```tsx +export functino usePhotoGallery { + // Old code from before. +} + +// CHANGE: Add the interface. export interface UserPhoto { filepath: string; webviewPath?: string; @@ -92,53 +139,130 @@ export interface UserPhoto { Back at the top of the function (right after the call to `usePhotoGallery`, we will define a state variable to store the array of each photo captured with the Camera. ```tsx -const [photos, setPhotos] = useState([]); +export function usePhotoGallery { + // CHANGE: Add the photos array. + const [photos, setPhotos] = useState([]); + + // Old code from before. +} ``` When the camera is done taking a picture, the resulting Photo returned from Capacitor will be stored in the `photo` variable. We want to create a new photo object and add it to the photos state array. We make sure we don't accidentally mutate the current photos array by making a new array, and then call `setPhotos` to store the array into state. Update the `takePhoto` method and add this code after the getPhoto call: ```tsx -const fileName = Date.now() + '.jpeg'; -const newPhotos = [ - { - filepath: fileName, - webviewPath: photo.webPath, - }, - ...photos, -]; -setPhotos(newPhotos); -``` +// Old code from before. -Next, let's expose the photos array from our hook. Update the return statement to include the photos: +export function usePhotoGallery() { + const [photos, setPhotos] = useState([]); + // CHANGE: Create new fileName variable with date and .jpeg + const fileName = Date.now() + '.jpeg'; + + const takePhoto = async () => { + // Photo Code + + // CHANGE: Add in newPhotos after getPhoto call + const newPhotos = [ + { + filepath: fileName, + webviewPath: photo.webPath, + }, + ...photos, + ]; + setPhotos(newPhotos); + }; + + // CHANGE: Update return statement to include photos. + return { + photos, + takePhoto, + }; +} -```tsx -return { - photos, - takePhoto, -}; +// Old code from before. ``` -And back in the Tab2 component, get access to the photos: +`usePhotoGallery.ts` should now look like this: ```tsx -const { photos, takePhoto } = usePhotoGallery(); +import { useState, useEffect } from 'react'; +import { isPlatform } from '@ionic/react'; +import { Camera, CameraResultType, CameraSource, Photo } from '@capacitor/camera'; +import { Filesystem, Directory } from '@capacitor/filesystem'; +import { Preferences } from '@capacitor/preferences'; +import { Capacitor } from '@capacitor/core'; + +export function usePhotoGallery() { + const [photos, setPhotos] = useState([]); + const fileName = Date.now() + '.jpeg'; + + const takePhoto = async () => { + const photo = await Camera.getPhoto({ + resultType: CameraResultType.Uri, + source: CameraSource.Camera, + quality: 100, + }); + + const newPhotos = [ + { + filepath: fileName, + webviewPath: photo.webPath, + }, + ...photos, + ]; + setPhotos(newPhotos); + }; + + return { + photos, + takePhoto, + }; +} + +export interface UserPhoto { + filepath: string; + webviewPath?: string; +} ``` -With the photo(s) stored into the main array we can display the images on the screen. Add a [Grid component](https://ionicframework.com/docs/api/grid) so that each photo will display nicely as photos are added to the gallery, and loop through each photo in the Photos array, adding an Image component (``) for each. Point the `src` (source) to the photo’s path: +Next, move over to `Tab2.tsx` so we can display the image on the screen. With the photo(s) stored into the main array we can display the images on the screen. Add a [Grid component](https://ionicframework.com/docs/api/grid) so that each photo will display nicely as photos are added to the gallery, and loop through each photo in the Photos array, adding an Image component (``) for each. Point the `src` (source) to the photo’s path: ```tsx - - - - {photos.map((photo, index) => ( - - - - ))} - - - - +// Old code + +// CHANGE: Import usePhotoGallery Hook +import { usePhotoGallery } from '../hooks/usePhotoGallery'; + +const Tab2: React.FC = () => { + // CHANGE: Get access to photos from usePhotoGallery + const { photos, takePhoto } = usePhotoGallery(); + + return ( + + + + Tab 2 + + + + + + + {photos.map((photo, index) => ( + + + + ))} + + + + takePhoto()}> + + + + + + ); +}; ``` Save all files. Within the web browser, click the Camera button and take another photo. This time, the photo is displayed in the Photo Gallery! diff --git a/docs/react/your-first-app/3-saving-photos.md b/docs/react/your-first-app/3-saving-photos.md index eae9a61df7b..e5a4951ed19 100644 --- a/docs/react/your-first-app/3-saving-photos.md +++ b/docs/react/your-first-app/3-saving-photos.md @@ -17,40 +17,55 @@ We will use the `writeFile` method initially, but we will use the others coming Next, create a couple of new functions in `usePhotoGallery`: ```tsx -export function usePhotoGallery() { - const savePicture = async (photo: Photo, fileName: string): Promise => { - const base64Data = await base64FromPath(photo.webPath!); - const savedFile = await Filesystem.writeFile({ - path: fileName, - data: base64Data, - directory: Directory.Data, - }); +import { useState, useEffect } from 'react'; +import { isPlatform } from '@ionic/react'; +import { Camera, CameraResultType, CameraSource, Photo } from '@capacitor/camera'; +import { Filesystem, Directory } from '@capacitor/filesystem'; +import { Preferences } from '@capacitor/preferences'; +import { Capacitor } from '@capacitor/core'; - // Use webPath to display the new image instead of base64 since it's - // already loaded into memory - return { - filepath: fileName, - webviewPath: photo.webPath, +export function usePhotoGallery() { + // Same old code from before. + + // CHANGE: Add in new function to save pictures + const savePicture = async (photo: Photo, fileName: string): Promise => { + const base64Data = await base64FromPath(photo.webPath!); + const savedFile = await Filesystem.writeFile({ + path: fileName, + data: base64Data, + directory: Directory.Data, + }); + + // Use webPath to display the new image instead of base64 since it's + // already loaded into memory + return { + filepath: fileName, + webviewPath: photo.webPath, + }; }; - }; + + // Same old code from before. } +// CHANGE: Add a function that allows the photo to be downloaded from the supplied path export async function base64FromPath(path: string): Promise { - const response = await fetch(path); - const blob = await response.blob(); - return new Promise((resolve, reject) => { - const reader = new FileReader(); - reader.onerror = reject; - reader.onload = () => { - if (typeof reader.result === 'string') { - resolve(reader.result); - } else { - reject('method did not return a string'); - } - }; - reader.readAsDataURL(blob); - }); + const response = await fetch(path); + const blob = await response.blob(); + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onerror = reject; + reader.onload = () => { + if (typeof reader.result === 'string') { + resolve(reader.result); + } else { + reject('method did not return a string'); + } + }; + reader.readAsDataURL(blob); + }); } + +// Old code from before. ``` :::note @@ -64,18 +79,109 @@ Next we use the Capacitor [Filesystem API](https://capacitorjs.com/docs/apis/fil Last, call `savePicture` and pass in the photo object and filename directly underneath the call to `setPhotos` in the `takePhoto` method. Here is the full method: ```tsx -const takePhoto = async () => { - const photo = await Camera.getPhoto({ - resultType: CameraResultType.Uri, - source: CameraSource.Camera, - quality: 100, - }); - - const fileName = Date.now() + '.jpeg'; - const savedFileImage = await savePicture(photo, fileName); - const newPhotos = [savedFileImage, ...photos]; - setPhotos(newPhotos); -}; +// Old code from before. + +export function usePhotoGallery() { + // Old code from before. + + // CHANGE: Update the takePhoto function to utilize capacitor filesystem + const takePhoto = async () => { + const photo = await Camera.getPhoto({ + resultType: CameraResultType.Uri, + source: CameraSource.Camera, + quality: 100, + }); + + const newPhotos = [ + { + filepath: fileName, + webviewPath: photo.webPath, + }, + ...photos, + ]; + setPhotos(newPhotos); + }; + + // Old code from before +} + +// Old code from before. +``` + +`usePhotoGallery.ts` should now look like this: + +```tsx +import { useState, useEffect } from 'react'; +import { isPlatform } from '@ionic/react'; +import { Camera, CameraResultType, CameraSource, Photo } from '@capacitor/camera'; +import { Filesystem, Directory } from '@capacitor/filesystem'; +import { Preferences } from '@capacitor/preferences'; +import { Capacitor } from '@capacitor/core'; + +export function usePhotoGallery() { + const [photos, setPhotos] = useState([]); + const fileName = Date.now() + '.jpeg'; + + const savePicture = async (photo: Photo, fileName: string): Promise => { + const base64Data = await base64FromPath(photo.webPath!); + const savedFile = await Filesystem.writeFile({ + path: fileName, + data: base64Data, + directory: Directory.Data, + }); + + // Use webPath to display the new image instead of base64 since it's + // already loaded into memory + return { + filepath: fileName, + webviewPath: photo.webPath, + }; + }; + + const takePhoto = async () => { + const photo = await Camera.getPhoto({ + resultType: CameraResultType.Uri, + source: CameraSource.Camera, + quality: 100, + }); + + const newPhotos = [ + { + filepath: fileName, + webviewPath: photo.webPath, + }, + ...photos, + ]; + setPhotos(newPhotos); + }; + + return { + photos, + takePhoto, + }; +} + +export async function base64FromPath(path: string): Promise { + const response = await fetch(path); + const blob = await response.blob(); + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onerror = reject; + reader.onload = () => { + if (typeof reader.result === 'string') { + resolve(reader.result); + } else { + reject('method did not return a string'); + } + }; + reader.readAsDataURL(blob); + }); +} + +export interface UserPhoto { + filepath: string; + webviewPath?: string; +} ``` There we go! Each time a new photo is taken, it’s now automatically saved to the filesystem. diff --git a/docs/react/your-first-app/4-loading-photos.md b/docs/react/your-first-app/4-loading-photos.md index 75c1cb8e901..cd177d32a03 100644 --- a/docs/react/your-first-app/4-loading-photos.md +++ b/docs/react/your-first-app/4-loading-photos.md @@ -20,7 +20,10 @@ Fortunately, this is easy: we’ll leverage the Capacitor [Preferences API](http Begin by defining a constant variable that will act as the key for the store before the `usePhotoGallery` function definition in `src/hooks/usePhotoGallery.ts`: ```tsx +// CHANGE: Createa constant variable that will act as a key to store const PHOTO_STORAGE = 'photos'; + +// Old code from before export function usePhotoGallery() {} ``` @@ -29,29 +32,70 @@ Then, use the `Storage` class to get access to the get and set methods for readi At the end of the `takePhoto` function, add a call to `Preferences.set()` to save the Photos array. By adding it here, the Photos array is stored each time a new photo is taken. This way, it doesn’t matter when the app user closes or switches to a different app - all photo data is saved. ```tsx -Preferences.set({ key: PHOTO_STORAGE, value: JSON.stringify(newPhotos) }); +// Old code from before. +export function usePhotoGallery() { + // Old code from before. + + const takePhoto = async () => { + const photo = await Camera.getPhoto({ + resultType: CameraResultType.Uri, + source: CameraSource.Camera, + quality: 100, + }); + + const newPhotos = [ + { + filepath: fileName, + webviewPath: photo.webPath, + }, + ...photos, + ]; + setPhotos(newPhotos); + // CHANGE: Add a call to save the photos array + Preferences.set({ key: PHOTO_STORAGE, value: JSON.stringify(newPhotos) }); + }; + // Old code from before + return { + photos, + takePhoto, + }; +} + +// Old code from before. ``` With the photo array data saved, we will create a method that will retrieve the data when the hook loads. We will do so by using React's `useEffect` hook. Insert this above the `takePhoto` declaration. Here is the code, and we will break it down: ```tsx -useEffect(() => { - const loadSaved = async () => { - const { value } = await Preferences.get({ key: PHOTO_STORAGE }); - const photosInPreferences = (value ? JSON.parse(value) : []) as UserPhoto[]; - - for (let photo of photosInPreferences) { - const file = await Filesystem.readFile({ - path: photo.filepath, - directory: Directory.Data, - }); - // Web platform only: Load the photo as base64 data - photo.webviewPath = `data:image/jpeg;base64,${file.data}`; +// Old code from before. +export function usePhotoGallery() { + // Old code from before. + + // CHANGE: Add useEffect hook + useEffect(() => { + const loadSaved = async () => { + const { value } = await Preferences.get({ key: PHOTO_STORAGE }); + const photosInPreferences = (value ? JSON.parse(value) : []) as UserPhoto[]; + + for (let photo of photosInPreferences) { + const file = await Filesystem.readFile({ + path: photo.filepath, + directory: Directory.Data, + }); + // Web platform only: Load the photo as base64 data + photo.webviewPath = `data:image/jpeg;base64,${file.data}`; + } + setPhotos(photosInPreferences); + }; + loadSaved(); + }, []); + + const takePhotos = async () => { + // Old code from before. } - setPhotos(photosInPreferences); - }; - loadSaved(); -}, []); + +} +// Old code from before. ``` This seems a bit scary at first, so let's walk through it, first by looking at the second parameter we pass into the hook: the dependency array `[]`. @@ -62,4 +106,100 @@ The first parameter to `useEffect` is the function that will be called by the ef On mobile (coming up next!), we can directly set the source of an image tag - `` - to each photo file on the Filesystem, displaying them automatically. On the web, however, we must read each image from the Filesystem into base64 format, because the Filesystem API stores them in base64 within [IndexedDB](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API) under the hood. +`usePhotoGallery.ts` should now look like this: + +```tsx +import { useState, useEffect } from 'react'; +import { isPlatform } from '@ionic/react'; +import { Camera, CameraResultType, CameraSource, Photo } from '@capacitor/camera'; +import { Filesystem, Directory } from '@capacitor/filesystem'; +import { Preferences } from '@capacitor/preferences'; +import { Capacitor } from '@capacitor/core'; + +const PHOTO_STORAGE = 'photos'; + +export function usePhotoGallery() { + const [photos, setPhotos] = useState([]); + const fileName = Date.now() + '.jpeg'; + const savePicture = async (photo: Photo, fileName: string): Promise => { + const base64Data = await base64FromPath(photo.webPath!); + const savedFile = await Filesystem.writeFile({ + path: fileName, + data: base64Data, + directory: Directory.Data, + }); + + // Use webPath to display the new image instead of base64 since it's + // already loaded into memory + return { + filepath: fileName, + webviewPath: photo.webPath, + }; + }; + + useEffect(() => { + const loadSaved = async () => { + const { value } = await Preferences.get({ key: PHOTO_STORAGE }); + const photosInPreferences = (value ? JSON.parse(value) : []) as UserPhoto[]; + + for (let photo of photosInPreferences) { + const file = await Filesystem.readFile({ + path: photo.filepath, + directory: Directory.Data, + }); + // Web platform only: Load the photo as base64 data + photo.webviewPath = `data:image/jpeg;base64,${file.data}`; + } + setPhotos(photosInPreferences); + }; + loadSaved(); + }, []); + + const takePhoto = async () => { + const photo = await Camera.getPhoto({ + resultType: CameraResultType.Uri, + source: CameraSource.Camera, + quality: 100, + }); + + const newPhotos = [ + { + filepath: fileName, + webviewPath: photo.webPath, + }, + ...photos, + ]; + setPhotos(newPhotos); + Preferences.set({ key: PHOTO_STORAGE, value: JSON.stringify(newPhotos) }); + }; + + return { + photos, + takePhoto, + }; +} + +export async function base64FromPath(path: string): Promise { + const response = await fetch(path); + const blob = await response.blob(); + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onerror = reject; + reader.onload = () => { + if (typeof reader.result === 'string') { + resolve(reader.result); + } else { + reject('method did not return a string'); + } + }; + reader.readAsDataURL(blob); + }); +} + +export interface UserPhoto { + filepath: string; + webviewPath?: string; +} +``` + That’s it! We’ve built a complete Photo Gallery feature in our Ionic app that works on the web. Next up, we’ll transform it into a mobile app for iOS and Android! diff --git a/docs/react/your-first-app/5-adding-mobile.md b/docs/react/your-first-app/5-adding-mobile.md index fa1799da9d1..c910f61ce11 100644 --- a/docs/react/your-first-app/5-adding-mobile.md +++ b/docs/react/your-first-app/5-adding-mobile.md @@ -13,61 +13,205 @@ Let’s start with making some small code changes - then our app will “just wo First, we’ll update the photo saving functionality to support mobile. In the `savePicture` function, check which platform the app is running on. If it’s “hybrid” (Capacitor or Cordova, the two native runtimes), then read the photo file into base64 format using the `readFile` method. Also, return the complete file path to the photo using the Filesystem API. When setting the `webviewPath`, use the special `Capacitor.convertFileSrc` method ([details here](https://ionicframework.com/docs/core-concepts/webview#file-protocol)). Otherwise, use the same logic as before when running the app on the web. ```tsx -const savePicture = async (photo: Photo, fileName: string): Promise => { - let base64Data: string | Blob; - // "hybrid" will detect Cordova or Capacitor; - if (isPlatform('hybrid')) { - const file = await Filesystem.readFile({ - path: photo.path!, - }); - base64Data = file.data; - } else { - base64Data = await base64FromPath(photo.webPath!); - } - const savedFile = await Filesystem.writeFile({ - path: fileName, - data: base64Data, - directory: Directory.Data, - }); - - if (isPlatform('hybrid')) { - // Display the new image by rewriting the 'file://' path to HTTP - // Details: https://ionicframework.com/docs/building/webview#file-protocol - return { - filepath: savedFile.uri, - webviewPath: Capacitor.convertFileSrc(savedFile.uri), - }; - } else { - // Use webPath to display the new image instead of base64 since it's - // already loaded into memory - return { - filepath: fileName, - webviewPath: photo.webPath, - }; - } -}; +// Old code from before. +export function usePhotoGallery() { + // Old code from before. + + // CHANGE: Update savePicture function + const savePicture = async (photo: Photo, fileName: string): Promise => { + let base64Data: string | Blob; + // "hybrid" will detect Cordova or Capacitor; + if (isPlatform('hybrid')) { + const file = await Filesystem.readFile({ + path: photo.path!, + }); + base64Data = file.data; + } else { + base64Data = await base64FromPath(photo.webPath!); + } + const savedFile = await Filesystem.writeFile({ + path: fileName, + data: base64Data, + directory: Directory.Data, + }); + + if (isPlatform('hybrid')) { + // Display the new image by rewriting the 'file://' path to HTTP + // Details: https://ionicframework.com/docs/building/webview#file-protocol + return { + filepath: savedFile.uri, + webviewPath: Capacitor.convertFileSrc(savedFile.uri), + }; + } else { + // Use webPath to display the new image instead of base64 since it's + // already loaded into memory + return { + filepath: fileName, + webviewPath: photo.webPath, + }; + } + }; + + // Old code from before. +} + +// Old code from before. ``` Next, add a new bit of logic in the `loadSaved` function. On mobile, we can directly point to each photo file on the Filesystem and display them automatically. On the web, however, we must read each image from the Filesystem into base64 format. This is because the Filesystem API uses [IndexedDB](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API) under the hood. Update the `loadSaved` function inside of `useEffect` to: ```tsx -const loadSaved = async () => { - const { value } = await Preferences.get({ key: PHOTO_STORAGE }); - - const photosInPreferences = (value ? JSON.parse(value) : []) as UserPhoto[]; - // If running on the web... - if (!isPlatform('hybrid')) { - for (let photo of photosInPreferences) { - const file = await Filesystem.readFile({ - path: photo.filepath, - directory: Directory.Data, - }); - // Web platform only: Load the photo as base64 data - photo.webviewPath = `data:image/jpeg;base64,${file.data}`; - } - } - setPhotos(photosInPreferences); -}; +// Old code from before. +export function usePhotoGallery() { + // Old code from before. + + useEffect(() => { + // CHANGE: Update loadSaved function within useEffect + const loadSaved = async () => { + const { value } = await Preferences.get({ key: PHOTO_STORAGE }); + + const photosInPreferences = (value ? JSON.parse(value) : []) as UserPhoto[]; + // If running on the web... + if (!isPlatform('hybrid')) { + for (let photo of photosInPreferences) { + const file = await Filesystem.readFile({ + path: photo.filepath, + directory: Directory.Data, + }); + // Web platform only: Load the photo as base64 data + photo.webviewPath = `data:image/jpeg;base64,${file.data}`; + } + } + setPhotos(photosInPreferences); + }; + + + + }, []); + + // Old code from before. +} + +// Old code from before. ``` Our Photo Gallery now consists of one codebase that runs on the web, Android, and iOS. Next up, the part you’ve been waiting for - deploying the app to a device. + +`usePhotoGallery.ts` should now look like this: + +```tsx +import { useState, useEffect } from 'react'; +import { isPlatform } from '@ionic/react'; +import { Camera, CameraResultType, CameraSource, Photo } from '@capacitor/camera'; +import { Filesystem, Directory } from '@capacitor/filesystem'; +import { Preferences } from '@capacitor/preferences'; +import { Capacitor } from '@capacitor/core'; + +const PHOTO_STORAGE = 'photos'; + +export function usePhotoGallery() { + const [photos, setPhotos] = useState([]); + const fileName = Date.now() + '.jpeg'; + const savePicture = async (photo: Photo, fileName: string): Promise => { + let base64Data: string | Blob; + // "hybrid" will detect Cordova or Capacitor; + if (isPlatform('hybrid')) { + const file = await Filesystem.readFile({ + path: photo.path!, + }); + base64Data = file.data; + } else { + base64Data = await base64FromPath(photo.webPath!); + } + const savedFile = await Filesystem.writeFile({ + path: fileName, + data: base64Data, + directory: Directory.Data, + }); + + if (isPlatform('hybrid')) { + // Display the new image by rewriting the 'file://' path to HTTP + // Details: https://ionicframework.com/docs/building/webview#file-protocol + return { + filepath: savedFile.uri, + webviewPath: Capacitor.convertFileSrc(savedFile.uri), + }; + } else { + // Use webPath to display the new image instead of base64 since it's + // already loaded into memory + return { + filepath: fileName, + webviewPath: photo.webPath, + }; + } + }; + + useEffect(() => { + const loadSaved = async () => { + const { value } = await Preferences.get({ key: PHOTO_STORAGE }); + + const photosInPreferences = (value ? JSON.parse(value) : []) as UserPhoto[]; + // If running on the web... + if (!isPlatform('hybrid')) { + for (let photo of photosInPreferences) { + const file = await Filesystem.readFile({ + path: photo.filepath, + directory: Directory.Data, + }); + // Web platform only: Load the photo as base64 data + photo.webviewPath = `data:image/jpeg;base64,${file.data}`; + } + } + setPhotos(photosInPreferences); + }; + + + + }, []); + + const takePhoto = async () => { + const photo = await Camera.getPhoto({ + resultType: CameraResultType.Uri, + source: CameraSource.Camera, + quality: 100, + }); + + const newPhotos = [ + { + filepath: fileName, + webviewPath: photo.webPath, + }, + ...photos, + ]; + setPhotos(newPhotos); + Preferences.set({ key: PHOTO_STORAGE, value: JSON.stringify(newPhotos) }); + }; + + return { + photos, + takePhoto, + }; +} + +export async function base64FromPath(path: string): Promise { + const response = await fetch(path); + const blob = await response.blob(); + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onerror = reject; + reader.onload = () => { + if (typeof reader.result === 'string') { + resolve(reader.result); + } else { + reject('method did not return a string'); + } + }; + reader.readAsDataURL(blob); + }); +} + +export interface UserPhoto { + filepath: string; + webviewPath?: string; +} +``` diff --git a/docs/react/your-first-app/7-live-reload.md b/docs/react/your-first-app/7-live-reload.md index 023da7a8844..2b65ba560bc 100644 --- a/docs/react/your-first-app/7-live-reload.md +++ b/docs/react/your-first-app/7-live-reload.md @@ -29,27 +29,80 @@ The Live Reload server will start up, and the native IDE of choice will open if With Live Reload running and the app is open on your device, let’s implement photo deletion functionality. Open `Tab2.tsx` then import `useState` from React and `UserPhoto` from the `usePhotoGallery` hook: ```tsx +// Other Imports + +// CHANGE: Import UserPhoto, usePhotoGallery hook and useState from react. import React, { useState } from 'react'; import { usePhotoGallery, UserPhoto } from '../hooks/usePhotoGallery'; -// other imports + +const Tab2: React.FC = () => { +}; ``` Next, reference the `deletePhoto` function, which we'll create soon: ```tsx -const { photos, takePhoto, deletePhoto } = usePhotoGallery(); +// Same old code from before. + +const Tab2: React.FC = () => { + // CHANGE: Reference deletePhoto function + const { photos, takePhoto, deletePhoto } = usePhotoGallery(); + + // Same old code from before. +}; + ``` Next, add a state value to store information about the photo to delete: ```tsx -const [photoToDelete, setPhotoToDelete] = useState(); +// Same old code from before. + +const Tab2: React.FC = () => { + // Same old code from before. + + // CHANGE: Add a state value for photo deletion. + const [photoToDelete, setPhotoToDelete] = useState(); + + // Same old code from before. +}; ``` When a user clicks on an image, we will show the action sheet by changing the state value to the photo. Update the `` element to: ```tsx - setPhotoToDelete(photo)} src={photo.webviewPath} /> +// Same old code from before. + +const Tab2: React.FC = () => { + // Same old code from before. + + return ( + + + + Tab 2 + + + + + + {photos.map((photo, index) => ( + + + setPhotoToDelete(photo)} src={photo.webviewPath} /> + + ))} + + + + takePhoto()}> + + + + + + ); +}; ``` Next, add an [IonActionSheet](https://ionicframework.com/docs/api/action-sheet) dialog with the option to either delete the selected photo or cancel (close) the dialog. We will set the isOpen property based on if photoToDelete has a value or not. @@ -57,28 +110,60 @@ Next, add an [IonActionSheet](https://ionicframework.com/docs/api/action-sheet) In the JSX, put the following component before the closing `` tag. ```tsx - { - if (photoToDelete) { - deletePhoto(photoToDelete); - setPhotoToDelete(undefined); - } - }, - }, - { - text: 'Cancel', - icon: close, - role: 'cancel', - }, - ]} - onDidDismiss={() => setPhotoToDelete(undefined)} -/> +// Same old code from before. + +const Tab2: React.FC = () => { + // Same old code from before. + + return ( + + + + Tab 2 + + + + + + {photos.map((photo, index) => ( + + setPhotoToDelete(photo)} src={photo.webviewPath} /> + + ))} + + + + takePhoto()}> + + + + + { + if (photoToDelete) { + deletePhoto(photoToDelete); + setPhotoToDelete(undefined); + } + }, + }, + { + text: 'Cancel', + icon: close, + role: 'cancel', + }, + ]} + onDidDismiss={() => setPhotoToDelete(undefined)} + /> + + + ); +}; ``` Above, we added two options: `Delete` that calls `deletePhoto` function (to be added next) and `Cancel`, which when given the role of “cancel” will automatically close the action sheet. It's also important to set the onDidDismiss function and set our photoToDelete back to undefined when the modal goes away. That way, when another image is clicked, the action sheet notices the change in the value of photoToDelete. @@ -86,35 +171,254 @@ Above, we added two options: `Delete` that calls `deletePhoto` function (to be a Next, we need to implement the deletePhoto method that will come from the `usePhotoGallery` hook. Open the file and paste in the following function in the hook: ```tsx -const deletePhoto = async (photo: UserPhoto) => { - // Remove this photo from the Photos reference data array - const newPhotos = photos.filter((p) => p.filepath !== photo.filepath); - - // Update photos array cache by overwriting the existing photo array - Preferences.set({ key: PHOTO_STORAGE, value: JSON.stringify(newPhotos) }); - - // delete photo file from filesystem - const filename = photo.filepath.substr(photo.filepath.lastIndexOf('/') + 1); - await Filesystem.deleteFile({ - path: filename, - directory: Directory.Data, - }); - setPhotos(newPhotos); -}; +// Same old code from before. + +export function usePhotoGallery() { + // Same old code from before. + + // CHANGE: Implement deletePhoto method within usePhotoGallery hook. + const deletePhoto = async (photo: UserPhoto) => { + // Remove this photo from the Photos reference data array + const newPhotos = photos.filter((p) => p.filepath !== photo.filepath); + + // Update photos array cache by overwriting the existing photo array + Preferences.set({ key: PHOTO_STORAGE, value: JSON.stringify(newPhotos) }); + + // delete photo file from filesystem + const filename = photo.filepath.substr(photo.filepath.lastIndexOf('/') + 1); + await Filesystem.deleteFile({ + path: filename, + directory: Directory.Data, + }); + setPhotos(newPhotos); + }; + + // CHANGE: Update return and add deletePhoto function + return { + photos, + takePhoto, + deletePhoto, + }; +} + +// Same old code from before. ``` The selected photo is removed from the Photos array first. Then, we use the Capacitor Preferences API to update the cached version of the Photos array. Finally, we delete the actual photo file itself using the Filesystem API. -Make sure to return the `deletePhoto` function so it is as a part of the hook API that we expose: +Save this file, then tap on a photo again and choose the “Delete” option. This time, the photo is deleted! Implemented much faster using Live Reload. 💪 + +In the final portion of this tutorial, we’ll walk you through the basics of the Appflow product used to build and deploy your application to users' devices. + + +`Tab2.tsx` should look like this: ```tsx -return { - photos, - takePhoto, - deletePhoto, +import { camera, trash, close } from 'ionicons/icons'; +import { + IonContent, + IonHeader, + IonPage, + IonTitle, + IonToolbar, + IonFab, + IonFabButton, + IonIcon, + IonGrid, + IonRow, + IonCol, + IonImg, + IonActionSheet, +} from '@ionic/react'; +import ExploreContainer from '../components/ExploreContainer'; +import './Tab2.css'; +import { usePhotoGallery, UserPhoto } from '../hooks/usePhotoGallery'; + +const Tab2: React.FC = () => { + const { photos, takePhoto, deletePhoto } = usePhotoGallery(); + const [photoToDelete, setPhotoToDelete] = useState(); + + return ( + + + + Tab 2 + + + + + + {photos.map((photo, index) => ( + + setPhotoToDelete(photo)} src={photo.webviewPath} /> + + ))} + + + + takePhoto()}> + + + + { + if (photoToDelete) { + deletePhoto(photoToDelete); + setPhotoToDelete(undefined); + } + }, + }, + { + text: 'Cancel', + icon: close, + role: 'cancel', + }, + ]} + onDidDismiss={() => setPhotoToDelete(undefined)} + /> + + + ); }; + +export default Tab2; ``` -Save this file, then tap on a photo again and choose the “Delete” option. This time, the photo is deleted! Implemented much faster using Live Reload. 💪 -In the final portion of this tutorial, we’ll walk you through the basics of the Appflow product used to build and deploy your application to users' devices. +`usePhotoGallery.ts` should look like this: + +```tsx +import { useState, useEffect } from 'react'; +import { isPlatform } from '@ionic/react'; +import { Camera, CameraResultType, CameraSource, Photo } from '@capacitor/camera'; +import { Filesystem, Directory } from '@capacitor/filesystem'; +import { Preferences } from '@capacitor/preferences'; +import { Capacitor } from '@capacitor/core'; + + +const PHOTO_STORAGE = 'photos'; + +export function usePhotoGallery() { + const [photos, setPhotos ] = useState([]); + + const savePicture = async (photo: Photo, fileName: string): Promise => { + let base64Data: string | Blob; + // "hybrid" will detect Cordova or Capacitor: + if (isPlatform('hybrid')) { + const file = await Filesystem.readFile({ + path: photo.path!, + }); + base64Data = file.data; + } else { + base64Data = await base64FromPath(photo.webPath!); + } + + + const savedFile = await Filesystem.writeFile({ + path: fileName, + data: base64Data, + directory: Directory.Data, + }); + + if (isPlatform('hybrid')) { + // Display the new image by rewriting the 'file://' path to HTTP + // Details: https://ionicframework.com/docs/building/webview#file-protocol + return { + filepath: savedFile.uri, + webviewPath: Capacitor.convertFileSrc(savedFile.uri), + }; + } else { + // Use webPath to display the new image instead of base64 since it's + // already loaded into memory + return { + filepath: fileName, + webviewPath: photo.webPath, + }; + } + } + + useEffect(() => { + const loadSaved = async() => { + const { value } = await Preferences.get({ key: PHOTO_STORAGE }); + const photosInPreferences = (value? JSON.parse(value) : []) as UserPhoto[]; + + if (!isPlatform('hybrid')) { + for (let photo of photosInPreferences) { + const file = await Filesystem.readFile({ + path: photo.filepath, + directory: Directory.Data, + }); + // Web platform only: Load the photo as base64 data + photo.webviewPath = `data:image/jpeg;base64,${file.data}`; + } + } + setPhotos(photosInPreferences); + }; + loadSaved(); + }, []); + + const takePhoto = async () => { + const photo = await Camera.getPhoto({ + resultType: CameraResultType.Uri, + source: CameraSource.Camera, + quality: 100, + }); + + const fileName = Date.now() + '.jpeg'; + const savedFileImage = await savePicture(photo, fileName); + const newPhotos = [savedFileImage, ...photos]; + setPhotos(newPhotos); + Preferences.set({ key: PHOTO_STORAGE, value: JSON.stringify(newPhotos) }); + }; + + const deletePhoto = async (photo: UserPhoto) => { + // Remove this photo from the Photos reference data array + const newPhotos = photos.filter((p) => p.filepath !== photo.filepath); + + // Update photos array cache by overwriting the existing photo array + Preferences.set({ key: PHOTO_STORAGE, value: JSON.stringify(newPhotos) }); + + // delete photo file from filesystem + const filename = photo.filepath.substr(photo.filepath.lastIndexOf('/') + 1); + await Filesystem.deleteFile({ + path: filename, + directory: Directory.Data, + }); + setPhotos(newPhotos); + }; + + return { + photos, + takePhoto, + deletePhoto, + }; +} + +export async function base64FromPath(path: string): Promise { + const response = await fetch(path); + const blob = await response.blob(); + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onerror = reject; + reader.onload = () => { + if (typeof reader.result === 'string') { + resolve(reader.result); + } else { + reject('method did not return a string'); + } + }; + reader.readAsDataURL(blob); + }); +} + +export interface UserPhoto { + filepath: string; + webviewPath?: string; +} +``` \ No newline at end of file From 4ff6c316bbd9cabb10fce06076d962c7675afeaf Mon Sep 17 00:00:00 2001 From: joesphchang Date: Thu, 17 Jul 2025 10:59:35 -0500 Subject: [PATCH 04/16] docs(react): ran npm run lint and fixed spelling in docs --- docs/react/your-first-app.md | 6 +- docs/react/your-first-app/2-taking-photos.md | 182 +++++----- docs/react/your-first-app/3-saving-photos.md | 218 ++++++------ docs/react/your-first-app/4-loading-photos.md | 242 +++++++------- docs/react/your-first-app/5-adding-mobile.md | 310 +++++++++--------- docs/react/your-first-app/7-live-reload.md | 284 ++++++++-------- 6 files changed, 610 insertions(+), 632 deletions(-) diff --git a/docs/react/your-first-app.md b/docs/react/your-first-app.md index 311e7bd3728..5a147cad92c 100644 --- a/docs/react/your-first-app.md +++ b/docs/react/your-first-app.md @@ -108,7 +108,7 @@ import App from './App'; // CHANGE: Add the following import. import { defineCustomElements } from '@ionic/pwa-elements/loader'; -// Call the element loader before the render call +// CHANGE: Call the element loader before the render call defineCustomElements(window); const container = document.getElementById('root'); @@ -159,7 +159,7 @@ Open `/src/pages/Tab2.tsx`. We see: Photo Gallery ``` -We put the visual aspects of our app into . In this case, it’s where we’ll add a button that opens the device’s camera as well as displays the image captured by the camera. Start by adding a [floating action button](https://ionicframework.com/docs/api/fab) (FAB) to the bottom of the page and set the camera image as the icon. +We put the visual aspects of our app into ``. In this case, it’s where we’ll add a button that opens the device’s camera as well as displays the image captured by the camera. Start by adding a [floating action button](https://ionicframework.com/docs/api/fab) (FAB) to the bottom of the page and set the camera image as the icon. ```tsx // CHANGE: Add the following import. @@ -216,7 +216,7 @@ import { images, square, triangle } from 'ionicons/icons'; Within the tab bar (``), change the label to “Photos” and the `ellipse` icon to `images` for the middle tab button: ```tsx -// Keep other imports +// Keep other imports // CHANGE: Add the following import. import { images, square, triangle } from 'ionicons/icons'; diff --git a/docs/react/your-first-app/2-taking-photos.md b/docs/react/your-first-app/2-taking-photos.md index 68270ff90c6..e0b3eafd32e 100644 --- a/docs/react/your-first-app/2-taking-photos.md +++ b/docs/react/your-first-app/2-taking-photos.md @@ -29,12 +29,7 @@ import { useState, useEffect } from 'react'; import { isPlatform } from '@ionic/react'; // CHANGE: Add the following imports -import { - Camera, - CameraResultType, - CameraSource, - Photo, -} from '@capacitor/camera'; +import { Camera, CameraResultType, CameraSource, Photo } from '@capacitor/camera'; import { Filesystem, Directory } from '@capacitor/filesystem'; import { Preferences } from '@capacitor/preferences'; import { Capacitor } from '@capacitor/core'; @@ -48,30 +43,25 @@ Next, create a function named usePhotoGallery: import { useState, useEffect } from 'react'; import { isPlatform } from '@ionic/react'; -import { - Camera, - CameraResultType, - CameraSource, - Photo, -} from '@capacitor/camera'; +import { Camera, CameraResultType, CameraSource, Photo } from '@capacitor/camera'; import { Filesystem, Directory } from '@capacitor/filesystem'; import { Preferences } from '@capacitor/preferences'; import { Capacitor } from '@capacitor/core'; export function usePhotoGallery() { - // CHANGE: ADd the usePhotoGallery function. - const takePhoto = async () => { - // Take a photo - const photo = await Camera.getPhoto({ - resultType: CameraResultType.Uri, - source: CameraSource.Camera, - quality: 100, - }); - }; - - return { - takePhoto, - }; + // CHANGE: Add the usePhotoGallery function. + const takePhoto = async () => { + // Take a photo + const photo = await Camera.getPhoto({ + resultType: CameraResultType.Uri, + source: CameraSource.Camera, + quality: 100, + }); + }; + + return { + takePhoto, + }; } ``` @@ -88,25 +78,25 @@ The last step we need to take is to use the new hook from the Tab2 page. Go back import { usePhotoGallery } from '../hooks/usePhotoGallery'; const Tab2: React.FC = () => { - // CHANGE: Get access to `takePhoto` method by using the hook - const { takePhoto } = usePhotoGallery(); - - return ( - - - - Tab 2 - - - - - takePhoto()}> - - - - - - ); + // CHANGE: Get access to `takePhoto` method by using the hook + const { takePhoto } = usePhotoGallery(); + + return ( + + + + Tab 2 + + + + + takePhoto()}> + + + + + + ); }; export default Tab2; @@ -126,7 +116,7 @@ First we will create a new type to define our Photo, which will hold specific me ```tsx export functino usePhotoGallery { - // Old code from before. + // Same old code from before. } // CHANGE: Add the interface. @@ -143,42 +133,42 @@ export function usePhotoGallery { // CHANGE: Add the photos array. const [photos, setPhotos] = useState([]); - // Old code from before. + // Same old code from before. } ``` When the camera is done taking a picture, the resulting Photo returned from Capacitor will be stored in the `photo` variable. We want to create a new photo object and add it to the photos state array. We make sure we don't accidentally mutate the current photos array by making a new array, and then call `setPhotos` to store the array into state. Update the `takePhoto` method and add this code after the getPhoto call: ```tsx -// Old code from before. +// Same old code from before. export function usePhotoGallery() { - const [photos, setPhotos] = useState([]); - // CHANGE: Create new fileName variable with date and .jpeg - const fileName = Date.now() + '.jpeg'; - - const takePhoto = async () => { - // Photo Code - - // CHANGE: Add in newPhotos after getPhoto call - const newPhotos = [ - { - filepath: fileName, - webviewPath: photo.webPath, - }, - ...photos, - ]; - setPhotos(newPhotos); - }; - - // CHANGE: Update return statement to include photos. - return { - photos, - takePhoto, - }; + const [photos, setPhotos] = useState([]); + // CHANGE: Create new fileName variable with date and .jpeg + const fileName = Date.now() + '.jpeg'; + + const takePhoto = async () => { + // Same old code from before. + + // CHANGE: Add in newPhotos after getPhoto call + const newPhotos = [ + { + filepath: fileName, + webviewPath: photo.webPath, + }, + ...photos, + ]; + setPhotos(newPhotos); + }; + + // CHANGE: Update return statement to include photos. + return { + photos, + takePhoto, + }; } -// Old code from before. +// Same old code from before. ``` `usePhotoGallery.ts` should now look like this: @@ -192,30 +182,30 @@ import { Preferences } from '@capacitor/preferences'; import { Capacitor } from '@capacitor/core'; export function usePhotoGallery() { - const [photos, setPhotos] = useState([]); - const fileName = Date.now() + '.jpeg'; - - const takePhoto = async () => { - const photo = await Camera.getPhoto({ - resultType: CameraResultType.Uri, - source: CameraSource.Camera, - quality: 100, - }); - - const newPhotos = [ - { - filepath: fileName, - webviewPath: photo.webPath, - }, - ...photos, - ]; - setPhotos(newPhotos); - }; - - return { - photos, - takePhoto, - }; + const [photos, setPhotos] = useState([]); + const fileName = Date.now() + '.jpeg'; + + const takePhoto = async () => { + const photo = await Camera.getPhoto({ + resultType: CameraResultType.Uri, + source: CameraSource.Camera, + quality: 100, + }); + + const newPhotos = [ + { + filepath: fileName, + webviewPath: photo.webPath, + }, + ...photos, + ]; + setPhotos(newPhotos); + }; + + return { + photos, + takePhoto, + }; } export interface UserPhoto { @@ -227,7 +217,7 @@ export interface UserPhoto { Next, move over to `Tab2.tsx` so we can display the image on the screen. With the photo(s) stored into the main array we can display the images on the screen. Add a [Grid component](https://ionicframework.com/docs/api/grid) so that each photo will display nicely as photos are added to the gallery, and loop through each photo in the Photos array, adding an Image component (``) for each. Point the `src` (source) to the photo’s path: ```tsx -// Old code +// Same old code from before. // CHANGE: Import usePhotoGallery Hook import { usePhotoGallery } from '../hooks/usePhotoGallery'; diff --git a/docs/react/your-first-app/3-saving-photos.md b/docs/react/your-first-app/3-saving-photos.md index e5a4951ed19..aa54812590a 100644 --- a/docs/react/your-first-app/3-saving-photos.md +++ b/docs/react/your-first-app/3-saving-photos.md @@ -25,47 +25,47 @@ import { Preferences } from '@capacitor/preferences'; import { Capacitor } from '@capacitor/core'; export function usePhotoGallery() { - // Same old code from before. - - // CHANGE: Add in new function to save pictures - const savePicture = async (photo: Photo, fileName: string): Promise => { - const base64Data = await base64FromPath(photo.webPath!); - const savedFile = await Filesystem.writeFile({ - path: fileName, - data: base64Data, - directory: Directory.Data, - }); - - // Use webPath to display the new image instead of base64 since it's - // already loaded into memory - return { - filepath: fileName, - webviewPath: photo.webPath, - }; + // Same old code from before. + + // CHANGE: Add in new function to save pictures + const savePicture = async (photo: Photo, fileName: string): Promise => { + const base64Data = await base64FromPath(photo.webPath!); + const savedFile = await Filesystem.writeFile({ + path: fileName, + data: base64Data, + directory: Directory.Data, + }); + + // Use webPath to display the new image instead of base64 since it's + // already loaded into memory + return { + filepath: fileName, + webviewPath: photo.webPath, }; + }; - // Same old code from before. + // Same old code from before. } // CHANGE: Add a function that allows the photo to be downloaded from the supplied path export async function base64FromPath(path: string): Promise { - const response = await fetch(path); - const blob = await response.blob(); - return new Promise((resolve, reject) => { - const reader = new FileReader(); - reader.onerror = reject; - reader.onload = () => { - if (typeof reader.result === 'string') { - resolve(reader.result); - } else { - reject('method did not return a string'); - } - }; - reader.readAsDataURL(blob); - }); + const response = await fetch(path); + const blob = await response.blob(); + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onerror = reject; + reader.onload = () => { + if (typeof reader.result === 'string') { + resolve(reader.result); + } else { + reject('method did not return a string'); + } + }; + reader.readAsDataURL(blob); + }); } -// Old code from before. +// Same old code from before. ``` :::note @@ -79,33 +79,33 @@ Next we use the Capacitor [Filesystem API](https://capacitorjs.com/docs/apis/fil Last, call `savePicture` and pass in the photo object and filename directly underneath the call to `setPhotos` in the `takePhoto` method. Here is the full method: ```tsx -// Old code from before. +// Same old code from before. export function usePhotoGallery() { - // Old code from before. - - // CHANGE: Update the takePhoto function to utilize capacitor filesystem - const takePhoto = async () => { - const photo = await Camera.getPhoto({ - resultType: CameraResultType.Uri, - source: CameraSource.Camera, - quality: 100, - }); - - const newPhotos = [ - { - filepath: fileName, - webviewPath: photo.webPath, - }, - ...photos, - ]; - setPhotos(newPhotos); - }; - - // Old code from before + // Same old code from before. + + // CHANGE: Update the takePhoto function to utilize capacitor filesystem + const takePhoto = async () => { + const photo = await Camera.getPhoto({ + resultType: CameraResultType.Uri, + source: CameraSource.Camera, + quality: 100, + }); + + const newPhotos = [ + { + filepath: fileName, + webviewPath: photo.webPath, + }, + ...photos, + ]; + setPhotos(newPhotos); + }; + + // Same old code from before } -// Old code from before. +// Same old code from before. ``` `usePhotoGallery.ts` should now look like this: @@ -119,63 +119,63 @@ import { Preferences } from '@capacitor/preferences'; import { Capacitor } from '@capacitor/core'; export function usePhotoGallery() { - const [photos, setPhotos] = useState([]); - const fileName = Date.now() + '.jpeg'; - - const savePicture = async (photo: Photo, fileName: string): Promise => { - const base64Data = await base64FromPath(photo.webPath!); - const savedFile = await Filesystem.writeFile({ - path: fileName, - data: base64Data, - directory: Directory.Data, - }); - - // Use webPath to display the new image instead of base64 since it's - // already loaded into memory - return { - filepath: fileName, - webviewPath: photo.webPath, - }; - }; - - const takePhoto = async () => { - const photo = await Camera.getPhoto({ - resultType: CameraResultType.Uri, - source: CameraSource.Camera, - quality: 100, - }); - - const newPhotos = [ - { - filepath: fileName, - webviewPath: photo.webPath, - }, - ...photos, - ]; - setPhotos(newPhotos); - }; - - return { - photos, - takePhoto, + const [photos, setPhotos] = useState([]); + const fileName = Date.now() + '.jpeg'; + + const savePicture = async (photo: Photo, fileName: string): Promise => { + const base64Data = await base64FromPath(photo.webPath!); + const savedFile = await Filesystem.writeFile({ + path: fileName, + data: base64Data, + directory: Directory.Data, + }); + + // Use webPath to display the new image instead of base64 since it's + // already loaded into memory + return { + filepath: fileName, + webviewPath: photo.webPath, }; + }; + + const takePhoto = async () => { + const photo = await Camera.getPhoto({ + resultType: CameraResultType.Uri, + source: CameraSource.Camera, + quality: 100, + }); + + const newPhotos = [ + { + filepath: fileName, + webviewPath: photo.webPath, + }, + ...photos, + ]; + setPhotos(newPhotos); + }; + + return { + photos, + takePhoto, + }; } export async function base64FromPath(path: string): Promise { - const response = await fetch(path); - const blob = await response.blob(); - return new Promise((resolve, reject) => { - const reader = new FileReader(); - reader.onerror = reject; - reader.onload = () => { - if (typeof reader.result === 'string') { - resolve(reader.result); - } else { - reject('method did not return a string'); - } - }; - reader.readAsDataURL(blob); - }); + const response = await fetch(path); + const blob = await response.blob(); + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onerror = reject; + reader.onload = () => { + if (typeof reader.result === 'string') { + resolve(reader.result); + } else { + reject('method did not return a string'); + } + }; + reader.readAsDataURL(blob); + }); } export interface UserPhoto { diff --git a/docs/react/your-first-app/4-loading-photos.md b/docs/react/your-first-app/4-loading-photos.md index cd177d32a03..4ef6ef6284c 100644 --- a/docs/react/your-first-app/4-loading-photos.md +++ b/docs/react/your-first-app/4-loading-photos.md @@ -20,10 +20,10 @@ Fortunately, this is easy: we’ll leverage the Capacitor [Preferences API](http Begin by defining a constant variable that will act as the key for the store before the `usePhotoGallery` function definition in `src/hooks/usePhotoGallery.ts`: ```tsx -// CHANGE: Createa constant variable that will act as a key to store +// CHANGE: Create a constant variable that will act as a key to store const PHOTO_STORAGE = 'photos'; -// Old code from before +// Same old code from before export function usePhotoGallery() {} ``` @@ -32,70 +32,70 @@ Then, use the `Storage` class to get access to the get and set methods for readi At the end of the `takePhoto` function, add a call to `Preferences.set()` to save the Photos array. By adding it here, the Photos array is stored each time a new photo is taken. This way, it doesn’t matter when the app user closes or switches to a different app - all photo data is saved. ```tsx -// Old code from before. +// Same old code from before. export function usePhotoGallery() { - // Old code from before. + // Same old code from before. - const takePhoto = async () => { - const photo = await Camera.getPhoto({ - resultType: CameraResultType.Uri, - source: CameraSource.Camera, - quality: 100, - }); + const takePhoto = async () => { + const photo = await Camera.getPhoto({ + resultType: CameraResultType.Uri, + source: CameraSource.Camera, + quality: 100, + }); - const newPhotos = [ - { - filepath: fileName, - webviewPath: photo.webPath, - }, - ...photos, - ]; - setPhotos(newPhotos); - // CHANGE: Add a call to save the photos array - Preferences.set({ key: PHOTO_STORAGE, value: JSON.stringify(newPhotos) }); - }; - // Old code from before - return { - photos, - takePhoto, - }; + const newPhotos = [ + { + filepath: fileName, + webviewPath: photo.webPath, + }, + ...photos, + ]; + setPhotos(newPhotos); + // CHANGE: Add a call to save the photos array + Preferences.set({ key: PHOTO_STORAGE, value: JSON.stringify(newPhotos) }); + }; + // Same old code from before + return { + photos, + takePhoto, + }; } -// Old code from before. +// Same old code from before. ``` With the photo array data saved, we will create a method that will retrieve the data when the hook loads. We will do so by using React's `useEffect` hook. Insert this above the `takePhoto` declaration. Here is the code, and we will break it down: ```tsx -// Old code from before. +// Same old code from before. export function usePhotoGallery() { - // Old code from before. - - // CHANGE: Add useEffect hook - useEffect(() => { - const loadSaved = async () => { - const { value } = await Preferences.get({ key: PHOTO_STORAGE }); - const photosInPreferences = (value ? JSON.parse(value) : []) as UserPhoto[]; - - for (let photo of photosInPreferences) { - const file = await Filesystem.readFile({ - path: photo.filepath, - directory: Directory.Data, - }); - // Web platform only: Load the photo as base64 data - photo.webviewPath = `data:image/jpeg;base64,${file.data}`; - } - setPhotos(photosInPreferences); - }; - loadSaved(); - }, []); - - const takePhotos = async () => { - // Old code from before. - } + // Same old code from before. + + // CHANGE: Add useEffect hook + useEffect(() => { + const loadSaved = async () => { + const { value } = await Preferences.get({ key: PHOTO_STORAGE }); + const photosInPreferences = (value ? JSON.parse(value) : []) as UserPhoto[]; + for (let photo of photosInPreferences) { + const file = await Filesystem.readFile({ + path: photo.filepath, + directory: Directory.Data, + }); + // Web platform only: Load the photo as base64 data + photo.webviewPath = `data:image/jpeg;base64,${file.data}`; + } + setPhotos(photosInPreferences); + }; + loadSaved(); + }, []); + + const takePhotos = async () => { + // Same old code from before. + }; } -// Old code from before. + +// Same old code from before. ``` This seems a bit scary at first, so let's walk through it, first by looking at the second parameter we pass into the hook: the dependency array `[]`. @@ -119,81 +119,81 @@ import { Capacitor } from '@capacitor/core'; const PHOTO_STORAGE = 'photos'; export function usePhotoGallery() { - const [photos, setPhotos] = useState([]); - const fileName = Date.now() + '.jpeg'; - const savePicture = async (photo: Photo, fileName: string): Promise => { - const base64Data = await base64FromPath(photo.webPath!); - const savedFile = await Filesystem.writeFile({ - path: fileName, - data: base64Data, - directory: Directory.Data, - }); - - // Use webPath to display the new image instead of base64 since it's - // already loaded into memory - return { - filepath: fileName, - webviewPath: photo.webPath, - }; + const [photos, setPhotos] = useState([]); + const fileName = Date.now() + '.jpeg'; + const savePicture = async (photo: Photo, fileName: string): Promise => { + const base64Data = await base64FromPath(photo.webPath!); + const savedFile = await Filesystem.writeFile({ + path: fileName, + data: base64Data, + directory: Directory.Data, + }); + + // Use webPath to display the new image instead of base64 since it's + // already loaded into memory + return { + filepath: fileName, + webviewPath: photo.webPath, }; + }; - useEffect(() => { - const loadSaved = async () => { - const { value } = await Preferences.get({ key: PHOTO_STORAGE }); - const photosInPreferences = (value ? JSON.parse(value) : []) as UserPhoto[]; - - for (let photo of photosInPreferences) { - const file = await Filesystem.readFile({ - path: photo.filepath, - directory: Directory.Data, - }); - // Web platform only: Load the photo as base64 data - photo.webviewPath = `data:image/jpeg;base64,${file.data}`; - } - setPhotos(photosInPreferences); - }; - loadSaved(); - }, []); - - const takePhoto = async () => { - const photo = await Camera.getPhoto({ - resultType: CameraResultType.Uri, - source: CameraSource.Camera, - quality: 100, - }); + useEffect(() => { + const loadSaved = async () => { + const { value } = await Preferences.get({ key: PHOTO_STORAGE }); + const photosInPreferences = (value ? JSON.parse(value) : []) as UserPhoto[]; - const newPhotos = [ - { - filepath: fileName, - webviewPath: photo.webPath, - }, - ...photos, - ]; - setPhotos(newPhotos); - Preferences.set({ key: PHOTO_STORAGE, value: JSON.stringify(newPhotos) }); - }; - - return { - photos, - takePhoto, + for (let photo of photosInPreferences) { + const file = await Filesystem.readFile({ + path: photo.filepath, + directory: Directory.Data, + }); + // Web platform only: Load the photo as base64 data + photo.webviewPath = `data:image/jpeg;base64,${file.data}`; + } + setPhotos(photosInPreferences); }; + loadSaved(); + }, []); + + const takePhoto = async () => { + const photo = await Camera.getPhoto({ + resultType: CameraResultType.Uri, + source: CameraSource.Camera, + quality: 100, + }); + + const newPhotos = [ + { + filepath: fileName, + webviewPath: photo.webPath, + }, + ...photos, + ]; + setPhotos(newPhotos); + Preferences.set({ key: PHOTO_STORAGE, value: JSON.stringify(newPhotos) }); + }; + + return { + photos, + takePhoto, + }; } export async function base64FromPath(path: string): Promise { - const response = await fetch(path); - const blob = await response.blob(); - return new Promise((resolve, reject) => { - const reader = new FileReader(); - reader.onerror = reject; - reader.onload = () => { - if (typeof reader.result === 'string') { - resolve(reader.result); - } else { - reject('method did not return a string'); - } - }; - reader.readAsDataURL(blob); - }); + const response = await fetch(path); + const blob = await response.blob(); + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onerror = reject; + reader.onload = () => { + if (typeof reader.result === 'string') { + resolve(reader.result); + } else { + reject('method did not return a string'); + } + }; + reader.readAsDataURL(blob); + }); } export interface UserPhoto { diff --git a/docs/react/your-first-app/5-adding-mobile.md b/docs/react/your-first-app/5-adding-mobile.md index c910f61ce11..07a0e120329 100644 --- a/docs/react/your-first-app/5-adding-mobile.md +++ b/docs/react/your-first-app/5-adding-mobile.md @@ -13,86 +13,83 @@ Let’s start with making some small code changes - then our app will “just wo First, we’ll update the photo saving functionality to support mobile. In the `savePicture` function, check which platform the app is running on. If it’s “hybrid” (Capacitor or Cordova, the two native runtimes), then read the photo file into base64 format using the `readFile` method. Also, return the complete file path to the photo using the Filesystem API. When setting the `webviewPath`, use the special `Capacitor.convertFileSrc` method ([details here](https://ionicframework.com/docs/core-concepts/webview#file-protocol)). Otherwise, use the same logic as before when running the app on the web. ```tsx -// Old code from before. +// Same old code from before. export function usePhotoGallery() { - // Old code from before. + // Same old code from before. + + // CHANGE: Update savePicture function + const savePicture = async (photo: Photo, fileName: string): Promise => { + let base64Data: string | Blob; + // "hybrid" will detect Cordova or Capacitor; + if (isPlatform('hybrid')) { + const file = await Filesystem.readFile({ + path: photo.path!, + }); + base64Data = file.data; + } else { + base64Data = await base64FromPath(photo.webPath!); + } + const savedFile = await Filesystem.writeFile({ + path: fileName, + data: base64Data, + directory: Directory.Data, + }); - // CHANGE: Update savePicture function - const savePicture = async (photo: Photo, fileName: string): Promise => { - let base64Data: string | Blob; - // "hybrid" will detect Cordova or Capacitor; - if (isPlatform('hybrid')) { - const file = await Filesystem.readFile({ - path: photo.path!, - }); - base64Data = file.data; - } else { - base64Data = await base64FromPath(photo.webPath!); - } - const savedFile = await Filesystem.writeFile({ - path: fileName, - data: base64Data, - directory: Directory.Data, - }); - - if (isPlatform('hybrid')) { - // Display the new image by rewriting the 'file://' path to HTTP - // Details: https://ionicframework.com/docs/building/webview#file-protocol - return { - filepath: savedFile.uri, - webviewPath: Capacitor.convertFileSrc(savedFile.uri), - }; - } else { - // Use webPath to display the new image instead of base64 since it's - // already loaded into memory - return { - filepath: fileName, - webviewPath: photo.webPath, - }; - } + if (isPlatform('hybrid')) { + // Display the new image by rewriting the 'file://' path to HTTP + // Details: https://ionicframework.com/docs/building/webview#file-protocol + return { + filepath: savedFile.uri, + webviewPath: Capacitor.convertFileSrc(savedFile.uri), + }; + } else { + // Use webPath to display the new image instead of base64 since it's + // already loaded into memory + return { + filepath: fileName, + webviewPath: photo.webPath, }; - - // Old code from before. + } + }; + + // Same old code from before. } -// Old code from before. +// Same old code from before. ``` Next, add a new bit of logic in the `loadSaved` function. On mobile, we can directly point to each photo file on the Filesystem and display them automatically. On the web, however, we must read each image from the Filesystem into base64 format. This is because the Filesystem API uses [IndexedDB](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API) under the hood. Update the `loadSaved` function inside of `useEffect` to: ```tsx -// Old code from before. +// Same old code from before. export function usePhotoGallery() { - // Old code from before. - - useEffect(() => { - // CHANGE: Update loadSaved function within useEffect - const loadSaved = async () => { - const { value } = await Preferences.get({ key: PHOTO_STORAGE }); - - const photosInPreferences = (value ? JSON.parse(value) : []) as UserPhoto[]; - // If running on the web... - if (!isPlatform('hybrid')) { - for (let photo of photosInPreferences) { - const file = await Filesystem.readFile({ - path: photo.filepath, - directory: Directory.Data, - }); - // Web platform only: Load the photo as base64 data - photo.webviewPath = `data:image/jpeg;base64,${file.data}`; - } - } - setPhotos(photosInPreferences); - }; - - - - }, []); - - // Old code from before. + // Same old code from before. + + useEffect(() => { + // CHANGE: Update loadSaved function within useEffect + const loadSaved = async () => { + const { value } = await Preferences.get({ key: PHOTO_STORAGE }); + + const photosInPreferences = (value ? JSON.parse(value) : []) as UserPhoto[]; + // If running on the web... + if (!isPlatform('hybrid')) { + for (let photo of photosInPreferences) { + const file = await Filesystem.readFile({ + path: photo.filepath, + directory: Directory.Data, + }); + // Web platform only: Load the photo as base64 data + photo.webviewPath = `data:image/jpeg;base64,${file.data}`; + } + } + setPhotos(photosInPreferences); + }; + }, []); + + // Same old code from before. } -// Old code from before. +// Same old code from before. ``` Our Photo Gallery now consists of one codebase that runs on the web, Android, and iOS. Next up, the part you’ve been waiting for - deploying the app to a device. @@ -110,104 +107,101 @@ import { Capacitor } from '@capacitor/core'; const PHOTO_STORAGE = 'photos'; export function usePhotoGallery() { - const [photos, setPhotos] = useState([]); - const fileName = Date.now() + '.jpeg'; - const savePicture = async (photo: Photo, fileName: string): Promise => { - let base64Data: string | Blob; - // "hybrid" will detect Cordova or Capacitor; - if (isPlatform('hybrid')) { - const file = await Filesystem.readFile({ - path: photo.path!, - }); - base64Data = file.data; - } else { - base64Data = await base64FromPath(photo.webPath!); - } - const savedFile = await Filesystem.writeFile({ - path: fileName, - data: base64Data, - directory: Directory.Data, - }); - - if (isPlatform('hybrid')) { - // Display the new image by rewriting the 'file://' path to HTTP - // Details: https://ionicframework.com/docs/building/webview#file-protocol - return { - filepath: savedFile.uri, - webviewPath: Capacitor.convertFileSrc(savedFile.uri), - }; - } else { - // Use webPath to display the new image instead of base64 since it's - // already loaded into memory - return { - filepath: fileName, - webviewPath: photo.webPath, - }; - } - }; + const [photos, setPhotos] = useState([]); + const fileName = Date.now() + '.jpeg'; + const savePicture = async (photo: Photo, fileName: string): Promise => { + let base64Data: string | Blob; + // "hybrid" will detect Cordova or Capacitor; + if (isPlatform('hybrid')) { + const file = await Filesystem.readFile({ + path: photo.path!, + }); + base64Data = file.data; + } else { + base64Data = await base64FromPath(photo.webPath!); + } + const savedFile = await Filesystem.writeFile({ + path: fileName, + data: base64Data, + directory: Directory.Data, + }); - useEffect(() => { - const loadSaved = async () => { - const { value } = await Preferences.get({ key: PHOTO_STORAGE }); - - const photosInPreferences = (value ? JSON.parse(value) : []) as UserPhoto[]; - // If running on the web... - if (!isPlatform('hybrid')) { - for (let photo of photosInPreferences) { - const file = await Filesystem.readFile({ - path: photo.filepath, - directory: Directory.Data, - }); - // Web platform only: Load the photo as base64 data - photo.webviewPath = `data:image/jpeg;base64,${file.data}`; - } - } - setPhotos(photosInPreferences); - }; - - - - }, []); - - const takePhoto = async () => { - const photo = await Camera.getPhoto({ - resultType: CameraResultType.Uri, - source: CameraSource.Camera, - quality: 100, - }); - - const newPhotos = [ - { - filepath: fileName, - webviewPath: photo.webPath, - }, - ...photos, - ]; - setPhotos(newPhotos); - Preferences.set({ key: PHOTO_STORAGE, value: JSON.stringify(newPhotos) }); + if (isPlatform('hybrid')) { + // Display the new image by rewriting the 'file://' path to HTTP + // Details: https://ionicframework.com/docs/building/webview#file-protocol + return { + filepath: savedFile.uri, + webviewPath: Capacitor.convertFileSrc(savedFile.uri), }; - + } else { + // Use webPath to display the new image instead of base64 since it's + // already loaded into memory return { - photos, - takePhoto, + filepath: fileName, + webviewPath: photo.webPath, + }; + } + }; + + useEffect(() => { + const loadSaved = async () => { + const { value } = await Preferences.get({ key: PHOTO_STORAGE }); + + const photosInPreferences = (value ? JSON.parse(value) : []) as UserPhoto[]; + // If running on the web... + if (!isPlatform('hybrid')) { + for (let photo of photosInPreferences) { + const file = await Filesystem.readFile({ + path: photo.filepath, + directory: Directory.Data, + }); + // Web platform only: Load the photo as base64 data + photo.webviewPath = `data:image/jpeg;base64,${file.data}`; + } + } + setPhotos(photosInPreferences); }; + }, []); + + const takePhoto = async () => { + const photo = await Camera.getPhoto({ + resultType: CameraResultType.Uri, + source: CameraSource.Camera, + quality: 100, + }); + + const newPhotos = [ + { + filepath: fileName, + webviewPath: photo.webPath, + }, + ...photos, + ]; + setPhotos(newPhotos); + Preferences.set({ key: PHOTO_STORAGE, value: JSON.stringify(newPhotos) }); + }; + + return { + photos, + takePhoto, + }; } export async function base64FromPath(path: string): Promise { - const response = await fetch(path); - const blob = await response.blob(); - return new Promise((resolve, reject) => { - const reader = new FileReader(); - reader.onerror = reject; - reader.onload = () => { - if (typeof reader.result === 'string') { - resolve(reader.result); - } else { - reject('method did not return a string'); - } - }; - reader.readAsDataURL(blob); - }); + const response = await fetch(path); + const blob = await response.blob(); + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onerror = reject; + reader.onload = () => { + if (typeof reader.result === 'string') { + resolve(reader.result); + } else { + reject('method did not return a string'); + } + }; + reader.readAsDataURL(blob); + }); } export interface UserPhoto { diff --git a/docs/react/your-first-app/7-live-reload.md b/docs/react/your-first-app/7-live-reload.md index 2b65ba560bc..107cf3397eb 100644 --- a/docs/react/your-first-app/7-live-reload.md +++ b/docs/react/your-first-app/7-live-reload.md @@ -31,12 +31,11 @@ With Live Reload running and the app is open on your device, let’s implement p ```tsx // Other Imports -// CHANGE: Import UserPhoto, usePhotoGallery hook and useState from react. +// CHANGE: Import UserPhoto, usePhotoGallery hook and useState from react. import React, { useState } from 'react'; import { usePhotoGallery, UserPhoto } from '../hooks/usePhotoGallery'; -const Tab2: React.FC = () => { -}; +const Tab2: React.FC = () => {}; ``` Next, reference the `deletePhoto` function, which we'll create soon: @@ -45,36 +44,35 @@ Next, reference the `deletePhoto` function, which we'll create soon: // Same old code from before. const Tab2: React.FC = () => { - // CHANGE: Reference deletePhoto function + // CHANGE: Reference deletePhoto function const { photos, takePhoto, deletePhoto } = usePhotoGallery(); - // Same old code from before. + // Same old code from before. }; - ``` Next, add a state value to store information about the photo to delete: ```tsx -// Same old code from before. +// Same old code from before. const Tab2: React.FC = () => { - // Same old code from before. + // Same old code from before. - // CHANGE: Add a state value for photo deletion. - const [photoToDelete, setPhotoToDelete] = useState(); + // CHANGE: Add a state value for photo deletion. + const [photoToDelete, setPhotoToDelete] = useState(); - // Same old code from before. + // Same old code from before. }; ``` When a user clicks on an image, we will show the action sheet by changing the state value to the photo. Update the `` element to: ```tsx -// Same old code from before. +// Same old code from before. const Tab2: React.FC = () => { - // Same old code from before. + // Same old code from before. return ( @@ -113,7 +111,7 @@ In the JSX, put the following component before the closing `` tag. // Same old code from before. const Tab2: React.FC = () => { - // Same old code from before. + // Same old code from before. return ( @@ -171,34 +169,34 @@ Above, we added two options: `Delete` that calls `deletePhoto` function (to be a Next, we need to implement the deletePhoto method that will come from the `usePhotoGallery` hook. Open the file and paste in the following function in the hook: ```tsx -// Same old code from before. +// Same old code from before. export function usePhotoGallery() { - // Same old code from before. - - // CHANGE: Implement deletePhoto method within usePhotoGallery hook. - const deletePhoto = async (photo: UserPhoto) => { - // Remove this photo from the Photos reference data array - const newPhotos = photos.filter((p) => p.filepath !== photo.filepath); + // Same old code from before. - // Update photos array cache by overwriting the existing photo array - Preferences.set({ key: PHOTO_STORAGE, value: JSON.stringify(newPhotos) }); + // CHANGE: Implement deletePhoto method within usePhotoGallery hook. + const deletePhoto = async (photo: UserPhoto) => { + // Remove this photo from the Photos reference data array + const newPhotos = photos.filter((p) => p.filepath !== photo.filepath); - // delete photo file from filesystem - const filename = photo.filepath.substr(photo.filepath.lastIndexOf('/') + 1); - await Filesystem.deleteFile({ - path: filename, - directory: Directory.Data, - }); - setPhotos(newPhotos); - }; + // Update photos array cache by overwriting the existing photo array + Preferences.set({ key: PHOTO_STORAGE, value: JSON.stringify(newPhotos) }); - // CHANGE: Update return and add deletePhoto function - return { - photos, - takePhoto, - deletePhoto, - }; + // delete photo file from filesystem + const filename = photo.filepath.substr(photo.filepath.lastIndexOf('/') + 1); + await Filesystem.deleteFile({ + path: filename, + directory: Directory.Data, + }); + setPhotos(newPhotos); + }; + + // CHANGE: Update return and add deletePhoto function + return { + photos, + takePhoto, + deletePhoto, + }; } // Same old code from before. @@ -210,8 +208,7 @@ Save this file, then tap on a photo again and choose the “Delete” option. Th In the final portion of this tutorial, we’ll walk you through the basics of the Appflow product used to build and deploy your application to users' devices. - -`Tab2.tsx` should look like this: +`Tab2.tsx` should look like this: ```tsx import { camera, trash, close } from 'ionicons/icons'; @@ -237,7 +234,7 @@ import { usePhotoGallery, UserPhoto } from '../hooks/usePhotoGallery'; const Tab2: React.FC = () => { const { photos, takePhoto, deletePhoto } = usePhotoGallery(); const [photoToDelete, setPhotoToDelete] = useState(); - + return ( @@ -290,135 +287,132 @@ const Tab2: React.FC = () => { export default Tab2; ``` - `usePhotoGallery.ts` should look like this: ```tsx import { useState, useEffect } from 'react'; import { isPlatform } from '@ionic/react'; -import { Camera, CameraResultType, CameraSource, Photo } from '@capacitor/camera'; +import { Camera, CameraResultType, CameraSource, Photo } from '@capacitor/camera'; import { Filesystem, Directory } from '@capacitor/filesystem'; import { Preferences } from '@capacitor/preferences'; import { Capacitor } from '@capacitor/core'; - const PHOTO_STORAGE = 'photos'; export function usePhotoGallery() { - const [photos, setPhotos ] = useState([]); - - const savePicture = async (photo: Photo, fileName: string): Promise => { - let base64Data: string | Blob; - // "hybrid" will detect Cordova or Capacitor: - if (isPlatform('hybrid')) { - const file = await Filesystem.readFile({ - path: photo.path!, - }); - base64Data = file.data; - } else { - base64Data = await base64FromPath(photo.webPath!); - } - - - const savedFile = await Filesystem.writeFile({ - path: fileName, - data: base64Data, - directory: Directory.Data, - }); - - if (isPlatform('hybrid')) { - // Display the new image by rewriting the 'file://' path to HTTP - // Details: https://ionicframework.com/docs/building/webview#file-protocol - return { - filepath: savedFile.uri, - webviewPath: Capacitor.convertFileSrc(savedFile.uri), - }; - } else { - // Use webPath to display the new image instead of base64 since it's - // already loaded into memory - return { - filepath: fileName, - webviewPath: photo.webPath, - }; - } + const [photos, setPhotos] = useState([]); + + const savePicture = async (photo: Photo, fileName: string): Promise => { + let base64Data: string | Blob; + // "hybrid" will detect Cordova or Capacitor: + if (isPlatform('hybrid')) { + const file = await Filesystem.readFile({ + path: photo.path!, + }); + base64Data = file.data; + } else { + base64Data = await base64FromPath(photo.webPath!); } - useEffect(() => { - const loadSaved = async() => { - const { value } = await Preferences.get({ key: PHOTO_STORAGE }); - const photosInPreferences = (value? JSON.parse(value) : []) as UserPhoto[]; - - if (!isPlatform('hybrid')) { - for (let photo of photosInPreferences) { - const file = await Filesystem.readFile({ - path: photo.filepath, - directory: Directory.Data, - }); - // Web platform only: Load the photo as base64 data - photo.webviewPath = `data:image/jpeg;base64,${file.data}`; - } - } - setPhotos(photosInPreferences); - }; - loadSaved(); - }, []); - - const takePhoto = async () => { - const photo = await Camera.getPhoto({ - resultType: CameraResultType.Uri, - source: CameraSource.Camera, - quality: 100, - }); - - const fileName = Date.now() + '.jpeg'; - const savedFileImage = await savePicture(photo, fileName); - const newPhotos = [savedFileImage, ...photos]; - setPhotos(newPhotos); - Preferences.set({ key: PHOTO_STORAGE, value: JSON.stringify(newPhotos) }); - }; + const savedFile = await Filesystem.writeFile({ + path: fileName, + data: base64Data, + directory: Directory.Data, + }); - const deletePhoto = async (photo: UserPhoto) => { - // Remove this photo from the Photos reference data array - const newPhotos = photos.filter((p) => p.filepath !== photo.filepath); + if (isPlatform('hybrid')) { + // Display the new image by rewriting the 'file://' path to HTTP + // Details: https://ionicframework.com/docs/building/webview#file-protocol + return { + filepath: savedFile.uri, + webviewPath: Capacitor.convertFileSrc(savedFile.uri), + }; + } else { + // Use webPath to display the new image instead of base64 since it's + // already loaded into memory + return { + filepath: fileName, + webviewPath: photo.webPath, + }; + } + }; - // Update photos array cache by overwriting the existing photo array - Preferences.set({ key: PHOTO_STORAGE, value: JSON.stringify(newPhotos) }); + useEffect(() => { + const loadSaved = async () => { + const { value } = await Preferences.get({ key: PHOTO_STORAGE }); + const photosInPreferences = (value ? JSON.parse(value) : []) as UserPhoto[]; - // delete photo file from filesystem - const filename = photo.filepath.substr(photo.filepath.lastIndexOf('/') + 1); - await Filesystem.deleteFile({ - path: filename, + if (!isPlatform('hybrid')) { + for (let photo of photosInPreferences) { + const file = await Filesystem.readFile({ + path: photo.filepath, directory: Directory.Data, - }); - setPhotos(newPhotos); + }); + // Web platform only: Load the photo as base64 data + photo.webviewPath = `data:image/jpeg;base64,${file.data}`; + } + } + setPhotos(photosInPreferences); }; + loadSaved(); + }, []); + + const takePhoto = async () => { + const photo = await Camera.getPhoto({ + resultType: CameraResultType.Uri, + source: CameraSource.Camera, + quality: 100, + }); - return { - photos, - takePhoto, - deletePhoto, - }; + const fileName = Date.now() + '.jpeg'; + const savedFileImage = await savePicture(photo, fileName); + const newPhotos = [savedFileImage, ...photos]; + setPhotos(newPhotos); + Preferences.set({ key: PHOTO_STORAGE, value: JSON.stringify(newPhotos) }); + }; + + const deletePhoto = async (photo: UserPhoto) => { + // Remove this photo from the Photos reference data array + const newPhotos = photos.filter((p) => p.filepath !== photo.filepath); + + // Update photos array cache by overwriting the existing photo array + Preferences.set({ key: PHOTO_STORAGE, value: JSON.stringify(newPhotos) }); + + // delete photo file from filesystem + const filename = photo.filepath.substr(photo.filepath.lastIndexOf('/') + 1); + await Filesystem.deleteFile({ + path: filename, + directory: Directory.Data, + }); + setPhotos(newPhotos); + }; + + return { + photos, + takePhoto, + deletePhoto, + }; } export async function base64FromPath(path: string): Promise { - const response = await fetch(path); - const blob = await response.blob(); - return new Promise((resolve, reject) => { - const reader = new FileReader(); - reader.onerror = reject; - reader.onload = () => { - if (typeof reader.result === 'string') { - resolve(reader.result); - } else { - reject('method did not return a string'); - } - }; - reader.readAsDataURL(blob); - }); + const response = await fetch(path); + const blob = await response.blob(); + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onerror = reject; + reader.onload = () => { + if (typeof reader.result === 'string') { + resolve(reader.result); + } else { + reject('method did not return a string'); + } + }; + reader.readAsDataURL(blob); + }); } export interface UserPhoto { - filepath: string; - webviewPath?: string; + filepath: string; + webviewPath?: string; } -``` \ No newline at end of file +``` From e361e12d475a4f5d07fce3ddac4a4e35673ac2ce Mon Sep 17 00:00:00 2001 From: Maria Hutt Date: Tue, 21 Oct 2025 12:42:14 -0700 Subject: [PATCH 05/16] docs(components): remove redundant --- docs/components.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/docs/components.md b/docs/components.md index e3add853aa8..4d1b2cf28a2 100644 --- a/docs/components.md +++ b/docs/components.md @@ -32,10 +32,6 @@ Ionic apps are made of high-level building blocks called Components, which allow

Action Sheets display a set of options with the ability to confirm or cancel an action.

- -

Action Sheets display a set of options with the ability to confirm or cancel an action.

-
-

Alerts are a great way to offer the user the ability to choose a specific action or list of actions.

From 26ed242b59560825a919994fc527c91c5611d8b8 Mon Sep 17 00:00:00 2001 From: Maria Hutt Date: Tue, 21 Oct 2025 13:42:54 -0700 Subject: [PATCH 06/16] Update docs/react/your-first-app.md --- docs/react/your-first-app.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/react/your-first-app.md b/docs/react/your-first-app.md index 5a147cad92c..2c7c7ae837e 100644 --- a/docs/react/your-first-app.md +++ b/docs/react/your-first-app.md @@ -159,7 +159,7 @@ Open `/src/pages/Tab2.tsx`. We see: Photo Gallery ``` -We put the visual aspects of our app into ``. In this case, it’s where we’ll add a button that opens the device’s camera as well as displays the image captured by the camera. Start by adding a [floating action button](https://ionicframework.com/docs/api/fab) (FAB) to the bottom of the page and set the camera image as the icon. +We put the visual aspects of our app into ``. In this case, it’s where we’ll add a button that opens the device’s camera as well as displays the image captured by the camera. Start by adding a [floating action button](https://ionicframework.com/docs/api/fab) (FAB) to the bottom of the page and set the camera image as the icon. ```tsx // CHANGE: Add the following import. From 32b0976e67dbd1b477470e3c0b8ae1e1cadbed54 Mon Sep 17 00:00:00 2001 From: Maria Hutt Date: Tue, 21 Oct 2025 16:01:02 -0700 Subject: [PATCH 07/16] docs(react): update your first app page --- docs/react/your-first-app.md | 134 ++++++++++++++++++++--------------- 1 file changed, 77 insertions(+), 57 deletions(-) diff --git a/docs/react/your-first-app.md b/docs/react/your-first-app.md index 2c7c7ae837e..1fa162b9243 100644 --- a/docs/react/your-first-app.md +++ b/docs/react/your-first-app.md @@ -4,10 +4,10 @@ sidebar_label: Build Your First App --- - React Apps | Build Your First Ionic Framework React Application + Build Your First Ionic Mobile App: React Development Tutorial @@ -43,9 +43,8 @@ Download and install these right away to ensure an optimal Ionic development exp - **Node.js** for interacting with the Ionic ecosystem. [Download the LTS version here](https://nodejs.org/en/). - **A code editor** for... writing code! We are fans of [Visual Studio Code](https://code.visualstudio.com/). - **Command-line interface/terminal (CLI)**: - - **Windows** users: for the best Ionic experience, we recommend the built-in command line (cmd) or the Powershell - CLI, running in Administrator mode. - - **Mac/Linux** users, virtually any terminal will work. + - **Windows** users: for the best Ionic experience, we recommend the built-in command line (cmd) or the Powershell CLI, running in Administrator mode. + - **Mac/Linux** users: virtually any terminal will work. ## Install Ionic Tooling @@ -97,8 +96,6 @@ It's a separate dependency, so install it next: npm install @ionic/pwa-elements ``` -After installation, open up the project in your code editor of choice. - Next, import `@ionic/pwa-elements` by editing `src/main.tsx`. ```tsx @@ -108,7 +105,7 @@ import App from './App'; // CHANGE: Add the following import. import { defineCustomElements } from '@ionic/pwa-elements/loader'; -// CHANGE: Call the element loader before the render call +// CHANGE: Call the element loader before the render call. defineCustomElements(window); const container = document.getElementById('root'); @@ -124,7 +121,7 @@ That’s it! Now for the fun part - let’s see the app in action. ## Run the App -Run this command in your shell: +Run this command next: ```shell ionic serve @@ -141,30 +138,45 @@ There are three tabs. Click on the Tab2 tab. It’s a blank canvas, aka the perf Open `/src/pages/Tab2.tsx`. We see: ```tsx - - - - Tab 2 - - - - - - +import { IonContent, IonHeader, IonPage, IonTitle, IonToolbar } from '@ionic/react'; +import ExploreContainer from '../components/ExploreContainer'; +import './Tab2.css'; + +const Tab2: React.FC = () => { + return ( + + + + Tab 2 + + + + + + Tab 2 + + + + + + ); +}; + +export default Tab2; ``` -`IonHeader` represents the top navigation and toolbar, with "Tab 2" as the title. Let’s rename it: +`IonHeader` represents the top navigation and toolbar, with "Tab 2" as the title (there are two of them due to iOS [Collapsible Large Title](../api/title#collapsible-large-titles) support). Let’s rename both `IonTitle` elements to: ```tsx Photo Gallery ``` -We put the visual aspects of our app into ``. In this case, it’s where we’ll add a button that opens the device’s camera as well as displays the image captured by the camera. Start by adding a [floating action button](https://ionicframework.com/docs/api/fab) (FAB) to the bottom of the page and set the camera image as the icon. +We put the visual aspects of our app into ``. In this case, it’s where we’ll add a button that opens the device’s camera as well as displays the image captured by the camera. Start by adding a [floating action button](../api/fab) (FAB) to the bottom of the page and set the camera image as the icon. ```tsx // CHANGE: Add the following import. import { camera, trash, close } from 'ionicons/icons'; -// CHANGE: Add the following import. +// CHANGE: Update the following import. import { IonContent, IonHeader, @@ -180,7 +192,8 @@ import { IonImg, IonActionSheet, } from '@ionic/react'; -import ExploreContainer from '../components/ExploreContainer'; +// CHANGE: Remove or comment out `ExploreContainer`. +// import ExploreContainer from '../components/ExploreContainer'; import './Tab2.css'; const Tab2: React.FC = () => { @@ -188,18 +201,24 @@ const Tab2: React.FC = () => { - Tab 2 + Photo Gallery - + + + Photo Gallery + + + {/* CHANGE: Add the floating action button. */} - takePhoto()}> + - + {/* CHANGE: Remove or comment out `ExploreContainer`. */} + {/* */} ); }; @@ -207,7 +226,7 @@ const Tab2: React.FC = () => { export default Tab2; ``` -Next, open `src/App.tsx`, remove the `ellipse` icon from the import and import the `images` icon instead: +Next, open `src/App.tsx` and replace the imported `ellipse` icon with the `images` icon. ```tsx import { images, square, triangle } from 'ionicons/icons'; @@ -217,42 +236,43 @@ Within the tab bar (``), change the label to “Photos” and the `el ```tsx // Keep other imports -// CHANGE: Add the following import. +// CHANGE: Update the following import. import { images, square, triangle } from 'ionicons/icons'; const App: React.FC = () => ( - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + {/* CHANGE: Update icon. */} + - - - + + + + From e3410a203a5c5d8188d2114ef3dd10faa8d90d4c Mon Sep 17 00:00:00 2001 From: Maria Hutt Date: Tue, 21 Oct 2025 16:03:47 -0700 Subject: [PATCH 08/16] docs(react): update file paths --- docs/react/your-first-app.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/react/your-first-app.md b/docs/react/your-first-app.md index 1fa162b9243..1ca57d5bc4e 100644 --- a/docs/react/your-first-app.md +++ b/docs/react/your-first-app.md @@ -165,13 +165,13 @@ const Tab2: React.FC = () => { export default Tab2; ``` -`IonHeader` represents the top navigation and toolbar, with "Tab 2" as the title (there are two of them due to iOS [Collapsible Large Title](../api/title#collapsible-large-titles) support). Let’s rename both `IonTitle` elements to: +`IonHeader` represents the top navigation and toolbar, with "Tab 2" as the title (there are two of them due to iOS [Collapsible Large Title](../api/title.md#collapsible-large-titles) support). Let’s rename both `IonTitle` elements to: ```tsx Photo Gallery ``` -We put the visual aspects of our app into ``. In this case, it’s where we’ll add a button that opens the device’s camera as well as displays the image captured by the camera. Start by adding a [floating action button](../api/fab) (FAB) to the bottom of the page and set the camera image as the icon. +We put the visual aspects of our app into ``. In this case, it’s where we’ll add a button that opens the device’s camera as well as displays the image captured by the camera. Start by adding a [floating action button](../api/fab.md) (FAB) to the bottom of the page and set the camera image as the icon. ```tsx // CHANGE: Add the following import. From 2cefaad5bb39fb0e91eedec03bf95786fbd0f874 Mon Sep 17 00:00:00 2001 From: Maria Hutt Date: Mon, 27 Oct 2025 12:03:33 -0700 Subject: [PATCH 09/16] docs(react): upate your first app page --- docs/react/your-first-app.md | 40 +++++++++++++++++++++--------------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/docs/react/your-first-app.md b/docs/react/your-first-app.md index 1ca57d5bc4e..8b744d1deba 100644 --- a/docs/react/your-first-app.md +++ b/docs/react/your-first-app.md @@ -30,11 +30,11 @@ We'll create a Photo Gallery app that offers the ability to take photos with you Highlights include: -- One React-based codebase that runs on the web, iOS, and Android using Ionic Framework [UI components](https://ionicframework.com/docs/components). +- One React-based codebase that runs on the web, iOS, and Android using Ionic Framework [UI components](../components.md). - Deployed as a native iOS and Android mobile app using [Capacitor](https://capacitorjs.com), Ionic's official native app runtime. -- Photo Gallery functionality powered by the Capacitor [Camera](https://capacitorjs.com/docs/apis/camera), [Filesystem](https://capacitorjs.com/docs/apis/filesystem), and [Preferences](https://capacitorjs.com/docs/apis/preferences) APIs. +- Photo Gallery functionality powered by the Capacitor [Camera](../native/camera.md), [Filesystem](../native/filesystem.md), and [Preferences](../native/preferences.md) APIs. -Find the complete app code referenced in this guide [on GitHub](https://github.com/ionic-team/photo-gallery-capacitor-react). +Find the complete app code referenced in this guide [on GitHub](https://github.com/ionic-team/tutorial-photo-gallery-react). ## Download Required Tools @@ -88,7 +88,7 @@ npm install @capacitor/camera @capacitor/preferences @capacitor/filesystem ### PWA Elements -Some Capacitor plugins, including the Camera API, provide the web-based functionality and UI via the Ionic [PWA Elements library](https://github.com/ionic-team/pwa-elements). +Some Capacitor plugins, including the [Camera API](../native/camera.md), provide the web-based functionality and UI via the Ionic [PWA Elements library](https://github.com/ionic-team/pwa-elements). It's a separate dependency, so install it next: @@ -131,7 +131,7 @@ And voilà! Your Ionic app is now running in a web browser. Most of your app can ## Photo Gallery!!! -There are three tabs. Click on the Tab2 tab. It’s a blank canvas, aka the perfect spot to transform into a Photo Gallery. The Ionic CLI features Live Reload, so when you make changes and save them, the app is updated immediately! +There are three tabs. Click on the "Tab2" tab. It’s a blank canvas, aka the perfect spot to transform into a Photo Gallery. The Ionic CLI features Live Reload, so when you make changes and save them, the app is updated immediately! ![Animated GIF showing the live reload feature in an Ionic app, with changes in code immediately updating the app in a web browser.](/img/guides/react/first-app/live-reload.gif 'Live Reload Feature in Ionic App') @@ -226,18 +226,28 @@ const Tab2: React.FC = () => { export default Tab2; ``` -Next, open `src/App.tsx` and replace the imported `ellipse` icon with the `images` icon. +Next, open `src/App.tsx`. Change the label to “Photos” and the `ellipse` icon to `images` for the middle tab button. ```tsx -import { images, square, triangle } from 'ionicons/icons'; -``` - -Within the tab bar (``), change the label to “Photos” and the `ellipse` icon to `images` for the middle tab button: - -```tsx -// Keep other imports +import { Redirect, Route } from 'react-router-dom'; +import { + IonApp, + IonIcon, + IonLabel, + IonRouterOutlet, + IonTabBar, + IonTabButton, + IonTabs, + setupIonicReact, +} from '@ionic/react'; +import { IonReactRouter } from '@ionic/react-router'; // CHANGE: Update the following import. import { images, square, triangle } from 'ionicons/icons'; +import Tab1 from './pages/Tab1'; +import Tab2 from './pages/Tab2'; +import Tab3 from './pages/Tab3'; + +/* Ionic styles are not shown in this example to keep it brief but will be included in the Ionic package downloaded for your app. Do not remove them. */ const App: React.FC = () => ( @@ -279,8 +289,4 @@ const App: React.FC = () => ( ); ``` -:::note -In Ionic React, icons are imported individually from `ionicons/icons` and set to the icon prop. -::: - That’s just the start of all the cool things we can do with Ionic. Up next, implement camera taking functionality on the web, then build it for iOS and Android. From 7e9bb4d203fb1863b04289f44f446d7ab9d0a79e Mon Sep 17 00:00:00 2001 From: Maria Hutt Date: Mon, 27 Oct 2025 12:46:07 -0700 Subject: [PATCH 10/16] docs(react): update taking photos page --- docs/react/your-first-app/2-taking-photos.md | 238 ++++++++++--------- 1 file changed, 122 insertions(+), 116 deletions(-) diff --git a/docs/react/your-first-app/2-taking-photos.md b/docs/react/your-first-app/2-taking-photos.md index e0b3eafd32e..9da35706ba5 100644 --- a/docs/react/your-first-app/2-taking-photos.md +++ b/docs/react/your-first-app/2-taking-photos.md @@ -4,55 +4,33 @@ sidebar_label: Taking Photos --- - Take Photos From The Camera on React Apps - Ionic Documentation + Build Camera API for iOS, Android & Web | Ionic Capacitor Camera -Now for the fun part - adding the ability to take photos with the device’s camera using the Capacitor [Camera API](https://capacitorjs.com/docs/apis/camera). We’ll begin with building it for the web, then make some small tweaks to make it work on mobile (iOS and Android). +Now for the fun part - adding the ability to take photos with the device’s camera using the Capacitor [Camera API](../../native/camera.md). We’ll begin with building it for the web, then make some small tweaks to make it work on mobile (iOS and Android). -To do so, we will create our own custom React hook that will manage the photos for the gallery. +## Photo Gallery Hook -:::note -If you are not familiar with React Hooks, [Introducing React Hooks](https://react.dev/reference/react/hooks) from the official React docs is a good resource to start with. -::: +We will create a [custom React hook](https://react.dev/learn/reusing-logic-with-custom-hooks#extracting-your-own-custom-hook-from-a-component) to manage the photos for the gallery. Create a new file at `src/hooks/usePhotoGallery.ts` and open it up. -A custom hook is just a function that uses other React hooks. And that's what we will be doing! We will start by importing the various hooks and utilities we will be using from React core, the Ionic React Hooks project, and Capacitor: +Next, define a new method, `usePhotoGallery()`, that will contain the core logic to take a device photo and save it to the filesystem. Let’s start by opening the device camera. -```tsx -// CHANGE: Add the following imports +```ts import { useState, useEffect } from 'react'; -import { isPlatform } from '@ionic/react'; - -// CHANGE: Add the following imports import { Camera, CameraResultType, CameraSource, Photo } from '@capacitor/camera'; import { Filesystem, Directory } from '@capacitor/filesystem'; import { Preferences } from '@capacitor/preferences'; -import { Capacitor } from '@capacitor/core'; - -export function usePhotoGallery() {} -``` - -Next, create a function named usePhotoGallery: - -```tsx -import { useState, useEffect } from 'react'; -import { isPlatform } from '@ionic/react'; - -import { Camera, CameraResultType, CameraSource, Photo } from '@capacitor/camera'; -import { Filesystem, Directory } from '@capacitor/filesystem'; -import { Preferences } from '@capacitor/preferences'; -import { Capacitor } from '@capacitor/core'; export function usePhotoGallery() { - // CHANGE: Add the usePhotoGallery function. - const takePhoto = async () => { + const addNewToGallery = async () => { // Take a photo - const photo = await Camera.getPhoto({ + const capturedPhoto = await Camera.getPhoto({ resultType: CameraResultType.Uri, source: CameraSource.Camera, quality: 100, @@ -60,37 +38,57 @@ export function usePhotoGallery() { }; return { - takePhoto, + addNewToGallery, }; } ``` -Our `usePhotoGallery` hook exposes a method called takePhoto, which in turn calls into Capacitor's getPhoto method. - Notice the magic here: there's no platform-specific code (web, iOS, or Android)! The Capacitor Camera plugin abstracts that away for us, leaving just one method call - `getPhoto()` - that will open up the device's camera and allow us to take photos. -The last step we need to take is to use the new hook from the Tab2 page. Go back to Tab2.tsx and import the hook: +Next, in `Tab2.tsx`, import the `usePhotoGallery` method and destructure it to call its `addNewToGallery` method. ```tsx -// Keep the other imports - -// CHANGE: Import the usePhotoGallery hook +import { camera, trash, close } from 'ionicons/icons'; +import { + IonContent, + IonHeader, + IonPage, + IonTitle, + IonToolbar, + IonFab, + IonFabButton, + IonIcon, + IonGrid, + IonRow, + IonCol, + IonImg, + IonActionSheet, +} from '@ionic/react'; +// CHANGE: Add `usePhotoGallery` import. import { usePhotoGallery } from '../hooks/usePhotoGallery'; +import './Tab2.css'; const Tab2: React.FC = () => { - // CHANGE: Get access to `takePhoto` method by using the hook - const { takePhoto } = usePhotoGallery(); + // CHANGE: Destructure `addNewToGallery()` from `usePhotoGallery()`. + const { addNewToGallery } = usePhotoGallery(); return ( - Tab 2 + Photo Gallery - + + + + Photo Gallery + + + - takePhoto()}> + {/* CHANGE: Add a click event listener to the floating action button. */} + addNewToGallery()}> @@ -102,109 +100,114 @@ const Tab2: React.FC = () => { export default Tab2; ``` -Save the file, and if you’re not already, restart the development server in your browser by running `ionic serve`. On the Photo Gallery tab, click the Camera button. If your computer has a webcam of any sort, a modal window appears. Take a selfie! +If it's not running already, restart the development server in your browser by running `ionic serve`. On the Photo Gallery tab, click the Camera button. If your computer has a webcam of any sort, a modal window appears. Take a selfie! ![A photo gallery app displaying a webcam selfie.](/img/guides/first-app-cap-ng/camera-web.png 'Webcam Selfie in Photo Gallery') _(Your selfie is probably much better than mine)_ -After taking a photo, it disappears. We still need to display it within our app and save it for future access. +After taking a photo, it disappears right away. We need to display it within our app and save it for future access. ## Displaying Photos -First we will create a new type to define our Photo, which will hold specific metadata. Add the following UserPhoto interface to the `usePhotoGallery.ts` file, somewhere outside of the main function: +Return to `usePhotoGallery.ts`. -```tsx -export functino usePhotoGallery { - // Same old code from before. +Outside of the `usePhotoGallery` method definition (the very bottom of the file), create a new interface, `UserPhoto`, to hold our photo metadata. + +```ts +export function usePhotoGallery { + // Same old code from before. } -// CHANGE: Add the interface. +// CHANGE: Add the `UserPhoto` interface. export interface UserPhoto { filepath: string; webviewPath?: string; } ``` -Back at the top of the function (right after the call to `usePhotoGallery`, we will define a state variable to store the array of each photo captured with the Camera. +Above the `addNewToGallery()` method, define an array of `UserPhoto`, which will contain a reference to each photo captured with the Camera. Make it a state variable using React's [useState hook](https://react.dev/reference/react/useState). -```tsx +```ts export function usePhotoGallery { - // CHANGE: Add the photos array. - const [photos, setPhotos] = useState([]); + // CHANGE: Add the photos array. + const [photos, setPhotos] = useState([]); - // Same old code from before. + // Same old code from before. } ``` -When the camera is done taking a picture, the resulting Photo returned from Capacitor will be stored in the `photo` variable. We want to create a new photo object and add it to the photos state array. We make sure we don't accidentally mutate the current photos array by making a new array, and then call `setPhotos` to store the array into state. Update the `takePhoto` method and add this code after the getPhoto call: - -```tsx -// Same old code from before. +Over in the `addNewToGallery()` method, add the newly captured photo to the beginning of the `photos` array. Then, update the `userPhotoGallery` return statement with the `photos` array. +```ts export function usePhotoGallery() { const [photos, setPhotos] = useState([]); - // CHANGE: Create new fileName variable with date and .jpeg - const fileName = Date.now() + '.jpeg'; - const takePhoto = async () => { - // Same old code from before. + const addNewToGallery = async () => { + // Take a photo + const capturedPhoto = await Camera.getPhoto({ + resultType: CameraResultType.Uri, + source: CameraSource.Camera, + quality: 100, + }); - // CHANGE: Add in newPhotos after getPhoto call - const newPhotos = [ + // CHANGE: Create the `fileName` with current timestamp. + const fileName = Date.now() + '.jpeg'; + // CHANGE: Create `savedImageFile` matching `UserPhoto` interface. + const savedImageFile = [ { filepath: fileName, - webviewPath: photo.webPath, + webviewPath: capturedPhoto.webPath, }, ...photos, ]; - setPhotos(newPhotos); + + // CHANGE: Update the `photos` array with the new photo. + setPhotos(savedImageFile); }; - // CHANGE: Update return statement to include photos. return { + addNewToGallery, + // CHANGE: Update return statement to include `photos` array. photos, - takePhoto, }; } - -// Same old code from before. ``` `usePhotoGallery.ts` should now look like this: -```tsx +```ts import { useState, useEffect } from 'react'; -import { isPlatform } from '@ionic/react'; import { Camera, CameraResultType, CameraSource, Photo } from '@capacitor/camera'; import { Filesystem, Directory } from '@capacitor/filesystem'; import { Preferences } from '@capacitor/preferences'; -import { Capacitor } from '@capacitor/core'; export function usePhotoGallery() { const [photos, setPhotos] = useState([]); - const fileName = Date.now() + '.jpeg'; - const takePhoto = async () => { - const photo = await Camera.getPhoto({ + const addNewToGallery = async () => { + // Take a photo + const capturedPhoto = await Camera.getPhoto({ resultType: CameraResultType.Uri, source: CameraSource.Camera, quality: 100, }); - const newPhotos = [ + const fileName = Date.now() + '.jpeg'; + const savedImageFile = [ { filepath: fileName, - webviewPath: photo.webPath, + webviewPath: capturedPhoto.webPath, }, ...photos, ]; - setPhotos(newPhotos); + + setPhotos(savedImageFile); }; return { + addNewToGallery, photos, - takePhoto, }; } @@ -214,47 +217,50 @@ export interface UserPhoto { } ``` -Next, move over to `Tab2.tsx` so we can display the image on the screen. With the photo(s) stored into the main array we can display the images on the screen. Add a [Grid component](https://ionicframework.com/docs/api/grid) so that each photo will display nicely as photos are added to the gallery, and loop through each photo in the Photos array, adding an Image component (``) for each. Point the `src` (source) to the photo’s path: +Next, switch to `Tab2.tsx` to display the images. We'll add a [Grid component](../../api/grid.md) to ensure the photos display neatly as they're added to the gallery. Inside the grid, loop through each photo in the `UserPhoto`'s `photos` array. For each item, add an [Image component](../../api/img.md) and set its `src` property to the photo's path. ```tsx -// Same old code from before. - -// CHANGE: Import usePhotoGallery Hook -import { usePhotoGallery } from '../hooks/usePhotoGallery'; - const Tab2: React.FC = () => { - // CHANGE: Get access to photos from usePhotoGallery - const { photos, takePhoto } = usePhotoGallery(); - - return ( - - - - Tab 2 - + // CHANGE: Add `photos` array to destructure from `usePhotoGallery()`. + const { photos, addNewToGallery } = usePhotoGallery(); + + return ( + + + + Photo Gallery + + + + + + Photo Gallery + - - - - - {photos.map((photo, index) => ( - - - - ))} - - - - takePhoto()}> - - - - - - ); + + {/* CHANGE: Add a grid component to display the photos. */} + + + {/* CHANGE: Create a new column and image component for each photo. */} + {photos.map((photo) => ( + + + + ))} + + + + + addNewToGallery()}> + + + + + + ); }; ``` -Save all files. Within the web browser, click the Camera button and take another photo. This time, the photo is displayed in the Photo Gallery! +Within the web browser, click the camera button and take another photo. This time, the photo is displayed in the Photo Gallery! Up next, we’ll add support for saving the photos to the filesystem, so they can be retrieved and displayed in our app at a later time. From 290abfb419a877dc791e72a9c69e24e57e6b49e6 Mon Sep 17 00:00:00 2001 From: Maria Hutt Date: Mon, 27 Oct 2025 15:12:22 -0700 Subject: [PATCH 11/16] docs(react): update saving photos page --- docs/react/your-first-app/3-saving-photos.md | 241 +++++++++++-------- 1 file changed, 138 insertions(+), 103 deletions(-) diff --git a/docs/react/your-first-app/3-saving-photos.md b/docs/react/your-first-app/3-saving-photos.md index aa54812590a..92fc228df78 100644 --- a/docs/react/your-first-app/3-saving-photos.md +++ b/docs/react/your-first-app/3-saving-photos.md @@ -1,129 +1,115 @@ --- +title: Saving Photos to the Filesystem sidebar_label: Saving Photos --- # Saving Photos to the Filesystem -We’re now able to take multiple photos and display them in a photo gallery on the second tab of our app. These photos, however, are not currently being stored permanently, so when the app is closed, they will be lost. +We’re now able to take multiple photos and display them in a photo gallery on the second tab of our app. These photos, however, are not currently being stored permanently, so when the app is closed, they will be deleted. ## Filesystem API -Fortunately, saving them to the filesystem only takes a few steps. Begin by opening the `usePhotoGallery` hook (`src/hooks/usePhotoGallery.ts`), and get access to the `writeFile` method from the `Filesystem` class: +Fortunately, saving them to the filesystem only takes a few steps. Begin by creating a new class method, `savePicture()`, in the `usePhotoGallery()` method. -:::note -We will use the `writeFile` method initially, but we will use the others coming up shortly, so we'll go ahead and import them now. -::: - -Next, create a couple of new functions in `usePhotoGallery`: - -```tsx +```ts import { useState, useEffect } from 'react'; -import { isPlatform } from '@ionic/react'; import { Camera, CameraResultType, CameraSource, Photo } from '@capacitor/camera'; import { Filesystem, Directory } from '@capacitor/filesystem'; import { Preferences } from '@capacitor/preferences'; -import { Capacitor } from '@capacitor/core'; export function usePhotoGallery() { // Same old code from before. - // CHANGE: Add in new function to save pictures + // CHANGE: Add the `savePicture()` method. const savePicture = async (photo: Photo, fileName: string): Promise => { - const base64Data = await base64FromPath(photo.webPath!); - const savedFile = await Filesystem.writeFile({ - path: fileName, - data: base64Data, - directory: Directory.Data, - }); - - // Use webPath to display the new image instead of base64 since it's - // already loaded into memory return { - filepath: fileName, - webviewPath: photo.webPath, + filepath: 'soon...', + webviewPath: 'soon...', }; }; - // Same old code from before. + return { + addNewToGallery, + photos, + }; } -// CHANGE: Add a function that allows the photo to be downloaded from the supplied path -export async function base64FromPath(path: string): Promise { - const response = await fetch(path); - const blob = await response.blob(); - return new Promise((resolve, reject) => { - const reader = new FileReader(); - reader.onerror = reject; - reader.onload = () => { - if (typeof reader.result === 'string') { - resolve(reader.result); - } else { - reject('method did not return a string'); - } - }; - reader.readAsDataURL(blob); - }); +export interface UserPhoto { + filepath: string; + webviewPath?: string; } - -// Same old code from before. ``` -:::note -The base64FromPath method is a helper util that downloads a file from the supplied path and returns a base64 representation of that file. -::: - -We pass in the `photo` object, which represents the newly captured device photo, as well as the fileName, which will provide a path for the file to be stored to. +We can use this new method immediately in `addNewToGallery()`. -Next we use the Capacitor [Filesystem API](https://capacitorjs.com/docs/apis/filesystem) to save the photo to the filesystem. We start by converting the photo to base64 format, then feed the data to the Filesystem’s `writeFile` function. - -Last, call `savePicture` and pass in the photo object and filename directly underneath the call to `setPhotos` in the `takePhoto` method. Here is the full method: - -```tsx -// Same old code from before. +```ts +import { useState, useEffect } from 'react'; +import { Camera, CameraResultType, CameraSource, Photo } from '@capacitor/camera'; +import { Filesystem, Directory } from '@capacitor/filesystem'; +import { Preferences } from '@capacitor/preferences'; export function usePhotoGallery() { - // Same old code from before. + const [photos, setPhotos] = useState([]); - // CHANGE: Update the takePhoto function to utilize capacitor filesystem - const takePhoto = async () => { - const photo = await Camera.getPhoto({ + const addNewToGallery = async () => { + // Take a photo + const capturedPhoto = await Camera.getPhoto({ resultType: CameraResultType.Uri, source: CameraSource.Camera, quality: 100, }); - const newPhotos = [ - { - filepath: fileName, - webviewPath: photo.webPath, - }, - ...photos, - ]; - setPhotos(newPhotos); + const fileName = Date.now() + '.jpeg'; + // CHANGE: Add `savedImageFile()`. + // Save the picture and add it to photo collection + const savedImageFile = await savePicture(capturedPhoto, fileName); + + // CHANGE: Update state with new photo. + setPhotos([savedImageFile, ...photos]); }; - // Same old code from before + // CHANGE: Add `savePicture()` method. + const savePicture = async (photo: Photo, fileName: string): Promise => { + return { + filepath: 'soon...', + webviewPath: 'soon...', + }; + }; + + return { + addNewToGallery, + photos, + }; } -// Same old code from before. +export interface UserPhoto { + filepath: string; + webviewPath?: string; +} ``` -`usePhotoGallery.ts` should now look like this: +We'll use the Capacitor [Filesystem API](../../native/filesystem.md) to save the photo. First, convert the photo to base64 format. -```tsx +Then, pass the data to the Filesystem's `writeFile` method. Recall that we display photos by setting the image's source path (`src`) to the `webviewPath` property. So, set the `webviewPath` and return the new `Photo` object. + +For now, create a new helper method, `convertBlobToBase64()`, to implement the necessary logic for running on the web. + +```ts import { useState, useEffect } from 'react'; -import { isPlatform } from '@ionic/react'; import { Camera, CameraResultType, CameraSource, Photo } from '@capacitor/camera'; import { Filesystem, Directory } from '@capacitor/filesystem'; import { Preferences } from '@capacitor/preferences'; -import { Capacitor } from '@capacitor/core'; export function usePhotoGallery() { - const [photos, setPhotos] = useState([]); - const fileName = Date.now() + '.jpeg'; + // Same old code from before. + // CHANGE: Update the `savePicture()` method. const savePicture = async (photo: Photo, fileName: string): Promise => { - const base64Data = await base64FromPath(photo.webPath!); + // Fetch the photo, read as a blob, then convert to base64 format + const response = await fetch(photo.webPath!); + const blob = await response.blob(); + const base64Data = (await convertBlobToBase64(blob)) as string; + const savedFile = await Filesystem.writeFile({ path: fileName, data: base64Data, @@ -138,50 +124,99 @@ export function usePhotoGallery() { }; }; - const takePhoto = async () => { - const photo = await Camera.getPhoto({ + // CHANGE: Add the `convertBlobToBase64()` method. + const convertBlobToBase64 = (blob: Blob) => { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onerror = reject; + reader.onload = () => { + resolve(reader.result); + }; + reader.readAsDataURL(blob); + }); + }; + + return { + addNewToGallery, + photos, + }; +} + +export interface UserPhoto { + filepath: string; + webviewPath?: string; +} +``` + +`usePhotoGallery.ts` should now look like this: + +```ts +import { useState, useEffect } from 'react'; +import { Camera, CameraResultType, CameraSource, Photo } from '@capacitor/camera'; +import { Filesystem, Directory } from '@capacitor/filesystem'; +import { Preferences } from '@capacitor/preferences'; + +export function usePhotoGallery() { + const [photos, setPhotos] = useState([]); + + const addNewToGallery = async () => { + // Take a photo + const capturedPhoto = await Camera.getPhoto({ resultType: CameraResultType.Uri, source: CameraSource.Camera, quality: 100, }); - const newPhotos = [ - { - filepath: fileName, - webviewPath: photo.webPath, - }, - ...photos, - ]; - setPhotos(newPhotos); + const fileName = Date.now() + '.jpeg'; + // Save the picture and add it to photo collection + const savedImageFile = await savePicture(capturedPhoto, fileName); + + setPhotos([savedImageFile, ...photos]); + }; + + const savePicture = async (photo: Photo, fileName: string): Promise => { + // Fetch the photo, read as a blob, then convert to base64 format + const response = await fetch(photo.webPath!); + const blob = await response.blob(); + const base64Data = (await convertBlobToBase64(blob)) as string; + + const savedFile = await Filesystem.writeFile({ + path: fileName, + data: base64Data, + directory: Directory.Data, + }); + + // Use webPath to display the new image instead of base64 since it's + // already loaded into memory + return { + filepath: fileName, + webviewPath: photo.webPath, + }; + }; + + const convertBlobToBase64 = (blob: Blob) => { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onerror = reject; + reader.onload = () => { + resolve(reader.result); + }; + reader.readAsDataURL(blob); + }); }; return { + addNewToGallery, photos, - takePhoto, }; } -export async function base64FromPath(path: string): Promise { - const response = await fetch(path); - const blob = await response.blob(); - return new Promise((resolve, reject) => { - const reader = new FileReader(); - reader.onerror = reject; - reader.onload = () => { - if (typeof reader.result === 'string') { - resolve(reader.result); - } else { - reject('method did not return a string'); - } - }; - reader.readAsDataURL(blob); - }); -} - export interface UserPhoto { filepath: string; webviewPath?: string; } ``` -There we go! Each time a new photo is taken, it’s now automatically saved to the filesystem. +Obtaining the camera photo as base64 format on the web appears to be a bit trickier than on mobile. In reality, we’re just using built-in web APIs: [fetch()](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) as a neat way to read the file into blob format, then FileReader’s [readAsDataURL()](https://developer.mozilla.org/en-US/docs/Web/API/FileReader/readAsDataURL) to convert the photo blob to base64. + +There we go! Each time a new photo is taken, it’s now automatically saved to the filesystem. Next up, we'll load and display our saved images. From a386a0c43bdc092d0f67d1e350ae04378c5d89ab Mon Sep 17 00:00:00 2001 From: Maria Hutt Date: Mon, 27 Oct 2025 16:24:46 -0700 Subject: [PATCH 12/16] docs(react): update loading photos page --- docs/react/your-first-app/4-loading-photos.md | 209 +++++++++--------- 1 file changed, 103 insertions(+), 106 deletions(-) diff --git a/docs/react/your-first-app/4-loading-photos.md b/docs/react/your-first-app/4-loading-photos.md index 4ef6ef6284c..e01577e5f9d 100644 --- a/docs/react/your-first-app/4-loading-photos.md +++ b/docs/react/your-first-app/4-loading-photos.md @@ -4,196 +4,187 @@ sidebar_label: Loading Photos --- - Loading Photos from the Filesystem Using A Key-Value Store + Loading Photos from the Filesystem with React | Ionic Capacitor Camera +# Loading Photos from the Filesystem + We’ve implemented photo taking and saving to the filesystem. There’s one last piece of functionality missing: the photos are stored in the filesystem, but we need a way to save pointers to each file so that they can be displayed again in the photo gallery. -Fortunately, this is easy: we’ll leverage the Capacitor [Preferences API](https://capacitorjs.com/docs/apis/preferences) to store our array of Photos in a key-value store. +Fortunately, this is easy: we’ll leverage the Capacitor [Preferences API](../../native/preferences.md) to store our array of Photos in a key-value store. ## Preferences API -Begin by defining a constant variable that will act as the key for the store before the `usePhotoGallery` function definition in `src/hooks/usePhotoGallery.ts`: +Open `usePhotoGallery.ts` and begin by defining a constant variable that will act as the key for the store. -```tsx -// CHANGE: Create a constant variable that will act as a key to store -const PHOTO_STORAGE = 'photos'; +```ts +export function usePhotoGallery() { + const [photos, setPhotos] = useState([]); + // CHANGE: Add a key for photo storage. + const PHOTO_STORAGE = 'photos'; -// Same old code from before -export function usePhotoGallery() {} + // Same old code from before. +} ``` -Then, use the `Storage` class to get access to the get and set methods for reading and writing to device storage: +Next, at the end of the `addNewToGallery()` method, add a call to the `Preferences.set()` method to save the `photos` array. By adding it here, the `photos` array is stored each time a new photo is taken. This way, it doesn’t matter when the app user closes or switches to a different app - all photo data is saved. + +```ts +const addNewToGallery = async () => { + // Same old code from before. + + // CHANGE: Add method to cache all photo data for future retrieval. + Preferences.set({ key: PHOTO_STORAGE, value: JSON.stringify(newPhotos) }); +}; +``` -At the end of the `takePhoto` function, add a call to `Preferences.set()` to save the Photos array. By adding it here, the Photos array is stored each time a new photo is taken. This way, it doesn’t matter when the app user closes or switches to a different app - all photo data is saved. +With the photo array data saved, create a new method in the `usePhotoGallery()` called `loadSaved()` that can retrieve the photo data. We use the same key to retrieve the `photos` array in JSON format, then parse it into an array. -```tsx -// Same old code from before. +```ts export function usePhotoGallery() { - // Same old code from before. + const [photos, setPhotos] = useState([]); - const takePhoto = async () => { - const photo = await Camera.getPhoto({ - resultType: CameraResultType.Uri, - source: CameraSource.Camera, - quality: 100, - }); + const PHOTO_STORAGE = 'photos'; - const newPhotos = [ - { - filepath: fileName, - webviewPath: photo.webPath, - }, - ...photos, - ]; - setPhotos(newPhotos); - // CHANGE: Add a call to save the photos array - Preferences.set({ key: PHOTO_STORAGE, value: JSON.stringify(newPhotos) }); - }; - // Same old code from before - return { - photos, - takePhoto, - }; -} + // CHANGE: Add useEffect hook. + useEffect(() => { + // CHANGE: Add `loadSaved()` method. + const loadSaved = async () => { + const { value } = await Preferences.get({ key: PHOTO_STORAGE }); + const photosInPreferences = (value ? JSON.parse(value) : []) as UserPhoto[]; + }; -// Same old code from before. + loadSaved(); + }, []); + + // Same old code from before. +} ``` -With the photo array data saved, we will create a method that will retrieve the data when the hook loads. We will do so by using React's `useEffect` hook. Insert this above the `takePhoto` declaration. Here is the code, and we will break it down: +The second parameter, the empty dependency array (`[]`), is what tells React to only run the function once. Normally, [useEffect hooks](https://react.dev/reference/react/useEffect) run after every render, but passing an empty array prevents it from running again because none of the dependencies, the values the hook relies on, will ever change. -```tsx -// Same old code from before. +On mobile (coming up next!), we can directly set the source of an image tag - `` - to each photo file on the `Filesystem`, displaying them automatically. On the web, however, we must read each image from the `Filesystem` into base64 format, using a new `base64` property on the `Photo` object. This is because the `Filesystem` API uses [IndexedDB](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API) under the hood. Add the following code to complete the `loadSaved()` method. + +```ts export function usePhotoGallery() { - // Same old code from before. + const [photos, setPhotos] = useState([]); + + const PHOTO_STORAGE = 'photos'; - // CHANGE: Add useEffect hook useEffect(() => { + // CHANGE: Update `loadSaved()` method. const loadSaved = async () => { const { value } = await Preferences.get({ key: PHOTO_STORAGE }); const photosInPreferences = (value ? JSON.parse(value) : []) as UserPhoto[]; - for (let photo of photosInPreferences) { + // CHANGE: Display the photo by reading into base64 format. + for (const photo of photosInPreferences) { const file = await Filesystem.readFile({ path: photo.filepath, directory: Directory.Data, }); - // Web platform only: Load the photo as base64 data photo.webviewPath = `data:image/jpeg;base64,${file.data}`; } + setPhotos(photosInPreferences); }; + loadSaved(); }, []); - const takePhotos = async () => { - // Same old code from before. - }; + // Same old code from before. } - -// Same old code from before. ``` -This seems a bit scary at first, so let's walk through it, first by looking at the second parameter we pass into the hook: the dependency array `[]`. - -The useEffect hook, by default, gets called each time a component renders, unless, we pass in a dependency array. In that case, it will only run when a dependency gets updated. In our case we only want it to be called once. By passing in an empty array, which will not be changed, we can prevent the hook from being called multiple times. - -The first parameter to `useEffect` is the function that will be called by the effect. We pass in an anonymous arrow function, and inside of it we define another asynchronous method and then immediately call this. We have to call the async function from within the hook as the hook callback can't be asynchronous itself. - -On mobile (coming up next!), we can directly set the source of an image tag - `` - to each photo file on the Filesystem, displaying them automatically. On the web, however, we must read each image from the Filesystem into base64 format, because the Filesystem API stores them in base64 within [IndexedDB](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API) under the hood. - `usePhotoGallery.ts` should now look like this: -```tsx +```ts import { useState, useEffect } from 'react'; -import { isPlatform } from '@ionic/react'; import { Camera, CameraResultType, CameraSource, Photo } from '@capacitor/camera'; import { Filesystem, Directory } from '@capacitor/filesystem'; import { Preferences } from '@capacitor/preferences'; -import { Capacitor } from '@capacitor/core'; - -const PHOTO_STORAGE = 'photos'; export function usePhotoGallery() { const [photos, setPhotos] = useState([]); - const fileName = Date.now() + '.jpeg'; - const savePicture = async (photo: Photo, fileName: string): Promise => { - const base64Data = await base64FromPath(photo.webPath!); - const savedFile = await Filesystem.writeFile({ - path: fileName, - data: base64Data, - directory: Directory.Data, - }); - // Use webPath to display the new image instead of base64 since it's - // already loaded into memory - return { - filepath: fileName, - webviewPath: photo.webPath, - }; - }; + const PHOTO_STORAGE = 'photos'; useEffect(() => { const loadSaved = async () => { const { value } = await Preferences.get({ key: PHOTO_STORAGE }); const photosInPreferences = (value ? JSON.parse(value) : []) as UserPhoto[]; - for (let photo of photosInPreferences) { + for (const photo of photosInPreferences) { const file = await Filesystem.readFile({ path: photo.filepath, directory: Directory.Data, }); - // Web platform only: Load the photo as base64 data photo.webviewPath = `data:image/jpeg;base64,${file.data}`; } + setPhotos(photosInPreferences); }; + loadSaved(); }, []); - const takePhoto = async () => { - const photo = await Camera.getPhoto({ + const addNewToGallery = async () => { + // Take a photo + const capturedPhoto = await Camera.getPhoto({ resultType: CameraResultType.Uri, source: CameraSource.Camera, quality: 100, }); - const newPhotos = [ - { - filepath: fileName, - webviewPath: photo.webPath, - }, - ...photos, - ]; + const fileName = Date.now() + '.jpeg'; + // Save the picture and add it to photo collection + const savedImageFile = await savePicture(capturedPhoto, fileName); + + const newPhotos = [savedImageFile, ...photos]; setPhotos(newPhotos); + Preferences.set({ key: PHOTO_STORAGE, value: JSON.stringify(newPhotos) }); }; - return { - photos, - takePhoto, + const savePicture = async (photo: Photo, fileName: string): Promise => { + // Fetch the photo, read as a blob, then convert to base64 format + const response = await fetch(photo.webPath!); + const blob = await response.blob(); + const base64Data = (await convertBlobToBase64(blob)) as string; + + const savedFile = await Filesystem.writeFile({ + path: fileName, + data: base64Data, + directory: Directory.Data, + }); + + // Use webPath to display the new image instead of base64 since it's + // already loaded into memory + return { + filepath: fileName, + webviewPath: photo.webPath, + }; }; -} -export async function base64FromPath(path: string): Promise { - const response = await fetch(path); - const blob = await response.blob(); - return new Promise((resolve, reject) => { - const reader = new FileReader(); - reader.onerror = reject; - reader.onload = () => { - if (typeof reader.result === 'string') { + const convertBlobToBase64 = (blob: Blob) => { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onerror = reject; + reader.onload = () => { resolve(reader.result); - } else { - reject('method did not return a string'); - } - }; - reader.readAsDataURL(blob); - }); + }; + reader.readAsDataURL(blob); + }); + }; + + return { + addNewToGallery, + photos, + }; } export interface UserPhoto { @@ -202,4 +193,10 @@ export interface UserPhoto { } ``` +:::note +If you're seeing broken image links or missing photos after following these steps, you may need to open your browser's dev tools and clear both [localStorage](https://developer.chrome.com/docs/devtools/storage/localstorage) and [IndexedDB](https://developer.chrome.com/docs/devtools/storage/indexeddb). + +In localStorage, look for domain `http://localhost:8100` and key `CapacitorStorage.photos`. In IndexedDB, find a store called "FileStorage". Your photos will have a key like `/DATA/123456789012.jpeg`. +::: + That’s it! We’ve built a complete Photo Gallery feature in our Ionic app that works on the web. Next up, we’ll transform it into a mobile app for iOS and Android! From 6072689c1385b3b77c7a5d2bd080d386c33cb52e Mon Sep 17 00:00:00 2001 From: Maria Hutt Date: Tue, 28 Oct 2025 09:21:03 -0700 Subject: [PATCH 13/16] docs(react): update adding mobile page --- docs/react/your-first-app/5-adding-mobile.md | 262 ++++++++++--------- 1 file changed, 139 insertions(+), 123 deletions(-) diff --git a/docs/react/your-first-app/5-adding-mobile.md b/docs/react/your-first-app/5-adding-mobile.md index 07a0e120329..7a34c6b27fa 100644 --- a/docs/react/your-first-app/5-adding-mobile.md +++ b/docs/react/your-first-app/5-adding-mobile.md @@ -1,125 +1,184 @@ --- +title: Adding Mobile sidebar_label: Adding Mobile --- + + Adding Mobile Support with React | Ionic Capacitor Camera + + + # Adding Mobile Our photo gallery app won’t be complete until it runs on iOS, Android, and the web - all using one codebase. All it takes is some small logic changes to support mobile platforms, installing some native tooling, then running the app on a device. Let’s go! +## Import Platform API + Let’s start with making some small code changes - then our app will “just work” when we deploy it to a device. -## Platform-specific Logic +Import the Ionic [Platform API](../platform.md) into `usePhotoGallery.ts`, which is used to retrieve information about the current device. In this case, it’s useful for selecting which code to execute based on the platform the app is running on (web or mobile). + +Add `isPlatform` to the imports at the top of the file to use the `isPlatform` method. `Capacitor` is also imported to help with file paths on mobile devices. -First, we’ll update the photo saving functionality to support mobile. In the `savePicture` function, check which platform the app is running on. If it’s “hybrid” (Capacitor or Cordova, the two native runtimes), then read the photo file into base64 format using the `readFile` method. Also, return the complete file path to the photo using the Filesystem API. When setting the `webviewPath`, use the special `Capacitor.convertFileSrc` method ([details here](https://ionicframework.com/docs/core-concepts/webview#file-protocol)). Otherwise, use the same logic as before when running the app on the web. +```ts +import { useState, useEffect } from 'react'; +import { Camera, CameraResultType, CameraSource, Photo } from '@capacitor/camera'; +import { Filesystem, Directory } from '@capacitor/filesystem'; +import { Preferences } from '@capacitor/preferences'; +// CHANGE: Add imports. +import { isPlatform } from '@ionic/react'; +import { Capacitor } from '@capacitor/core'; -```tsx // Same old code from before. -export function usePhotoGallery() { - // Same old code from before. +``` - // CHANGE: Update savePicture function - const savePicture = async (photo: Photo, fileName: string): Promise => { - let base64Data: string | Blob; - // "hybrid" will detect Cordova or Capacitor; - if (isPlatform('hybrid')) { +## Platform-specific Logic + +First, we’ll update the photo saving functionality to support mobile. In the `savePicture` method, check which platform the app is running on. If it’s “hybrid” (Capacitor, the native runtime), then read the photo file into base64 format using the `Filesystem`'s' `readFile()` method. Otherwise, use the same logic as before when running the app on the web. + +Update `savePicture` to look like the following: + +```ts +// CHANGE: Update the `savePicture()` method. +const savePicture = async (photo: Photo, fileName: string): Promise => { + let base64Data: string | Blob; + // CHANGE: Add platform check. + // "hybrid" will detect mobile - iOS or Android + if (isPlatform('hybrid')) { + const file = await Filesystem.readFile({ + path: photo.path!, + }); + base64Data = file.data; + } else { + // Fetch the photo, read as a blob, then convert to base64 format + const response = await fetch(photo.webPath!); + const blob = await response.blob(); + base64Data = (await convertBlobToBase64(blob)) as string; + } + + const savedFile = await Filesystem.writeFile({ + path: fileName, + data: base64Data, + directory: Directory.Data, + }); + + // CHANGE: Add platform check. + if (isPlatform('hybrid')) { + // Display the new image by rewriting the 'file://' path to HTTP + return { + filepath: savedFile.uri, + webviewPath: Capacitor.convertFileSrc(savedFile.uri), + }; + } else { + // Use webPath to display the new image instead of base64 since it's + // already loaded into memory + return { + filepath: fileName, + webviewPath: photo.webPath, + }; + } +}; +``` + +Next, add a new bit of logic in the `loadSaved()` method. On mobile, we can directly point to each photo file on the Filesystem and display them automatically. On the web, however, we must read each image from the Filesystem into base64 format. This is because the Filesystem API uses [IndexedDB](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API) under the hood. Update the `loadSaved()` method: + +```ts +// CHANGE: Update the `loadSaved` method. +const loadSaved = async () => { + const { value: photoList } = await Preferences.get({ key: PHOTO_STORAGE }); + const photosInPreferences = (photoList ? JSON.parse(photoList) : []) as UserPhoto[]; + + // CHANGE: Add platform check. + // If running on the web... + if (!isPlatform('hybrid')) { + for (const photo of photosInPreferences) { const file = await Filesystem.readFile({ - path: photo.path!, + path: photo.filepath, + directory: Directory.Data, }); - base64Data = file.data; - } else { - base64Data = await base64FromPath(photo.webPath!); + photo.webviewPath = `data:image/jpeg;base64,${file.data}`; } - const savedFile = await Filesystem.writeFile({ - path: fileName, - data: base64Data, - directory: Directory.Data, - }); + } - if (isPlatform('hybrid')) { - // Display the new image by rewriting the 'file://' path to HTTP - // Details: https://ionicframework.com/docs/building/webview#file-protocol - return { - filepath: savedFile.uri, - webviewPath: Capacitor.convertFileSrc(savedFile.uri), - }; - } else { - // Use webPath to display the new image instead of base64 since it's - // already loaded into memory - return { - filepath: fileName, - webviewPath: photo.webPath, - }; - } - }; + setPhotos(photosInPreferences); +}; +``` - // Same old code from before. -} +Our Photo Gallery now consists of one codebase that runs on the web, Android, and iOS. -// Same old code from before. -``` +`usePhotoGallery.ts` should now look like this: -Next, add a new bit of logic in the `loadSaved` function. On mobile, we can directly point to each photo file on the Filesystem and display them automatically. On the web, however, we must read each image from the Filesystem into base64 format. This is because the Filesystem API uses [IndexedDB](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API) under the hood. Update the `loadSaved` function inside of `useEffect` to: +```ts +import { useState, useEffect } from 'react'; +import { Camera, CameraResultType, CameraSource, Photo } from '@capacitor/camera'; +import { Filesystem, Directory } from '@capacitor/filesystem'; +import { Preferences } from '@capacitor/preferences'; +import { isPlatform } from '@ionic/react'; +import { Capacitor } from '@capacitor/core'; -```tsx -// Same old code from before. export function usePhotoGallery() { - // Same old code from before. + const [photos, setPhotos] = useState([]); + + const PHOTO_STORAGE = 'photos'; useEffect(() => { - // CHANGE: Update loadSaved function within useEffect const loadSaved = async () => { - const { value } = await Preferences.get({ key: PHOTO_STORAGE }); + const { value: photoList } = await Preferences.get({ key: PHOTO_STORAGE }); + const photosInPreferences = (photoList ? JSON.parse(photoList) : []) as UserPhoto[]; - const photosInPreferences = (value ? JSON.parse(value) : []) as UserPhoto[]; // If running on the web... if (!isPlatform('hybrid')) { - for (let photo of photosInPreferences) { + for (const photo of photosInPreferences) { const file = await Filesystem.readFile({ path: photo.filepath, directory: Directory.Data, }); - // Web platform only: Load the photo as base64 data photo.webviewPath = `data:image/jpeg;base64,${file.data}`; } } + setPhotos(photosInPreferences); }; - }, []); - - // Same old code from before. -} -// Same old code from before. -``` + loadSaved(); + }, []); -Our Photo Gallery now consists of one codebase that runs on the web, Android, and iOS. Next up, the part you’ve been waiting for - deploying the app to a device. + const addNewToGallery = async () => { + // Take a photo + const capturedPhoto = await Camera.getPhoto({ + resultType: CameraResultType.Uri, + source: CameraSource.Camera, + quality: 100, + }); -`usePhotoGallery.ts` should now look like this: + const fileName = Date.now() + '.jpeg'; + // Save the picture and add it to photo collection + const savedImageFile = await savePicture(capturedPhoto, fileName); -```tsx -import { useState, useEffect } from 'react'; -import { isPlatform } from '@ionic/react'; -import { Camera, CameraResultType, CameraSource, Photo } from '@capacitor/camera'; -import { Filesystem, Directory } from '@capacitor/filesystem'; -import { Preferences } from '@capacitor/preferences'; -import { Capacitor } from '@capacitor/core'; + const newPhotos = [savedImageFile, ...photos]; + setPhotos(newPhotos); -const PHOTO_STORAGE = 'photos'; + Preferences.set({ key: PHOTO_STORAGE, value: JSON.stringify(newPhotos) }); + }; -export function usePhotoGallery() { - const [photos, setPhotos] = useState([]); - const fileName = Date.now() + '.jpeg'; const savePicture = async (photo: Photo, fileName: string): Promise => { let base64Data: string | Blob; - // "hybrid" will detect Cordova or Capacitor; + // "hybrid" will detect mobile - iOS or Android if (isPlatform('hybrid')) { const file = await Filesystem.readFile({ path: photo.path!, }); base64Data = file.data; } else { - base64Data = await base64FromPath(photo.webPath!); + // Fetch the photo, read as a blob, then convert to base64 format + const response = await fetch(photo.webPath!); + const blob = await response.blob(); + base64Data = (await convertBlobToBase64(blob)) as string; } + const savedFile = await Filesystem.writeFile({ path: fileName, data: base64Data, @@ -128,7 +187,6 @@ export function usePhotoGallery() { if (isPlatform('hybrid')) { // Display the new image by rewriting the 'file://' path to HTTP - // Details: https://ionicframework.com/docs/building/webview#file-protocol return { filepath: savedFile.uri, webviewPath: Capacitor.convertFileSrc(savedFile.uri), @@ -143,69 +201,27 @@ export function usePhotoGallery() { } }; - useEffect(() => { - const loadSaved = async () => { - const { value } = await Preferences.get({ key: PHOTO_STORAGE }); - - const photosInPreferences = (value ? JSON.parse(value) : []) as UserPhoto[]; - // If running on the web... - if (!isPlatform('hybrid')) { - for (let photo of photosInPreferences) { - const file = await Filesystem.readFile({ - path: photo.filepath, - directory: Directory.Data, - }); - // Web platform only: Load the photo as base64 data - photo.webviewPath = `data:image/jpeg;base64,${file.data}`; - } - } - setPhotos(photosInPreferences); - }; - }, []); - - const takePhoto = async () => { - const photo = await Camera.getPhoto({ - resultType: CameraResultType.Uri, - source: CameraSource.Camera, - quality: 100, + const convertBlobToBase64 = (blob: Blob) => { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onerror = reject; + reader.onload = () => { + resolve(reader.result); + }; + reader.readAsDataURL(blob); }); - - const newPhotos = [ - { - filepath: fileName, - webviewPath: photo.webPath, - }, - ...photos, - ]; - setPhotos(newPhotos); - Preferences.set({ key: PHOTO_STORAGE, value: JSON.stringify(newPhotos) }); }; return { + addNewToGallery, photos, - takePhoto, }; } -export async function base64FromPath(path: string): Promise { - const response = await fetch(path); - const blob = await response.blob(); - return new Promise((resolve, reject) => { - const reader = new FileReader(); - reader.onerror = reject; - reader.onload = () => { - if (typeof reader.result === 'string') { - resolve(reader.result); - } else { - reject('method did not return a string'); - } - }; - reader.readAsDataURL(blob); - }); -} - export interface UserPhoto { filepath: string; webviewPath?: string; } ``` + +Next up, the part you’ve been waiting for - deploying the app to a device. From cb2cdff50dbf675c237e1022f4c6397de79a5136 Mon Sep 17 00:00:00 2001 From: Maria Hutt Date: Tue, 28 Oct 2025 13:28:28 -0700 Subject: [PATCH 14/16] docs(react): update live reload page --- docs/react/your-first-app/7-live-reload.md | 477 ++++++++++----------- 1 file changed, 227 insertions(+), 250 deletions(-) diff --git a/docs/react/your-first-app/7-live-reload.md b/docs/react/your-first-app/7-live-reload.md index b53c237f665..dd6ae69b589 100644 --- a/docs/react/your-first-app/7-live-reload.md +++ b/docs/react/your-first-app/7-live-reload.md @@ -1,180 +1,56 @@ --- +title: Rapid App Development with Live Reload sidebar_label: Live Reload --- + + Rapid App Development with Live Reload with React | Ionic Capacitor Camera + + + # Rapid App Development with Live Reload So far, we’ve seen how easy it is to develop a cross-platform app that works everywhere. The development experience is pretty quick, but what if I told you there was a way to go faster? -We can use the Ionic CLI’s [Live Reload functionality](https://ionicframework.com/docs/cli/livereload) to boost our productivity when building Ionic apps. When active, Live Reload will reload the browser and/or WebView when changes in the app are detected. +We can use the Ionic CLI’s [Live Reload functionality](../../cli/livereload.md) to boost our productivity when building Ionic apps. When active, Live Reload will reload the browser and/or WebView when changes in the app are detected. ## Live Reload Remember `ionic serve`? That was Live Reload working in the browser, allowing us to iterate quickly. -We can also use it when developing on iOS and Android devices. This is particularly useful when writing code that interacts with native plugins. Since we need to run native plugin code on a device in order to verify that it works, having a way to quickly write code, build and deploy it, then test it is crucial to keeping up our development speed. +We can also use it when developing on iOS and Android devices. This is particularly useful when writing code that interacts with native plugins - we must run it on a device to verify that it works. Therefore, being able to quickly write, build, test, and deploy code is crucial to keeping up our development speed. Let’s use Live Reload to implement photo deletion, the missing piece of our Photo Gallery feature. Select your platform of choice (iOS or Android) and connect a device to your computer. Next, run either command in a terminal, based on your chosen platform: ```shell -$ ionic cap run ios -l --external +ionic cap run ios -l --external -$ ionic cap run android -l --external +ionic cap run android -l --external ``` The Live Reload server will start up, and the native IDE of choice will open if not opened already. Within the IDE, click the Play button to launch the app onto your device. ## Deleting Photos -With Live Reload running and the app open on your device, let’s implement photo deletion functionality. In your code editor (not Android Studio or Xcode), open `Tab2.tsx` then import `useState` from React and `UserPhoto` from the `usePhotoGallery` hook: - -```tsx -// Other Imports - -// CHANGE: Import UserPhoto, usePhotoGallery hook and useState from react. -import React, { useState } from 'react'; -import { usePhotoGallery, UserPhoto } from '../hooks/usePhotoGallery'; - -const Tab2: React.FC = () => {}; -``` - -Next, reference the `deletePhoto` function, which we'll create soon: - -```tsx -// Same old code from before. - -const Tab2: React.FC = () => { - // CHANGE: Reference deletePhoto function - const { photos, takePhoto, deletePhoto } = usePhotoGallery(); - - // Same old code from before. -}; -``` - -Next, add a state value to store information about the photo to delete: - -```tsx -// Same old code from before. - -const Tab2: React.FC = () => { - // Same old code from before. - - // CHANGE: Add a state value for photo deletion. - const [photoToDelete, setPhotoToDelete] = useState(); - - // Same old code from before. -}; -``` - -When a user clicks on an image, we will show the action sheet by changing the state value to the photo. Update the `` element to: +With Live Reload running and the app open on your device, let’s implement photo deletion functionality. -```tsx -// Same old code from before. +In `usePhotoGallery.ts`, add the `deletePhoto()` method. The selected photo is removed from the `photos` array first. Then, we delete the actual photo file itself using the Filesystem API. -const Tab2: React.FC = () => { - // Same old code from before. - - return ( - - - - Tab 2 - - - - - - {photos.map((photo, index) => ( - - - setPhotoToDelete(photo)} src={photo.webviewPath} /> - - ))} - - - - takePhoto()}> - - - - - - ); -}; -``` - -Next, add an [IonActionSheet](https://ionicframework.com/docs/api/action-sheet) dialog with the option to either delete the selected photo or cancel (close) the dialog. We will set the isOpen property based on if photoToDelete has a value or not. - -In the JSX, put the following component before the closing `` tag. - -```tsx -// Same old code from before. - -const Tab2: React.FC = () => { - // Same old code from before. - - return ( - - - - Tab 2 - - - - - - {photos.map((photo, index) => ( - - setPhotoToDelete(photo)} src={photo.webviewPath} /> - - ))} - - - - takePhoto()}> - - - - - { - if (photoToDelete) { - deletePhoto(photoToDelete); - setPhotoToDelete(undefined); - } - }, - }, - { - text: 'Cancel', - icon: close, - role: 'cancel', - }, - ]} - onDidDismiss={() => setPhotoToDelete(undefined)} - /> - - - ); -}; -``` - -Above, we added two options: `Delete` that calls `deletePhoto` function (to be added next) and `Cancel`, which when given the role of “cancel” will automatically close the action sheet. It's also important to set the onDidDismiss function and set our photoToDelete back to undefined when the modal goes away. That way, when another image is clicked, the action sheet notices the change in the value of photoToDelete. - -Next, we need to implement the deletePhoto method that will come from the `usePhotoGallery` hook. Open the file and paste in the following function in the hook: - -```tsx -// Same old code from before. +```ts +import { useState, useEffect } from 'react'; +import { Camera, CameraResultType, CameraSource, Photo } from '@capacitor/camera'; +import { Filesystem, Directory } from '@capacitor/filesystem'; +import { Preferences } from '@capacitor/preferences'; +import { isPlatform } from '@ionic/react'; +import { Capacitor } from '@capacitor/core'; export function usePhotoGallery() { // Same old code from before. - // CHANGE: Implement deletePhoto method within usePhotoGallery hook. + // CHANGE: Add `deletePhoto()` method. const deletePhoto = async (photo: UserPhoto) => { // Remove this photo from the Photos reference data array const newPhotos = photos.filter((p) => p.filepath !== photo.filepath); @@ -188,75 +64,51 @@ export function usePhotoGallery() { path: filename, directory: Directory.Data, }); + setPhotos(newPhotos); }; - // CHANGE: Update return and add deletePhoto function return { photos, takePhoto, + // CHANGE: Add `deletePhoto()` to the return statement. deletePhoto, }; } -// Same old code from before. +export interface UserPhoto { + filepath: string; + webviewPath?: string; +} ``` -The selected photo is removed from the Photos array first. Then, we use the Capacitor Preferences API to update the cached version of the Photos array. Finally, we delete the actual photo file itself using the Filesystem API. - -Save this file, then tap on a photo again and choose the “Delete” option. This time, the photo is deleted! Implemented much faster using Live Reload. 💪 - -In the final portion of this tutorial, we’ll walk you through the basics of the Appflow product used to build and deploy your application to users' devices. - -`Tab2.tsx` should look like this: +Next, in `Tab2.tsx`, implement the `IonActionSheet` component. We're adding two options: "Delete", which calls `usePhotoGallery.deletePicture()`, and "Cancel". The cancel button will automatically closes the action sheet when assigned the "cancel" role. ```tsx -import { camera, trash, close } from 'ionicons/icons'; -import { - IonContent, - IonHeader, - IonPage, - IonTitle, - IonToolbar, - IonFab, - IonFabButton, - IonIcon, - IonGrid, - IonRow, - IonCol, - IonImg, - IonActionSheet, -} from '@ionic/react'; -import ExploreContainer from '../components/ExploreContainer'; -import './Tab2.css'; -import { usePhotoGallery, UserPhoto } from '../hooks/usePhotoGallery'; +// Same old code from before. +// change: Add React import. +import { useState } from 'react'; +// CHANGE: Add `UserPhoto` type import. +import type { UserPhoto } from '../hooks/usePhotoGallery'; const Tab2: React.FC = () => { - const { photos, takePhoto, deletePhoto } = usePhotoGallery(); + // CHANGE: Add `deletePhoto()` method. + const { photos, addNewToGallery, deletePhoto } = usePhotoGallery(); + // CHANGE: Add state for the photo to delete. const [photoToDelete, setPhotoToDelete] = useState(); return ( - - - Tab 2 - - - - - {photos.map((photo, index) => ( - - setPhotoToDelete(photo)} src={photo.webviewPath} /> - - ))} - - + {/* Same old code from before. */} + - takePhoto()}> + addNewToGallery()}> + + {/* CHANGE: Add action sheet for deleting photos. */} { text: 'Cancel', icon: close, role: 'cancel', + handler: () => { + // Nothing to do, action sheet is automatically closed + }, }, ]} onDidDismiss={() => setPhotoToDelete(undefined)} - /> + > ); }; - -export default Tab2; ``` -`usePhotoGallery.ts` should look like this: +Add a click handler to the `` element. When the app user taps on a photo in our gallery, we’ll display an [Action Sheet](../../api/action-sheet.md) dialog with the option to either delete the selected photo or cancel (close) the dialog. ```tsx + + + {photos.map((photo) => ( + + {/* CHANGE: Add a click event listener to each image. */} + setPhotoToDelete(photo)} /> + + ))} + + +``` + +Remember that removing the photo from the `photos` array triggers the `cachePhotos` method for us automatically. + +Tap on a photo again and choose the “Delete” option. The photo is deleted! Implemented much faster using Live Reload. 💪 + +In the final portion of this tutorial, we’ll walk you through the basics of the Appflow product used to build and deploy your application to users' devices. + +
+ Full code for `usePhotoGallery.ts` + +```ts import { useState, useEffect } from 'react'; -import { isPlatform } from '@ionic/react'; import { Camera, CameraResultType, CameraSource, Photo } from '@capacitor/camera'; import { Filesystem, Directory } from '@capacitor/filesystem'; import { Preferences } from '@capacitor/preferences'; +import { isPlatform } from '@ionic/react'; import { Capacitor } from '@capacitor/core'; -const PHOTO_STORAGE = 'photos'; - export function usePhotoGallery() { const [photos, setPhotos] = useState([]); + const PHOTO_STORAGE = 'photos'; + + useEffect(() => { + const loadSaved = async () => { + const { value: photoList } = await Preferences.get({ key: PHOTO_STORAGE }); + const photosInPreferences = (photoList ? JSON.parse(photoList) : []) as UserPhoto[]; + + // If running on the web... + if (!isPlatform('hybrid')) { + for (const photo of photosInPreferences) { + const file = await Filesystem.readFile({ + path: photo.filepath, + directory: Directory.Data, + }); + photo.webviewPath = `data:image/jpeg;base64,${file.data}`; + } + } + + setPhotos(photosInPreferences); + }; + + loadSaved(); + }, []); + + const addNewToGallery = async () => { + // Take a photo + const capturedPhoto = await Camera.getPhoto({ + resultType: CameraResultType.Uri, + source: CameraSource.Camera, + quality: 100, + }); + + const fileName = Date.now() + '.jpeg'; + // Save the picture and add it to photo collection + const savedImageFile = await savePicture(capturedPhoto, fileName); + + const newPhotos = [savedImageFile, ...photos]; + setPhotos(newPhotos); + + Preferences.set({ key: PHOTO_STORAGE, value: JSON.stringify(newPhotos) }); + }; + const savePicture = async (photo: Photo, fileName: string): Promise => { let base64Data: string | Blob; - // "hybrid" will detect Cordova or Capacitor: + // "hybrid" will detect mobile - iOS or Android if (isPlatform('hybrid')) { const file = await Filesystem.readFile({ path: photo.path!, }); base64Data = file.data; } else { - base64Data = await base64FromPath(photo.webPath!); + // Fetch the photo, read as a blob, then convert to base64 format + const response = await fetch(photo.webPath!); + const blob = await response.blob(); + base64Data = (await convertBlobToBase64(blob)) as string; } const savedFile = await Filesystem.writeFile({ @@ -322,7 +240,6 @@ export function usePhotoGallery() { if (isPlatform('hybrid')) { // Display the new image by rewriting the 'file://' path to HTTP - // Details: https://ionicframework.com/docs/building/webview#file-protocol return { filepath: savedFile.uri, webviewPath: Capacitor.convertFileSrc(savedFile.uri), @@ -337,40 +254,18 @@ export function usePhotoGallery() { } }; - useEffect(() => { - const loadSaved = async () => { - const { value } = await Preferences.get({ key: PHOTO_STORAGE }); - const photosInPreferences = (value ? JSON.parse(value) : []) as UserPhoto[]; - - if (!isPlatform('hybrid')) { - for (let photo of photosInPreferences) { - const file = await Filesystem.readFile({ - path: photo.filepath, - directory: Directory.Data, - }); - // Web platform only: Load the photo as base64 data - photo.webviewPath = `data:image/jpeg;base64,${file.data}`; - } - } - setPhotos(photosInPreferences); - }; - loadSaved(); - }, []); - - const takePhoto = async () => { - const photo = await Camera.getPhoto({ - resultType: CameraResultType.Uri, - source: CameraSource.Camera, - quality: 100, + const convertBlobToBase64 = (blob: Blob) => { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onerror = reject; + reader.onload = () => { + resolve(reader.result); + }; + reader.readAsDataURL(blob); }); - - const fileName = Date.now() + '.jpeg'; - const savedFileImage = await savePicture(photo, fileName); - const newPhotos = [savedFileImage, ...photos]; - setPhotos(newPhotos); - Preferences.set({ key: PHOTO_STORAGE, value: JSON.stringify(newPhotos) }); }; + // CHANGE: Add `deletePhoto()` method. const deletePhoto = async (photo: UserPhoto) => { // Remove this photo from the Photos reference data array const newPhotos = photos.filter((p) => p.filepath !== photo.filepath); @@ -384,35 +279,117 @@ export function usePhotoGallery() { path: filename, directory: Directory.Data, }); + setPhotos(newPhotos); }; return { + addNewToGallery, photos, - takePhoto, + // CHANGE: Add `deletePhoto()` to the return statement. deletePhoto, }; } -export async function base64FromPath(path: string): Promise { - const response = await fetch(path); - const blob = await response.blob(); - return new Promise((resolve, reject) => { - const reader = new FileReader(); - reader.onerror = reject; - reader.onload = () => { - if (typeof reader.result === 'string') { - resolve(reader.result); - } else { - reject('method did not return a string'); - } - }; - reader.readAsDataURL(blob); - }); -} - export interface UserPhoto { filepath: string; webviewPath?: string; } ``` + +
+ +
+ Full code for `Tab2.tsx` + +```tsx +import { camera, trash, close } from 'ionicons/icons'; +import { + IonContent, + IonHeader, + IonPage, + IonTitle, + IonToolbar, + IonFab, + IonFabButton, + IonIcon, + IonGrid, + IonRow, + IonCol, + IonImg, + IonActionSheet, +} from '@ionic/react'; +import { useState } from 'react'; +import type { UserPhoto } from '../hooks/usePhotoGallery'; +import { usePhotoGallery } from '../hooks/usePhotoGallery'; +import './Tab2.css'; + +const Tab2: React.FC = () => { + const { photos, addNewToGallery, deletePhoto } = usePhotoGallery(); + const [photoToDelete, setPhotoToDelete] = useState(); + + return ( + + + + Photo Gallery + + + + + + Photo Gallery + + + + + + {photos.map((photo) => ( + + {/* CHANGE: Add a click event listener to each image. */} + setPhotoToDelete(photo)} /> + + ))} + + + + + addNewToGallery()}> + + + + + { + if (photoToDelete) { + deletePhoto(photoToDelete); + setPhotoToDelete(undefined); + } + }, + }, + { + text: 'Cancel', + icon: close, + role: 'cancel', + handler: () => { + // Nothing to do, action sheet is automatically closed + }, + }, + ]} + onDidDismiss={() => setPhotoToDelete(undefined)} + > + + + ); +}; + +export default Tab2; +``` + +
From 978d7e6d04f119c64d87dac7ec619c8d2e5b0580 Mon Sep 17 00:00:00 2001 From: Maria Hutt Date: Tue, 28 Oct 2025 14:52:09 -0700 Subject: [PATCH 15/16] docs(react): update your first app pages --- docs/react/your-first-app.md | 10 +- docs/react/your-first-app/2-taking-photos.md | 12 +- docs/react/your-first-app/3-saving-photos.md | 18 +- docs/react/your-first-app/4-loading-photos.md | 12 +- docs/react/your-first-app/5-adding-mobile.md | 2 +- .../your-first-app/6-deploying-mobile.md | 21 +- docs/react/your-first-app/7-live-reload.md | 235 +----------------- docs/react/your-first-app/8-distribute.md | 9 + 8 files changed, 61 insertions(+), 258 deletions(-) diff --git a/docs/react/your-first-app.md b/docs/react/your-first-app.md index 8b744d1deba..c10e0c7bd9c 100644 --- a/docs/react/your-first-app.md +++ b/docs/react/your-first-app.md @@ -4,13 +4,15 @@ sidebar_label: Build Your First App --- - Build Your First Ionic Mobile App: React Development Tutorial + Build Your First Ionic Mobile App with React | Ionic Capacitor Camera +# Your First Ionic App: React + The great thing about Ionic is that with one codebase, you can build for any platform using just HTML, CSS, and JavaScript. Follow along as we learn the fundamentals of Ionic app development by creating a realistic app step by step. Here’s the finished app running on all 3 platforms: @@ -34,7 +36,7 @@ Highlights include: - Deployed as a native iOS and Android mobile app using [Capacitor](https://capacitorjs.com), Ionic's official native app runtime. - Photo Gallery functionality powered by the Capacitor [Camera](../native/camera.md), [Filesystem](../native/filesystem.md), and [Preferences](../native/preferences.md) APIs. -Find the complete app code referenced in this guide [on GitHub](https://github.com/ionic-team/tutorial-photo-gallery-react). +Find the [complete app code](https://github.com/ionic-team/tutorial-photo-gallery-react) referenced in this guide on GitHub. ## Download Required Tools @@ -66,7 +68,7 @@ Consider setting up npm to operate globally without elevated permissions. See [R ## Create an App -Next, create an Ionic React app that uses the “Tabs” starter template and adds Capacitor for native functionality: +Next, create an Ionic React app that uses the "Tabs" starter template and adds Capacitor for native functionality: ```shell ionic start photo-gallery tabs --type=react --capacitor diff --git a/docs/react/your-first-app/2-taking-photos.md b/docs/react/your-first-app/2-taking-photos.md index 9da35706ba5..305970a351a 100644 --- a/docs/react/your-first-app/2-taking-photos.md +++ b/docs/react/your-first-app/2-taking-photos.md @@ -4,13 +4,15 @@ sidebar_label: Taking Photos --- - Build Camera API for iOS, Android & Web | Ionic Capacitor Camera + Take Photos with Camera API for iOS, Android & Web with React | Ionic Capacitor Camera +# Taking Photos with the Camera + Now for the fun part - adding the ability to take photos with the device’s camera using the Capacitor [Camera API](../../native/camera.md). We’ll begin with building it for the web, then make some small tweaks to make it work on mobile (iOS and Android). ## Photo Gallery Hook @@ -45,7 +47,7 @@ export function usePhotoGallery() { Notice the magic here: there's no platform-specific code (web, iOS, or Android)! The Capacitor Camera plugin abstracts that away for us, leaving just one method call - `getPhoto()` - that will open up the device's camera and allow us to take photos. -Next, in `Tab2.tsx`, import the `usePhotoGallery` method and destructure it to call its `addNewToGallery` method. +Next, in `Tab2.tsx`, import the `usePhotoGallery()` method and destructure it to call its `addNewToGallery()` method. ```tsx import { camera, trash, close } from 'ionicons/icons'; @@ -112,7 +114,7 @@ After taking a photo, it disappears right away. We need to display it within our Return to `usePhotoGallery.ts`. -Outside of the `usePhotoGallery` method definition (the very bottom of the file), create a new interface, `UserPhoto`, to hold our photo metadata. +Outside of the `usePhotoGallery()` method definition (the very bottom of the file), create a new interface, `UserPhoto`, to hold our photo metadata. ```ts export function usePhotoGallery { @@ -130,14 +132,14 @@ Above the `addNewToGallery()` method, define an array of `UserPhoto`, which will ```ts export function usePhotoGallery { - // CHANGE: Add the photos array. + // CHANGE: Add the `photos` array. const [photos, setPhotos] = useState([]); // Same old code from before. } ``` -Over in the `addNewToGallery()` method, add the newly captured photo to the beginning of the `photos` array. Then, update the `userPhotoGallery` return statement with the `photos` array. +Over in the `addNewToGallery()` method, add the newly captured photo to the beginning of the `photos` array. Then, update the `userPhotoGallery()` return statement with the `photos` array. ```ts export function usePhotoGallery() { diff --git a/docs/react/your-first-app/3-saving-photos.md b/docs/react/your-first-app/3-saving-photos.md index 92fc228df78..a1ec2b70c86 100644 --- a/docs/react/your-first-app/3-saving-photos.md +++ b/docs/react/your-first-app/3-saving-photos.md @@ -3,6 +3,14 @@ title: Saving Photos to the Filesystem sidebar_label: Saving Photos --- + + Saving Photos to the Filesystem with React | Ionic Capacitor Camera + + + # Saving Photos to the Filesystem We’re now able to take multiple photos and display them in a photo gallery on the second tab of our app. These photos, however, are not currently being stored permanently, so when the app is closed, they will be deleted. @@ -60,12 +68,13 @@ export function usePhotoGallery() { }); const fileName = Date.now() + '.jpeg'; - // CHANGE: Add `savedImageFile()`. + // CHANGE: Add `savedImageFile`. // Save the picture and add it to photo collection const savedImageFile = await savePicture(capturedPhoto, fileName); // CHANGE: Update state with new photo. - setPhotos([savedImageFile, ...photos]); + const newPhotos = [savedImageFile, ...photos]; + setPhotos(newPhotos); }; // CHANGE: Add `savePicture()` method. @@ -124,7 +133,7 @@ export function usePhotoGallery() { }; }; - // CHANGE: Add the `convertBlobToBase64()` method. + // CHANGE: Add `convertBlobToBase64()` method. const convertBlobToBase64 = (blob: Blob) => { return new Promise((resolve, reject) => { const reader = new FileReader(); @@ -171,7 +180,8 @@ export function usePhotoGallery() { // Save the picture and add it to photo collection const savedImageFile = await savePicture(capturedPhoto, fileName); - setPhotos([savedImageFile, ...photos]); + const newPhotos = [savedImageFile, ...photos]; + setPhotos(newPhotos); }; const savePicture = async (photo: Photo, fileName: string): Promise => { diff --git a/docs/react/your-first-app/4-loading-photos.md b/docs/react/your-first-app/4-loading-photos.md index e01577e5f9d..050fbc9e513 100644 --- a/docs/react/your-first-app/4-loading-photos.md +++ b/docs/react/your-first-app/4-loading-photos.md @@ -54,8 +54,8 @@ export function usePhotoGallery() { useEffect(() => { // CHANGE: Add `loadSaved()` method. const loadSaved = async () => { - const { value } = await Preferences.get({ key: PHOTO_STORAGE }); - const photosInPreferences = (value ? JSON.parse(value) : []) as UserPhoto[]; + const { value: photoList } = await Preferences.get({ key: PHOTO_STORAGE }); + const photosInPreferences = (photoList ? JSON.parse(photoList) : []) as UserPhoto[]; }; loadSaved(); @@ -78,8 +78,8 @@ export function usePhotoGallery() { useEffect(() => { // CHANGE: Update `loadSaved()` method. const loadSaved = async () => { - const { value } = await Preferences.get({ key: PHOTO_STORAGE }); - const photosInPreferences = (value ? JSON.parse(value) : []) as UserPhoto[]; + const { value: photoList } = await Preferences.get({ key: PHOTO_STORAGE }); + const photosInPreferences = (photoList ? JSON.parse(photoList) : []) as UserPhoto[]; // CHANGE: Display the photo by reading into base64 format. for (const photo of photosInPreferences) { @@ -115,8 +115,8 @@ export function usePhotoGallery() { useEffect(() => { const loadSaved = async () => { - const { value } = await Preferences.get({ key: PHOTO_STORAGE }); - const photosInPreferences = (value ? JSON.parse(value) : []) as UserPhoto[]; + const { value: photoList } = await Preferences.get({ key: PHOTO_STORAGE }); + const photosInPreferences = (photoList ? JSON.parse(photoList) : []) as UserPhoto[]; for (const photo of photosInPreferences) { const file = await Filesystem.readFile({ diff --git a/docs/react/your-first-app/5-adding-mobile.md b/docs/react/your-first-app/5-adding-mobile.md index 7a34c6b27fa..bf6fe31d14d 100644 --- a/docs/react/your-first-app/5-adding-mobile.md +++ b/docs/react/your-first-app/5-adding-mobile.md @@ -86,7 +86,7 @@ const savePicture = async (photo: Photo, fileName: string): Promise = Next, add a new bit of logic in the `loadSaved()` method. On mobile, we can directly point to each photo file on the Filesystem and display them automatically. On the web, however, we must read each image from the Filesystem into base64 format. This is because the Filesystem API uses [IndexedDB](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API) under the hood. Update the `loadSaved()` method: ```ts -// CHANGE: Update the `loadSaved` method. +// CHANGE: Update `loadSaved` method. const loadSaved = async () => { const { value: photoList } = await Preferences.get({ key: PHOTO_STORAGE }); const photosInPreferences = (photoList ? JSON.parse(photoList) : []) as UserPhoto[]; diff --git a/docs/react/your-first-app/6-deploying-mobile.md b/docs/react/your-first-app/6-deploying-mobile.md index da780fa76ab..c8c1e9ebbea 100644 --- a/docs/react/your-first-app/6-deploying-mobile.md +++ b/docs/react/your-first-app/6-deploying-mobile.md @@ -1,10 +1,23 @@ --- +title: Deploying to iOS and Android sidebar_label: Deploying Mobile --- + + Adding Mobile Support with React | Ionic Capacitor Camera + + + # Deploying to iOS and Android -Since we added Capacitor to our project when it was first created, there’s only a handful of steps remaining until the Photo Gallery app is on our device! Remember, you can find the complete source code for this app [here](https://github.com/ionic-team/photo-gallery-capacitor-react). +Since we added Capacitor to our project when it was first created, there’s only a handful of steps remaining until the Photo Gallery app is on our device! + +:::note +Remember, you can find the complete source code for this app [here](https://github.com/ionic-team/photo-gallery-capacitor-react). +::: ## Capacitor Setup @@ -19,8 +32,8 @@ ionic build Next, create both the iOS and Android projects: ```shell -$ ionic cap add ios -$ ionic cap add android +ionic cap add ios +ionic cap add android ``` Both android and ios folders at the root of the project are created. These are entirely standalone native projects that should be considered part of your Ionic app (i.e., check them into source control, edit them using their native tooling, etc.). @@ -51,7 +64,7 @@ First, run the Capacitor `open` command, which opens the native iOS project in X ionic cap open ios ``` -In order for some native plugins to work, user permissions must be configured. In our photo gallery app, this includes the Camera plugin: iOS displays a modal dialog automatically after the first time that `Camera.getPhoto()` is called, prompting the user to allow the app to use the Camera. The permission that drives this is labeled “Privacy - Camera Usage.” To set it, the `Info.plist` file must be modified ([more details here](https://capacitorjs.com/docs/ios/configuration)). To access it, click "Info," then expand "Custom iOS Target Properties." +In order for some native plugins to work, user permissions must be configured. In our photo gallery app, this includes the Camera plugin: iOS displays a modal dialog automatically after the first time that `Camera.getPhoto()` is called, prompting the user to allow the app to use the Camera. The permission that drives this is labeled "Privacy - Camera Usage." To set it, the `Info.plist` file must be modified ([more details here](https://capacitorjs.com/docs/ios/configuration)). To access it, click "Info," then expand "Custom iOS Target Properties." ![The Info.plist file in Xcode showing the NSCameraUsageDescription key added for camera access.](/img/guides/first-app-cap-ng/xcode-info-plist.png 'Xcode Info.plist Configuration') diff --git a/docs/react/your-first-app/7-live-reload.md b/docs/react/your-first-app/7-live-reload.md index dd6ae69b589..dfc6e7cbf06 100644 --- a/docs/react/your-first-app/7-live-reload.md +++ b/docs/react/your-first-app/7-live-reload.md @@ -70,7 +70,7 @@ export function usePhotoGallery() { return { photos, - takePhoto, + addNewToGallery, // CHANGE: Add `deletePhoto()` to the return statement. deletePhoto, }; @@ -160,236 +160,3 @@ Remember that removing the photo from the `photos` array triggers the `cachePhot Tap on a photo again and choose the “Delete” option. The photo is deleted! Implemented much faster using Live Reload. 💪 In the final portion of this tutorial, we’ll walk you through the basics of the Appflow product used to build and deploy your application to users' devices. - -
- Full code for `usePhotoGallery.ts` - -```ts -import { useState, useEffect } from 'react'; -import { Camera, CameraResultType, CameraSource, Photo } from '@capacitor/camera'; -import { Filesystem, Directory } from '@capacitor/filesystem'; -import { Preferences } from '@capacitor/preferences'; -import { isPlatform } from '@ionic/react'; -import { Capacitor } from '@capacitor/core'; - -export function usePhotoGallery() { - const [photos, setPhotos] = useState([]); - - const PHOTO_STORAGE = 'photos'; - - useEffect(() => { - const loadSaved = async () => { - const { value: photoList } = await Preferences.get({ key: PHOTO_STORAGE }); - const photosInPreferences = (photoList ? JSON.parse(photoList) : []) as UserPhoto[]; - - // If running on the web... - if (!isPlatform('hybrid')) { - for (const photo of photosInPreferences) { - const file = await Filesystem.readFile({ - path: photo.filepath, - directory: Directory.Data, - }); - photo.webviewPath = `data:image/jpeg;base64,${file.data}`; - } - } - - setPhotos(photosInPreferences); - }; - - loadSaved(); - }, []); - - const addNewToGallery = async () => { - // Take a photo - const capturedPhoto = await Camera.getPhoto({ - resultType: CameraResultType.Uri, - source: CameraSource.Camera, - quality: 100, - }); - - const fileName = Date.now() + '.jpeg'; - // Save the picture and add it to photo collection - const savedImageFile = await savePicture(capturedPhoto, fileName); - - const newPhotos = [savedImageFile, ...photos]; - setPhotos(newPhotos); - - Preferences.set({ key: PHOTO_STORAGE, value: JSON.stringify(newPhotos) }); - }; - - const savePicture = async (photo: Photo, fileName: string): Promise => { - let base64Data: string | Blob; - // "hybrid" will detect mobile - iOS or Android - if (isPlatform('hybrid')) { - const file = await Filesystem.readFile({ - path: photo.path!, - }); - base64Data = file.data; - } else { - // Fetch the photo, read as a blob, then convert to base64 format - const response = await fetch(photo.webPath!); - const blob = await response.blob(); - base64Data = (await convertBlobToBase64(blob)) as string; - } - - const savedFile = await Filesystem.writeFile({ - path: fileName, - data: base64Data, - directory: Directory.Data, - }); - - if (isPlatform('hybrid')) { - // Display the new image by rewriting the 'file://' path to HTTP - return { - filepath: savedFile.uri, - webviewPath: Capacitor.convertFileSrc(savedFile.uri), - }; - } else { - // Use webPath to display the new image instead of base64 since it's - // already loaded into memory - return { - filepath: fileName, - webviewPath: photo.webPath, - }; - } - }; - - const convertBlobToBase64 = (blob: Blob) => { - return new Promise((resolve, reject) => { - const reader = new FileReader(); - reader.onerror = reject; - reader.onload = () => { - resolve(reader.result); - }; - reader.readAsDataURL(blob); - }); - }; - - // CHANGE: Add `deletePhoto()` method. - const deletePhoto = async (photo: UserPhoto) => { - // Remove this photo from the Photos reference data array - const newPhotos = photos.filter((p) => p.filepath !== photo.filepath); - - // Update photos array cache by overwriting the existing photo array - Preferences.set({ key: PHOTO_STORAGE, value: JSON.stringify(newPhotos) }); - - // delete photo file from filesystem - const filename = photo.filepath.substr(photo.filepath.lastIndexOf('/') + 1); - await Filesystem.deleteFile({ - path: filename, - directory: Directory.Data, - }); - - setPhotos(newPhotos); - }; - - return { - addNewToGallery, - photos, - // CHANGE: Add `deletePhoto()` to the return statement. - deletePhoto, - }; -} - -export interface UserPhoto { - filepath: string; - webviewPath?: string; -} -``` - -
- -
- Full code for `Tab2.tsx` - -```tsx -import { camera, trash, close } from 'ionicons/icons'; -import { - IonContent, - IonHeader, - IonPage, - IonTitle, - IonToolbar, - IonFab, - IonFabButton, - IonIcon, - IonGrid, - IonRow, - IonCol, - IonImg, - IonActionSheet, -} from '@ionic/react'; -import { useState } from 'react'; -import type { UserPhoto } from '../hooks/usePhotoGallery'; -import { usePhotoGallery } from '../hooks/usePhotoGallery'; -import './Tab2.css'; - -const Tab2: React.FC = () => { - const { photos, addNewToGallery, deletePhoto } = usePhotoGallery(); - const [photoToDelete, setPhotoToDelete] = useState(); - - return ( - - - - Photo Gallery - - - - - - Photo Gallery - - - - - - {photos.map((photo) => ( - - {/* CHANGE: Add a click event listener to each image. */} - setPhotoToDelete(photo)} /> - - ))} - - - - - addNewToGallery()}> - - - - - { - if (photoToDelete) { - deletePhoto(photoToDelete); - setPhotoToDelete(undefined); - } - }, - }, - { - text: 'Cancel', - icon: close, - role: 'cancel', - handler: () => { - // Nothing to do, action sheet is automatically closed - }, - }, - ]} - onDidDismiss={() => setPhotoToDelete(undefined)} - > - - - ); -}; - -export default Tab2; -``` - -
diff --git a/docs/react/your-first-app/8-distribute.md b/docs/react/your-first-app/8-distribute.md index 398f572b64b..36002ba46b5 100644 --- a/docs/react/your-first-app/8-distribute.md +++ b/docs/react/your-first-app/8-distribute.md @@ -1,7 +1,16 @@ --- +title: Build and Deploy your App sidebar_label: Distribute --- + + Build and Deploy your App with React | Ionic Capacitor Camera + + + # Build and Deploy your App Now that you have built your first app, you are going to want to get it distributed so everyone can start using it. The mechanics of building and deploying your application can be quite cumbersome. That is where [Appflow](https://ionic.io/docs/appflow/) comes into play. Appflow allows you to effectively generate web and native builds, push out live app updates, publish your app to the app stores, and automate the whole process. The entire Quickstart guide can be found [here](https://ionic.io/docs/appflow/quickstart). From 97888594bf9f6e482a198866d4abea1f9a79c82d Mon Sep 17 00:00:00 2001 From: Maria Hutt Date: Tue, 28 Oct 2025 16:53:46 -0700 Subject: [PATCH 16/16] docs(react): update your first app pages --- docs/react/your-first-app.md | 2 +- docs/react/your-first-app/5-adding-mobile.md | 4 ++-- docs/react/your-first-app/6-deploying-mobile.md | 2 +- docs/react/your-first-app/7-live-reload.md | 2 +- docs/react/your-first-app/8-distribute.md | 6 +++--- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/react/your-first-app.md b/docs/react/your-first-app.md index c10e0c7bd9c..c33ad00dc58 100644 --- a/docs/react/your-first-app.md +++ b/docs/react/your-first-app.md @@ -228,7 +228,7 @@ const Tab2: React.FC = () => { export default Tab2; ``` -Next, open `src/App.tsx`. Change the label to “Photos” and the `ellipse` icon to `images` for the middle tab button. +Next, open `src/views/TabsPage.vue`. Change the label to "Photos" and the `ellipse` icon to `images` for the middle tab button. ```tsx import { Redirect, Route } from 'react-router-dom'; diff --git a/docs/react/your-first-app/5-adding-mobile.md b/docs/react/your-first-app/5-adding-mobile.md index bf6fe31d14d..1b2915d2d1e 100644 --- a/docs/react/your-first-app/5-adding-mobile.md +++ b/docs/react/your-first-app/5-adding-mobile.md @@ -37,9 +37,9 @@ import { Capacitor } from '@capacitor/core'; ## Platform-specific Logic -First, we’ll update the photo saving functionality to support mobile. In the `savePicture` method, check which platform the app is running on. If it’s “hybrid” (Capacitor, the native runtime), then read the photo file into base64 format using the `Filesystem`'s' `readFile()` method. Otherwise, use the same logic as before when running the app on the web. +First, we’ll update the photo saving functionality to support mobile. In the `savePicture()` method, check which platform the app is running on. If it’s “hybrid” (Capacitor, the native runtime), then read the photo file into base64 format using the `Filesystem`'s' `readFile()` method. Otherwise, use the same logic as before when running the app on the web. -Update `savePicture` to look like the following: +Update `savePicture()` to look like the following: ```ts // CHANGE: Update the `savePicture()` method. diff --git a/docs/react/your-first-app/6-deploying-mobile.md b/docs/react/your-first-app/6-deploying-mobile.md index c8c1e9ebbea..d6101c33d09 100644 --- a/docs/react/your-first-app/6-deploying-mobile.md +++ b/docs/react/your-first-app/6-deploying-mobile.md @@ -56,7 +56,7 @@ ionic cap sync To build an iOS app, you’ll need a Mac computer. ::: -Capacitor iOS apps are configured and managed through Xcode (Apple’s iOS/Mac IDE), with dependencies managed by CocoaPods. Before running this app on an iOS device, there's a couple of steps to complete. +Capacitor iOS apps are configured and managed through Xcode (Apple’s iOS/Mac IDE), with dependencies managed by [CocoaPods](https://cocoapods.org/). Before running this app on an iOS device, there's a couple of steps to complete. First, run the Capacitor `open` command, which opens the native iOS project in Xcode: diff --git a/docs/react/your-first-app/7-live-reload.md b/docs/react/your-first-app/7-live-reload.md index dfc6e7cbf06..3652b25f53c 100644 --- a/docs/react/your-first-app/7-live-reload.md +++ b/docs/react/your-first-app/7-live-reload.md @@ -58,7 +58,7 @@ export function usePhotoGallery() { // Update photos array cache by overwriting the existing photo array Preferences.set({ key: PHOTO_STORAGE, value: JSON.stringify(newPhotos) }); - // delete photo file from filesystem + // Delete photo file from filesystem const filename = photo.filepath.substr(photo.filepath.lastIndexOf('/') + 1); await Filesystem.deleteFile({ path: filename, diff --git a/docs/react/your-first-app/8-distribute.md b/docs/react/your-first-app/8-distribute.md index 36002ba46b5..1ebffeceda7 100644 --- a/docs/react/your-first-app/8-distribute.md +++ b/docs/react/your-first-app/8-distribute.md @@ -72,7 +72,7 @@ To dive into more details on the steps to deploy a live update, as well as addit Next up is a native binary for your app build and deploy process. This is done via the [Ionic Package](https://ionic.io/docs/appflow/package/intro) service. First things first, you will need to create a [Package build](https://ionic.io/docs/appflow/package/builds). This can be done by clicking the `Start build` icon from the `Commits` tab or by clicking the `New build` button in the top right from the `Build > Builds` tab. Then you will select the proper commit for your build and fill in all of the several required fields and any optional fields that you want to specify. After filling in all of the information and the build begins, you can check out it's progress and review the logs if you encounter any errors. -Given a successful Package build, and iOS binary (`.ipa` or IPA) or and Android binary (`.apk` or APK) file becomes available to you. The file can subsequently be downloaded so you can install it on a device by clicking the file name in the `Artifacts` section in the right of the build detail page or clicking the `Download IPA/APK` icon on the build in the `Build > Builds` tab. +Given a successful Package build, an iOS binary (`.ipa` or IPA) or/and an Android binary (`.apk` or APK) file becomes available to you. The file can subsequently be downloaded so you can install it on a device by clicking the file name in the `Artifacts` section in the right of the build detail page or clicking the `Download IPA/APK` icon on the build in the `Build > Builds` tab. Further information regarding building native binaries can be found inside of the [Build a Native Binary](https://ionic.io/docs/appflow/quickstart/package) section inside the Appflow docs. @@ -102,8 +102,8 @@ For access to the ability to create a Native Configuration, you will need to be ## What’s Next? -Congratulations! You developed a complete cross-platform Photo Gallery app that runs on the web, iOS, and Android. Not only that, you have also then built the app and deployed it to you users devices! +Congratulations! You developed a complete cross-platform Photo Gallery app that runs on the web, iOS, and Android. Not only that, you have also then built the app and deployed it to your users' devices! -There are many paths to follow from here. Try adding another [Ionic UI component](https://ionicframework.com/docs/components) to the app, or more [native functionality](https://capacitorjs.com/docs/apis). The sky’s the limit. Once you have added another feature run the build and deploy process again through Appflow to get it out to your users. +There are many paths to follow from here. Try adding another [Ionic UI component](../../components.md) to the app, or more [native functionality](https://capacitorjs.com/docs/apis). The sky’s the limit. Once you have added another feature, run the build and deploy process again through Appflow to get it out to your users. Happy app building! 💙