Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions frontend/src/locales/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@
"editThis": "Diese Seite bearbeiten",
"export": "Export",
"exportSetlistList": "Liste als PDF",
"exportSetlistListTxt": "Liste als TXT",
"exportSetlistSheets": "Liedzettel als PDF",
"exportSetlistSheetsTxt": "Liedzettel als TXT",
"filetypeOsz": "OpenLP Service [.osz]",
"filetypePdf": "PDF-Datei [.pdf]",
"filetypeSng": "SongBeamer-Datei [.sng]",
Expand Down Expand Up @@ -310,6 +312,7 @@
"copiedToClipboard": "In die Zwischenablage kopiert",
"exportedPdf": "Exportiert als PDF",
"exportedSetlistPdfText": "Die Setlist wurde erfolgreich als PDF Datei exportiert.",
"exportedSetlistTextText": "Die Setlist wurde erfolgreich im Textformat exportiert.",
"exportedSng": "Exportiert als SNG",
"exportedSongPdfText": "Der Song wurde erfolgreich als PDF Datei exportiert.",
"exportedSongSngText": "Der Song wurde erfolgreich im SNG Format exportiert.",
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@
"editThis": "Edit this page",
"export": "Export",
"exportSetlistList": "List as PDF",
"exportSetlistListTxt": "List as TXT",
"exportSetlistSheets": "Sheets as PDF",
"exportSetlistSheetsTxt": "Sheets as TXT",
"filetypeOsz": "OpenLP Service [.osz]",
"filetypePdf": "PDF file [.pdf]",
"filetypeSng": "SongBeamer file [.sng]",
Expand Down Expand Up @@ -310,6 +312,7 @@
"copiedToClipboard": "Copied to clipboard",
"exportedPdf": "Exported as PDF",
"exportedSetlistPdfText": "The setlist was successfully exported as PDF file.",
"exportedSetlistTextText": "The setlist was successfully exported in plain text format.",
"exportedSng": "Exported as SNG",
"exportedSongPdfText": "The song was successfully exported in PDF format.",
"exportedSongSngText": "The song was successfully exported in SNG format.",
Expand Down
23 changes: 23 additions & 0 deletions frontend/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,28 @@ function parsedContent(content: string, keyOffset: number, showChords: boolean,
}
};

// Build plain text content for a song's songsheet: header (title [key]), lyrics (transposed by keyOffset, with chord
// lines optionally stripped and verse numbers re-attached to their lyric line), and an authors/copyright footer.
const songPlainTextContent = (song: SongEntity, keyOffset: number, tuning: string, showChords: boolean): string => {
// add header
var content = song.title + ' [' + tuning + ']' + '\n\n';
parsedContent(song.content, keyOffset, showChords, false).forEach(part => {
var block = part.content;
// re-attach the verse number to its lyric line (skipping a leading chord line, padded to stay aligned)
if (part.type == 'v' && part.number != '0') {
var lines = block.split('\n');
var target = isChordLine(lines[0]) ? 1 : 0;
if (target == 1) lines[0] = ' ' + lines[0];
lines[target] = part.number + '. ' + (lines[target] ?? '');
block = lines.join('\n');
}
content += block + '\n\n';
});
content += song.authors?.join(', ') + '\n\n'
+ '© ' + (song.year ? song.year + ' ' : '') + song.publisher.replace(/(?:\r\n|\r|\n)/g, '; ');
return content;
};

