diff --git a/src/html/index_generator.py b/src/html/index_generator.py index 7cb791e0..bea6f3de 100644 --- a/src/html/index_generator.py +++ b/src/html/index_generator.py @@ -228,6 +228,78 @@ def create_sitemap_xml(list_of_songs_meta, target_dir): tree.write(sitemap_path, pretty_print=True, xml_declaration=True, encoding='utf-8') logging.info(f"Sitemap created at {sitemap_path}") +def create_index_json(list_of_songs_meta, target_dir): + """Create index.json with all songs and songbooks metadata""" + import json + + index_json_path = os.path.join(target_dir, "index.json") + + # Build songs list + songs_data = [] + for song in list_of_songs_meta: + if song.is_alias(): + continue + + song_data = { + "id": song.base_file_name(), + "title": song.effectiveTitle(), + "html_url": f"./songs_html/{song.base_file_name()}.html", + "pdf_a4_url": f"./songs_pdf/{song.base_file_name()}.a4.pdf", + "pdf_a5_url": f"./songs_pdf/{song.base_file_name()}.a5.pdf", + } + + # Add optional fields if they exist + if song.aliases(): + song_data["aliases"] = song.aliases() + if song.artist(): + song_data["artist"] = song.artist() + if song.genre(): + song_data["genre"] = song.genre() + if song.lang(): + song_data["lang"] = song.lang() + if song.text_author(): + song_data["text_author"] = song.text_author() + + songs_data.append(song_data) + + # Build songbooks list + songbooks_data = [] + for songbook in sb.songbooks(): + if not songbook.hidden(): + songbook_data = { + "id": songbook.id(), + "title": songbook.title(), + "epub_url": f"./{songbook.id()}.epub", + "pdf_a4_url": f"./songs_tex/{songbook.id()}_a4.pdf", + "pdf_a5_url": f"./songs_tex/{songbook.id()}_a5.pdf", + } + + # Add optional fields + if songbook.subtitle(): + songbook_data["subtitle"] = songbook.subtitle() + if songbook.url(): + songbook_data["url"] = songbook.url() + if songbook.publisher(): + songbook_data["publisher"] = songbook.publisher() + if songbook.place(): + songbook_data["place"] = songbook.place() + + songbooks_data.append(songbook_data) + + # Create the complete index structure + index_data = { + "version": "1.0", + "generated": datetime.datetime.now().strftime("%Y-%m-%dT%H:%M:%S+00:00"), + "songs": songs_data, + "songbooks": songbooks_data + } + + # Write JSON file + with open(index_json_path, 'w', encoding='utf-8') as f: + json.dump(index_data, f, ensure_ascii=False, indent=2) + + logging.info(f"Index JSON created at {index_json_path} with {len(songs_data)} songs and {len(songbooks_data)} songbooks") + def main(): songbook_file = os.path.join(sb.repo_dir(), "songbooks/default.songbook.yaml") if len(sys.argv) == 1 else sys.argv[1] songbook = sb.load_songbook_spec_from_yaml(songbook_file) @@ -237,10 +309,26 @@ def main(): create_index_html(songbook.list_of_songs(), target_dir) create_sitemap_xml(songbook.list_of_songs(), target_dir) + create_index_json(songbook.list_of_songs(), target_dir) index_js_path =os.path.join(target_dir, "index.js") if os.path.exists(index_js_path): os.remove(index_js_path) os.symlink(os.path.join(sb.repo_dir(), 'src', 'html', 'templates', "index.js"), index_js_path) + # Create symlink for common.js (shared utility) + common_js_path = os.path.join(target_dir, "common.js") + if os.path.exists(common_js_path): + os.remove(common_js_path) + os.symlink(os.path.join(sb.repo_dir(), 'src', 'html', 'templates', "common.js"), common_js_path) + + # Create symlinks for songbook_edit files + songbook_edit_files = ["songbook_edit.html", "songbook_edit.js", "songbook_edit.css"] + for filename in songbook_edit_files: + target_path = os.path.join(target_dir, filename) + source_path = os.path.join(sb.repo_dir(), 'src', 'html', 'templates', 'songbook_edit', filename) + if os.path.exists(target_path): + os.remove(target_path) + os.symlink(source_path, target_path) + main() \ No newline at end of file diff --git a/src/html/templates/common.js b/src/html/templates/common.js index 6642f77a..ddbd17d0 100644 --- a/src/html/templates/common.js +++ b/src/html/templates/common.js @@ -10,4 +10,11 @@ function edit(file) { document.body.appendChild(d); d.submit(); +} + +// Shared utility function for HTML escaping +function escapeHtml(text) { + const div = document.createElement('div'); + div.textContent = text; + return div.innerHTML; } \ No newline at end of file diff --git a/src/html/templates/songbook_edit/songbook_edit.css b/src/html/templates/songbook_edit/songbook_edit.css new file mode 100644 index 00000000..501712e3 --- /dev/null +++ b/src/html/templates/songbook_edit/songbook_edit.css @@ -0,0 +1,666 @@ +* { + box-sizing: border-box; +} + +body { + font-family: 'Roboto', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; + margin: 0; + padding: 0; + background: #f8f9fa; + color: #212529; +} + +.container { + max-width: 1400px; + margin: 0 auto; + padding: 20px; +} + +header { + background: white; + padding: 20px; + margin-bottom: 20px; + border-radius: 8px; + box-shadow: 0 2px 4px rgba(0,0,0,0.1); +} + +h1 { + margin: 0 0 10px 0; + color: #212529; + font-size: 2em; +} + +.subtitle { + color: #6c757d; + margin: 0; +} + +.songbook-description { + margin-bottom: 20px; +} + +.song-selection-header { + background: white; + padding: 15px 20px; + margin-bottom: 20px; + border-radius: 8px; + box-shadow: 0 2px 4px rgba(0,0,0,0.1); +} + +.song-selection-header h2 { + margin: 0; + color: #343a40; + font-size: 1.5em; +} + +.layout { + display: grid; + grid-template-columns: 1fr auto 1fr; + gap: 20px; + align-items: start; +} + +.layout .panel { + border-radius: 8px; +} + +.layout .panel h3 { + margin-top: 0; + color: #343a40; + border-bottom: 2px solid #dee2e6; + padding-bottom: 10px; + font-size: 1.3em; +} + +.arrow-separator { + width: 30px; + height: 100%; + position: relative; + display: flex; + align-items: start; + justify-content: center; + padding-top: 80px; +} + +.arrow-separator::before { + content: ''; + position: absolute; + width: 0; + height: 0; + border-top: 15px solid transparent; + border-bottom: 15px solid transparent; + border-left: 20px solid #007bff; +} + +@media (max-width: 968px) { + .layout { + grid-template-columns: 1fr; + } + + .arrow-separator { + width: 100%; + height: 30px; + transform: rotate(90deg); + } +} + +.panel { + background: white; + padding: 20px; + border-radius: 8px; + box-shadow: 0 2px 4px rgba(0,0,0,0.1); +} + +.panel h2 { + margin-top: 0; + color: #343a40; + border-bottom: 2px solid #dee2e6; + padding-bottom: 10px; +} + +.search-box { + width: 100%; + padding: 12px; + font-size: 16px; + border: 2px solid #dee2e6; + border-radius: 4px; + margin-bottom: 15px; + transition: border-color 0.2s; +} + +.search-box:focus { + outline: none; + border-color: #007bff; +} + +.filter-section { + margin-bottom: 15px; +} + +.filter-section label { + display: block; + margin-bottom: 5px; + font-weight: 500; + color: #495057; +} + +.filter-section select { + width: 100%; + padding: 8px; + border: 1px solid #dee2e6; + border-radius: 4px; + font-size: 14px; +} + +.stats { + background: #e9ecef; + padding: 10px; + border-radius: 4px; + margin-bottom: 15px; + font-size: 14px; +} + +.song-list { + max-height: 600px; + overflow-y: auto; + border: 1px solid #dee2e6; + border-radius: 4px; +} + +.song-item { + padding: 10px 15px; + border-bottom: 1px solid #e9ecef; + cursor: pointer; + display: flex; + align-items: center; + gap: 10px; + transition: background-color 0.2s; +} + +.song-item:hover { + background-color: #f8f9fa; +} + +.song-item:last-child { + border-bottom: none; +} + +.song-item.selected { + background-color: #e7f3ff; +} + +.song-item input[type="checkbox"] { + width: 18px; + height: 18px; + cursor: pointer; +} + +.song-info { + flex: 1; +} + +.song-title { + font-weight: 500; + color: #212529; +} + +.song-title a { + color: inherit; + text-decoration: none; + border-bottom: 1px solid transparent; + transition: all 0.2s; +} + +.song-title a:hover { + color: #0056b3; + border-bottom-color: #0056b3; +} + +.song-title a::after { + content: '↗'; + font-size: 0.8em; + margin-left: 4px; + opacity: 0.6; +} + +.song-meta { + font-size: 12px; + color: #6c757d; + margin-top: 2px; +} + +/* Aggregated items (artists, text_authors, genres) */ +.aggregated-item { + padding: 12px 15px; + border-bottom: 1px solid #e9ecef; + display: flex; + align-items: center; + gap: 12px; + transition: background-color 0.2s; + background: linear-gradient(to right, #f8f9fa 0%, #ffffff 100%); + border-left: 4px solid transparent; +} + +.aggregated-item:hover { + background: linear-gradient(to right, #e9ecef 0%, #f8f9fa 100%); +} + +.aggregated-item input[type="checkbox"] { + width: 18px; + height: 18px; + cursor: pointer; + flex-shrink: 0; +} + +.aggregated-item.artist-item { + border-left-color: #007bff; +} + +.aggregated-item.artist-item:hover { + background: linear-gradient(to right, #cfe2ff 0%, #e7f1ff 100%); +} + +.aggregated-item.text-author-item { + border-left-color: #28a745; +} + +.aggregated-item.text-author-item:hover { + background: linear-gradient(to right, #d4edda 0%, #e9f7ec 100%); +} + +.aggregated-item.genre-item { + border-left-color: #ffc107; +} + +.aggregated-item.genre-item:hover { + background: linear-gradient(to right, #fff3cd 0%, #fff9e6 100%); +} + +.aggregated-item .material-symbols-outlined { + font-size: 24px; + color: #6c757d; +} + +.aggregated-item .item-info { + flex: 1; +} + +.aggregated-item .item-title { + font-weight: 600; + color: #212529; + font-size: 14px; +} + +.aggregated-item .item-meta { + font-size: 11px; + color: #6c757d; + margin-top: 2px; + font-style: italic; +} + +.form-group { + margin-bottom: 15px; +} + +.form-group label { + display: block; + margin-bottom: 5px; + font-weight: 500; + color: #495057; +} + +.form-group input, +.form-group textarea { + width: 100%; + padding: 8px; + border: 1px solid #dee2e6; + border-radius: 4px; + font-size: 14px; + font-family: inherit; +} + +.form-group textarea { + resize: vertical; + min-height: 60px; +} + +/* Dynamic Filters */ +#dynamicFiltersList { + margin-top: 10px; +} + +.filter-item { + background: #f8f9fa; + border: 1px solid #dee2e6; + border-radius: 4px; + padding: 10px; + margin-bottom: 10px; +} + +.filter-controls { + display: flex; + gap: 8px; + margin-bottom: 5px; + flex-wrap: wrap; +} + +.filter-controls select { + padding: 6px; + border: 1px solid #dee2e6; + border-radius: 4px; + font-size: 13px; + background: white; +} + +.filter-controls input[type="text"] { + flex: 1; + min-width: 150px; + padding: 6px; + border: 1px solid #dee2e6; + border-radius: 4px; + font-size: 13px; +} + +.filter-match-count { + font-size: 12px; + color: #28a745; + font-weight: 500; + margin-top: 5px; +} + +.selected-songs { + margin-top: 15px; + max-height: 600px; + overflow-y: auto; +} + +.selection-summary { + background: #e7f3ff; + padding: 10px; + border-radius: 4px; + margin-bottom: 10px; + font-size: 13px; +} + +.filter-summary { + padding: 4px 0; + color: #495057; +} + +.final-song-list { + margin-top: 10px; + border: 1px solid #dee2e6; + border-radius: 4px; + background: white; +} + +.final-song-list summary { + padding: 10px; + cursor: pointer; + background: #f8f9fa; + user-select: none; +} + +.final-song-list summary:hover { + background: #e9ecef; +} + +.final-songs-container { + max-height: 400px; + overflow-y: auto; + padding: 5px; +} + +.final-song-item { + padding: 6px 10px; + border-bottom: 1px solid #e9ecef; + display: flex; + align-items: center; + gap: 8px; + font-size: 13px; +} + +.final-song-item:last-child { + border-bottom: none; +} + +.final-song-item .song-number { + color: #6c757d; + font-weight: 500; + min-width: 30px; +} + +.final-song-item .song-title { + flex: 1; + color: #212529; +} + +.final-song-item .song-title a { + color: inherit; + text-decoration: none; + border-bottom: 1px solid transparent; + transition: all 0.2s; +} + +.final-song-item .song-title a:hover { + color: #0056b3; + border-bottom-color: #0056b3; +} + +.final-song-item .song-title a::after { + content: '↗'; + font-size: 0.8em; + margin-left: 4px; + opacity: 0.6; +} + +.final-song-item .remove-song-btn { + background: #dc3545; + color: white; + border: none; + padding: 4px 12px; + border-radius: 4px; + cursor: pointer; + font-size: 12px; + font-weight: 500; +} + +.final-song-item .remove-song-btn:hover { + background: #c82333; +} + +.final-song-item .filter-badge { + color: #6c757d; + font-size: 12px; + font-style: italic; +} + +.selected-song { + background: #f8f9fa; + padding: 8px 12px; + margin-bottom: 5px; + border-radius: 4px; + display: flex; + justify-content: space-between; + align-items: center; +} + +.remove-btn { + background: #dc3545; + color: white; + border: none; + padding: 4px 8px; + border-radius: 3px; + cursor: pointer; + font-size: 12px; +} + +.remove-btn:hover { + background: #c82333; +} + +.action-buttons { + display: flex; + gap: 10px; + margin-top: 20px; +} + +.btn { + padding: 10px 20px; + border: none; + border-radius: 4px; + font-size: 14px; + cursor: pointer; + transition: background-color 0.2s; + font-weight: 500; +} + +.btn-primary { + background: #007bff; + color: white; + flex: 1; +} + +.btn-primary:hover { + background: #0056b3; +} + +.btn-secondary { + background: #6c757d; + color: white; + flex: 1; +} + +.btn-secondary:hover { + background: #5a6268; +} + +.output-area { + margin-top: 15px; +} + +.output-area textarea { + width: 100%; + min-height: 300px; + font-family: 'Courier New', monospace; + font-size: 12px; + padding: 10px; + border: 1px solid #dee2e6; + border-radius: 4px; +} + +.batch-actions { + display: flex; + gap: 10px; + margin-bottom: 15px; +} + +.batch-actions button { + padding: 6px 12px; + font-size: 13px; +} + +.empty-state { + text-align: center; + padding: 40px 20px; + color: #6c757d; +} + +.loader { + text-align: center; + padding: 40px; + color: #6c757d; +} + +.file-upload { + background: #fff3cd; + border: 2px dashed #ffc107; + border-radius: 4px; + padding: 20px; + margin-bottom: 20px; + text-align: center; +} + +.file-upload input[type="file"] { + display: none; +} + +.file-upload label { + display: inline-block; + padding: 10px 20px; + background: #ffc107; + color: #212529; + border-radius: 4px; + cursor: pointer; + font-weight: 500; +} + +.file-upload label:hover { + background: #e0a800; +} + +.dynamic-filters { + margin-top: 20px; + padding-top: 15px; + border-top: 2px solid #dee2e6; +} + +.dynamic-filters h3 { + font-size: 1em; + margin: 0 0 10px 0; + color: #495057; +} + +.filter-item { + background: #f8f9fa; + padding: 10px; + margin-bottom: 10px; + border-radius: 4px; + border: 1px solid #dee2e6; +} + +.filter-item-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 8px; +} + +.filter-item select, +.filter-item input { + width: 100%; + padding: 6px; + border: 1px solid #dee2e6; + border-radius: 4px; + font-size: 13px; + margin-bottom: 5px; +} + +.filter-remove-btn { + background: #dc3545; + color: white; + border: none; + padding: 3px 8px; + border-radius: 3px; + cursor: pointer; + font-size: 11px; +} + +.filter-remove-btn:hover { + background: #c82333; +} + +.add-filter-btn { + background: #28a745; + color: white; + border: none; + padding: 8px 16px; + border-radius: 4px; + cursor: pointer; + font-size: 13px; + width: 100%; +} + +.add-filter-btn:hover { + background: #218838; +} diff --git a/src/html/templates/songbook_edit/songbook_edit.html b/src/html/templates/songbook_edit/songbook_edit.html new file mode 100644 index 00000000..713de0bb --- /dev/null +++ b/src/html/templates/songbook_edit/songbook_edit.html @@ -0,0 +1,129 @@ + + + + + + Kreator Śpiewnika - Spiewaj.com + + + + +
+
+

