Steve Lhomme pushed to branch master at VideoLAN / VLC


Commits:
d0c0f0d6 by Arpit Benjamin at 2026-02-18T08:17:22+00:00
qt: introduce `MLItemId` serialization

- - - - -
cf7940a2 by Arpit Benjamin at 2026-02-18T08:17:22+00:00
qt: add "album_id" role in `MLAudioModel`

- - - - -
253253ed by Arpit Benjamin at 2026-02-18T08:17:22+00:00
qt: add `MLAudio::getAlbumId()`

- - - - -
8a70214d by Fatih Uzunoglu at 2026-02-18T08:17:22+00:00
qt: use `INVALID_MLITEMID_ID` instead of `-1` in `MLItemId::fromString()`

- - - - -
6d2de733 by Fatih Uzunoglu at 2026-02-18T08:17:22+00:00
qt: introduce `MLItemId::isValid()`

- - - - -
d5c4520f by Fatih Uzunoglu at 2026-02-18T08:17:22+00:00
qml: introduce `sortingFromHeader` property in `TableViewExt`

Co-authored-by: Arpit Benjamin <[email protected]>

- - - - -
8d3b83c2 by Fatih Uzunoglu at 2026-02-18T08:17:22+00:00
qml: introduce `useCurrentSectionLabel` property in `TableViewExt`

Co-authored-by: Arpit Benjamin <[email protected]>

- - - - -
3e46ea41 by Fatih Uzunoglu at 2026-02-18T08:17:22+00:00
qml: expose `contentItem` in `TableViewExt`

- - - - -
deb1b8c7 by Fatih Uzunoglu at 2026-02-18T08:17:22+00:00
qml: expose `currentSection` in `TableViewExt`

- - - - -
0f3047b2 by Fatih Uzunoglu at 2026-02-18T08:17:22+00:00
qml: introduce `MusicAlbumSectionDelegate`

Co-authored-by: Arpit Benjamin <[email protected]>

- - - - -
09f6cdd2 by Fatih Uzunoglu at 2026-02-18T08:17:22+00:00
qml: forward the sort menu in `PageLoader`

- - - - -
32cce276 by Fatih Uzunoglu at 2026-02-18T08:17:22+00:00
qt: add setting `album-sections`

- - - - -
d8aa5705 by Fatih Uzunoglu at 2026-02-18T08:17:22+00:00
qt: introduce `SortMenuAlbums`

Which contains an action that toggles album sections.

- - - - -
07a81dd7 by Fatih Uzunoglu at 2026-02-18T08:17:22+00:00
qt: register `SortMenuAlbums`

- - - - -
4bdc5b32 by Fatih Uzunoglu at 2026-02-18T08:17:22+00:00
qml: use `SortMenuAlbums` in `MusicArtistsAlbums`

- - - - -
95c40bb1 by Fatih Uzunoglu at 2026-02-18T08:17:22+00:00
qml: fix icon does not respect alpha component of color in `ButtonExt`

- - - - -
a08c1491 by Fatih Uzunoglu at 2026-02-18T08:17:22+00:00
qml: use `MusicAlbumSectionDelegate` in `MusicArtist`

Co-authored-by: Arpit Benjamin <[email protected]>

- - - - -
8996a4ed by Fatih Uzunoglu at 2026-02-18T08:17:22+00:00
qml: disable section change buttons in `MusicArtist`

Currently it does not work reliably, even though
theoretically it should work fine. This is possibly
about `contentHeight` not being calculated by the
view when sections are used, rather than the logic
we have at the moment for changing the section.

Until a workaround is found, this patch disables
the sections buttons.

- - - - -
4786e505 by Fatih Uzunoglu at 2026-02-18T08:17:22+00:00
qml: make use of sort and search properties in list mode in `MusicArtist`

The horizontal album view is gone, so the sort menu should
be applicable to the audio model that is used by the sole
vertical list view.

- - - - -
49c0baf1 by Fatih Uzunoglu at 2026-02-18T08:17:22+00:00
qml: do not show album column if there are album sections in `MusicArtist`

... and display track number instead.

- - - - -


22 changed files:

- modules/gui/qt/Makefile.am
- modules/gui/qt/maininterface/mainctx.cpp
- modules/gui/qt/maininterface/mainctx.hpp
- modules/gui/qt/maininterface/mainui.cpp
- modules/gui/qt/medialibrary/medialib.cpp
- modules/gui/qt/medialibrary/medialib.hpp
- modules/gui/qt/medialibrary/mlaudio.cpp
- modules/gui/qt/medialibrary/mlaudio.hpp
- modules/gui/qt/medialibrary/mlaudiomodel.cpp
- modules/gui/qt/medialibrary/mlaudiomodel.hpp
- modules/gui/qt/medialibrary/mlqmltypes.hpp
- + modules/gui/qt/medialibrary/qml/MusicAlbumSectionDelegate.qml
- modules/gui/qt/medialibrary/qml/MusicArtist.qml
- modules/gui/qt/medialibrary/qml/MusicArtistsAlbums.qml
- modules/gui/qt/medialibrary/qml/MusicArtistsDisplay.qml
- modules/gui/qt/menus/qml_menu_wrapper.cpp
- modules/gui/qt/menus/qml_menu_wrapper.hpp
- modules/gui/qt/meson.build
- modules/gui/qt/widgets/qml/ButtonExt.qml
- modules/gui/qt/widgets/qml/PageLoader.qml
- modules/gui/qt/widgets/qml/TableViewExt.qml
- po/POTFILES.in


Changes:

=====================================
modules/gui/qt/Makefile.am
=====================================
@@ -1043,7 +1043,8 @@ libqml_module_medialibrary_a_QML = \
        medialibrary/qml/VideoGridItem.qml \
        medialibrary/qml/VideoInfoExpandPanel.qml \
        medialibrary/qml/VideoListDisplay.qml \
-       medialibrary/qml/VideoGridDisplay.qml
+       medialibrary/qml/VideoGridDisplay.qml \
+       medialibrary/qml/MusicAlbumSectionDelegate.qml
 nodist_libqml_module_medialibrary_a_SOURCES = medialibrary_qmlassets.cpp
 $(libqml_module_medialibrary_a_QML:.qml=.cpp) : 
$(builddir)/medialibrary/res.qrc
 $(libqml_module_medialibrary_a_QML:.qml=.cpp) : 
QML_CACHEGEN_ARGS=--resource=$(builddir)/medialibrary/res.qrc


=====================================
modules/gui/qt/maininterface/mainctx.cpp
=====================================
@@ -321,6 +321,7 @@ MainCtx::~MainCtx()
 
     settings->setValue( "grid-view", m_gridView );
     settings->setValue( "grouping", m_grouping );
+    settings->setValue( "album-sections", m_albumSections );
 
     settings->setValue( "color-scheme-index", m_colorScheme->currentIndex() );
     /* Save the stackCentralW sizes */
@@ -468,6 +469,8 @@ void MainCtx::loadFromSettingsImpl(const bool callSignals)
 
     loadFromSettings(m_showRemainingTime, "MainWindow/ShowRemainingTime", 
false, &MainCtx::showRemainingTimeChanged);
 
+    loadFromSettings(m_albumSections, "MainWindow/album-sections", true, 
&MainCtx::albumSectionsChanged);
+
     const auto colorSchemeIndex = getSettings()->value( 
"MainWindow/color-scheme-index", 0 ).toInt();
     m_colorScheme->setCurrentIndex(colorSchemeIndex);
 
@@ -715,6 +718,15 @@ void MainCtx::setGrouping(Grouping grouping)
     emit groupingChanged(grouping);
 }
 
