2017年11月26日 星期日

Ionic3+ firebase

FireBase推出最新的雲端資料庫的版本(cloud Firestore), 有別之前的RealTime的資料庫,更引進rxjsoberserablesubscription的模式,讓程式更具有高彈性,我們就簡單的實作範例。

一、建立Colud Firestore
選擇下拉式選單è Cloud Firestore



二、引用firestore

安裝angularfire2  
npm install angularfire2 –save 

import { AngularFireModule } from 'angularfire2';
import {AngularFirestoreModule} from 'angularfire2/firestore';

新版的colud firestore的設定檔需要加上projectId
// AF2 Settings
export const firebaseConfig = {
  apiKey: "xxxxxxxxxxx",
  authDomain: "fiyy4.firebaseapp.com",
  databaseURL: "https://firstapp-xxxx.firebaseio.com",
  projectId: "fyyyy4",
  storageBucket: "firstyy.appspot.com",
  messagingSenderId: "1233456789"
};

注入AngularFireModuleAngularFirestoreModule
imports: [
    BrowserModule,
    IonicModule.forRoot(MyApp),
    AngularFireModule.initializeApp(firebaseConfig),
    AngularFirestoreModule
  ],

三、ProvideràMoney_service.ts建立CRUD的函式

AngularFirestoreCollection 用來取得資料列表(查詢)
AngularFirestoreDocument 用來允許編輯/刪除資料
Observable 用來取得資料陣列有更新的值
import { Observable } from 'rxjs/Observable';
import { AngularFirestore, AngularFirestoreCollection, AngularFirestoreDocument } from 'angularfire2/firestore';

宣告
valueChange: 傳回Json的物件,不具有metadata的屬性
export class MoneyServiceProvider {
  itemCollections: AngularFirestoreCollection<any>; //資料庫傳回值型態
  items: Observable<any[]>;
  constructor(public afs: AngularFirestore) {
    this.itemCollections = this.afs.collection('moneyCost', ref => ref.orderBy('costDate')); //取回資料庫moneyCost,並依日期排序
    this.items = this.itemCollections.valueChanges();//取回值
  }

新增
snapshotChanges:可以用來異動資料庫,本身具有metadata的屬性
  public add(item) {
    this.itemCollections.add(item);
    const listObservable = this.itemCollections.snapshotChanges()
  }

刪除
AngularFirestoreDocument:取回Firestore Collection 的物件
public remove(itemID) {
    const itemDoc: AngularFirestoreDocument<any> = this.afs.doc<any>('moneyCost/' + itemID);
    itemDoc.delete();
  }

List:取得id
  public list() {
    return this.itemCollections.snapshotChanges().map(actions => {
      return actions.map(a => {
        const data = a.payload.doc.data() as any;
        data.id = a.payload.doc.id;
        return data;
      });
    });
  }

整理陣列的資料 data[key]=[{},{}]
  public groupList(items:any[]){
    let total=0;
    const groupItem=[];
    items.forEach(eachObj => {
      if(groupItem[eachObj.costDate] == undefined){
        groupItem[eachObj.costDate]=[];
      }
total=+eachObj.cost;
groupItem[eachObj.costDate].push({key:eachObj.costDate,
value:eachObj,sum:total});
    });
    return groupItem;
  }

Reduce:累加器,會自動依序陣陣值取出作運算後,傳回最終結果
public getCostSum(items):Observable<number> {
    // return 1000;
    return items.map((items: any[]) => {
      return items.reduce((prev, curr: any) => {            
              return prev + Number(curr.cost);               
      }, 0);
  })}

四、建立Componentàmoney-list.ts
import { MoneyServiceProvider } from '../../providers/money-service/money-service';
import { AngularFirestore, AngularFirestoreCollection } from 'angularfire2/firestore';

宣告物件
interface money {
  productId: string;
  costDate: string;
  cost: number;
}

export class MoneyListComponent implements OnDestroy {
  private subscription: ISubscription;
  itemCollections: AngularFirestoreCollection<money>;
  items:any[];
  obserItem:Observable<any[]>;
  groupList: any[];
  sumCost: Number;
  constructor(public afs: AngularFirestore, public moneyService: MoneyServiceProvider) {
    this.obserItem= moneyService.list(); //取回資料
    this.obserItem.subscribe(x=>{
//將取回來的值放回到變數中,透過item 在畫面上呈現
      this.items=x;
      this.groupList =moneyService.groupList(this.items);
      this.doSum (this.items); //計算總合
    });
  }
}

//累加器,會自動傳回最後值的運算結果
doSum(items) {
    this.items.reduce((prev, curr: any) =>{
         return this.sumCost = prev + Number(curr.cost);
    } ,0);
  }

  doDelete(itemId: any) {
    this.moneyService.remove(itemId); //刪除document
  }

  ngOnDestroy() {
//解除訂閱
    this.obserItem.subscribe().unsubscribe();
  }
}

Component: money-list.html

