From 30ebae6dae2c9d7589724f15349db3fdcdd5ef94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20M=C3=BCller?= Date: Wed, 5 Aug 2026 12:08:20 +0200 Subject: [PATCH 1/9] =?UTF-8?q?=E2=9E=95=20Add/edit=20slides=20from=20Setl?= =?UTF-8?q?istShow?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/models.ts | 11 +- frontend/src/definitions.ts | 4 +- frontend/src/locales/de.json | 5 + frontend/src/locales/en.json | 5 + frontend/src/modals/SetlistSet.vue | 79 ++++++------ frontend/src/modals/SlideSet.vue | 84 +++++++++++++ frontend/src/modals/SongSet.vue | 8 +- frontend/src/utils.ts | 6 +- frontend/src/views/SetlistShow.vue | 142 +++++++++++++++++----- frontend/src/views/SongShow.vue | 32 ++--- frontend/src/widgets/WidgetSongList.vue | 2 + frontend/src/widgets/WidgetSongOfYear.vue | 2 + 12 files changed, 293 insertions(+), 87 deletions(-) create mode 100644 frontend/src/modals/SlideSet.vue diff --git a/backend/models.ts b/backend/models.ts index 668d82db..0074c7a0 100644 --- a/backend/models.ts +++ b/backend/models.ts @@ -25,6 +25,15 @@ export type SetlistSong = { key: string; // Custom key (previously named 'tuning') }; +export type SetlistSlide = { + type: 'plain'; // slide content formatter; more types (e.g. 'markdown') may be added later + title: string; // Displayed slide title + content: string; // Slide content +}; + +// A setlist.songs entry: entries with an `id` are songs, entries without one are slides +export type SetlistEntry = SetlistSong | SetlistSlide; + export type SetlistEntity = { active: boolean; // If true, the setlist is currently syncing positions createdBy: string; // User id of the creator (previously named 'creator') @@ -36,7 +45,7 @@ export type SetlistEntity = { remoteText?: boolean; // Presentation: broadcast chords-visible state to synced viewers sharedWith: string[]; // List of user ids with whom this setlist is shared slug: string; // Unique setlist url slug (previously named 'id') - songs: SetlistSong[]; // List of song ids and custom keys of songs the setlist contains + songs: SetlistEntry[]; // List of songs (with custom keys) and slides the setlist contains title: string; // Displayed setlist title }; export type Setlist = { diff --git a/frontend/src/definitions.ts b/frontend/src/definitions.ts index d8d6f4b7..9106f955 100644 --- a/frontend/src/definitions.ts +++ b/frontend/src/definitions.ts @@ -1,4 +1,4 @@ -import type { SongEntity, SetlistEntity, SetlistSong } from '@backend/models'; +import type { SongEntity, SetlistEntity, SetlistEntry } from '@backend/models'; import type { UserRole } from '@backend/definitions'; /** @@ -59,7 +59,7 @@ export type SetlistFormData = Partial & { title: string; isPublic: boolean; date: string; - songs: SetlistSong[]; + songs: SetlistEntry[]; }; /** diff --git a/frontend/src/locales/de.json b/frontend/src/locales/de.json index c136e36d..c3addf0b 100644 --- a/frontend/src/locales/de.json +++ b/frontend/src/locales/de.json @@ -9,12 +9,14 @@ "summary": "Speichern, Synchronisieren und Präsentieren von Liedern und Setlisten" }, "button": { + "addSlide": "Folie hinzufügen", "assign": "Zuweisen", "back": "Zurück", "cancel": "Abbrechen", "changePassword": "Passwort ändern", "copy": "Kopieren", "createSetlist": "Setliste Erstellen", + "createSlide": "Folie Erstellen", "createSong": "Song Erstellen", "delete": "Löschen", "docsOnGithub": "Doku auf GitHub", @@ -41,6 +43,7 @@ "signOut": "Abmelden", "top": "Nach oben", "updateSetlist": "Setliste Aktualisieren", + "updateSlide": "Folie Aktualisieren", "updateSong": "Song Aktualisieren" }, "divider": { @@ -126,8 +129,10 @@ "deleteSetlist": "Setlist Löschen", "deleteSong": "Song Löschen", "editSetlist": "Setlist Bearbeiten", + "editSlide": "Folie Bearbeiten", "editSong": "Song Bearbeiten", "newSetlist": "Neue Setlist", + "newSlide": "Neue Folie", "newSong": "Neuer Song", "songInfo": "Song Informationen", "songSyntaxCheatsheet": "Song-Syntax Spickzettel", diff --git a/frontend/src/locales/en.json b/frontend/src/locales/en.json index c4accd8a..0911d47e 100644 --- a/frontend/src/locales/en.json +++ b/frontend/src/locales/en.json @@ -9,12 +9,14 @@ "summary": "Store, manage, synchronize and present songs and setlists" }, "button": { + "addSlide": "Add Slide", "assign": "Assign", "back": "Back", "cancel": "Cancel", "changePassword": "Change Password", "copy": "Copy", "createSetlist": "Create Setlist", + "createSlide": "Create Slide", "createSong": "Create Song", "delete": "Delete", "docsOnGithub": "Docs on GitHub", @@ -41,6 +43,7 @@ "signOut": "Sign Out", "top": "Top", "updateSetlist": "Update Setlist", + "updateSlide": "Update Slide", "updateSong": "Update Song" }, "divider": { @@ -126,8 +129,10 @@ "deleteSetlist": "Delete Setlist", "deleteSong": "Delete Song", "editSetlist": "Edit Setlist", + "editSlide": "Edit Slide", "editSong": "Edit Song", "newSetlist": "New Setlist", + "newSlide": "New Slide", "newSong": "New Song", "songInfo": "Song Information", "songSyntaxCheatsheet": "Song Syntax Cheatsheet", diff --git a/frontend/src/modals/SetlistSet.vue b/frontend/src/modals/SetlistSet.vue index 8999f6c8..194865d7 100644 --- a/frontend/src/modals/SetlistSet.vue +++ b/frontend/src/modals/SetlistSet.vue @@ -203,7 +203,7 @@
{{ setlist.songs?.length }} {{ t('text.selected') }}
-
- - - -
-
{{ element.key ?? findSong(element.id)?.key }}
-
- - - -
-
-
- {{ findSong(element.id)?.title }} + + @@ -271,14 +276,14 @@ import { injectStrict, setlistCollectionKey, setlistsKey, songsKey, userKey } fr import '@vuepic/vue-datepicker/dist/main.css'; import { enGB, de } from 'date-fns/locale'; import type { Locale } from 'date-fns'; -import { keyScale, humanDate, throwError, urlify, browserPrefersDark, sortTags } from '@/utils.js'; +import { keyScale, humanDate, throwError, urlify, browserPrefersDark, sortTags, isSlide } from '@/utils.js'; import type { ThrowableError, SetlistFormData } from '@/definitions'; import { notify } from '@kyvg/vue3-notification'; import { ref, reactive, computed, watch, type PropType } from 'vue'; import { useI18n } from 'vue-i18n'; import { useRouter } from 'vue-router'; import { SongLanguage, SongTag as SongTagEnum } from '@backend/definitions'; -import type { SetlistEntity } from '@backend/models'; +import type { SetlistEntity, SetlistEntry, SetlistSong } from '@backend/models'; import { VueDatePicker as Datepicker } from '@vuepic/vue-datepicker'; import draggable from 'vuedraggable'; import DropDown from '@/elements/DropDown.vue'; @@ -360,8 +365,8 @@ const resetErrors = () => { // build local editable setlist state from the initial (blank or existing) setlist const buildFormState = (initial: SetlistFormData): SetlistFormData => ({ ...initial, - // only show undeleted songs - songs: initial.songs.filter(s => findSong(s.id)), + // only show undeleted songs, keep slides as-is + songs: initial.songs.filter(s => isSlide(s) || findSong(s.id)), }); // setlist input data @@ -380,11 +385,16 @@ const addSong = (id: string) => { // remove song from current song selection const removeSong = (id: string) => { - setlist.value.songs = setlist.value.songs.filter((s) => s.id !== id); + setlist.value.songs = setlist.value.songs.filter((s) => isSlide(s) || s.id !== id); +}; + +// remove the setlist entry (song or slide) at the given position +const removeEntry = (index: number) => { + setlist.value.songs.splice(index, 1); }; // check if given song exists on current song selection -const idExists = (id: string) => setlist.value?.songs.some((s) => s.id === id); +const idExists = (id: string) => setlist.value?.songs.some((s) => !isSlide(s) && s.id === id); // add or remove song of given id const songSelection = (id: string, addition: boolean) => { @@ -447,6 +457,7 @@ const performedSongs = computed(() => { let sortedSetlists = setlists.value.toSorted((a, b) => b.entity.date.localeCompare(a.entity.date)); sortedSetlists.forEach(setlist => { setlist.entity.songs.forEach(song => { + if (isSlide(song)) return; if (!(song.id in songs)) { songs[song.id] = humanDate(setlist.entity.date, loc, false); } @@ -460,9 +471,9 @@ const updateDate = (newDate: Date) => { setlist.value.date = newDate.toISOString().slice(0,10); }; -// tune the song at given position up +// tune the song at given position up (only ever called for song entries, never slides) const tuneUp = (position: number) => { - let songs = setlist.value.songs; + let songs = setlist.value.songs as SetlistSong[]; // update tuning let tone = songs[position].key ? songs[position].key : findSong(songs[position].id)?.key; let i = keyScale.indexOf(tone ?? ''); @@ -472,12 +483,12 @@ const tuneUp = (position: number) => { tone = keyScale[++i]; } // save tuning in setlist - setlist.value.songs[position].key = tone; + songs[position].key = tone; }; -// tune the song at given position down +// tune the song at given position down (only ever called for song entries, never slides) const tuneDown = (position: number) => { - let songs = setlist.value.songs; + let songs = setlist.value.songs as SetlistSong[]; // update tuning let tone = songs[position].key ? songs[position].key : findSong(songs[position].id)?.key; let i = keyScale.indexOf(tone ?? ''); @@ -487,7 +498,7 @@ const tuneDown = (position: number) => { tone = keyScale[--i]; } // save tuning in setlist - setlist.value.songs[position].key = tone; + songs[position].key = tone; }; // create a human readable record key of format YYYYMMDD-the-setlist-title diff --git a/frontend/src/modals/SlideSet.vue b/frontend/src/modals/SlideSet.vue new file mode 100644 index 00000000..1d87cfd0 --- /dev/null +++ b/frontend/src/modals/SlideSet.vue @@ -0,0 +1,84 @@ + + + diff --git a/frontend/src/modals/SongSet.vue b/frontend/src/modals/SongSet.vue index 3ef1f67d..036bba87 100644 --- a/frontend/src/modals/SongSet.vue +++ b/frontend/src/modals/SongSet.vue @@ -229,7 +229,7 @@ diff --git a/frontend/src/widgets/WidgetSongList.vue b/frontend/src/widgets/WidgetSongList.vue index 4576307f..44f15723 100644 --- a/frontend/src/widgets/WidgetSongList.vue +++ b/frontend/src/widgets/WidgetSongList.vue @@ -147,8 +147,8 @@ const popularSongs = () => { order.value = SortOrder.Popular; let list: Record = {}; setlists.value.forEach(setlist => { - if (setlist.entity.songs) { - setlist.entity.songs.forEach(song => { + if (setlist.entity.entries) { + setlist.entity.entries.forEach(song => { if (isSlide(song)) return; if (!list.hasOwnProperty(song.id)) { list[song.id] = 1; diff --git a/frontend/src/widgets/WidgetSongOfYear.vue b/frontend/src/widgets/WidgetSongOfYear.vue index ed1fe08e..efccccd5 100644 --- a/frontend/src/widgets/WidgetSongOfYear.vue +++ b/frontend/src/widgets/WidgetSongOfYear.vue @@ -89,11 +89,11 @@ const songOfYear = computed(() => { let list: Record> = {}; setlists.value.forEach(setlist => { let year = setlist.entity.date.slice(0, 4); - if (year && setlist.entity.songs) { + if (year && setlist.entity.entries) { if (!list.hasOwnProperty(year)) { list[year] = {}; } - setlist.entity.songs.forEach(song => { + setlist.entity.entries.forEach(song => { if (isSlide(song)) return; if (songs.value.some(s => s.entity.slug == song.id)) { if (!list[year].hasOwnProperty(song.id)) { From 31135c210f33b881d13b6a95dec8c3fce2d99e00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20M=C3=BCller?= Date: Sat, 8 Aug 2026 23:51:40 +0200 Subject: [PATCH 4/9] =?UTF-8?q?=E2=9E=95=20Edit=20slides=20from=20SetlistS?= =?UTF-8?q?how=20on=20row=20click?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/elements/ModalDialog.vue | 2 +- frontend/src/views/SetlistShow.vue | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/frontend/src/elements/ModalDialog.vue b/frontend/src/elements/ModalDialog.vue index 265c13e2..39e78039 100644 --- a/frontend/src/elements/ModalDialog.vue +++ b/frontend/src/elements/ModalDialog.vue @@ -26,7 +26,7 @@ }" @click.stop="null" > -
+
{{ title }}