diff --git a/qml/components/PlaylistManager.qml b/qml/components/PlaylistManager.qml index 07d9c46..23d21b2 100644 --- a/qml/components/PlaylistManager.qml +++ b/qml/components/PlaylistManager.qml @@ -1,6 +1,7 @@ import QtQuick 2.0 import io.thp.pyotherside 1.5 + Item { id: root @@ -214,7 +215,7 @@ Item { playlistPython.playTrack(id) currentTrackIndex() } - +/* function playPosition(id) { console.log(id) playlistPython.canNext = false @@ -222,47 +223,103 @@ Item { playlistPython.playPosition(id) currentTrackIndex() } - +*/ function insertTrack(id) { console.log("PlaylistManager.insertTrack", id) playlistPython.insertTrack(id) currentTrackIndex() } - +/* function nextTrack() { console.log("Next track called", mediaController.playbackState) playlistPython.nextTrack() currentTrackIndex() } - +*/ function nextTrackClicked() { console.log("Next track clicked") mediaController.blockAutoNext = true playlistPython.nextTrack() currentTrackIndex() mediaController.blockAutoNext = false + if (playlistStorage.currentPlaylistName) { + playlistStorage.updatePosition(playlistStorage.currentPlaylistName, currentIndex); + } } function restartTrack(id) { playlistPython.restartTrack() currentTrackIndex() } - +/* function previousTrack() { playlistPython.canNext = false playlistPython.previousTrack() currentTrackIndex() } - +*/ function previousTrackClicked() { playlistPython.canNext = false mediaController.blockAutoNext = true playlistPython.previousTrack() currentTrackIndex() + if (playlistStorage.currentPlaylistName) { + playlistStorage.updatePosition(playlistStorage.currentPlaylistName, currentIndex); + } } function generateList() { console.log("Playlist changed from main.qml") playlistPython.generateList() } + + // Neue Funktionen zum Speichern/Laden + function saveCurrentPlaylist(name) { + var trackIds = []; + for(var i = 0; i < size; i++) { + trackIds.push(requestPlaylistItem(i)); + } + playlistStorage.savePlaylist(name, trackIds, currentIndex); + playlistStorage.currentPlaylistName = name; + } + + function loadSavedPlaylist(name) { + playlistStorage.loadPlaylist(name); + } + + // Überschreibe die Navigation-Funktionen + function nextTrack() { + console.log("Next track called", mediaController.playbackState) + playlistPython.nextTrack() + currentTrackIndex() + // Speichere Fortschritt + if (playlistStorage.currentPlaylistName) { + playlistStorage.updatePosition(playlistStorage.currentPlaylistName, currentIndex); + } + } + + function previousTrack() { + playlistPython.canNext = false + playlistPython.previousTrack() + currentTrackIndex() + // Speichere Fortschritt + if (playlistStorage.currentPlaylistName) { + playlistStorage.updatePosition(playlistStorage.currentPlaylistName, currentIndex); + } + } + + function playPosition(position) { + playlistPython.canNext = false + mediaController.blockAutoNext = true + playlistPython.playPosition(position) + currentTrackIndex() + // Speichere Fortschritt + if (playlistStorage.currentPlaylistName) { + playlistStorage.updatePosition(playlistStorage.currentPlaylistName, position); + } + } + + function getSavedPlaylists() { + return playlistStorage.getPlaylistInfo(); + } } diff --git a/qml/components/PlaylistStorage.qml b/qml/components/PlaylistStorage.qml new file mode 100644 index 0000000..d9cd8b5 --- /dev/null +++ b/qml/components/PlaylistStorage.qml @@ -0,0 +1,158 @@ +import QtQuick 2.0 +import QtQuick.LocalStorage 2.0 + +Item { + id: root + + // Signale für Playlist-Events + signal playlistSaved(string name, var trackIds) + signal playlistLoaded(string name, var trackIds, int position) + signal playlistsChanged() + signal playlistDeleted(string name) + + // Initialisiere Datenbank + function getDatabase() { + return LocalStorage.openDatabaseSync( + "TidalPlayerDB", + "1.0", + "Tidal Player Playlist Storage", + 1000000 + ); + } + + // Erstelle Tabellen + function initDatabase() { + var db = getDatabase(); + db.transaction(function(tx) { + // Erweiterte Tabelle mit Position und Timestamp + tx.executeSql('CREATE TABLE IF NOT EXISTS playlists( + name TEXT PRIMARY KEY, + tracks TEXT, + position INTEGER DEFAULT 0, + last_played TIMESTAMP DEFAULT CURRENT_TIMESTAMP + )'); + }); + } + + // Speichere Playlist mit Position + function savePlaylist(name, trackIds, position) { + var db = getDatabase(); + var tracksJson = JSON.stringify(trackIds); + + db.transaction(function(tx) { + tx.executeSql('INSERT OR REPLACE INTO playlists (name, tracks, position, last_played) VALUES(?, ?, ?, CURRENT_TIMESTAMP)', + [name, tracksJson, position]); + }); + + playlistSaved(name, trackIds); + playlistsChanged(); + } + + // Lade Playlist mit Position + function loadPlaylist(name) { + var db = getDatabase(); + var result; + + db.transaction(function(tx) { + result = tx.executeSql('SELECT tracks, position FROM playlists WHERE name = ?', [name]); + if (result.rows.length > 0) { + var trackIds = JSON.parse(result.rows.item(0).tracks); + var position = result.rows.item(0).position; + + // Aktualisiere last_played + tx.executeSql('UPDATE playlists SET last_played = CURRENT_TIMESTAMP WHERE name = ?', [name]); + + playlistLoaded(name, trackIds, position); + } + }); + } + + // Update Position einer Playlist + function updatePosition(name, position) { + var db = getDatabase(); + + db.transaction(function(tx) { + tx.executeSql('UPDATE playlists SET position = ?, last_played = CURRENT_TIMESTAMP WHERE name = ?', + [position, name]); + }); + } + + // Lösche Playlist + function deletePlaylist(name) { + var db = getDatabase(); + + db.transaction(function(tx) { + tx.executeSql('DELETE FROM playlists WHERE name = ?', [name]); + }); + + playlistDeleted(name); + playlistsChanged(); + } + + // Hole alle Playlist-Namen mit Zusatzinformationen + function getPlaylistInfo() { + var db = getDatabase(); + var playlists = []; + + db.transaction(function(tx) { + var result = tx.executeSql('SELECT name, position, tracks, last_played FROM playlists ORDER BY last_played DESC'); + for (var i = 0; i < result.rows.length; i++) { + var item = result.rows.item(i); + var tracks = JSON.parse(item.tracks); + playlists.push({ + name: item.name, + position: item.position, + trackCount: tracks.length, + lastPlayed: item.last_played + }); + } + }); + + return playlists; + } + + // In PlaylistManager.qml oder wo der PlaylistStorage verwendet wird + function saveCurrentPlaylistState() { + var trackIds = [] + for(var i = 0; i < playlistManager.size; i++) { + var id = playlistManager.requestPlaylistItem(i) + trackIds.push(id) + } + // Speichere als spezielle Playlist "_current" + playlistStorage.savePlaylist("_current", trackIds, playlistManager.currentIndex) + } + + // Beim Laden + function loadCurrentPlaylistState() { + var currentPlaylist = playlistStorage.loadPlaylist("_current") + if (currentPlaylist && currentPlaylist.tracks.length > 0) { + playlistManager.clearPlayList() + for (var i = 0; i < currentPlaylist.tracks.length; i++) { + playlistManager.appendTrack(currentPlaylist.tracks[i]) + } + // Position wiederherstellen + if (currentPlaylist.position >= 0) { + playlistManager.playPosition(currentPlaylist.position) + } + } + } + Component.onCompleted: { + initDatabase(); + loadCurrentPlaylistState() + } + // Bei App-Beendigung + Component.onDestruction: { + saveCurrentPlaylistState() + } + + // Optional: Bei wichtigen Playlist-Änderungen + Connections { + target: playlistManager + onListChanged: { + saveCurrentPlaylistState() + } + onCurrentIndexChanged: { + saveCurrentPlaylistState() + } + } +} diff --git a/qml/components/TidalCache.qml b/qml/components/TidalCache.qml index ab98275..e2138b1 100644 --- a/qml/components/TidalCache.qml +++ b/qml/components/TidalCache.qml @@ -293,6 +293,7 @@ id: root bio: result.bio, timestamp: Date.now(), } + console.log("Adding to cache ...") saveArtistToCache(artistData) return artistData diff --git a/qml/dialogs/saveplaylist.qml b/qml/dialogs/saveplaylist.qml new file mode 100644 index 0000000..edc5035 --- /dev/null +++ b/qml/dialogs/saveplaylist.qml @@ -0,0 +1,40 @@ +import QtQuick 2.0 +import Sailfish.Silica 1.0 + +Dialog { + id: dialog + + property string playlistName: nameField.text + property string suggestedName: "" // Wird von der AlbumPage übergeben + + canAccept: nameField.text.length > 0 + + Column { + width: parent.width + spacing: Theme.paddingMedium + + DialogHeader { + title: qsTr("Save as Playlist") + } + + TextField { + id: nameField + width: parent.width + placeholderText: qsTr("Enter playlist name") + label: qsTr("Playlist name") + text: suggestedName // Verwendet den übergebenen Albumtitel + EnterKey.enabled: text.length > 0 + EnterKey.iconSource: "image://theme/icon-m-enter-accept" + EnterKey.onClicked: dialog.accept() + } + + Label { + x: Theme.horizontalPageMargin + width: parent.width - 2*x + text: qsTr("This will create a new playlist from all tracks in this album.") + wrapMode: Text.Wrap + font.pixelSize: Theme.fontSizeSmall + color: Theme.secondaryColor + } + } +} diff --git a/qml/harbour-tidalplayer.qml b/qml/harbour-tidalplayer.qml index 2f03b4f..015230b 100644 --- a/qml/harbour-tidalplayer.qml +++ b/qml/harbour-tidalplayer.qml @@ -60,6 +60,7 @@ ApplicationWindow tidalApi.playTrackId(track) } } + } TidalCache @@ -67,6 +68,26 @@ ApplicationWindow id: cacheManager } + PlaylistStorage { + id: playlistStorage + + property string currentPlaylistName: "" + + onPlaylistLoaded: { + // Wenn eine Playlist geladen wird + currentPlaylistName = name; + playlistManager.clearPlayList(); + trackIds.forEach(function(trackId) { + playlistManager.appendTrack(trackId); + }); + // Setze die gespeicherte Position + if (position >= 0) { + playlistManager.playPosition(position); + } + } + } + + MediaController { id: mediaController diff --git a/qml/pages/AlbumPage.qml b/qml/pages/AlbumPage.qml index e5d22ea..b4bdc5d 100644 --- a/qml/pages/AlbumPage.qml +++ b/qml/pages/AlbumPage.qml @@ -10,9 +10,23 @@ Page { property int albumId: -1 property var albumData: null property bool isHeaderCollapsed: false + //property alias model: listModel allowedOrientations: Orientation.All + function saveAlbumAsPlaylist(name) { + var trackIds = [] + // Wir nutzen die Tracks aus dem Album-Cache + if (albumData) { + var tracks = tidalApi.getAlbumTracks(albumId) + for(var i = 0; i < trackList.model.count; i++) { + var track = trackList.model.get(i) + trackIds.push(track.trackid) + } + playlistStorage.savePlaylist(name, trackIds, 0) + } + } + SilicaFlickable { id: flickable anchors { @@ -32,6 +46,24 @@ Page { } PullDownMenu { + + MenuItem { + text: qsTr("Save as Playlist") + onClicked: { + if (albumData) { + var dialog = pageStack.push(Qt.resolvedUrl("../dialogs/saveplaylist.qml"), { + "suggestedName": albumData.title + }) + dialog.accepted.connect(function() { + if (dialog.playlistName.length > 0) { + saveAlbumAsPlaylist(dialog.playlistName) + } + }) + } + } + enabled: albumData !== null + } + MenuItem { text: minPlayerPanel.open ? "Hide player" : "Show player" onClicked: minPlayerPanel.open = !minPlayerPanel.open diff --git a/qml/pages/ArtistPage.qml b/qml/pages/ArtistPage.qml index fa404c0..f051125 100644 --- a/qml/pages/ArtistPage.qml +++ b/qml/pages/ArtistPage.qml @@ -369,7 +369,6 @@ function processWimpLinks(text) { // Neues Signal für Alben onAlbumofArtist: { - albumsView.model.append({ title: album_info.title, cover: album_info.image, @@ -378,7 +377,6 @@ function processWimpLinks(text) { } onSimilarArtist: { - simartistView.model.append({ name: artist_info.name, cover: artist_info.image, diff --git a/qml/pages/FirstPage.qml b/qml/pages/FirstPage.qml index ac67db6..193a978 100644 --- a/qml/pages/FirstPage.qml +++ b/qml/pages/FirstPage.qml @@ -24,6 +24,12 @@ Page { // PullDownMenu and PushUpMenu must be declared in SilicaFlickable, SilicaListView or SilicaGridView PullDownMenu { + + MenuItem { + text: qsTr("Saved Playlists") + onClicked: pageStack.push(Qt.resolvedUrl("SavedPlaylistsPage.qml")) + } + MenuItem { text: qsTr("Settings") onClicked: { diff --git a/qml/pages/SavedPlaylistsPage.qml b/qml/pages/SavedPlaylistsPage.qml new file mode 100644 index 0000000..3799066 --- /dev/null +++ b/qml/pages/SavedPlaylistsPage.qml @@ -0,0 +1,133 @@ +import QtQuick 2.0 +import Sailfish.Silica 1.0 + +Page { + id: page + + SilicaListView { + id: listView + anchors.fill: parent + + header: PageHeader { + title: qsTr("Saved Playlists") + } + + PullDownMenu { + MenuItem { + text: qsTr("Save current playlist") + onClicked: { + var dialog = pageStack.push(savePlaylistDialog) + } + } + } + + model: ListModel { + id: playlistModel + } + + delegate: ListItem { + id: delegate // ID hinzugefügt + width: parent.width + contentHeight: column.height + Theme.paddingMedium + + Column { + id: column + x: Theme.horizontalPageMargin + width: parent.width - 2*x + spacing: Theme.paddingSmall + + Label { + width: parent.width + text: model.name + color: delegate.highlighted ? Theme.highlightColor : Theme.primaryColor + truncationMode: TruncationMode.Fade + } + + Label { + width: parent.width + text: qsTr("Track %1 of %2").arg(model.position + 1).arg(model.trackCount) + font.pixelSize: Theme.fontSizeExtraSmall + color: Theme.secondaryColor + } + + Label { + width: parent.width + text: Qt.formatDateTime(new Date(model.lastPlayed), "dd.MM.yyyy hh:mm") + font.pixelSize: Theme.fontSizeExtraSmall + color: Theme.secondaryColor + } + } + + menu: ContextMenu { + MenuItem { + text: qsTr("Load") + onClicked: { + playlistManager.loadSavedPlaylist(model.name) + } + } + MenuItem { + text: qsTr("Delete") + onClicked: { + Remorse.itemAction(delegate, qsTr("Deleting"), function() { + playlistManager.deleteSavedPlaylist(model.name) + loadPlaylists() + }) + } + } + } + } + + + ViewPlaceholder { + enabled: listView.count === 0 + text: qsTr("No saved playlists") + hintText: qsTr("Pull down to save the current playlist") + } + + VerticalScrollDecorator {} + } + + Component { + id: savePlaylistDialog + + Dialog { + canAccept: nameField.text.length > 0 + + Column { + width: parent.width + spacing: Theme.paddingMedium + + DialogHeader { + title: qsTr("Save Playlist") + } + + TextField { + id: nameField + width: parent.width + placeholderText: qsTr("Enter playlist name") + label: qsTr("Playlist name") + EnterKey.enabled: text.length > 0 + EnterKey.iconSource: "image://theme/icon-m-enter-accept" + EnterKey.onClicked: accept() + } + } + + onAccepted: { + playlistManager.saveCurrentPlaylist(nameField.text) + loadPlaylists() + } + } + } + + function loadPlaylists() { + playlistModel.clear() + var playlists = playlistManager.getSavedPlaylists() + for(var i = 0; i < playlists.length; i++) { + playlistModel.append(playlists[i]) + } + } + + Component.onCompleted: { + loadPlaylists() + } +} diff --git a/qml/pages/Search.qml b/qml/pages/Search.qml index c82cbee..7e085e9 100644 --- a/qml/pages/Search.qml +++ b/qml/pages/Search.qml @@ -35,7 +35,7 @@ Item { id: searchField width: parent.width placeholderText: qsTr("Type and Search") - text: "Corvus Corax" + text: "" label: qsTr("Please wait for login ...") enabled: tidalApi.loginTrue diff --git a/qml/pages/TrackList.qml b/qml/pages/TrackList.qml index 9a0973e..a954e79 100644 --- a/qml/pages/TrackList.qml +++ b/qml/pages/TrackList.qml @@ -9,7 +9,8 @@ Item { property string playlistId: "" property int albumId: -1 property string type: "current" // "playlist" oder "current" oder "album" oder "tracklist" - + property int currentIndex: playlistManager.currentIndex + property alias model: listModel Timer { id: updateTimer interval: 100 // 100ms Verzögerung @@ -20,20 +21,20 @@ Item { var id = playlistManager.requestPlaylistItem(i) var track = cacheManager.getTrackInfo(id) if (track) { - listModel.append({ - "title": track.title, - "artist": track.artist, - "album": track.album, - "id": track.id, - "duration": track.duration, - "image": track.image, - "index": track.index - }) + listModel.append({ + "title": track.title, + "artist": track.artist, + "album": track.album, + "id": track.id, + "trackid": track.id, + "duration": track.duration, + "image": track.image, + "index": i + }) } else { console.log("No track data for index:", i) } } - //highlight_index = playlistManager.current_track } } @@ -70,6 +71,16 @@ Item { width: parent.width contentHeight: contentRow.height + Theme.paddingMedium + // Highlight für aktuellen Track + highlighted: type === "current" && model.index === root.currentIndex + + Rectangle { + visible: type === "current" && model.index === root.currentIndex + anchors.fill: parent + color: Theme.rgba(Theme.highlightBackgroundColor, 0.2) + z: -1 + } + Row { id: contentRow anchors { @@ -79,6 +90,17 @@ Item { } spacing: Theme.paddingMedium + // Optionaler Indikator für aktuellen Track + Label { + visible: type === "current" && model.index === root.currentIndex + text: "▶" // oder ein anderes Symbol + color: Theme.highlightColor + font.pixelSize: Theme.fontSizeMedium + width: visible ? implicitWidth : 0 + verticalAlignment: Text.AlignVCenter + height: coverImage.height + } + Image { id: coverImage width: Theme.itemSizeMedium @@ -101,9 +123,15 @@ Item { Label { width: parent.width text: model.title - color: listEntry.highlighted ? Theme.highlightColor : Theme.primaryColor + color: { + if (type === "current" && model.index === root.currentIndex) { + return Theme.highlightColor + } + return listEntry.highlighted ? Theme.highlightColor : Theme.primaryColor + } font.pixelSize: Theme.fontSizeMedium truncationMode: TruncationMode.Fade + font.bold: type === "current" && model.index === root.currentIndex } Row { @@ -112,7 +140,12 @@ Item { Label { text: model.artist - color: listEntry.highlighted ? Theme.secondaryHighlightColor : Theme.secondaryColor + color: { + if (type === "current" && model.index === root.currentIndex) { + return Theme.secondaryHighlightColor + } + return listEntry.highlighted ? Theme.secondaryHighlightColor : Theme.secondaryColor + } font.pixelSize: Theme.fontSizeSmall } @@ -127,18 +160,35 @@ Item { ? Format.formatDuration(model.duration, Formatter.DurationLong) : Format.formatDuration(model.duration, Formatter.DurationShort) text: dur - color: listEntry.highlighted ? Theme.secondaryHighlightColor : Theme.secondaryColor + color: { + if (type === "current" && model.index === root.currentIndex) { + return Theme.secondaryHighlightColor + } + return listEntry.highlighted ? Theme.secondaryHighlightColor : Theme.secondaryColor + } font.pixelSize: Theme.fontSizeSmall } } } } + onClicked: { + if (type === "current") { + playlistManager.playPosition(Math.floor(model.index)) // Stelle sicher, dass es ein Integer ist + } else { + playlistManager.playTrack(model.trackid) + } + } + menu: ContextMenu { MenuItem { text: qsTr("Play Now") onClicked: { - playlistManager.playTrack(model.trackid) + if (type === "current") { + playlistManager.playPosition(Math.floor(model.index)) // Stelle sicher, dass es ein Integer ist + } else { + playlistManager.playTrack(model.trackid) + } } } MenuItem { @@ -146,15 +196,14 @@ Item { onClicked: { playlistManager.appendTrack(model.trackid) } + visible: type !== "current" } - } - - onClicked: { - if (type === "current") { - playlistManager.playPosition(model.index) - - } else { - playlistManager.playTrack(model.trackid) + MenuItem { + text: qsTr("Remove from Queue") + onClicked: { + // TODO: Implementiere Remove-Funktion + } + visible: type === "current" } } } @@ -172,14 +221,10 @@ Item { Component.onCompleted: { if (type === "playlist") { - // Playlist-Tracks laden tidalApi.getPlaylistTracks(playlistId) - } else if (type == "album") - { + } else if (type == "album") { tidalApi.getAlbumTracks(albumId) - } - else { - // Aktuelle Playlist laden + } else { playlistManager.generateList() } } @@ -212,7 +257,7 @@ Item { } } - onTopTracksofArtist: { + onTopTracksofArtist: { if (type === "tracklist") { listModel.append({ "title": track_info.title, @@ -234,7 +279,7 @@ Item { "title": title, "artist": artist, "album": album, - "trackid": trackid, + "trackid": id, "duration": duration, "image": image, "index": index @@ -242,8 +287,14 @@ Item { } } + onCurrentTrack: { + if (type === "current") { + tracks.positionViewAtIndex(position, ListView.Center) + } + } + onListChanged: { - console.log("update playlist") + console.log("update playlist") if (type === "current") { console.log("update current playlist") listModel.clear() diff --git a/qml/playlistmanager.py b/qml/playlistmanager.py index 6ec21b3..243f362 100644 --- a/qml/playlistmanager.py +++ b/qml/playlistmanager.py @@ -69,10 +69,13 @@ def PreviousTrack(self): def PlayPosition(self, position): """Spielt einen Track an einer bestimmten Position""" - if 0 <= position < len(self.playlist): - self.current_index = position - self._notify_current_track() - #self._notify_playlist_state() + try: + position = int(position) # Konvertiere zu Integer + if 0 <= position < len(self.playlist): + self.current_index = position + self._notify_current_track() + except (ValueError, TypeError): + print(f"Invalid position value: {position}") def RestartTrack(self): """Startet den aktuellen Track neu"""