// file download
const download = (data: string | Blob, filename: string, isBlob = false): void => {
var a = document.createElement('a');
Expand Down Expand Up @@ -524,6 +546,7 @@ export {
bufferAndDefer,
maximizePresentFontsize,
parsedContent,
songPlainTextContent,
download,
humanDate,
humanFileSize,
Expand Down
72 changes: 64 additions & 8 deletions frontend/src/views/SetlistShow.vue
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@
class="px-3 py-2 w-full flex items-center gap-3 hover:bg-blade-100 dark:hover:bg-blade-750"
@click="exportPdf('list')"
>
<icon-file-text class="w-5 h-5 stroke-1.5" />
<icon-file-type-pdf class="w-5 h-5 stroke-1.5" />
{{ t('button.exportSetlistList') }}
</button>
<button
Expand All @@ -135,11 +135,25 @@
<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="exportTxt('list')"
>
<icon-file-type-txt class="w-5 h-5 stroke-1.5" />
{{ t('button.exportSetlistListTxt') }}
</button>
<button
class="px-3 py-2 w-full flex items-center gap-3 hover:bg-blade-100 dark:hover:bg-blade-750"
@click="exportTxt('sheets')"
>
<icon-file-stack class="w-5 h-5 stroke-1.5" />
{{ t('button.exportSetlistSheetsTxt') }}
</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" />
<icon-file-music class="w-5 h-5 stroke-1.5" />
{{ t('button.filetypeOsz') }}
</button>
</template>
Expand Down Expand Up @@ -213,7 +227,7 @@
class="px-3 py-2 w-full flex items-center gap-3 hover:bg-blade-100 dark:hover:bg-blade-750 sm:hidden"
@click="exportPdf('list')"
>
<icon-file-text class="w-5 h-5 stroke-1.5" />
<icon-file-type-pdf class="w-5 h-5 stroke-1.5" />
{{ t('button.exportSetlistList') }}
</button>
<button
Expand All @@ -223,6 +237,27 @@
<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 sm:hidden"
@click="exportTxt('list')"
>
<icon-file-type-txt class="w-5 h-5 stroke-1.5" />
{{ t('button.exportSetlistListTxt') }}
</button>
<button
class="px-3 py-2 w-full flex items-center gap-3 hover:bg-blade-100 dark:hover:bg-blade-750 sm:hidden"
@click="exportTxt('sheets')"
>
<icon-file-stack class="w-5 h-5 stroke-1.5" />
{{ t('button.exportSetlistSheetsTxt') }}
</button>
<button
class="px-3 py-2 w-full flex items-center gap-3 hover:bg-blade-100 dark:hover:bg-blade-750 sm:hidden"
@click="exportOsz()"
>
<icon-file-music class="w-5 h-5 stroke-1.5" />
{{ t('button.filetypeOsz') }}
</button>
</drop-down>
</div>
</div>
Expand Down Expand Up @@ -519,7 +554,7 @@

<script setup lang="ts">
import { injectStrict, hkChordsKey, hkPresentKey, hkSyncKey, noActiveModalKey, setlistCollectionKey, setlistsKey, songsKey, userKey, usersKey, versionKey } from '@/keys';
import { keyScale, parsedContent, humanDate, throwError, download, openLyricsXML, firstParam, isSlide } from '@/utils.js';
import { keyScale, parsedContent, songPlainTextContent, humanDate, throwError, download, openLyricsXML, firstParam, isSlide } from '@/utils.js';
import type { ThrowableError, SetlistSongPresentation, SetlistPresentationEntry } from '@/definitions';
import { logicAnd } from '@vueuse/math';
import { notify } from '@kyvg/vue3-notification';
Expand Down Expand Up @@ -557,8 +592,10 @@ import {
IconExternalLink,
IconEye,
IconFiles,
IconFile,
IconFileText,
IconFileMusic,
IconFileStack,
IconFileTypePdf,
IconFileTypeTxt,
IconLock,
IconMarkdown,
IconMenuOrder,
Expand Down Expand Up @@ -940,7 +977,8 @@ const removeSlide = async (index: number) => {
};

// copy list to clipboard in given format (plain|markdown|slack)
const copyList = (format: 'plain' | 'markdown' | 'slack') => {
// build setlist song list content in given format (plain|markdown|slack)
const setlistListText = (format: 'plain' | 'markdown' | 'slack'): string => {
const list = setlistSongs.value.map((song, i) => {
const title = song.title;
const subtitle = song.subtitle;
Expand All @@ -959,16 +997,34 @@ const copyList = (format: 'plain' | 'markdown' | 'slack') => {
});
// Add link to list
list.push(...['', format === 'markdown' ? `<${window.location.href}>` : window.location.href]);
return list.join('\n');
};

const copyList = (format: 'plain' | 'markdown' | 'slack') => {
// Copy to clipboard
navigator.clipboard.writeText(list.join('\n'));
navigator.clipboard.writeText(setlistListText(format));
notify({
title: t('toast.copiedToClipboard'),
text: t('toast.setlistFormatCopiedText', { format: format }),
type: 'primary'
});
};

// export setlist as one combined plain text file (mode: sheets|list)
const exportTxt = (mode: 'sheets' | 'list') => {
const content = mode == 'list'
? setlistListText('plain')
: setlistSongs.value.map(song => songPlainTextContent(song, song.customTuningDelta, song.customTuning, chords.value)).join('\n\n');
const type = (mode == 'sheets' ? t('text.songsheets') : t('text.list')).toLowerCase();
download(content, `${setlistKey}-${type}.txt`);
// toast success message
notify({
title: t('toast.exportedText'),
text: t('toast.exportedSetlistTextText'),
type: 'primary'
});
};

// download pdf in given mode (sheets|list)
const exportPdf = (mode: 'sheets' | 'list') => {
var content = mode == 'sheets' ? getPdfSongsheets() : getPdfSetlist();
Expand Down
33 changes: 3 additions & 30 deletions frontend/src/views/SongShow.vue
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,7 @@

<script setup lang="ts">
import { injectStrict, hkBackKey, hkChordsKey, hkDownKey, hkForwardKey, hkPresentKey, hkResetKey, hkUpKey, noActiveModalKey, setlistsKey, songsKey, userKey, versionKey } from '@/keys';
import { keyScale, isChordLine, parsedContent, download, openLyricsXML, firstParam, isSlide } from '@/utils.js';
import { keyScale, isChordLine, parsedContent, songPlainTextContent, download, openLyricsXML, firstParam, isSlide } from '@/utils.js';
import type { SetlistSong } from '@backend/models';
import { logicAnd, logicOr } from '@vueuse/math';
import { notify } from '@kyvg/vue3-notification';
Expand Down Expand Up @@ -439,36 +439,9 @@ const songExists = computed(() => {
const exportTxt = () => {
const s = song.value;
if (!s) return;
// add header
var content = s.title
+ ' [' + keyScale[(12 + keyScale.indexOf(s.key ?? '') + (key.value % 12)) % 12] + ']'
+ '\n\n';
var lines = s.content.split(EOL);
// process lines
for (var i = 0; i < lines.length; i++) {
var line = lines[i];
// handle chord line
if (!chords.value && isChordLine(line)) continue;
// handle verse marker indentation
if (line.trim().toLowerCase().indexOf('--v') >= 0 && !isNaN(parseInt(line.trim().charAt(3)))) {
// if next line is chord line, prepend number to the line after
if (isChordLine(lines[i+1])) {
lines[i+2] = line.trim().charAt(3) + '. ' + lines[i+2];
// add 3 spaces to next line to sync chords with text again
lines[i+1] = ' ' + lines[i+1];
} else {
lines[i+1] = line.trim().charAt(3) + '. ' + lines[i+1];
}
}
// handle marker
if (line.trim().indexOf('--') >= 0) continue;
// keep line for export
content += line + EOL;
}
content += EOL + s.authors?.join(', ') + EOL + EOL
+ '© ' + (s.year ? s.year + ' ' : '') + s.publisher.replace(/(?:\r\n|\r|\n)/g, '; ');
const tuning = keyScale[(12 + keyScale.indexOf(s.key ?? '') + (key.value % 12)) % 12];
// start download
download(content, songId + '.txt');
download(songPlainTextContent(s, key.value, tuning, chords.value), songId + '.txt');
// toast success message
notify({
title: t('toast.exportedText'),
Expand Down
Loading