From 9955fd4a6262d26336c9e62444ec923592db2e71 Mon Sep 17 00:00:00 2001 From: joesphchang Date: Wed, 9 Jul 2025 15:11:33 -0500 Subject: [PATCH 1/4] 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 2/4] 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 3/4] 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 4/4] 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 +```