Skip to content

Commit

Permalink
Merge pull request #14 from karlitos/electron-app
Browse files Browse the repository at this point in the history
Electron app
  • Loading branch information
karlitos authored Sep 23, 2020
2 parents 9b01782 + 1d25f02 commit d210899
Show file tree
Hide file tree
Showing 9 changed files with 138 additions and 42 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ The App allows to download the jsonresume-themes from NPM automatically and use
* [ ] Possibility to delete downloaded themes
* [ ] Support for local themes
* [x] Export of the rendered resume in ALL formats
* [ ] Selecting formats for export
* [x] Selecting formats for export
* [ ] More mature GUI, improved styling

### To do
Expand Down
4 changes: 4 additions & 0 deletions app/bootstrap-override.css
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,7 @@
.form-control {
height: 36px;
}

.btn .caret, .dropdown-menu > li > .checkbox {
margin-left: 1em;
}
86 changes: 49 additions & 37 deletions app/main/ipc-event-listeners.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,10 +66,11 @@ export const saveCvListener = async (evt: IpcMainInvokeEvent, cvData: Record<str
* @param evt {IpcMainInvokeEvent} The invoke event
* @param cvData {Object} The structured CV data
* @param theme {IThemeEntry} The selected theme which should be used for creating HTML markup
* @param exportCvAfterProcessing {boolean} Whether ot
* @param selectedFormatsForExport {Object} The object with the selected formats for export
* @param exportCvAfterProcessing {boolean} Whether or not should the CV be exported after processing
*/
export const processCvListener = async (evt: IpcMainInvokeEvent, cvData: Record<string, any>, theme: IThemeEntry,
exportCvAfterProcessing: boolean) => {
selectedFormatsForExport: Record<string, any>, exportCvAfterProcessing: boolean) => {
try {
// IDEA: run the theme render fn in sandbox - https://www.npmjs.com/package/vm2
const markup = await createMarkup(cvData, await getLocalTheme(theme));
Expand All @@ -90,44 +91,55 @@ export const processCvListener = async (evt: IpcMainInvokeEvent, cvData: Record<
const parsedFilePath = path.parse(saveDialogReturnVal.filePath);

// PDF export
const pdfData = await BrowserView.fromId(2).webContents.printToPDF({pageSize: 'A4', landscape: false});
await fs.promises.writeFile(`${path.resolve(parsedFilePath.dir, parsedFilePath.name)}.pdf`, pdfData);
logSuccess('The Resume in PDF format has been saved!');

const pageRect = await BrowserView.fromId(2).webContents.executeJavaScript(
`(() => { return {x: 0, y: 0, width: document.body.offsetWidth, height: document.body.offsetHeight}})()`);

OFFSCREEN_RENDERER = new BrowserWindow({
enableLargerThanScreen: true,
show: false,
webPreferences: {
offscreen: true,
nodeIntegration: false, // is default value after Electron v5
contextIsolation: true, // protect against prototype pollution
enableRemoteModule: false, // turn off remote
if (selectedFormatsForExport.pdf) {
const pdfData = await BrowserView.fromId(2).webContents.printToPDF({pageSize: 'A4', landscape: false});
await fs.promises.writeFile(`${path.resolve(parsedFilePath.dir, parsedFilePath.name)}.pdf`, pdfData);
logSuccess('The Resume in PDF format has been saved!');
}

if (selectedFormatsForExport.png) {
const pageRect = await BrowserView.fromId(2).webContents.executeJavaScript(
`(() => { return {x: 0, y: 0, width: document.body.offsetWidth, height: document.body.offsetHeight}})()`);

OFFSCREEN_RENDERER = new BrowserWindow({
enableLargerThanScreen: true,
show: false,
webPreferences: {
offscreen: true,
nodeIntegration: false, // is default value after Electron v5
contextIsolation: true, // protect against prototype pollution
enableRemoteModule: false, // turn off remote
}
});

const timeout = setTimeout(() => { throw 'Exporting of the resume timed out!'}, CV_EXPORT_TIMEOUT);

// Export the 'painted' image as screenshot
OFFSCREEN_RENDERER.webContents.on('paint', async (evt, dirtyRect, image) => {
await fs.promises.writeFile(`${path.resolve(parsedFilePath.dir, parsedFilePath.name)}.png`, image.toPNG());
clearTimeout(timeout);
logSuccess('The Resume in PNG format has been saved!');
OFFSCREEN_RENDERER.destroy();
});

// PNG export
OFFSCREEN_RENDERER.setContentSize(pageRect.width, pageRect.height);
await OFFSCREEN_RENDERER.loadURL(`data:text/html;charset=utf-8,${encodeURIComponent(markup)}`);
await OFFSCREEN_RENDERER.webContents.insertCSS('html, body {overflow: hidden}');
// const screenshot = await OFFSCREEN_RENDERER.webContents.capturePage(pageRect);
// await fs.promises.writeFile(`${saveDialogReturnVal.filePath}.png`, screenshot.toPNG());
// logSuccess('The Resume in PNG format has been saved!');
}

const remainingOutputFormats = Object.keys(selectedFormatsForExport).reduce((formats, currentFormat): Array<string> => {
if (currentFormat !== 'pdf' && currentFormat !== 'png' && selectedFormatsForExport[currentFormat]) {
formats.push(currentFormat);
}
});

const timeout = setTimeout(() => { throw 'Exporting of the resume timed out!'}, CV_EXPORT_TIMEOUT);

// Export the 'painted' image as screenshot
OFFSCREEN_RENDERER.webContents.on('paint', async (evt, dirtyRect, image) => {
await fs.promises.writeFile(`${path.resolve(parsedFilePath.dir, parsedFilePath.name)}.png`, image.toPNG());
clearTimeout(timeout);
logSuccess('The Resume in PNG format has been saved!');
OFFSCREEN_RENDERER.destroy();
});

// PNG export
OFFSCREEN_RENDERER.setContentSize(pageRect.width, pageRect.height);
await OFFSCREEN_RENDERER.loadURL(`data:text/html;charset=utf-8,${encodeURIComponent(markup)}`);
await OFFSCREEN_RENDERER.webContents.insertCSS('html, body {overflow: hidden}');
// const screenshot = await OFFSCREEN_RENDERER.webContents.capturePage(pageRect);
// await fs.promises.writeFile(`${saveDialogReturnVal.filePath}.png`, screenshot.toPNG());
// logSuccess('The Resume in PNG format has been saved!');
return formats;
}, []);

// HTML & DOCX export
await exportToMultipleFormats(markup, parsedFilePath.name, parsedFilePath.dir, await getLocalTheme(theme), 'A4', [ 'docx', 'html'])
await exportToMultipleFormats(markup, parsedFilePath.name, parsedFilePath.dir, await getLocalTheme(theme), 'A4', remainingOutputFormats);
}
}
return Promise.resolve(markup);
Expand Down
69 changes: 68 additions & 1 deletion app/renderer/components/App/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ export default function App()
if (themeListFetchingError) {
setNotifications([...notifications, themeListFetchingError])
}
const [selectedFormatsForExport, setSelectedFormatsForExport] = useState({
pdf: true,
png: false,
html: false,
docx: false,
} as Record<string, any>);
const [fetchingThemeInProgress, setFetchingThemeInProgress] = useState(false);
const [processingThemeInProgress, setProcessingThemeInProgress] = useState(false);
const [exportCvAfterProcessing, setExportCvAfterProcessing] = useState(false);
Expand Down Expand Up @@ -52,6 +58,7 @@ export default function App()

/**
* Form-data-change handler making react-jsonschema-form controlled component.
* @param changeEvent {IChangeEvent} The rjsf-form change event
*/
const handleFormDataChange = (changeEvent: IChangeEvent) => {
setCvData(changeEvent.formData);
Expand All @@ -64,6 +71,23 @@ export default function App()
cvForm.current.submit();
};

/**
* Checkbox change-handler updating the state of the selected output formats
* @param evt {HTMLInputElement} The change event of the checkbox
*/
const handleFormatsForExportChange = (evt: ChangeEvent<HTMLInputElement>) => {
// ignore the setter in case of undefined or other monkey business
if (typeof selectedFormatsForExport[evt.target.name] === "boolean") {
const newSelectedFormatsForExportState = { ...selectedFormatsForExport, [evt.target.name]: evt.target.checked };
// Do not allow unselecting all formats
if (Object.keys(newSelectedFormatsForExportState).every((k) => !newSelectedFormatsForExportState[k])) {
setNotifications([...notifications, {type: 'warning', text: 'At least one format must be selected for export!'}])
return;
}
setSelectedFormatsForExport({...selectedFormatsForExport, [evt.target.name]: evt.target.checked})
}
};

/**
* Click-handler for the Export-cv-button which triggers the CV export.
*/
Expand Down Expand Up @@ -124,7 +148,7 @@ export default function App()
const selectedTheme = themeList[parseInt(themeSelector.current.value)];
// set the state of processing-state-in-progress to true
setProcessingThemeInProgress(true);
window.api.invoke(VALID_INVOKE_CHANNELS['process-cv'], submitEvent.formData, selectedTheme, exportCvAfterProcessing )
window.api.invoke(VALID_INVOKE_CHANNELS['process-cv'], submitEvent.formData, selectedTheme, selectedFormatsForExport, exportCvAfterProcessing )
.then((markup: string) => {
// TODO: success notification
}).catch((err: PromiseRejectionEvent) => {
Expand Down Expand Up @@ -165,6 +189,49 @@ export default function App()
disabled={fetchingThemeInProgress || processingThemeInProgress}>
Export CV
</button>
<div className="btn-group pull-right" role="group">
<button type="button" className="btn btn-default dropdown-toggle" data-toggle="dropdown"
aria-haspopup="true" aria-expanded="false">
Select output formats
<span className="caret"></span>
</button>
<ul className="dropdown-menu">
<li>
<div className="checkbox">
<label>
<input type="checkbox" name="pdf" onChange={handleFormatsForExportChange}
checked={selectedFormatsForExport['pdf']}/>
Document PDF
</label>
</div>
</li>
<li>
<div className="checkbox">
<label>
<input type="checkbox" name="png" onChange={handleFormatsForExportChange}
checked={selectedFormatsForExport['png']}/>
Image PNG
</label>
</div>
</li><li>
<div className="checkbox">
<label>
<input type="checkbox" name="html" onChange={handleFormatsForExportChange}
checked={selectedFormatsForExport['html']}/>
Website HTML
</label>
</div>
</li><li>
<div className="checkbox">
<label>
<input type="checkbox" name="docx" onChange={handleFormatsForExportChange}
checked={selectedFormatsForExport['docx']}/>
Word DOCX
</label>
</div>
</li>
</ul>
</div>
<button className='btn pull-right'
onClick={handleSaveCvDataClick}
disabled={saveCvDataInProgress}>
Expand Down
1 change: 1 addition & 0 deletions app/renderer/renderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@

import '../index.css?raw';
// Don't change the order, override has to come after the main bootstrap
import 'bootstrap3/dist/js/bootstrap';
import 'bootstrap3/dist/css/bootstrap.min.css?raw';
import '../bootstrap-override.css?raw';
import 'ez-space-css/css/ez-space.css?raw';
Expand Down
5 changes: 5 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
},
"name": "kiss-my-resume",
"productName": "kiss-my-resume",
"version": "1.0.0-beta.1",
"version": "1.1.0-beta.1",
"description": "KissMyResume is a Swiss Army knife for resumes and CVs build with the KISS principle in mind.",
"bundleDependencies": [],
"config": {
Expand Down Expand Up @@ -102,6 +102,7 @@
"handlebars": "^4.7.6",
"html-docx-js": "^0.3.1",
"is-url": "^1.2.4",
"jquery": "1.9.1 - 3",
"json2yaml": "^1.1.0",
"jsonresume-theme-flat": "^0.3.7",
"jsonresume-theme-mocha-responsive": "^1.0.0",
Expand Down
8 changes: 7 additions & 1 deletion webpack.plugins.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
const OptimizeCssnanoPlugin = require('@intervolga/optimize-cssnano-plugin');
const CopyPlugin = require('copy-webpack-plugin');
const { DefinePlugin } = require('webpack');
const { DefinePlugin, ProvidePlugin } = require('webpack');

const path = require('path');
// const puppeteer = require('puppeteer');
Expand All @@ -17,6 +17,12 @@ const path = require('path');
module.exports = {
forkTsCheckerWebpackPlugin: new ForkTsCheckerWebpackPlugin(),
optimizeCssnanoPlugin: new OptimizeCssnanoPlugin({}),
provideJqueryPlugin: new ProvidePlugin({
$: 'jquery',
jquery: 'jquery',
'window.jQuery': 'jquery',
jQuery:'jquery'
}),
copyPlugin: new CopyPlugin({
patterns: [
// This fix missing assets for html-docx-js
Expand Down
2 changes: 1 addition & 1 deletion webpack.renderer.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ module.exports = {
output: {
publicPath: './../',
},
plugins: [plugins.forkTsCheckerWebpackPlugin, plugins.optimizeCssnanoPlugin],
plugins: [plugins.forkTsCheckerWebpackPlugin, plugins.optimizeCssnanoPlugin, plugins.provideJqueryPlugin],
resolve: {
extensions: ['.js', '.ts', '.jsx', '.tsx', '.css']
},
Expand Down

0 comments on commit d210899

Please sign in to comment.