Skip to content

Commit

Permalink
➕ Merge pull request #217 from devmount/export-openlp-service-list
Browse files Browse the repository at this point in the history
OpenLP service list export for setlists
  • Loading branch information
devmount authored Jun 5, 2024
2 parents 965bd60 + 33eb67d commit 6f118d6
Show file tree
Hide file tree
Showing 7 changed files with 143 additions and 36 deletions.
11 changes: 11 additions & 0 deletions frontend/package-lock.json

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

1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"@vuepic/vue-datepicker": "^8.1.1",
"@vueuse/core": "^10.2.1",
"@vueuse/math": "^10.2.1",
"@zip.js/zip.js": "^2.7.45",
"chart.js": "^4.0.1",
"date-fns": "^3.2.0",
"firebase": "^10.0.0",
Expand Down
1 change: 1 addition & 0 deletions frontend/src/locales/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
"filetypeSng": "SongBeamer-Datei [.sng]",
"filetypeTxt": "Text-Datei [.txt]",
"filetypeXml": "OpenLyrics-Datei [.xml]",
"filetypeOsz": "OpenLP Service [.osz]",
"formatMarkdown": "Als Markdown formatiert",
"formatPlain": "Reines Textformat",
"formatSlack": "Für Slack formatiert",
Expand Down
1 change: 1 addition & 0 deletions frontend/src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
"filetypeSng": "SongBeamer file [.sng]",
"filetypeTxt": "Plain text file [.txt]",
"filetypeXml": "OpenLyrics file [.xml]",
"filetypeOsz": "OpenLP Service [.osz]",
"formatMarkdown": "Markdown formatted",
"formatPlain": "As plain text",
"formatSlack": "Slack formatted",
Expand Down
38 changes: 36 additions & 2 deletions frontend/src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -187,9 +187,9 @@ const parsedContent = (content, tuning, showChords, twoColumns) => {
};

