Commit 7e298959 authored by Cursor AI's avatar Cursor AI Committed by Jakob Moser
Browse files

Refactor: Move SongListModel to singleton in model directory, add Song class,...

Refactor: Move SongListModel to singleton in model directory, add Song class, and update usage for persistence
parent 3fa656e7
Loading
Loading
Loading
Loading
+4 −5
Original line number Diff line number Diff line
@@ -4,19 +4,18 @@ import Message from "./components/pieces/Message.mjs"
import ClearSearchLink from "./components/pieces/ClearSearchLink.mjs"
import Song from "./components/pieces/Song.mjs"
import PaginationControls from "./components/pieces/PaginationControls.mjs"
import SongListModel from "./SongListModel.mjs"
import SongListModel from "./model/SongListModel.js"

const SongList = {
    oninit: function() {
        this.model = SongListModel.create()
        this.model.oninit()
        SongListModel.oninit()
    },
    onbeforeupdate: function(vnode, old) {
        return this.model.onbeforeupdate(vnode, old)
        return SongListModel.onbeforeupdate(vnode, old)
    },
    view: function(vnode) {
        const { currentView, searchTerm, onSearchTermChange } = vnode.attrs
        const model = this.model
        const model = SongListModel

        if (model.isLoading && (!model.allSongs || model.allSongs.length === 0)) {
            return [

frontend/SongListModel.mjs

deleted100644 → 0
+0 −241
Original line number Diff line number Diff line
const FAVORITES_STORAGE_KEY = "songAppFavorites"
const API_URL_SONGS = "/api/songs"

const SongListModel = {
    create() {
        return {
            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 => ({
                            ...songFromServer,
                            isFavorite: favoriteIdsFromStorage.has(songFromServer.id),
                        }))
                        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()
            },

            onbeforeupdate: function (vnode, old) {
                if (
                    vnode.attrs.currentView !== old.attrs.currentView ||
                    vnode.attrs.searchTerm !== old.attrs.searchTerm
                ) {
                    this.currentPage = 1
                }
                return true
            },

            onPageChange: function (newPage) {
                if (newPage >= 1 && newPage <= this.totalPagesComputed) {
                    this.currentPage = newPage
                }
            },

            getProcessedSongsForView: function (viewFilter, searchTerm) {
                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,
                        )
                        if (initialSongsForView.length === 0) {
                            // Überhaupt keine Favoriten
                            messageFromTabFilter =
                                "Du hast noch keine Favoriten markiert."
                        }
                        break
                    case "popular":
                        // initialSongsForView bleibt leer
                        messageFromTabFilter =
                            "Die Liste der beliebten Songs ist bald verfügbar!"
                        break
                    case "all":
                    default:
                        initialSongsForView = this.allSongs
                        if (initialSongsForView.length === 0 && !this.isLoading) {
                            messageFromTabFilter = "Keine Songs in der Bibliothek."
                        }
                        break
                }

                let songsAfterSearch = initialSongsForView.slice()
                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()
                    songsAfterSearch = initialSongsForView.filter(
                        song =>
                            (song.title &&
                                song.title.toLowerCase().includes(lowerSearchTerm)) ||
                            (song.artist &&
                                song.artist.toLowerCase().includes(lowerSearchTerm)),
                    )

                    if (viewFilter === "favorites") {
                        const totalFavoritesOverall = initialSongsForView.length // Anzahl Favoriten vor der Suche
                        const favoritesFoundBySearch = songsAfterSearch.length

                        if (totalFavoritesOverall > 0) {
                            // Nur wenn es überhaupt Favoriten gab
                            if (favoritesFoundBySearch < totalFavoritesOverall) {
                                if (favoritesFoundBySearch === 0) {
                                    // Suche aktiv, Favoriten vorhanden, aber Suche findet keine davon
                                    composedMessage = {
                                        type: "filtered_favorites_none_found_by_search",
                                        total: totalFavoritesOverall,
                                    }
                                } else {
                                    // Suche aktiv, Favoriten vorhanden, Suche findet einige, aber nicht alle
                                    composedMessage = {
                                        type: "filtered_favorites_some_hidden",
                                        total: totalFavoritesOverall,
                                        showing: favoritesFoundBySearch,
                                    }
                                }
                            }
                            // Wenn favoritesFoundBySearch === totalFavoritesOverall, hat die Suche nichts ausgeblendet -> keine spezielle Nachricht hier.
                        } else {
                            // Keine Favoriten vorhanden, aber Suche ist aktiv.
                            // messageFromTabFilter ("Du hast noch keine Favoriten markiert.") ist hier passend.
                            composedMessage = messageFromTabFilter
                        }
                    }

                    // Allgemeine Nachricht, wenn die Suche keine Ergebnisse liefert (und nicht schon durch Favoriten-Logik abgedeckt)
                    if (songsAfterSearch.length === 0 && !composedMessage) {
                        composedMessage = "Keine Songs für deine Suche gefunden."
                    }
                } else {
                    // Kein Suchbegriff aktiv
                    composedMessage = messageFromTabFilter
                }

                const totalFilteredItems = songsAfterSearch.length // Gesamtanzahl der Items nach Filterung/Suche (über alle Seiten)

                // 3. Paginierungslogik
                const totalPages =
                    Math.ceil(totalFilteredItems / this.itemsPerPage) || 1
                this.totalPagesComputed = totalPages

                // Sicherstellen, dass currentPage innerhalb gültiger Grenzen liegt
                if (this.currentPage > totalPages) this.currentPage = totalPages
                if (this.currentPage < 1) this.currentPage = 1

                const startIndex = (this.currentPage - 1) * this.itemsPerPage
                const paginatedSongs = songsAfterSearch.slice(
                    startIndex,
                    startIndex + this.itemsPerPage,
                )

                // 4. Songs für die aktuelle Seite nach Künstler gruppieren
                const groups = {}
                if (paginatedSongs.length > 0) {
                    paginatedSongs.forEach(song => {
                        groups[song.artist] = groups[song.artist] || []
                        groups[song.artist].push(song)
                    })
                }
                const sortedArtistNames = Object.keys(groups).sort((a, b) =>
                    a.localeCompare(b),
                )
                const finalGroupedSongsOnPage = {}
                sortedArtistNames.forEach(artist => {
                    finalGroupedSongsOnPage[artist] = groups[artist].sort((a, b) =>
                        a.title.localeCompare(b.title),
                    )
                })

                // 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) {
                    composedMessage = "Keine Songs entsprechen den aktuellen Kriterien."
                }

                return {
                    groupedSongs: finalGroupedSongsOnPage, // Songs für die aktuelle Seite
                    message: composedMessage, // Kann null, string oder Objekt sein
                    hasContentOnPage: paginatedSongs.length > 0,
                    currentPage: this.currentPage,
                    totalPages: this.totalPagesComputed,
                    totalFilteredItems: totalFilteredItems, // Gesamtanzahl gefilterter Items
                }
            },

            toggleFavorite: function (songId) {
                const song = this.allSongs.find(s => s.id === songId)
                if (song) {
                    song.isFavorite = !song.isFavorite
                    const currentFavoriteIds = new Set()
                    this.allSongs.forEach(s => {
                        if (s.isFavorite) {
                            currentFavoriteIds.add(s.id)
                        }
                    })
                    this._saveFavoriteIdsToStorage(currentFavoriteIds)
                }
            },
        }
    }
}

