Angular 7 でカウントダウンタイマー

Angularチュートリアルを一通りこなしました。わーい。HTTPの章がボリューミーでしんどかったですが勉強になりました。

angular.jp

インメモリで仮設のAPIを作って試せるところがなかなかいいですね。

で、ちょっとなにか作ってみようと思って、デュアルモニターに対応するカウントダウンタイマーを作ってみました。よかったら使ってみてください。

http://tiimer.net/


f:id:wayaguchi:20190610164229p:plain

使い方はシンプルです。

サイトにアクセスして、”Open a new Window” ボタンで別ウインドウ(タブ)を作って、それをデュアルディスプレイ側に持っていきます。そのあとでどちらかのウインドウで分秒を入力して”Set”をクリックするとタイマーが両側のウインドウでスタートします。00:00になるとアラームが鳴ります。音を止めるには”BeepOff”をクリックしてください。

起動時やウインドウオープン時のソースの読み込み以外はサーバとの通信はありません。

 

ソースコードはこちらです。

tiimer/angular at master · kawaguti/tiimer · GitHub

作った過程を以下に記録しておきます。

 

要件: デュアルモニター対応のカウントダウンタイマー

研修でよくカウントダウンタイマーを使うのですが、よく使っていたアプリケーションがOSアップデートの波についていけず、次々と使えなくなるという問題がありました。いろんな人が軽い気持ちでつくてみることができるような性質のアプリですので、マーケットにも常に似たようなアプリが存在していて、次のものを見つければいいのですが、ちょっとめんどくさいんですよね。今回は一つ加えたい要件もあったので、自作してみることにしました。

その要件とは

デュアルモニターでプレゼンしているときに、発表者ディスプレイ側で操作して、受講者向けディスプレイに時間を出したい!

です。

 これを実現する方法は数限りなくあると思いますが、今回はAngularの練習ということもあるので、こういうソリューションを目指しました。

JavaScriptでタイマーを実現し、かつ、HTML の New Window で新しいウインドウを起動して、子ウィンドウ側にタイマーを表示する 

結果的にはもう少し踏み込んだ、複数ウインドウを同期するカウントダウンタイマーになりました。(実装の都合により)

 

要素技術1: AngularのObservableを使って時刻を更新する 

まずはカウントダウンタイマー部分の実装ですが、Angular Tutorial に出てきた、 pub/sub型のイベント処理を扱う Observable という仕組みをつかうと簡単にできそうです。

....と思って探したら素晴らしいサンプルがありました。

qiita.com

このサンプルのいいところは、Observable の使い方を端的に示すだけではなく、html側のパイプの使い方も教えてくれているところです。「もっとシンプルな方法や、間違いの指摘等あればコメントよろしくお願いします。」ということですが、全く思いつきませんでした。ありがとうございます。

要素技術1の派生: カウントダウンタイマー

現在時刻が更新できたら、次はカウントダウンタイマーです。毎秒このイベントが起こることを信じて、残り秒を remainedSec という変数に持ち、これを秒ごとに減算して、ゼロになったら完了、ということにしてみます。

targetSec が最初にセットした残り秒数、remainedSec が現在の残り秒数、elapsedSecが現在の経過秒数です。

https://github.com/kawaguti/tiimer/blob/master/angular/src/app/app.component.ts

