Loading frontend/components/pieces/Artist.mjs +0 −2 Original line number Diff line number Diff line Loading @@ -9,7 +9,6 @@ const Artist = { currentView, searchTerm, currentPage, onToggleFavorite, } = vnode.attrs return m( "div.artist", Loading @@ -22,7 +21,6 @@ const Artist = { m(Song, { key: s.id, song: s, onToggleFavorite, }), ), ], Loading frontend/components/pieces/Song.mjs +4 −4 Original line number Diff line number Diff line // Song.mjs const Song = { view: function (vnode) { const { song, onToggleFavorite } = vnode.attrs const { song } = vnode.attrs const heartIconName = "favorite" return m("div.song", { key: song.id }, [ Loading @@ -13,9 +13,9 @@ const Song = { m( "span.material-symbols-outlined.heart-icon", { class: song.isFavorite ? "is-favorite" : "", onclick: () => onToggleFavorite(song.id), title: song.isFavorite class: song.favorite ? "is-favorite" : "", onclick: () => song.toggleFavorite(), title: song.favorite ? "Als Favorit entfernen" : "Als Favorit markieren", }, Loading frontend/components/pieces/SongList.mjs +0 −1 Original line number Diff line number Diff line Loading @@ -97,7 +97,6 @@ const SongList = { currentView, query, currentPage, onToggleFavorite: model.toggleFavorite.bind(model), }), ), ) Loading frontend/model/Song.js +20 −13 Original line number Diff line number Diff line export default class Song { constructor({ id, title, artist, isFavorite = false }) { this.id = id this.title = title this.artist = artist this.isFavorite = isFavorite import { Base } from "./Base.mjs" import { getFavoriteIds, addFavorite, removeFavorite } from "./favorites.js" export default class Song extends Base { static async forceLoad() { return await m.request({ url: "/api/songs" }) } get favorite() { return getFavoriteIds().has(this.id) } set favorite(isFavorite) { if (isFavorite) { addFavorite(this.id) } else { removeFavorite(this.id) } } static fromObject(obj) { return new Song({ id: obj.id, title: obj.title, artist: obj.artist, isFavorite: obj.isFavorite ?? false, }) toggleFavorite() { this.favorite = !this.favorite } } frontend/model/SongListModel.js +15 −89 Original line number Diff line number Diff line import Song from "./Song.js" const FAVORITES_STORAGE_KEY = "songAppFavorites" const API_URL_SONGS = "/api/songs" const SongListModel = { allSongs: [], isLoading: true, currentPage: 1, itemsPerPage: 60, // Wie vom Benutzer angegeben totalPagesComputed: 1, _loadFavoriteIdsFromStorage: function () { try { const storedFavorites = localStorage.getItem(FAVORITES_STORAGE_KEY) if (storedFavorites) { return new Set(JSON.parse(storedFavorites)) } } catch (e) { console.error( "Fehler beim Laden der Favoriten aus localStorage:", e, ) } return new Set() }, _saveFavoriteIdsToStorage: function (favoriteIdsSet) { try { localStorage.setItem( FAVORITES_STORAGE_KEY, JSON.stringify(Array.from(favoriteIdsSet)), ) } catch (e) { console.error( "Fehler beim Speichern der Favoriten in localStorage:", e, ) } }, loadAllSongsFromServer: function () { this.isLoading = true this.currentPage = 1 const favoriteIdsFromStorage = this._loadFavoriteIdsFromStorage() m.request({ method: "GET", url: API_URL_SONGS, }) .then(dataFromServer => { const songsArray = Array.isArray(dataFromServer) ? dataFromServer : [] this.allSongs = songsArray.map(songFromServer => { const song = Song.fromObject(songFromServer) song.isFavorite = favoriteIdsFromStorage.has(song.id) return song }) this.isLoading = false m.redraw() }) .catch(error => { console.error("Fehler beim Laden der Songs vom Server:", error) this.allSongs = [] this.isLoading = false m.redraw() }) }, oninit: function () { this.loadAllSongsFromServer() oninit: async function () { await Song.load() }, onbeforeupdate: function (vnode, old) { if ( vnode.attrs.currentView !== old.attrs.currentView || vnode.attrs.searchTerm !== old.attrs.searchTerm (vnode.attrs.query || "") !== (old.attrs.query || "") ) { this.currentPage = 1 } Loading @@ -87,15 +25,15 @@ const SongListModel = { } }, getProcessedSongsForView: function (viewFilter, searchTerm) { getProcessedSongsForView: function (viewFilter, query) { let initialSongsForView = [] let messageFromTabFilter = null // Nachricht rein basierend auf Tab-Auswahl // 1. Songs basierend auf dem aktuellen Tab (View) filtern switch (viewFilter) { case "favorites": initialSongsForView = this.allSongs.filter( song => song.isFavorite, initialSongsForView = (Song.all || []).filter( song => song.favorite, ) if (initialSongsForView.length === 0) { // Überhaupt keine Favoriten Loading @@ -110,8 +48,8 @@ const SongListModel = { break case "all": default: initialSongsForView = this.allSongs if (initialSongsForView.length === 0 && !this.isLoading) { initialSongsForView = Song.all || [] if (initialSongsForView.length === 0) { messageFromTabFilter = "Keine Songs in der Bibliothek." } break Loading @@ -121,15 +59,15 @@ const SongListModel = { let composedMessage = null // Endgültige Nachricht für den Benutzer // 2. Suchbegriff anwenden und Nachrichten erstellen const trimmedSearchTerm = searchTerm ? searchTerm.trim() : "" if (trimmedSearchTerm !== "") { const lowerSearchTerm = trimmedSearchTerm.toLowerCase() const trimmedQuery = query ? query.trim() : "" if (trimmedQuery !== "") { const lowerQuery = trimmedQuery.toLowerCase() songsAfterSearch = initialSongsForView.filter( song => (song.title && song.title.toLowerCase().includes(lowerSearchTerm)) || song.title.toLowerCase().includes(lowerQuery)) || (song.artist && song.artist.toLowerCase().includes(lowerSearchTerm)), song.artist.toLowerCase().includes(lowerQuery)), ) if (viewFilter === "favorites") { Loading Loading @@ -193,7 +131,7 @@ const SongListModel = { // 5. Endgültige Nachricht, falls nach Filterung/Suche gar keine Items da sind (über alle Seiten) // und noch keine spezifischere Nachricht (z.B. Favoriten-Logik) gesetzt wurde. if (totalFilteredItems === 0 && !composedMessage && !this.isLoading) { if (totalFilteredItems === 0 && !composedMessage) { composedMessage = "Keine Songs entsprechen den aktuellen Kriterien." } Loading @@ -205,19 +143,7 @@ const SongListModel = { totalPages: totalPages, totalFilteredItems: totalFilteredItems, } }, toggleFavorite: function (songId) { const song = this.allSongs.find(s => s.id === songId) if (song) { song.isFavorite = !song.isFavorite // Persist favorites const favoriteIds = this.allSongs .filter(s => s.isFavorite) .map(s => s.id) this._saveFavoriteIdsToStorage(new Set(favoriteIds)) } }, } export default SongListModel Loading
frontend/components/pieces/Artist.mjs +0 −2 Original line number Diff line number Diff line Loading @@ -9,7 +9,6 @@ const Artist = { currentView, searchTerm, currentPage, onToggleFavorite, } = vnode.attrs return m( "div.artist", Loading @@ -22,7 +21,6 @@ const Artist = { m(Song, { key: s.id, song: s, onToggleFavorite, }), ), ], Loading
frontend/components/pieces/Song.mjs +4 −4 Original line number Diff line number Diff line // Song.mjs const Song = { view: function (vnode) { const { song, onToggleFavorite } = vnode.attrs const { song } = vnode.attrs const heartIconName = "favorite" return m("div.song", { key: song.id }, [ Loading @@ -13,9 +13,9 @@ const Song = { m( "span.material-symbols-outlined.heart-icon", { class: song.isFavorite ? "is-favorite" : "", onclick: () => onToggleFavorite(song.id), title: song.isFavorite class: song.favorite ? "is-favorite" : "", onclick: () => song.toggleFavorite(), title: song.favorite ? "Als Favorit entfernen" : "Als Favorit markieren", }, Loading
frontend/components/pieces/SongList.mjs +0 −1 Original line number Diff line number Diff line Loading @@ -97,7 +97,6 @@ const SongList = { currentView, query, currentPage, onToggleFavorite: model.toggleFavorite.bind(model), }), ), ) Loading
frontend/model/Song.js +20 −13 Original line number Diff line number Diff line export default class Song { constructor({ id, title, artist, isFavorite = false }) { this.id = id this.title = title this.artist = artist this.isFavorite = isFavorite import { Base } from "./Base.mjs" import { getFavoriteIds, addFavorite, removeFavorite } from "./favorites.js" export default class Song extends Base { static async forceLoad() { return await m.request({ url: "/api/songs" }) } get favorite() { return getFavoriteIds().has(this.id) } set favorite(isFavorite) { if (isFavorite) { addFavorite(this.id) } else { removeFavorite(this.id) } } static fromObject(obj) { return new Song({ id: obj.id, title: obj.title, artist: obj.artist, isFavorite: obj.isFavorite ?? false, }) toggleFavorite() { this.favorite = !this.favorite } }
frontend/model/SongListModel.js +15 −89 Original line number Diff line number Diff line import Song from "./Song.js" const FAVORITES_STORAGE_KEY = "songAppFavorites" const API_URL_SONGS = "/api/songs" const SongListModel = { allSongs: [], isLoading: true, currentPage: 1, itemsPerPage: 60, // Wie vom Benutzer angegeben totalPagesComputed: 1, _loadFavoriteIdsFromStorage: function () { try { const storedFavorites = localStorage.getItem(FAVORITES_STORAGE_KEY) if (storedFavorites) { return new Set(JSON.parse(storedFavorites)) } } catch (e) { console.error( "Fehler beim Laden der Favoriten aus localStorage:", e, ) } return new Set() }, _saveFavoriteIdsToStorage: function (favoriteIdsSet) { try { localStorage.setItem( FAVORITES_STORAGE_KEY, JSON.stringify(Array.from(favoriteIdsSet)), ) } catch (e) { console.error( "Fehler beim Speichern der Favoriten in localStorage:", e, ) } }, loadAllSongsFromServer: function () { this.isLoading = true this.currentPage = 1 const favoriteIdsFromStorage = this._loadFavoriteIdsFromStorage() m.request({ method: "GET", url: API_URL_SONGS, }) .then(dataFromServer => { const songsArray = Array.isArray(dataFromServer) ? dataFromServer : [] this.allSongs = songsArray.map(songFromServer => { const song = Song.fromObject(songFromServer) song.isFavorite = favoriteIdsFromStorage.has(song.id) return song }) this.isLoading = false m.redraw() }) .catch(error => { console.error("Fehler beim Laden der Songs vom Server:", error) this.allSongs = [] this.isLoading = false m.redraw() }) }, oninit: function () { this.loadAllSongsFromServer() oninit: async function () { await Song.load() }, onbeforeupdate: function (vnode, old) { if ( vnode.attrs.currentView !== old.attrs.currentView || vnode.attrs.searchTerm !== old.attrs.searchTerm (vnode.attrs.query || "") !== (old.attrs.query || "") ) { this.currentPage = 1 } Loading @@ -87,15 +25,15 @@ const SongListModel = { } }, getProcessedSongsForView: function (viewFilter, searchTerm) { getProcessedSongsForView: function (viewFilter, query) { let initialSongsForView = [] let messageFromTabFilter = null // Nachricht rein basierend auf Tab-Auswahl // 1. Songs basierend auf dem aktuellen Tab (View) filtern switch (viewFilter) { case "favorites": initialSongsForView = this.allSongs.filter( song => song.isFavorite, initialSongsForView = (Song.all || []).filter( song => song.favorite, ) if (initialSongsForView.length === 0) { // Überhaupt keine Favoriten Loading @@ -110,8 +48,8 @@ const SongListModel = { break case "all": default: initialSongsForView = this.allSongs if (initialSongsForView.length === 0 && !this.isLoading) { initialSongsForView = Song.all || [] if (initialSongsForView.length === 0) { messageFromTabFilter = "Keine Songs in der Bibliothek." } break Loading @@ -121,15 +59,15 @@ const SongListModel = { let composedMessage = null // Endgültige Nachricht für den Benutzer // 2. Suchbegriff anwenden und Nachrichten erstellen const trimmedSearchTerm = searchTerm ? searchTerm.trim() : "" if (trimmedSearchTerm !== "") { const lowerSearchTerm = trimmedSearchTerm.toLowerCase() const trimmedQuery = query ? query.trim() : "" if (trimmedQuery !== "") { const lowerQuery = trimmedQuery.toLowerCase() songsAfterSearch = initialSongsForView.filter( song => (song.title && song.title.toLowerCase().includes(lowerSearchTerm)) || song.title.toLowerCase().includes(lowerQuery)) || (song.artist && song.artist.toLowerCase().includes(lowerSearchTerm)), song.artist.toLowerCase().includes(lowerQuery)), ) if (viewFilter === "favorites") { Loading Loading @@ -193,7 +131,7 @@ const SongListModel = { // 5. Endgültige Nachricht, falls nach Filterung/Suche gar keine Items da sind (über alle Seiten) // und noch keine spezifischere Nachricht (z.B. Favoriten-Logik) gesetzt wurde. if (totalFilteredItems === 0 && !composedMessage && !this.isLoading) { if (totalFilteredItems === 0 && !composedMessage) { composedMessage = "Keine Songs entsprechen den aktuellen Kriterien." } Loading @@ -205,19 +143,7 @@ const SongListModel = { totalPages: totalPages, totalFilteredItems: totalFilteredItems, } }, toggleFavorite: function (songId) { const song = this.allSongs.find(s => s.id === songId) if (song) { song.isFavorite = !song.isFavorite // Persist favorites const favoriteIds = this.allSongs .filter(s => s.isFavorite) .map(s => s.id) this._saveFavoriteIdsToStorage(new Set(favoriteIds)) } }, } export default SongListModel