import { dialog, getCurrentWindow } from "@electron/remote";
import React from "react";
import {
observable,
computed,
action,
runInAction,
makeObservable
} from "mobx";
import { observer } from "mobx-react";
import { clipboard, nativeImage, SaveDialogOptions } from "electron";
import {
formatTransferSpeed,
formatDateTimeLong
} from "eez-studio-shared/util";
import {
writeBinaryData,
getFileName,
getFileNameWithoutExtension,
getTempDirPath,
fileExists
} from "eez-studio-shared/util-electron";
import { formatBytes } from "eez-studio-shared/formatBytes";
import * as notification from "eez-studio-ui/notification";
import type { IStore } from "eez-studio-shared/store";
import { SAMPLING_RATE_UNIT } from "eez-studio-shared/units";
import { Toolbar } from "eez-studio-ui/toolbar";
import { IconAction, TextAction } from "eez-studio-ui/action";
import { Icon } from "eez-studio-ui/icon";
import pdfToPng from "pdf-services/pdf-to-png";
import { IActivityLogEntry } from "instrument/window/history/activity-log";
import { FileState } from "instrument/connection/file-state";
import type { IAppStore } from "instrument/window/history/history";
import { HistoryItem } from "instrument/window/history/item";
import { HistoryItemPreview } from "instrument/window/history/item-preview";
import { HistoryItemInstrumentInfo } from "../HistoryItemInstrumentInfo";
import { PLOTTER_ICON } from "project-editor/ui-components/icons";
////////////////////////////////////////////////////////////////////////////////
const ImagePreview = observer(
class ImagePreview extends React.Component<{
src: string;
}> {
zoom: boolean = false;
constructor(props: { src: string }) {
super(props);
makeObservable(this, {
zoom: observable,
toggleZoom: action.bound
});
}
toggleZoom() {
this.zoom = !this.zoom;
}
render() {
return (
);
}
}
);
////////////////////////////////////////////////////////////////////////////////
// TODO create temp dir in main process
let getPdfTempDirPathPromise = getTempDirPath();
const PdfPreview = observer(
class PdfPreview extends React.Component<{
historyItem: HistoryItem;
}> {
thumbnail: string;
url: string;
zoom: boolean = false;
constructor(props: any) {
super(props);
makeObservable(this, {
thumbnail: observable,
url: observable,
zoom: observable,
toggleZoom: action.bound
});
}
toggleZoom() {
this.zoom = !this.zoom;
}
update() {
if (this.zoom) {
if (!this.url) {
(async () => {
const [tempDirPath] = await getPdfTempDirPathPromise;
const tempFilePath =
tempDirPath +
"/" +
this.props.historyItem.id +
".pdf";
let exists = await fileExists(tempFilePath);
if (!exists) {
await writeBinaryData(
tempFilePath,
this.props.historyItem.data
);
}
return new URL(`file:///${tempFilePath}`).href;
})().then(
action((url: string) => {
this.url = url;
})
);
}
} else {
if (!this.thumbnail) {
pdfToPng(this.props.historyItem.data)
.then(result => {
runInAction(() => (this.thumbnail = result));
})
.catch(error => {
console.error("PDF to PNG error", error);
});
}
}
}
componentDidMount() {
this.update();
}
componentDidUpdate() {
this.update();
}
render() {
let content;
if (this.zoom) {
content = this.url && (
);
} else {
content = (
);
}
return (
{content}
);
}
}
);
////////////////////////////////////////////////////////////////////////////////
export const FileHistoryItemComponent = observer(
class FileHistoryItemComponent extends React.Component<
{
appStore: IAppStore;
historyItem: FileHistoryItem;
},
{}
> {
setVisibleTimeoutId: any;
onAbortFileTransfer = () => {
this.props.appStore.instrument.connection.abortLongOperation();
};
onSave = async () => {
let filters = [];
let fileExtension = this.props.historyItem.fileExtension;
if (fileExtension) {
fileExtension = fileExtension.toLowerCase();
filters.push({
name: fileExtension.toUpperCase() + " Files",
extensions: [fileExtension]
});
}
filters.push({ name: "All Files", extensions: ["*"] });
let options: SaveDialogOptions = { filters };
if (this.props.historyItem.sourceFilePath) {
options.defaultPath = getFileName(
this.props.historyItem.sourceFilePath
);
}
const result = await dialog.showSaveDialog(
getCurrentWindow(),
options
);
let filePath = result.filePath;
if (filePath) {
if (
fileExtension &&
!filePath.toLowerCase().endsWith(fileExtension)
) {
filePath += "." + fileExtension;
}
await writeBinaryData(filePath, this.props.historyItem.data);
notification.success(`Saved as "${filePath}"`);
}
};
onSaveAsCsvInProgress = false;
onSaveAsCsv = async () => {
const convertToCsv = this.props.historyItem.convertToCsv;
if (!convertToCsv) {
return;
}
if (this.onSaveAsCsvInProgress) {
return;
}
runInAction(() => (this.onSaveAsCsvInProgress = true));
let data = await convertToCsv();
if (!data) {
notification.error(`Failed to convert to CSV!`);
runInAction(() => (this.onSaveAsCsvInProgress = false));
return;
}
let options: SaveDialogOptions = {
filters: [
{
name: "CSV Files",
extensions: ["csv"]
},
{ name: "All Files", extensions: ["*"] }
]
};
if (this.props.historyItem.sourceFilePath) {
options.defaultPath = getFileNameWithoutExtension(
this.props.historyItem.sourceFilePath
);
}
const result = await dialog.showSaveDialog(
getCurrentWindow(),
options
);
let filePath = result.filePath;
if (filePath) {
if (!filePath.toLowerCase().endsWith("csv")) {
filePath += ".csv";
}
try {
await writeBinaryData(filePath, data);
notification.success(`Saved as "${filePath}"`);
} catch (err) {
console.error(err);
notification.error(err.toString());
}
}
runInAction(() => (this.onSaveAsCsvInProgress = false));
};
onCopy = () => {
if (this.props.historyItem.isImage) {
let image = nativeImage.createFromBuffer(
Buffer.from(this.props.historyItem.data, "binary")
);
clipboard.writeImage(image);
notification.success("Image copied to the clipboard");
} else if (this.props.historyItem.isText) {
clipboard.writeText(this.props.historyItem.data.toString());
notification.success("Text copied to the clipboard");
}
};
constructor(props: {
appStore: IAppStore;
historyItem: FileHistoryItem;
}) {
super(props);
makeObservable(this, {
onSaveAsCsvInProgress: observable
});
}
getDirectionInfo() {
if (this.props.historyItem.direction === "upload") {
return "Sending file ...";
} else if (this.props.historyItem.direction === "download") {
return "Receiving file ...";
} else if (this.props.historyItem.direction === "plotter") {
return "Waiting for data ...";
} else {
return "Attaching file ...";
}
}
render() {
let body;
if (
!this.props.historyItem.state ||
this.props.historyItem.state === "init" ||
this.props.historyItem.state === "upload-filesize" ||
this.props.historyItem.state === "upload-start"
) {
body =
{this.getDirectionInfo()}
;
} else if (this.props.historyItem.state === "progress") {
let percent = this.props.historyItem.expectedDataLength
? Math.floor(
(100 * this.props.historyItem.dataLength) /
this.props.historyItem.expectedDataLength
)
: 0;
let progress;
if (this.props.historyItem.expectedDataLength) {
let transferSpeed = formatTransferSpeed(
this.props.historyItem.transferSpeed
);
progress = `${percent}% (${this.props.historyItem.dataLength} of ${this.props.historyItem.expectedDataLength}) ${transferSpeed}`;
} else {
progress = `${this.props.historyItem.dataLength} bytes`;
}
body = (
{this.getDirectionInfo()}
{progress}
{this.props.historyItem.direction === "upload" && (
)}
);
} else if (
this.props.historyItem.state === "error" ||
this.props.historyItem.state === "upload-error"
) {
body = (
Failed!
{this.props.historyItem.error}
);
} else if (this.props.historyItem.state === "timeout") {
body = (
);
} else if (this.props.historyItem.state === "abort") {
body = (
);
} else if (this.props.historyItem.transferSucceeded) {
let preview: JSX.Element | null = null;
let actions;
if (this.props.historyItem.fileType) {
preview = this.props.historyItem.getPreviewElement(
this.props.appStore
);
if (this.props.historyItem.state !== "live") {
actions = (
{this.props.historyItem.direction !=
"plotter" && (
)}
{this.props.historyItem.convertToCsv && (
)}
{(this.props.historyItem.isImage ||
this.props.historyItem.isText) && (
)}
{this.props.historyItem.renderAddNoteAction(
this.props.appStore
)}
{this.props.historyItem.renderAddMediaNoteAction(
this.props.appStore
)}
);
}
}
body = (
{this.props.historyItem.sourceFilePath && (
{this.props.historyItem.sourceFilePath}
{this.props.historyItem
.destinationFilePath && (
{
this.props.historyItem
.destinationFilePath
}
)}
)}
{(this.props.historyItem.fileTypeAsDisplayString
? this.props.historyItem
.fileTypeAsDisplayString + ", "
: "") +
formatBytes(
this.props.historyItem.fileLength
)}
{this.props.historyItem.description}
{preview}
{actions}
{this.props.historyItem.renderNote(this.props.appStore)}
{this.props.historyItem.renderMediaNote(
this.props.appStore
)}
);
} else {
body = this.props.historyItem.state;
}
return (
{formatDateTimeLong(
this.props.historyItem.date
)}
{this.props.historyItem.getSourceDescriptionElement(
this.props.appStore
)}
{body}
);
}
}
);
export class FileHistoryItem extends HistoryItem {
constructor(store: IStore, activityLogEntry: IActivityLogEntry) {
super(store, activityLogEntry);
makeObservable(this, {
fileState: computed,
fileLength: computed,
fileType: computed,
fileTypeAsDisplayString: computed,
sourceFilePath: computed,
destinationFilePath: computed,
isImage: computed,
isPdf: computed,
isText: computed,
direction: computed,
state: computed,
transferSucceeded: computed,
expectedDataLength: computed,
dataLength: computed,
transferSpeed: computed,
error: computed,
description: computed
});
}
get info() {
return (
{this.fileTypeAsDisplayString && (
{this.fileTypeAsDisplayString + " file"}
)}
{this.renderNoteInfo()}
);
}
getListItemElement(appStore: IAppStore): React.ReactNode {
return (
);
}
getPreviewElement(appStore: IAppStore): JSX.Element | null {
if (!this.data) {
return null;
}
if (this.isImage) {
let imageData =
"data:image/" +
this.fileExtension +
";base64," +
Buffer.from(this.data, "binary").toString("base64");
return ;
} else if (this.isPdf) {
return ;
}
return null;
}
get fileState() {
return this.messageObject as FileState;
}
get fileLength() {
if (typeof this.fileState.dataLength === "number") {
return this.fileState.dataLength;
}
if (this.data) {
return this.data.length;
}
return 0;
}
get fileType() {
return this.fileState.fileType;
}
get fileTypeAsDisplayString() {
if (!this.fileType) {
return "unknown";
}
if (typeof this.fileType === "string") {
return this.fileType;
}
if (this.direction === "plotter") {
return undefined;
}
return this.fileType.mime;
}
get fileExtension() {
let fileExtension;
if (typeof this.fileType === "string") {
if (this.fileType === "image") {
fileExtension = "png";
} else if (this.fileType.startsWith("image/")) {
fileExtension = this.fileType.slice("image/".length);
} else if (this.fileType === "CSV") {
fileExtension = "csv";
}
} else {
fileExtension = this.fileType.ext;
}
return fileExtension;
}
get sourceFilePath() {
return this.fileState.sourceFilePath;
}
get destinationFilePath() {
return this.fileState.destinationFilePath;
}
get isImage() {
if (!this.fileType) {
return false;
}
if (typeof this.fileType === "string") {
return this.fileType.startsWith("image");
}
return this.fileType.mime.startsWith("image");
}
get isPdf() {
if (!this.fileType) {
return false;
}
if (typeof this.fileType === "string") {
return this.fileType.startsWith("application/pdf");
}
return this.fileType.mime.startsWith("application/pdf");
}
get isText() {
if (!this.fileType) {
return false;
}
if (typeof this.fileType === "string") {
return this.fileType === "CSV";
}
return this.fileType.mime.startsWith("text");
}
get direction() {
if (this.type === "instrument/file-download") {
return "download";
}
if (this.type === "instrument/file-upload") {
return "upload";
}
if (this.type === "instrument/plotter") {
return "plotter";
}
return "attachment";
}
get state() {
return this.fileState.state;
}
get transferSucceeded() {
return (
this.state === "success" ||
this.state === "upload-finish" ||
this.state == "live"
);
}
get expectedDataLength() {
return this.fileState.expectedDataLength;
}
get dataLength() {
return this.fileState.dataLength;
}
get transferSpeed() {
return this.fileState.transferSpeed;
}
get error() {
return this.fileState.error;
}
get description() {
if (!this.fileState.description) {
return null;
}
let index = this.fileState.description.indexOf(", Preamble:");
if (index === -1) {
return {this.fileState.description}
;
}
let firstRow = this.fileState.description.slice(0, index);
try {
// add unit to sample rate
firstRow = firstRow.replace(
/(.*Sampling rate: )(.*)/,
(match, a, b) => {
return (
a +
SAMPLING_RATE_UNIT.formatValue(parseFloat(b), 0, " ")
);
}
);
} catch (err) {
console.error(err);
}
let secondRow = this.fileState.description
.slice(index + 2)
.split(",")
.join(", ");
return (
{firstRow}
{secondRow}
);
}
convertToCsv?: () => Promise | undefined =
undefined;
}