+void MainCtx::setAlbumSections(bool enabled)
+{
+    if (m_albumSections == enabled)
+        return;
+
+    m_albumSections = enabled;
+    emit albumSectionsChanged(enabled);
+}
+
 void MainCtx::setInterfaceAlwaysOnTop( bool on_top )
 {
     if (b_interfaceOnTop == on_top)


=====================================
modules/gui/qt/maininterface/mainctx.hpp
=====================================
@@ -129,6 +129,7 @@ class MainCtx : public QObject
     Q_PROPERTY(float safeArea READ safeArea NOTIFY safeAreaChanged FINAL)
     Q_PROPERTY(VideoSurfaceProvider* videoSurfaceProvider READ 
getVideoSurfaceProvider WRITE setVideoSurfaceProvider NOTIFY 
hasEmbededVideoChanged FINAL)
     Q_PROPERTY(int mouseHideTimeout READ mouseHideTimeout NOTIFY 
mouseHideTimeoutChanged FINAL)
+    Q_PROPERTY(bool albumSections READ albumSections WRITE setAlbumSections 
NOTIFY albumSectionsChanged FINAL)
 
     Q_PROPERTY(CSDButtonModel *csdButtonModel READ csdButtonModel CONSTANT 
FINAL)
 
@@ -219,6 +220,7 @@ public:
     inline MediaLib* getMediaLibrary() const { return m_medialib; }
     inline bool hasGridView() const { return m_gridView; }
     inline Grouping grouping() const { return m_grouping; }
+    inline bool albumSections() const { return m_albumSections; }
     inline ColorSchemeModel* getColorScheme() const { return m_colorScheme; }
     bool hasVLM() const;
     bool useClientSideDecoration() const;
@@ -450,6 +452,8 @@ protected:
 
     int m_mouseHideTimeout = 1000;
 
+    bool m_albumSections = true;
+
     OsType m_osName;
     int m_osVersion;
 
@@ -480,6 +484,7 @@ public slots:
     void setShowRemainingTime( bool );
     void setGridView( bool );
     void setGrouping( Grouping );
+    void setAlbumSections( bool );
     void incrementIntfUserScaleFactor( bool increment);
     void setIntfUserScaleFactor( double );
     void setHasToolbarMenu( bool );
@@ -530,6 +535,7 @@ signals:
     void gridViewChanged( bool );
     void hasGridListModeChanged( bool );
     void groupingChanged( Grouping );
+    void albumSectionsChanged( bool );
     void colorSchemeChanged( QString );
     void useClientSideDecorationChanged();
     void hasToolbarMenuChanged();


=====================================
modules/gui/qt/maininterface/mainui.cpp
=====================================
@@ -243,6 +243,7 @@ void MainUI::registerQMLTypes()
         qmlRegisterType<StringListMenu>( uri, versionMajor, versionMinor, 
"StringListMenu" );
         qmlRegisterType<SortMenu>( uri, versionMajor, versionMinor, "SortMenu" 
);
         qmlRegisterType<SortMenuVideo>( uri, versionMajor, versionMinor, 
"SortMenuVideo" );
+        qmlRegisterType<SortMenuAlbums>( uri, versionMajor, versionMinor, 
"SortMenuAlbums" );
         qmlRegisterType<QmlGlobalMenu>( uri, versionMajor, versionMinor, 
"QmlGlobalMenu" );
         qmlRegisterType<QmlMenuBar>( uri, versionMajor, versionMinor, 
"QmlMenuBar" );
 


=====================================
modules/gui/qt/medialibrary/medialib.cpp
=====================================
@@ -107,6 +107,10 @@ static void 
convertQVariantListToPlaylistMedias(vlc_medialibrary_t* ml, QVariant
     }
 }
 
+MLItemId MediaLib::deserializeMlItemIdFromString(const QString& serialized_id) 
{
+    return MLItemId::fromString(serialized_id);
+}
+
 void MediaLib::addToPlaylist(const QString& mrl, const QStringList &options)
 {
     QVector<vlc::playlist::Media> medias;


=====================================
modules/gui/qt/medialibrary/medialib.hpp
=====================================
@@ -46,6 +46,8 @@ public:
     MediaLib(qt_intf_t* _intf, vlc::playlist::PlaylistController* 
playlistController, QObject* _parent = nullptr );
     ~MediaLib();
 
+    Q_INVOKABLE static MLItemId deserializeMlItemIdFromString(const QString& 
serialized_id);
+
     Q_INVOKABLE void addToPlaylist(const MLItemId &itemId, const QStringList 
&options = {});
     Q_INVOKABLE void addToPlaylist(const QString& mrl, const QStringList 
&options = {});
     Q_INVOKABLE void addToPlaylist(const QUrl& mrl, const QStringList &options 
= {});


=====================================
modules/gui/qt/medialibrary/mlaudio.cpp
=====================================
@@ -32,8 +32,10 @@ MLAudio::MLAudio(vlc_medialibrary_t* _ml, const 
vlc_ml_media_t *_data)
     if ( _data->album_track.i_album_id != 0 )
     {
         ml_unique_ptr<vlc_ml_album_t> album(vlc_ml_get_album(_ml, 
_data->album_track.i_album_id));
-        if (album)
+        if (album) {
+            m_albumId =  album->i_id;
             m_albumTitle =  album->psz_title;
+        }
     }
 
     if ( _data->album_track.i_artist_id != 0 )
@@ -44,6 +46,11 @@ MLAudio::MLAudio(vlc_medialibrary_t* _ml, const 
vlc_ml_media_t *_data)
     }
 }
 
+MLItemId MLAudio::getAlbumId() const {
+    return {m_albumId, VLC_ML_PARENT_ALBUM};
+}
+
+
 QString MLAudio::getAlbumTitle() const
 {
     return m_albumTitle;


=====================================
modules/gui/qt/medialibrary/mlaudio.hpp
=====================================
@@ -33,10 +33,12 @@ public:
     QString getArtist() const;
     unsigned int getTrackNumber() const;
     unsigned int getDiscNumber() const;
+    MLItemId getAlbumId() const;
 
 private:
     QString m_albumTitle;
     QString m_artist;
     unsigned int m_trackNumber;
     unsigned int m_discNumber;
+    int64_t m_albumId;
 };


=====================================
modules/gui/qt/medialibrary/mlaudiomodel.cpp
=====================================
@@ -44,6 +44,8 @@ QVariant MLAudioModel::itemRoleData(const MLItem *item, const 
int role) const
         return QVariant::fromValue(audio->getAlbumTitle());
     case AUDIO_ALBUM_FIRST_SYMBOL:
         return QVariant::fromValue(getFirstSymbol(audio->getAlbumTitle()));
+    case AUDIO_ALBUM_ID:
+        return QVariant::fromValue(audio->getAlbumId().toString());
     default:
         return MLMediaModel::itemRoleData(item, role);
     }
@@ -63,6 +65,7 @@ QHash<int, QByteArray> MLAudioModel::roleNames() const
         {AUDIO_ARTIST_FIRST_SYMBOL, "main_artist_first_symbol"},
         {AUDIO_ALBUM, "album_title"},
         {AUDIO_ALBUM_FIRST_SYMBOL, "album_title_first_symbol"},
+        {AUDIO_ALBUM_ID, "album_id"},
     });
 
     return hash;


=====================================
modules/gui/qt/medialibrary/mlaudiomodel.hpp
=====================================
@@ -39,7 +39,9 @@ public:
         AUDIO_ARTIST_FIRST_SYMBOL,
         AUDIO_ALBUM,
         AUDIO_ALBUM_FIRST_SYMBOL,
-    };
+        AUDIO_ALBUM_ID,
+    }
+    ;
 
 public:
     explicit MLAudioModel(QObject *parent = nullptr);


=====================================
modules/gui/qt/medialibrary/mlqmltypes.hpp
=====================================
@@ -23,12 +23,23 @@
 # include "config.h"
 #endif
 
+#include <QHash>
 #include <QObject>
 #include <vlc_common.h>
 #include <vlc_media_library.h>
 
 static constexpr int64_t INVALID_MLITEMID_ID = 0;
 
+static const QHash<QStringView, vlc_ml_parent_type> ml_parent_map = {
+    { QStringLiteral("VLC_ML_PARENT_ALBUM"), VLC_ML_PARENT_ALBUM },
+    { QStringLiteral("VLC_ML_PARENT_ARTIST"), VLC_ML_PARENT_ARTIST },
+    { QStringLiteral("VLC_ML_PARENT_SHOW"), VLC_ML_PARENT_SHOW },
+    { QStringLiteral("VLC_ML_PARENT_GENRE"), VLC_ML_PARENT_GENRE },
+    { QStringLiteral("VLC_ML_PARENT_GROUP"), VLC_ML_PARENT_GROUP },
+    { QStringLiteral("VLC_ML_PARENT_FOLDER"), VLC_ML_PARENT_FOLDER },
+    { QStringLiteral("VLC_ML_PARENT_PLAYLIST"), VLC_ML_PARENT_PLAYLIST }
+};
+
 class MLItemId
 {
     Q_GADGET
@@ -51,6 +62,10 @@ public:
     int64_t id;
     vlc_ml_parent_type type;
 
+    Q_INVOKABLE bool isValid() const {
+        return (id != INVALID_MLITEMID_ID);
+    }
+
     Q_INVOKABLE constexpr bool hasParent() const {
         return (type != VLC_ML_PARENT_UNKNOWN);
     }
@@ -71,6 +86,22 @@ public:
         }
 #undef ML_PARENT_TYPE_CASE
     }