建立group pipe 呈現複雜資料結構 data[key]=[{},{}],轉換資料結構 [{key:1,value=data},{key:2,value=data}]
@Pipe({
  name: 'group',
})
export class GroupByPipe implements PipeTransform {
  transform(value: any, args: any[] = null): any {
    let keys = [];
    for (let key in value) {
      keys.push({ key: key, value: value[key] });
    }
    return keys;
  }
}

加上關鍵字group 就會將資料置換{key,value}
<ion-list>
        <ion-item-group *ngFor="let item of groupList|group">
                <ion-item-divider color="light">
                        <h3>
                                <font color=black>{{item.key|date}}</font>
                        </h3>
                </ion-item-divider>
                <ion-item *ngFor="let subItem of item.value">
                        <ion-row>
                                <ion-col col-lg-4>
                                   <font color=blue>{{subItem.value.productId}}</font>
                                </ion-col>
                                <ion-col col-lg-4>
                                <font>{{subItem.value.cost|currency}} </font>
                                </ion-col>
                                <ion-col col-lg-4>
                                        <button ion-button color="danger" small  round (click)="doDelete(subItem.value.id)">刪除</button>
                                </ion-col>
                        </ion-row>
                </ion-item>
        </ion-item-group>
       <h1><font color=red>總額<input type="hidden" [(ngModel)]="sumCost"> {{sumCost}}</font></h1>
     </ion-list>

範例如下
新增

 

列表

以上總結 Colud Firestore 可以讓資料傳回時,可先轉換資料型態/結構/運算值等等,更具有彈性。


2017年10月11日 星期三

如何實作 Ionic 3 的 WebRTC

 不少人都在詢問如何實作cordovaWebRTC,目前作者使用Peer.JS 就能完成一連串的影像互傳的功能,此篇文章在『Angular2 簡易版的webRTC』已簡單介紹過了,目前要更進一步實作出Cordova 在手機上與電腦也能互傳影像。

一、介紹FireBase
支援real-time的一種雲端服務,更重要的一點是完全免費的喔!!! 簡單的key-valueJSON格式,支援資料庫的匯入和匯出,順帶一提免費版的有些容量是有限制的。

為何需要雲端資料庫FireBase儲存資料,因為Peer.js 會產生一組perid讓對方可以透過perid 互傳影像,所以儲存登入者名稱和peerid…

二、登入FireBase 開啟功能


Google 登入à go to console à新增專案




















點選overview旁邊的設定à專案設定



選擇 firebase 加入網路應用程式

複製以下的設定檔
















一、建立ionic 專案

ionic start myApp tabs


$ ionic cordova plugin add cordova-plugin-firebase
$ npm install --save @ionic-native/firebase 
$ npm install angularfire2 firebase --save

Providers/databaseservice/user.ts (實作資料層)

import { Injectable } from '@angular/core';
import {AngularFireDatabase,FirebaseListObservable } from 'angularfire2/database';

@Injectable()
export class UserService {
   constructor(private _af: AngularFireDatabase) {
   }

   public listUsers(): FirebaseListObservable<any[]>{
      return this._af.list('users');
   }

   public addUser(userName,peerID){
        this.listUsers().push({ userName: userName, peerID: peerID});
   }
  
   public removeUser(userName){
       let removeItem = this._af.list('users', {
           query: {
               orderByChild: "userName",
               equalTo: userName
           }
       });

    // Query the list for the purposes of this example:
       removeItem.subscribe((items) => {
           // Remove the matching item:
           if (items.length) {
               removeItem.remove(items[0].$key)
                   .then(() => console.log('removed ' + items[0].$key))
                   .catch((error) => console.log(error));
           }
       });
   }
}

修改app.module.ts
import { AngularFireModule } from 'angularfire2';
import {AngularFireDatabaseModule} from 'angularfire2/database';
import { UserService } from "../providers/databaseservice/users";

// AF2 Settings
export const firebaseConfig = {
  apiKey: "xxxxxxxxxx",
  authDomain: "xxxxxxxxx.firebaseapp.com",
  databaseURL: "https:// xxxxxxxxx.firebaseio.com",
  storageBucket: "xxxxxxxxx.appspot.com",
  messagingSenderId: "xxxxxxxxx"
};


注入module
imports: [
    BrowserModule,
    IonicModule.forRoot(MyApp),
    AngularFireModule.initializeApp(firebaseConfig),
    AngularFireDatabaseModule
  ],

注入prvider
  providers: [
    StatusBar,
    SplashScreen,
    UserService,
    {provide: ErrorHandler, useClass: IonicErrorHandler},
  ]

ionic g page login  建立登入頁

pages/login/login.html
<ion-header>
  <ion-navbar color="primary">
    <ion-title  align-title="center">登錄</ion-title>
  </ion-navbar>
</ion-header>
<ion-content padding>
  <ion-item>
    <ion-label fixed>名稱</ion-label>
    <ion-input type="text" [(ngModel)]="name"></ion-input>
  </ion-item>

  <ion-item>
    <ion-label fixed>密碼</ion-label>
    <ion-input type="text" [(ngModel)]="password"></ion-input>
  </ion-item>
  <p>
   <button ion-button full round (click)="login(name)">登錄</button>
