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