Kreator Śpiewnika

+

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

+
+ + +
+

Opis Śpiewnika

+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+ + +
+

Wybierz ulubione piosenki

+
+ +
+ +
+

Dostępne Piosenki

+ + + + + +
+ Wyświetlone: 0 / 0 +
+ +
+ + +
+ +
+
Ładowanie piosenek...
+
+
+ + +
+ + +
+

Wybrane piosenki

+ +
+ Liczba wybranych: 0 +
+ +
+
Nie wybrano jeszcze żadnych piosenek
+
+ +
+ + +
+ + +
+
+
+ + + + + diff --git a/src/html/templates/songbook_edit/songbook_edit.js b/src/html/templates/songbook_edit/songbook_edit.js new file mode 100644 index 00000000..20f2ad6c --- /dev/null +++ b/src/html/templates/songbook_edit/songbook_edit.js @@ -0,0 +1,651 @@ +let allSongs = []; +let selectedSongIds = new Set(); +let songById = new Map(); +let dynamicFilters = []; +let filterCounter = 0; +let activeAggregatedFilters = new Map(); // Track which aggregated filters are active + +// Load index.json on page load +async function loadSongs() { + try { + const response = await fetch('index.json'); + const data = await response.json(); + allSongs = data.songs; + + // Create song lookup map + allSongs.forEach(song => { + songById.set(song.id, song); + }); + + populateFilters(); + renderSongList(); + updateStats(); + } catch (error) { + console.error('Failed to load index.json:', error); + document.getElementById('songList').innerHTML = + '
Nie można załadować piosenek automatycznie.
Użyj przycisku powyżej, aby załadować plik index.json ręcznie.
'; + document.getElementById('fileUploadSection').style.display = 'block'; + } +} + +function loadJSONFile(event) { + const file = event.target.files[0]; + if (!file) return; + + const reader = new FileReader(); + reader.onload = function(e) { + try { + const data = JSON.parse(e.target.result); + allSongs = data.songs; + + // Create song lookup map + allSongs.forEach(song => { + songById.set(song.id, song); + }); + + document.getElementById('fileUploadSection').style.display = 'none'; + populateFilters(); + renderSongList(); + updateStats(); + } catch (error) { + alert('Błąd podczas parsowania pliku JSON: ' + error.message); + } + }; + reader.readAsText(file); +} + +function populateFilters() { + // No longer needed - we'll show aggregated results in the search list +} + +function getFilteredItems() { + const searchTerm = document.getElementById('searchBox').value.toLowerCase().trim(); + + if (!searchTerm) { + // Return all songs when no search term + return allSongs.map(song => ({ + type: 'song', + data: song + })); + } + + // Collect matching items + const results = []; + const matchedArtists = new Set(); + const matchedTextAuthors = new Set(); + const matchedGenres = new Set(); + + // Find matching songs and collect their metadata + allSongs.forEach(song => { + const searchableText = [ + song.title, + song.artist || '', + song.text_author || '', + song.genre || '', + ...(song.aliases || []) + ].join(' ').toLowerCase(); + + if (searchableText.includes(searchTerm)) { + results.push({ + type: 'song', + data: song + }); + + // Collect metadata for aggregation + if (song.artist && song.artist.toLowerCase().includes(searchTerm)) { + matchedArtists.add(song.artist); + } + if (song.text_author && song.text_author.toLowerCase().includes(searchTerm)) { + matchedTextAuthors.add(song.text_author); + } + if (song.genre && song.genre.toLowerCase().includes(searchTerm)) { + matchedGenres.add(song.genre); + } + } + }); + + // Add aggregated items at the top + const aggregatedItems = []; + + Array.from(matchedArtists).sort().forEach(artist => { + aggregatedItems.push({ + type: 'artist', + data: { name: artist } + }); + }); + + Array.from(matchedTextAuthors).sort().forEach(author => { + aggregatedItems.push({ + type: 'text_author', + data: { name: author } + }); + }); + + Array.from(matchedGenres).sort().forEach(genre => { + aggregatedItems.push({ + type: 'genre', + data: { name: genre } + }); + }); + + return [...aggregatedItems, ...results]; +} + +function renderSongList() { + const songList = document.getElementById('songList'); + const items = getFilteredItems(); + + if (items.length === 0) { + songList.innerHTML = '
Nie znaleziono wyników
'; + return; + } + + songList.innerHTML = items.map(item => { + if (item.type === 'song') { + const song = item.data; + const isSelected = selectedSongIds.has(song.id); + const meta = []; + if (song.artist) meta.push(song.artist); + if (song.genre) meta.push(song.genre); + + const aliasText = song.aliases && song.aliases.length > 0 + ? ` (${escapeHtml(song.aliases.join(', '))})` + : ''; + + return ` +
+ +
+
+ ${escapeHtml(song.title)}${aliasText} +
+ ${meta.length > 0 ? `
${escapeHtml(meta.join(' • '))}
` : ''} +
+
+ `; + } else if (item.type === 'artist') { + return renderAggregatedItem('artist', item.data.name); + } else if (item.type === 'text_author') { + return renderAggregatedItem('text_author', item.data.name); + } else if (item.type === 'genre') { + return renderAggregatedItem('genre', item.data.name); + } + }).join(''); + + updateStats(); +} + +// Helper function to render aggregated filter items +function renderAggregatedItem(type, name) { + const filterKey = `${type}:${name}`; + const isChecked = activeAggregatedFilters.has(filterKey); + + const config = { + artist: { icon: 'person', label: 'Wykonawca' }, + text_author: { icon: 'edit_note', label: 'Autor tekstu' }, + genre: { icon: 'category', label: 'Gatunek' } + }; + + const { icon, label } = config[type]; + + return ` +
+ + ${icon} +
+
${label}: ${escapeHtml(name)}
+
Zaznacz aby dodać filtr "Wszystkie pasujące"
+
+
+ `; +} + +function toggleAggregatedFilterByData(checkbox) { + const type = checkbox.dataset.filterType; + const value = checkbox.dataset.filterValue; + const checked = checkbox.checked; + toggleAggregatedFilter(type, value, checked); +} + +function toggleAggregatedFilter(type, value, checked) { + const filterKey = `${type}:${value}`; + + if (checked) { + // Add filter + activeAggregatedFilters.set(filterKey, { type, value }); + addDynamicFilter(type, value); + } else { + // Remove filter + activeAggregatedFilters.delete(filterKey); + // Find and remove the matching filter + const filterToRemove = dynamicFilters.find(f => f.type === type && f.value === value); + if (filterToRemove) { + removeDynamicFilter(filterToRemove.id); + } + } + + renderSongList(); + renderSelectedSongs(); +} + +function selectByArtist(artist) { + addDynamicFilter('artist', artist); +} + +function selectByTextAuthor(author) { + addDynamicFilter('text_author', author); +} + +function selectByGenre(genre) { + addDynamicFilter('genre', genre); +} + +function toggleSong(songId) { + if (selectedSongIds.has(songId)) { + selectedSongIds.delete(songId); + } else { + selectedSongIds.add(songId); + } + renderSongList(); + renderSelectedSongs(); +} + +function renderSelectedSongs() { + const container = document.getElementById('selectedSongs'); + const selectedCount = document.getElementById('selectedCount'); + + // Calculate all songs that will be included (filters + individual selections) + const allIncludedSongs = getAllIncludedSongs(); + + selectedCount.textContent = allIncludedSongs.size; + + if (allIncludedSongs.size === 0 && dynamicFilters.length === 0) { + container.innerHTML = '
Nie wybrano jeszcze żadnych piosenek
'; + return; + } + + // Convert to array and sort alphabetically by title + const songsArray = Array.from(allIncludedSongs) + .map(id => songById.get(id)) + .filter(song => song) // Remove any undefined + .sort((a, b) => a.title.localeCompare(b.title, 'pl')); + + // Show summary + let html = ''; + + if (dynamicFilters.length > 0) { + html += '
'; + html += 'Filtry:
'; + dynamicFilters.forEach(filter => { + const matchCount = getMatchingsongsForFilter(filter).length; + const typeLabels = { + 'genre': 'Gatunek', + 'artist': 'Wykonawca', + 'text_author': 'Autor tekstu' + }; + html += `
+ ${typeLabels[filter.type]}: ${escapeHtml(filter.value)} (${matchCount}) + +
`; + }); + html += '
'; + } + + html += '
'; + html += `Razem (po deduplikacji): ${allIncludedSongs.size} ${allIncludedSongs.size === 1 ? 'piosenka' : allIncludedSongs.size < 5 ? 'piosenki' : 'piosenek'}`; + html += '
'; + + // Show expandable list + html += '
'; + html += 'Finalna lista piosenek'; + html += '
'; + html += songsArray.map(song => { + const isExplicit = selectedSongIds.has(song.id); + const matchingFilters = dynamicFilters.filter(filter => { + return getMatchingsongsForFilter(filter).some(s => s.id === song.id); + }); + + const typeLabels = { + 'genre': 'Gatunek', + 'artist': 'Wykonawca', + 'text_author': 'Autor tekstu' + }; + + let badge = ''; + if (isExplicit) { + badge = ``; + } else if (matchingFilters.length > 0) { + const filterDescriptions = matchingFilters.map(f => + `${typeLabels[f.type]}: ${escapeHtml(f.value)}` + ).join(', '); + badge = `Wybrany przez filtr dynamiczny: ${filterDescriptions}`; + } + + const aliasText = song.aliases && song.aliases.length > 0 + ? ` (${escapeHtml(song.aliases.join(', '))})` + : ''; + + return ` +
+ ${songsArray.indexOf(song) + 1}. + + ${escapeHtml(song.title)}${aliasText} + + ${badge} +
+ `; + }).join(''); + html += '
'; + html += '
'; + + container.innerHTML = html; +} + +function getAllIncludedSongs() { + const includedSongs = new Set(); + + // Add songs from dynamic filters + dynamicFilters.forEach(filter => { + const matchingSongs = getMatchingsongsForFilter(filter); + matchingSongs.forEach(song => includedSongs.add(song.id)); + }); + + // Add individually selected songs + selectedSongIds.forEach(id => includedSongs.add(id)); + + return includedSongs; +} + +function removeSong(songId) { + selectedSongIds.delete(songId); + renderSongList(); + renderSelectedSongs(); +} + +function selectAllVisible() { + const items = getFilteredItems(); + items.forEach(item => { + if (item.type === 'song') { + selectedSongIds.add(item.data.id); + } + }); + renderSongList(); + renderSelectedSongs(); +} + +function deselectAllVisible() { + const items = getFilteredItems(); + items.forEach(item => { + if (item.type === 'song') { + selectedSongIds.delete(item.data.id); + } + }); + renderSongList(); + renderSelectedSongs(); +} + +function clearSelection() { + if (confirm('Czy na pewno chcesz wyczyścić wszystkie wybrane piosenki?')) { + selectedSongIds.clear(); + renderSongList(); + renderSelectedSongs(); + } +} + +function updateStats() { + const items = getFilteredItems(); + const songCount = items.filter(item => item.type === 'song').length; + const totalItems = items.length; + + if (songCount === totalItems) { + document.getElementById('visibleCount').textContent = `${songCount} piosenek`; + } else { + const otherCount = totalItems - songCount; + document.getElementById('visibleCount').textContent = `${songCount} piosenek, ${otherCount} grup`; + } + + document.getElementById('totalCount').textContent = `${allSongs.length} piosenek`; +} + +// Dynamic Filters +function addDynamicFilter(type = 'genre', value = '') { + const filterId = filterCounter++; + dynamicFilters.push({ + id: filterId, + type: type, + value: value + }); + renderDynamicFilters(); + renderSongList(); +} + +function removeDynamicFilter(filterId) { + const filter = dynamicFilters.find(f => f.id === filterId); + if (filter) { + // Remove from activeAggregatedFilters if it exists + const filterKey = `${filter.type}:${filter.value}`; + activeAggregatedFilters.delete(filterKey); + } + dynamicFilters = dynamicFilters.filter(f => f.id !== filterId); + renderDynamicFilters(); + renderSongList(); + renderSelectedSongs(); +} + +function updateDynamicFilter(filterId, field, value) { + const filter = dynamicFilters.find(f => f.id === filterId); + if (filter) { + filter[field] = value; + renderDynamicFilters(); + } +} + +function getMatchingsongsForFilter(filter) { + return allSongs.filter(song => { + if (filter.type === 'genre') { + return song.genre === filter.value; + } else if (filter.type === 'artist') { + return song.artist === filter.value; + } else if (filter.type === 'text_author') { + return song.text_author === filter.value; + } + return false; + }); +} + +function renderDynamicFilters() { + const container = document.getElementById('dynamicFiltersList'); + + // If container doesn't exist, filters are shown in selectedSongs panel instead + if (!container) { + return; + } + + if (dynamicFilters.length === 0) { + container.innerHTML = ''; + return; + } + + container.innerHTML = dynamicFilters.map(filter => { + const matchingCount = getMatchingsongsForFilter(filter).length; + const typeLabels = { + 'genre': 'Gatunek', + 'artist': 'Wykonawca', + 'text_author': 'Autor tekstu' + }; + + return ` +
+
+ + + +
+
+ Dopasowane: ${matchingCount} ${matchingCount === 1 ? 'piosenka' : matchingCount < 5 ? 'piosenki' : 'piosenek'} +
+
+ `}).join(''); +} + +function generateYAML() { + const songbookId = document.getElementById('songbookId').value.trim(); + const songbookTitle = document.getElementById('songbookTitle').value.trim(); + const songbookSubtitle = document.getElementById('songbookSubtitle').value.trim(); + const songbookPublisher = document.getElementById('songbookPublisher').value.trim(); + const songbookPlace = document.getElementById('songbookPlace').value.trim(); + + if (!songbookId) { + alert('Proszę podać ID śpiewnika'); + return; + } + + if (!songbookTitle) { + alert('Proszę podać tytuł śpiewnika'); + return; + } + + if (selectedSongIds.size === 0 && dynamicFilters.length === 0) { + alert('Proszę wybrać przynajmniej jedną piosenkę lub dodać filtr'); + return; + } + + // Generate UUID + const uuid = generateUUID(); + + // Build songbook data structure + const songbook = { + id: songbookId, + uuid: uuid, + title: songbookTitle, + url: `https://spiewaj.com/#${songbookId}`, + songs: [] + }; + + if (songbookSubtitle) { + songbook.subtitle = songbookSubtitle; + } + + if (songbookPublisher) { + songbook.publisher = songbookPublisher; + } + + if (songbookPlace) { + songbook.place = songbookPlace; + } + + // Add dynamic filters first + dynamicFilters.forEach(filter => { + if (filter.value) { + const filterObj = {}; + filterObj[filter.type] = { equals: filter.value }; + songbook.songs.push(filterObj); + } + }); + + // Add individual selected songs as glob patterns + Array.from(selectedSongIds).forEach(id => { + songbook.songs.push({ glob: `songs/**/${id}.xml` }); + }); + + const yaml = JSON.stringify({ songbook }, null, 2); + document.getElementById('yamlOutput').value = yaml; + document.getElementById('outputArea').style.display = 'block'; +} + +function generateUUID() { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { + const r = Math.random() * 16 | 0; + const v = c === 'x' ? r : (r & 0x3 | 0x8); + return v.toString(16); + }); +} + +function escapeYAML(text) { + if (text.includes('"') || text.includes('\n') || text.includes(':')) { + return text.replace(/"/g, '\\"'); + } + return text; +} + +function downloadYAML() { + const yaml = document.getElementById('yamlOutput').value; + const songbookId = document.getElementById('songbookId').value.trim(); + 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 copyToClipboard() { + const textarea = document.getElementById('yamlOutput'); + textarea.select(); + document.execCommand('copy'); + alert('Skopiowano do schowka!'); +} + +// Auto-generate songbook ID from title +let userEditedId = false; // Track if user manually edited the ID + +function generateIdFromTitle(title) { + return title + .toLowerCase() + .normalize('NFD').replace(/[\u0300-\u036f]/g, '') // Remove diacritics + .replace(/ą/g, 'a') + .replace(/ć/g, 'c') + .replace(/ę/g, 'e') + .replace(/ł/g, 'l') + .replace(/ń/g, 'n') + .replace(/ó/g, 'o') + .replace(/ś/g, 's') + .replace(/ź/g, 'z') + .replace(/ż/g, 'z') + .replace(/[^a-z0-9]+/g, '_') // Replace non-alphanumeric with underscore + .replace(/^_+|_+$/g, '') // Remove leading/trailing underscores + .replace(/_+/g, '_'); // Replace multiple underscores with single +} + +// Event listeners +document.getElementById('searchBox').addEventListener('input', renderSongList); + +document.getElementById('songbookTitle').addEventListener('input', function() { + if (!userEditedId) { + const id = generateIdFromTitle(this.value); + document.getElementById('songbookId').value = id; + } +}); + +document.getElementById('songbookId').addEventListener('input', function() { + // If user manually edits the ID, stop auto-generation + userEditedId = true; +}); + +document.getElementById('songbookId').addEventListener('focus', function() { + // When user focuses on ID field, assume they want to edit it manually + userEditedId = true; +}); + +// Initialize +loadSongs();