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

refactor: separate model and view logic for SongList

Extracted all state, data fetching, filtering, pagination, and favorite logic from SongList.mjs into a new SongListModel.mjs. SongList.mjs now only contains the view logic and delegates to the model for all data and actions.
parent 28500b15
Loading
Loading
Loading
Loading
+13 −235
Changes for frontend/SongList.mjs: 13 added lines, 235 removed lines.
Original line number Diff line number Diff line
// frontend/SongList.mjs
import SongItem from "./SongItem.mjs"
import PaginationControls from "./PaginationControls.mjs"
import SongItem from "./components/pieces/SongItem.mjs"
import PaginationControls from "./components/pieces/PaginationControls.mjs"
import SongListModel from "./SongListModel.mjs"

const FAVORITES_STORAGE_KEY = "songAppFavorites"
const API_URL_SONGS = "/api/songs"

const SongList = {
    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()
    oninit: function(vnode) {
        this.model = SongListModel.create()
        this.model.oninit()
    },

    _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
        }
        return this.model.onbeforeupdate(vnode, old)
    },

    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)
        }
    },

    view: function(vnode) {
        const { currentView, searchTerm, onSearchTermChange } = vnode.attrs
        const model = this.model

        if (this.isLoading && (!this.allSongs || this.allSongs.length === 0)) {
        if (model.isLoading && (!model.allSongs || model.allSongs.length === 0)) {
            return m(
                "main",
                m("div.loading-placeholder", "Lade Songs vom Server..."),
@@ -254,7 +32,7 @@ const SongList = {
            currentPage,
            totalPages,
            totalFilteredItems,
        } = this.getProcessedSongsForView(currentView, searchTerm)
        } = model.getProcessedSongsForView(currentView, searchTerm)

        const artistsOnPage = Object.keys(groupedSongs)
        let messageElement = null
@@ -316,7 +94,7 @@ const SongList = {
        // Fallback-Nachricht, wenn keine spezifische Nachricht erstellt wurde,
        // aber absolut keine Items vorhanden sind (über alle Seiten).
        // Dies sollte durch getProcessedSongsForView abgedeckt sein, ist aber ein Sicherheitsnetz.
        if (!messageElement && totalFilteredItems === 0 && !this.isLoading) {
        if (!messageElement && totalFilteredItems === 0 && !model.isLoading) {
            messageElement = m(
                "p.view-message",
                "Keine Songs entsprechen den aktuellen Kriterien.",
@@ -344,7 +122,7 @@ const SongList = {
                                    key: s.id,
                                    song: s,
                                    onToggleFavorite:
                                        this.toggleFavorite.bind(this),
                                        model.toggleFavorite.bind(model),
                                }),
                            ),
                        ],
@@ -358,7 +136,7 @@ const SongList = {
                m(PaginationControls, {
                    currentPage: currentPage,
                    totalPages: totalPages,
                    onPageChange: this.onPageChange.bind(this),
                    onPageChange: model.onPageChange.bind(model),
                }),
            )
        }
+241 −0
Changes for frontend/SongListModel.mjs: 241 added lines, 0 removed lines.
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