From b0f65b69e0b1b8b3f9bc0c1ac64c5a1dd05e2e9e Mon Sep 17 00:00:00 2001 From: Piotr Tabor Date: Fri, 12 Jun 2026 19:00:42 +0200 Subject: [PATCH 1/6] fix: ignore dangling symlinks during shutil.copytree in backend --- containers/backend/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/containers/backend/main.py b/containers/backend/main.py index 73564515..043f5782 100644 --- a/containers/backend/main.py +++ b/containers/backend/main.py @@ -135,7 +135,7 @@ def setup_work_dir(temp_dir: str) -> str: dst = os.path.join(work_dir, item) if os.path.exists(src): if os.path.isdir(src): - shutil.copytree(src, dst, dirs_exist_ok=True) + shutil.copytree(src, dst, dirs_exist_ok=True, symlinks=True, ignore_dangling_symlinks=True) else: shutil.copy2(src, dst) return work_dir From 4fafec97cda2b9def0d5423bcfd7c30d08dca8cf Mon Sep 17 00:00:00 2001 From: Piotr Tabor Date: Fri, 12 Jun 2026 19:04:29 +0200 Subject: [PATCH 2/6] chore: add test_venv to ignores --- .dockerignore | 2 ++ .gcloudignore | 2 ++ 2 files changed, 4 insertions(+) diff --git a/.dockerignore b/.dockerignore index 65fd64e5..51a81ddb 100644 --- a/.dockerignore +++ b/.dockerignore @@ -2,7 +2,9 @@ .idea/ .python_venv/ .direnv/ +.venv/ venv/ +test_venv/ containers/backend/venv/ build/ npmtmp/ diff --git a/.gcloudignore b/.gcloudignore index 65fd64e5..51a81ddb 100644 --- a/.gcloudignore +++ b/.gcloudignore @@ -2,7 +2,9 @@ .idea/ .python_venv/ .direnv/ +.venv/ venv/ +test_venv/ containers/backend/venv/ build/ npmtmp/ From eafecde5b934e99b12e9acc049f10dc96dcb3bf3 Mon Sep 17 00:00:00 2001 From: Piotr Tabor Date: Fri, 12 Jun 2026 19:20:10 +0200 Subject: [PATCH 3/6] feat: add A4/A5 selection and inline PDF render status for single song --- editor/index.html | 56 ++++++++++++++++++++++++---------------------- editor/songbook.js | 7 ++++++ 2 files changed, 36 insertions(+), 27 deletions(-) diff --git a/editor/index.html b/editor/index.html index c6624c99..630d0783 100644 --- a/editor/index.html +++ b/editor/index.html @@ -76,7 +76,7 @@ await commit(e.target); } ); songEditor.addEventListener("git:commitAndPublish", (e)=>commitAndPublish(e) ); - songEditor.addEventListener("pdf:render", (e) => renderPdf(e.target)); + songEditor.addEventListener("pdf:render", (e) => renderPdf(e.target, e.detail?.papersize || "a4")); const topNav = document.getElementById("topnav"); for (const nav of [document.getElementById("topnav"), document.getElementById("bottomnav")]) { @@ -232,52 +232,54 @@

Musisz się zalogować

} } - async function renderPdf(songbook) { + async function renderPdf(songbook, papersize = "a4") { const {serialized, title} = songbook.SerializeWithChange(); const {branch} = parseParams(); const payload = { xml_content: serialized, format: "single", - papersize: "a4", + papersize: papersize, title: title || "Song", branch: branch || "main" }; + const statusSpan = songbook.shadowRoot ? songbook.shadowRoot.getElementById("pdfRenderStatus") : null; + renderPdfCloudRun( payload, "/api/render/song_xml", - () => notice("Generowanie PDF... Proszę czekać (może potrwać do kilkunastu sekund)", songbook), + () => { + if (statusSpan) { + statusSpan.innerHTML = `⏳ Generowanie PDF... (ok. 10-15s)`; + } else { + notice("Generowanie PDF... Proszę czekać (może potrwać do kilkunastu sekund)", songbook); + } + }, (url) => { const dateStr = new Date().toISOString().split('T')[0]; const filename = encodeURIComponent(`${title || 'piosenka'}_${dateStr}.pdf`); const finalUrl = `${url}?filename=${filename}&disposition=inline`; - const noticeDiv = document.createElement("div"); - noticeDiv.classList.add("notice"); - noticeDiv.innerHTML = ` -
-

PDF wygenerowany!

- Gotowe! Otwórz Podgląd PDF -
- Zamknij to okienko -
-
- `; - document.body.appendChild(noticeDiv); + if (statusSpan) { + statusSpan.innerHTML = `✅ Gotowe! Otwórz PDF`; + setTimeout(() => { if (statusSpan.innerHTML.includes('Otwórz PDF')) statusSpan.innerHTML = ''; }, 60000); + } else { + const noticeDiv = document.createElement("div"); + noticeDiv.classList.add("notice"); + noticeDiv.innerHTML = ` +
+

PDF wygenerowany!

+ Otwórz Podgląd PDF +
Zamknij to okienko
+
`; + document.body.appendChild(noticeDiv); + } }, (err, errUrl) => { + if (statusSpan) { + statusSpan.innerHTML = `❌ Błąd generowania PDF.`; + } alert("Błąd generowania: " + err); } ); diff --git a/editor/songbook.js b/editor/songbook.js index c0485b09..17298bd5 100644 --- a/editor/songbook.js +++ b/editor/songbook.js @@ -32,6 +32,11 @@ export class SongEditor extends HTMLElement { + +
@@ -166,10 +171,12 @@ export class SongEditor extends HTMLElement { this.openUrl.addEventListener("click", () => this.OpenUrl()); this.buttonSave.addEventListener("click", () => Save(this)); this.buttonRenderPdf.addEventListener("click", () => { + const papersize = this.shadow.getElementById("pdfPapersize").value; const renderEvent = new CustomEvent("pdf:render", { bubbles: true, cancelable: true, composed: true, + detail: { papersize: papersize } }); this.dispatchEvent(renderEvent); }); From 909acfc9085e87f6fdb6c3f872586d50c15d8182 Mon Sep 17 00:00:00 2001 From: Piotr Tabor Date: Fri, 12 Jun 2026 19:21:45 +0200 Subject: [PATCH 4/6] feat: clear inline PDF generated status upon any song edits --- editor/songbook.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/editor/songbook.js b/editor/songbook.js index 17298bd5..99d3d628 100644 --- a/editor/songbook.js +++ b/editor/songbook.js @@ -212,6 +212,16 @@ export class SongEditor extends HTMLElement { } this.attributeChangedCallback("git"); + + // Clear PDF "Gotowe" status if the user edits anything + const clearPdfStatus = () => { + const statusSpan = this.shadow.getElementById("pdfRenderStatus"); + if (statusSpan && statusSpan.innerHTML.includes("Gotowe")) { + statusSpan.innerHTML = ""; + } + }; + this.shadow.addEventListener("input", clearPdfStatus); + this.shadow.addEventListener("change", clearPdfStatus); } mapAttribute(attr) { From 9b7ff6e70c04c3da4578f567580886e7316688c1 Mon Sep 17 00:00:00 2001 From: Piotr Tabor Date: Fri, 12 Jun 2026 23:09:50 +0200 Subject: [PATCH 5/6] fix: access closed shadow root properly via shadow property in single song editor --- editor/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/editor/index.html b/editor/index.html index 630d0783..9d661012 100644 --- a/editor/index.html +++ b/editor/index.html @@ -244,7 +244,7 @@

Musisz się zalogować

branch: branch || "main" }; - const statusSpan = songbook.shadowRoot ? songbook.shadowRoot.getElementById("pdfRenderStatus") : null; + const statusSpan = songbook.shadow ? songbook.shadow.getElementById("pdfRenderStatus") : null; renderPdfCloudRun( payload, From b28f206ea7daca5b203fdc91b79d0235c0adc6ed Mon Sep 17 00:00:00 2001 From: Piotr Tabor Date: Thu, 2 Jul 2026 21:51:22 +0200 Subject: [PATCH 6/6] feat: add load and save YAML options to songbook editor --- .../songbook_edit/songbook_edit.html | 6 + .../templates/songbook_edit/songbook_edit.js | 106 ++++++++++++++++++ 2 files changed, 112 insertions(+) diff --git a/src/html/templates/songbook_edit/songbook_edit.html b/src/html/templates/songbook_edit/songbook_edit.html index 07e960f4..1ecc04e8 100644 --- a/src/html/templates/songbook_edit/songbook_edit.html +++ b/src/html/templates/songbook_edit/songbook_edit.html @@ -12,6 +12,11 @@

Kreator Śpiewnika

Stwórz swój własny śpiewnik wybierając ulubione piosenki

+
+ + + +
@@ -140,6 +145,7 @@

Wybrane piosenki

+ diff --git a/src/html/templates/songbook_edit/songbook_edit.js b/src/html/templates/songbook_edit/songbook_edit.js index aa850e53..7e0961a7 100644 --- a/src/html/templates/songbook_edit/songbook_edit.js +++ b/src/html/templates/songbook_edit/songbook_edit.js @@ -617,6 +617,112 @@ function copyToClipboard() { alert('Skopiowano do schowka!'); } +function downloadYAMLDirect() { + const yaml = generateYAMLString(); + if (!yaml) return; + + const songbookId = document.getElementById('songbookId').value.trim() || 'spiewnik'; + const filename = `${songbookId}.songbook.yaml`; + + const blob = new Blob([yaml], { type: 'text/yaml' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = filename; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); +} + +function loadFromYAMLFile(event) { + const file = event.target.files[0]; + if (!file) return; + + const reader = new FileReader(); + reader.onload = function(e) { + try { + const content = e.target.result; + let data; + if (typeof jsyaml !== 'undefined') { + data = jsyaml.load(content); + } else { + data = JSON.parse(content); + } + + if (!data || !data.songbook) { + throw new Error("Nieprawidłowy format pliku: brak obiektu 'songbook'"); + } + + const sb = data.songbook; + + // Populate form fields + document.getElementById('songbookId').value = sb.id || ''; + document.getElementById('songbookTitle').value = sb.title || ''; + document.getElementById('songbookSubtitle').value = sb.subtitle || ''; + document.getElementById('songbookPublisher').value = sb.publisher || ''; + document.getElementById('songbookPlace').value = sb.place || ''; + + userEditedId = !!sb.id; // Prevents auto-generation of ID + + // Clear current selections + selectedSongIds.clear(); + dynamicFilters = []; + filterCounter = 0; + activeAggregatedFilters.clear(); + + // Parse songs + if (sb.songs && Array.isArray(sb.songs)) { + sb.songs.forEach(item => { + if (item.glob) { + let found = false; + for (const song of allSongs) { + if (song.path === item.glob || `songs/**/${song.id}.xml` === item.glob) { + selectedSongIds.add(song.id); + found = true; + break; + } + } + // Fallback + if (!found) { + const match = item.glob.match(/([^\/]+)\.xml$/); + if (match) { + const songId = match[1]; + if (songById.has(songId)) { + selectedSongIds.add(songId); + } + } + } + } else if (item.genre && item.genre.equals) { + addDynamicFilter('genre', item.genre.equals); + activeAggregatedFilters.set(`genre:${item.genre.equals}`, { type: 'genre', value: item.genre.equals }); + } else if (item.artist && item.artist.equals) { + addDynamicFilter('artist', item.artist.equals); + activeAggregatedFilters.set(`artist:${item.artist.equals}`, { type: 'artist', value: item.artist.equals }); + } else if (item.text_author && item.text_author.equals) { + addDynamicFilter('text_author', item.text_author.equals); + activeAggregatedFilters.set(`text_author:${item.text_author.equals}`, { type: 'text_author', value: item.text_author.equals }); + } + }); + } + + // Render updates + renderDynamicFilters(); + renderSongList(); + renderSelectedSongs(); + + alert('Śpiewnik został pomyślnie wczytany!'); + + } catch (error) { + alert('Błąd podczas wczytywania pliku: ' + error.message); + } + + // Clear input + event.target.value = ''; + }; + reader.readAsText(file); +} + async function renderPDF() { const yaml = generateYAMLString(); if (!yaml) return;