ocoge/apptool.js

453 lines
18 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/** Electron / Node.js に由来する機能に関する諸々 */
/** ブラウザ動作時にもちょびっと対応 */
'use strict'
// 定数
const ugj_const = {
app_name: 'ocoge',
mascot_dirname: 'img',
mascot_defname: 'tamachee.png',
library_dirname: 'lib',
document_root: 'Documents',
tmp_dir: '.ocogeclub/tmp', //ホームディレクトリからのパス
executable_path: '.ocogeclub/apps/', //ホームディレクトリからのパス
blocks_dir: './blocks', // 動作するコードを web demo で生成するためには「./」をつける
localStorage_fname: 'ocoge.json',
error_ja_all: 'エラーが発生しました。\n『おこげ倶楽部』までお問い合わせください。',
pig: 'pigpio',
lg: 'lgpio', // 対応未定
i2c_defbus: '1', // 文字列リテラルで指定
lang: 'js',
dev_hash: '4e9205f9b7e571bec1aa52ab7871f420684fcf96149672a4d550a95863d6b072'
}
/** クラス appTool ****************************************************************** */
// Electron 動作用
class appTool {
constructor() {
this.path = require('path') //window.ocogeapi.path
this.fs = require('fs') //window.ocogeapi.fs
this.ipcRenderer = require('electron').ipcRenderer //window.ocogeapi.electron_ipcRenderer
this.shell = require('electron').shell //window.ocogeapi.electron_shell
this.saveFilepath = null;
this.wsChanged = false;
this.children = [];
this.gpio_backend = ugj_const.pig;
this.i2c_bus = ugj_const.i2c_defbus;
this.lang = ugj_const.lang;
this.doc_root = this.path.join(process.env["HOME"], ugj_const.document_root);
this.doc_current = this.path.join(process.env["HOME"], ugj_const.document_root);
this.executable_path = this.path.join(process.env["HOME"], ugj_const.executable_path);
// this.blocks_dir = this.path.join(process.env["HOME"], ugj_const.blocks_dir);
this.tmp_dir = this.path.join(process.env["HOME"], ugj_const.tmp_dir);
const EventEmitter = require('events');
this.ugjEmitter = new EventEmitter();
}
// static init = async () => {
// return new elUtil(await elUtil.get_app_path());
// }
// test
async init() {
this.app_path = await this.ipcRenderer.invoke('get_app_path');
this.mascotFilePath = this.path.join(this.app_path, ugj_const.mascot_dirname, ugj_const.mascot_defname);
this.library_path = this.path.join(this.app_path, ugj_const.library_dirname);
this.blocks_dir = this.path.join(this.app_path, ugj_const.blocks_dir);
// this.loadSensorblocks();
}
// センサーブロックのロード
// loadSensorblocks() {
// // ディレクトリの有無
// if (!this.fs.existsSync(this.blocks_dir)) return;
// // ブロックデータ格納ディレクトリのリスト
// const allDirents = this.fs.readdirSync(this.blocks_dir, { withFileTypes: true });
// const blocks_list = allDirents.filter(dirent => dirent.isDirectory()).map(({ name }) => name);
// // センサーカテゴリのインスタンス
// let category_sensors = workspace.getToolbox().getToolboxItemById('category_sensors');
// let flyout_contents = []; // フライアウトのjsonのリスト
// for (let sensor_dir of blocks_list) { //ディレクトリ巡り
// if (sensor_dir.charAt(0) == '.') continue; //隠しディレクトリをスキップ
// // フライアウトのjsonを取得してパース、リストに追加
// let fname = this.path.join(this.blocks_dir, sensor_dir, 'index.json');
// let json_text = this.fs.readFileSync(fname);
// let obj = JSON.parse(json_text);
// flyout_contents = flyout_contents.concat(obj);
// // ブロック定義のスクリプト要素をbody要素の最後に追加
// fname = this.path.join(this.blocks_dir, sensor_dir, 'index.js');
// let script = document.createElement('script');
// script.type = 'text/javascript';
// script.src = fname;
// document.body.appendChild(script);
// }
// let lastline = [{
// "kind": "label",
// "text": " ",
// "web-line": "4.0",
// "web-line-width": "200"
// }];
// flyout_contents = flyout_contents.concat(lastline);
// // センサーカテゴリのフライアウトをアップデート
// category_sensors.updateFlyoutContents(flyout_contents);
// }
// 0で数値の桁合わせ : NUM=値 LEN=桁数
zeroPadding(NUM, LEN) {
return (Array(LEN).join('0') + NUM).slice(-LEN);
}
// 現在の日付時刻から workspace フォルダ内のユニークなファイルパスを作成
getUniqueFilepath() {
let today = new Date();
let filename = today.getFullYear() + '-' + this.zeroPadding((today.getMonth() + 1), 2) + '-' + this.zeroPadding(today.getDate(), 2) + '-' + this.zeroPadding(today.getHours(), 2) + '-' + this.zeroPadding(today.getMinutes(), 2) + '-' + this.zeroPadding(today.getSeconds(), 2);
let filepath = this.path.join(this.doc_current, filename);
return filepath;
}
// リンクを外部ブラウザで開く
openURL(url) {
this.shell.openExternal(url);
}
// saveFilepath を更新
// ウィンドウタイトルバーテキストを変更
setSaveFilepath(filepath) {
this.saveFilepath = filepath;
this.setWsChanged(false);
}
// ワークスペースが変更された・保存された
// ウィンドウタイトルバーテキストを変更
setWsChanged(changed) {
let title;
this.wsChanged = changed;
if (this.saveFilepath) title = this.saveFilepath + ' - ' + ugj_const.app_name;
else title = ugj_const.app_name;
if (changed) title = '*' + title;
this.ipcRenderer.send('set_title', title);
}
// 保存ファイルプロパティを更新
newFile() { this.setSaveFilepath(null); }
// ワークスペースファイル読み込みの一連の動作のラッパ
async loadWsFile() {
let filepath = await this.openFile('xml', this.doc_current);
if (filepath.length > 0) {
if (this.saveFilepath === null) {
this.setSaveFilepath(filepath);
} //読み込みに失敗してもsaveFilepathが更新されてしまうのはちょっと具合が悪いかも
this.doc_current = this.path.dirname(filepath);
return this.readFromFile(filepath);
} else {
return '';
}
}
// その他ファイル読み込みの一連の動作のラッパ
async loadFile(ext) {
let filepath = await this.openFile(ext, this.doc_current);
if (filepath.length > 0) {
this.doc_current = this.path.dirname(filepath);
return this.readFromFile(filepath);
} else {
return '';
}
}
async selectMascotFile() {
return await this.openFile('png', ugj_const.mascot_path);
}
// オープンファイルダイアログ
async openFile(ext, dpath) {
let title = 'Select a file';
let filter;
if (ext == 'xml') {
filter = { name: 'XML - Extensible Markup Language', extensions: ['xml'] };
} else if (ext == 'js') {
filter = { name: 'JS - JavaScript', extensions: ['js'] };
} else if (ext == 'png') {
filter = { name: 'PNG - Portable Network Graphics', extensions: ['png'] };
} else {
filter = { name: 'text file', extensions: ['txt'] };
}
let filepaths = await this.ipcRenderer.invoke('open_dialog', title, dpath, filter);
if (filepaths == undefined) {
return '';
} else {
return filepaths[0];
}
}
// ファイルからデータを読み込み
readFromFile(filepath) {
let data = '';
try {
data = this.fs.readFileSync(filepath, 'utf-8');
}
catch (err) {
console.log(err);
}
return data;
}
// テキストファイル読み込み: 外部スクリプト動的読み込みに使用
readTextFile(filepath) {
return this.readFromFile(filepath);
}
// ワークスペースファイル保存の一連の動作のラッパ
async saveWsFile(data) {
if (this.saveFilepath === null) {
let filepath = await this.selectSaveFile('xml');
if (filepath === undefined) { //キャンセル
return undefined;
} else {
this.setSaveFilepath(filepath);
} //これも保存が成功したら変更するようにすべきかしら
} else this.setWsChanged(false);
return this.writeToFile(this.saveFilepath, data);
}
// その他ファイル保存の一連の動作のラッパ
async saveFile(data, ext) {
let filepath = await this.selectSaveFile(ext);
if (filepath === undefined) { //キャンセル
return undefined;
}
return this.writeToFile(filepath, data);
}
// ファイル保存ダイアログ
async selectSaveFile(ext) {
let title = '保存先を決定してください';
let filter, filter_name;
let defName;
if (ext == 'xml') {
filter = { name: 'xml file', extensions: ['xml'] };
defName = this.getUniqueFilepath() + '.xml';
} else if (ext == 'js' || ext == 'py') {
if (ext == 'js') filter_name = 'javascript file';
else filter_name = 'python file'
filter = { name: filter_name, extensions: [ext] };
// ワークスペース保存名がある場合、それをベースにファイル名の候補を決める
if (this.saveFilepath === null) {
defName = this.getUniqueFilepath() + '.' + ext;
} else {
let dirname = this.path.dirname(this.saveFilepath);
let basename = this.path.basename(this.saveFilepath, '.xml');
defName = this.path.join(dirname, basename) + '.' + ext;
}
} else {
filter = { name: 'text file', extensions: ['txt'] };
}
let filename = await this.ipcRenderer.invoke('save_dialog', title, defName, filter);
if (filename) this.doc_current = this.path.dirname(filename);
return filename;
}
// ファイル書き込み
writeToFile(filepath, data) {
try {
this.fs.writeFileSync(filepath, data);
return true;
}
catch (err) {
return false;
}
}
// GPIO 関連リロードでGPIOをロックしたままハンドルを失うのを防ぐ
cleanupGPIO() {
// this.ugjEmitter.emit('device_stop');//デバイス停止イベント
require('@ocoge.club/' + this.gpio_backend).close_all_handle();
}
// 設定(保存ファイルパスと未保存フラグ)をローカルストレージに保存
savePrefsToLS() {
let wc = '0';
if (this.wsChanged) wc = '1';
// const ser_port = document.getElementById('dlgPort').value;
let o = {
'saveFilepath': this.saveFilepath,
'wsChanged': wc,
'mascotFilePath': this.mascotFilePath,
'doc_current': this.doc_current,
'i2c_bus': this.i2c_bus,
'lang': this.lang
};
let s = JSON.stringify(o);
localStorage.setItem(ugj_const.localStorage_fname, s);
}
// 設定(保存ファイルパスと未保存フラグ)をローカルストレージからロード
loadPrefsFromLS() {
let s = localStorage.getItem(ugj_const.localStorage_fname);
if (s !== null) {
let o = JSON.parse(s);
this.setSaveFilepath(o.saveFilepath);
if (o.wsChanged == '0') this.setWsChanged(false);
else this.setWsChanged(true);
if (o.mascotFilePath) this.setMascotFilePath(o.mascotFilePath);
if (o.doc_current) this.doc_current = o.doc_current;
if (o.i2c_bus) {
this.i2c_bus = o.i2c_bus;
this.ipcRenderer.send('i2c_check_menu', 'i2c-' + this.i2c_bus);
}
if (o.lang) {
this.setLang(o.lang);
this.ipcRenderer.send('lang_check_menu', this.lang);
}
}
}
// マスコット画像パスをプロパティにセット
setMascotFilePath(fpath) {
this.mascotFilePath = fpath;
}
getMascotFilePath() {
return this.mascotFilePath;
}
// i2cバス番号変更
setI2cbusNo(n) {
this.i2c_bus = n;
}
// 言語変更
setLang(l) {
this.lang = l;
const exp = document.getElementById('dlgExport');
if (l == 'js') {
exp.innerText = 'ファイルへ保存';
exp.title = 'ソースコードをファイルに保存します。';
} else {
exp.innerText = 'デプロイ';
exp.title = 'ソースコードをデバイスに転送します。';
}
}
// PyBfm を起動
launchPyBfm() {
let script_path = this.path.join(apptool.library_path, 'pybfm.py');
require('child_process').spawn('python3', [script_path]);
}
// ファイル名にアプリケーションのドキュメントルートまでのパスをつけて返す
getDocPath(filename) {
return this.path.join(this.appDocRoot, filename);
}
// Python コードフォーマッタ Black をコール
pyBeautify = (code) => {
let formatted = '';
formatted = require('child_process').spawnSync('python3', ['-m', 'black', '-'], { input: code }).stdout.toString();
if (!formatted) formatted = code;
return formatted;
}
}
// ブラウザ動作用
class webTool {
constructor() {
// GPIOブロックは使えません
this.gpio_backend = ugj_const.pig;
this.lang = 'js';
this.blocks_dir = ugj_const.blocks_dir;
}
// マスコット
getMascotFilePath() { return `./img/${ugj_const.mascot_defname}`; }
//ワークスペースのダウンロード
saveWsFile(xml_text) {
let blob = new Blob([xml_text], { "type": "text/xml" });
const downLoadLink = document.createElement("a");
document.body.appendChild(downLoadLink);
downLoadLink.download = 'workspace.xml';
downLoadLink.href = URL.createObjectURL(blob);
downLoadLink.click();
downLoadLink.parentElement.removeChild(downLoadLink);
return true;
}
//ワークスペースのインポート
loadWsFile() {
const fileInputEl = document.createElement('input');
document.body.appendChild(fileInputEl);
fileInputEl.type = 'file';
fileInputEl.addEventListener('change', ev => {
let reader = new FileReader();
reader.readAsText(ev.target.files[0]);
reader.addEventListener('load', () => {
let xml = Blockly.Xml.textToDom(reader.result);
Blockly.Xml.domToWorkspace(xml, workspace);
});
});
fileInputEl.click();
fileInputEl.parentElement.removeChild(fileInputEl);
return '';
}
// index.jsから万一呼ばれても何もしない関数
openURL() { ; }
selectMascotFile() { ; }
setMascotFilePath(fname) { ; }
saveFile() { ; }
savePrefsToLS() { ; }
loadPrefsFromLS() { ; }
newFile() { ; }
setWsChanged() { ; }
killAllChildren() { ; }
cleanupGPIO() { ; }
pyBeautify(code) { return code; }
path = {
join: function (a = '', b = '', c = '', d = '', e = '') {
const removeTrailingSlash = path => path.endsWith('/') ? path.substr(0, path.length - 1) : path;
return removeTrailingSlash(a) +
(b ? '/' + removeTrailingSlash(b) : '') +
(c ? '/' + removeTrailingSlash(c) : '') +
(d ? '/' + removeTrailingSlash(d) : '') +
(e ? '/' + removeTrailingSlash(e) : '')
}
}
}
// Electron 動作 / ブラウザ動作自動判別
// const is_el = (typeof window.ocogeapi !== 'undefined')
const is_app = (typeof require === 'function');
// utilクラスのインスタンスを返す
const appTool_new = () => {
if (is_app) {
let ap = new appTool;
ap.init();
return ap;
}
else return new webTool;
}
// "require" for web browsers if contextIsolation is false && nodeIntegration is true:
if (!is_app) {
var require = module_name => {
let block;
switch (module_name) {
case '@tensorflow/tfjs-node':
block = 'TensorFlow';
break;
case '@tensorflow-models/blazeface':
block = '顔認識';
break;
case 'axios':
block = 'URLを取得';
break;
case 'nodemailer':
block = 'メール送信';
break;
case '@ocoge.club/pigpio':
block = 'GPIO';
break;
case 'fs':
block = 'ファイル';
break;
case 'path':
block = 'キャンバス保存';
break;
case 'child_process':
block = '外部プログラム実行';
break;
default:
throw new Error(ugj_const.error_ja_all);
}
throw `ブロック「${block}」は、Web体験版ではご利用になれません。\n詳しくは https://ocoge.club/ をご覧ください。`;
}
var setscriptlangtopy = () => apptool.lang = 'py';
}