diff --git a/CMakeLists.txt b/CMakeLists.txt index c346035ba195..015076278442 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -3570,6 +3570,8 @@ if(QML) src/qml/qmlmixxxcontrollerscreen.cpp src/qml/qmlplayermanagerproxy.cpp src/qml/qmlplayerproxy.cpp + src/qml/qmlplaylistproxy.cpp + src/qml/qmlcrateproxy.cpp src/qml/qmlsidebarmodelproxy.cpp src/qml/qmllibrarytracklistcolumn.cpp src/qml/qmltrackproxy.cpp diff --git a/res/qml/Library/Browser.qml b/res/qml/Library/Browser.qml index 3e28934e6b8a..62f0b2942981 100644 --- a/res/qml/Library/Browser.qml +++ b/res/qml/Library/Browser.qml @@ -49,7 +49,12 @@ Rectangle { delegate: FocusScope { required property string label + required property string itemName required property var icon + required property var capabilities + + readonly property bool canCreate: capabilities & Mixxx.LibrarySource.Capability.Create + readonly property bool canAddTrack: capabilities & Mixxx.LibrarySource.Capability.AddTrack readonly property real indentation: 40 readonly property real padding: 5 @@ -151,7 +156,7 @@ Rectangle { color: Theme.textColor } Item { - visible: rowMouseArea.containsMouse && isTreeNode && hasChildren + visible: (rowMouseArea.containsMouse || popup.opened) && isTreeNode && canCreate id: newItem height: parent.height anchors { @@ -211,6 +216,63 @@ Rectangle { PathLine { y: 6; x: 8 } } } + MouseArea { + anchors.fill: parent + onPressed: { + popup.x = parent.width + popup.y = parent.height / 2 - popup.height / 2 + popup.open() + popup.forceActiveFocus(Qt.PopupFocusReason) + } + cursorShape: Qt.PointingHandCursor + } + Skin.ActionPopup { + id: popup + padding: 6 + focus: true + Text { + Layout.alignment: Qt.AlignHCenter + text: qsTr("New %1").arg(itemName) + font.weight: Font.Bold + font.pixelSize: 14 + color: Theme.white + } + Skin.InputField { + Layout.fillWidth: true + Layout.preferredHeight: 36 + Layout.margins: 4 + focus: true + id: newItemName + input.onAccepted: { + if (input.text) { + create(input.text) + } + popup.close() + } + } + RowLayout { + Layout.fillWidth: true + Skin.ActionButton { + Layout.fillWidth: true + label.text: qsTr("Cancel") + onPressed: { + popup.close() + } + } + Skin.ActionButton { + Layout.fillWidth: true + opacity: newItemName.text || newItemName.input.text ? 1 : 0.4 + category: Skin.ActionButton.Action + label.text: qsTr("Create") + onPressed: { + if (newItemName.text) { + create(input.text) + popup.close() + } + } + } + } + } } } } diff --git a/res/qml/Library/SourceTree.qml b/res/qml/Library/SourceTree.qml index 470a03da7101..800651818f82 100644 --- a/res/qml/Library/SourceTree.qml +++ b/res/qml/Library/SourceTree.qml @@ -21,6 +21,8 @@ Mixxx.LibrarySourceTree { id: dragArea anchors.fill: parent capabilities: cell.caps + playlists: playlistSource + crates: crateSource onPressed: { if (pressedButtons == Qt.LeftButton) { @@ -191,4 +193,162 @@ Mixxx.LibrarySourceTree { label: qsTr("All...") columns: root.defaultColumns } + Mixxx.LibraryPlaylistSource { + id: playlistSource + label: qsTr("Playlist") + itemName: qsTr("playlist") + capabilities: Mixxx.LibrarySource.Capability.Create | Mixxx.LibrarySource.Capability.AddTrack + onRequestCreate: (name) => { + // TODO create a new item with given name + print("onRequestCreate", name) + } + onRequestAddTrack: (item, track) => { + // TODO add track to current item + print("onRequestAddTrack", item, track) + } + icon: "../images/library_playlist.png" + + columns: root.defaultColumns + } + Mixxx.LibraryCrateSource { + id: crateSource + label: qsTr("Crate") + itemName: qsTr("crate") + capabilities: Mixxx.LibrarySource.Capability.Create | Mixxx.LibrarySource.Capability.AddTrack + onRequestCreate: (name) => { + // TODO create a new item with given name + print("onRequestCreate", name) + } + onRequestAddTrack: (item, track) => { + // TODO add track to current item + print("onRequestAddTrack", item, track) + } + icon: "../images/library_crates.png" + + columns: root.defaultColumns + } + Mixxx.LibraryExplorerSource { + label: qsTr("Explorer") + icon: "../images/library_explorer.png" + columns: [ + Mixxx.TrackListColumn { + + label: qsTr("Preview") + delegate: Rectangle { + color: decoration + implicitHeight: 30 + + Image { + anchors.fill: parent + fillMode: Image.PreserveAspectCrop + source: cover_art + clip: true + asynchronous: true + } + } + }, + Mixxx.TrackListColumn { + label: qsTr("Filename") + delegate: DefaultDelegate {} + }, + Mixxx.TrackListColumn { + role: Mixxx.TrackListColumn.Role.Artist + + label: qsTr("Artist") + delegate: DefaultDelegate {} + }, + Mixxx.TrackListColumn { + role: Mixxx.TrackListColumn.Role.Title + + label: qsTr("Title") + delegate: DefaultDelegate {} + }, + Mixxx.TrackListColumn { + + label: qsTr("Album") + delegate: DefaultDelegate {} + }, + Mixxx.TrackListColumn { + + label: qsTr("Track #") + delegate: DefaultDelegate {} + }, + Mixxx.TrackListColumn { + + label: qsTr("Year") + delegate: DefaultDelegate {} + }, + Mixxx.TrackListColumn { + + label: qsTr("Genre") + delegate: DefaultDelegate {} + }, + Mixxx.TrackListColumn { + + label: qsTr("Composer") + delegate: DefaultDelegate {} + }, + Mixxx.TrackListColumn { + + label: qsTr("Comment") + delegate: DefaultDelegate {} + }, + Mixxx.TrackListColumn { + + label: qsTr("Duration") + delegate: DefaultDelegate {} + }, + Mixxx.TrackListColumn { + + label: qsTr("BPM") + delegate: DefaultDelegate {} + }, + Mixxx.TrackListColumn { + + label: qsTr("Key") + delegate: DefaultDelegate {} + }, + Mixxx.TrackListColumn { + + label: qsTr("Type") + delegate: DefaultDelegate {} + }, + Mixxx.TrackListColumn { + + label: qsTr("Bitrate") + delegate: DefaultDelegate {} + }, + Mixxx.TrackListColumn { + role: Mixxx.TrackListColumn.Role.Location + + label: qsTr("Location") + delegate: DefaultDelegate {} + }, + Mixxx.TrackListColumn { + + label: qsTr("Album Artist") + delegate: DefaultDelegate {} + }, + Mixxx.TrackListColumn { + + label: qsTr("Grouping") + delegate: DefaultDelegate {} + }, + Mixxx.TrackListColumn { + + label: qsTr("File Modified") + delegate: DefaultDelegate {} + }, + Mixxx.TrackListColumn { + + label: qsTr("File Created") + delegate: DefaultDelegate {} + }, + Mixxx.TrackListColumn { + + label: qsTr("ReplayGain") + delegate: DefaultDelegate {} + } + ] + } } diff --git a/res/qml/Library/Track.qml b/res/qml/Library/Track.qml index 253c4a188b9e..8cb854b35a6e 100644 --- a/res/qml/Library/Track.qml +++ b/res/qml/Library/Track.qml @@ -2,11 +2,14 @@ import Mixxx 1.0 as Mixxx import QtQuick import QtQuick.Controls 2.15 import "../Theme" +import ".." as Skin MouseArea { id: dragArea required property var capabilities + required property var playlists + required property var crates readonly property var library: Mixxx.Library @@ -78,11 +81,72 @@ MouseArea { hasCapabilities(Mixxx.LibraryTrackListModel.Capability.AddToTrackSet) } + Instantiator { + model: playlists.list() + delegate: MenuItem { + text: modelData.name + onTriggered: modelData.addTrack(track) + enabled: !modelData.locked + } + + onObjectAdded: (index, object) => addToPlaylistMenu.insertItem(index, object) + onObjectRemoved: (index, object) => addToPlaylistMenu.removeItem(object) + } + MenuSeparator {} MenuItem { - enabled: false // TODO implement + id: createPlaylistItem + + property bool creating: false + text: qsTr("Create New Playlist") + contentItem: Item { + TextInput { + id: playlistNewName + visible: createPlaylistItem.creating + anchors.fill: parent + anchors.verticalCenter: parent.verticalCenter + anchors.horizontalCenter: parent.horizontalCenter + anchors.margins: 7 + focus: true + clip: true + color: acceptableInput ? "#000000" : 'red' + horizontalAlignment: TextInput.AlignLeft + onAccepted: { + if (!text) { + return + } + let result = playlists.create(text) + if (result != Mixxx.LibraryPlaylistSource.PlaylistCreateResult.Ok) { + // TODO UX feedback + console.warn("Create New Playlist", text, result) + return + } + playlists.get(text).addTrack(track) + text = "" + createPlaylistItem.creating = false + } + } + Text { + visible: !createPlaylistItem.creating + leftPadding: createPlaylistItem.indicator.width + rightPadding: createPlaylistItem.arrow.width + text: createPlaylistItem.text + font: createPlaylistItem.font + opacity: enabled ? 1.0 : 0.3 + color: "#000000" + horizontalAlignment: Text.AlignLeft + verticalAlignment: Text.AlignVCenter + elide: Text.ElideRight + } + TapHandler { + onTapped: { + createPlaylistItem.creating = true + playlistNewName.forceActiveFocus(); + } + } + } } } @@ -93,11 +157,73 @@ MouseArea { hasCapabilities(Mixxx.LibraryTrackListModel.Capability.AddToTrackSet) } + Instantiator { + model: crates.list([track]) + delegate: MenuItem { + text: modelData.name + checked: modelData.trackCount() == 1 + checkable: true + onToggled: { + if (checked) { + modelData.addTrack(track) + } else { + modelData.removeTrack(track) + } + } + enabled: !modelData.locked + } + + onObjectAdded: (index, object) => addToCrateMenu.insertItem(index, object) + onObjectRemoved: (index, object) => addToCrateMenu.removeItem(object) + } + MenuSeparator {} MenuItem { - enabled: false // TODO implement + id: createCrateItem + + property bool creating: false + text: qsTr("Create New Crate") + contentItem: Item { + TextInput { + id: crateNewName + visible: createCrateItem.creating + anchors.fill: parent + anchors.verticalCenter: parent.verticalCenter + anchors.horizontalCenter: parent.horizontalCenter + anchors.margins: 7 + focus: true + clip: true + color: acceptableInput ? "#000000" : 'red' + horizontalAlignment: TextInput.AlignLeft + onAccepted: { + if (text) { + crates.create(text) + } + text = "" + createCrateItem.creating = false + } + } + Text { + visible: !createCrateItem.creating + leftPadding: createCrateItem.indicator.width + rightPadding: createCrateItem.arrow.width + text: createCrateItem.text + font: createCrateItem.font + opacity: enabled ? 1.0 : 0.3 + color: "#000000" + horizontalAlignment: Text.AlignLeft + verticalAlignment: Text.AlignVCenter + elide: Text.ElideRight + } + TapHandler { + onTapped: { + createPlaylistItem.creating = true + playlistNewName.forceActiveFocus(); + } + } + } } } diff --git a/res/qml/images/library_computer.png b/res/qml/images/library_explorer.png similarity index 100% rename from res/qml/images/library_computer.png rename to res/qml/images/library_explorer.png diff --git a/src/library/trackset/baseplaylistfeature.h b/src/library/trackset/baseplaylistfeature.h index 933c391af32d..4270990b379a 100644 --- a/src/library/trackset/baseplaylistfeature.h +++ b/src/library/trackset/baseplaylistfeature.h @@ -39,6 +39,10 @@ class BasePlaylistFeature : public BaseTrackSetFeature { void selectPlaylistInSidebar(int playlistId, bool select = true); int getSiblingPlaylistIdOf(QModelIndex& start); + PlaylistDAO& dao() { + return m_playlistDao; + } + public slots: void activateChild(const QModelIndex& index) override; virtual void activatePlaylist(int playlistId); diff --git a/src/library/trackset/crate/cratefeature.h b/src/library/trackset/crate/cratefeature.h index 0289a6e20609..9800a02b2310 100644 --- a/src/library/trackset/crate/cratefeature.h +++ b/src/library/trackset/crate/cratefeature.h @@ -41,6 +41,11 @@ class CrateFeature : public BaseTrackSetFeature { TreeItemModel* sidebarModel() const override; + TrackCollection* const trackCollection() const { + return m_pTrackCollection; + ; + } + public slots: void activate() override; void activateChild(const QModelIndex& index) override; diff --git a/src/main.cpp b/src/main.cpp index f7741c98882f..3f4c0b1f6d6a 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -62,11 +62,6 @@ int runMixxx(MixxxApplication* pApp, const CmdlineArgs& args) { int exitCode; #ifdef MIXXX_USE_QML if (args.isQml()) { - // This is a workaround to support Qt 6.4.2, currently shipped on - // Ubuntu 24.04 See - // https://github.com/mixxxdj/mixxx/pull/14514#issuecomment-2770811094 - // for further details - qputenv("QT_QUICK_TABLEVIEW_COMPAT_VERSION", "6.4"); mixxx::qml::QmlApplication qmlApplication(pApp, args); exitCode = pApp->exec(); } else diff --git a/src/qml/qmlcrateproxy.cpp b/src/qml/qmlcrateproxy.cpp new file mode 100644 index 000000000000..3f8221fd15ae --- /dev/null +++ b/src/qml/qmlcrateproxy.cpp @@ -0,0 +1,63 @@ +#include "qml/qmlcrateproxy.h" + +#include +#include + +#include "library/trackcollection.h" +#include "library/trackset/crate/crate.h" +#include "moc_qmlcrateproxy.cpp" +#include "qmltrackproxy.h" +#include "track/track.h" +#include "util/assert.h" + +namespace mixxx { +namespace qml { + +QmlCrateProxy::QmlCrateProxy(QObject* parent, + TrackCollection* trackCollection, + const CrateSummary& crate) + : QObject(parent), + m_trackCollection(trackCollection), + m_internal(crate) { +} + +Q_INVOKABLE void QmlCrateProxy::addTrack(QmlTrackProxy* track) { + VERIFY_OR_DEBUG_ASSERT(track && track->internal()) { + return; + } + m_trackCollection->addCrateTracks(m_internal.getId(), {track->internal()->getId()}); +} +Q_INVOKABLE void QmlCrateProxy::removeTrack(QmlTrackProxy* track) { + VERIFY_OR_DEBUG_ASSERT(track && track->internal()) { + return; + } + m_trackCollection->removeCrateTracks(m_internal.getId(), {track->internal()->getId()}); +} + +QString QmlCrateProxy::name() const { + return m_internal.getName(); +} + +void QmlCrateProxy::setName(const QString& value) { + m_internal.setName(value); +} + +bool QmlCrateProxy::isLocked() const { + return m_internal.isLocked(); +} + +void QmlCrateProxy::setLocked(bool lock) { + m_internal.setLocked(lock); + m_trackCollection->updateCrate(m_internal); +} + +uint QmlCrateProxy::trackCount() const { + return m_internal.getTrackCount(); +} + +QmlLibraryTrackListModel* QmlCrateProxy::model() const { + return nullptr; +} + +} // namespace qml +} // namespace mixxx diff --git a/src/qml/qmlcrateproxy.h b/src/qml/qmlcrateproxy.h new file mode 100644 index 000000000000..0cb9ccc4e590 --- /dev/null +++ b/src/qml/qmlcrateproxy.h @@ -0,0 +1,53 @@ +#pragma once +#include +#include + +#include "library/dao/playlistdao.h" +#include "library/trackset/crate/crate.h" +#include "library/trackset/crate/cratestorage.h" +#include "library/trackset/crate/cratesummary.h" +#include "qmllibrarytracklistmodel.h" +#include "qmltrackproxy.h" + +class Library; +class TrackCollection; + +namespace mixxx { +namespace qml { + +class QmlCrateProxy : public QObject { + Q_OBJECT + Q_PROPERTY(QString name READ name WRITE setName NOTIFY nameChanged) + Q_PROPERTY(QmlLibraryTrackListModel* model READ model CONSTANT) + Q_PROPERTY(bool locked READ isLocked WRITE setLocked NOTIFY lockChanged) + QML_NAMED_ELEMENT(Crate) + QML_UNCREATABLE("") + + public: + explicit QmlCrateProxy(QObject* parent, + TrackCollection* trackCollection, + const CrateSummary& crate); + + Q_INVOKABLE void addTrack(mixxx::qml::QmlTrackProxy* track); + Q_INVOKABLE void removeTrack(mixxx::qml::QmlTrackProxy* track); + + QString name() const; + void setName(const QString& value); + + bool isLocked() const; + void setLocked(bool lock); + + Q_INVOKABLE uint trackCount() const; + + QmlLibraryTrackListModel* model() const; + signals: + void nameChanged(); + void lockChanged(); + + private: + TrackCollection* m_trackCollection; + CrateSummary m_internal; +}; + +} // namespace qml +} // namespace mixxx diff --git a/src/qml/qmllibrarysource.cpp b/src/qml/qmllibrarysource.cpp index cc11d6f16606..79f25db361bb 100644 --- a/src/qml/qmllibrarysource.cpp +++ b/src/qml/qmllibrarysource.cpp @@ -19,7 +19,10 @@ #include "library/trackset/playlistfeature.h" #include "library/treeitemmodel.h" #include "moc_qmllibrarysource.cpp" +#include "qml_owned_ptr.h" +#include "qmlconfigproxy.h" #include "qmllibraryproxy.h" +#include "qmlplaylistproxy.h" #include "track/track.h" AllTrackLibraryFeature::AllTrackLibraryFeature(Library* pLibrary, UserSettingsPointer pConfig) @@ -39,7 +42,8 @@ namespace qml { QmlLibrarySource::QmlLibrarySource( QObject* parent, const QList& columns) : QObject(parent), - m_columns(columns) { + m_columns(columns), + m_capabilities(0) { } void QmlLibrarySource::slotShowTrackModel(QAbstractItemModel* pModel) { @@ -56,6 +60,128 @@ QmlLibraryAllTrackSource::QmlLibraryAllTrackSource( this, &QmlLibrarySource::slotShowTrackModel); } +QmlLibraryPlaylistSource::QmlLibraryPlaylistSource( + QObject* parent, const QList& columns) + : QmlLibrarySource(parent, columns), + m_pLibraryFeature(std::make_unique( + QmlLibraryProxy::get(), QmlConfigProxy::get())) { + connect(m_pLibraryFeature.get(), + &LibraryFeature::showTrackModel, + this, + &QmlLibrarySource::slotShowTrackModel); +} +QmlLibraryPlaylistSource::PlaylistCreateResult QmlLibraryPlaylistSource::create( + const QString& name) const { + auto& dao = m_pLibraryFeature->dao(); + int existingId = dao.getPlaylistIdFromName(name); + + if (existingId != kInvalidPlaylistId) { + return PlaylistCreateResult::ConflictName; + } else if (name.isEmpty()) { + return PlaylistCreateResult::InvalidName; + } + + int playlistId = dao.createPlaylist(name); + + if (playlistId == kInvalidPlaylistId) { + return PlaylistCreateResult::Unknown; + } + + return PlaylistCreateResult::Ok; +} +QmlPlaylistProxy* QmlLibraryPlaylistSource::get(const QString& name) { + auto& playlistDao = m_pLibraryFeature->dao(); + int existingId = playlistDao.getPlaylistIdFromName(name); + + if (existingId == kInvalidPlaylistId) { + return nullptr; + } + + return make_qml_owned(this, playlistDao, existingId, name); +} + +QList QmlLibraryPlaylistSource::list() { + QList list; + auto& playlistDao = m_pLibraryFeature->dao(); + for (const auto& [id, name] : playlistDao.getPlaylists(PlaylistDAO::PLHT_NOT_HIDDEN)) { + list.append(make_qml_owned(this, playlistDao, id, name)); + } + return list; +} + +QmlLibraryCrateSource::QmlLibraryCrateSource( + QObject* parent, const QList& columns) + : QmlLibrarySource(parent, columns), + m_pLibraryFeature(std::make_unique( + QmlLibraryProxy::get(), QmlConfigProxy::get())) { + connect(m_pLibraryFeature.get(), + &LibraryFeature::showTrackModel, + this, + &QmlLibrarySource::slotShowTrackModel); +} + +QmlLibraryCrateSource::CrateCreateResult QmlLibraryCrateSource::create(const QString& name) const { + const auto& pTrackCollectionManager = m_pLibraryFeature->trackCollection(); + if (name.isEmpty()) { + return CrateCreateResult::InvalidName; + } + if (pTrackCollectionManager->crates().readCrateByName(name)) { + return CrateCreateResult::ConflictName; + } + Crate newCrate; + newCrate.setName(std::move(name)); + CrateId newCrateId; + if (pTrackCollectionManager->insertCrate(newCrate, &newCrateId)) { + DEBUG_ASSERT(newCrateId.isValid()); + newCrate.setId(newCrateId); + qDebug() << "Created new crate" << newCrate; + } else { + DEBUG_ASSERT(!newCrateId.isValid()); + qWarning() << "Failed to create new crate" + << "->" << newCrate.getName(); + return CrateCreateResult::Unknown; + } + return CrateCreateResult::Ok; +} + +QList QmlLibraryCrateSource::list(const QList& tracks) { + QList list; + auto& crateDao = m_pLibraryFeature; + auto* trackCollectionManager = QmlLibraryProxy::get() + ->trackCollectionManager() + ->internalCollection(); + + QList trackIds; + + for (const auto& track : tracks) { + trackIds.append(track->internal()->getId()); + } + + CrateSummarySelectResult allCrates( + trackCollectionManager + ->crates() + .selectCratesWithTrackCount({trackIds})); + + CrateSummary crate; + while (allCrates.populateNext(&crate)) { + list.append(make_qml_owned(this, trackCollectionManager, crate)); + } + return list; +} + +QmlLibraryExplorerSource::QmlLibraryExplorerSource( + QObject* parent, const QList& columns) + : QmlLibrarySource(parent, columns), + m_pLibraryFeature(std::make_unique( + QmlLibraryProxy::get(), + QmlConfigProxy::get(), + // TODO acquire recording manager from singleton implemented + nullptr)) { + connect(m_pLibraryFeature.get(), + &LibraryFeature::showTrackModel, + this, + &QmlLibrarySource::slotShowTrackModel); +} } // namespace qml } // namespace mixxx diff --git a/src/qml/qmllibrarysource.h b/src/qml/qmllibrarysource.h index 88e4d675d96a..46910fcbbf59 100644 --- a/src/qml/qmllibrarysource.h +++ b/src/qml/qmllibrarysource.h @@ -3,6 +3,7 @@ #include #include +#include #include #include #include @@ -17,8 +18,10 @@ #include "library/trackset/crate/cratefeature.h" #include "library/trackset/playlistfeature.h" #include "library/treeitem.h" -#include "qmlconfigproxy.h" -#include "qmllibrarytracklistmodel.h" +#include "qml_owned_ptr.h" +#include "qmlcrateproxy.h" +#include "qmllibraryproxy.h" +#include "qmlplaylistproxy.h" #include "util/parented_ptr.h" class LibraryTableModel; @@ -63,13 +66,23 @@ class QmlLibraryTrackListColumn; class QmlLibrarySource : public QObject { Q_OBJECT - Q_PROPERTY(QString label MEMBER m_label) - Q_PROPERTY(QString icon MEMBER m_icon) + Q_PROPERTY(QString label MEMBER m_label NOTIFY labelChanged) + Q_PROPERTY(QString itemName MEMBER m_itemName NOTIFY itemNameChanged) + Q_PROPERTY(QString icon MEMBER m_icon NOTIFY iconChanged) + Q_PROPERTY(uint capabilities MEMBER m_capabilities NOTIFY capabilitiesChanged) Q_PROPERTY(QQmlListProperty columns READ columnsQml) Q_CLASSINFO("DefaultProperty", "columns") QML_NAMED_ELEMENT(LibrarySource) QML_UNCREATABLE("Only accessible via its specialization") public: + enum class Capability { + None = 0u, + Create = 1u << 0u, + Rename = 1u << 1u, + AddTrack = 1u << 2u, + AddTrackSet = 1u << 3u, + }; + Q_ENUM(Capability) explicit QmlLibrarySource(QObject* parent = nullptr, const QList& columns = {}); @@ -80,6 +93,22 @@ class QmlLibrarySource : public QObject { const QList& columns() const { return m_columns; } + + const QString& label() const { + return m_label; + } + + const QString& icon() const { + return m_icon; + } + + const QString& itemName() const { + return m_itemName; + } + + uint capabilities() const { + return m_capabilities; + } virtual LibraryFeature* internal() = 0; public slots: void slotShowTrackModel(QAbstractItemModel* pModel); @@ -90,13 +119,46 @@ class QmlLibrarySource : public QObject { #else void requestTrackModel(std::shared_ptr pModel); #endif + void labelChanged(); + void itemNameChanged(); + void iconChanged(); + void capabilitiesChanged(); + + Q_INVOKABLE void requestCreate(const QString& requestedName); + Q_INVOKABLE void requestAddTrack(const QString& fileUrl); protected: QString m_label; + QString m_itemName; QString m_icon; + uint m_capabilities; QList m_columns; }; +class QmlLibraryPlaylistSource : public QmlLibrarySource { + Q_OBJECT + QML_NAMED_ELEMENT(LibraryPlaylistSource) + public: + enum class PlaylistCreateResult { + Ok, + InvalidName, + ConflictName, + Unknown + }; + Q_ENUM(PlaylistCreateResult); + explicit QmlLibraryPlaylistSource(QObject* parent = nullptr, + const QList& columns = {}); + + LibraryFeature* internal() override { + return m_pLibraryFeature.get(); + } + Q_INVOKABLE PlaylistCreateResult create(const QString& name) const; + Q_INVOKABLE mixxx::qml::QmlPlaylistProxy* get(const QString& name); + Q_INVOKABLE QList list(); + + private: + std::unique_ptr m_pLibraryFeature; +}; class QmlLibraryAllTrackSource : public QmlLibrarySource { Q_OBJECT QML_NAMED_ELEMENT(LibraryAllTrackSource) @@ -112,5 +174,45 @@ class QmlLibraryAllTrackSource : public QmlLibrarySource { std::unique_ptr m_pLibraryFeature; }; +class QmlLibraryCrateSource : public QmlLibrarySource { + Q_OBJECT + QML_NAMED_ELEMENT(LibraryCrateSource) + public: + enum class CrateCreateResult { + Ok, + InvalidName, + ConflictName, + Unknown + }; + Q_ENUM(CrateCreateResult); + explicit QmlLibraryCrateSource(QObject* parent = nullptr, + const QList& columns = {}); + + LibraryFeature* internal() override { + return m_pLibraryFeature.get(); + } + Q_INVOKABLE CrateCreateResult create(const QString& name) const; + Q_INVOKABLE QList list( + const QList& tracks); + + private: + std::unique_ptr m_pLibraryFeature; +}; + +class QmlLibraryExplorerSource : public QmlLibrarySource { + Q_OBJECT + QML_NAMED_ELEMENT(LibraryExplorerSource) + public: + explicit QmlLibraryExplorerSource(QObject* parent = nullptr, + const QList& columns = {}); + + LibraryFeature* internal() override { + return m_pLibraryFeature.get(); + } + + private: + std::unique_ptr m_pLibraryFeature; +}; + } // namespace qml } // namespace mixxx diff --git a/src/qml/qmlplaylistproxy.cpp b/src/qml/qmlplaylistproxy.cpp new file mode 100644 index 000000000000..71ce77d82f5f --- /dev/null +++ b/src/qml/qmlplaylistproxy.cpp @@ -0,0 +1,33 @@ +#include "qml/qmlplaylistproxy.h" + +#include +#include + +#include "moc_qmlplaylistproxy.cpp" +#include "qmltrackproxy.h" +#include "track/track.h" +#include "util/assert.h" + +namespace mixxx { +namespace qml { + +QmlPlaylistProxy::QmlPlaylistProxy(QObject* parent, PlaylistDAO& dao, int pid, const QString& name) + : QObject(parent), + m_id(pid), + m_name(name), + m_dao(dao) { +} + +Q_INVOKABLE void QmlPlaylistProxy::addTrack(QmlTrackProxy* track) { + VERIFY_OR_DEBUG_ASSERT(track && track->internal()) { + return; + } + m_dao.appendTrackToPlaylist(track->internal()->getId(), m_id); +} + +QmlLibraryTrackListModel* QmlPlaylistProxy::model() const { + return nullptr; +} + +} // namespace qml +} // namespace mixxx diff --git a/src/qml/qmlplaylistproxy.h b/src/qml/qmlplaylistproxy.h new file mode 100644 index 000000000000..f5077b31f2fb --- /dev/null +++ b/src/qml/qmlplaylistproxy.h @@ -0,0 +1,48 @@ +#pragma once +#include +#include + +#include "library/dao/playlistdao.h" +#include "qmllibrarytracklistmodel.h" +#include "qmltrackproxy.h" + +class Library; + +namespace mixxx { +namespace qml { + +class QmlPlaylistProxy : public QObject { + Q_OBJECT + Q_PROPERTY(QString name MEMBER m_name NOTIFY nameChanged) + Q_PROPERTY(mixxx::qml::QmlLibraryTrackListModel* model READ model CONSTANT) + Q_PROPERTY(bool locked READ isLocked WRITE setLocked NOTIFY lockChanged) + QML_NAMED_ELEMENT(Playlist) + QML_UNCREATABLE("Only accessible via a LibraryPlaylistSource") + + public: + explicit QmlPlaylistProxy(QObject* parent, PlaylistDAO& dao, int pid, const QString& name); + + Q_INVOKABLE void addTrack(mixxx::qml::QmlTrackProxy* track); + + bool isLocked() const { + return m_dao.isPlaylistLocked(m_id); + } + + void setLocked(bool lock) { + m_dao.setPlaylistLocked(m_id, lock); + emit lockChanged(); + } + + QmlLibraryTrackListModel* model() const; + signals: + void nameChanged(); + void lockChanged(); + + private: + QString m_name; + int m_id; + PlaylistDAO& m_dao; +}; + +} // namespace qml +} // namespace mixxx diff --git a/src/qml/qmlsidebarmodelproxy.cpp b/src/qml/qmlsidebarmodelproxy.cpp index 4483a732ccab..e4366b3db75d 100644 --- a/src/qml/qmlsidebarmodelproxy.cpp +++ b/src/qml/qmlsidebarmodelproxy.cpp @@ -19,6 +19,8 @@ namespace { const QHash kRoleNames = { {Qt::DisplayRole, "label"}, {QmlSidebarModelProxy::IconRole, "icon"}, + {QmlSidebarModelProxy::ItemNameRole, "itemName"}, + {QmlSidebarModelProxy::CapabilitiesRole, "capabilities"}, }; } // namespace @@ -62,19 +64,67 @@ QmlSidebarModelProxy::QmlSidebarModelProxy(QObject* parent) } QmlSidebarModelProxy::~QmlSidebarModelProxy() = default; +QVariant QmlSidebarModelProxy::data(const QModelIndex& index, int role) const { + if (index.internalPointer() != this) { + return SidebarModel::data(index, role); + } + VERIFY_OR_DEBUG_ASSERT(index.isValid() && index.row() >= 0 && + index.row() < m_pQmlFeatures.length()) { + return {}; + } + switch (role) { + case Qt::DisplayRole: + return m_pQmlFeatures[index.row()]->label(); + case QmlSidebarModelProxy::IconRole: + return m_pQmlFeatures[index.row()]->icon(); + case QmlSidebarModelProxy::ItemNameRole: + return m_pQmlFeatures[index.row()]->itemName(); + case QmlSidebarModelProxy::CapabilitiesRole: + return m_pQmlFeatures[index.row()]->capabilities(); + default: + return SidebarModel::data(index, role); + } +} + void QmlSidebarModelProxy::update(const QList& sources) { beginResetModel(); qDeleteAll(m_sFeatures); - for (const auto& librarySource : sources) { - VERIFY_OR_DEBUG_ASSERT(librarySource) { + qDeleteAll(m_pQmlFeatures); + for (auto* pLibrarySource : sources) { + VERIFY_OR_DEBUG_ASSERT(pLibrarySource) { continue; } - connect(librarySource, + connect(pLibrarySource, &QmlLibrarySource::requestTrackModel, this, &QmlSidebarModelProxy::slotShowTrackModel); - auto* pLibrarySource = librarySource->internal(); - addLibraryFeature(pLibrarySource); + m_pQmlFeatures.append(pLibrarySource); + addLibraryFeature(pLibrarySource->internal()); + const auto newIndex = index(m_sFeatures.length() - 1, 0); + connect(pLibrarySource, + &QmlLibrarySource::labelChanged, + this, + [this, newIndex]() { + emit dataChanged(newIndex, newIndex, {Qt::DisplayRole}); + }); + connect(pLibrarySource, + &QmlLibrarySource::itemNameChanged, + this, + [this, newIndex]() { + emit dataChanged(newIndex, newIndex, {QmlSidebarModelProxy::IconRole}); + }); + connect(pLibrarySource, + &QmlLibrarySource::iconChanged, + this, + [this, newIndex]() { + emit dataChanged(newIndex, newIndex, {QmlSidebarModelProxy::ItemNameRole}); + }); + connect(pLibrarySource, + &QmlLibrarySource::capabilitiesChanged, + this, + [this, newIndex]() { + emit dataChanged(newIndex, newIndex, {QmlSidebarModelProxy::CapabilitiesRole}); + }); } endResetModel(); } diff --git a/src/qml/qmlsidebarmodelproxy.h b/src/qml/qmlsidebarmodelproxy.h index 8607da4c2562..96c0834b89ef 100644 --- a/src/qml/qmlsidebarmodelproxy.h +++ b/src/qml/qmlsidebarmodelproxy.h @@ -25,8 +25,10 @@ class QmlSidebarModelProxy : public SidebarModel { QML_ANONYMOUS public: enum Roles { - LabelRole = Qt::UserRole, + LabelRole = SidebarModel::DataRole + 1, IconRole, + ItemNameRole, + CapabilitiesRole, }; Q_ENUM(Roles); Q_DISABLE_COPY_MOVE(QmlSidebarModelProxy) @@ -37,6 +39,8 @@ class QmlSidebarModelProxy : public SidebarModel { return m_tracklist.get(); } + QVariant data(const QModelIndex& index, + int role = Qt::DisplayRole) const override; void update(const QList& sources); QHash roleNames() const override; Q_INVOKABLE QVariant get(int row) const; @@ -49,6 +53,7 @@ class QmlSidebarModelProxy : public SidebarModel { private: std::shared_ptr m_tracklist; + QList m_pQmlFeatures; }; } // namespace qml