Ionic > カメラロールなどから画像を選択して、アップロードする(browser, native両方)

## 2018/12/11 加筆
本投稿では頑張ってcordova pluginを使用して画像のアップロードを試みていますが、browser環境で使用できるcordovaプラグインは非常に少なく、今回使用するcameraプラグインもbrowserで使用できる数少ないプラグインの一つです。

参考
https://forum.ionicframework.com/t/can-i-use-ionic-native-components-in-a-pwa-or-do-i-need-to-publish-my-app-for-ios-android/105300/3

というわけで、そもそもbrowser環境の開発はcordovaで行うべきでないのかもしれません。私の結論としては、普通にHTMLの機能を使用して画像のアップロードを行った方が簡潔で汎用性が高いと考えています。

なお、Ionic(Angular)でフォームを使用して画像をアップロードする方法はこちらの記事が参考になりました。
https://medium.com/@amcdnl/file-uploads-with-angular-reactive-forms-960fd0b34cb5

それでもあえて、cordovaを使用する場合ということで、この投稿を参考ください。

## 概要
- カメラロール、もしくはカメラで直接撮影した画像を、firebase firestoreにアップロードする。
- iOS, Android環境の他、Browser環境にも対応する。
- (ionic 3 です)
 

## 必要なプラグイン

- [Camera](https://ionicframework.com/docs/native/camera/)
- [File](https://ionicframework.com/docs/native/file/)
- ~~[Fiel Transfer](https://ionicframework.com/docs/native/file-transfer/)~~

なので、実行する必要があるコマンドは以下の通り

```console
ionic cordova plugin add cordova-plugin-camera
npm install --save @ionic-native/camera
ionic cordova plugin add cordova-plugin-file
npm install --save @ionic-native/file
ionic cordova plugin add cordova-plugin-file-transfer # 実行しないでください
npm install --save @ionic-native/file-transfer        # 実行しないでください
```

> 全てのプラグインがAndroid, Browser, iOSに対応しているようです。

と思ったら、いきなりつまづきました。File Transter Plugin はすでに役目を終えているそうです。
↓https://github.com/apache/cordova-plugin-file-transfer
> With the new features introduced in XMLHttpRequest, this plugin is not needed any more. Migrating from this plugin to using the new features of XMLHttpRequest, is explained in this Cordova blog post.

(ionic公式ドキュメントにも少し注釈があれば。)

なので、以下を実行

```console
ionic cordova plugin remove cordova-plugin-file-transfer
npm uninstall --save @ionic-native/file-transfer
```

で、どうするかと言うと、XMLHttpRequestが役目を全うしてくれるらしいです。といことはAngularだとHttpClientを使用すると良さそうですね。と言うか今回はfirebaseSDKを使用するので、必要ありませんでした。

## モジュールを変更
```typescript:app.module
import { File } from '@ionic-native/file';
import { Camera } from '@ionic-native/camera';
import { AngularFireStorageModule } from 'angularfire2/storage';

@NgModule({
  imports: [
    BrowserModule,
    IonicModule.forRoot(MyApp),
    AngularFireModule.initializeApp(firebaseConfig,{}), // firebaseConfigはご自身の環境に合わせて
    AngularFireStorageModule
  ],
})
```

## 画像アップロードページを作成

```console
ionic g page uploadPage
```

### インポートを編集
```typescript:upload-page.ts
import { IonicPage, NavController, NavParams, AlertController } from 'ionic-angular';
import { Camera, CameraOptions } from '@ionic-native/camera';
import { DomSanitizer } from '@angular/platform-browser';
import { AngularFireStorage } from 'angularfire2/storage';
import { Observable } from 'rxjs/Rx';
import { SafeUrl } from '@angular/platform-browser/src/security/dom_sanitization_service';
```

### 変数を定義

```typescript:upload-page.ts
public image_uri: any; // 画像のdata_uri
public image_uri_for_preview: SafeUrl; // 画像のdata_uriをプレビュー用にサニタイズ
```

### DIを編集

```typescript:upload-page.ts
constructor(
    public navCtrl: NavController, public navParams: NavParams,
    private camera: Camera,
    private domSanitizer: DomSanitizer,
    private platform: Platform,
    private actionSheetCtrl: ActionSheetController,
    private storage: AngularFireStorage
    ) {}
```

## 画像データを取得

@ionic-native/cameraのgetPictureでは入力ソースを一種類しか選択出来ないため、最初にアクションシートを表示してユーザに入力ソースを選ばせます。PCではインカム or ファイル、SPではカメラorフォトギャラリーになります。アクションシート内の各ボタンを押した時のハンドラーがgetImageを呼び、ユーザに画像を選ばせて、data_uri形式でimgae_uriに格納します。

```typescript:upload-page.ts
  // 入力ソースを選択する
  public imageSourceSelection = () => {
    
    // アクションシートを表示して、ソースを選択させる
    const actionSheet = this.actionSheetCtrl.create({
      title: '画像を選択',
      buttons: [
        {
          text: '撮影する',
          icon: 'camera',
          handler: this.getImage( this.camera.PictureSourceType.CAMERA )
        },{
          text: '保存された画像を選択する',
          icon: 'folder',
          handler: this.getImage( this.camera.PictureSourceType.PHOTOLIBRARY )
        },{
          text: '戻る',
          role: 'cancel',
          handler: () => { console.log('Cancel clicked'); }
        }
      ]
    });
    actionSheet.present();
  };

  // 画像をsource_typeの方法で取得する
  public getImage( source_type: number ) {
    return () => {
      const options: CameraOptions = {
        quality:         100,
        destinationType: this.camera.DestinationType.FILE_URI,
        sourceType:      source_type,
        allowEdit:       true,
        mediaType:       this.camera.MediaType.PICTURE,
        encodingType:    this.camera.EncodingType.JPEG
      };

      const getPicture = this.camera.getPicture( options );

      // なぜかファイル選択モーダルが立ち上がらないので、強制的に発火 （ ionic-cameraのバグ? ）
      if ( !!document.querySelector( '.cordova-camera-select' ) ) {
        ( document.querySelector( '.cordova-camera-select' ) as HTMLElement ).click();
      }

      getPicture.then( ( imageData ) => {
        // base64にエンコードされた画像を受け取る
        this.image_uri = 'data:image/jpeg;charset=utf-8;base64, ' + imageData;
        // プレビュー用の変換もしておく
        this.image_uri_for_preview = this.domSanitizer.bypassSecurityTrustUrl( this.image_uri );
      }, ( err ) => {
        console.log( err );
        const alert = this.alertCtrl.create( {
          title:   '画像を選択できません。',
          buttons: [ 'OK' ]
        } );
        alert.present();
      } );
    }
  }
```

## アップロード

```typescript:upload-page.ts
  // 画像をアップロードする
  public uploadFile() {
    if ( !this.image_uri ) {
      return Observable.of( '' );
    }
    let loader = this.loadingCtrl.create({
      content: "アップロード中 ..."
    });
    loader.present();

    return Observable.fromPromise(
      this.storage.ref( /* path_to_image */ ).putString( this.image_uri, 'data_url' )
      .then( ( snapshot ) => {
        loader.dismiss();
        console.log( 'アップロード完了' );
      }).catch( ( error ) => {
        loader.dismiss();
        console.log( error );
        console.log( 'アップロード失敗' );
      })
    );
  }
```

## テンプレート

以上を加味して、テンプレートの例を書きます

```html:upload-page.html
<ion-content>
  <ion-item>
    <button ion-button color="secondary" (click)="imageSourceSelection()">画像を選択</button>
  </ion-item>
  <ion-item>
    <img [src]="image_uri_for_preview">
  </ion-item>
  <ion-item>
    <button ion-button (click)="uploadFile()">Upload</button>
  </ion-item>
</ion-content>
```

## SCSS

なお、PCのインカムを起動した際に撮影画面が崩れているので(これはバグ？)、スタイルで調整します。

```scss:upload-page.scss
.cordova-camera-capture {
  video {
    width: 100%;
    height: auto;
  }

  button {
    position: absolute;
    right: 0;
    bottom: 5px;
    background: #4889ff;
    colo: white;
    font-size: 35px;
    border-radius: 4px;
    padding: 8px;
  }
}
```

## 完成！

![タイトルなし.gif](https://qiita-image-store.s3.amazonaws.com/0/194978/67346a09-2496-096d-f67e-f252f457a4db.gif)

ただ画像をアップロードするだけなのに、少し手惑いました、、。

## 参考

[How to Upload File on Ionic 3 using Native File Transfer Plugin](https://www.djamware.com/post/599da16580aca768e4d2b130/how-to-upload-file-on-ionic-3-using-native-file-transfer-plugin)