// file download
const download = (data, filename) => {
const download = (data, filename, isBlob = false) => {
var a = document.createElement('a');
var file = new Blob([data], { type:'text/plain;charset=UTF-8' });
var file = isBlob ? data : new Blob([data], { type:'text/plain;charset=UTF-8' });
// IE10+
if (window.navigator.msSaveOrOpenBlob) {
window.navigator.msSaveOrOpenBlob(file, filename);
Expand Down Expand Up @@ -330,6 +330,39 @@ const browserPrefersDark = () => {
// trigger mailto
const mailto = (address) => window.location.href = 'mailto:' + address;

// build OpenLyrics XML for given song
const openLyricsXML = (song, version, locales = [], allTags = null) => {
const timestamp = (new Date()).toISOString().slice(0, -5);
const title = `<title>${song.title}</title>`;
const subtitle = song.subtitle ? `<title>${song.subtitle}</title>` : '';
const year = song.year ? `<released>${song.year}</released>` : '';
const copyright = song.year || song.publisher
? '<copyright>' + song.year + ' ' + song.publisher.replace(/(?:\r\n|\r|\n)/g, '; ') + '</copyright>'
: '';
const ccli = song.ccli ? `<ccliNo>${song.ccli}</ccliNo>` : '';
const authors = song.authors
? '<authors>' + song.authors.split('|').map(a => `<author>${a.trim()}</author>`).join('') + '</authors>'
: '';
const tags = song.tags && locales && allTags
? '<themes>' + song.tags.map(
tag => locales.map(l =>`<theme lang="${l}">${allTags[tag][l] ?? tag.key}</theme>`).join('')
).join('') + '</themes>'
: '';
const lyrics = parsedContent(song.content, song.tuning, false, false).map(p => {
const num = p.number > 0 ? p.number : '1';
return `<verse name="${p.type ? p.type.toUpperCase() : 'V'}${num}"><lines>` + p.content.replace(/\n/g, "<br />") + '</lines></verse>'
}).join('');

return `<?xml version='1.0' encoding='UTF-8'?>
<song xmlns="http://openlyrics.info/namespace/2009/song" version="0.9" createdIn="SongDrive ${version}" modifiedIn="SongDrive ${version}" modifiedDate="${timestamp}">
<properties>
<titles>${title}${subtitle}</titles>
${copyright}${year}${ccli}${authors}${tags}
</properties>
<lyrics>${lyrics}</lyrics>
</song>`;
};

export {
keyScale,
userRoles,
Expand All @@ -347,4 +380,5 @@ export {
sortTags,
browserPrefersDark,
mailto,
openLyricsXML,
}
93 changes: 91 additions & 2 deletions frontend/src/views/SetlistShow.vue
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,13 @@
<icon-files class="w-5 h-5 stroke-1.5" />
{{ t('button.exportSetlistSheets') }}
</button>
<button
class="px-3 py-2 w-full flex items-center gap-3 hover:bg-blade-100 dark:hover:bg-blade-750"
@click="exportOsz()"
>
<icon-file class="w-5 h-5 stroke-1.5" />
{{ t('button.filetypeOsz') }}
</button>
</template>
</dropdown>
</div>
Expand Down Expand Up @@ -458,7 +465,7 @@
</template>

<script setup>
import { keyScale, parsedContent, humanDate, throwError } from '@/utils.js';
import { keyScale, parsedContent, humanDate, throwError, download, openLyricsXML } from '@/utils.js';
import { logicAnd } from '@vueuse/math';
import { notify } from '@kyvg/vue3-notification';
import { ref, reactive, computed, inject } from 'vue';
Expand All @@ -469,7 +476,8 @@ import DoughnutChart from '@/charts/DoughnutChart.vue';
import draggable from 'vuedraggable';
import Dropdown from '@/elements/Dropdown.vue';
import { setDoc, updateDoc, doc, FieldValue } from 'firebase/firestore';
import pdfMake from "pdfmake/build/pdfmake";
import { BlobWriter, ZipWriter, TextReader } from '@zip.js/zip.js';
import pdfMake from 'pdfmake/build/pdfmake';
import PrimaryButton from '@/elements/PrimaryButton.vue';
import SecondaryButton from '@/elements/SecondaryButton.vue';
import SetlistDelete from '@/modals/SetlistDelete.vue';
Expand All @@ -491,6 +499,7 @@ import {
IconExternalLink,
IconEye,
IconFiles,
IconFile,
IconFileText,
IconLock,
IconMarkdown,
Expand Down Expand Up @@ -518,6 +527,7 @@ const hkChords = inject('hkChords');
const hkSync = inject('hkSync');
const hkPresent = inject('hkPresent');
const noActiveModal = inject('noActiveModal');
const version = inject('version');
// pdf creation
pdfMake.fonts = {
Expand Down Expand Up @@ -956,6 +966,85 @@ const getPdfSongsheets = () => {
return sheets;
};
const exportOsz = async () => {
// initialize file content of service data osj file for OpenLP
const content = [{
'openlp_core': {
'lite-service': false,
'service-theme': null,
'openlp-servicefile-version': 3
}
}];
// add service items, one per song
for (const key in setlist.value.songs) {
if (setlist.value.songs.hasOwnProperty(key) && setlist.value.songs[key].id in props.songs) {
// get song object
const song = props.songs[setlist.value.songs[key].id];
// handle song content parts
let itemData = [];
let parts = parsedContent(song.content, 0, false, false);
parts.forEach((part) => {
itemData.push({
'raw_slide': part.content,
'verseTag': (part.type ? part.type.toUpperCase() : 'V') + (part.number > 0 ? part.number.toString() : '1'),
});
});
content.push({
'serviceitem': {
'header': {
'name': 'songs',
'plugin': 'songs',
'theme': null,
'title': song.title,
'footer': [
song.title,
`${t('field.authors')}: ${song.authors}`
],
'type': 1,
'audit': [song.title, song.authors ? song.authors.split(' | ') : [], song.publisher, song.ccli.toString()],
'notes': '',
'from_plugin': false,
'capabilities': [2, 1, 5, 8, 9, 13, 22],
'search': '',
'data': {
'title': `${song.title.toLowerCase()}@${song.subtitle.toLowerCase()}`,
'alternate_title': song.subtitle,
'authors': song.authors,
'ccli_number': song.ccli,
'copyright': song.publisher
},
'xml_version': openLyricsXML(song, version),
'auto_play_slides_once': false,
'auto_play_slides_loop': false,
'timed_slide_interval': 0,
'start_time': 0,
'end_time': 0,
'media_length': 0,
'background_audio': [],
'theme_overwritten': false,
'will_auto_start': false,
'processor': null,
'metadata': [],
'sha256_file_hash': null,
'stored_filename': null
},
'data': itemData
}
});
}
}
// do export
console.log(content);
const blobWriter = new BlobWriter('application/zip');
const writer = new ZipWriter(blobWriter);
await writer.add('service_data.osj', new TextReader(JSON.stringify(content)));
await writer.close();
const blob = await blobWriter.getData();
download(blob, `${setlistKey}.osz`, true)
};
// component shortcuts
whenever(
logicAnd(hkChords, noActiveModal),
Expand Down
34 changes: 2 additions & 32 deletions frontend/src/views/SongShow.vue
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,7 @@
</template>

<script setup>
import { keyScale, isChordLine, parsedContent, download } from '@/utils.js';
import { keyScale, isChordLine, parsedContent, download, openLyricsXML } from '@/utils.js';
import { logicAnd, logicOr } from '@vueuse/math';
import { notify } from '@kyvg/vue3-notification';
import { ref, reactive, computed, inject, onMounted } from 'vue';
Expand Down Expand Up @@ -524,38 +524,8 @@ const exportSng = () => {
};
// export song in OpenLyrics XML format
const exportXml = () => {
// add header
const timestamp = (new Date()).toISOString().slice(0, -5);
const title = `<title>${song.value.title}</title>`;
const subtitle = song.value.subtitle ? `<title>${song.value.subtitle}</title>` : '';
const year = song.value.year ? `<released>${song.value.year}</released>` : '';
const copyright = song.value.year || song.value.publisher
? '<copyright>' + song.value.year + ' ' + song.value.publisher.replace(/(?:\r\n|\r|\n)/g, '; ') + '</copyright>'
: '';
const ccli = song.value.ccli ? `<ccliNo>${song.value.ccli}</ccliNo>` : '';
const authors = song.value.authors
? '<authors>' + song.value.authors.split('|').map(a => `<author>${a.trim()}</author>`).join('') + '</authors>'
: '';
const tags = song.value.tags
? '<themes>' + song.value.tags.map(
tag => availableLocales.map(l =>`<theme lang="${l}">${props.tags[tag][l] ?? tag.key}</theme>`).join('')
).join('') + '</themes>'
: '';
const lyrics = parsedContent(song.value.content, song.value.tuning, false, false).map(p => {
const num = p.number > 0 ? p.number : '1';
return `<verse name="${p.type}${num}"><lines>` + p.content.replace(/\n/g, "<br />") + '</lines></verse>'
}).join('');
const content = `<?xml version='1.0' encoding='UTF-8'?>
<song xmlns="http://openlyrics.info/namespace/2009/song" version="0.9" createdIn="SongDrive ${version}" modifiedIn="SongDrive ${version}" modifiedDate="${timestamp}">
<properties>
<titles>${title}${subtitle}</titles>
${copyright}${year}${ccli}${authors}${tags}
</properties>
<lyrics>${lyrics}</lyrics>
</song>
`;
// start download
download(content, songId + '.xml');
download(openLyricsXML(song.value, version, availableLocales, props.tags), songId + '.xml');
// toast success message
notify({
title: t('toast.exportedXml'),
Expand Down

0 comments on commit 6f118d6

Please sign in to comment.