Loading karaokatalog/ui/static/components/pieces/ClearSearchLink.js 0 → 100644 +27 −0 Changes for karaokatalog/ui/static/components/pieces/ClearSearchLink.js: 27 added lines, 0 removed lines. Original line number Diff line number Diff line /* TODO: TAINT: AI-generated. This file was AI-generated (Gemini and/or Cursor), and I haven't manually reviewed it yet. I should do this at some point in the future and change things I don't like. */ import search from "../../model/search.js" export default { view: function (vnode) { return ["Möchtest du ", m( "a.clear-search-link", { onclick: e => { e.preventDefault() search.query = null }, href: "#", role: "button", tabindex: 0, onkeypress: e => { if ((e.key === "Enter" || e.key === " ")) { e.preventDefault() search.query = null } }, }, `alle ${vnode.attrs.totalFavoriteCount} Favoriten anzeigen`, ), "?"] }, } karaokatalog/ui/static/components/pieces/PaginationControls.js 0 → 100644 +122 −0 Changes for karaokatalog/ui/static/components/pieces/PaginationControls.js: 122 added lines, 0 removed lines. Original line number Diff line number Diff line /* TODO: TAINT: AI-generated. This file was AI-generated (Gemini and/or Cursor), and I haven't manually reviewed it yet. I should do this at some point in the future and change things I don't like. */ const PaginationControls = { view: function (vnode) { const { currentPage, totalPages, onPageChange } = vnode.attrs if (totalPages <= 1) { return null // Keine Steuerelemente anzeigen, wenn nicht benötigt } const pages = [] const delta = 2 // Anzahl der Seiten, die um die aktuelle Seite herum angezeigt werden // Hilfsfunktion zum Hinzufügen einer Seitenzahl const addPage = pageNumber => { pages.push( m( "span.page-number", { class: pageNumber === currentPage ? "active" : "clickable", onclick: () => pageNumber !== currentPage ? onPageChange(pageNumber) : null, role: "button", tabindex: 0, // Für Tastaturnavigation onkeypress: e => { // Für Tastaturnavigation (Enter/Space) if ( (e.key === "Enter" || e.key === " ") && pageNumber !== currentPage ) { e.preventDefault() onPageChange(pageNumber) } }, "aria-label": `Gehe zu Seite ${pageNumber}`, "aria-current": pageNumber === currentPage ? "page" : undefined, }, pageNumber, ), ) } // "Zurück"-Button const prevButton = m( "button.pagination-button", { onclick: () => onPageChange(currentPage - 1), disabled: currentPage === 1, "aria-label": "Vorherige Seite", }, "Zurück", ) // "Weiter"-Button const nextButton = m( "button.pagination-button", { onclick: () => onPageChange(currentPage + 1), disabled: currentPage === totalPages, "aria-label": "Nächste Seite", }, "Weiter", ) // Logik zur Seitenzahlanzeige if (totalPages <= 7) { // Alle Seiten anzeigen, wenn es 7 oder weniger sind for (let i = 1; i <= totalPages; i++) { addPage(i) } } else { addPage(1) // Immer die erste Seite anzeigen if (currentPage > delta + 2) { // Auslassungspunkte anzeigen, wenn die aktuelle Seite weit vom Anfang entfernt ist pages.push(m("span.dots", "...")) } // Bereich der Seiten um die aktuelle Seite bestimmen let start = Math.max(2, currentPage - delta) let end = Math.min(totalPages - 1, currentPage + delta) if (currentPage <= delta + 1) { // Aktuelle Seite ist nahe am Anfang start = 2 end = Math.min(totalPages - 1, 2 * delta + 1) } else if (currentPage >= totalPages - delta) { // Aktuelle Seite ist nahe am Ende start = Math.max(2, totalPages - 2 * delta) end = totalPages - 1 } // Korrektur für den Fall, dass start und end sich überschneiden oder ungültig werden if (start > end && currentPage < totalPages / 2) { // Wenn start > end und wir sind eher am Anfang end = start // Zeige zumindest die Startseite } else if (start > end && currentPage > totalPages / 2) { // Wenn start > end und wir sind eher am Ende start = end // Zeige zumindest die Endseite } for (let i = start; i <= end; i++) { if (i > 0) addPage(i) // Sicherstellen, dass i positiv ist } if (currentPage < totalPages - delta - 1 && end < totalPages - 1) { // Auslassungspunkte anzeigen, wenn die aktuelle Seite weit vom Ende entfernt ist pages.push(m("span.dots", "...")) } if (totalPages > 1) addPage(totalPages) // Immer die letzte Seite anzeigen (wenn nicht 1) } return m("div.pagination-controls", [prevButton, ...pages, nextButton]) }, } export default PaginationControls karaokatalog/ui/static/components/pieces/Tab.js 0 → 100644 +16 −0 Changes for karaokatalog/ui/static/components/pieces/Tab.js: 16 added lines, 0 removed lines. Original line number Diff line number Diff line /* TODO: TAINT: AI-generated. This file was AI-generated (Gemini and/or Cursor), and I haven't manually reviewed it yet. I should do this at some point in the future and change things I don't like. */ export const Tab = { view(vnode) { return m( m.route.Link, { class: m.route.get() === vnode.attrs.href ? "active" : "", href: vnode.attrs.href, role: "button", // Für Barrierefreiheit tabindex: 0, // Für Tastaturnavigation }, vnode.attrs.label, ) }, } karaokatalog/ui/static/style.css 0 → 100644 +275 −0 Changes for karaokatalog/ui/static/style.css: 275 added lines, 0 removed lines. Original line number Diff line number Diff line /* TODO: TAINT: AI-generated. This file was AI-generated (Gemini and/or Cursor), and I haven't manually reviewed it yet. I should do this at some point in the future and change things I don't like. */ /* Globale Resets und Basis-Styling (wie vorher) */ body, html { margin: 0; padding: 0; font-family: "Roboto", sans-serif; background-color: #f4f4f9; color: #333; line-height: 1.6; } main > p { text-align: center; padding: 1em; font-size: 1.1em; color: #757575; } /* Suchleiste (Höhe ca. 60px) */ search { position: fixed; top: 0; left: 0; right: 0; background-color: #3f51b5; /* Indigo - Material Primary */ padding: 12px 16px; z-index: 1000; box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); box-sizing: border-box; /* Stellt sicher, dass Padding die Höhe nicht sprengt */ height: 60px; /* Feste Höhe für genaue Positionierung */ } input[type="search"] { width: 100%; padding: 10px 12px; border-radius: 4px; border: none; font-size: 16px; box-sizing: border-box; background-color: #fff; color: #333; } input[type="search"]::placeholder { color: #888; } /* Tab Bar (Höhe ca. 48px) */ nav { display: flex; position: fixed; top: 60px; /* Direkt unter der Suchleiste */ left: 0; right: 0; background-color: #3f51b5; /* Gleiche Farbe wie Suchleiste für einen einheitlichen Header-Block */ z-index: 999; /* Unter der Suchleiste, falls sie überlappen könnten */ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15); /* Dezenter Schatten */ height: 48px; /* Feste Höhe */ box-sizing: border-box; } nav a { flex-grow: 1; display: flex; /* Für vertikales Zentrieren des Texts */ align-items: center; justify-content: center; padding: 0 8px; /* Horizontal Padding */ text-align: center; color: rgba( 255, 255, 255, 0.75 ); /* Etwas helleres, aber gedämpftes Weiß für inaktive Tabs */ cursor: pointer; font-weight: 500; font-size: 0.9em; /* Leicht kleiner als Haupttext */ text-transform: uppercase; border-bottom: 3px solid transparent; /* Platzhalter für den aktiven Indikator */ transition: color 0.2s ease-in-out, border-bottom-color 0.2s ease-in-out; user-select: none; text-decoration: none; } nav a:hover { background-color: rgba(255, 255, 255, 0.1); /* Leichter Hover-Effekt */ } nav a.active { color: #ffffff; /* Reines Weiß für aktiven Tab */ border-bottom-color: #ff4081; /* Material Accent Pink für den Indikator */ } /* song .Liste Container */ main { padding-top: calc(60px + 48px); /* Höhe Suchleiste + Höhe TabBar */ padding-bottom: 20px; /* Platz für Paginierung, falls am Ende */ } /* Link zum Zurücksetzen der Suche in der main > p */ main > p a.clear-search-link, main > p .clear-search-link { /* Zweite Regel für den Fall, dass Mithril das 'a' nicht direkt rendert */ color: #3f51b5; /* Material Indigo, passend zum Header */ text-decoration: underline; cursor: pointer; font-weight: 500; /* Etwas hervorheben */ background: none; /* Sicherstellen, dass keine Button-Styles übernommen werden */ border: none; padding: 0; display: inline; /* Damit es sich wie ein normaler Link im Text verhält */ } main > p a.clear-search-link:hover, main > p a.clear-search-link:focus, main > p .clear-search-link:hover, main > p .clear-search-link:focus { color: #303f9f; /* Dunkleres Indigo für Hover/Focus */ text-decoration: none; /* Optional: Unterstreichung bei Hover entfernen */ } /* Restliche Styles für Artist-Group, song .Item etc. bleiben wie zuvor */ section.artist { margin: 12px 8px; background-color: #ffffff; border-radius: 4px; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); overflow: hidden; } section.artist > h2 { margin: 0; padding: 12px 16px; background-color: #e8eaf6; color: #303f9f; font-size: 1.1em; font-weight: 500; border-bottom: 1px solid #c5cae9; } .song { display: flex; justify-content: space-between; align-items: center; padding: 12px 16px; border-bottom: 1px solid #f0f0f0; transition: background-color 0.2s ease-in-out; } .song:last-child { border-bottom: none; } .song:hover { background-color: #f9f9f9; } .song .info { flex-grow: 1; margin-right: 8px; /* Abstand zum Icon */ overflow: hidden; /* Verhindert Textüberlauf */ } .song .title { font-size: 1em; font-weight: 400; color: #212121; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; /* Text abschneiden bei Überlauf */ } .song .artist { font-size: 0.85em; color: #757575; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .song .actions { margin-left: auto; flex-shrink: 0; } .heart-icon { cursor: pointer; color: #bdbdbd; font-size: 24px; user-select: none; transition: color 0.2s ease-in-out, transform 0.1s ease-out; vertical-align: middle; } .heart-icon:hover { transform: scale(1.1); } .heart-icon.is-favorite { color: #ff4081; font-variation-settings: "FILL" 1; } .material-symbols-outlined { font-variation-settings: "FILL" 0, "wght" 400, "GRAD" 0, "opsz" 24; } /* NEU: Pagination Controls Styles */ .pagination-controls { display: flex; justify-content: center; align-items: center; padding: 20px 0; /* Mehr Padding für bessere Sichtbarkeit */ margin-top: 10px; user-select: none; flex-wrap: wrap; /* Erlaubt Umbruch bei vielen Seitenzahlen auf kleinen Bildschirmen */ } .pagination-controls button.pagination-button, /* Klasse für Buttons hinzugefügt */ .pagination-controls span.page-number { margin: 4px; /* Etwas mehr Margin für Umbruch */ padding: 8px 12px; border: 1px solid #ddd; background-color: #fff; color: #3f51b5; /* Material Indigo */ cursor: pointer; border-radius: 4px; transition: background-color 0.2s, color 0.2s, border-color 0.2s; font-size: 0.9em; min-width: 36px; /* Mindestbreite für bessere Klickbarkeit */ text-align: center; box-sizing: border-box; } .pagination-controls button.pagination-button:hover:not(:disabled), .pagination-controls span.page-number.clickable:hover { background-color: #e8eaf6; /* Heller Indigo-Ton für Hover */ border-color: #c5cae9; } .pagination-controls button.pagination-button:disabled { color: #aaa; cursor: not-allowed; background-color: #f9f9f9; border-color: #eee; } .pagination-controls span.page-number.active { background-color: #3f51b5; color: #fff; border-color: #3f51b5; font-weight: bold; cursor: default; } .pagination-controls span.dots { padding: 8px 6px; color: #777; margin: 4px; font-size: 0.9em; } Loading
karaokatalog/ui/static/components/pieces/ClearSearchLink.js 0 → 100644 +27 −0 Changes for karaokatalog/ui/static/components/pieces/ClearSearchLink.js: 27 added lines, 0 removed lines. Original line number Diff line number Diff line /* TODO: TAINT: AI-generated. This file was AI-generated (Gemini and/or Cursor), and I haven't manually reviewed it yet. I should do this at some point in the future and change things I don't like. */ import search from "../../model/search.js" export default { view: function (vnode) { return ["Möchtest du ", m( "a.clear-search-link", { onclick: e => { e.preventDefault() search.query = null }, href: "#", role: "button", tabindex: 0, onkeypress: e => { if ((e.key === "Enter" || e.key === " ")) { e.preventDefault() search.query = null } }, }, `alle ${vnode.attrs.totalFavoriteCount} Favoriten anzeigen`, ), "?"] }, }
karaokatalog/ui/static/components/pieces/PaginationControls.js 0 → 100644 +122 −0 Changes for karaokatalog/ui/static/components/pieces/PaginationControls.js: 122 added lines, 0 removed lines. Original line number Diff line number Diff line /* TODO: TAINT: AI-generated. This file was AI-generated (Gemini and/or Cursor), and I haven't manually reviewed it yet. I should do this at some point in the future and change things I don't like. */ const PaginationControls = { view: function (vnode) { const { currentPage, totalPages, onPageChange } = vnode.attrs if (totalPages <= 1) { return null // Keine Steuerelemente anzeigen, wenn nicht benötigt } const pages = [] const delta = 2 // Anzahl der Seiten, die um die aktuelle Seite herum angezeigt werden // Hilfsfunktion zum Hinzufügen einer Seitenzahl const addPage = pageNumber => { pages.push( m( "span.page-number", { class: pageNumber === currentPage ? "active" : "clickable", onclick: () => pageNumber !== currentPage ? onPageChange(pageNumber) : null, role: "button", tabindex: 0, // Für Tastaturnavigation onkeypress: e => { // Für Tastaturnavigation (Enter/Space) if ( (e.key === "Enter" || e.key === " ") && pageNumber !== currentPage ) { e.preventDefault() onPageChange(pageNumber) } }, "aria-label": `Gehe zu Seite ${pageNumber}`, "aria-current": pageNumber === currentPage ? "page" : undefined, }, pageNumber, ), ) } // "Zurück"-Button const prevButton = m( "button.pagination-button", { onclick: () => onPageChange(currentPage - 1), disabled: currentPage === 1, "aria-label": "Vorherige Seite", }, "Zurück", ) // "Weiter"-Button const nextButton = m( "button.pagination-button", { onclick: () => onPageChange(currentPage + 1), disabled: currentPage === totalPages, "aria-label": "Nächste Seite", }, "Weiter", ) // Logik zur Seitenzahlanzeige if (totalPages <= 7) { // Alle Seiten anzeigen, wenn es 7 oder weniger sind for (let i = 1; i <= totalPages; i++) { addPage(i) } } else { addPage(1) // Immer die erste Seite anzeigen if (currentPage > delta + 2) { // Auslassungspunkte anzeigen, wenn die aktuelle Seite weit vom Anfang entfernt ist pages.push(m("span.dots", "...")) } // Bereich der Seiten um die aktuelle Seite bestimmen let start = Math.max(2, currentPage - delta) let end = Math.min(totalPages - 1, currentPage + delta) if (currentPage <= delta + 1) { // Aktuelle Seite ist nahe am Anfang start = 2 end = Math.min(totalPages - 1, 2 * delta + 1) } else if (currentPage >= totalPages - delta) { // Aktuelle Seite ist nahe am Ende start = Math.max(2, totalPages - 2 * delta) end = totalPages - 1 } // Korrektur für den Fall, dass start und end sich überschneiden oder ungültig werden if (start > end && currentPage < totalPages / 2) { // Wenn start > end und wir sind eher am Anfang end = start // Zeige zumindest die Startseite } else if (start > end && currentPage > totalPages / 2) { // Wenn start > end und wir sind eher am Ende start = end // Zeige zumindest die Endseite } for (let i = start; i <= end; i++) { if (i > 0) addPage(i) // Sicherstellen, dass i positiv ist } if (currentPage < totalPages - delta - 1 && end < totalPages - 1) { // Auslassungspunkte anzeigen, wenn die aktuelle Seite weit vom Ende entfernt ist pages.push(m("span.dots", "...")) } if (totalPages > 1) addPage(totalPages) // Immer die letzte Seite anzeigen (wenn nicht 1) } return m("div.pagination-controls", [prevButton, ...pages, nextButton]) }, } export default PaginationControls
karaokatalog/ui/static/components/pieces/Tab.js 0 → 100644 +16 −0 Changes for karaokatalog/ui/static/components/pieces/Tab.js: 16 added lines, 0 removed lines. Original line number Diff line number Diff line /* TODO: TAINT: AI-generated. This file was AI-generated (Gemini and/or Cursor), and I haven't manually reviewed it yet. I should do this at some point in the future and change things I don't like. */ export const Tab = { view(vnode) { return m( m.route.Link, { class: m.route.get() === vnode.attrs.href ? "active" : "", href: vnode.attrs.href, role: "button", // Für Barrierefreiheit tabindex: 0, // Für Tastaturnavigation }, vnode.attrs.label, ) }, }
karaokatalog/ui/static/style.css 0 → 100644 +275 −0 Changes for karaokatalog/ui/static/style.css: 275 added lines, 0 removed lines. Original line number Diff line number Diff line /* TODO: TAINT: AI-generated. This file was AI-generated (Gemini and/or Cursor), and I haven't manually reviewed it yet. I should do this at some point in the future and change things I don't like. */ /* Globale Resets und Basis-Styling (wie vorher) */ body, html { margin: 0; padding: 0; font-family: "Roboto", sans-serif; background-color: #f4f4f9; color: #333; line-height: 1.6; } main > p { text-align: center; padding: 1em; font-size: 1.1em; color: #757575; } /* Suchleiste (Höhe ca. 60px) */ search { position: fixed; top: 0; left: 0; right: 0; background-color: #3f51b5; /* Indigo - Material Primary */ padding: 12px 16px; z-index: 1000; box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); box-sizing: border-box; /* Stellt sicher, dass Padding die Höhe nicht sprengt */ height: 60px; /* Feste Höhe für genaue Positionierung */ } input[type="search"] { width: 100%; padding: 10px 12px; border-radius: 4px; border: none; font-size: 16px; box-sizing: border-box; background-color: #fff; color: #333; } input[type="search"]::placeholder { color: #888; } /* Tab Bar (Höhe ca. 48px) */ nav { display: flex; position: fixed; top: 60px; /* Direkt unter der Suchleiste */ left: 0; right: 0; background-color: #3f51b5; /* Gleiche Farbe wie Suchleiste für einen einheitlichen Header-Block */ z-index: 999; /* Unter der Suchleiste, falls sie überlappen könnten */ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15); /* Dezenter Schatten */ height: 48px; /* Feste Höhe */ box-sizing: border-box; } nav a { flex-grow: 1; display: flex; /* Für vertikales Zentrieren des Texts */ align-items: center; justify-content: center; padding: 0 8px; /* Horizontal Padding */ text-align: center; color: rgba( 255, 255, 255, 0.75 ); /* Etwas helleres, aber gedämpftes Weiß für inaktive Tabs */ cursor: pointer; font-weight: 500; font-size: 0.9em; /* Leicht kleiner als Haupttext */ text-transform: uppercase; border-bottom: 3px solid transparent; /* Platzhalter für den aktiven Indikator */ transition: color 0.2s ease-in-out, border-bottom-color 0.2s ease-in-out; user-select: none; text-decoration: none; } nav a:hover { background-color: rgba(255, 255, 255, 0.1); /* Leichter Hover-Effekt */ } nav a.active { color: #ffffff; /* Reines Weiß für aktiven Tab */ border-bottom-color: #ff4081; /* Material Accent Pink für den Indikator */ } /* song .Liste Container */ main { padding-top: calc(60px + 48px); /* Höhe Suchleiste + Höhe TabBar */ padding-bottom: 20px; /* Platz für Paginierung, falls am Ende */ } /* Link zum Zurücksetzen der Suche in der main > p */ main > p a.clear-search-link, main > p .clear-search-link { /* Zweite Regel für den Fall, dass Mithril das 'a' nicht direkt rendert */ color: #3f51b5; /* Material Indigo, passend zum Header */ text-decoration: underline; cursor: pointer; font-weight: 500; /* Etwas hervorheben */ background: none; /* Sicherstellen, dass keine Button-Styles übernommen werden */ border: none; padding: 0; display: inline; /* Damit es sich wie ein normaler Link im Text verhält */ } main > p a.clear-search-link:hover, main > p a.clear-search-link:focus, main > p .clear-search-link:hover, main > p .clear-search-link:focus { color: #303f9f; /* Dunkleres Indigo für Hover/Focus */ text-decoration: none; /* Optional: Unterstreichung bei Hover entfernen */ } /* Restliche Styles für Artist-Group, song .Item etc. bleiben wie zuvor */ section.artist { margin: 12px 8px; background-color: #ffffff; border-radius: 4px; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); overflow: hidden; } section.artist > h2 { margin: 0; padding: 12px 16px; background-color: #e8eaf6; color: #303f9f; font-size: 1.1em; font-weight: 500; border-bottom: 1px solid #c5cae9; } .song { display: flex; justify-content: space-between; align-items: center; padding: 12px 16px; border-bottom: 1px solid #f0f0f0; transition: background-color 0.2s ease-in-out; } .song:last-child { border-bottom: none; } .song:hover { background-color: #f9f9f9; } .song .info { flex-grow: 1; margin-right: 8px; /* Abstand zum Icon */ overflow: hidden; /* Verhindert Textüberlauf */ } .song .title { font-size: 1em; font-weight: 400; color: #212121; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; /* Text abschneiden bei Überlauf */ } .song .artist { font-size: 0.85em; color: #757575; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .song .actions { margin-left: auto; flex-shrink: 0; } .heart-icon { cursor: pointer; color: #bdbdbd; font-size: 24px; user-select: none; transition: color 0.2s ease-in-out, transform 0.1s ease-out; vertical-align: middle; } .heart-icon:hover { transform: scale(1.1); } .heart-icon.is-favorite { color: #ff4081; font-variation-settings: "FILL" 1; } .material-symbols-outlined { font-variation-settings: "FILL" 0, "wght" 400, "GRAD" 0, "opsz" 24; } /* NEU: Pagination Controls Styles */ .pagination-controls { display: flex; justify-content: center; align-items: center; padding: 20px 0; /* Mehr Padding für bessere Sichtbarkeit */ margin-top: 10px; user-select: none; flex-wrap: wrap; /* Erlaubt Umbruch bei vielen Seitenzahlen auf kleinen Bildschirmen */ } .pagination-controls button.pagination-button, /* Klasse für Buttons hinzugefügt */ .pagination-controls span.page-number { margin: 4px; /* Etwas mehr Margin für Umbruch */ padding: 8px 12px; border: 1px solid #ddd; background-color: #fff; color: #3f51b5; /* Material Indigo */ cursor: pointer; border-radius: 4px; transition: background-color 0.2s, color 0.2s, border-color 0.2s; font-size: 0.9em; min-width: 36px; /* Mindestbreite für bessere Klickbarkeit */ text-align: center; box-sizing: border-box; } .pagination-controls button.pagination-button:hover:not(:disabled), .pagination-controls span.page-number.clickable:hover { background-color: #e8eaf6; /* Heller Indigo-Ton für Hover */ border-color: #c5cae9; } .pagination-controls button.pagination-button:disabled { color: #aaa; cursor: not-allowed; background-color: #f9f9f9; border-color: #eee; } .pagination-controls span.page-number.active { background-color: #3f51b5; color: #fff; border-color: #3f51b5; font-weight: bold; cursor: default; } .pagination-controls span.dots { padding: 8px 6px; color: #777; margin: 4px; font-size: 0.9em; }