diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7ca11f79648d..2f5281d7d89a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -559,6 +559,79 @@ jobs: name: ${{ matrix.artifacts_name }} path: ${{ matrix.artifacts_path }} + build-flatpak: + name: "Flatpak" + container: + image: ghcr.io/flathub-infra/flatpak-github-actions:kde-6.10 + options: --privileged + volumes: + - /usr:/host/usr + - /opt:/host/opt + strategy: + matrix: + variant: + - arch: x86_64 + runner: ubuntu-24.04 + - arch: aarch64 + runner: ubuntu-24.04-arm + runs-on: ${{ matrix.variant.runner }} + steps: + - name: "Recover host disk space" + run: | + rm -rf /host/opt/hostedtoolcache/CodeQL + rm -rf /host/opt/hostedtoolcache/go + rm -rf /host/usr/local/lib/android + rm -rf /host/usr/local/.ghcup + rm -rf /host/usr/local/share/powershell + rm -rf /host/usr/share/swift + rm -rf /host/usr/share/dotnet + + - name: "Check out repository" + uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: "Store Git version" + run: | + git config --global --add safe.directory "${GITHUB_WORKSPACE}" + GIT_DESC=$(git describe --always --first-parent --dirty=-modified) + if [ -z "$GIT_DESC" ]; then + GIT_DESC="unknown" + fi + echo "GIT_DESC=$GIT_DESC" >> $GITHUB_ENV + + - name: "Build Flatpak" + uses: flatpak/flatpak-github-actions/flatpak-builder@v6 + with: + manifest-path: packaging/flatpak/org.mixxx.Mixxx.yaml + arch: ${{ matrix.variant.arch }} + build-bundle: false + upload-artifact: false + + - name: "Create Flatpak bundle" + run: | + flatpak build-bundle repo \ + --arch=${{ matrix.variant.arch }} \ + Mixxx-${GIT_DESC}-${{ matrix.variant.arch }}.flatpak org.mixxx.Mixxx + + - name: "Create Debug extension" + run: | + flatpak build-bundle repo \ + --arch=${{ matrix.variant.arch }} \ + --runtime Mixxx-${GIT_DESC}-${{ matrix.variant.arch }}.Debug.flatpak org.mixxx.Mixxx.Debug + + - name: "Upload Flatpak bundle" + uses: actions/upload-artifact@v6 + with: + name: Flatpak ${{ matrix.variant.arch }} + path: Mixxx-${{ env.GIT_DESC }}-${{ matrix.variant.arch }}.flatpak + + - name: "Upload Debug extension" + uses: actions/upload-artifact@v6 + with: + name: Flatpak Debug Extension ${{ matrix.variant.arch }} + path: Mixxx-${{ env.GIT_DESC }}-${{ matrix.variant.arch }}.Debug.flatpak + update_manifest: name: "Update manifest file on download server" runs-on: ubuntu-latest diff --git a/lib/reverb/Reverb.cc b/lib/reverb/Reverb.cc index 7e0bf2e56f61..1049235823f3 100644 --- a/lib/reverb/Reverb.cc +++ b/lib/reverb/Reverb.cc @@ -450,7 +450,7 @@ void MixxxPlateX2::processBuffer(const sample_t* in, sample_t* out, const uint f double damp = exp(-M_PI * (.0005+.9995*dampingParam)); tank.damping[0].set(damp); tank.damping[1].set(damp); - RampingValue send(pow(currentSend, 1.53), previousSend, frames); + RampingValue send(pow(previousSend, 1.53), pow(currentSend, 1.53), frames); // the modulated lattices interpolate, which needs truncated float DSP::FPTruncateMode _truncate; diff --git a/src/effects/backends/builtin/echoeffect.cpp b/src/effects/backends/builtin/echoeffect.cpp index 37306f88f1de..bfcb3cbe5aa4 100644 --- a/src/effects/backends/builtin/echoeffect.cpp +++ b/src/effects/backends/builtin/echoeffect.cpp @@ -167,13 +167,13 @@ void EchoEffect::processChannel( int read_position = pGroupState->write_position; decrementRing(&read_position, delay_samples, pGroupState->delay_buf.size()); - RampingValue send(send_current, - pGroupState->prev_send, + RampingValue send(pGroupState->prev_send, + send_current, engineParameters.framesPerBuffer()); // Feedback the delay buffer and then add the new input. - RampingValue feedback(feedback_current, - pGroupState->prev_feedback, + RampingValue feedback(pGroupState->prev_feedback, + feedback_current, engineParameters.framesPerBuffer()); int rampIndex = 0; diff --git a/src/library/dao/playlistdao.cpp b/src/library/dao/playlistdao.cpp index c5270cc8422c..46a687a2a0e3 100644 --- a/src/library/dao/playlistdao.cpp +++ b/src/library/dao/playlistdao.cpp @@ -13,7 +13,8 @@ #include "util/math.h" PlaylistDAO::PlaylistDAO() - : m_pAutoDJProcessor(nullptr) { + : m_currentHistoryPlaylist(kInvalidPlaylistId), + m_pAutoDJProcessor(nullptr) { } void PlaylistDAO::initialize(const QSqlDatabase& database) { @@ -330,19 +331,35 @@ bool PlaylistDAO::deleteUnlockedPlaylists(QStringList&& idStringList) { } bool PlaylistDAO::deleteAllUnlockedPlaylistsWithFewerTracks( - PlaylistDAO::HiddenType type, int minNumberOfTracks) { + PlaylistDAO::HiddenType type, + int minNumberOfTracks, + bool skipCurrHistory) { + // Note: this slot is also called after purging tracks in order to delete + // now empty history playlists. + // Though, if the current History is now also empty, we must not delete that! + // Else the following session log is lost -- or rather not recorded in the + // first place since the id passed to appendTrackToPlaylist() does not exist + // anymore. + // skipCurrHistory prevents that. VERIFY_OR_DEBUG_ASSERT(minNumberOfTracks > 0) { return false; // nothing to do, probably unintended invocation } QSqlQuery query(m_database); - query.prepare(QStringLiteral( + QString queryString = QStringLiteral( "SELECT id FROM Playlists " "WHERE (SELECT count(playlist_id) FROM PlaylistTracks WHERE " "Playlists.ID = PlaylistTracks.playlist_id) < :length AND " - "Playlists.hidden = :hidden AND Playlists.locked = 0")); + "Playlists.hidden = :hidden AND Playlists.locked = 0"); + if (skipCurrHistory) { + queryString.append(QStringLiteral(" AND Playlists.ID != :currHistoryId")); + } + query.prepare(queryString); query.bindValue(":hidden", static_cast(type)); query.bindValue(":length", minNumberOfTracks); + if (skipCurrHistory) { + query.bindValue(":currHistoryId", m_currentHistoryPlaylist); + } if (!query.exec()) { LOG_FAILED_QUERY(query); return false; @@ -352,6 +369,11 @@ bool PlaylistDAO::deleteAllUnlockedPlaylistsWithFewerTracks( while (query.next()) { idStringList.append(query.value(0).toString()); } + + if (idStringList.isEmpty()) { + return false; + } + qInfo() << "Prepared deletion of" << idStringList.size() << "playlists of type" << type << "that contain fewer than" << minNumberOfTracks << "tracks"; @@ -481,9 +503,29 @@ bool PlaylistDAO::removeTracksFromPlaylist(int playlistId, int startIndex) { return true; } +bool PlaylistDAO::playlistExists(const int playlistId) const { + ScopedTransaction transaction(m_database); + QSqlQuery query(m_database); + query.prepare(QStringLiteral("SELECT id FROM Playlists WHERE id = :id")); + query.bindValue(":id", playlistId); + + if (!query.exec()) { + LOG_FAILED_QUERY(query); + return false; + } + + if (query.next()) { + // id is guaranteed to be unique, so we can return here + return true; + } + // not found + return false; +} + bool PlaylistDAO::appendTracksToPlaylist(const QList& trackIds, const int playlistId) { // qDebug() << "PlaylistDAO::appendTracksToPlaylist" // << QThread::currentThread() << m_database.connectionName(); + DEBUG_ASSERT(playlistExists(playlistId)); // Start the transaction ScopedTransaction transaction(m_database); @@ -1094,7 +1136,8 @@ void PlaylistDAO::removeTracksFromPlaylists(const QList& trackIds, bool transaction.commit(); // We may now have empty history playlists. Remove them. - deleteAllUnlockedPlaylistsWithFewerTracks(PlaylistDAO::PLHT_SET_LOG, 1); + // Note: does not delete current History playlist. + deleteAllUnlockedPlaylistsWithFewerTracks(PlaylistDAO::PLHT_SET_LOG, 1, true); // update the sidebar emit playlistContentChanged(playlistIds); diff --git a/src/library/dao/playlistdao.h b/src/library/dao/playlistdao.h index f963cb03a806..e6248334d5c0 100644 --- a/src/library/dao/playlistdao.h +++ b/src/library/dao/playlistdao.h @@ -48,7 +48,8 @@ class PlaylistDAO : public QObject, public virtual DAO { /// Needs to be called inside a transaction. /// @return true on success, false on error bool deleteAllUnlockedPlaylistsWithFewerTracks(const PlaylistDAO::HiddenType type, - int minNumberOfTracks); + int minNumberOfTracks, + bool skipCurrHistory = false); // Rename a playlist void renamePlaylist(const int playlistId, const QString& newName); // Lock or unlock a playlist @@ -57,6 +58,8 @@ class PlaylistDAO : public QObject, public virtual DAO { int setPlaylistsLocked(const QSet& playlistIds, const bool lock); // Find out the state of a playlist lock bool isPlaylistLocked(const int playlistId) const; + // Check if a playlist exists + bool playlistExists(const int playlistId) const; // Append a list of tracks to a playlist bool appendTracksToPlaylist(const QList& trackIds, const int playlistId); // Append a track to a playlist @@ -120,6 +123,10 @@ class PlaylistDAO : public QObject, public virtual DAO { void getPlaylistsTrackIsIn(TrackId trackId, QSet* playlistSet) const; + void setCurrentHistoryPlaylistId(int id) { + m_currentHistoryPlaylist = id; + } + void setAutoDJProcessor(AutoDJProcessor* pAutoDJProcessor); signals: @@ -151,6 +158,7 @@ class PlaylistDAO : public QObject, public virtual DAO { void populatePlaylistMembershipCache(); QMultiHash m_playlistsTrackIsIn; + int m_currentHistoryPlaylist; AutoDJProcessor* m_pAutoDJProcessor; DISALLOW_COPY_AND_ASSIGN(PlaylistDAO); }; diff --git a/src/library/trackset/setlogfeature.cpp b/src/library/trackset/setlogfeature.cpp index 093ff3703eb3..cfff3fddf90a 100644 --- a/src/library/trackset/setlogfeature.cpp +++ b/src/library/trackset/setlogfeature.cpp @@ -383,6 +383,7 @@ void SetlogFeature::slotGetNewPlaylist() { << set_log_name; } else { m_recentTracks.clear(); + m_playlistDao.setCurrentHistoryPlaylistId(m_currentPlaylistId); } // reload child model again because the 'added' signal fired by PlaylistDAO @@ -447,6 +448,7 @@ void SetlogFeature::slotJoinWithPrevious() { // Change current setlog m_currentPlaylistId = previousPlaylistId; + m_playlistDao.setCurrentHistoryPlaylistId(m_currentPlaylistId); } qDebug() << "slotJoinWithPrevious() current:" << clickedPlaylistId diff --git a/src/widget/wlibrarytableview.cpp b/src/widget/wlibrarytableview.cpp index cb684bede547..6e3c22ef221c 100644 --- a/src/widget/wlibrarytableview.cpp +++ b/src/widget/wlibrarytableview.cpp @@ -111,15 +111,45 @@ bool WLibraryTableView::restoreTrackModelState( verticalScrollBar()->setValue(state->verticalScrollPosition); horizontalScrollBar()->setValue(state->horizontalScrollPosition); - auto* pSelection = selectionModel(); - pSelection->clearSelection(); + // Build a selection range rather than selecting each track individually, + // which can lag the GUI by spamming selectionChanged() handlers. + QItemSelectionModel* pSelectionModel = selectionModel(); + pSelectionModel->clearSelection(); + QItemSelection newSelection; QModelIndexList selectedRows = state->selectedRows; + QModelIndex topLeft; + QModelIndex bottomRight; if (!selectedRows.isEmpty()) { - for (auto index : std::as_const(selectedRows)) { - pSelection->select(index, - QItemSelectionModel::Select | QItemSelectionModel::Rows); + // In saveTrackModelState() we fill state->selectedRows with the sorted + // rows, hence no need to sort here. + for (const QModelIndex& index : std::as_const(selectedRows)) { + if (!topLeft.isValid()) { + // start new range. only done once for first row + topLeft = index; + bottomRight = index; + continue; + } + + if (index.row() == bottomRight.row() + 1) { + // continuous range + bottomRight = index; + continue; + } else { + // prev index was end of range, add current range to selection + // and start a new one + newSelection.select(topLeft, bottomRight); + topLeft = index; + bottomRight = index; + } + } + + // If we reached end, submit the last selection + if (bottomRight == selectedRows.last()) { + newSelection.select(topLeft, bottomRight); } } + pSelectionModel->select(newSelection, + QItemSelectionModel::Select | QItemSelectionModel::Rows); QModelIndex currIndex = state->currentIndex; restoreCurrentIndex(currIndex);