7254f7b4d1
Build / Build (macos-latest) (push) Has been cancelled
Build / Build (ubuntu-latest) (push) Has been cancelled
Build / Build (windows-latest) (push) Has been cancelled
Build / Analyze (javascript) (push) Has been cancelled
Build / Analyze (python) (push) Has been cancelled
682 lines
25 KiB
JavaScript
682 lines
25 KiB
JavaScript
|
|
import * as base from './base.js';
|
|
import * as child_process from 'child_process';
|
|
import * as electron from 'electron';
|
|
import * as fs from 'fs';
|
|
import * as http from 'http';
|
|
import * as https from 'https';
|
|
import * as node from './node.js';
|
|
import * as os from 'os';
|
|
import * as path from 'path';
|
|
import * as url from 'url';
|
|
import * as view from './view.js';
|
|
|
|
const desktop = {};
|
|
|
|
desktop.Host = class {
|
|
|
|
constructor() {
|
|
this._document = window.document;
|
|
this._window = window;
|
|
this._global = global;
|
|
this._telemetry = new base.Telemetry(this._window);
|
|
process.on('uncaughtException', (error) => {
|
|
this.exception(error, true);
|
|
this.message(error.message);
|
|
});
|
|
this._global.eval = () => {
|
|
throw new Error('eval.eval() not supported.');
|
|
};
|
|
this._window.eval = () => {
|
|
throw new Error('window.eval() not supported.');
|
|
};
|
|
this._window.addEventListener('unload', () => {
|
|
if (typeof __coverage__ !== 'undefined') {
|
|
const dir = path.join(process.cwd(), 'dist', 'nyc', '.nyc_output');
|
|
if (!fs.existsSync(dir)) {
|
|
fs.mkdirSync(dir, { recursive: true });
|
|
}
|
|
const base = path.basename(window.location.pathname, '.html');
|
|
const file = path.join(dir, `${base}.json`);
|
|
/* eslint-disable-next-line no-undef */
|
|
fs.writeFileSync(file, JSON.stringify(__coverage__));
|
|
}
|
|
});
|
|
this._environment = electron.ipcRenderer.sendSync('get-environment', {});
|
|
this._environment.menu = this._environment.titlebar && this._environment.platform !== 'darwin';
|
|
this._files = [];
|
|
electron.ipcRenderer.on('open', (sender, data) => {
|
|
this._open(data);
|
|
});
|
|
this._element('menu-button').style.opacity = 0;
|
|
if (!/^\d+\.\d+\.\d+$/.test(this.version)) {
|
|
throw new Error('Invalid version.');
|
|
}
|
|
const metadata = [];
|
|
metadata.push(os.arch());
|
|
let packager = '';
|
|
if (process.platform === 'linux') {
|
|
try {
|
|
child_process.execFileSync('dpkg', ['-S', process.execPath]);
|
|
packager = 'deb';
|
|
} catch {
|
|
try {
|
|
child_process.execFileSync("rpm", ["-qf", process.execPath]);
|
|
packager = 'rpm';
|
|
} catch {
|
|
// continue regardless of error
|
|
}
|
|
}
|
|
}
|
|
metadata.push(packager);
|
|
this._metadata = metadata.join('|');
|
|
}
|
|
|
|
get window() {
|
|
return this._window;
|
|
}
|
|
|
|
get document() {
|
|
return this._document;
|
|
}
|
|
|
|
get version() {
|
|
return this._environment.version;
|
|
}
|
|
|
|
get type() {
|
|
return 'Electron';
|
|
}
|
|
|
|
get metadata() {
|
|
return this._metadata;
|
|
}
|
|
|
|
async view(view) {
|
|
this._view = view;
|
|
const age = async () => {
|
|
const days = (new Date() - new Date(this._environment.date)) / (24 * 60 * 60 * 1000);
|
|
if (days > 180) {
|
|
this.document.body.classList.remove('spinner');
|
|
const link = this._element('logo-github').href;
|
|
for (;;) {
|
|
/* eslint-disable-next-line no-await-in-loop */
|
|
await this.message('Please update to the newest version.', null, 'Download');
|
|
this.openURL(link);
|
|
}
|
|
}
|
|
return Promise.resolve();
|
|
};
|
|
const consent = async () => {
|
|
const time = this.get('consent');
|
|
if (!time || (Date.now() - time) > 30 * 24 * 60 * 60 * 1000) {
|
|
let consent = true;
|
|
try {
|
|
const content = await this._request('https://ipinfo.io/json', { 'Content-Type': 'application/json' }, 2000);
|
|
const json = JSON.parse(content);
|
|
const countries = ['AT', 'BE', 'BG', 'HR', 'CZ', 'CY', 'DK', 'EE', 'FI', 'FR', 'DE', 'EL', 'HU', 'IE', 'IT', 'LV', 'LT', 'LU', 'MT', 'NL', 'NO', 'PL', 'PT', 'SK', 'ES', 'SE', 'GB', 'UK', 'GR', 'EU', 'RO'];
|
|
if (json && json.country && countries.indexOf(json.country) === -1) {
|
|
consent = false;
|
|
}
|
|
} catch {
|
|
// continue regardless of error
|
|
}
|
|
if (consent) {
|
|
this.document.body.classList.remove('spinner');
|
|
await this.message('This app uses cookies to report errors and anonymous usage information.', null, 'Accept');
|
|
}
|
|
this.set('consent', Date.now());
|
|
}
|
|
};
|
|
const telemetry = async () => {
|
|
if (this._environment.packaged) {
|
|
const measurement_id = '848W2NVWVH';
|
|
const user = this.get('user') || null;
|
|
const session = this.get('session') || null;
|
|
await this._telemetry.start(`G-${measurement_id}`, user && user.indexOf('.') !== -1 ? user : null, session);
|
|
this._telemetry.send('page_view', {
|
|
app_name: this.type,
|
|
app_version: this.version,
|
|
app_metadata: this.metadata
|
|
});
|
|
this._telemetry.send('scroll', {
|
|
percent_scrolled: 90,
|
|
app_name: this.type,
|
|
app_version: this.version,
|
|
app_metadata: this.metadata
|
|
});
|
|
this.set('user', this._telemetry.get('client_id'));
|
|
this.set('session', this._telemetry.session);
|
|
}
|
|
};
|
|
await age();
|
|
await consent();
|
|
await telemetry();
|
|
}
|
|
|
|
async start() {
|
|
if (this._files) {
|
|
const files = this._files;
|
|
delete this._files;
|
|
if (files.length > 0) {
|
|
const data = files.pop();
|
|
this._open(data);
|
|
}
|
|
}
|
|
this._window.addEventListener('focus', () => {
|
|
this._document.body.classList.add('active');
|
|
});
|
|
this._window.addEventListener('blur', () => {
|
|
this._document.body.classList.remove('active');
|
|
});
|
|
if (this._document.hasFocus()) {
|
|
this._document.body.classList.add('active');
|
|
}
|
|
electron.ipcRenderer.on('recents', (sender, data) => {
|
|
this._view.recents(data);
|
|
});
|
|
electron.ipcRenderer.on('export', (sender, data) => {
|
|
this._view.export(data.file);
|
|
});
|
|
electron.ipcRenderer.on('cut', () => {
|
|
this.document.execCommand('cut');
|
|
});
|
|
electron.ipcRenderer.on('copy', () => {
|
|
this.document.execCommand('copy');
|
|
});
|
|
electron.ipcRenderer.on('paste', () => {
|
|
if (this.document.queryCommandSupported('paste')) {
|
|
this.document.execCommand('paste');
|
|
} else if (this.document.queryCommandSupported('insertText')) {
|
|
const content = electron.clipboard.readText();
|
|
this.document.execCommand('insertText', false, content);
|
|
}
|
|
});
|
|
electron.ipcRenderer.on('selectall', () => {
|
|
this.document.execCommand('selectall');
|
|
});
|
|
electron.ipcRenderer.on('toggle', (sender, name) => {
|
|
this._view.toggle(name);
|
|
this.update({ ...this._view.options });
|
|
});
|
|
electron.ipcRenderer.on('zoom-in', () => {
|
|
this._element('zoom-in-button').click();
|
|
});
|
|
electron.ipcRenderer.on('zoom-out', () => {
|
|
this._element('zoom-out-button').click();
|
|
});
|
|
electron.ipcRenderer.on('zoom-reset', () => {
|
|
this._view.resetZoom();
|
|
});
|
|
electron.ipcRenderer.on('show-properties', () => {
|
|
this._element('sidebar-target-button').click();
|
|
});
|
|
electron.ipcRenderer.on('find', () => {
|
|
this._view.find();
|
|
});
|
|
electron.ipcRenderer.on('about', () => {
|
|
this._view.about();
|
|
});
|
|
this._element('titlebar-close').addEventListener('click', () => {
|
|
electron.ipcRenderer.sendSync('window-close', {});
|
|
});
|
|
this._element('titlebar-toggle').addEventListener('click', () => {
|
|
electron.ipcRenderer.sendSync('window-toggle', {});
|
|
});
|
|
this._element('titlebar-minimize').addEventListener('click', () => {
|
|
electron.ipcRenderer.sendSync('window-minimize', {});
|
|
});
|
|
electron.ipcRenderer.on('window-state', (sender, data) => {
|
|
if (this._environment.titlebar) {
|
|
this._element('target').style.marginTop = '32px';
|
|
this._element('target').style.height = 'calc(100% - 32px)';
|
|
this._element('sidebar-title').style.marginTop = '24px';
|
|
this._element('sidebar-closebutton').style.marginTop = '24px';
|
|
this._element('titlebar').classList.add('titlebar-visible');
|
|
}
|
|
if (this._environment.titlebar && this._environment.platform !== 'darwin' && !data.fullscreen) {
|
|
this._element('titlebar-control-box').classList.add('titlebar-control-box-visible');
|
|
} else {
|
|
this._element('titlebar-control-box').classList.remove('titlebar-control-box-visible');
|
|
}
|
|
this._element('menu-button').style.opacity = this._environment.menu ? 1 : 0;
|
|
this._element('titlebar-maximize').style.opacity = data.maximized ? 0 : 1;
|
|
this._element('titlebar-restore').style.opacity = data.maximized ? 1 : 0;
|
|
this._element('titlebar-toggle').setAttribute('title', data.maximized ? 'Restore' : 'Maximize');
|
|
});
|
|
electron.ipcRenderer.sendSync('update-window-state', {});
|
|
const openFileButton = this._element('open-file-button');
|
|
if (openFileButton) {
|
|
openFileButton.addEventListener('click', async () => {
|
|
await this.execute('open');
|
|
});
|
|
}
|
|
this.document.addEventListener('dragover', (e) => {
|
|
e.preventDefault();
|
|
});
|
|
this.document.addEventListener('drop', (e) => {
|
|
e.preventDefault();
|
|
});
|
|
this.document.body.addEventListener('drop', (e) => {
|
|
e.preventDefault();
|
|
const files = Array.from(e.dataTransfer.files);
|
|
const paths = files.map((file) => electron.webUtils.getPathForFile(file));
|
|
if (paths.length > 0) {
|
|
electron.ipcRenderer.send('drop-paths', { paths });
|
|
}
|
|
return false;
|
|
});
|
|
this._view.show('welcome');
|
|
}
|
|
|
|
environment(name) {
|
|
return this._environment[name];
|
|
}
|
|
|
|
async error(message) {
|
|
await this.message(message, true, 'OK');
|
|
}
|
|
|
|
async require(id) {
|
|
return import(`${id}.js`);
|
|
}
|
|
|
|
worker(id) {
|
|
return new this.window.Worker(`${id}.js`, { type: 'module' });
|
|
}
|
|
|
|
async save(name, extension, defaultPath) {
|
|
return new Promise((resolve, reject) => {
|
|
electron.ipcRenderer.once('show-save-dialog-complete', (event, data) => {
|
|
if (data.error) {
|
|
reject(new Error(data.error));
|
|
} else if (data.canceled) {
|
|
resolve(null);
|
|
} else {
|
|
resolve(data.filePath);
|
|
}
|
|
});
|
|
electron.ipcRenderer.send('show-save-dialog', {
|
|
title: 'Export Tensor',
|
|
defaultPath,
|
|
buttonLabel: 'Export',
|
|
filters: [{ name, extensions: [extension] }]
|
|
});
|
|
});
|
|
}
|
|
|
|
async export(file, blob) {
|
|
const window = this.window;
|
|
const reader = new window.FileReader();
|
|
reader.onload = (e) => {
|
|
const data = new Uint8Array(e.target.result);
|
|
fs.writeFile(file, data, null, async (error) => {
|
|
if (error) {
|
|
await this._view.error(error, 'Error writing file.');
|
|
}
|
|
});
|
|
};
|
|
let error = null;
|
|
if (!blob) {
|
|
error = new Error(`Export blob is '${JSON.stringify(blob)}'.`);
|
|
} else if (blob instanceof window.Blob === false) {
|
|
error = new Error(`Export blob type is '${typeof blob}'.`);
|
|
}
|
|
if (error) {
|
|
await this._view.error(error, 'Error exporting image.');
|
|
} else {
|
|
reader.readAsArrayBuffer(blob);
|
|
}
|
|
}
|
|
|
|
async execute(name, value) {
|
|
return new Promise((resolve, reject) => {
|
|
electron.ipcRenderer.once('execute-complete', (event, data) => {
|
|
if (data.error) {
|
|
reject(new Error(data.error));
|
|
} else {
|
|
resolve(data.value);
|
|
}
|
|
});
|
|
electron.ipcRenderer.send('execute', { name, value });
|
|
});
|
|
}
|
|
|
|
async asset(file) {
|
|
return this.fetch(file, 'utf-8', null);
|
|
}
|
|
|
|
async fetch(file, encoding, basename) {
|
|
return new Promise((resolve, reject) => {
|
|
const dirname = path.dirname(url.fileURLToPath(import.meta.url));
|
|
const root = path.resolve(basename || dirname);
|
|
const pathname = path.resolve(root, file);
|
|
const relative = path.relative(root, pathname);
|
|
if (relative !== '' && (relative.startsWith('..') || path.isAbsolute(relative))) {
|
|
reject(new Error(`The path '${pathname}' is invalid.`));
|
|
return;
|
|
}
|
|
fs.stat(pathname, (err, stat) => {
|
|
if (err && err.code === 'ENOENT') {
|
|
reject(new Error(`The file '${file}' does not exist.`));
|
|
} else if (err) {
|
|
reject(err);
|
|
} else if (!stat.isFile()) {
|
|
reject(new Error(`The path '${file}' is not a file.`));
|
|
} else if (stat && stat.size < 0x40000000) {
|
|
fs.readFile(pathname, encoding, (err, data) => {
|
|
if (err) {
|
|
reject(err);
|
|
} else {
|
|
resolve(encoding ? data : new base.BinaryStream(data));
|
|
}
|
|
});
|
|
} else if (encoding) {
|
|
reject(new Error(`The file '${file}' size (${stat.size.toString()}) for encoding '${encoding}' is greater than 2 GB.`));
|
|
} else {
|
|
const stream = new node.FileStream(pathname, 0, stat.size, stat.mtimeMs);
|
|
resolve(stream);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
openURL(url) {
|
|
electron.shell.openExternal(url);
|
|
}
|
|
|
|
exception(error, fatal) {
|
|
if (this._telemetry && error) {
|
|
try {
|
|
const name = error.name ? `${error.name}: ` : '';
|
|
const message = error.message ? error.message : JSON.stringify(error);
|
|
let context = '';
|
|
let stack = '';
|
|
if (error.stack) {
|
|
const format = (file, line, column) => {
|
|
return `${file.split('\\').join('/').split('/').pop()}:${line}:${column}`;
|
|
};
|
|
const match = error.stack.match(/\n {4}at (.*) \((.*):(\d*):(\d*)\)/);
|
|
if (match) {
|
|
stack = `${match[1]} (${format(match[2], match[3], match[4])})`;
|
|
} else {
|
|
const match = error.stack.match(/\n {4}at (.*):(\d*):(\d*)/);
|
|
if (match) {
|
|
stack = `(${format(match[1], match[2], match[3])})`;
|
|
} else {
|
|
const match = error.stack.match(/.*\n\s*(.*)\s*/);
|
|
if (match) {
|
|
[, stack] = match;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (error.context) {
|
|
context = typeof error.context === 'string' ? error.context : JSON.stringify(error.context);
|
|
}
|
|
this._telemetry.send('exception', {
|
|
app_name: this.type,
|
|
app_version: this.version,
|
|
app_metadata: this.metadata,
|
|
error_name: name,
|
|
error_message: message,
|
|
error_context: context,
|
|
error_stack: stack,
|
|
error_fatal: fatal ? true : false
|
|
});
|
|
} catch {
|
|
// continue regardless of error
|
|
}
|
|
}
|
|
}
|
|
|
|
event(name, params) {
|
|
if (name && params) {
|
|
params.app_name = this.type;
|
|
params.app_version = this.version;
|
|
params.app_metadata = this.metadata;
|
|
this._telemetry.send(name, params);
|
|
}
|
|
}
|
|
|
|
async _context(location) {
|
|
const basename = path.basename(location);
|
|
const stat = fs.statSync(location);
|
|
if (stat.isFile()) {
|
|
const dirname = path.dirname(location);
|
|
const stream = await this.fetch(basename, null, dirname);
|
|
return new desktop.Context(this, dirname, basename, stream);
|
|
} else if (stat.isDirectory()) {
|
|
const entries = new Map();
|
|
const walk = (dir) => {
|
|
for (const item of fs.readdirSync(dir)) {
|
|
const pathname = path.join(dir, item);
|
|
const stat = fs.statSync(pathname);
|
|
if (stat.isDirectory()) {
|
|
walk(pathname);
|
|
} else if (stat.isFile()) {
|
|
const stream = new node.FileStream(pathname, 0, stat.size, stat.mtimeMs);
|
|
const name = pathname.split(path.sep).join(path.posix.sep);
|
|
entries.set(name, stream);
|
|
}
|
|
}
|
|
};
|
|
walk(location);
|
|
return new desktop.Context(this, location, basename, null, entries);
|
|
}
|
|
throw new Error(`Unsupported path stat '${JSON.stringify(stat)}'.`);
|
|
}
|
|
|
|
async _open(location) {
|
|
if (this._files) {
|
|
this._files.push(location);
|
|
return;
|
|
}
|
|
const path = location.path;
|
|
const stat = fs.existsSync(path) ? fs.statSync(path) : null;
|
|
const size = stat && stat.isFile() ? stat.size : 0;
|
|
if (path && this._view.accept(path, size)) {
|
|
this._view.show('welcome spinner');
|
|
let context = null;
|
|
try {
|
|
context = await this._context(path);
|
|
this._telemetry.set('session_engaged', 1);
|
|
} catch (error) {
|
|
await this._view.error(error, 'Error while reading file.');
|
|
this.update({ path: null });
|
|
return;
|
|
}
|
|
try {
|
|
const attachment = await this._view.attach(context);
|
|
if (attachment) {
|
|
this._view.show(null);
|
|
} else {
|
|
const model = await this._view.open(context);
|
|
this._view.show(null);
|
|
const options = { ...this._view.options };
|
|
if (model) {
|
|
options.path = path;
|
|
this._title(location.label);
|
|
} else {
|
|
options.path = path;
|
|
this._title('');
|
|
}
|
|
electron.ipcRenderer.send('update-recents', { path });
|
|
this.update(options);
|
|
}
|
|
} catch (error) {
|
|
const options = { ...this._view.options };
|
|
if (error) {
|
|
await this._view.error(error);
|
|
}
|
|
this.update(options);
|
|
}
|
|
}
|
|
}
|
|
|
|
_request(location, headers, timeout) {
|
|
const window = this.window;
|
|
return new Promise((resolve, reject) => {
|
|
const url = new window.URL(location);
|
|
const protocol = url.protocol === 'https:' ? https : http;
|
|
const options = {};
|
|
options.headers = headers;
|
|
if (timeout) {
|
|
options.timeout = timeout;
|
|
}
|
|
const request = protocol.request(location, options, (response) => {
|
|
if (response.statusCode === 200) {
|
|
let data = '';
|
|
response.on('data', (chunk) => {
|
|
data += chunk;
|
|
});
|
|
response.on('error', (err) => {
|
|
reject(err);
|
|
});
|
|
response.on('end', () => {
|
|
resolve(data);
|
|
});
|
|
} else {
|
|
const error = new Error(`The web request failed with status code '${response.statusCode}'.`);
|
|
error.context = location;
|
|
reject(error);
|
|
}
|
|
});
|
|
request.on("error", (err) => {
|
|
reject(err);
|
|
});
|
|
request.on("timeout", () => {
|
|
request.destroy();
|
|
const error = new Error('The web request timed out.');
|
|
error.context = url;
|
|
reject(error);
|
|
});
|
|
request.end();
|
|
});
|
|
}
|
|
|
|
get(name) {
|
|
try {
|
|
return electron.ipcRenderer.sendSync('get-configuration', { name });
|
|
} catch {
|
|
// continue regardless of error
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
set(name, value) {
|
|
try {
|
|
electron.ipcRenderer.sendSync('set-configuration', { name, value });
|
|
} catch {
|
|
// continue regardless of error
|
|
}
|
|
}
|
|
|
|
delete(name) {
|
|
try {
|
|
electron.ipcRenderer.sendSync('delete-configuration', { name });
|
|
} catch {
|
|
// continue regardless of error
|
|
}
|
|
}
|
|
|
|
_title(label) {
|
|
const element = this._element('titlebar-content-text');
|
|
if (element) {
|
|
element.innerHTML = '';
|
|
if (label) {
|
|
const path = label.split(this._environment.separator || '/');
|
|
for (let i = 0; i < path.length; i++) {
|
|
const span = this.document.createElement('span');
|
|
span.innerHTML = ` ${path[i]} ${i === path.length - 1 ? '' : '<svg class="titlebar-icon" aria-hidden="true"><use xlink:href="#icon-arrow-right"></use></svg>'}`;
|
|
element.appendChild(span);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
_element(id) {
|
|
return this.document.getElementById(id);
|
|
}
|
|
|
|
update(data) {
|
|
electron.ipcRenderer.send('window-update', data);
|
|
}
|
|
|
|
async message(message, alert, action) {
|
|
return new Promise((resolve) => {
|
|
const type = this.document.body.getAttribute('class');
|
|
this._element('message-text').innerText = message || '';
|
|
const button = this._element('message-button');
|
|
if (action) {
|
|
button.style.removeProperty('display');
|
|
button.innerText = action;
|
|
button.onclick = () => {
|
|
button.onclick = null;
|
|
this.document.body.setAttribute('class', type);
|
|
resolve(0);
|
|
};
|
|
} else {
|
|
button.style.display = 'none';
|
|
button.onclick = null;
|
|
}
|
|
if (alert) {
|
|
this.document.body.setAttribute('class', 'alert');
|
|
} else {
|
|
this.document.body.classList.add('notification');
|
|
this.document.body.classList.remove('default');
|
|
}
|
|
if (action) {
|
|
button.focus();
|
|
}
|
|
});
|
|
}
|
|
};
|
|
|
|
desktop.Context = class {
|
|
|
|
constructor(host, folder, identifier, stream, entries) {
|
|
this._host = host;
|
|
this._folder = folder;
|
|
this._identifier = identifier;
|
|
this._stream = stream;
|
|
this._entries = entries || new Map();
|
|
}
|
|
|
|
get identifier() {
|
|
return this._identifier;
|
|
}
|
|
|
|
get stream() {
|
|
return this._stream;
|
|
}
|
|
|
|
get entries() {
|
|
return this._entries;
|
|
}
|
|
|
|
async asset(file) {
|
|
return this._host.asset(file);
|
|
}
|
|
|
|
async fetch(file, encoding, base) {
|
|
return this._host.fetch(file, encoding, base === undefined ? this._folder : base);
|
|
}
|
|
|
|
async require(id) {
|
|
return this._host.require(id);
|
|
}
|
|
|
|
error(error, fatal) {
|
|
this._host.exception(error, fatal);
|
|
}
|
|
};
|
|
|
|
if (typeof window !== 'undefined') {
|
|
window.addEventListener('load', () => {
|
|
const value = new desktop.Host();
|
|
window.__view__ = new view.View(value);
|
|
window.__view__.start();
|
|
});
|
|
}
|