</ion-content>

pages/login/login.ts
@Component({
  selector: 'page-login',
  templateUrl: 'login.html',
})
export class LoginPage {

  constructor(public navCtrl: NavController, public navParams: NavParams) {
  }

  login(name) {
     登入頁面並傳入參數
     this.navCtrl.push(TabsPage, {
      userName: name
    });
  }
}

pages/tabs/tabs.ts
import { Component } from '@angular/core';
import { HomePage } from '../home/home';
import { NavController,NavParams } from 'ionic-angular';

@Component({
  templateUrl: 'tabs.html'
})
export class TabsPage {
  userName; 宣告參數
  tab1Root = HomePage;

  constructor(params: NavParams) {
     this.userName = params.get("userName"); 取得參數
  }
}  

pages/home/home.html
<ion-header>
  <ion-navbar>
    <ion-title>{{viewUserName}} 歡迎</ion-title>
  </ion-navbar>
</ion-header>

<ion-content padding>
    <h1>{{mypeerid}}</h1>
      <ion-card>
        <ion-card-content>
            <video #myvideo></video>
        </ion-card-content>
    </ion-card>
    <ion-item *ngFor="let user of users | async"
        item="item" (click)="startCall(user.userName,user.peerID)">
        {{user.userName}} - {{user.$key}}</ion-item>

     <button ion-button full (click)="videoconnect()">開始通話</button>
     <button ion-button full (click)="stopCall(viewUserName)">結束通話</button>
</ion-content>

pages/home/home.ts
import { Component, ViewChild, OnInit } from '@angular/core';
import { NavController, NavParams } from 'ionic-angular';
import { FirebaseListObservable } from 'angularfire2/database'; 注入fireBase的相關模組
import { UserService } from "../../providers/databaseservice/users";

declare var Peer: any;   宣告peer的物件
@Component({
  selector: 'page-home',
  templateUrl: 'home.html'
})

export class HomePage implements OnInit {
  title = 'app';
  mypeerid: any;
  peer;
  anotherid;
  key;
  @ViewChild('myvideo') myVideo: any取得myVideo的元素
  users: FirebaseListObservable<any[]> fireBase的陣列集合
  viewUserName:any;
  _streamCopy;
  constructor(public navCtrl: NavController, public navParams: NavParams, public userService: UserService) {
    this.users = userService.listUsers(); 取得資料users的集合
    this.viewUserName=navParams.data;
  }

  ngOnInit() {
    let video = this.myVideo.nativeElement;
    this.peer = new Peer({ host: 'peerjs-server.herokuapp.com', secure: true, port: 443 })
    this.mypeerid = this.peer.id;
    setTimeout(() => {
      this.mypeerid = this.peer.id;
       this.userService.addUser(this.viewUserName,this.peer.id );
    }, 1000);
     
    this.peer.on('connection', function (conn) {
      conn.on('data', function (data) {
        console.log(data);
      });
    });

    var n = <any>navigator;
    n.getUserMedia = (n.getUserMedia || n.webkitGetUserMedia || n.mozGetUserMedia || n.msGetUserMedia);
    this.peer.on('call', function (call) {
      n.getUserMedia({ video: true, audio: false }, function (stream) {
        call.answer(stream);
        call.on('stream', function (remotestream) {
          video.src = URL.createObjectURL(remotestream);
          video.play();
        })
      }, function (err) {
        console.log('Failed to get stream', err);
      })
    })
  }

  startCall(userName,anotherID) {
    this.anotherid = anotherID;
    var conn = this.peer.connect(anotherID);
    conn.on('open', function () {
      conn.send('Message from that id');
    });
  }

   this.peer.on('disconnected', function () {
console.log("disconnected");
});

  videoconnect(){
    let video = this.myVideo.nativeElement;
    var localvar = this.peer;
    var fname = this.anotherid;
    var n = <any>navigator;
    console.log("another peerID:"+this.anotherid);
    n.getUserMedia = ( n.getUserMedia || n.webkitGetUserMedia || n.mozGetUserMedia  || n.msGetUserMedia );
   
    n.getUserMedia({
      video:  { width: 700, height: 720 } , audio:false
    }, function(stream) {
      var call = localvar.call(fname, stream);
      call.on('stream', function(remotestream) {
        video.src = URL.createObjectURL(remotestream);
        video.play();
      })
    }, function(err){
      alert(err.name);
      console.log('Failed to get stream', err);
    })
  }

  stopCall(userName) {
    this.userService.removeUser(userName);
    this.peer.destroy();
  }
}

Ionic cordova build android

設定Android的權限

Platforms/android/ AndroidManifest.xml
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.CAMERA" />
    <uses-feature android:name="android.hardware.camera" />
    <uses-feature android:name="android.hardware.camera.autofocus" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />


圖片示範