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
+
+
+
+
+
+
+
+
+
+
Opis Śpiewnika
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Dostępne Piosenki
+
+
+
Nie można automatycznie załadować index.json (CORS). Załaduj plik ręcznie:
+
+
+
+
+
+
+
+ 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 `
+
+
+
+
+ ${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 `
+
+ `;
+ }).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();