Mac用ゆっくり系ボイス再生アプリMYukkuriVoiceでユーザー辞書機能に対応した
https://github.com/taku-o/myukkurivoice
MYukkuriVoiceは、Mac用のゆっくりボイスを再生したり、録音したりする機能のあるアプリです。
ユーザー辞書機能を作ろうとして、
同系統のアプリ、SofTalk、棒読みちゃんでは、自力で文章を字句解析して置き換えていたようなので、
自分もまた自力で作ろうとしていたが、
AquesTalkのライブラリで、Mac環境用の辞書作成機能が提供されたので、想定より楽に作れてしまった。
動画作成用のアプリなので、宣伝は動画で行うべきである(主張)。
https://www.nicovideo.jp/watch/sm34217643
https://www.nicovideo.jp/watch/sm34217643
続・WAVファイル。JavaScriptでwavファイルを分解する
前回、wavファイルのフォーマットを調べたので、
wavファイルをパースできるようになった。JavaScriptで。
http://d.hatena.ne.jp/taku-o/20181120/1542726865
import * as fs from 'fs'; const cTable = require('console.table'); fs.readFile('sample.wav', 'binary', (err, content) => { if (err) { console.error(err); return; } let offset = 0; let d = null; let data:any = null; const tables = []; let buffer = Buffer.from(content, 'binary'); // 1-4 Chunk ID "RIFF" d = buffer.readUIntBE(offset, 4); data = Buffer.from(d.toString(16), 'hex').toString(); offset += 4; tables.push({ position: '1-4', header: 'Chunk ID "RIFF"', data: data, }); // 5-8 Chunk Size d = buffer.readUIntLE(offset, 4); data = d; offset += 4; tables.push({ position: '5-8', header: 'Chunk Size', data: data, }); // 9-12 Format "WAVE" d = buffer.readUIntBE(offset, 4); data = Buffer.from(d.toString(16), 'hex').toString(); offset += 4; tables.push({ position: '9-12', header: 'Format "WAVE"', data: data, }); // 13-16 Subchunk1 ID "fmt " d = buffer.readUIntBE(offset, 4); data = Buffer.from(d.toString(16), 'hex').toString(); offset += 4; tables.push({ position: '13-16', header: 'Subchunk1 ID "fmt "', data: data, }); // 17-20 Subchunk1 Size "16" d = buffer.readUIntLE(offset, 4); data = d; offset += 4; tables.push({ position: '17-20', header: 'Subchunk1 Size', data: data, }); // 21-22 Audio Format "1" d = buffer.readUIntLE(offset, 2); data = d; offset += 2; tables.push({ position: '21-22', header: 'Audio Format "1" PCM', data: data, }); // 23-24 Num Channels d = buffer.readUIntLE(offset, 2); data = d; offset += 2; tables.push({ position: '23-24', header: 'Num Channels', data: data, }); // 25-28 Sample Rate d = buffer.readUIntLE(offset, 4); data = d; offset += 4; tables.push({ position: '25-28', header: 'Sample Rate', data: data, }); // 29-32 Byte Rate d = buffer.readUIntLE(offset, 4); data = d; offset += 4; tables.push({ position: '29-32', header: 'Byte Rate', data: data, }); // 33-34 Block Align d = buffer.readUIntLE(offset, 2); data = d; offset += 2; tables.push({ position: '33-34', header: 'Block Align', data: data, }); // 35-36 Bits Per Sample d = buffer.readUIntLE(offset, 2); data = d; offset += 2; tables.push({ position: '35-36', header: 'Bits Per Sample', data: data, }); // 37-40 Subchunk2 ID "data" d = buffer.readUIntBE(offset, 4); data = Buffer.from(d.toString(16), 'hex').toString(); offset += 4; tables.push({ position: '37-40', header: 'Subchunk2 ID "data"', data: data, }); // 41-44 Subchunk2 Size d = buffer.readUIntLE(offset, 4); data = d; offset += 4; tables.push({ position: '41-44', header: 'Subchunk2 Size', data: data, }); // 45- Subchunk2 data console.table(tables); });
- 結果
position header data -------- -------------------- ------ 1-4 Chunk ID "RIFF" RIFF 5-8 Chunk Size 131108 9-12 Format "WAVE" WAVE 13-16 Subchunk1 ID "fmt " fmt 17-20 Subchunk1 Size 16 21-22 Audio Format "1" PCM 1 23-24 Num Channels 1 25-28 Sample Rate 44100 29-32 Byte Rate 88200 33-34 Block Align 2 35-36 Bits Per Sample 16 37-40 Subchunk2 ID "data" data 41-44 Subchunk2 Size 131072
パースした感想
良い勉強の題材でありました。
WAVファイルのフォーマットの確認
アプリで出力したWAVファイルが正しくないのではないか、という問題に遭遇して、
その際、バイナリファイルといろいろな資料を読みながら、
WAVファイルがどのような仕様になっているか調べたので、その時の調査メモ。
用意したもの
バイナリエディタと電卓。
大きな数値の項目はリトルエンディアンで、文字の項目はASCII。
16進数を10進数で読む。
なので、用意する電卓は↑の変換が出来るものでないと読めないですのぅ。
バイナリファイルを読む
実際に作ったWAVファイルをバイナリエディタで読んでみつつ、
どの位置にどのような値が入っているか確認。
WAVEファイル その1
bytes | 定義 | 入っていた値 | 解読 | 説明、または検算 |
---|---|---|---|---|
1-4 | Chunk ID | 52 49 46 46 | RIFF | "RIFF"固定 |
5-8 | Chunk Size | FC 72 00 00 | 29436 | ファイルサイズ - 8bytes |
ファイルサイズからChunk ID Chunk Size除いた分か | ||||
29444 - 8 = 29436 | ||||
9-12 | Format | 57 41 56 45 | WAVE | "WAVE"固定 |
13-16 | Subchunk1 ID | 66 6D 74 | fmt | "fmt"固定 |
17-20 | Subchunk1 Size | 20 10 00 00 | 16 | "16"固定。Subchunk1の長さ(21-36)なので |
21-22 | Audio Format | 01 00 | 1 | "1"固定。(PCMなら) |
23-24 | Num Channels | 01 00 | 1 | チャネル数 |
25-28 | Sample Rate | 40 1F 00 00 | 8000 | サンプリングレート |
29-32 | Byte Rate | 80 3E 00 00 | 16000 | ブロックサイズ |
SampleRate * NumChannels * BitsPerSample/8 | ||||
(8000 * 1 * 16) / 8 = 16000 | ||||
33-34 | Block Align | 02 00 | 2 | 1サンプルあたりのビット数 |
NumChannels * BitsPerSample / 8 | ||||
(1 * 16) / 8 = 2 | ||||
35-36 | Bits Per Sample | 10 00 | 16 | 8 bits = 8, 16 bits = 16 |
37-40 | Subchunk2 ID | 64 61 74 61 | data | "data"固定 |
41-44 | Subchunk2 Size | D8 72 00 00 | 29400 | このチャンクのデータのサイズ |
ここからファイルの最後までが波形データだから | ||||
ファイルサイズ - ここまでのサイズ(44)になるはず | ||||
29444 - 44 = 29400 | ||||
45- | data | ここからファイルの最後まで波形データが入っている |
WAVEファイル その2
bytes | 定義 | 入っていた値 | 解読 | 説明、または検算 |
---|---|---|---|---|
1-4 | Chunk ID | 52 49 46 46 | RIFF | "RIFF"固定 |
5-8 | Chunk Size | 24 00 02 00 | 131108 | ファイルサイズ - 8bytes |
131116 - 8 = 131108 | ||||
9-12 | Format | 57 41 56 45 | WAVE | "WAVE"固定 |
13-16 | Subchunk1 ID | 66 6D 74 | fmt | "fmt"固定 |
17-20 | Subchunk1 Size | 20 10 00 00 | 16 | "16"固定。 |
21-22 | Audio Format | 01 00 | 1 | "1"固定。(PCMなら) |
23-24 | Num Channels | 01 00 | 1 | チャネル数 |
25-28 | Sample Rate | 44 AC 00 00 | 44100 | サンプリングレート |
29-32 | Byte Rate | 88 58 01 00 | 88200 | ブロックサイズ |
SampleRate * NumChannels * BitsPerSample/8 | ||||
(44100 * 1 * 16) / 8 = 88200 | ||||
33-34 | Block Align | 02 00 | 2 | 1サンプルあたりのビット数 |
NumChannels * BitsPerSample / 8 | ||||
(1 * 16) / 8 = 2 | ||||
35-36 | Bits Per Sample | 10 00 | 16 | 8 bits = 8, 16 bits = 16 |
37-40 | Subchunk2 ID | 64 61 74 61 | data | "data"固定 |
41-44 | Subchunk2 Size | 00 00 02 00 | 131072 | このチャンクのデータのサイズ |
131116 - 44 = 131072 | ||||
45- | data | ここから波形データ |
WAVEのバイナリ読んだ感想
読みやすいフォーマット。
アプリからPCMのWAVEファイルを出力すると仮定した時、
ファイルサイズの箇所以外は割と固定値になる。
RIFFの仕様
WAVEで使われているRIFF (Resource Interchange File Format)の仕様も見ておく。
なるほどなー。
bytes | 定義 | 説明、または検算 | |
---|---|---|---|
1-4 | Chunk ID | ChunkのID | |
5-8 | Chunk Size | Chunkのサイズ - IDとSizeフィールドを除いた値 | |
9- | data | チャンクのデータ。データのサイズが偶数で無いなら埋める |
もし、WAVEファイルにiXMLチャンクを差し込むなら
WAVEファイルにiXMLチャンクという仕様があって、
ずっと、どうやってデータを入れれば良いのかサンプルが見つからなくて困っていたんだけど、
RIFFの仕様をみたら、分かってきた。
同じように入れれば良いのだろう。(試してない)
bytes | 定義 | 入れる値 | 解読 | 説明、または検算 |
---|---|---|---|---|
1-4 | Chunk ID | 69 58 4D 4C | iXML | "iXML"固定 |
5-8 | Chunk Size | iXMLのデータサイズ - 8 | ||
9- | data | iXMLのデータ |
chunkのdataに入れるデータは次のようなものになるだろう。
role nameの所以外は固定になりそう?
<?xml version="1.0" encoding="UTF-8"?> <BWFXML> <IXML_VERSION>2.02</IXML_VERSION> <TRACK_LIST> <TRACK_COUNT>1</TRACK_COUNT> <TRACK> <CHANNEL_INDEX>1</CHANNEL_INDEX> <INTERLEAVE_INDEX>1</INTERLEAVE_INDEX> <NAME>role name</NAME> </TRACK> </TRACK_LIST> </BWFXML>
↓実際に入れるデータ
<?xml version="1.0" encoding="UTF-8"?><BWFXML><IXML_VERSION>2.02</IXML_VERSION><TRACK_LIST><TRACK_COUNT>1</TRACK_COUNT><TRACK><CHANNEL_INDEX>1</CHANNEL_INDEX><INTERLEAVE_INDEX>1</INTERLEAVE_INDEX><NAME>role name</NAME></TRACK></TRACK_LIST></BWFXML>
結論。ダメだった。BWFの仕様に合わせる必要があって、
他にも入れないとイケナイデータがある。
それと、Chunkの順番が決まっているようだ。
マルチなウィンドウのElectronアプリで、メニュー制御する話
Electronのアプリで、ウィンドウ毎に、メニューを切り替えたいとか、
有効無効を切り替えたいことってあると思うんです。
今回はフォーカスが当たっている時だけ、メニューを有効にする方法の話。
メニューアイテムのenabledを切り替える
メニューの有効無効を切り替えるには、
メニューのアイテムのenabled属性を切り替えれば良し。
メニューを作るときに、id属性を設定しておいて。
import {Menu} from 'electron'; const menuList = [ { ... (略) ... }, { label: '辞書', submenu: [ { id: 'dict-close', // ←←← idを設定 enabled: false, label: '辞書を閉じる (⌘W)', click() { if (myApp.dictWindow) { myApp.dictWindow.close(); } }, }, { id: 'dict-tutorial', // ←←← idを設定 enabled: false, label: '辞書チュートリアル', click() { if (myApp.dictWindow) { myApp.dictWindow.webContents.send('menu', 'tutorial'); } }, }, {type: 'separator'}, { id: 'dict-add', // ←←← idを設定 enabled: false, label: '定義データ追加 (⌘N)', click() { if (myApp.dictWindow) { myApp.dictWindow.webContents.send('menu', 'add'); } }, }, ] }, { ... (略) ... }, ]; const menuTemplate = Menu.buildFromTemplate(menuList); Menu.setApplicationMenu(menuTemplate);
そのidでメニューを取り出して、enabled属性を切り替える。
import {Menu} from 'electron'; const dictMenuItems = [ 'dict-close', 'dict-tutorial', 'dict-add', 'dict-delete', 'dict-save', 'dict-cancel', 'dict-export', 'dict-reset', ]; const menu = Menu.getApplicationMenu(); for (let m of dictMenuItems) { const item = menu.getMenuItemById(m); item.enabled = true; // ←←← enabledを切り替え }
フォーカスされたらメニューを有効に、フォーカスが外れたらメニューを無効に
BrowserWindowがフォーカスされている時だけメニューを有効にするには、
focus、blur、それとcloseのイベントを拾う。
closeも拾って置かないと、フォーカスされた状態でウィンドウ閉じられると、
ウィンドウ閉じてるのにメニューが有効なままになる。
this.dictWindow.on('close', () => { disableDictMenu(); // ←←← イベントを拾って、メニューのenabledを切り替える }); this.dictWindow.on('focus', () => { enableDictMenu(); // ←←← イベントを拾って、メニューのenabledを切り替える }); this.dictWindow.on('blur', () => { disableDictMenu(); // ←←← イベントを拾って、メニューのenabledを切り替える });
ウィンドウまわりのコードも含めると。
... (略) ... function showDictWindow(): void { const acceptFirstMouse = this.appCfg.acceptFirstMouse; this.dictWindow = new BrowserWindow({ width: width, height: height, show: false, // show at did-finish-load event }); this.dictWindow.loadURL(`file://${__dirname}/contents-dict.html`); // window event this.dictWindow.webContents.on('did-finish-load', () => { myApp.dictWindow.show(); myApp.dictWindow.focus(); }); this.dictWindow.on('close', () => { disableDictMenu(); }); this.dictWindow.on('focus', () => { enableDictMenu(); }); this.dictWindow.on('blur', () => { disableDictMenu(); }); }
メニューのacceleratorが邪魔な話
メニューアイテムにacceleratorを設定していると、
そのショートカットキーは、全てのウィンドウで効いてしまう。
import {Menu} from 'electron'; const menuList = [ { ... (略) ... }, { label: '辞書', submenu: [ { role: 'quit', accelerator: 'Command+Q', // ←←← acceleratorってこれ }, ] }, { ... (略) ... }, ]; const menuTemplate = Menu.buildFromTemplate(menuList); Menu.setApplicationMenu(menuTemplate);
これが邪魔なら、なんらかの対処をいれましょう。
acceleratorを無効にするか、
有効にしたい時以外はacceleratorを切る。
終わり
オワリダヨ
Electronアプリで、アプリアイコンにドロップされたファイルを処理する
Finder上のアプリアイコンにファイルをドロップされた時、
Dock上のアプリアイコンにファイルをドロップされた時、
Electron製のアプリで、そのファイルを受け取って処理する機能の実装方法です。
(Macの話だったり、私製アプリのコードの断片が入ってたり、
Typescriptだったり、パラメータが雑だったりするけど、うまく読み替えて欲しい。)
アプリのデータの持ち方にもよりますけど、
・アプリが起動中にファイルをドロップする
・アプリが起動していない時にファイルをドロップする
で、処理方法を変える必要があると思う。
Info.plistでアプリがファイルを受け取れるように設定する
- Mac環境の話
- ファイルをアプリアイコンに落とした時に反応するように、アプリのInfo.plistを更新します。
- 次のようなplistファイルを作って、アプリのInfo.plistに組み込みます。
- この設定を入れないと、ファイルをドロップしても、アプリが反応しない。
- extend.plist
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> <key>CFBundleDocumentTypes</key> <array> <dict> <key>CFBundleTypeName</key> <string>All Files</string> <key>LSHandlerRank</key> <string>Owner</string> <key>LSItemContentTypes</key> <array> <string>public.text</string> <string>public.data</string> <string>public.content</string> </array> </dict> </array> </dict> </plist>
- このextend.plistを、electron-packagerコマンド実行時に組み込んで、Info.plistにパラメータを追加します。
- extend-infoで指定します。
electron-packager . YourAppName --platform=darwin --arch=x64 --extend-info=extend.plist
ファイルドロップのイベントを受け取る
- "will-finish-launching"のタイミングで、"open-file"イベントをハンドリングする処理を登録します。(ここより前で拾うとマズイのかな?試さないけど)
- main processのjavascript内に実装します。
- preventDefault()した後に、ファイルをハンドリングする処理を入れます。
import {app} from 'electron'; // receive drop file to app icon event let launchArgs = null; app.on('will-finish-launching', () => { app.on('open-file', (event, filePath) => { event.preventDefault(); if (myApp.mainWindow) { myApp.mainWindow.webContents.send('dropTextFile', filePath); } else { launchArgs = { filePath: filePath }; } }); }); ...(略)...
アプリアイコンにファイルをドロップする際、
- アプリが起動済みである場合と、
- アプリがまだ起動していない場合がありますよね。
アプリが起動済みで、BrowserWindowのインスタンスが既に作られているのであれば、
このようなファイルが渡されたよ、とイベントを飛ばせば事足ります。
if (myApp.mainWindow) { myApp.mainWindow.webContents.send('dropTextFile', filePath); }
BrowserWindowインスタンス初期化時に、ファイルの情報を渡す
- 起動していないアプリに、ファイルがドロップされた場合は、BrowserWindowのインスタンスはまだ作られていません。
- ウィンドウのインスタンスが出来てから、イベントを投げて、ファイルの情報を渡してあげましょう。
- (何らかの変数越しにパラメータを渡しても良いですけれど)
function showMainWindow(launchArgs): void { const myApp = this; let _launchArgs = launchArgs; mainWindow = new BrowserWindow({ width: 800, height: 800, x: x, y: y, show: false, // show at did-finish-load event }); mainWindow.loadURL(`file://${__dirname}/contents-main.html`); // main window event mainWindow.webContents.on('did-finish-load', () => { // receive drop file to app icon event if (_launchArgs && _launchArgs.filePath) { const filePath = _launchArgs.filePath; _launchArgs = null; // for window reload mainWindow.webContents.send('dropTextFile', filePath); } // show mainWindow.show(); mainWindow.focus(); });
- "did-finish-load"のタイミングで、BrowserWindowインスタンスにイベントを投げています。
- ドロップされたファイルの情報をクリアしておかないと、BrowserWindowを再読込した時に、また読み込まれてしまいますよ。
// main window event mainWindow.webContents.on('did-finish-load', () => { // receive drop file to app icon event if (_launchArgs && _launchArgs.filePath) { const filePath = _launchArgs.filePath; _launchArgs = null; // リロードされた時のために変数の中身を消しておく mainWindow.webContents.send('dropTextFile', filePath); } // show mainWindow.show(); mainWindow.focus(); });
renderer processに渡されたファイルを読み込む
- renderer processで渡されたファイルを読み込みます。
- 起動済みのアプリにイベントを投げられた場合、起動前のアプリにイベントを投げられた場合で、どちらも同じインターフェイスで処理出来ると良いですね。
- ここではipcRendererを使って処理しています。
// dropTextFile event ipcRenderer.on('dropTextFile', (event, filePath: string) => { fs.readFile(filePath, 'utf-8', (err: Error, data: string) => { if (err) { MessageService.error('テキストファイルを読み込めませんでした。', err); return; } const win = remote.getCurrentWindow(); win.focus(); $scope.yinput.source = data; $timeout(() => { $scope.$apply(); }); }); });
- アプリアイコンにファイルを落とした時は、Electronアプリにはフォーカスが当たっていません。
- このままでは、Electronアプリの画面のデータが更新されない、などの問題が起きます。
- focus()を実行して、ウィンドウにフォーカスを当てましょう。
var remote = require('electron').remote; const win = remote.getCurrentWindow(); win.focus();
makeSingleInstance
- アプリが複数立ち上がっていると、うまく動かない場合があると思います。
- (そういう記述を見た。いちいち検証しないよ。私のアプリではシングルインスタンスを有効にしないしね。)
- makeSingleInstance()を実行して、アプリを複数起動できないようにして対処します。
app.makeSingleInstance((argv: string[], workingDirectory: string) => {});
- 最新のバージョンでは、makeSingleInstance()は廃止され、代わりにrequestSingleInstanceLock()を使うことになりました。
- https://electronjs.org/docs/api/app
const locked = app.requestSingleInstanceLock()
おわり
おわりだよ
Electronアプリは、まずelectron-localshortcut入れて、Command+R、Command+Wを潰すのがセオリーだと思う
おまえ、セオリー語れるほど、Electronやってないだろう。
globalShortcut
Electronにはショートカットキーを制御するglobalShortcutという機能があるんですけど、
これは公式ドキュメントにも書いてあるとおり、グローバルに効いてしまう。
アプリにフォーカスが当たって無くても、効いちゃうんですね。
これはこれで便利だけど、効き過ぎちゃう。
こんなCommand+Sを封じるアプリとか作ったら、あらゆるアプリのCommand+Sを封じちゃって大変ですね。
やってはいけませんよ。
'use strict'; const electron = require('electron'); const app = electron.app; const globalShortcut = electron.globalShortcut; app.on('ready', function() { globalShortcut.register('Command+S', function() { // アプリが立ち上がっている間、Command+S による保存を封じるぞ // 嫌がらせかな? console.log('Command+S is pressed') }) });
electron-localshortcut
ということで、electron-localshortcutです。
アプリにショートカットキーをつけたいならこちらです。
npm install --save electron-localshortcut
BrowserWindowインスタンスを生成したあと、
ウィンドウ毎にショートカットキーを登録する。
昔はウィンドウ指定できなかった。今はショートカットキーを有効にするウィンドウを指定する仕様になった。
var electron = require('electron'); var app = electron.app; var localShortcut = require("electron-localshortcut"); var mainWindow; ...(中略)... mainWindow = new BrowserWindow({ width: 600, height: 600, acceptFirstMouse: true, show: false, // show at did-finish-load event }); mainWindow.loadURL(`file://${__dirname}/window-main.html`); // ショートカットキー // Command+Qでアプリを閉じる localShortcut.register(mainWindow, 'Command+Q', function() { app.quit(); }); // event mainWindow.webContents.on('did-finish-load', function() { mainWindow.show(); mainWindow.focus(); });
アプリらしく
で、今回のタイトルです。
Electronのアプリを作ると、
Command+R で画面のリロード、
Command+W でウィンドウを閉じる機能が動いてしまう。
ウェブブラウザに元々ついている機能です。
アプリにもよりますけど、自分はこれ、格好悪いと思う。
とてもブラウザブラウザ(?)してる。
なので、electron-localshortcut で、これのショートカットキーを上書きして、
機能にしないようにしておくべきなのではないか。そう思うのです。
localShortcut.register(mainWindow, 'Command+R', function() { // do nothing }); localShortcut.register(mainWindow, 'Command+W', function() { // do nothing });
おわり
おわりです。
なんかMac前提の話してるけど、細かいことは気にしない!!!
画像をElectron側にキャッシュしておく話
Electronを業務で使っている会社とかあるらしい。
で、自分も活用してみよう。Electronでどんなアプリを作るかって考えたら、
サーバーのデータを参照するElectronアプリも作れるけれど、
でも、それだったら、Webページでも良いわけで、
Electronを使うなら、やっぱりスタンドアローンなアプリにしたいじゃないですか。
しかし、業務で扱うデータって、残念ながら、だいたいサーバー上に置いてある。
そのデータに触れずに業務に役立つアプリを作ることは難しい。
それじゃ、こういう、ローカルとサーバ上、両方にデータがあるElectronアプリにするのが良いのかな、と。
そこで画像キャッシュだ
なぜ、画像かはわからないが、今回は画像だ。
毎回データをサーバーに取りに行かずに、(作るのが重い画像とかだ)
Electronのアプリ内部でキャッシュしておこう、という話。
実装
- まず最初にimage-cacheを入れておいて、
npm install --save image-cache
- cacheに画像を渡す処理。cacheから画像を取り出す処理を入れる。
# renderer.js var imageCache = require('image-cache'); ...(中略)... // tweet.user_image は 画像URL // // not found image cache if (! imageCache.isCachedSync(tweet.user_image)) { imageCache.setCache(tweet.user_image, function(error) { log.error(error); }); tweet.user_image_link = tweet.user_image; // found image cache } else { const img = imageCache.getCacheSync(tweet.user_image); tweet.user_image_link = img.data; }
- angular.jsだけど
- image-cacheから取り出したcacheは、BASE64な文字になっている。そのままそれを表示して良い。
# renderer.html <li ng-repeat="item in streams track by item.idx"> <img ng-src="{{item.user_image_link}}" width="32" height="32"> </li>
まとめ
Electronあまり関係ない話になってしまった。