Skip to content

Commit

Permalink
Merge pull request #1 from jonathanschneider/dev-pro7
Browse files Browse the repository at this point in the history
Support of ProPresenter 7
  • Loading branch information
jonathanschneider authored Feb 17, 2021
2 parents c373a45 + 6a3ca61 commit 3cb82e9
Show file tree
Hide file tree
Showing 50 changed files with 3,794 additions and 300 deletions.
20 changes: 8 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,16 @@

As a church in Germany we sing a lot of songs in English and want to display German translations as well. For our international community we want to display English translations for the German songs. Sometimes we want to switch the language. And on the stage display we'd rather only show the lyrics without the translations.

Accomplishing this in ProPresenter 6 can be quite cumbersome so this little suite offers the following functions to manipulate ProPresenter 6 files:
Accomplishing this in ProPresenter can be quite cumbersome so this little suite offers the following functions to manipulate ProPresenter 7 (.pro) and ProPresenter 6 (.pro6) files:
* Copy the top layer text field to the slide notes (to display the slide notes on the stage display)
* Merge two presentations into one (merge two languages)
* Switch the layers of two text fields in one presentation (switch languages)

![alt text](https://github.com/jonathanschneider/ProPresenter-Suite/blob/master/assets/images/ProPresenter-Suite.png "GUI")
![alt text](https://github.com/jonathanschneider/ProPresenter-Suite/blob/master/assets/images/gui.png "GUI")

## Installation

Executables for Windows and macOS are available under [releases](https://github.com/jonathanschneider/ProPresenter-Suite/releases).

For Windows [UnRTF](https://www.gnu.org/software/unrtf/) must be installed manually.
Installers for Windows and macOS are available in under [releases](https://github.com/jonathanschneider/ProPresenter-Suite/releases).

## Usage

Expand All @@ -24,13 +22,7 @@ For Windows [UnRTF](https://www.gnu.org/software/unrtf/) must be installed manua
1. In ProPresenter force save by pressing Ctrl + S or Cmd + S.
1. Choose "Revert" in the dialog.

Unfortunately the GUI is only in German. Pull requests are welcome.

## Development

I have started to upgrade the app for ProPresenter 7. If you can't wait or want to contribute, check out the [dev-pro7](https://github.com/jonathanschneider/ProPresenter-Suite/tree/dev-pro7) branch.

### Getting Started
## Contribution

If you want to contribute, here are some hints to get you started:

Expand All @@ -56,3 +48,7 @@ Package app and create installers:

npm run package-mac
npm run create-installer-mac
## Credits

greyshirtguy for his [proto](https://github.com/greyshirtguy/ProPresenter7-Proto) files to decode ProPresenter 7 files
Binary file removed assets/images/ProPresenter-Suite.png
Binary file not shown.
Binary file added assets/images/gui.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
34 changes: 13 additions & 21 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,63 +7,55 @@
</head>

<body>
<h1>ProPresenter Suite</h1>

<p>Klicke auf "Durchsuchen" und wähle die Datei aus, die bearbeitet werden soll.<br>
Nachdem die Funktion die Datei bearbeitet hat, drücke in ProPresenter "Cmd + S"
und wähle "Revert", um das Dokument neu zu laden.</p>

<hr>
<div class="row">
<h3>Notizen füllen</h3>
<h3>Fill Notes</h3>
<div class="column left">
<p>Füllt die Notizen eines Dokuments mit dem gesungenem Text.</p>
<p>Fills the slide notes with the text from the slides.</p>
</div>
<div class="colum right">
<button id="fillNotesBtn">Durchsuchen</button>
<button id="fillNotesBtn">Browse</button>
</div>
</div>

<hr>
<!-- <div class="row"> -->
<h3>Zwei Sprachen zusammenführen</h3>
<p>Führt zwei Dokumente zu einem zusammen, in dem es die Textelemente aus dem
Dokument mit der Übersetzung in das Dokument mit dem gesungenen Text einfügt.</p>
<h3>Merge Two Languages</h3>
<p>Merges two documents by adding the text elements of the second document (translation) to the first document and saves it as a new document.</p>
<div class="row">
<p>Dokument mit gesungenem Text:</p>
<p>Document with text to sing:</p>
<!-- </div> -->
<!-- <div class="row"> -->
<div class="column left">
<input id="fileLang1Field" type="text" value="" readonly>
</div>
<div class="column right">
<button id="browseLang1">Durchsuchen</button>
<button id="browseLang1">Browse</button>
</div>
</div>
<div class="row">
<p>Dokument mit Übersetzung:</p>
<p>Document with translation:</p>
<!-- </div>
<div class="row"> -->
<div class="column left">
<input id="fileLang2Field" type="text" value="" readonly>
</div>
<div class="column right">
<button id="browseLang2">Durchsuchen</button>
<button id="browseLang2">Browse</button>
</div>
</div>
<div class="row">
<button id="mergeLangBtn" style="margin-top:10px" disabled>Starten</button>
<button id="mergeLangBtn" style="margin-top:10px" disabled>Run</button>
</div>
<!-- </div> -->

<hr>
<div class="row">
<h3>Sprachen tauschen</h3>
<h3>Switch Languages</h3>
<div class="column left">
<p>Tauscht die Sprachen in einem Dokument.</p>
<p>Switches the languages in a document and saves it as a new doucment.</p>
</div>
<div class="column right">
<button id="switchLangBtn">Durchsuchen</button>
<button id="switchLangBtn">Browse</button>
</div>
</div>

Expand Down
173 changes: 173 additions & 0 deletions lib/editPro6.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
const {
ipcRenderer
} = require('electron');
const fs = require('fs');
const xml2js = require('xml2js');
const parser = new xml2js.Parser();
const builder = new xml2js.Builder();
const rtf2text = require('rtf2text');
const path = require('path');

// Functions
async function mergeLang(pathToFile1, pathToFile2) {
// Read files
const file1 = fs.readFileSync(pathToFile1, 'utf8');
const file2 = fs.readFileSync(pathToFile2, 'utf8');

// Parse files
let parsedFiles = await Promise.all([parseFile(file1), parseFile(file2)]);

ipcRenderer.send('log', 'Started merging');

// Check if documents have same number of groups
if (parsedFiles[0].RVPresentationDocument.array[0].RVSlideGrouping.length != parsedFiles[1].RVPresentationDocument.array[0].RVSlideGrouping.length) {
throw "Documents don't have the same number of groups";
}

// Loop through all groups of document 1
parsedFiles[0].RVPresentationDocument.array[0].RVSlideGrouping.forEach(function(currentGroup, indexGroup) {
// Check if slides exist, otherwise abort
if ((!currentGroup.array[0].hasOwnProperty('RVDisplaySlide')) || !parsedFiles[1].RVPresentationDocument.array[0].RVSlideGrouping[indexGroup].array[0].hasOwnProperty('RVDisplaySlide')) {
throw "At least one document doesn't have any slides";
} else if (currentGroup.array[0].RVDisplaySlide.length != parsedFiles[1].RVPresentationDocument.array[0].RVSlideGrouping[indexGroup].array[0].RVDisplaySlide.length) {
throw "Documents don't have the same number of slides";
}

// Loop through all slides
currentGroup.array[0].RVDisplaySlide.forEach(function(currentSlide, indexSlide) {
if (currentSlide.array[1].hasOwnProperty('RVTextElement') &
parsedFiles[1].RVPresentationDocument.array[0].RVSlideGrouping[indexGroup].array[0].RVDisplaySlide[indexSlide].array[1].hasOwnProperty('RVTextElement')) {
// Append text element from document 2
currentSlide.array[1].RVTextElement.push(parsedFiles[1].RVPresentationDocument.array[0].RVSlideGrouping[indexGroup].array[0].RVDisplaySlide[indexSlide].array[1].RVTextElement[0]);
currentSlide.array[1].RVTextElement[0].$.displayName = 'Main';
currentSlide.array[1].RVTextElement[1].$.displayName = 'Translation';
}
});
});
ipcRenderer.send('log', 'Languages merged');

newFile = buildXML(parsedFiles[0]); // Re-build XML

let newPathToFile = pathToFile1.replace('.pro6', ' (merged).pro6');
writeFile(newPathToFile, newFile); // Write file
return 'File saved as "' + path.basename(newPathToFile) + '"';
}

async function switchLang(pathToFile) {
let file = await parseFile(fs.readFileSync(pathToFile, 'utf8')); // Read and parse file

// Loop through all groups
file.RVPresentationDocument.array[0].RVSlideGrouping.forEach(function(currentGroup, indexGroup) {

// Check if slides exist, otherwise throw error
if (!currentGroup.array[0].hasOwnProperty('RVDisplaySlide')) throw "Document doesn't have any slides";

// Loop through all slides
currentGroup.array[0].RVDisplaySlide.forEach(function(currentSlide, indexSlide) {
if (currentSlide.array[1].hasOwnProperty('RVTextElement') &&
currentSlide.array[1].RVTextElement.length == 2) {
// Save string of top text element
let string = currentSlide.array[1].RVTextElement[0].NSString[0];
currentSlide.array[1].RVTextElement[0].NSString[0] = currentSlide.array[1].RVTextElement[1].NSString[0];
currentSlide.array[1].RVTextElement[0].$.displayName = 'Main';
currentSlide.array[1].RVTextElement[1].NSString[0] = string;
currentSlide.array[1].RVTextElement[1].$.displayName = 'Translation';
}
});
});

ipcRenderer.send('log', 'Languages switched');

file = await fillNotes(file); // Fill notes
file = buildXML(file); // Re-build XML

let newPathToFile = pathToFile.replace('.pro6', ' (switched).pro6');
writeFile(newPathToFile, file);
return 'File saved as "' + path.basename(pathToFile) + '"';
}

async function mainFillNotes(pathToFile) {
let file = fs.readFileSync(pathToFile, 'utf8'); // Read file
file = await parseFile(file); // Parse file
file = await fillNotes(file); // Fill notes
file = buildXML(file); // Re-build XML
writeFile(pathToFile, file); // Write file
return 'File saved as "' + path.basename(pathToFile) + '"';
}

// Fill notes
async function fillNotes(file) {
// Loop through all groups
for (const group of file.RVPresentationDocument.array[0].RVSlideGrouping) {

// Check if slides exist, otherwise throw error
if (!group.array[0].hasOwnProperty('RVDisplaySlide')) throw "Document doesn't have any slides";

// Loop through all slides
for (const slide of group.array[0].RVDisplaySlide) {
if (slide.array[1].hasOwnProperty('RVTextElement')) {
// Get text from RTF
let text = await getText(slide.array[1].RVTextElement[0].NSString[0]._);

// Write to notes
if (text != 'Double-click to edit') {
slide.$.notes = text;
//console.log(currentSlide.$.notes);
}
}
}
}

ipcRenderer.send('log', 'Notes filled');
return (file);
}

// Parse file
function parseFile(file) { // Promisify XML parser
return new Promise((resolve, reject) => {
parser.parseString(file, function(err, result) {
if (err) {
ipcRenderer.send('log', 'Error during parsing');
reject(err);
} else {
ipcRenderer.send('log', 'File parsed');
resolve(result);
}
});
});
}

function getText(data) { // Promisify RTF parser
return new Promise((resolve, reject) => {
let buffer = Buffer.from(data, 'base64'); // Convert to buffer

rtf2text.string(buffer, (err, text) => {
if (err) {
refect(err);
} else {
resolve(text);
}
});
});
}

// Build XML file
function buildXML(file) {
ipcRenderer.send('log', 'XML rebuilt');
return builder.buildObject(file);
}

// Write file
function writeFile(pathToFile, file) {
try {
fs.writeFileSync(path.resolve(pathToFile), file);
} catch (error) {
ipcRenderer.send('log', error);
throw "Error writing file";
}
ipcRenderer.send('log', 'File saved as "' + path.basename(pathToFile) + '"');
}

exports.fillNotes = mainFillNotes;
exports.mergeLanguages = mergeLang;
exports.switchLanguages = switchLang;
Loading

0 comments on commit 3cb82e9

Please sign in to comment.