From c152d58e3c227d150c291393c66dbd2520ecd377 Mon Sep 17 00:00:00 2001 From: soundproofboot Date: Mon, 14 Jul 2025 09:55:51 -0500 Subject: [PATCH 1/7] docs(vue): add context to code blocks small change to surrounding text in your first app page 1 --- docs/vue/your-first-app.md | 199 +++++++++++++++++++++++++++++++------ 1 file changed, 170 insertions(+), 29 deletions(-) diff --git a/docs/vue/your-first-app.md b/docs/vue/your-first-app.md index 1ebeda7d1b4..87f990da148 100644 --- a/docs/vue/your-first-app.md +++ b/docs/vue/your-first-app.md @@ -102,9 +102,53 @@ After installation, open up the project in your code editor of choice. Next, import `@ionic/pwa-elements` by editing `src/main.ts`. ```tsx -// Above the createApp() line +import { createApp } from 'vue' +import App from './App.vue' +import router from './router'; + +import { IonicVue } from '@ionic/vue'; +// CHANGE: Add the following import. import { defineCustomElements } from '@ionic/pwa-elements/loader'; + +/* Core CSS required for Ionic components to work properly */ +import '@ionic/vue/css/core.css'; + +/* Basic CSS for apps built with Ionic */ +import '@ionic/vue/css/normalize.css'; +import '@ionic/vue/css/structure.css'; +import '@ionic/vue/css/typography.css'; + +/* Optional CSS utils that can be commented out */ +import '@ionic/vue/css/padding.css'; +import '@ionic/vue/css/float-elements.css'; +import '@ionic/vue/css/text-alignment.css'; +import '@ionic/vue/css/text-transformation.css'; +import '@ionic/vue/css/flex-utils.css'; +import '@ionic/vue/css/display.css'; + +/** + * Ionic Dark Mode + * ----------------------------------------------------- + * For more info, please see: + * https://ionicframework.com/docs/theming/dark-mode + */ + +/* @import '@ionic/vue/css/palettes/dark.always.css'; */ +/* @import '@ionic/vue/css/palettes/dark.class.css'; */ +import '@ionic/vue/css/palettes/dark.system.css'; + +/* Theme variables */ +import './theme/variables.css'; + +// CHANGE: Call the element loader before the createApp() call defineCustomElements(window); +const app = createApp(App) + .use(IonicVue) + .use(router); + +router.isReady().then(() => { + app.mount('#app'); +}); ``` That’s it! Now for the fun part - let’s see the app in action. @@ -146,30 +190,72 @@ Open `/src/views/Tab2.vue`. We see: + + ``` -`ion-header` represents the top navigation and toolbar, with "Tab 2" as the title. Let’s rename it: +`ion-header` represents the top navigation and toolbar, with "Tab 2" as the title (there are two of them due to iOS [Collapsible Large Title](https://ionicframework.com/docs/api/title#collapsible-large-titles) support). Rename both `ion-title` elements to: ```html 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. But first, remove the `ExploreContainer` component, beginning with the import statement: - -```tsx -import ExploreContainer from '@/components/ExploreContainer.vue'; -``` - -Next, remove the `ExploreContainer` node from the HTML markup in the `template`. +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. But first, remove both the `ExploreContainer` component and its import statement: ```html - + + + ``` We'll replace it with a [floating action button](https://ionicframework.com/docs/api/fab) (FAB). First, update the imports within the ` ``` Since our pages are generated as [Vue Single File Components](https://vuejs.org/api/sfc-spec.html) using the [` ``` We’ll be creating the `takePhoto` method and the logic to use the Camera and other native features in a moment. -Next, open `src/views/TabsPage.vue`, remove the `ellipse` icon from the import and import the `images` icon instead: - -```tsx -import { images, square, triangle } from 'ionicons/icons'; -``` - -Within the tab bar (``), change the label to "Photos" and the `ellipse` icon to `images` for the middle tab button: +Next, open `src/views/TabsPage.vue`. Remove the `ellipse` icon from the import and import the `images` icon instead. Then, within the tab bar (``), change the label to "Photos" and the `ellipse` icon to `images` for the middle tab button: ```html - - - Photos - + + + ``` That’s just the start of all the cool things we can do with Ionic. Up next, implementing camera taking functionality on the web, then building for iOS and Android. From 59cef22d03dba5089627d9589a8877b5ce13b18e Mon Sep 17 00:00:00 2001 From: soundproofboot Date: Mon, 14 Jul 2025 12:58:54 -0500 Subject: [PATCH 2/7] docs(vue): add context to code blocks and small changes to surrounding text --- docs/vue/your-first-app/2-taking-photos.md | 255 ++++++++++++++++----- 1 file changed, 202 insertions(+), 53 deletions(-) diff --git a/docs/vue/your-first-app/2-taking-photos.md b/docs/vue/your-first-app/2-taking-photos.md index 859c3a4565a..6521dd89c69 100644 --- a/docs/vue/your-first-app/2-taking-photos.md +++ b/docs/vue/your-first-app/2-taking-photos.md @@ -17,15 +17,22 @@ Create a new file at `src/composables/usePhotoGallery.ts` and open it up. We will start by importing the various utilities we will use from Vue core and Capacitor: ```typescript +// CHANGE: Add imports from `vue` and `capacitor`. import { ref, onMounted, watch } from 'vue'; import { Camera, CameraResultType, CameraSource, Photo } from '@capacitor/camera'; import { Filesystem, Directory } from '@capacitor/filesystem'; import { Preferences } from '@capacitor/preferences'; ``` -Next, create a function named usePhotoGallery: +Next, create a function named `usePhotoGallery`: ```typescript +import { ref, onMounted, watch } from 'vue'; +import { Camera, CameraResultType, CameraSource, Photo } from '@capacitor/camera'; +import { Filesystem, Directory } from '@capacitor/filesystem'; +import { Preferences } from '@capacitor/preferences'; + +// CHANGE: Create `usePhotoGallery` function. export const usePhotoGallery = () => { const takePhoto = async () => { const photo = await Camera.getPhoto({ @@ -45,33 +52,53 @@ Our `usePhotoGallery` function exposes a method called takePhoto, which in turn Notice the magic here: there's no platform-specific code (web, iOS, or Android)! The Capacitor Camera plugin abstracts that away for us, leaving just one method call - `getPhoto()` - that will open up the device's camera and allow us to take photos. -The last step we need to take is to use the new function from the Tab2 page. Go back to `Tab2Page.vue` and import it: - -```tsx -import { usePhotoGallery } from '@/composables/usePhotoGallery'; -``` - -Destructure the `takePhoto` function from `usePhotoGallery` so we can use it in our `template`: +The last step we need to take is to use the new function in the Tab2 page. Go back to `Tab2Page.vue`. + +Import `usePhotoGallery` and destructure the `takePhoto` function so we can use it in our `template`: + +```html + -```tsx ``` @@ -86,68 +113,190 @@ After taking a photo, it disappears right away. We still need to display it with ## Displaying Photos -First we will create a new type to define our Photo, which will hold specific metadata. Add the following UserPhoto interface to the `usePhotoGallery.ts` file, somewhere outside of the main function: +First we will create a new type to define our Photo, which will hold specific metadata. Back in `usePhotoGallery.ts`, add the following `UserPhoto` interface below the main function: -```tsx +```typescript +export const usePhotoGallery = () => { + // Same old code from before. +}; + +// CHANGE: Add the `UserPhoto` interface. export interface UserPhoto { filepath: string; webviewPath?: string; } ``` -At the top of the `usePhotoGallery` function, define an array so we can store each photo captured with the Camera. Make it a reactive variable using Vue's [ref function](https://v3.vuejs.org/guide/composition-api-introduction.html#reactive-variables-with-ref). +At the top of the `usePhotoGallery` function, define an array so we can store each photo captured with the Camera. Make it a reactive variable using Vue's [ref function](https://vuejs.org/api/reactivity-core.html#ref). -```tsx -const photos = ref([]); +```typescript +export const usePhotoGallery = () => { + // CHANGE: Add the `photos` array. + const photos = ref([]); + + // other code +}; ``` -When the camera is done taking a picture, the resulting `Photo` returned from Capacitor will be added to the `photos` array. Update the `takePhoto` function, adding this code after the `Camera.getPhoto` line: +When the camera is done taking a picture, the resulting `Photo` returned from Capacitor will be added to the `photos` array. Update the `takePhoto` function with the following: -```tsx -const fileName = Date.now() + '.jpeg'; -const savedFileImage = { - filepath: fileName, - webviewPath: photo.webPath, -}; +```typescript +const takePhoto = async () => { + const photo = await Camera.getPhoto({ + resultType: CameraResultType.Uri, + source: CameraSource.Camera, + quality: 100, + }); + // CHANGE: Create the `fileName` with current timestamp. + const fileName = Date.now() + '.jpeg'; + // CHANGE: Create `savedFileImage` matching `UserPhoto` interface. + const savedFileImage = { + filepath: fileName, + webviewPath: photo.webPath, + }; -photos.value = [savedFileImage, ...photos.value]; + // CHANGE: Update the `photos` array with the new photo. + photos.value = [savedFileImage, ...photos.value]; +}; ``` -Next, update the return statement to include the photos array: +Next, update the `userPhotoGallery` return statement to include the `photos` array: -```tsx -return { - photos, - takePhoto, +```typescript +export const usePhotoGallery = () => { + // other code + + // CHANGE: Update return statement to include `photos` array. + return { + photos, + takePhoto + }; }; ``` -Back in the Tab2 component, update the import statement to include the `UserPhoto` interface: +`usePhotoGallery.ts` should now look like this: -```tsx -import { usePhotoGallery, UserPhoto } from '@/composables/usePhotoGallery'; +```typescript +import { ref, onMounted, watch } from 'vue'; +import { Camera, CameraResultType, CameraSource, Photo } from '@capacitor/camera'; +import { Filesystem, Directory } from '@capacitor/filesystem'; +import { Preferences } from '@capacitor/preferences'; + +export const usePhotoGallery = () => { + const photos = ref([]); + + const takePhoto = async () => { + const photo = await Camera.getPhoto({ + resultType: CameraResultType.Uri, + source: CameraSource.Camera, + quality: 100, + }); + const fileName = Date.now() + '.jpeg'; + const savedFileImage = { + filepath: fileName, + webviewPath: photo.webPath, + }; + + photos.value = [savedFileImage, ...photos.value]; + }; + + return { + photos, + takePhoto + }; +}; + +export interface UserPhoto { + filepath: string; + webviewPath?: string; +} ``` -Then, get access to the photos array: +Back in `Tab2Page.vue`, update the import statement to include the `UserPhoto` interface and get access to the `photos` array: -```tsx +```html + + + ``` With the photo(s) stored into the main array we can now display the images on the screen. Add a [Grid component](https://ionicframework.com/docs/api/grid) so that each photo will display nicely as they 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 - - - - - - - - - - - +```html + + + ``` 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! From 223365314fe7ed7a470dbc44e3aef3bb2dc8acfa Mon Sep 17 00:00:00 2001 From: soundproofboot Date: Mon, 14 Jul 2025 13:50:19 -0500 Subject: [PATCH 3/7] docs(vue): add context to code blocks and small changes to surrounding text --- docs/vue/your-first-app/3-saving-photos.md | 221 +++++++++++++++++---- 1 file changed, 184 insertions(+), 37 deletions(-) diff --git a/docs/vue/your-first-app/3-saving-photos.md b/docs/vue/your-first-app/3-saving-photos.md index 04f5085738f..ec8e60b99db 100644 --- a/docs/vue/your-first-app/3-saving-photos.md +++ b/docs/vue/your-first-app/3-saving-photos.md @@ -12,59 +12,206 @@ Fortunately, saving them to the filesystem only takes a few steps. Begin by open The Filesystem API requires that files written to disk are passed in as base64 data, so this helper function will be used in a moment to assist with that: -```tsx -const convertBlobToBase64 = (blob: Blob) => - new Promise((resolve, reject) => { - const reader = new FileReader(); - reader.onerror = reject; - reader.onload = () => { - resolve(reader.result); - }; - reader.readAsDataURL(blob); +```typescript +import { ref, onMounted, watch } from 'vue'; +import { Camera, CameraResultType, CameraSource, Photo } from '@capacitor/camera'; +import { Filesystem, Directory } from '@capacitor/filesystem'; +import { Preferences } from '@capacitor/preferences'; + +export const usePhotoGallery = () => { + const photos = ref([]); + + const takePhoto = async () => { + // Same old code from before. + }; + + // CHANGE: Add the `convertBloblToBase64` method. + const convertBlobToBase64 = (blob: Blob) => + new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onerror = reject; + reader.onload = () => { + resolve(reader.result); + }; + reader.readAsDataURL(blob); }); + + return { + photos, + takePhoto + }; +}; + +export interface UserPhoto { + filepath: string; + webviewPath?: string; +} ``` -Next, add a function to save the photo to the filesystem. We pass in the `photo` object, which represents the newly captured device photo, as well as the fileName, which will provide a path for the file to be stored to. +Next, we'll add a function to save the photo to the filesystem. We pass in the `photo` object, which represents the newly captured device photo, as well as the fileName, the path where the file will be stored. -Next we use the Capacitor [Filesystem API](https://capacitorjs.com/docs/apis/filesystem) to save the photo to the filesystem. We start by converting the photo to base64 format, then feed the data to the Filesystem’s `writeFile` function: +We use the Capacitor [Filesystem API](https://capacitorjs.com/docs/apis/filesystem) to save the photo to the filesystem. Create a new method inside `usePhotoGallery` called `savePicture`. This method will first convert the photo to base64 format, then feed the data to the Filesystem’s `writeFile` function: -```tsx -const savePicture = async (photo: Photo, fileName: string): Promise => { - // Fetch the photo, read as a blob, then convert to base64 format - const response = await fetch(photo.webPath!); - const blob = await response.blob(); - const base64Data = (await convertBlobToBase64(blob)) as string; - - const savedFile = await Filesystem.writeFile({ - path: fileName, - data: base64Data, - directory: Directory.Data, - }); +```typescript +import { ref, onMounted, watch } from 'vue'; +import { Camera, CameraResultType, CameraSource, Photo } from '@capacitor/camera'; +import { Filesystem, Directory } from '@capacitor/filesystem'; +import { Preferences } from '@capacitor/preferences'; + +export const usePhotoGallery = () => { + const photos = ref([]); + + const takePhoto = async () => { + // Same old code from before. + }; + + const convertBlobToBase64 = (blob: Blob) => { + // Same old code from before. + }; + + // CHANGE: Add the `savePicture` method. + const savePicture = async (photo: Photo, fileName: string): Promise => { + // Fetch the photo, read as a blob, then convert to base64 format + const response = await fetch(photo.webPath!); + const blob = await response.blob(); + const base64Data = (await convertBlobToBase64(blob)) as string; + + const savedFile = await Filesystem.writeFile({ + path: fileName, + data: base64Data, + directory: Directory.Data, + }); + + // Use webPath to display the new image instead of base64 since it's + // already loaded into memory + return { + filepath: fileName, + webviewPath: photo.webPath, + }; + }; + + return { + photos, + takePhoto + }; +}; + +export interface UserPhoto { + filepath: string; + webviewPath?: string; +} +``` + +Last, update the `takePhoto` function to call `savePicture`: + +```typescript +import { ref, onMounted, watch } from 'vue'; +import { Camera, CameraResultType, CameraSource, Photo } from '@capacitor/camera'; +import { Filesystem, Directory } from '@capacitor/filesystem'; +import { Preferences } from '@capacitor/preferences'; + +export const usePhotoGallery = () => { + const photos = ref([]); + + // CHANGE: Update the `takePhoto` method. + const takePhoto = async () => { + const photo = await Camera.getPhoto({ + resultType: CameraResultType.Uri, + source: CameraSource.Camera, + quality: 100, + }); + const fileName = Date.now() + '.jpeg'; + // CHANGE: Update to call `savePicture` method. + const savedFileImage = await savePicture(photo, fileName); + + photos.value = [savedFileImage, ...photos.value]; + }; + + const convertBlobToBase64 = (blob: Blob) => { + // Same old code from before. + }; + + const savePicture = async (photo: Photo, fileName: string): Promise => { + // Same old code from before. + }; - // Use webPath to display the new image instead of base64 since it's - // already loaded into memory return { - filepath: fileName, - webviewPath: photo.webPath, + photos, + takePhoto }; }; + +export interface UserPhoto { + filepath: string; + webviewPath?: string; +} ``` -Last, update the `takePhoto` function to call `savePicture`. Once the photo has been saved, insert it into the front of reactive `photos` array: +There we go! Each time a new photo is taken, it’s now automatically saved to the filesystem. + +`usePhotoGallery.ts` should now look like this: ```tsx -const takePhoto = async () => { - const photo = await Camera.getPhoto({ - resultType: CameraResultType.Uri, - source: CameraSource.Camera, - quality: 100, +import { ref, onMounted, watch } from 'vue'; +import { Camera, CameraResultType, CameraSource, Photo } from '@capacitor/camera'; +import { Filesystem, Directory } from '@capacitor/filesystem'; +import { Preferences } from '@capacitor/preferences'; + +export const usePhotoGallery = () => { + const photos = ref([]); + + 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); + + photos.value = [savedFileImage, ...photos.value]; + }; + + const convertBlobToBase64 = (blob: Blob) => + new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onerror = reject; + reader.onload = () => { + resolve(reader.result); + }; + reader.readAsDataURL(blob); }); - const fileName = Date.now() + '.jpeg'; - const savedFileImage = await savePicture(photo, fileName); + const savePicture = async (photo: Photo, fileName: string): Promise => { + // Fetch the photo, read as a blob, then convert to base64 format + const response = await fetch(photo.webPath!); + const blob = await response.blob(); + const base64Data = (await convertBlobToBase64(blob)) as string; + + const savedFile = await Filesystem.writeFile({ + path: fileName, + data: base64Data, + directory: Directory.Data, + }); + + // Use webPath to display the new image instead of base64 since it's + // already loaded into memory + return { + filepath: fileName, + webviewPath: photo.webPath, + }; + }; - photos.value = [savedFileImage, ...photos.value]; + return { + photos, + takePhoto + }; }; + +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. +Next up, we'll load and display our saved images. From 6a21dbfb8034a78375450be7f3f2979c69536bbe Mon Sep 17 00:00:00 2001 From: soundproofboot Date: Mon, 14 Jul 2025 14:43:46 -0500 Subject: [PATCH 4/7] docs(vue): add context to code blocks and small changes to surrounding text --- docs/vue/your-first-app/4-loading-photos.md | 275 ++++++++++++++++++-- 1 file changed, 250 insertions(+), 25 deletions(-) diff --git a/docs/vue/your-first-app/4-loading-photos.md b/docs/vue/your-first-app/4-loading-photos.md index 7cd07ef8794..475a43df27a 100644 --- a/docs/vue/your-first-app/4-loading-photos.md +++ b/docs/vue/your-first-app/4-loading-photos.md @@ -13,57 +13,282 @@ 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 at the top of the `usePhotoGallery` function in `src/composables/usePhotoGallery.ts`: ```tsx -const PHOTO_STORAGE = 'photos'; +export const usePhotoGallery = () => { + // CHANGE: Add the `PHOTO_STORAGE` key. + const PHOTO_STORAGE = 'photos'; + const photos = ref([]); + + const takePhoto = async () => { + // Same old code from before. + }; + + const convertBlobToBase64 = (blob: Blob) => { + // Same old code from before. + }; + + const savePicture = async (photo: Photo, fileName: string): Promise => { + // Same old code from before. + }; + + return { + photos, + takePhoto + }; +}; ``` -Next, add a `cachePhotos` function that saves the Photos array as JSON to preferences: +Next, add a `cachePhotos` method that saves the Photos array as JSON to preferences: ```tsx -const cachePhotos = () => { - Preferences.set({ - key: PHOTO_STORAGE, - value: JSON.stringify(photos.value), - }); +export const usePhotoGallery = () => { + const PHOTO_STORAGE = 'photos'; + const photos = ref([]); + + const takePhoto = async () => { + // Same old code from before. + }; + + const convertBlobToBase64 = (blob: Blob) => { + // Same old code from before. + }; + + // CHANGE: Add the `cachePhotos` method. + const cachePhotos = () => { + Preferences.set({ + key: PHOTO_STORAGE, + value: JSON.stringify(photos.value), + }); + }; + + const savePicture = async (photo: Photo, fileName: string): Promise => { + // Same old code from before. + }; + + return { + photos, + takePhoto + }; }; ``` -Next, use the Vue [watch function](https://v3.vuejs.org/guide/composition-api-introduction.html#reacting-to-changes-with-watch) to watch the `photos` array. Whenever the array is modified (in this case, taking or deleting photos), trigger the `cachePhotos` function. Not only do we get to reuse code, but it also doesn’t matter when the app user closes or switches to a different app - photo data is always saved. +Next, use the Vue [watch function](https://vuejs.org/api/reactivity-core.html#watch) to watch the `photos` array. Whenever the array is modified (in this case, taking or deleting photos), trigger the `cachePhotos` method. Not only do we get to reuse code, but it also doesn’t matter when the app user closes or switches to a different app - photo data is always saved. + +Add the call to the `watch` function above the return statement in `usePhotoGallery`: ```tsx -watch(photos, cachePhotos); +export const usePhotoGallery = () => { + const PHOTO_STORAGE = 'photos'; + const photos = ref([]); + + const takePhoto = async () => { + // Same old code from before. + }; + + const convertBlobToBase64 = (blob: Blob) => { + // Same old code from before. + }; + + const cachePhotos = () => { + // Same old code from before. + }; + + const savePicture = async (photo: Photo, fileName: string): Promise => { + // Same old code from before. + }; + + // CHANGE: Add call to `watch` with `photos` array and `cachePhotos` method. + watch(photos, cachePhotos); + + return { + photos, + takePhoto + }; +}; ``` -Now that the photo array data is saved, create a function to retrieve the data when Tab2 loads. First, retrieve photo data from Preferences, then each photo's data into base64 format: +Now that the photo array data is saved, we need a way to retrieve the data when Tab2 loads. Create a new method in `usePhotoGallery` called `loadSaved` which will first retrieve photo data from Preferences, then convert each photo's data to base64 format: ```tsx -const loadSaved = async () => { - const photoList = await Preferences.get({ key: PHOTO_STORAGE }); - const photosInPreferences = photoList.value ? JSON.parse(photoList.value) : []; +export const usePhotoGallery = () => { + const PHOTO_STORAGE = 'photos'; + const photos = ref([]); - for (const photo of photosInPreferences) { - const file = await Filesystem.readFile({ - path: photo.filepath, - directory: Directory.Data, - }); - photo.webviewPath = `data:image/jpeg;base64,${file.data}`; - } + const takePhoto = async () => { + // Same old code from before. + }; + + const convertBlobToBase64 = (blob: Blob) => { + // Same old code from before. + }; - photos.value = photosInPreferences; + const cachePhotos = () => { + // Same old code from before. + }; + + const savePicture = async (photo: Photo, fileName: string): Promise => { + // Same old code from before. + }; + + // CHANGE: Add the `loadSaved` method. + const loadSaved = async () => { + const photoList = await Preferences.get({ key: PHOTO_STORAGE }); + const photosInPreferences = photoList.value ? JSON.parse(photoList.value) : []; + + for (const photo of photosInPreferences) { + const file = await Filesystem.readFile({ + path: photo.filepath, + directory: Directory.Data, + }); + photo.webviewPath = `data:image/jpeg;base64,${file.data}`; + } + + photos.value = photosInPreferences; + }; + + watch(photos, cachePhotos); + + return { + photos, + takePhoto + }; }; ``` 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. -Finally, we need a way to call the `loadSaved` function when the Photo Gallery page is loaded. To do so, use the Vue [mounted lifecycle hook](https://v3.vuejs.org/guide/composition-api-introduction.html#lifecycle-hook-registration-inside-setup). Earlier we had already imported `onMounted` from Vue: +Finally, we need a way to call the `loadSaved` method when the Photo Gallery page is loaded. To do so, use the Vue [mounted lifecycle hook](https://vuejs.org/api/options-lifecycle.html#mounted). Above the `usePhotoGallery` return statement where we added the call to `watch` earlier, add a call to the `onMounted` function and pass in the `loadSaved` method created above: ```tsx -import { ref, onMounted, watch } from 'vue'; +export const usePhotoGallery = () => { + const PHOTO_STORAGE = 'photos'; + const photos = ref([]); + + const takePhoto = async () => { + // Same old code from before. + }; + + const convertBlobToBase64 = (blob: Blob) => { + // Same old code from before. + }; + + const cachePhotos = () => { + // Same old code from before. + }; + + const savePicture = async (photo: Photo, fileName: string): Promise => { + // Same old code from before. + }; + + const loadSaved = async () => { + // Same old code from before. + }; + + // CHANGE: Add call to `onMounted` with the `loadSaved` method. + onMounted(loadSaved); + watch(photos, cachePhotos); + + return { + photos, + takePhoto + }; +}; ``` -Within the `usePhotoGallery` function, add the `onMounted` function and call `loadSaved`: +After these updates to the `usePhotoGallery` function, your `usePhotoGallery.ts` file should look like this: ```tsx -onMounted(loadSaved); +import { ref, onMounted, watch } from 'vue'; +import { Camera, CameraResultType, CameraSource, Photo } from '@capacitor/camera'; +import { Filesystem, Directory } from '@capacitor/filesystem'; +import { Preferences } from '@capacitor/preferences'; + +export const usePhotoGallery = () => { + const PHOTO_STORAGE = 'photos'; + const photos = ref([]); + + 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); + + photos.value = [savedFileImage, ...photos.value]; + }; + + const convertBlobToBase64 = (blob: Blob) => + new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onerror = reject; + reader.onload = () => { + resolve(reader.result); + }; + reader.readAsDataURL(blob); + }); + + const cachePhotos = () => { + Preferences.set({ + key: PHOTO_STORAGE, + value: JSON.stringify(photos.value), + }); + }; + + const savePicture = async (photo: Photo, fileName: string): Promise => { + // Fetch the photo, read as a blob, then convert to base64 format + const response = await fetch(photo.webPath!); + const blob = await response.blob(); + const base64Data = (await convertBlobToBase64(blob)) as string; + + const savedFile = await Filesystem.writeFile({ + path: fileName, + data: base64Data, + directory: Directory.Data, + }); + + // Use webPath to display the new image instead of base64 since it's + // already loaded into memory + return { + filepath: fileName, + webviewPath: photo.webPath, + }; + }; + + const loadSaved = async () => { + const photoList = await Preferences.get({ key: PHOTO_STORAGE }); + const photosInPreferences = photoList.value ? JSON.parse(photoList.value) : []; + + for (const photo of photosInPreferences) { + const file = await Filesystem.readFile({ + path: photo.filepath, + directory: Directory.Data, + }); + photo.webviewPath = `data:image/jpeg;base64,${file.data}`; + } + + photos.value = photosInPreferences; + }; + + onMounted(loadSaved); + watch(photos, cachePhotos); + + return { + photos, + takePhoto + }; +}; + +export interface UserPhoto { + filepath: string; + webviewPath?: string; +} ``` +:::note +If you're seeing broken image links or missing photos after following these steps, you may need to open your browser's dev tools and clear both [localStorage](https://developer.chrome.com/docs/devtools/storage/localstorage) and [IndexedDB](https://developer.chrome.com/docs/devtools/storage/indexeddb). + +In localStorage, look for domain `http://localhost:8100` and key `CapacitorStorage.photos`. In IndexedDB, find a store called "FileStorage". Your photos will have a key like `/DATA/123456789012.jpeg`. +::: + That’s it! We’ve built a complete Photo Gallery feature in our Ionic app that works on the web. Next up, we’ll transform it into a mobile app for iOS and Android! From 3635891f02ddb512f682abb80e604c82884bebfc Mon Sep 17 00:00:00 2001 From: soundproofboot Date: Mon, 14 Jul 2025 15:25:58 -0500 Subject: [PATCH 5/7] docs(vue): add context to code blocks and small changes to surrounding text --- docs/vue/your-first-app/5-adding-mobile.md | 135 ++++++++++++++++++++- 1 file changed, 131 insertions(+), 4 deletions(-) diff --git a/docs/vue/your-first-app/5-adding-mobile.md b/docs/vue/your-first-app/5-adding-mobile.md index 61a1a51191d..7d0eb5a75fc 100644 --- a/docs/vue/your-first-app/5-adding-mobile.md +++ b/docs/vue/your-first-app/5-adding-mobile.md @@ -6,16 +6,24 @@ Let’s start with making some small code changes - then our app will "just work ## Platform-specific Logic -First, we’ll update the photo saving functionality to support mobile. We'll run slightly different code depending on the platform - mobile or web. Import the `Platform` API from Ionic Vue and `Capacitor` from Capacitor's `core` package: +First, we’ll update the photo saving functionality to support mobile. We'll run slightly different code depending on the platform - mobile or web. At the top of `usePhotoGallery.ts`, import `isPlatform` from Ionic Vue and `Capacitor` from Capacitor's `core` package: ```tsx +import { ref, onMounted, watch } from 'vue'; +import { Camera, CameraResultType, CameraSource, Photo } from '@capacitor/camera'; +import { Filesystem, Directory } from '@capacitor/filesystem'; +import { Preferences } from '@capacitor/preferences'; +// CHANGE: Add imports from `@ionic/vue` and `@capcitor/core`. import { isPlatform } from '@ionic/vue'; import { Capacitor } from '@capacitor/core'; + +// Same old code from before. ``` -In the `savePicture` function, check which platform the app is running on. If it’s "hybrid" (Capacitor, the native runtime), then read the photo file into base64 format using the `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://capacitorjs.com/docs/basics/utilities#convertfilesrc)). Otherwise, use the same logic as before when running the app on the web. +Next, update the `usePhotoGallery` function's `savePicture` method to check which platform the app is running on. If it’s "hybrid" (Capacitor, the native runtime), then read the photo file into base64 format using the `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://capacitorjs.com/docs/basics/utilities#convertfilesrc)). Otherwise, use the same logic as before when running the app on the web. ```tsx +// CHANGE: Update the `savePicture` method to include branches for web and native. const savePicture = async (photo: Photo, fileName: string): Promise => { let base64Data: string | Blob; // "hybrid" will detect mobile - iOS or Android @@ -30,6 +38,7 @@ const savePicture = async (photo: Photo, fileName: string): Promise = const blob = await response.blob(); base64Data = (await convertBlobToBase64(blob)) as string; } + const savedFile = await Filesystem.writeFile({ path: fileName, data: base64Data, @@ -54,9 +63,10 @@ const savePicture = async (photo: Photo, fileName: string): Promise = }; ``` -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: +Next, add a new bit of logic in the `loadSaved` method. On mobile, we can directly point to each photo file on the Filesystem and display them automatically. On the web, however, we must read each image from the Filesystem into base64 format. This is because the Filesystem API uses [IndexedDB](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API) under the hood. Update the `loadSaved` function: ```tsx +// CHANGE: Update the `loadSaved` method to convert to base64 on web platform only. const loadSaved = async () => { const photoList = await Preferences.get({ key: PHOTO_STORAGE }); const photosInPreferences = photoList.value ? JSON.parse(photoList.value) : []; @@ -77,4 +87,121 @@ const loadSaved = async () => { }; ``` -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. +Our Photo Gallery now consists of one codebase that runs on the web, Android, and iOS. + +`usePhotoGallery.ts` should now look like this: + +```tsx +import { ref, onMounted, watch } from 'vue'; +import { Camera, CameraResultType, CameraSource, Photo } from '@capacitor/camera'; +import { Filesystem, Directory } from '@capacitor/filesystem'; +import { Preferences } from '@capacitor/preferences'; +import { isPlatform } from '@ionic/vue'; +import { Capacitor } from '@capacitor/core'; + +export const usePhotoGallery = () => { + const PHOTO_STORAGE = 'photos'; + const photos = ref([]); + + 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); + + photos.value = [savedFileImage, ...photos.value]; + }; + + const convertBlobToBase64 = (blob: Blob) => + new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onerror = reject; + reader.onload = () => { + resolve(reader.result); + }; + reader.readAsDataURL(blob); + }); + + const cachePhotos = () => { + Preferences.set({ + key: PHOTO_STORAGE, + value: JSON.stringify(photos.value), + }); + }; + + const savePicture = async (photo: Photo, fileName: string): Promise => { + let base64Data: string | Blob; + // "hybrid" will detect mobile - iOS or Android + if (isPlatform('hybrid')) { + const file = await Filesystem.readFile({ + path: photo.path!, + }); + base64Data = file.data; + } else { + // Fetch the photo, read as a blob, then convert to base64 format + const response = await fetch(photo.webPath!); + const blob = await response.blob(); + base64Data = (await convertBlobToBase64(blob)) as string; + } + + const savedFile = await Filesystem.writeFile({ + path: fileName, + data: base64Data, + directory: Directory.Data, + }); + + if (isPlatform('hybrid')) { + // Display the new image by rewriting the 'file://' path to HTTP + // 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 loadSaved = async () => { + const photoList = await Preferences.get({ key: PHOTO_STORAGE }); + const photosInPreferences = photoList.value ? JSON.parse(photoList.value) : []; + + // If running on the web... + if (!isPlatform('hybrid')) { + for (const photo of photosInPreferences) { + const file = await Filesystem.readFile({ + path: photo.filepath, + directory: Directory.Data, + }); + // Web platform only: Load the photo as base64 data + photo.webviewPath = `data:image/jpeg;base64,${file.data}`; + } + } + + photos.value = photosInPreferences; + }; + + onMounted(loadSaved); + watch(photos, cachePhotos); + + return { + photos, + takePhoto + }; +}; + +export interface UserPhoto { + filepath: string; + webviewPath?: string; +} +``` + +Next up, the part you’ve been waiting for - deploying the app to a device. From 6fb72c41ecff11ee03ca74c3d14cce4b611a2c5b Mon Sep 17 00:00:00 2001 From: soundproofboot Date: Wed, 16 Jul 2025 12:36:55 -0500 Subject: [PATCH 6/7] docs(vue): add context to code blocks and small changes to surrounding text --- docs/vue/your-first-app/7-live-reload.md | 164 ++++++++++++++++++----- 1 file changed, 133 insertions(+), 31 deletions(-) diff --git a/docs/vue/your-first-app/7-live-reload.md b/docs/vue/your-first-app/7-live-reload.md index 9c0cb476b80..ad69a410085 100644 --- a/docs/vue/your-first-app/7-live-reload.md +++ b/docs/vue/your-first-app/7-live-reload.md @@ -25,11 +25,18 @@ $ ionic cap run android -l --external The Live Reload server will start up, and the native IDE of choice will open if not opened already. Within the IDE, click the Play button to launch the app onto your device. ## Deleting Photos +With Live Reload running and the app is open on your device, let’s implement photo deletion functionality. We'll display an [IonActionSheet](https://ionicframework.com/docs/api/action-sheet) with the option to delete a photo. -With Live Reload running and the app is open on your device, let’s implement photo deletion functionality. Open `Tab2Page.vue` then import the `actionSheetController`. We'll display an [IonActionSheet](https://ionicframework.com/docs/api/action-sheet) with the option to delete a photo: +Open `Tab2Page.vue` and add `actionSheetController` to the imports from `@ionic/vue`. We also need to add a reference to the `deletePhoto` method, which we'll create soon in `usePhotoGallery()`: +```html + -```tsx + ``` When a user clicks/taps on an image, we will show the action sheet. Add a click handler to the `` element: ```html - + + + ``` -Next, within `script setup`, call the `create` function to open a dialog with the option to either delete the selected photo or cancel (close) the dialog: +Next, within `script setup`, create a new function called `showActionSheet`. `showActionSheet` will call the `create` method on the `actionSheetController` to open a dialog with the option to either delete the selected photo or cancel (close) the dialog: -```tsx +```html + + + ``` -Next, we need to implement the `deletePhoto` method in the `usePhotoGallery` function. Open the file then add: +Next, open `usePhotoGallery.ts`. We need to implement the `deletePhoto` method in the `usePhotoGallery` function. We also need to update the return statement to include `deletePhoto`: ```tsx -const deletePhoto = async (photo: UserPhoto) => { - // Remove this photo from the Photos reference data array - photos.value = photos.value.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, - }); +import { ref, onMounted, watch } from 'vue'; +import { Camera, CameraResultType, CameraSource, Photo } from '@capacitor/camera'; +import { Filesystem, Directory } from '@capacitor/filesystem'; +import { Preferences } from '@capacitor/preferences'; +import { isPlatform } from '@ionic/vue'; +import { Capacitor } from '@capacitor/core'; + +export const usePhotoGallery = () => { + const PHOTO_STORAGE = 'photos'; + const photos = ref([]); + + const takePhoto = async () => { + // Same old code from before. + }; + + const convertBlobToBase64 = (blob: Blob) => { + // Same old code from before. + }; + + const cachePhotos = () => { + // Same old code from before. + }; + + const savePicture = async (photo: Photo, fileName: string): Promise => { + // Same old code from before. + }; + + const loadSaved = async () => { + // Same old code from before. + }; + + // CHANGE: Add the `deletePhoto` method. + const deletePhoto = async (photo: UserPhoto) => { + // Remove this photo from the Photos reference data array + photos.value = photos.value.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, + }); + }; + + onMounted(loadSaved); + watch(photos, cachePhotos); + + // CHANGE: Add `deletePhoto` to the return statement. + return { + photos, + takePhoto, + deletePhoto + }; }; + +export interface UserPhoto { + filepath: string; + webviewPath?: string; +} ``` The selected photo is removed from the `photos` array first, then we delete the photo file using the Filesystem API. @@ -119,16 +231,6 @@ const cachePhotos = () => { watch(photos, cachePhotos); ``` -Finally, return the `deletePhoto` function: - -```tsx -return { - photos, - takePhoto, - deletePhoto, -}; -``` - 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. From bc40cfb0ff1f0e32529657b59092b926a355bd72 Mon Sep 17 00:00:00 2001 From: soundproofboot Date: Wed, 16 Jul 2025 12:52:59 -0500 Subject: [PATCH 7/7] docs(vue): run linter --- docs/vue/your-first-app.md | 88 +++++++------ docs/vue/your-first-app/2-taking-photos.md | 122 +++++++++--------- docs/vue/your-first-app/3-saving-photos.md | 12 +- docs/vue/your-first-app/4-loading-photos.md | 20 +-- docs/vue/your-first-app/5-adding-mobile.md | 4 +- docs/vue/your-first-app/7-live-reload.md | 132 ++++++++++---------- 6 files changed, 189 insertions(+), 189 deletions(-) diff --git a/docs/vue/your-first-app.md b/docs/vue/your-first-app.md index 87f990da148..c9a56693823 100644 --- a/docs/vue/your-first-app.md +++ b/docs/vue/your-first-app.md @@ -102,8 +102,8 @@ After installation, open up the project in your code editor of choice. Next, import `@ionic/pwa-elements` by editing `src/main.ts`. ```tsx -import { createApp } from 'vue' -import App from './App.vue' +import { createApp } from 'vue'; +import App from './App.vue'; import router from './router'; import { IonicVue } from '@ionic/vue'; @@ -142,9 +142,7 @@ import './theme/variables.css'; // CHANGE: Call the element loader before the createApp() call defineCustomElements(window); -const app = createApp(App) - .use(IonicVue) - .use(router); +const app = createApp(App).use(IonicVue).use(router); router.isReady().then(() => { app.mount('#app'); @@ -192,8 +190,8 @@ Open `/src/views/Tab2.vue`. We see: ``` @@ -226,9 +224,9 @@ We put the visual aspects of our app into ``. In this case, it’s ``` @@ -253,23 +251,23 @@ We'll replace it with a [floating action button](https://ionicframework.com/docs ``` @@ -302,21 +300,21 @@ Add the FAB to the bottom of the page. Use the camera image as the icon, and cal ``` @@ -352,9 +350,9 @@ Next, open `src/views/TabsPage.vue`. Remove the `ellipse` icon from the import a ``` diff --git a/docs/vue/your-first-app/2-taking-photos.md b/docs/vue/your-first-app/2-taking-photos.md index 6521dd89c69..508c360e46e 100644 --- a/docs/vue/your-first-app/2-taking-photos.md +++ b/docs/vue/your-first-app/2-taking-photos.md @@ -80,26 +80,26 @@ Import `usePhotoGallery` and destructure the `takePhoto` function so we can use ``` @@ -133,7 +133,7 @@ At the top of the `usePhotoGallery` function, define an array so we can store ea export const usePhotoGallery = () => { // CHANGE: Add the `photos` array. const photos = ref([]); - + // other code }; ``` @@ -169,7 +169,7 @@ export const usePhotoGallery = () => { // CHANGE: Update return statement to include `photos` array. return { photos, - takePhoto + takePhoto, }; }; ``` @@ -202,7 +202,7 @@ export const usePhotoGallery = () => { return { photos, - takePhoto + takePhoto, }; }; @@ -220,26 +220,26 @@ Back in `Tab2Page.vue`, update the import statement to include the `UserPhoto` i ``` @@ -278,24 +278,24 @@ With the photo(s) stored into the main array we can now display the images on th ``` diff --git a/docs/vue/your-first-app/3-saving-photos.md b/docs/vue/your-first-app/3-saving-photos.md index ec8e60b99db..9003acbed27 100644 --- a/docs/vue/your-first-app/3-saving-photos.md +++ b/docs/vue/your-first-app/3-saving-photos.md @@ -34,11 +34,11 @@ export const usePhotoGallery = () => { resolve(reader.result); }; reader.readAsDataURL(blob); - }); + }); return { photos, - takePhoto + takePhoto, }; }; @@ -92,7 +92,7 @@ export const usePhotoGallery = () => { return { photos, - takePhoto + takePhoto, }; }; @@ -137,7 +137,7 @@ export const usePhotoGallery = () => { return { photos, - takePhoto + takePhoto, }; }; @@ -180,7 +180,7 @@ export const usePhotoGallery = () => { resolve(reader.result); }; reader.readAsDataURL(blob); - }); + }); const savePicture = async (photo: Photo, fileName: string): Promise => { // Fetch the photo, read as a blob, then convert to base64 format @@ -204,7 +204,7 @@ export const usePhotoGallery = () => { return { photos, - takePhoto + takePhoto, }; }; diff --git a/docs/vue/your-first-app/4-loading-photos.md b/docs/vue/your-first-app/4-loading-photos.md index 475a43df27a..0fc4d603cb1 100644 --- a/docs/vue/your-first-app/4-loading-photos.md +++ b/docs/vue/your-first-app/4-loading-photos.md @@ -32,7 +32,7 @@ export const usePhotoGallery = () => { return { photos, - takePhoto + takePhoto, }; }; ``` @@ -66,7 +66,7 @@ export const usePhotoGallery = () => { return { photos, - takePhoto + takePhoto, }; }; ``` @@ -98,10 +98,10 @@ export const usePhotoGallery = () => { // CHANGE: Add call to `watch` with `photos` array and `cachePhotos` method. watch(photos, cachePhotos); - + return { photos, - takePhoto + takePhoto, }; }; ``` @@ -146,10 +146,10 @@ export const usePhotoGallery = () => { }; watch(photos, cachePhotos); - + return { photos, - takePhoto + takePhoto, }; }; ``` @@ -186,10 +186,10 @@ export const usePhotoGallery = () => { // CHANGE: Add call to `onMounted` with the `loadSaved` method. onMounted(loadSaved); watch(photos, cachePhotos); - + return { photos, - takePhoto + takePhoto, }; }; ``` @@ -226,7 +226,7 @@ export const usePhotoGallery = () => { resolve(reader.result); }; reader.readAsDataURL(blob); - }); + }); const cachePhotos = () => { Preferences.set({ @@ -275,7 +275,7 @@ export const usePhotoGallery = () => { return { photos, - takePhoto + takePhoto, }; }; diff --git a/docs/vue/your-first-app/5-adding-mobile.md b/docs/vue/your-first-app/5-adding-mobile.md index 7d0eb5a75fc..3105a873ce1 100644 --- a/docs/vue/your-first-app/5-adding-mobile.md +++ b/docs/vue/your-first-app/5-adding-mobile.md @@ -123,7 +123,7 @@ export const usePhotoGallery = () => { resolve(reader.result); }; reader.readAsDataURL(blob); - }); + }); const cachePhotos = () => { Preferences.set({ @@ -194,7 +194,7 @@ export const usePhotoGallery = () => { return { photos, - takePhoto + takePhoto, }; }; diff --git a/docs/vue/your-first-app/7-live-reload.md b/docs/vue/your-first-app/7-live-reload.md index ad69a410085..0b8113842be 100644 --- a/docs/vue/your-first-app/7-live-reload.md +++ b/docs/vue/your-first-app/7-live-reload.md @@ -25,36 +25,38 @@ $ ionic cap run android -l --external The Live Reload server will start up, and the native IDE of choice will open if not opened already. Within the IDE, click the Play button to launch the app onto your device. ## Deleting Photos + With Live Reload running and the app is open on your device, let’s implement photo deletion functionality. We'll display an [IonActionSheet](https://ionicframework.com/docs/api/action-sheet) with the option to delete a photo. Open `Tab2Page.vue` and add `actionSheetController` to the imports from `@ionic/vue`. We also need to add a reference to the `deletePhoto` method, which we'll create soon in `usePhotoGallery()`: + ```html ``` @@ -104,51 +106,51 @@ Next, within `script setup`, create a new function called `showActionSheet`. `sh ``` @@ -206,7 +208,7 @@ export const usePhotoGallery = () => { return { photos, takePhoto, - deletePhoto + deletePhoto, }; };