diff --git a/app/src/actions/Publish.ts b/app/src/actions/Publish.ts index be161c1..a7f73cf 100644 --- a/app/src/actions/Publish.ts +++ b/app/src/actions/Publish.ts @@ -17,31 +17,36 @@ export const setTopic = (topic?: string): Action => { export const openFile = () => async (dispatch: Dispatch, getState: () => AppState) => { try { const file = await getFileContent() - dispatch( - setPayload(Base64Message.fromBuffer(file.data).toUnicodeString() - )) + if (file) { + dispatch( + setPayload(Base64Message.fromBuffer(file.data).toUnicodeString() + )) + } } catch (error) { dispatch(showError(error)) } - } type FileParameters = { name: string, data: Buffer } -async function getFileContent(): Promise { +async function getFileContent(): Promise { const rejectReasons = { noFileSelected: 'No file selected', errorReadingFile: 'Error reading file' } - const openDialogReturnValue = await rendererRpc.call(makeOpenDialogRpc(), { + const { canceled, filePaths } = await rendererRpc.call(makeOpenDialogRpc(), { properties: ['openFile'], securityScopedBookmarks: true, }) - const selectedFile = openDialogReturnValue.filePaths && openDialogReturnValue.filePaths[0] + if (canceled) { + return + } + + const selectedFile = filePaths[0] if (!selectedFile) { throw rejectReasons.noFileSelected } diff --git a/app/src/components/Sidebar/ValueRenderer/ValuePanel.tsx b/app/src/components/Sidebar/ValueRenderer/ValuePanel.tsx index 61c5b2f..fc07a51 100644 --- a/app/src/components/Sidebar/ValueRenderer/ValuePanel.tsx +++ b/app/src/components/Sidebar/ValueRenderer/ValuePanel.tsx @@ -1,6 +1,7 @@ import * as q from '../../../../../backend/src/Model' import ActionButtons from './ActionButtons' import Copy from '../../helper/Copy' +import Save from '../../helper/Save' import DateFormatter from '../../helper/DateFormatter' import MessageHistory from './MessageHistory' import Panel from '../Panel' @@ -59,6 +60,12 @@ function ValuePanel(props: Props) { return node?.message && decodeMessage(node.message)?.message?.toUnicodeString() }, [node, decodeMessage]) + const getBuffer = () => { + if (node?.message && node.message.payload) { + return node.message.payload.toBuffer() + } + } + function messageMetaInfo() { if (!props.node || !props.node.message) { return null @@ -93,10 +100,13 @@ function ValuePanel(props: Props) { const [value] = node && node.message && node.message.payload ? node.message.payload?.format(node.type) : [null, undefined] const copyValue = value ? : null + const saveValue = value ? : null return ( - Value {copyValue} + + Value {copyValue} {saveValue} + {renderViewOptions()}
diff --git a/app/src/components/helper/Save.tsx b/app/src/components/helper/Save.tsx new file mode 100644 index 0000000..d085786 --- /dev/null +++ b/app/src/components/helper/Save.tsx @@ -0,0 +1,85 @@ +import * as React from 'react' +import { connect } from 'react-redux' +import Check from '@material-ui/icons/Check' +import CustomIconButton from './CustomIconButton' +import { promises as fsPromise } from 'fs' +import { SaveAlt } from '@material-ui/icons' +import { bindActionCreators } from 'redux' +import { rendererRpc } from '../../../../events' +import { makeSaveDialogRpc } from '../../../../events/OpenDialogRequest' + +import { globalActions } from '../../actions' + +export async function saveToFile(buffer: Buffer | string): Promise { + const rejectReasons = { + errorWritingFile: 'Error writing file', + } + + const { canceled, filePath } = await rendererRpc.call(makeSaveDialogRpc(), { + securityScopedBookmarks: true, + }) + + if (!canceled && filePath !== undefined) { + try { + await fsPromise.writeFile(filePath, buffer) + return filePath + } catch (error) { + throw rejectReasons.errorWritingFile + } + } +} + +interface Props { + getBuffer: () => Buffer | undefined + actions: { + global: typeof globalActions + } +} + +interface State { + didSave: boolean +} + +class Save extends React.PureComponent { + constructor(props: Props) { + super(props) + this.state = { didSave: false } + } + + private handleClick = async (event: React.MouseEvent) => { + event.stopPropagation() + const buffer = this.props.getBuffer() + if (buffer !== undefined) { + const filename = await saveToFile(buffer) + this.props.actions.global.showNotification(`Saved to ${filename}`) + this.setState({ didSave: true }) + setTimeout(() => { + this.setState({ didSave: false }) + }, 1500) + } + } + + public render() { + const icon = !this.state.didSave ? ( + + ) : ( + + ) + + return ( + +
{icon}
+
+ ) + } +} + +const mapDispatchToProps = (dispatch: any) => { + return { + actions: { + global: bindActionCreators(globalActions, dispatch), + }, + } +} + +export default connect(undefined, mapDispatchToProps)(Save) diff --git a/events/OpenDialogRequest.ts b/events/OpenDialogRequest.ts index 6ef1254..fa79934 100644 --- a/events/OpenDialogRequest.ts +++ b/events/OpenDialogRequest.ts @@ -1,4 +1,4 @@ -import { OpenDialogOptions, OpenDialogReturnValue } from 'electron' +import { OpenDialogOptions, OpenDialogReturnValue, SaveDialogOptions, SaveDialogReturnValue } from 'electron' import { RpcEvent } from './EventSystem/Rpc' export function makeOpenDialogRpc(): RpcEvent { @@ -6,3 +6,9 @@ export function makeOpenDialogRpc(): RpcEvent { + return { + topic: 'saveDialog', + } +} \ No newline at end of file diff --git a/src/electron.ts b/src/electron.ts index 7b4cf77..6acd034 100644 --- a/src/electron.ts +++ b/src/electron.ts @@ -10,7 +10,7 @@ import buildOptions from './buildOptions' import { waitForDevServer, isDev, runningUiTestOnCi, loadDevTools } from './development' import { shouldAutoUpdate, handleAutoUpdate } from './autoUpdater' import { registerCrashReporter } from './registerCrashReporter' -import { makeOpenDialogRpc } from '../events/OpenDialogRequest' +import { makeOpenDialogRpc, makeSaveDialogRpc } from '../events/OpenDialogRequest' import { backendRpc, getAppVersion } from '../events' registerCrashReporter() @@ -25,6 +25,11 @@ app.whenReady().then(() => { backendRpc.on(makeOpenDialogRpc(), async request => { return dialog.showOpenDialog(BrowserWindow.getFocusedWindow() ?? BrowserWindow.getAllWindows()[0], request) }) + + backendRpc.on(makeSaveDialogRpc(), async request => { + return dialog.showSaveDialog(BrowserWindow.getFocusedWindow() ?? BrowserWindow.getAllWindows()[0], request) + }) + backendRpc.on(getAppVersion, async () => app.getVersion()) })