diff --git a/interfaces/constants.h b/interfaces/constants.h index 8b60f3cc1..49cff5189 100644 --- a/interfaces/constants.h +++ b/interfaces/constants.h @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2011 - 2022 UnionTech Software Technology Co., Ltd. +// SPDX-FileCopyrightText: 2011 - 2026 UnionTech Software Technology Co., Ltd. // // SPDX-License-Identifier: LGPL-3.0-or-later @@ -114,6 +114,7 @@ enum PluginFlag { Attribute_CanInsert = 0x400, // 插件属性-是否支持在其前面插入其他的插件,普通的快捷插件是支持的 Attribute_CanSetting = 0x800, // 插件属性-是否可以在控制中心设置显示或隐藏,如果设置了这个属性,请实现PluginsItemInterfaceV2::icon 接口,并返回在`控制中心-个性化-任务栏-插件区域`中显示的图标 Attribute_ForceDock = 0x1000, // 插件属性-强制显示在任务栏上 + Attribute_HasCard = 0x2000, // 插件属性-是否提供卡片区域 surface Attribute_Normal = Attribute_CanDrag | Attribute_CanInsert | Attribute_CanSetting, // 普通插件 diff --git a/interfaces/pluginsiteminterface_v3.h b/interfaces/pluginsiteminterface_v3.h new file mode 100644 index 000000000..2a49de85f --- /dev/null +++ b/interfaces/pluginsiteminterface_v3.h @@ -0,0 +1,41 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later + +#ifndef PLUGINSITEMINTERFACE_V3_H +#define PLUGINSITEMINTERFACE_V3_H + +#include "pluginsiteminterface_v2.h" + +#include +#include + +class PluginsItemInterfaceV3 : public PluginsItemInterfaceV2 +{ +public: + /** + * @brief Item key that should be exported as a card surface. + * + * The item key is still scoped by pluginName(), so the compositor can use + * "pluginName::itemKey" as a stable surface id. + */ + virtual QString cardItemKey() const { return {}; } + + /** + * @brief Native window for the card item. + * + * The loader exposes this window to the dock compositor as a Wayland + * surface. QML or QWidget based UI should be handled by plugin itself. + * The plugin keeps ownership of the returned window. + */ + virtual QWindow *cardWindow() const { return nullptr; } +}; + +QT_BEGIN_NAMESPACE + +#define ModuleInterface_iid_V3 "com.deepin.dock.PluginsItemInterface_V3" + +Q_DECLARE_INTERFACE(PluginsItemInterfaceV3, ModuleInterface_iid_V3) +QT_END_NAMESPACE + +#endif // PLUGINSITEMINTERFACE_V3_H diff --git a/plugins/dde-dock/media/CMakeLists.txt b/plugins/dde-dock/media/CMakeLists.txt index 5a450bbe7..36d3c576c 100644 --- a/plugins/dde-dock/media/CMakeLists.txt +++ b/plugins/dde-dock/media/CMakeLists.txt @@ -17,7 +17,7 @@ file(GLOB_RECURSE SRCS ) find_package(PkgConfig REQUIRED) -find_package(Qt${QT_VERSION_MAJOR} ${REQUIRED_QT_VERSION} REQUIRED COMPONENTS Widgets Svg DBus) +find_package(Qt${QT_VERSION_MAJOR} ${REQUIRED_QT_VERSION} REQUIRED COMPONENTS Widgets Svg DBus Quick Qml) find_package(Dtk${DTK_VERSION_MAJOR} REQUIRED COMPONENTS Widget) add_definitions("${QT_DEFINITIONS} -DQT_PLUGIN") @@ -33,6 +33,8 @@ target_link_libraries(${PLUGIN_NAME} PRIVATE Qt${QT_VERSION_MAJOR}::DBus Qt${QT_VERSION_MAJOR}::Widgets Qt${QT_VERSION_MAJOR}::Svg + Qt${QT_VERSION_MAJOR}::Quick + Qt${QT_VERSION_MAJOR}::Qml dockpluginmanager-interface ) diff --git a/plugins/dde-dock/media/mediacontroller.cpp b/plugins/dde-dock/media/mediacontroller.cpp index 880d2b200..38ce977f9 100644 --- a/plugins/dde-dock/media/mediacontroller.cpp +++ b/plugins/dde-dock/media/mediacontroller.cpp @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2019 - 2023 UnionTech Software Technology Co., Ltd. +// SPDX-FileCopyrightText: 2019 - 2026 UnionTech Software Technology Co., Ltd. // // SPDX-License-Identifier: LGPL-3.0-or-later @@ -10,6 +10,40 @@ #include #include #include +#include +#include + +namespace { + +QString metadataText(const QVariant &value) +{ + if (value.canConvert()) { + return value.toStringList().join(QStringLiteral(", ")); + } + + if (value.metaType().id() == QMetaType::QVariantList) { + QStringList parts; + const auto values = value.toList(); + for (const auto &item : values) { + const QString text = item.toString(); + if (!text.isEmpty()) { + parts.append(text); + } + } + return parts.join(QStringLiteral(", ")); + } + + return value.toString(); +} + +QString playerIdentity(const QString &service) +{ + QString fallback = service; + fallback.remove(QStringLiteral("org.mpris.MediaPlayer2.")); + return fallback; +} + +} MediaController::MediaController() : m_mediaPlayer2(nullptr) @@ -22,17 +56,34 @@ MediaController::MediaController() void MediaController::loadMediaPath(const QString &path) { - DBusMediaPlayer2 *newMediaPlayer = new DBusMediaPlayer2(path, "/org/mpris/MediaPlayer2", QDBusConnection::sessionBus(), this); + QDBusInterface propertiesInter(path, + QStringLiteral("/org/mpris/MediaPlayer2"), + QStringLiteral("org.freedesktop.DBus.Properties"), + QDBusConnection::sessionBus()); + const QDBusPendingCall asyncCall = propertiesInter.asyncCall(QStringLiteral("Get"), + QStringLiteral("org.mpris.MediaPlayer2"), + QStringLiteral("CanShowInUI")); + auto *watcher = new QDBusPendingCallWatcher(asyncCall, this); + connect(watcher, &QDBusPendingCallWatcher::finished, this, + [this, path](QDBusPendingCallWatcher *watcher) { + const QDBusPendingReply reply = *watcher; + watcher->deleteLater(); - if (!newMediaPlayer->canShowInUI()) { - newMediaPlayer->deleteLater(); - return; - } + const bool showInUI = reply.isError() ? true : reply.value().variant().toBool(); + if (showInUI) { + finishLoadMediaPath(path); + } + }); +} - if (!m_mediaPlayer2) - Q_EMIT mediaAcquired(); +void MediaController::finishLoadMediaPath(const QString &path) +{ + DBusMediaPlayer2 *newMediaPlayer = new DBusMediaPlayer2(path, "/org/mpris/MediaPlayer2", QDBusConnection::sessionBus(), this); + + const bool wasEmpty = !m_mediaPlayer2; m_path = path; MediaModel::ref().setPath(path); + m_appName = playerIdentity(path); // save paths if (!m_mediaPaths.contains(path)) @@ -46,9 +97,24 @@ void MediaController::loadMediaPath(const QString &path) connect(m_mediaPlayer2, &DBusMediaPlayer2::MetadataChanged, this, &MediaController::onMetaDataChanged); connect(m_mediaPlayer2, &DBusMediaPlayer2::PlaybackStatusChanged, this, &MediaController::onPlaybackStatusChanged); connect(m_mediaPlayer2, &DBusMediaPlayer2::CanControlChanged, &MediaModel::ref(), &MediaModel::onCanControlChanged); + connect(m_mediaPlayer2, &DBusMediaPlayer2::CanGoNextChanged, this, &MediaController::canGoNextChanged); + connect(m_mediaPlayer2, &DBusMediaPlayer2::CanGoPreviousChanged, this, &MediaController::canGoPreviousChanged); + connect(m_mediaPlayer2, &DBusMediaPlayer2::CanControlChanged, this, [this] { + Q_EMIT canTogglePlaybackChanged(canTogglePlayback()); + }); + connect(m_mediaPlayer2, &DBusMediaPlayer2::CanPlayChanged, this, [this] { + Q_EMIT canTogglePlaybackChanged(canTogglePlayback()); + }); + connect(m_mediaPlayer2, &DBusMediaPlayer2::CanPauseChanged, this, [this] { + Q_EMIT canTogglePlaybackChanged(canTogglePlayback()); + }); + syncPlayerProperties(); onMetaDataChanged(); onPlaybackStatusChanged(); + + if (wasEmpty) + Q_EMIT mediaAcquired(); } void MediaController::onMetaDataChanged() @@ -57,10 +123,18 @@ void MediaController::onMetaDataChanged() const auto &meta = m_mediaPlayer2->metadata(); MediaModel::MediaInfo info; - info.title = meta.value("xesam:title").toString(); - info.artist = meta.value("xesam:artist").toString(); - info.pixmap = QPixmap(QUrl(meta.value("mpris:artUrl").toString()).toLocalFile()).scaled(QSize(32, 32), Qt::IgnoreAspectRatio); + info.title = metadataText(meta.value("xesam:title")); + info.artist = metadataText(meta.value("xesam:artist")); + const QString artSource = meta.value("mpris:artUrl").toString(); + + const QString artPath = QUrl(artSource).toLocalFile(); + if (!artPath.isEmpty()) { + QImageReader reader(artPath); + reader.setScaledSize(QSize(32, 32)); + info.pixmap = QPixmap::fromImage(reader.read()); + } MediaModel::ref().setMediaInfo(info); + Q_EMIT mediaInfoChanged(); } void MediaController::onPlaybackStatusChanged() @@ -68,7 +142,9 @@ void MediaController::onPlaybackStatusChanged() if (!m_mediaPlayer2) return; const QString &stat = m_mediaPlayer2->playbackStatus(); - MediaModel::ref().setPlayState(stat == "Playing"); + const bool playing = stat == "Playing"; + MediaModel::ref().setPlayState(playing); + Q_EMIT playingChanged(playing); } void MediaController::removeMediaPath(const QString &path) @@ -85,6 +161,12 @@ void MediaController::removeMediaPath(const QString &path) m_mediaPlayer2->deleteLater(); m_mediaPlayer2 = nullptr; + m_path.clear(); + MediaModel::ref().setPath(QString()); + MediaModel::ref().setMediaInfo(MediaModel::MediaInfo()); + MediaModel::ref().setPlayState(false); + MediaModel::ref().onCanControlChanged(false); + clearCardState(); Q_EMIT mediaLosted(); } @@ -96,6 +178,20 @@ void MediaController::next() } } +void MediaController::previous() +{ + if (m_mediaPlayer2) { + m_mediaPlayer2->Previous(); + } +} + +void MediaController::togglePlayback() +{ + if (m_mediaPlayer2) { + m_mediaPlayer2->PlayPause(); + } +} + void MediaController::raise() { if (!m_path.isEmpty()) { @@ -121,4 +217,91 @@ void MediaController::play() bool MediaController::isWorking() const { return m_mediaPlayer2 != nullptr; -} \ No newline at end of file +} + +void MediaController::syncPlayerProperties() +{ + if (m_mediaPlayer2) { + MediaModel::ref().onCanControlChanged(m_mediaPlayer2->canControl()); + } + + Q_EMIT canGoNextChanged(canGoNext()); + Q_EMIT canGoPreviousChanged(canGoPrevious()); + Q_EMIT canTogglePlaybackChanged(canTogglePlayback()); +} + +bool MediaController::playing() const +{ + return m_mediaPlayer2 && m_mediaPlayer2->playbackStatus() == QStringLiteral("Playing"); +} + +bool MediaController::canGoNext() const +{ + return m_mediaPlayer2 && m_mediaPlayer2->canGoNext(); +} + +bool MediaController::canGoPrevious() const +{ + return m_mediaPlayer2 && m_mediaPlayer2->canGoPrevious(); +} + +bool MediaController::canTogglePlayback() const +{ + return m_mediaPlayer2 + && m_mediaPlayer2->canControl() + && (m_mediaPlayer2->canPlay() || m_mediaPlayer2->canPause()); +} + +QString MediaController::titleText() const +{ + if (!m_mediaPlayer2) { + return tr("Music"); + } + + const auto metadata = m_mediaPlayer2->metadata(); + const QString title = metadataText(metadata.value("xesam:title")); + if (!title.isEmpty()) { + return title; + } + + const QString artist = metadataText(metadata.value("xesam:artist")); + if (!artist.isEmpty()) { + return artist; + } + + return tr("Music"); +} + +QString MediaController::subtitleText() const +{ + if (m_mediaPlayer2) { + const auto metadata = m_mediaPlayer2->metadata(); + const QString title = metadataText(metadata.value("xesam:title")); + const QString artist = metadataText(metadata.value("xesam:artist")); + if (!artist.isEmpty() && artist != title) { + return artist; + } + } + + return m_appName; +} + +QString MediaController::artSource() const +{ + return m_mediaPlayer2 + ? m_mediaPlayer2->metadata().value("mpris:artUrl").toString() + : QString(); +} + +bool MediaController::hasArt() const +{ + return !artSource().isEmpty(); +} + +void MediaController::clearCardState() +{ + m_appName.clear(); + Q_EMIT mediaInfoChanged(); + Q_EMIT playingChanged(false); + syncPlayerProperties(); +} diff --git a/plugins/dde-dock/media/mediacontroller.h b/plugins/dde-dock/media/mediacontroller.h index 231a98925..26f6f8af0 100644 --- a/plugins/dde-dock/media/mediacontroller.h +++ b/plugins/dde-dock/media/mediacontroller.h @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2019 - 2023 UnionTech Software Technology Co., Ltd. +// SPDX-FileCopyrightText: 2019 - 2026 UnionTech Software Technology Co., Ltd. // // SPDX-License-Identifier: LGPL-3.0-or-later @@ -14,21 +14,45 @@ class MediaController : public QObject { Q_OBJECT + Q_PROPERTY(bool playing READ playing NOTIFY playingChanged) + Q_PROPERTY(bool canGoNext READ canGoNext NOTIFY canGoNextChanged) + Q_PROPERTY(bool canGoPrevious READ canGoPrevious NOTIFY canGoPreviousChanged) + Q_PROPERTY(bool canTogglePlayback READ canTogglePlayback NOTIFY canTogglePlaybackChanged) + Q_PROPERTY(QString titleText READ titleText NOTIFY mediaInfoChanged) + Q_PROPERTY(QString subtitleText READ subtitleText NOTIFY mediaInfoChanged) + Q_PROPERTY(QString artSource READ artSource NOTIFY mediaInfoChanged) + Q_PROPERTY(bool hasArt READ hasArt NOTIFY mediaInfoChanged) public: MediaController(); ~MediaController() {} bool isWorking() const; - void next(); - void pause(); - void play(); - void raise(); + bool playing() const; + bool canGoNext() const; + bool canGoPrevious() const; + bool canTogglePlayback() const; + QString titleText() const; + QString subtitleText() const; + QString artSource() const; + bool hasArt() const; + + Q_INVOKABLE void next(); + Q_INVOKABLE void previous(); + Q_INVOKABLE void togglePlayback(); + Q_INVOKABLE void pause(); + Q_INVOKABLE void play(); + Q_INVOKABLE void raise(); const QString &mediaPath() const { return m_path; } Q_SIGNALS: void mediaAcquired() const; void mediaChanged() const; void mediaLosted() const; + void playingChanged(bool playing) const; + void canGoNextChanged(bool canGoNext) const; + void canGoPreviousChanged(bool canGoPrevious) const; + void canTogglePlaybackChanged(bool canTogglePlayback) const; + void mediaInfoChanged() const; public Q_SLOTS: void loadMediaPath(const QString &path); @@ -36,12 +60,18 @@ public Q_SLOTS: void onMetaDataChanged(); void onPlaybackStatusChanged(); +private: + void finishLoadMediaPath(const QString &path); + void syncPlayerProperties(); + void clearCardState(); + private: DBusMediaPlayer2 *m_mediaPlayer2; MediaMonitor *m_mediaMonitor; QString m_path; QStringList m_mediaPaths; + QString m_appName; }; #endif diff --git a/plugins/dde-dock/media/mediaplugin.cpp b/plugins/dde-dock/media/mediaplugin.cpp index eada2aca9..4de40ef2e 100644 --- a/plugins/dde-dock/media/mediaplugin.cpp +++ b/plugins/dde-dock/media/mediaplugin.cpp @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2020 - 2022 UnionTech Software Technology Co., Ltd. +// SPDX-FileCopyrightText: 2020 - 2026 UnionTech Software Technology Co., Ltd. // // SPDX-License-Identifier: LGPL-3.0-or-later @@ -7,6 +7,9 @@ #include "quickpanelwidget.h" #include "plugins-logging-category.h" +#include +#include + #define MEDIA_KEY "media-key" #define STATE_KEY "enable" @@ -19,6 +22,14 @@ MediaPlugin::MediaPlugin(QObject *parent) } +MediaPlugin::~MediaPlugin() +{ + if (m_cardView) { + delete m_cardView; + m_cardView = nullptr; + } +} + void MediaPlugin::init(PluginProxyInterface *proxyInter) { m_proxyInter = proxyInter; @@ -27,23 +38,14 @@ void MediaPlugin::init(PluginProxyInterface *proxyInter) m_quickPanelWidget.reset(new QuickPanelWidget(m_controller.data())); m_quickPanelWidget->setFixedHeight(60); - if (pluginIsDisable()) { - qCDebug(MEDIA) << "Media plugin init, plugin is disabled"; - return; - } - connect(m_controller.data(), &MediaController::mediaAcquired, this, [this] { - m_proxyInter->itemAdded(this, MEDIA_KEY); - }); - connect(m_controller.data(), &MediaController::mediaLosted, this, [this] { - m_proxyInter->itemRemoved(this, MEDIA_KEY); - }); + connect(m_controller.data(), &MediaController::mediaAcquired, this, &MediaPlugin::refreshPluginItemsVisible); + connect(m_controller.data(), &MediaController::mediaLosted, this, &MediaPlugin::refreshPluginItemsVisible); connect(m_quickPanelWidget.data(), &QuickPanelWidget::requestHideApplet, this, [this] { if (m_proxyInter) m_proxyInter->requestSetAppletVisible(this, MEDIA_KEY, false); }); - if (m_controller->isWorking()) - m_proxyInter->itemAdded(this, MEDIA_KEY); + refreshPluginItemsVisible(); } const QString MediaPlugin::pluginName() const @@ -126,10 +128,41 @@ void MediaPlugin::pluginSettingsChanged() void MediaPlugin::refreshPluginItemsVisible() { - if (pluginIsDisable()) + if (!m_proxyInter) { + return; + } + + if (pluginIsDisable() || !m_controller || !m_controller->isWorking()) { m_proxyInter->itemRemoved(this, MEDIA_KEY); - else - m_proxyInter->itemAdded(this, MEDIA_KEY); + m_proxyInter->itemRemoved(this, cardItemKey()); + return; + } + + m_proxyInter->itemAdded(this, MEDIA_KEY); + m_proxyInter->itemAdded(this, cardItemKey()); +} + +QString MediaPlugin::cardItemKey() const +{ + return QStringLiteral("media-card"); } +QWindow *MediaPlugin::cardWindow() const +{ + if (m_cardView) { + return m_cardView; + } + + auto view = new QQuickView; + view->setColor(Qt::transparent); + view->setResizeMode(QQuickView::SizeRootObjectToView); + view->rootContext()->setContextProperty(QStringLiteral("mediaController"), m_controller.data()); + view->setSource(QUrl(QStringLiteral("qrc:/media/card"))); + if (view->status() == QQuickView::Error) { + delete view; + return nullptr; + } + m_cardView = view; + return m_cardView; +} diff --git a/plugins/dde-dock/media/mediaplugin.h b/plugins/dde-dock/media/mediaplugin.h index 03ef6ee59..88c12131b 100644 --- a/plugins/dde-dock/media/mediaplugin.h +++ b/plugins/dde-dock/media/mediaplugin.h @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2020 - 2022 UnionTech Software Technology Co., Ltd. +// SPDX-FileCopyrightText: 2020 - 2026 UnionTech Software Technology Co., Ltd. // // SPDX-License-Identifier: LGPL-3.0-or-later @@ -6,19 +6,24 @@ #define MEDIAPLUGIN_H #include "mediacontroller.h" -#include "pluginsiteminterface_v2.h" +#include "pluginsiteminterface_v3.h" #include "quickpanelwidget.h" #include "dtkcore_global.h" -class MediaPlugin : public QObject, public PluginsItemInterfaceV2 +#include + +class QQuickView; + +class MediaPlugin : public QObject, public PluginsItemInterfaceV3 { Q_OBJECT - Q_INTERFACES(PluginsItemInterfaceV2) - Q_PLUGIN_METADATA(IID ModuleInterface_iid_V2 FILE "media.json") + Q_INTERFACES(PluginsItemInterfaceV3) + Q_PLUGIN_METADATA(IID ModuleInterface_iid_V3 FILE "media.json") public: explicit MediaPlugin(QObject *parent = nullptr); + ~MediaPlugin() override; virtual const QString pluginName() const Q_DECL_OVERRIDE; virtual const QString pluginDisplayName() const Q_DECL_OVERRIDE; @@ -34,8 +39,10 @@ class MediaPlugin : public QObject, public PluginsItemInterfaceV2 virtual int itemSortKey(const QString &itemKey) Q_DECL_OVERRIDE; virtual void setSortKey(const QString &itemKey, const int order) Q_DECL_OVERRIDE; virtual void refreshIcon(const QString &itemKey) Q_DECL_OVERRIDE; - virtual Dock::PluginFlags flags() const override { return Dock::Type_Quick | Dock::Quick_Panel_Full; } + virtual Dock::PluginFlags flags() const override { return Dock::Type_Quick | Dock::Quick_Panel_Full | Dock::Attribute_HasCard; } virtual void pluginSettingsChanged() override; + QString cardItemKey() const override; + QWindow *cardWindow() const override; private: void refreshPluginItemsVisible(); @@ -43,6 +50,7 @@ class MediaPlugin : public QObject, public PluginsItemInterfaceV2 private: QScopedPointer m_controller; QScopedPointer m_quickPanelWidget; + mutable QPointer m_cardView; }; #endif // MEDIAPLUGIN_H diff --git a/plugins/dde-dock/media/resources/media.qrc b/plugins/dde-dock/media/resources/media.qrc index 6ac36e964..21a73478a 100644 --- a/plugins/dde-dock/media/resources/media.qrc +++ b/plugins/dde-dock/media/resources/media.qrc @@ -1,6 +1,7 @@ texts/play-next_24px.svg + texts/play-previous_24px.svg texts/open-arrow_16px.svg texts/play-pause_24px.svg texts/play-start_24px.svg @@ -8,4 +9,7 @@ deepin-music.svg + + qml/MusicCard.qml + diff --git a/plugins/dde-dock/media/resources/qml/MusicCard.qml b/plugins/dde-dock/media/resources/qml/MusicCard.qml new file mode 100644 index 000000000..231ca2fb6 --- /dev/null +++ b/plugins/dde-dock/media/resources/qml/MusicCard.qml @@ -0,0 +1,278 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +import QtQuick 2.15 +import QtQuick.Window 2.15 +import Qt5Compat.GraphicalEffects +import org.deepin.dtk 1.0 as D + +Item { + id: root + + implicitWidth: 190 + implicitHeight: 40 + + readonly property bool dark: D.DTK.themeType === D.ApplicationHelper.DarkType + readonly property int contentHeight: { + const cardHeight = Math.max(30, height > 0 ? height : implicitHeight) + const verticalInset = Math.max(4, Math.round(cardHeight * 0.16)) + return Math.max(24, cardHeight - verticalInset * 2) + } + readonly property int artworkSize: Math.max(22, Math.min(34, Math.round(contentHeight * 0.86))) + readonly property int buttonSize: Math.max(26, Math.min(30, contentHeight)) + readonly property int buttonSpacing: Math.max(3, Math.round(contentHeight * 0.07)) + readonly property int animationDuration: 100 + property real hoverProgress: hoverHandler.hovered ? 1 : 0 + + Behavior on hoverProgress { + NumberAnimation { + duration: root.animationDuration + 70 + easing.type: Easing.OutCubic + } + } + + HoverHandler { + id: hoverHandler + } + + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.LeftButton + onClicked: mediaController.raise() + } + + component MusicActionButton: Item { + id: button + + property string iconName + property int layoutIndex + property int hoverDelay + property real revealProgress: hoverHandler.hovered ? 1 : 0 + readonly property bool interactive: enabled && revealProgress > 0.55 + + signal clicked() + + implicitWidth: root.buttonSize + implicitHeight: root.buttonSize + x: { + const collapsedX = (parent.width - width) / 2 + const expandedX = layoutIndex * (root.buttonSize + root.buttonSpacing) + return collapsedX + (expandedX - collapsedX) * revealProgress + } + scale: 0.7 + revealProgress * 0.3 + opacity: revealProgress + + Behavior on revealProgress { + SequentialAnimation { + PauseAnimation { + duration: hoverHandler.hovered ? button.hoverDelay : 0 + } + NumberAnimation { + duration: hoverHandler.hovered ? root.animationDuration + 110 : root.animationDuration + easing.type: Easing.OutCubic + } + } + } + + HoverHandler { + id: buttonHoverHandler + enabled: button.interactive + } + + Rectangle { + anchors.fill: parent + radius: width / 2 + color: root.dark ? Qt.rgba(1, 1, 1, 0.12) : Qt.rgba(0, 0, 0, 0.08) + opacity: buttonHoverHandler.hovered ? button.revealProgress : 0 + + Behavior on opacity { + NumberAnimation { + duration: root.animationDuration + easing.type: Easing.OutCubic + } + } + } + + Image { + id: iconImage + + anchors.centerIn: parent + width: 16 + height: 16 + source: "qrc:/icons/deepin/builtin/texts/" + button.iconName + "_24px.svg" + sourceSize: Qt.size(width, height) + fillMode: Image.PreserveAspectFit + smooth: true + visible: false + } + + ColorOverlay { + anchors.fill: iconImage + source: iconImage + color: root.dark ? "white" : "black" + opacity: button.enabled ? 1 : 0.38 + } + + MouseArea { + anchors.fill: parent + enabled: button.interactive + hoverEnabled: true + cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor + onClicked: button.clicked() + } + } + + Item { + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + height: root.contentHeight + + Item { + id: artworkSlot + + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + width: Math.max(root.artworkSize, root.contentHeight + 2) + height: width + + Rectangle { + id: artworkFrame + + anchors.centerIn: parent + width: root.artworkSize + height: root.artworkSize + radius: 4 + color: root.dark ? Qt.rgba(1, 1, 1, 0.08) : Qt.rgba(0, 0, 0, 0.06) + visible: mediaController.hasArt && artworkImage.status === Image.Ready + } + + Image { + anchors.centerIn: parent + width: root.artworkSize + height: root.artworkSize + source: "qrc:/deepin-music.svg" + sourceSize: Qt.size(width, height) + fillMode: Image.PreserveAspectFit + smooth: true + visible: !artworkFrame.visible + } + + Image { + id: artworkImage + + anchors.fill: artworkFrame + visible: false + source: mediaController.artSource + fillMode: Image.PreserveAspectCrop + asynchronous: true + cache: false + sourceSize: { + const pixelSize = Math.round(40 * Math.max(1, Screen.devicePixelRatio)) + return Qt.size(pixelSize, pixelSize) + } + mipmap: true + smooth: true + } + + Rectangle { + id: artworkMask + + anchors.fill: artworkFrame + radius: 4 + visible: false + } + + OpacityMask { + anchors.fill: artworkFrame + source: artworkImage + maskSource: artworkMask + visible: artworkFrame.visible + cached: false + } + + Rectangle { + anchors.fill: artworkFrame + radius: 4 + color: "transparent" + border.width: 1 + border.color: Qt.rgba(0, 0, 0, 0.2) + visible: artworkFrame.visible + } + } + + Item { + anchors.left: artworkSlot.right + anchors.leftMargin: 4 + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + height: Math.max(infoColumn.implicitHeight, controls.height) + clip: true + + Column { + id: infoColumn + + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + width: parent.width + spacing: 1 + opacity: 1 - root.hoverProgress + scale: 1 - root.hoverProgress * 0.08 + x: -10 * root.hoverProgress + visible: opacity > 0.01 + + Text { + width: parent.width + text: mediaController.titleText + color: root.dark ? Qt.rgba(1, 1, 1, 0.96) : Qt.rgba(0, 0, 0, 0.92) + font.pixelSize: Math.max(11, Math.round(root.contentHeight * 0.4)) + renderType: Text.NativeRendering + elide: Text.ElideRight + } + + Text { + width: parent.width + text: mediaController.subtitleText + color: root.dark ? Qt.rgba(1, 1, 1, 0.68) : Qt.rgba(0, 0, 0, 0.58) + font.pixelSize: Math.max(9, Math.round(root.contentHeight * 0.24)) + renderType: Text.NativeRendering + elide: Text.ElideRight + visible: text.length > 0 + } + } + + Item { + id: controls + + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + width: root.buttonSize * 3 + root.buttonSpacing * 2 + height: root.buttonSize + + MusicActionButton { + layoutIndex: 0 + hoverDelay: 24 + iconName: "play-previous" + enabled: mediaController.canGoPrevious + onClicked: mediaController.previous() + } + + MusicActionButton { + layoutIndex: 1 + iconName: mediaController.playing ? "play-pause" : "play-start" + enabled: mediaController.canTogglePlayback + onClicked: mediaController.togglePlayback() + } + + MusicActionButton { + layoutIndex: 2 + hoverDelay: 48 + iconName: "play-next" + enabled: mediaController.canGoNext + onClicked: mediaController.next() + } + } + } + } +} diff --git a/plugins/dde-dock/media/resources/texts/play-previous_24px.svg b/plugins/dde-dock/media/resources/texts/play-previous_24px.svg new file mode 100644 index 000000000..d6af8aeaa --- /dev/null +++ b/plugins/dde-dock/media/resources/texts/play-previous_24px.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/loader/CMakeLists.txt b/src/loader/CMakeLists.txt index fd396f518..3e7913ccc 100644 --- a/src/loader/CMakeLists.txt +++ b/src/loader/CMakeLists.txt @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2023 UnionTech Software Technology Co., Ltd. +# SPDX-FileCopyrightText: 2023 - 2026 UnionTech Software Technology Co., Ltd. # # SPDX-License-Identifier: CC0-1.0 @@ -17,6 +17,8 @@ add_executable(trayplugin-loader dockdbusproxy.h widgetplugin.h widgetplugin.cpp + cardpluginitem.h + cardpluginitem.cpp pluginitem.h pluginitem.cpp quickpluginitem.h diff --git a/src/loader/cardpluginitem.cpp b/src/loader/cardpluginitem.cpp new file mode 100644 index 000000000..acbe8fb96 --- /dev/null +++ b/src/loader/cardpluginitem.cpp @@ -0,0 +1,69 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "cardpluginitem.h" + +CardPluginItem::CardPluginItem(PluginsItemInterfaceV3 *pluginInterface, + const QString &itemKey, + QObject *parent) + : QObject(parent) + , m_pluginInterface(pluginInterface) + , m_itemKey(itemKey) +{ +} + +CardPluginItem::~CardPluginItem() +{ + if (m_window) { + m_window->hide(); + } + + m_window = nullptr; +} + +bool CardPluginItem::init() +{ + if (!m_pluginInterface) { + return false; + } + + auto window = m_pluginInterface->cardWindow(); + if (!window) { + return false; + } + + m_window = window; + m_window->setTitle(QStringLiteral("%1:%2-card").arg(m_pluginInterface->pluginName(), m_itemKey)); + m_window->setFlag(Qt::FramelessWindowHint, true); + + return true; +} + +void CardPluginItem::show() +{ + if (m_window) { + m_window->show(); + } +} + +void CardPluginItem::hide() +{ + if (m_window) { + m_window->hide(); + } +} + +QWindow *CardPluginItem::window() const +{ + return m_window; +} + +void CardPluginItem::resize(const QSize &size) +{ + if (!m_window || !size.isValid() || size.isEmpty() || m_window->size() == size) { + return; + } + + m_window->resize(size); +} diff --git a/src/loader/cardpluginitem.h b/src/loader/cardpluginitem.h new file mode 100644 index 000000000..7cb6d74ce --- /dev/null +++ b/src/loader/cardpluginitem.h @@ -0,0 +1,33 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include "pluginsiteminterface_v3.h" + +#include +#include +#include +#include + +class CardPluginItem : public QObject +{ +public: + explicit CardPluginItem(PluginsItemInterfaceV3 *pluginInterface, + const QString &itemKey, + QObject *parent = nullptr); + ~CardPluginItem() override; + + bool init(); + void show(); + void hide(); + + QWindow *window() const; + void resize(const QSize &size); + +private: + PluginsItemInterfaceV3 *m_pluginInterface = nullptr; + QString m_itemKey; + QPointer m_window; +}; diff --git a/src/loader/pluginmanager.cpp b/src/loader/pluginmanager.cpp index 10f6b3de2..fb84cb560 100644 --- a/src/loader/pluginmanager.cpp +++ b/src/loader/pluginmanager.cpp @@ -3,6 +3,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later #include "pluginmanager.h" +#include "pluginsiteminterface_v3.h" #include "pluginsiteminterface_v2.h" #include "widgetplugin.h" @@ -76,7 +77,10 @@ void PluginManager::loadPlugin(const QString &pluginFilePath) } QObject *instance = pluginLoader->instance(); - PluginsItemInterface *interface = qobject_cast(instance); + PluginsItemInterface *interface = qobject_cast(instance); + if (!interface) { + interface = qobject_cast(instance); + } if (!interface) { interface = qobject_cast(instance); } diff --git a/src/loader/widgetplugin.cpp b/src/loader/widgetplugin.cpp index c5f3c276d..586451d39 100644 --- a/src/loader/widgetplugin.cpp +++ b/src/loader/widgetplugin.cpp @@ -110,12 +110,19 @@ WidgetPlugin::WidgetPlugin(PluginsItemInterface* pluginsItemInterface, QObject * WidgetPlugin::~WidgetPlugin() { + qDeleteAll(m_cardItems); + m_cardItems.clear(); } void WidgetPlugin::itemAdded(PluginsItemInterface * const itemInter, const QString &itemKey) { qDebug() << "itemAdded:" << itemKey; + auto flag = getPluginFlags(); + if ((flag & Dock::Attribute_HasCard) && createCardItemIfNeeded(itemInter, itemKey)) { + return; + } + if (flag & Dock::Type_Quick) { if (!Plugin::EmbedPlugin::contains(itemInter->pluginName(), Plugin::EmbedPlugin::Quick)) { PluginItem *item = new QuickPluginItem(itemInter, itemKey); @@ -216,6 +223,12 @@ void WidgetPlugin::itemUpdate(PluginsItemInterface * const itemInter, const QStr void WidgetPlugin::itemRemoved(PluginsItemInterface * const itemInter, const QString &itemKey) { Q_UNUSED(itemInter) + if (auto cardItem = m_cardItems.take(itemKey)) { + cardItem->hide(); + cardItem->deleteLater(); + return; + } + auto widget = m_pluginsItemInterface->itemWidget(itemKey); if(widget && widget->window() && widget->window()->windowHandle()) { widget->window()->windowHandle()->hide(); @@ -367,6 +380,48 @@ Plugin::EmbedPlugin* WidgetPlugin::getPlugin(QWidget* widget) return Plugin::EmbedPlugin::get(widget->windowHandle()); } +bool WidgetPlugin::createCardItemIfNeeded(PluginsItemInterface *itemInter, const QString &itemKey) +{ + auto cardInterface = dynamic_cast(itemInter); + if (!cardInterface || cardInterface->cardItemKey() != itemKey) { + return false; + } + + if (auto cardItem = m_cardItems.value(itemKey)) { + cardItem->show(); + return true; + } + + auto cardItem = new CardPluginItem(cardInterface, itemKey, this); + if (!cardItem->init() || !cardItem->window()) { + cardItem->deleteLater(); + qWarning() << "create card plugin surface failed" << itemInter->pluginName() << itemKey; + return false; + } + + auto plugin = Plugin::EmbedPlugin::get(cardItem->window()); + if (!plugin) { + qWarning() << "Failed to get EmbedPlugin for card window" << itemInter->pluginName() << itemKey; + cardItem->deleteLater(); + return false; + } + + plugin->setPluginFlags(getPluginFlags()); + plugin->setPluginId(itemInter->pluginName()); + plugin->setDisplayName(itemInter->pluginDisplayName()); + plugin->setItemKey(itemKey); + plugin->setPluginType(Plugin::EmbedPlugin::Card); + plugin->setPluginSizePolicy(itemInter->pluginSizePolicy()); + connect(plugin, &Plugin::EmbedPlugin::dockColorThemeChanged, this, &WidgetPlugin::onDockColorThemeChanged, Qt::UniqueConnection); + connect(plugin, &Plugin::EmbedPlugin::eventGeometry, cardItem, [cardItem](const QRect &geometry) { + cardItem->resize(geometry.size()); + }); + + m_cardItems.insert(itemKey, cardItem); + cardItem->show(); + return true; +} + void WidgetPlugin::initConnections(Plugin::EmbedPlugin *plugin, PluginItem *pluginItem) { if (!plugin) diff --git a/src/loader/widgetplugin.h b/src/loader/widgetplugin.h index 6d804cb7c..d72f87b72 100644 --- a/src/loader/widgetplugin.h +++ b/src/loader/widgetplugin.h @@ -1,12 +1,14 @@ -// SPDX-FileCopyrightText: 2023 UnionTech Software Technology Co., Ltd. +// SPDX-FileCopyrightText: 2023 - 2026 UnionTech Software Technology Co., Ltd. // // SPDX-License-Identifier: GPL-3.0-or-later #pragma once +#include "cardpluginitem.h" #include "pluginitem.h" -#include "pluginsiteminterface_v2.h" +#include "pluginsiteminterface_v3.h" +#include #include #include #include @@ -47,6 +49,7 @@ public Q_SLOTS: private: Plugin::EmbedPlugin* getPlugin(QWidget*); void initConnections(Plugin::EmbedPlugin *plugin, PluginItem *pluginItem); + bool createCardItemIfNeeded(PluginsItemInterface *itemInter, const QString &itemKey); int getPluginFlags(); void pluginUpdateDockSize(const QSize &size); @@ -58,6 +61,7 @@ public Q_SLOTS: private: PluginsItemInterface* m_pluginsItemInterface; QScopedPointer m_pluginItem; + QHash m_cardItems; }; } diff --git a/src/tray-wayland-integration/plugin.cpp b/src/tray-wayland-integration/plugin.cpp index cb63a0a8d..1d09f6351 100644 --- a/src/tray-wayland-integration/plugin.cpp +++ b/src/tray-wayland-integration/plugin.cpp @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2023 UnionTech Software Technology Co., Ltd. +// SPDX-FileCopyrightText: 2023 - 2026 UnionTech Software Technology Co., Ltd. // // SPDX-License-Identifier: GPL-3.0-or-later @@ -6,6 +6,7 @@ #include #include +#include #include DGUI_USE_NAMESPACE @@ -149,9 +150,15 @@ EmbedPlugin* EmbedPlugin::get(QWindow* window) s_map.remove(window); }); - QObject::connect(window, &QWindow::visibleChanged, window, [window, plugin] (bool visible) { + QPointer pluginGuard(plugin); + QObject::connect(window, &QWindow::visibleChanged, window, [window, pluginGuard] (bool visible) { if (!visible) { - plugin->deleteLater(); + if (!pluginGuard) { + s_map.remove(window); + return; + } + + pluginGuard->deleteLater(); s_map.remove(window); } }); diff --git a/src/tray-wayland-integration/plugin.h b/src/tray-wayland-integration/plugin.h index 52f0a789a..1e68efe24 100644 --- a/src/tray-wayland-integration/plugin.h +++ b/src/tray-wayland-integration/plugin.h @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2023 UnionTech Software Technology Co., Ltd. +// SPDX-FileCopyrightText: 2023 - 2026 UnionTech Software Technology Co., Ltd. // // SPDX-License-Identifier: GPL-3.0-or-later @@ -27,6 +27,7 @@ class EmbedPlugin : public QObject Tray = 1, Fixed, Quick, + Card, }; Q_ENUM(PluginType)