+
+    Q_INVOKABLE static inline MLItemId fromString(const QStringView& 
serialized_id) {
+        const QList<QStringView> parts = serialized_id.split('-'); // Type, ID
+        if (parts.length() != 2) {
+            return {INVALID_MLITEMID_ID, VLC_ML_PARENT_UNKNOWN};
+        }
+
+        const QStringView type = parts[0].trimmed();
+        bool conversionSuccessful = false;
+        std::int64_t item_id = 
parts[1].trimmed().toLongLong(&conversionSuccessful);
+        if (!conversionSuccessful) {
+            return {INVALID_MLITEMID_ID, VLC_ML_PARENT_UNKNOWN};
+        }
+
+        return { item_id, ml_parent_map.value(type, VLC_ML_PARENT_UNKNOWN) };
+    }
 };
 
 


=====================================
modules/gui/qt/medialibrary/qml/MusicAlbumSectionDelegate.qml
=====================================
@@ -0,0 +1,399 @@
+/*****************************************************************************
+ * Copyright (C) 2025 VLC authors and VideoLAN
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * ( at your option ) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, 
USA.
+ *****************************************************************************/
+import QtQuick
+import QtQuick.Layouts
+import QtQuick.Templates as T
+
+import VLC.MainInterface
+import VLC.Widgets as Widgets
+import VLC.Style
+import VLC.Util
+import VLC.MediaLibrary
+
+T.Pane {
+    id: root
+
+    implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset,
+                            implicitContentWidth + leftPadding + rightPadding)
+    implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset,
+                             implicitContentHeight + topPadding + 
bottomPadding)
+
+    verticalPadding: VLCStyle.margin_small
+    horizontalPadding: VLCStyle.margin_normal
+
+    spacing: VLCStyle.margin_xsmall
+
+    required property string section
+
+    required property MLAlbumModel model
+
+    property bool retainWhileLoading: true // akin to 
`Image::retainWhileLoading`
+
+    property bool previousSectionButtonEnabled: true
+    property bool nextSectionButtonEnabled: true
+
+    // For preventing initial twitching, and to prevent loading fallback just 
to discard it immediately right after:
+    property bool _initialFetchCompleted: false
+
+    property var _albumData
+    readonly property url _albumCover: _initialFetchCompleted ? 
((_albumData?.cover && (_albumData.cover !== "")) ? _albumData.cover
+                                                                               
                                   : VLCStyle.noArtAlbumCover)
+                                                              : ""
+
+    signal changeToNextSectionRequested()
+    signal changeToPreviousSectionRequested()
+
+    readonly property ColorContext theme: ColorContext {
+        id: theme
+        colorSet: ColorContext.View
+    }
+
+    Component.onCompleted: {
+        Qt.callLater(root.fetchAlbumData)
+    }
+
+    onModelChanged: {
+        Qt.callLater(root.fetchAlbumData)
+    }
+
+    onSectionChanged: {
+        Qt.callLater(root.fetchAlbumData)
+    }
+
+    Connections {
+        target: root.model
+
+        function onLoadingChanged() {
+            Qt.callLater(root.fetchAlbumData)
+        }
+
+        function onLayoutChanged() {
+            Qt.callLater(root.fetchAlbumData)
+        }
+
+        function onDataChanged() {
+            Qt.callLater(root.fetchAlbumData)
+        }
+
+        function onModelReset() {
+            Qt.callLater(root.fetchAlbumData)
+        }
+    }
+
+    function fetchAlbumData() {
+        if (!root.model)
+            return
+
+        if (root.model.loading)
+            return
+
+        if (section.length === 0)
+            return
+
+        if (!root.retainWhileLoading)
+            root._albumData = null
+
+        const mlItem = MediaLib.deserializeMlItemIdFromString(section)
+        console.assert(mlItem.isValid())
+
+        root.model.getDataById(mlItem).then((albumData, taskId) => {
+            root._albumData = albumData
+            root._initialFetchCompleted = true
+        })
+    }
+
+    background: Rectangle {
+        visible: root._initialFetchCompleted
+
+        // NOTE: Transparent rectangle has an optimization that it does not 
use a scene graph node.
+        color: blurEffect.visible ? "transparent" : (theme.palette?.isDark ? 
"black" : "white")
+
+        // NOTE: `FrostedGlassEffect` is purposefully not used here. It should 
be only used when depth
+        //       is relevant and exposed to the user, such as for popups, or 
the mini player (mini
+        //       player is placed on top of the main view and naturally gives 
that impression to the
+        //       user). Here, one way to provide that would be having parallax 
scrolling, but for now
+        //       it is not used.
+        Widgets.DualKawaseBlur {
+            id: blurEffect
+
+            anchors.left: parent.left
+            anchors.right: parent.right
+            anchors.verticalCenter: parent.verticalCenter
+
+            height: source ? ((source.implicitHeight / source.implicitWidth) * 
width) : implicitHeight
+
+            // Instead of clipping in the parent, denote the viewport here so 
we both
+            // do not need to clip the excess, and also save significant video 
memory:
+            viewportRect: Qt.rect((width - parent.width) / 2, (height - 
parent.height) / 2,
+                                  parent.width, parent.height)
+
+            mode: Widgets.DualKawaseBlur.TwoPass
+            radius: 1
+
+            // Sections are re-used, but they may not release GPU resources 
immediately.
+            // This ensures resources are freed to limit peak VRAM consumption.
+            source: visible ? root.contentItem.artworkTextureProvider : null
+
+            visible: (GraphicsInfo.shaderType === GraphicsInfo.RhiShader) &&
+                     !!root.contentItem?.artworkTextureProvider
+
+            postprocess: true
+            tint: root.theme.palette?.isDark ? "black" : "white"
+            tintStrength: 0.7
+            backgroundColor: theme.bg.secondary
+        }
+    }
+
+    component TitleLabel: Widgets.SubtitleLabel {
+        required property MusicAlbumSectionDelegate delegate
+
+        font.pixelSize: VLCStyle.fontSize_xxlarge
+
+        text: delegate._initialFetchCompleted ? (delegate._albumData?.title || 
qsTr("Unknown title"))
+                                              : " " // to get the implicit 
height during layouting at initialization
+        color: theme.fg.primary
+    }
+
+    component CaptionLabel: Widgets.CaptionLabel {
+        required property MusicAlbumSectionDelegate delegate
+
+        text: {
+            if (!delegate._initialFetchCompleted)
+                return " " // to get the implicit height during layouting at 
initialization
+
+            const _albumData = delegate._albumData
+            if (!_albumData)
+                return ""
+
+            const parts = []
+
+            parts.push(_albumData.main_artist || qsTr("Unknown artist"))
+
+            const year = _albumData.release_year
+            if (year)
+                parts.push(year)
+
+            const count = _albumData.nb_tracks ?? 0
+            parts.push(qsTr("%1 track(s)").arg(count))
+
+            const duration = _albumData.duration?.formatHMS()
+            if (duration)
+                parts.push(duration)
+
+            return parts.join(" • ")
+        }
+
+        visible: (text.length > 0)
+
+        color: theme.fg.secondary
+    }
+
+    component PlayButton: Widgets.ActionButtonPrimary {
+        required property MusicAlbumSectionDelegate delegate
+
+        iconTxt: VLCIcons.play
+        text: qsTr("Play")
+        enabled: !!delegate?._albumData?.id
+        visible: delegate._initialFetchCompleted
+
+        onClicked: {
+            MediaLib.addAndPlay(delegate._albumData.id)
+        }
+    }
+
+    component EnqueueButton: Widgets.ActionButtonOverlay {
+        required property MusicAlbumSectionDelegate delegate
+
+        iconTxt: VLCIcons.enqueue
+        text: qsTr("Enqueue")
+        enabled: !!delegate?._albumData?.id
+        visible: delegate._initialFetchCompleted
+
+        onClicked: {
+            MediaLib.addToPlaylist(delegate._albumData.id)
+        }
+    }
+
+    component PreviousSectionButton: Widgets.ActionButtonOverlay {
+        required property MusicAlbumSectionDelegate delegate
+
+        iconTxt: VLCIcons.chevron_up
+        text: qsTr("Prev")
+        enabled: delegate.previousSectionButtonEnabled
+
+        Component.onCompleted: {
+            clicked.connect(delegate, 
delegate.changeToPreviousSectionRequested)
+        }
+    }
+
+    component NextSectionButton: Widgets.ActionButtonOverlay {
+        required property MusicAlbumSectionDelegate delegate
+
+        iconTxt: VLCIcons.chevron_down
+        text: qsTr("Next")
+        enabled: delegate.nextSectionButtonEnabled
+
+        Component.onCompleted: {
+            clicked.connect(delegate, delegate.changeToNextSectionRequested)
+        }
+    }
+
+    contentItem: RowLayout {
+        id: _contentItem
+
+        spacing: root.spacing
+
+        readonly property Item artworkTextureProvider: (artwork.status === 
Image.Ready) ? artwork.textureProviderItem
+                                                                               
         : null
+
+        readonly property bool compactDownButtons: (_contentItem.width < 
VLCStyle.colWidth(3))
+        readonly property bool compactRightButtons: (_contentItem.width < 
VLCStyle.colWidth(5))
+
+        Widgets.ImageExt {
+            id: artwork
+
+            Layout.fillHeight: true
+            Layout.preferredHeight: VLCStyle.cover_small
+            Layout.preferredWidth: VLCStyle.cover_small
+
+            radius: VLCStyle.expandCover_music_radius
+
+            source: root._albumCover
+
+            backgroundColor: theme.bg.primary
+
+            // There are many sections, we need to be conservative regarding 
the source
+            // size to limit average video and system memory consumption. The 
visual of
+            // the image here is already expected to be (much) lower than the 
source
+            // size, but it is for using the same source for the blur effect 
as texture
+            // provider instead of having another image. There, the visual is 
bigger
+            // than the source size, but since we are using blur effect, the 
result is
+            // acceptable. Imperfections of low quality blurring due to linear 
upscaling
+            // in the source is tolerated by two-pass dual kawase blur, which 
is good
+            // but ends up having stronger blurring than what we desire. 
Still, consuming
+            // less resources is considered more important, at least for now:
+            sourceSize: Qt.size(Helpers.alignUp(Screen.desktopAvailableWidth / 
8, 32), 0)
+
+            cache: false
+
+            asynchronous: true
+
+            Widgets.DefaultShadow {
+                visible: (artwork.status === Image.Ready)
+            }
+        }
+
+        Column {
+            Layout.fillWidth: true
+            Layout.alignment: Qt.AlignVCenter
+
+            spacing: VLCStyle.margin_xsmall
+
+            Column {
+                anchors.left: parent.left
+                anchors.right: parent.right
+
+                spacing: 0
+
+                TitleLabel {
+                    delegate: root
+
+                    anchors.left: parent.left
+                    anchors.right: parent.right
+                }
+
+                CaptionLabel {
+                    delegate: root
+
+                    anchors.left: parent.left
+                    anchors.right: parent.right
+                }
+            }
+
+            Row {
+                anchors.left: parent.left
+                anchors.right: parent.right
+
+                spacing: VLCStyle.margin_small
+
+                PlayButton {
+                    id: playButton
+
+                    delegate: root
+
+                    focus: true
+                    activeFocusOnTab: false
+
+                    showText: !_contentItem.compactDownButtons
+
+                    Navigation.parentItem: root
+                    Navigation.rightItem: enqueueButton
+                }
+
+                EnqueueButton {
+                    id: enqueueButton
+
+                    delegate: root
+
+                    activeFocusOnTab: false
+
+                    showText: !_contentItem.compactDownButtons
+
+                    Navigation.parentItem: root
+                    // Navigation.rightItem: previousSectionButton.enabled ? 
previousSectionButton
+                    //                                                     : 
nextSectionButton
+                    Navigation.leftItem: playButton
+                }
+            }
+        }
+
+        // Column {
+        //     Layout.alignment: Qt.AlignVCenter
+
+        //     spacing: VLCStyle.margin_small
+
+        //     PreviousSectionButton {
+        //         id: previousSectionButton
+
+        //         delegate: root
+
+        //         activeFocusOnTab: false
+
+        //         showText: !_contentItem.compactRightButtons
+
+        //         Navigation.parentItem: root
+        //         Navigation.downItem: nextSectionButton
+        //         Navigation.leftItem: enqueueButton
+        //     }
+
+        //     NextSectionButton {
+        //         id: nextSectionButton
+
+        //         delegate: root
+
+        //         activeFocusOnTab: false
+
+        //         showText: !_contentItem.compactRightButtons
+
+        //         Navigation.parentItem: root
+        //         Navigation.upItem: previousSectionButton
+        //         Navigation.leftItem: enqueueButton
+        //     }
+        // }
+    }
+}