export class AppComponent implements OnInit, OnDestroy {
now: Observable<Date>;
intervalList = [];
targetSec: number;
elapsedSec: number;
remainedSec: number;
remainedString: string;
public ngOnInit() {
this.now = new Observable((observer) => {
this.intervalList.push(setInterval(() => {
if ( this.targetSec > 0 && this.remainedSec > 0) {
this.elapsedSec++;
this.remainedSec = this.targetSec - this.elapsedSec;
this.remainedString =
Math.floor(this.remainedSec / 60).toString().padStart(2, '0') + ':' +
Math.floor(this.remainedSec % 60).toString().padStart(2, '0');
}
if ( this.remainedString === '00:00' && this.beepFlag ) { AppComponent.beep(); }

observer.next(new Date());
}, 1000));
});

残り秒数表示が秒だと分単位がわかりにくいので、表示用にremainedStringというのを作っています。これは mm:ss 形式で残り秒数を表示します。

表示が 00:00 になったらビープ音を鳴らす条件式が最後に入っています。ビープ音については次項で説明します。。

要素技術2: ビープ音を鳴らす

カウントが 0 になったら (表示が 00:00 なら) ビープ音を鳴らしたいです。いくつも思いつくのですが、できる限り簡単で、ネットワークに依存しない実装が欲しかったので、こちらを参考にさせていただきました。

qiita.com

音声データもそのまま頂いてしまいました。すみません。

https://github.com/kawaguti/tiimer/blob/master/angular/src/app/app.component.ts

public static beep() {
// tslint:disable-next-line:max-line-length
const base64 = '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';
const sound = new Audio('data:audio/wav;base64,' + base64);
sound.play();
}

beepFlag という変数を持っておいて、カウントダウン開始時にこれをTrueにします。カウントがゼロになったときに、このフラグがTrueだったら、このbeepメソッドを呼んでビープを鳴らします。ユーザーが止めない限り毎秒鳴らしておきます。

ユーザーが止める操作をしたら、beepFlagを False にして次のイベントからはbeepが呼ばれないようになります。

if ( this.remainedString === '00:00' && this.beepFlag ) { AppComponent.beep(); }

要素技術3: マルチウインドウでの同期

普通のHTMLでは、window.openというメソッドで新しいウインドウを開くことができます。このときの戻り値がウインドウオブジェクトというもので、これを経由して小ウインドウのオブジェクトにアクセスすることができます...のですが、ちょっとめんどくさいので、ライブラリを探しました。いいのがありました。

nolanus.github.io

メッセージのキューを実装していて、ウインドウ間でメッセージをやりとりできます。Angular の Observable でメッセージを受け取ることができるところが素敵でした。サンプルがよくてきていて、初心者の私でもわかりやすく、とてもありがたかったです。

f:id:wayaguchi:20190610154950p:plain

サンプルをちょっと変更して、受け取ったメッセージが 0 < n < 3600 の数字だったら、 それを目標の秒数としてセットしてカウントダウンを開始せよ、という司令ということにします。

あと、ビープ音が鳴っているときに消す命令として「beefOff」を設定しました。この文字列が来たらビープ音をOffにします。

https://github.com/kawaguti/tiimer/blob/master/angular/src/app/app.component.ts

this.multiWindowService.onMessage().subscribe((value: Message) => {
// tslint:disable-next-line:radix
const newsec = parseInt(value.data);
if ( newsec > 0 && newsec < 60 * 60 ) {
this.targetSec = newsec;
this.elapsedSec = 0;
this.remainedSec = newsec;
}
if ( value.data === 'beepOff' ) { this.beepFlag = false; }
this.logs.unshift('Received a message from ' + value.senderId + ': ' + value.data);
});

ウインドウIDごとに出し先を変えられるのですが、それは不要なので、基本ブロードキャストします。ブロードキャストといっても、ウインドウのリストにforEachで全部メッセージを投げるだけの実装にしました。

public broadcastMessage( message: string ) {
this.windows.forEach((window) => {
this.sendMessage(window.id, message);
});
}

困ったところ: ビープ音がウインドウごとに鳴ってしまう

要素技術を合わせるときに困ったところは、ビープ音がウインドウごとに鳴ってしまうことでした。

これは、カウントダウンの指示を出したウインドウでだけビープ音が鳴るようにする( beepFlag を Trueにする) ということで解決しました。

https://github.com/kawaguti/tiimer/blob/master/angular/src/app/app.component.html

<button type="submit" class="btn btn-primary"
(click)="broadcastTime(minInput.value, secInput.value);minInput.value='';secInput.value='';beepFlag=true;">Set
</button>

ただ、カウントダウン開始の指示を出したウインドウ以外で止めたいこともあるので、beepFlag の Off はブロードキャストするようにしています。

https://github.com/kawaguti/tiimer/blob/master/angular/src/app/app.component.html

<button class="btn btn-secondary" (click)="beepFlag = false; broadcastMessage('beepOff');">BeepOff</button>

https://github.com/kawaguti/tiimer/blob/master/angular/src/app/app.component.ts

this.multiWindowService.onMessage().subscribe((value: Message) => {
      (中略)
if ( value.data === 'beepOff' ) { this.beepFlag = false; }
      (中略)
});

 

ウインドウサイズに合わせて文字を大きくしたい

細かい話ですが、ウインドウサイズに合わせて文字を大きくしたかったので、HTML5で加わった vh/vw というCSS要素を使いました。

dev.classmethod.jp

とりあえず style 要素で貼っています。そのうちCSSファイルに動かしたい。

https://github.com/kawaguti/tiimer/blob/master/angular/src/app/app.component.html

<p style="Font-Size: 20vw; Margin: 15vh 2vw 0">
{{ remainedString }}
</p>

 

ビルドとデプロイ

手元で動くようになったので、Angular で本番用にビルドしてデプロイしました。

$ ng build --prod

Date: 2019-06-10T06:17:12.545Z
Hash: acbc4eea2ebde4796da1
Time: 29359ms
chunk {0} runtime.26209474bfa8dc87a77c.js (runtime) 1.41 kB [entry] [rendered]
chunk {1} es2015-polyfills.bda95d5896422d031328.js (es2015-polyfills) 56.6 kB [initial] [rendered]
chunk {2} main.780e3db59e24bd6cf759.js (main) 242 kB [initial] [rendered]
chunk {3} polyfills.8bbb231b43165d65d357.js (polyfills) 41 kB [initial] [rendered]
chunk {4} styles.3ff695c00d717f2d2a11.css (styles) 0 bytes [initial] [rendered]

Azureへの静的ファイルのデプロイは前回の記事の手順で行っています。

kawaguti.hateblo.jp

kawaguti.hateblo.jp

 

今後の課題

  • ネットに繋がってない状態でも使いたい!
  • スマホだとスリープに落ちたところでカウントがずれる!
  • スマホだとビープ音がならない!

といった課題が見つかったので、次は Electron か Swift で作ってみたいと思います。あ、二番目は内部ロジックの変更(命令を残り秒数ではなく、目標時間にする)で対応できそうですね。

蛇足ですが、こういう要望が見つかるところが、「動くソフトウェア」の大事なところだな、と思いました。