export default SongListModel 
 No newline at end of file

frontend/model/Song.js

0 → 100644
+17 −0
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
    }

    static fromObject(obj) {
        return new Song({
            id: obj.id,
            title: obj.title,
            artist: obj.artist,
            isFavorite: obj.isFavorite ?? false,
        })
    }
} 
 No newline at end of file
+221 −0
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()
    },

    onbeforeupdate: function (vnode, old) {
        if (
            vnode.attrs.currentView !== old.attrs.currentView ||
            vnode.attrs.searchTerm !== old.attrs.searchTerm
        ) {
            this.currentPage = 1
        }
        return true
    },

    onPageChange: function (newPage) {
        if (newPage >= 1 && newPage <= this.totalPagesComputed) {
            this.currentPage = newPage
        }
    },

    getProcessedSongsForView: function (viewFilter, searchTerm) {
        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,
                )
                if (initialSongsForView.length === 0) {
                    // Überhaupt keine Favoriten
                    messageFromTabFilter =
                        "Du hast noch keine Favoriten markiert."
                }
                break
            case "popular":
                // initialSongsForView bleibt leer
                messageFromTabFilter =
                    "Die Liste der beliebten Songs ist bald verfügbar!"
                break
            case "all":
            default:
                initialSongsForView = this.allSongs
                if (initialSongsForView.length === 0 && !this.isLoading) {
                    messageFromTabFilter = "Keine Songs in der Bibliothek."
                }
                break
        }

        let songsAfterSearch = initialSongsForView.slice()
        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()
            songsAfterSearch = initialSongsForView.filter(
                song =>
                    (song.title &&
                        song.title.toLowerCase().includes(lowerSearchTerm)) ||
                    (song.artist &&
                        song.artist.toLowerCase().includes(lowerSearchTerm)),
            )

            if (viewFilter === "favorites") {
                const totalFavoritesOverall = initialSongsForView.length // Anzahl Favoriten vor der Suche
                const favoritesFoundBySearch = songsAfterSearch.length

                if (totalFavoritesOverall > 0) {
                    if (favoritesFoundBySearch > 0) {
                        composedMessage = {
                            type: "filtered_favorites_some_hidden",
                            showing: favoritesFoundBySearch,
                            total: totalFavoritesOverall,
                        }
                    } else {
                        composedMessage = {
                            type: "filtered_favorites_none_found_by_search",
                            total: totalFavoritesOverall,
                        }
                    }
                }
            }
        } else {
            // Kein Suchbegriff aktiv
            composedMessage = messageFromTabFilter
        }

        const totalFilteredItems = songsAfterSearch.length // Gesamtanzahl der Items nach Filterung/Suche (über alle Seiten)

        // 3. Paginierungslogik
        const totalPages =
            Math.ceil(totalFilteredItems / this.itemsPerPage) || 1
        this.totalPagesComputed = totalPages

        // Sicherstellen, dass currentPage innerhalb gültiger Grenzen liegt
        if (this.currentPage > totalPages) this.currentPage = totalPages
        if (this.currentPage < 1) this.currentPage = 1

        const startIndex = (this.currentPage - 1) * this.itemsPerPage
        const paginatedSongs = songsAfterSearch.slice(
            startIndex,
            startIndex + this.itemsPerPage,
        )

        // 4. Songs für die aktuelle Seite nach Künstler gruppieren
        const groups = {}
        if (paginatedSongs.length > 0) {
            paginatedSongs.forEach(song => {
                groups[song.artist] = groups[song.artist] || []
                groups[song.artist].push(song)
            })
        }
        const sortedArtistNames = Object.keys(groups).sort((a, b) =>
            a.localeCompare(b),
        )
        const finalGroupedSongsOnPage = {}
        sortedArtistNames.forEach(artist => {
            finalGroupedSongsOnPage[artist] = groups[artist].sort((a, b) =>
                a.title.localeCompare(b.title),
            )
        })

        // 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) {
            composedMessage = "Keine Songs entsprechen den aktuellen Kriterien."
        }

        return {
            groupedSongs: finalGroupedSongsOnPage,
            message: composedMessage,
            hasContentOnPage: paginatedSongs.length > 0,
            currentPage: this.currentPage,
            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 
 No newline at end of file