=====================================
modules/gui/qt/medialibrary/qml/MusicArtist.qml
=====================================
@@ -1,5 +1,5 @@
 /*****************************************************************************
- * Copyright (C) 2020 VLC authors and VideoLAN
+ * Copyright (C) 2025 VLC authors and VideoLAN
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -16,7 +16,9 @@
  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, 
USA.
  *****************************************************************************/
 import QtQuick.Controls
+import QtQuick.Templates as T
 import QtQuick
+import QtQuick.Window
 import QtQml.Models
 import QtQuick.Layouts
 
@@ -50,18 +52,53 @@ FocusScope {
 
     property bool isSearchable: true
 
-    property alias searchPattern: albumModel.searchPattern
-    property alias sortOrder: albumModel.sortOrder
-    property alias sortCriteria: albumModel.sortCriteria
+    property string searchPattern
+    property int sortOrder
+    property string sortCriteria
+
+    readonly property MLBaseModel _effectiveModel: MainCtx.gridView ? 
albumModel : trackModel
+
+    onSearchPatternChanged: {
+        _effectiveModel.searchPattern = root.searchPattern
+    }
+
+    onSortOrderChanged: {
+        _effectiveModel.sortOrder = root.sortOrder
+    }
+
+    onSortCriteriaChanged: {
+        // FIXME: Criteria is set to empty for a brief period during 
initialization,
+        //        call later prevents setting the criteria empty.
+        Qt.callLater(() => {
+            _effectiveModel.sortCriteria = root.sortCriteria
+        })
+    }
+
+    Connections {
+        target: root._effectiveModel
+
+        function onSearchPatternChanged() {
+            if (root.searchPattern !== root._effectiveModel.searchPattern)
+                root.searchPattern = root._effectiveModel.searchPattern
+        }
+
+        function onSortOrderChanged() {
+            if (root.sortOrder !== root._effectiveModel.sortOrder)
+                root.sortOrder = root._effectiveModel.sortOrder
+        }
+
+        function onSortCriteriaChanged() {
+            if (root.sortCriteria !== root._effectiveModel.sortCriteria)
+                root.sortCriteria = root._effectiveModel.sortCriteria
+        }
+    }
 
     // current index of album model
     readonly property int currentIndex: {
         if (!_currentView)
            return -1
-        else if (MainCtx.gridView)
-           return _currentView.currentIndex
         else
-           return headerItem.albumsListView.currentIndex
+           return _currentView.currentIndex
     }
 
     property real rightPadding
@@ -82,23 +119,26 @@ FocusScope {
             _currentView.contentY = newContentY
     }
 
-    property Component header: FocusScope {
+    property Component header: T.Pane {
         id: headerFs
 
-        property Item albumsListView: albumsLoader.status === Loader.Ready ? 
albumsLoader.item.albumsListView: null
-
         focus: true
         height: col.height
         width: root.width
 
+        implicitWidth: Math.max(implicitBackgroundWidth + leftInset + 
rightInset,
+                                implicitContentWidth + leftPadding + 
rightPadding)
+        implicitHeight: Math.max(implicitBackgroundHeight + topInset + 
bottomInset,
+                                 implicitContentHeight + topPadding + 
bottomPadding)
+
+        signal changeToNextSectionRequested()
+        signal changeToPreviousSectionRequested()
+
         function setCurrentItemFocus(reason) {
-            if (albumsListView)
-                albumsListView.setCurrentItemFocus(reason);
-            else
-                artistBanner.setCurrentItemFocus(reason);
+            headerFs.forceActiveFocus(reason)
         }
 
-        Column {
+        contentItem: Column {
             id: col
 
             height: implicitHeight
@@ -120,174 +160,224 @@ FocusScope {
                         root.navigationShowHeader(0, height)
                 }
 
-                Navigation.parentItem: root
-                Navigation.downAction: function() {
-                    if (albumsListView)
-                        albumsListView.setCurrentItemFocus(Qt.TabFocusReason);
-                    else
-                        _currentView.setCurrentItemFocus(Qt.TabFocusReason);
+                Connections {
+                    enabled: !MainCtx.gridView
+                    target: trackModel
 
+                    function onSortCriteriaChanged() {
+                        if (MainCtx.albumSections &&
+                            trackModel.sortCriteria !== "album_title") {
+                            MainCtx.albumSections = false
+                        }
+                    }
                 }
-            }
 
-            Widgets.ViewHeader {
-                view: root
+                property string _oldSortCriteria
 
-                leftPadding: root._contentLeftMargin
-                bottomPadding: VLCStyle.layoutTitle_bottom_padding -
-                               (MainCtx.gridView ? 0 : 
VLCStyle.gridItemSelectedBorder)
+                function adjustAlbumSections() {
+                    if (!artistBanner) // context is lost, Qt 6.2 bug
+                        return
 
-                text: qsTr("Albums")
+                    if (!(root._currentView instanceof Widgets.TableViewExt))
+                        return
+
+                    if (MainCtx.albumSections) {
+                        const albumTitleSortCriteria = "album_title"
+                        if (trackModel.sortCriteria !== 
albumTitleSortCriteria) {
+                            artistBanner._oldSortCriteria = 
trackModel.sortCriteria
+                            trackModel.sortCriteria = albumTitleSortCriteria
+                        }
+                    } else {
+                        if (artistBanner._oldSortCriteria.length > 0) {
+                            trackModel.sortCriteria = 
artistBanner._oldSortCriteria
+                            artistBanner._oldSortCriteria = ""
+                        }
+                    }
+
+                    if (root._currentView)
+                        root._currentView.albumSections = MainCtx.albumSections
+                }
+
+                Component.onCompleted: {
+                    MainCtx.albumSectionsChanged.connect(artistBanner, 
adjustAlbumSections)
+                    root._currentViewChanged.connect(artistBanner, 
adjustAlbumSections)
+                    adjustAlbumSections()
+                }
+
+                Navigation.parentItem: root
+                Navigation.downItem: pinnedMusicAlbumSectionLoader
             }
 
             Loader {
-                id: albumsLoader
+                id: pinnedMusicAlbumSectionLoader
 
-                active: !MainCtx.gridView
-                focus: true
+                anchors.left: parent.left
+                anchors.right: parent.right
 
-                onActiveFocusChanged: {
-                    // make sure content is visible with activeFocus
-                    if (activeFocus)
-                        root.navigationShowHeader(y, height)
-                }
+                active: (root._currentView instanceof Widgets.TableViewExt)
+                visible: active
 
-                sourceComponent: Column {
-                    property alias albumsListView: albumsList
+                sourceComponent: MusicAlbumSectionDelegate {
+                    id: pinnedMusicAlbumSection
 
-                    width: albumsList.width
-                    height: implicitHeight
+                    model: albumModel
 
-                    spacing: VLCStyle.tableView_spacing - 
VLCStyle.margin_xxxsmall
+                    verticalPadding: VLCStyle.margin_xsmall
 
-                    Widgets.ListViewExt {
-                        id: albumsList
+                    focus: true
 
-                        x: root._contentLeftMargin - 
VLCStyle.gridItemSelectedBorder
+                    readonly property Widgets.TableViewExt tableView: 
root._currentView
 
-                        width: root.width - root.rightPadding - 
root._contentLeftMargin - root._contentRightMargin
-                        height: gridHelper.cellHeight + topMargin + 
bottomMargin + VLCStyle.margin_xxxsmall
+                    section: tableView.currentSection || ""
 
-                        leftMargin: VLCStyle.gridItemSelectedBorder
-                        rightMargin: leftMargin
+                    previousSectionButtonEnabled: 
tableView._firstSectionInstance && (tableView._firstSectionInstance.section !== 
section)
+                    nextSectionButtonEnabled: tableView._lastSectionInstance 
&& (tableView._lastSectionInstance.section !== section)
 
-                        topMargin: VLCStyle.gridItemSelectedBorder
-                        bottomMargin: VLCStyle.gridItemSelectedBorder
+                    Component.onCompleted: {
+                        changeToPreviousSectionRequested.connect(headerFs, 
headerFs.changeToPreviousSectionRequested)
+                        changeToNextSectionRequested.connect(headerFs, 
headerFs.changeToNextSectionRequested)
+                    }
 
-                        displayMarginBeginning: root._contentLeftMargin
-                        displayMarginEnd: root._contentRightMargin + 
VLCStyle.gridItemSelectedBorder
+                    background: Rectangle {
+                        color: theme.bg.secondary
+                    }
 
-                        focus: true
+                    contentItem: RowLayout {
+                        id: _contentItem
 
-                        model: albumModel
-                        selectionModel: albumSelectionModel
-                        orientation: ListView.Horizontal
-                        spacing: VLCStyle.column_spacing
-                        buttonMargin: (gridHelper.cellHeight - 
gridHelper.textHeight - buttonLeft.height) / 2 +
-                                      VLCStyle.gridItemSelectedBorder
+                        spacing: pinnedMusicAlbumSection.spacing
 
-                        Navigation.parentItem: root
+                        readonly property bool compactButtons: 
(pinnedMusicAlbumSection.width < (displayPositioningButtons ? 
VLCStyle.colWidth(6)
+                                                                               
                                            : VLCStyle.colWidth(5)))
+                        readonly property bool displayPositioningButtons: 
!!pinnedMusicAlbumSection.tableView?.albumSections // not possible otherwise
 
-                        Navigation.upAction: function() {
-                            
artistBanner.setCurrentItemFocus(Qt.TabFocusReason);
-                        }
+                        Widgets.ImageExt {
+                            Layout.fillHeight: true
 
-                        Navigation.downAction: function() {
-                            root.setCurrentItemFocus(Qt.TabFocusReason);
-                        }
+                            Layout.preferredHeight: 
VLCStyle.trackListAlbumCover_heigth
+                            Layout.preferredWidth: 
VLCStyle.trackListAlbumCover_width
 
-                        GridSizeHelper {
-                            id: gridHelper
+                            source: pinnedMusicAlbumSection._albumCover ? 
pinnedMusicAlbumSection._albumCover : VLCStyle.noArtArtist
 
-                            availableWidth: albumsList.width
-                            basePictureWidth: VLCStyle.gridCover_music_width
-                            basePictureHeight: VLCStyle.gridCover_music_height
-                        }
+                            sourceSize: Qt.size(width * eDPR, height * eDPR)
+
+                            readonly property real eDPR: 
MainCtx.effectiveDevicePixelRatio(Window.window)
 
-                        delegate: Widgets.GridItem {
-                            id: gridItem
+                            backgroundColor: theme.bg.primary
 
-                            required property var model
-                            required property int index
+                            fillMode: Image.PreserveAspectFit
 
-                            y: selectedBorderWidth
+                            asynchronous: true
+                            cache: true
 
-                            width: gridHelper.cellWidth
-                            height: gridHelper.cellHeight
+                            Widgets.DefaultShadow {
+                                visible: (parent.status === Image.Ready)
+                            }
+                        }
 
-                            pictureWidth: gridHelper.maxPictureWidth
-                            pictureHeight: gridHelper.maxPictureHeight
+                        Column {
+                            Layout.fillWidth: true
 
-                            image: model.cover || ""
-                            fallbackImage: VLCStyle.noArtAlbumCover
+                            MusicAlbumSectionDelegate.TitleLabel {
+                                id: titleLabel
 
-                            fillMode: Image.PreserveAspectCrop
+                                anchors.left: parent.left
+                                anchors.right: parent.right
 
-                            title: model.title || qsTr("Unknown title")
-                            subtitle: model.release_year || ""
-                            subtitleVisible: true
-                            textAlignHCenter: true
-                            dragItem: albumDragItem
+                                delegate: pinnedMusicAlbumSection
 
-                            // updates to selection is manually handled for 
optimization purpose
-                            Component.onCompleted: _updateSelected()
+                                Layout.alignment: Qt.AlignLeft | 
Qt.AlignVCenter
 
-                            onIndexChanged: _updateSelected()
+                                Layout.fillWidth: true
 
-                            onPlayClicked: play()
-                            onItemDoubleClicked: play()
+                                Layout.maximumWidth: implicitWidth
 
-                            onItemClicked: (modifier) => {
-                                albumsList.selectionModel.updateSelection( 
modifier , albumsList.currentIndex, index )
-                                albumsList.currentIndex = index
-                                albumsList.forceActiveFocus()
+                                font.pixelSize: VLCStyle.fontSize_normal
+                                font.weight: Font.DemiBold
                             }
 
-                            Connections {
-                                target: albumsList.selectionModel
+                            MusicAlbumSectionDelegate.CaptionLabel {
+                                id: captionLabel
 
-                                function onSelectionChanged(selected, 
deselected) {
-                                    const idx = 
albumModel.index(gridItem.index, 0)
-                                    const findInSelection = s => s.find(range 
=> range.contains(idx)) !== undefined
+                                anchors.left: parent.left
+                                anchors.right: parent.right
 
-                                    // NOTE: we only get diff of the selection
-                                    if (findInSelection(selected))
-                                        gridItem.selected = true
-                                    else if (findInSelection(deselected))
-                                        gridItem.selected = false
-                                }
-                            }
+                                Layout.alignment: Qt.AlignLeft | 
Qt.AlignVCenter
 
-                            onContextMenuButtonClicked: (_, globalMousePos) => 
{
-                                albumSelectionModel.updateSelection( 
Qt.NoModifier , albumsList.currentIndex, index )
-                                
contextMenu.popup(albumSelectionModel.selectedIndexes
-                                                  , globalMousePos)
+                                delegate: pinnedMusicAlbumSection
                             }
+                        }
 
-                            function play() {
-                                if ( model.id !== undefined ) {
-                                    MediaLib.addAndPlay( model.id )
-                                }
-                            }
+                        MusicAlbumSectionDelegate.PlayButton {
+                            id: playButton
 
-                            function _updateSelected() {
-                                selected = 
albumSelectionModel.isRowSelected(gridItem.index)
-                            }
+                            delegate: pinnedMusicAlbumSection
+
+                            showText: !_contentItem.compactButtons
+                            focus: true
+
+                            Navigation.parentItem: pinnedMusicAlbumSection
+                            Navigation.rightItem: enqueueButton
                         }
 
-                        onActionAtIndex: (index) => { albumModel.addAndPlay( 
new Array(index) ) }
-                    }
+                        MusicAlbumSectionDelegate.EnqueueButton {
+                            id: enqueueButton
+
+                            delegate: pinnedMusicAlbumSection
+
+                            showText: !_contentItem.compactButtons
+
+                            Navigation.parentItem: pinnedMusicAlbumSection
+                            // Navigation.rightItem: previousSectionButton
+                            Navigation.leftItem: playButton
+                        }
+
+                        // MusicAlbumSectionDelegate.PreviousSectionButton {
+                        //     id: previousSectionButton
+
+                        //     delegate: pinnedMusicAlbumSection
+
+                        //     visible: _contentItem.displayPositioningButtons
+
+                        //     showText: !_contentItem.compactButtons
+
+                        //     Navigation.parentItem: pinnedMusicAlbumSection
+                        //     Navigation.rightItem: nextSectionButton
+                        //     Navigation.leftItem: enqueueButton
+                        // }
+
+                        // MusicAlbumSectionDelegate.NextSectionButton {
+                        //     id: nextSectionButton
 
-                    Widgets.ViewHeader {
-                        view: root
+                        //     delegate: pinnedMusicAlbumSection
 
-                        leftPadding: root._contentLeftMargin
-                        topPadding: 0
+                        //     visible: _contentItem.displayPositioningButtons
 
-                        text: qsTr("Tracks")
+                        //     showText: !_contentItem.compactButtons
+
+                        //     Navigation.parentItem: pinnedMusicAlbumSection
+                        //     Navigation.leftItem: previousSectionButton
+                        // }
+                    }
+
+                    Navigation.parentItem: root
+                    Navigation.upItem: artistBanner
+                    Navigation.downAction: function() {
+                        tableView.setCurrentItemFocus(Qt.TabFocusReason)
                     }
                 }
             }
+
+            Widgets.ViewHeader {
+                view: root
+
+                leftPadding: root._contentLeftMargin
+                bottomPadding: VLCStyle.layoutTitle_bottom_padding -
+                               (MainCtx.gridView ? 0 : 
VLCStyle.gridItemSelectedBorder)
+                topPadding: pinnedMusicAlbumSectionLoader.active ? 
bottomPadding : VLCStyle.layoutTitle_top_padding
+
+                text: qsTr("Albums")
+            }
         }
     }
 
@@ -392,7 +482,7 @@ FocusScope {
     Widgets.MLDragItem {
         id: albumDragItem
 
-        view: (root._currentView instanceof Widgets.TableViewExt) ? 
root._currentView?.headerItem?.albumsListView
+        view: (root._currentView instanceof Widgets.TableViewExt) ? 
(root._currentView?.headerItem?.albumsListView ?? null)
                                                                   : 
root._currentView
         indexes: indexesFlat ? albumSelectionModel.selectedIndexesFlat
                              : albumSelectionModel.selectedIndexes
@@ -537,9 +627,231 @@ FocusScope {
             }
 
             header: root.header
-            headerPositioning: ListView.InlineHeader
             rowHeight: VLCStyle.tableCoverRow_height
 
+            property bool albumSections: true
+
+            section.property: "album_id"
+            section.delegate: albumSections ? 
musicAlbumSectionDelegateComponent : null
+
+            readonly property var _artistId: root.artistId
+
+            on_ArtistIdChanged: {
+                if (albumSections) {
+                    _sections.length = 0 // This clears the sections list
+
+                    // FIXME: Sections may get invalid section name on artist 
change, Qt bug?
+                    albumSections = false
+                    albumSections = true
+                }
+            }
+
+            Binding on listView.cacheBuffer {
+                // FIXME
+                // 
https://doc.qt.io/qt-6/qml-qtquick-listview.html#variable-delegate-size-and-section-labels
+                when: tableView_id.albumSections
+                value: Math.max(tableView_id.height * 2, 
tableView_id.Screen.desktopAvailableHeight)
+            }
+
+            property alias contentYBehavior: contentYBehavior
+
+            property MusicAlbumSectionDelegate _firstSectionInstance
+            property MusicAlbumSectionDelegate _lastSectionInstance
+
+            property list<MusicAlbumSectionDelegate> _sections
+
+            Component {
+                id: musicAlbumSectionDelegateComponent
+
+                MusicAlbumSectionDelegate {
+                    id: musicAlbumSectionDelegate
+
+                    width: tableView_id.width
+
+                    model: albumModel
+
+                    previousSectionButtonEnabled: 
tableView_id._firstSectionInstance && (tableView_id._firstSectionInstance !== 
this)
+                    nextSectionButtonEnabled: 
tableView_id._lastSectionInstance && (tableView_id._lastSectionInstance !== 
this)
+
+                    Navigation.parentItem: tableView_id
+                    Navigation.upAction: function() {
+                        let item = tableView_id.listView.itemAt(0, y - 1)
+                        if (item) {
+                            item.forceActiveFocus(Qt.BacktabFocusReason)
+                            tableView_id.currentIndex = item.index
+                            tableView_id.positionViewAtIndex(item.index, 
ItemView.Contain)
+                        }
+                    }
+                    Navigation.downAction: function() {
+                        let item = tableView_id.listView.itemAt(0, y + height 
+ 1)
+                        if (item) {
+                            item.forceActiveFocus(Qt.TabFocusReason)
+                            tableView_id.currentIndex = item.index
+                            tableView_id.positionViewAtIndex(item.index, 
ItemView.Contain)
+                        }
+                    }
+
+                    Component.onCompleted: {
+                        tableView_id._sections.push(this)
+
+                        // WARNING: Sections are reused.
+                        // NOTE: Scrolling does not change the y of items of 
content item,
+                        //       listening to y and visible changes is not 
really terrible.
+                        sectionChanged.connect(this, adjustSectionInstances)
+                        yChanged.connect(this, adjustSectionInstances)
+                        adjustSectionInstances()
+                    }
+
+                    function adjustSectionInstances() {
+                        if (tableView_id._firstSectionInstance) {
+                            if (y < tableView_id._firstSectionInstance.y)
+                                tableView_id._firstSectionInstance = this
+                        } else {
+                            tableView_id._firstSectionInstance = this
+                        }
+
+                        if (tableView_id._lastSectionInstance) {
+                            if (y > tableView_id._lastSectionInstance.y)
+                                tableView_id._lastSectionInstance = this
+                        } else {
+                            tableView_id._lastSectionInstance = this
+                        }
+                    }
+
+                    Connections {
+                        target: tableView_id.headerItem
+
+                        function onChangeToPreviousSectionRequested() {
+                            if (tableView_id.currentSection === 
musicAlbumSectionDelegate.section)
+                                
musicAlbumSectionDelegate.changeToPreviousSectionRequested()
+                        }
+
+                        function onChangeToNextSectionRequested() {
+                            if (tableView_id.currentSection === 
musicAlbumSectionDelegate.section)
+                                
musicAlbumSectionDelegate.changeToNextSectionRequested()
+                        }
+                    }
+
+                    function changeSection(forward: bool) {
+                        // We have to probe the section on demand, as 
otherwise we
+                        // would have to track the section unnecessarily. Note 
that
+                        // reusing sections complicates things a lot.
+
+                        const currentSectionFirstItemPosY = (y + height + 1)
+                        let item = tableView_id.listView.itemAt(0, 
currentSectionFirstItemPosY) // current section first item
+
+                        if (!item || item.ListView.section !== section) {
+                            // If there is no first item, there should be no 
section:
+                            console.warn("Could not find the required first 
item at y-pos: %1 of section: %2 (%3)! Manually iterating all 
items...".arg(currentSectionFirstItemPosY)
+                                                                               
                                                                    
.arg(section).arg(this))
+                            item = null
+
+                            for (let i = 0; i < tableView_id.count; ++i) {
+                                const t = tableView_id.itemAtIndex(i)
+                                if (t && (t.ListView.section === section)) {
+                                    item = t
+                                    break
+                                } else if (!t) {
+                                    console.debug(this, ": ListView count is 
%1, but itemAtIndex(%2) returned null!".arg(tableView_id.count).arg(i)) // Too 
low cacheBuffer?
+                                }
+                            }
+
+                            if (!item) {
+                                console.error(this, ": Could not find the 
required first item! Try again after increasing the cache buffer of the view.")
+                                return
+                            }
+                        }
+
+                        console.assert(item.index !== undefined)
+
+                        let itemIndex = item.index
+                        let targetSectionName
+
+                        // FIXME: We check each item until reaching the 
next/previous section. This does not mean that the
+                        //        whole list is checked, still, this should be 
removed when Qt provides such functionality.
+                        for (let i = itemIndex; forward ? (i < 
tableView_id.count) : (i >= 0); forward ? ++i : --i) {
+                            item = tableView_id.itemAtIndex(i)
+
+                            if (!item) {
+                                if (!targetSectionName) {
+                                    // This function is called when there is 
no previous/next section:
+                                    console.error(this, ": Expected item at 
index", i, "does not exist! Try again after increasing the cache buffer of the 
view.")
+                                    return
+                                } else {
+                                    break
+                                }
+                            }
+
+                            const currentItemSection = item.ListView.section
+                            console.assert(currentItemSection && 
currentItemSection.length > 0)
+                            if (currentItemSection !== section) {
+                                itemIndex = i
+                                if (forward) {
+                                    // First item of the next section.
+                                    targetSectionName = currentItemSection
+                                    break
+                                } else {
+                                    if (!targetSectionName) {
+                                        targetSectionName = currentItemSection
+                                    } else if (currentItemSection !== 
targetSectionName) {
+                                        // First item of the previous section.
+                                        ++itemIndex
+                                        break
+                                    }
+                                }
+                            }
+                        }
+
+                        if (!targetSectionName) {
+                            console.error(this, ": Could not find the target 
section! Possible Qt bug.")
+                            return
+                        }
+
+                        if (activeFocus) {
+                            // If this section has focus, the target section
+                            // should also have focus:
+                            for (let i = 0; i < _sections.length; ++i) {
+                                const targetSection = _sections[i]
+                                if (targetSection?.section === 
targetSectionName) {
+                                    targetSection.focus = true
+                                    break
+                                }
+                            }
+                        }
+
+                        // FIXME: Not the best approach, but Qt does not seem 
to provide an alternative.
+                        //        Adjusting `contentY` is proved to be 
unreliable, especially when there
+                        //        are sections.
+                        // FIXME: Qt does not provide the `contentY` with 
`positionViewAtIndex()` for us
+                        //        to animate. For that reason, we capture the 
new `contentY`, adjust
+                        //        `contentY` to it is old value then enable 
the animation and set `contentY`
+                        //        to its new value.
+                        const oldContentY = tableView_id.contentY
+                        // NOTE: `positionViewAtIndex()` actually positions 
the view to the section, so
+                        //       we do not need to subtract the section height 
here:
+                        tableView_id.positionViewAtIndex(itemIndex, 
ListView.Beginning)
+                        const newContentY = tableView_id.contentY
+                        if (Math.abs(oldContentY - newContentY) >= 
Number.EPSILON) {
+                            tableView_id.contentYBehavior.enabled = false
+                            tableView_id.contentY = oldContentY
+                            tableView_id.contentYBehavior.enabled = true
+                            tableView_id.contentY = newContentY
+                            tableView_id.contentYBehavior.enabled = false
+                        }
+                    }
+
+                    onChangeToPreviousSectionRequested: {
+                        changeSection(false)
+                    }
+
+                    onChangeToNextSectionRequested: {
+                        changeSection(true)
+                    }
+                }
+            }
+
+            useCurrentSectionLabel: false
+
             displayMarginBeginning: root.displayMarginBeginning
             displayMarginEnd: root.displayMarginEnd
 
@@ -552,16 +864,55 @@ FocusScope {
                 model: {
                     criteria: "title",
 
-                    subCriterias: [ "duration", "album_title" ],
+                    subCriterias: MainCtx.albumSections ? ["track_number", 
"duration"]
+                                                        : ["duration", 
"album_title"],
 
                     text: qsTr("Title"),
 
-                    headerDelegate: tableColumns.titleHeaderDelegate,
-                    colDelegate: tableColumns.titleDelegate
+                    headerDelegate: MainCtx.albumSections ? 
tableColumns.titleTextHeaderDelegate
+                                                          : 
tableColumns.titleHeaderDelegate,
+                    colDelegate: MainCtx.albumSections ? 
tableColumns.titleTextDelegate
+                                                       : 
tableColumns.titleDelegate
                 }
             }]
 
-            property var _modelMedium: [{
+            property var _modelMedium: MainCtx.albumSections ? [{
+                size: .2,
+
+                model: {
+                    criteria: "track_number",
+
+                    text: qsTr("#"),
+
+                    showSection: "",
+
+                    hCenterText: true
+                }
+            }, {
+                weight: 1,
+
+                model: {
+                    criteria: "title",
+
+                    text: qsTr("Title"),
+
+                    headerDelegate: tableColumns.titleTextHeaderDelegate,
+                    colDelegate: tableColumns.titleTextDelegate
+                }
+            }, {
+                size: 1,
+
+                model: {
+                    criteria: "duration",
+
+                    text: qsTr("Duration"),
+
+                    showSection: "",
+
+                    headerDelegate: tableColumns.timeHeaderDelegate,
+                    colDelegate: tableColumns.timeColDelegate
+                }
+            }] : [{
                 weight: 1,
 
                 model: {
@@ -615,6 +966,18 @@ FocusScope {
 
             onDragItemChanged: console.assert(tableView_id.dragItem === 
tableDragItem)
 
+            Behavior on listView.contentY {
+                id: contentYBehavior
+
+                enabled: false
+
+                // NOTE: Usage of `SmoothedAnimation` is intentional here.
+                SmoothedAnimation {
+                    duration: VLCStyle.duration_veryLong
+                    easing.type: Easing.InOutSine
+                }
+            }
+
             Widgets.MLDragItem {
                 id: tableDragItem
 


=====================================
modules/gui/qt/medialibrary/qml/MusicArtistsAlbums.qml
=====================================
@@ -26,6 +26,7 @@ import VLC.MediaLibrary
 import VLC.Util
 import VLC.Widgets as Widgets
 import VLC.Style
+import VLC.Menus
 
 FocusScope {
     id: root
@@ -35,11 +36,22 @@ FocusScope {
     property int leftPadding: 0
     property int rightPadding: 0
 
-    property var sortModel: [
-        { text: qsTr("Alphabetic"),  criteria: "title" },
-        { text: qsTr("Release Year"),  criteria: "release_year" }
+    property var sortModel: MainCtx.gridView ? [
+        { text: qsTr("Title"), criteria: "title" },
+        { text: qsTr("Release Year"), criteria: "release_year" },
+    ] : [
+        { text: qsTr("Title"), criteria: "title" },
+        { text: qsTr("Release Year"), criteria: "release_year" },
+        { text: qsTr("Album Title"), criteria: "album_title" },
+        { text: qsTr("Duration"), criteria: "duration" }
     ]
 
+    property SortMenuAlbums sortMenu: SortMenuAlbums {
+        ctx: MainCtx
+
+        sectionsVisible: !MainCtx.gridView
+    }
+
     property int initialIndex: 0
     property int initialAlbumIndex: 0
     property var artistId: undefined


=====================================
modules/gui/qt/medialibrary/qml/MusicArtistsDisplay.qml
=====================================
@@ -86,6 +86,21 @@ Widgets.PageLoader {
             sortOrder: MainCtx.sort.order
             sortCriteria: MainCtx.sort.criteria
 
+            onSearchPatternChanged: {
+                MainCtx.search.pattern = searchPattern
+                seachPattern = Qt.binding(() => { return 
MainCtx.search.pattern })
+            }
+
+            onSortOrderChanged: {
+                MainCtx.sort.order = sortOrder
+                sortOrder = Qt.binding(() => { return MainCtx.sort.order })
+            }
+
+            onSortCriteriaChanged: {
+                MainCtx.sort.criteria = sortCriteria
+                sortCriteria = Qt.binding(() => { return MainCtx.sort.criteria 
})
+            }
+
             displayMarginBeginning: root.displayMarginBeginning
             displayMarginEnd: root.displayMarginEnd
 


=====================================
modules/gui/qt/menus/qml_menu_wrapper.cpp
=====================================
@@ -215,6 +215,25 @@ void SortMenuVideo::onPopup(QMenu * menu) /* override */
     }
 }
 
+void SortMenuAlbums::onPopup(QMenu *menu)
+{
+    if (!m_sectionsVisible)
+        return;
+
+    assert(m_ctx);
+    assert(menu);
+
+    menu->addSeparator();
+
+    QAction *action = menu->addAction(qtr("Album sections"));
+    action->setCheckable(true);
+    action->setChecked(m_ctx->albumSections());
+    connect(action, &QAction::toggled, this, [this] (bool enabled) {
+        if (Q_LIKELY(m_ctx))
+            m_ctx->setAlbumSections(enabled);
+    });
+}
+
 QmlGlobalMenu::QmlGlobalMenu(QObject *parent)
     : VLCMenuBar(parent)
 {


=====================================
modules/gui/qt/menus/qml_menu_wrapper.hpp
=====================================
@@ -135,6 +135,16 @@ signals:
     void grouping(MainCtx::Grouping grouping);
 };
 
+class SortMenuAlbums : public SortMenu
+{
+    Q_OBJECT
+
+    SIMPLE_MENU_PROPERTY(bool, sectionsVisible, false)
+
+protected:
+    void onPopup(QMenu * menu) override;
+};
+
 //inherit VLCMenuBar so we can access menu creation functions
 class QmlGlobalMenu : public VLCMenuBar
 {


=====================================
modules/gui/qt/meson.build
=====================================
@@ -645,7 +645,8 @@ qml_modules += {
         'medialibrary/qml/VideoGridItem.qml',
         'medialibrary/qml/VideoInfoExpandPanel.qml',
         'medialibrary/qml/VideoListDisplay.qml',
-        'medialibrary/qml/VideoGridDisplay.qml'
+        'medialibrary/qml/VideoGridDisplay.qml',
+        'medialibrary/qml/MusicAlbumSectionDelegate.qml',
     ),
 }
 


=====================================
modules/gui/qt/widgets/qml/ButtonExt.qml
=====================================
@@ -137,7 +137,7 @@ T.Button {
             horizontalAlignment: Text.AlignHCenter
             verticalAlignment: Text.AlignVCenter
 
-            color: Qt.alpha(control.color, control.busy ? 0.0 : 1.0)
+            color: control.busy ? "transparent" : control.color
 
             font.pixelSize: control.iconSize
 


=====================================
modules/gui/qt/widgets/qml/PageLoader.qml
=====================================
@@ -41,6 +41,8 @@ StackViewExt {
 
     readonly property var sortModel: currentItem?.sortModel ?? null
 
+    readonly property var sortMenu: currentItem?.sortMenu ?? null
+
     //property is *not* readOnly, a PageLoader may define a localMenuDelegate 
common for its subviews (music, video)
     property Component localMenuDelegate: (currentItem?.localMenuDelegate
                                     && (currentItem.localMenuDelegate 
instanceof Component)) ? currentItem.localMenuDelegate : null


=====================================
modules/gui/qt/widgets/qml/TableViewExt.qml
=====================================
@@ -140,6 +140,9 @@ FocusScope {
                                                       // contextButton is 
implemented as fixed column
                                                       - 
VLCStyle.contextButton_width - (VLCStyle.contextButton_margin * 2)
 
+    property bool sortingFromHeader: true
+    property bool useCurrentSectionLabel: true
+
     // Aliases
 
     property alias topMargin: view.topMargin
@@ -152,6 +155,8 @@ FocusScope {
 
     property alias delegate: view.delegate
 
+    property alias contentItem: view.contentItem
+
     property alias contentY     : view.contentY
     property alias contentHeight: view.contentHeight
 
@@ -190,6 +195,8 @@ FocusScope {
 
     readonly property var itemAtIndex: view.itemAtIndex
 
+    property alias currentSection: view.currentSection
+
     // Signals
 
     //forwarded from subview
@@ -310,7 +317,8 @@ FocusScope {
 
                 text: view.currentSection
                 color: view.colorContext.accent
-                visible: view.headerPositioning === ListView.OverlayHeader
+                visible: root.useCurrentSectionLabel
+                         && view.headerPositioning === ListView.OverlayHeader
                          && text !== ""
                          && view.contentY > (row.height - col.height - 
row.topPadding)
                          && row.visible
@@ -398,6 +406,8 @@ FocusScope {
 
                             TapHandler {
                                 onTapped: (eventPoint, button) => {
+                                    if (!root.sortingFromHeader)
+                                        return
                                     if (!(modelData.model.isSortable ?? true))
                                         return
                                     else if (root.model.sortCriteria !== 
modelData.model.criteria)


=====================================
po/POTFILES.in
=====================================
@@ -798,6 +798,7 @@ modules/gui/qt/medialibrary/qml/VideoInfoExpandPanel.qml
 modules/gui/qt/medialibrary/qml/VideoListDisplay.qml
 modules/gui/qt/medialibrary/qml/VideoPlaylistsDisplay.qml
 modules/gui/qt/medialibrary/qml/VideoGridDisplay.qml
+modules/gui/qt/medialibrary/qml/MusicAlbumSectionDelegate.qml
 modules/gui/qt/menus/custom_menus.cpp
 modules/gui/qt/menus/custom_menus.hpp
 modules/gui/qt/menus/menus.cpp



View it on GitLab: 
https://code.videolan.org/videolan/vlc/-/compare/99a1f7424d1151276febe95cdd9d00fd92766bb5...49c0baf169ad3cf83146a1d7d98caf05120d1ed6

-- 
View it on GitLab: 
https://code.videolan.org/videolan/vlc/-/compare/99a1f7424d1151276febe95cdd9d00fd92766bb5...49c0baf169ad3cf83146a1d7d98caf05120d1ed6
You're receiving this email because of your account on code.videolan.org.


VideoLAN code repository instance
_______________________________________________
vlc-commits mailing list
[email protected]
https://mailman.videolan.org/listinfo/vlc-commits

Reply via email to