feat: add playlist, recent files and MPRIS backends
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
6dc9377de1
commit
54ad4eecf3
18 files changed
+2240
-4
No files matched your search
+14
-1
@@ -18,8 +18,20 @@ target_link_libraries(qtpotato_core PUBLIC
|
||||
|
||||
add_executable(qtpotato
|
||||
main.cpp
|
||||
models/recentfilesmodel.cpp
|
||||
models/recentfilesmodel.h
|
||||
mpris/mpriscontroller.cpp
|
||||
mpris/mpriscontroller.h
|
||||
mpris/mprisitem.cpp
|
||||
mpris/mprisitem.h
|
||||
mpris/playeradaptor.cpp
|
||||
mpris/playeradaptor.h
|
||||
mpris/rootadaptor.cpp
|
||||
mpris/rootadaptor.h
|
||||
mpv/mpvitem.cpp
|
||||
mpv/mpvitem.h
|
||||
playlist/playlistmodel.cpp
|
||||
playlist/playlistmodel.h
|
||||
translate/apikeystore.cpp
|
||||
translate/apikeystore.h
|
||||
translate/llmclient.cpp
|
||||
@@ -51,7 +63,7 @@ ecm_target_qml_sources(qtpotato SOURCES
|
||||
qml/SettingsPage.qml
|
||||
)
|
||||
|
||||
target_include_directories(qtpotato PRIVATE mpv translate)
|
||||
target_include_directories(qtpotato PRIVATE models mpris mpv playlist translate)
|
||||
target_compile_definitions(qtpotato PRIVATE QTPOTATO_VERSION_STRING="${PROJECT_VERSION}")
|
||||
|
||||
target_link_libraries(qtpotato PRIVATE
|
||||
@@ -63,6 +75,7 @@ target_link_libraries(qtpotato PRIVATE
|
||||
Qt6::Widgets
|
||||
Qt6::Network
|
||||
Qt6::Concurrent
|
||||
Qt6::DBus
|
||||
KF6::I18n
|
||||
KF6::I18nQml
|
||||
KF6::CoreAddons
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
// SPDX-FileCopyrightText: 2026 Tri Vo Khac
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include "recentfilesmodel.h"
|
||||
|
||||
#include <KConfigGroup>
|
||||
#include <KSharedConfig>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
using namespace Qt::Literals::StringLiterals;
|
||||
|
||||
namespace
|
||||
{
|
||||
// Stored even when fewer are shown, so raising the limit brings old entries back.
|
||||
constexpr int StoredFiles = 50;
|
||||
|
||||
KConfigGroup configGroup()
|
||||
{
|
||||
return KSharedConfig::openStateConfig()->group(u"RecentFiles"_s);
|
||||
}
|
||||
}
|
||||
|
||||
RecentFilesModel::RecentFilesModel(QObject *parent)
|
||||
: QAbstractListModel(parent)
|
||||
{
|
||||
const QStringList urls = configGroup().readEntry(u"Urls"_s, QStringList());
|
||||
for (const QString &url : urls) {
|
||||
m_urls.append(QUrl(url));
|
||||
}
|
||||
}
|
||||
|
||||
int RecentFilesModel::rowCount(const QModelIndex &parent) const
|
||||
{
|
||||
return parent.isValid() ? 0 : count();
|
||||
}
|
||||
|
||||
QVariant RecentFilesModel::data(const QModelIndex &index, int role) const
|
||||
{
|
||||
if (!checkIndex(index, CheckIndexOption::IndexIsValid)) {
|
||||
return {};
|
||||
}
|
||||
const QUrl &url = m_urls.at(index.row());
|
||||
switch (role) {
|
||||
case UrlRole:
|
||||
return url;
|
||||
case Qt::DisplayRole:
|
||||
case NameRole:
|
||||
return url.isLocalFile() ? url.fileName() : url.toDisplayString();
|
||||
case PathRole:
|
||||
return url.isLocalFile() ? url.toLocalFile() : url.toDisplayString();
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
QHash<int, QByteArray> RecentFilesModel::roleNames() const
|
||||
{
|
||||
return {
|
||||
{UrlRole, "url"},
|
||||
{NameRole, "name"},
|
||||
{PathRole, "path"},
|
||||
};
|
||||
}
|
||||
|
||||
int RecentFilesModel::count() const
|
||||
{
|
||||
return int(std::min<qsizetype>(m_urls.size(), m_maxCount));
|
||||
}
|
||||
|
||||
int RecentFilesModel::maxCount() const
|
||||
{
|
||||
return m_maxCount;
|
||||
}
|
||||
|
||||
void RecentFilesModel::setMaxCount(int maxCount)
|
||||
{
|
||||
maxCount = std::clamp(maxCount, 0, StoredFiles);
|
||||
if (maxCount == m_maxCount) {
|
||||
return;
|
||||
}
|
||||
beginResetModel();
|
||||
m_maxCount = maxCount;
|
||||
endResetModel();
|
||||
Q_EMIT maxCountChanged();
|
||||
Q_EMIT countChanged();
|
||||
}
|
||||
|
||||
void RecentFilesModel::add(const QUrl &url)
|
||||
{
|
||||
if (!url.isValid()) {
|
||||
return;
|
||||
}
|
||||
beginResetModel();
|
||||
m_urls.removeAll(url);
|
||||
m_urls.prepend(url);
|
||||
if (m_urls.size() > StoredFiles) {
|
||||
m_urls.resize(StoredFiles);
|
||||
}
|
||||
endResetModel();
|
||||
Q_EMIT countChanged();
|
||||
save();
|
||||
}
|
||||
|
||||
void RecentFilesModel::clear()
|
||||
{
|
||||
beginResetModel();
|
||||
m_urls.clear();
|
||||
endResetModel();
|
||||
Q_EMIT countChanged();
|
||||
save();
|
||||
}
|
||||
|
||||
void RecentFilesModel::save() const
|
||||
{
|
||||
QStringList urls;
|
||||
for (const QUrl &url : m_urls) {
|
||||
urls.append(url.toString());
|
||||
}
|
||||
KConfigGroup group = configGroup();
|
||||
group.writeEntry(u"Urls"_s, urls);
|
||||
group.sync();
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
// SPDX-FileCopyrightText: 2026 Tri Vo Khac
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QAbstractListModel>
|
||||
#include <QList>
|
||||
#include <QUrl>
|
||||
#include <QtQml/qqmlregistration.h>
|
||||
|
||||
// Recently opened files, newest first, kept in the application's state file.
|
||||
class RecentFilesModel : public QAbstractListModel
|
||||
{
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
QML_SINGLETON
|
||||
|
||||
Q_PROPERTY(int count READ count NOTIFY countChanged)
|
||||
// Rows beyond this are hidden (but kept, in case it is raised again).
|
||||
Q_PROPERTY(int maxCount READ maxCount WRITE setMaxCount NOTIFY maxCountChanged)
|
||||
|
||||
public:
|
||||
enum Roles {
|
||||
UrlRole = Qt::UserRole + 1,
|
||||
NameRole,
|
||||
PathRole,
|
||||
};
|
||||
Q_ENUM(Roles)
|
||||
|
||||
explicit RecentFilesModel(QObject *parent = nullptr);
|
||||
|
||||
int rowCount(const QModelIndex &parent = {}) const override;
|
||||
QVariant data(const QModelIndex &index, int role) const override;
|
||||
QHash<int, QByteArray> roleNames() const override;
|
||||
|
||||
int count() const;
|
||||
int maxCount() const;
|
||||
void setMaxCount(int maxCount);
|
||||
|
||||
// Moves \a url to the top, adding it if needed.
|
||||
Q_INVOKABLE void add(const QUrl &url);
|
||||
Q_INVOKABLE void clear();
|
||||
|
||||
Q_SIGNALS:
|
||||
void countChanged();
|
||||
void maxCountChanged();
|
||||
|
||||
private:
|
||||
void save() const;
|
||||
|
||||
QList<QUrl> m_urls;
|
||||
int m_maxCount = 10;
|
||||
};
|
||||
@@ -0,0 +1,498 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 George Florea Bănuș <georgefb899@gmail.com>
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
#include "mpriscontroller.h"
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QDBusConnection>
|
||||
#include <QTimer>
|
||||
|
||||
#include "playeradaptor.h"
|
||||
#include "rootadaptor.h"
|
||||
|
||||
using namespace Qt::StringLiterals;
|
||||
|
||||
class MprisControllerPrivate
|
||||
{
|
||||
Q_DISABLE_COPY(MprisControllerPrivate);
|
||||
|
||||
public:
|
||||
MprisControllerPrivate(MprisController *q)
|
||||
: q_ptr(q)
|
||||
, rootAdaptor(*q)
|
||||
, playerAdaptor(*q)
|
||||
{
|
||||
}
|
||||
MprisController *const q_ptr;
|
||||
|
||||
QTimer notifyTimer;
|
||||
|
||||
// Interface MediaPlayer2
|
||||
RootAdaptor rootAdaptor;
|
||||
bool canQuit = false;
|
||||
bool fullscreen = false;
|
||||
bool canSetFullscreen = false;
|
||||
bool canRaise = false;
|
||||
bool hasTrackList = false;
|
||||
QString identity;
|
||||
QString desktopEntry;
|
||||
QStringList supportedUriSchemes;
|
||||
QStringList supportedMimeTypes;
|
||||
|
||||
// Interface MediaPlayer2.Player
|
||||
PlayerAdaptor playerAdaptor;
|
||||
QString playbackStatus;
|
||||
QString loopStatus;
|
||||
double rate = 1.0;
|
||||
bool shuffle = false;
|
||||
QVariantMap metadata;
|
||||
double volume = 1.0;
|
||||
qlonglong position = 0;
|
||||
double minimumRate = 0.0;
|
||||
double maximumRate = 1.0;
|
||||
bool canGoNext = false;
|
||||
bool canGoPrevious = false;
|
||||
bool canPlay = false;
|
||||
bool canPause = false;
|
||||
bool canSeek = false;
|
||||
bool canControl = false;
|
||||
};
|
||||
|
||||
MprisController::MprisController(QObject *parent)
|
||||
: QObject{parent}
|
||||
, d_ptr{std::make_unique<MprisControllerPrivate>(this)}
|
||||
{
|
||||
d_ptr->notifyTimer.setSingleShot(true);
|
||||
d_ptr->notifyTimer.setInterval(100);
|
||||
d_ptr->notifyTimer.callOnTimeout([this]() {
|
||||
d_ptr->playerAdaptor.notify(u"Metadata"_s, d_ptr->metadata);
|
||||
});
|
||||
|
||||
// register mpris dbus service; the instance suffix lets several players coexist
|
||||
QDBusConnection::sessionBus().registerService(u"org.mpris.MediaPlayer2.qtpotato.instance%1"_s.arg(QCoreApplication::applicationPid()));
|
||||
QDBusConnection::sessionBus().registerObject(u"/org/mpris/MediaPlayer2"_s, this, QDBusConnection::ExportAdaptors);
|
||||
}
|
||||
|
||||
MprisController::~MprisController() = default;
|
||||
|
||||
void MprisController::raise()
|
||||
{
|
||||
Q_EMIT raiseRequested();
|
||||
}
|
||||
|
||||
void MprisController::quit()
|
||||
{
|
||||
Q_EMIT quitRequested();
|
||||
}
|
||||
|
||||
void MprisController::next()
|
||||
{
|
||||
Q_EMIT nextRequested();
|
||||
}
|
||||
|
||||
void MprisController::previous()
|
||||
{
|
||||
Q_EMIT previousRequested();
|
||||
}
|
||||
|
||||
void MprisController::pause()
|
||||
{
|
||||
Q_EMIT pauseRequested();
|
||||
}
|
||||
|
||||
void MprisController::playpause()
|
||||
{
|
||||
Q_EMIT playpauseRequested();
|
||||
}
|
||||
|
||||
void MprisController::stop()
|
||||
{
|
||||
Q_EMIT stopRequested();
|
||||
}
|
||||
|
||||
void MprisController::play()
|
||||
{
|
||||
Q_EMIT playRequested();
|
||||
}
|
||||
|
||||
void MprisController::seek(qlonglong offset)
|
||||
{
|
||||
Q_EMIT seekRequested(offset);
|
||||
}
|
||||
|
||||
void MprisController::seekTo(qlonglong position)
|
||||
{
|
||||
Q_EMIT seekToRequested(position);
|
||||
}
|
||||
|
||||
void MprisController::openUri(QString uri)
|
||||
{
|
||||
Q_EMIT openUriRequested(uri);
|
||||
}
|
||||
|
||||
bool MprisController::canQuit() const
|
||||
{
|
||||
return d_ptr->canQuit;
|
||||
}
|
||||
|
||||
void MprisController::setCanQuit(bool newCanQuit)
|
||||
{
|
||||
if (d_ptr->canQuit == newCanQuit) {
|
||||
return;
|
||||
}
|
||||
d_ptr->canQuit = newCanQuit;
|
||||
d_ptr->rootAdaptor.notify(u"CanQuit"_s, d_ptr->canQuit);
|
||||
}
|
||||
|
||||
bool MprisController::fullscreen() const
|
||||
{
|
||||
return d_ptr->fullscreen;
|
||||
}
|
||||
|
||||
void MprisController::setFullscreen(bool newFullscreen)
|
||||
{
|
||||
if (d_ptr->fullscreen == newFullscreen) {
|
||||
return;
|
||||
}
|
||||
d_ptr->fullscreen = newFullscreen;
|
||||
d_ptr->rootAdaptor.notify(u"Fullscreen"_s, d_ptr->fullscreen);
|
||||
}
|
||||
|
||||
void MprisController::requestFullscreenChange(bool newFullscreen)
|
||||
{
|
||||
Q_EMIT fullscreenChangeRequested(newFullscreen);
|
||||
}
|
||||
|
||||
bool MprisController::canSetFullscreen() const
|
||||
{
|
||||
return d_ptr->canSetFullscreen;
|
||||
}
|
||||
|
||||
void MprisController::setCanSetFullscreen(bool newCanSetFullscreen)
|
||||
{
|
||||
if (d_ptr->canSetFullscreen == newCanSetFullscreen) {
|
||||
return;
|
||||
}
|
||||
d_ptr->canSetFullscreen = newCanSetFullscreen;
|
||||
d_ptr->rootAdaptor.notify(u"CanSetFullscreen"_s, d_ptr->canSetFullscreen);
|
||||
}
|
||||
|
||||
bool MprisController::canRaise() const
|
||||
{
|
||||
return d_ptr->canRaise;
|
||||
}
|
||||
|
||||
void MprisController::setCanRaise(bool newCanRaise)
|
||||
{
|
||||
if (d_ptr->canRaise == newCanRaise) {
|
||||
return;
|
||||
}
|
||||
d_ptr->canRaise = newCanRaise;
|
||||
d_ptr->rootAdaptor.notify(u"CanRaise"_s, d_ptr->canRaise);
|
||||
}
|
||||
|
||||
bool MprisController::hasTrackList() const
|
||||
{
|
||||
return d_ptr->hasTrackList;
|
||||
}
|
||||
|
||||
void MprisController::setHasTrackList(bool newHasTrackList)
|
||||
{
|
||||
if (d_ptr->hasTrackList == newHasTrackList) {
|
||||
return;
|
||||
}
|
||||
d_ptr->hasTrackList = newHasTrackList;
|
||||
d_ptr->rootAdaptor.notify(u"HasTrackList"_s, d_ptr->hasTrackList);
|
||||
}
|
||||
|
||||
QString MprisController::identity() const
|
||||
{
|
||||
return d_ptr->identity;
|
||||
}
|
||||
|
||||
void MprisController::setIdentity(const QString &newIdentity)
|
||||
{
|
||||
if (d_ptr->identity == newIdentity) {
|
||||
return;
|
||||
}
|
||||
d_ptr->identity = newIdentity;
|
||||
d_ptr->rootAdaptor.notify(u"Identity"_s, d_ptr->identity);
|
||||
}
|
||||
|
||||
QString MprisController::desktopEntry() const
|
||||
{
|
||||
return d_ptr->desktopEntry;
|
||||
}
|
||||
|
||||
void MprisController::setDesktopEntry(const QString &newDesktopEntry)
|
||||
{
|
||||
if (d_ptr->desktopEntry == newDesktopEntry) {
|
||||
return;
|
||||
}
|
||||
d_ptr->desktopEntry = newDesktopEntry;
|
||||
d_ptr->rootAdaptor.notify(u"DesktopEntry"_s, d_ptr->desktopEntry);
|
||||
}
|
||||
|
||||
QStringList MprisController::supportedUriSchemes() const
|
||||
{
|
||||
return d_ptr->supportedUriSchemes;
|
||||
}
|
||||
|
||||
void MprisController::setSupportedUriSchemes(const QStringList &newSupportedUriSchemes)
|
||||
{
|
||||
if (d_ptr->supportedUriSchemes == newSupportedUriSchemes) {
|
||||
return;
|
||||
}
|
||||
d_ptr->supportedUriSchemes = newSupportedUriSchemes;
|
||||
d_ptr->rootAdaptor.notify(u"SupportedUriSchemes"_s, d_ptr->supportedUriSchemes);
|
||||
}
|
||||
|
||||
QStringList MprisController::supportedMimeTypes() const
|
||||
{
|
||||
return d_ptr->supportedMimeTypes;
|
||||
}
|
||||
|
||||
void MprisController::setSupportedMimeTypes(const QStringList &newSupportedMimeTypes)
|
||||
{
|
||||
if (d_ptr->supportedMimeTypes == newSupportedMimeTypes) {
|
||||
return;
|
||||
}
|
||||
d_ptr->supportedMimeTypes = newSupportedMimeTypes;
|
||||
d_ptr->rootAdaptor.notify(u"SupportedMimeTypes"_s, d_ptr->supportedMimeTypes);
|
||||
}
|
||||
|
||||
QString MprisController::playbackStatus() const
|
||||
{
|
||||
return d_ptr->playbackStatus;
|
||||
}
|
||||
|
||||
void MprisController::setPlaybackStatus(const QString &newStatus)
|
||||
{
|
||||
if (d_ptr->playbackStatus == newStatus) {
|
||||
return;
|
||||
}
|
||||
d_ptr->playbackStatus = newStatus;
|
||||
d_ptr->playerAdaptor.notify(u"PlaybackStatus"_s, d_ptr->playbackStatus);
|
||||
}
|
||||
|
||||
QString MprisController::loopStatus() const
|
||||
{
|
||||
return d_ptr->loopStatus;
|
||||
}
|
||||
|
||||
void MprisController::requestLoopStatusChange(const QString &newLoopStatus)
|
||||
{
|
||||
Q_EMIT loopStatusChangeRequested(newLoopStatus);
|
||||
}
|
||||
|
||||
double MprisController::rate() const
|
||||
{
|
||||
return d_ptr->rate;
|
||||
}
|
||||
|
||||
void MprisController::setRate(const double newRate)
|
||||
{
|
||||
if (d_ptr->rate == newRate) {
|
||||
return;
|
||||
}
|
||||
d_ptr->rate = newRate;
|
||||
d_ptr->playerAdaptor.notify(u"Rate"_s, d_ptr->rate);
|
||||
}
|
||||
|
||||
void MprisController::requestRateChange(double newRate)
|
||||
{
|
||||
Q_EMIT rateChangeRequested(newRate);
|
||||
}
|
||||
|
||||
void MprisController::setLoopStatus(const QString &newLoopStatus)
|
||||
{
|
||||
if (d_ptr->loopStatus == newLoopStatus) {
|
||||
return;
|
||||
}
|
||||
d_ptr->loopStatus = newLoopStatus;
|
||||
d_ptr->playerAdaptor.notify(u"LoopStatus"_s, d_ptr->loopStatus);
|
||||
}
|
||||
|
||||
bool MprisController::shuffle() const
|
||||
{
|
||||
return d_ptr->shuffle;
|
||||
}
|
||||
|
||||
void MprisController::setShuffle(const bool newShuffle)
|
||||
{
|
||||
if (d_ptr->shuffle == newShuffle) {
|
||||
return;
|
||||
}
|
||||
d_ptr->shuffle = newShuffle;
|
||||
d_ptr->playerAdaptor.notify(u"Shuffle"_s, d_ptr->shuffle);
|
||||
}
|
||||
|
||||
void MprisController::requestShuffleChange(bool newShuffle)
|
||||
{
|
||||
Q_EMIT shuffleChangeRequested(newShuffle);
|
||||
}
|
||||
|
||||
QVariantMap MprisController::metadata() const
|
||||
{
|
||||
return d_ptr->metadata;
|
||||
}
|
||||
|
||||
void MprisController::setMetadata(const QString &field, const QVariant value)
|
||||
{
|
||||
d_ptr->metadata.insert(field, value);
|
||||
d_ptr->notifyTimer.start();
|
||||
}
|
||||
|
||||
double MprisController::volume() const
|
||||
{
|
||||
return d_ptr->volume;
|
||||
}
|
||||
|
||||
void MprisController::setVolume(const double newVolume)
|
||||
{
|
||||
if (d_ptr->volume == newVolume) {
|
||||
return;
|
||||
}
|
||||
d_ptr->volume = newVolume;
|
||||
d_ptr->playerAdaptor.notify(u"Volume"_s, d_ptr->volume);
|
||||
}
|
||||
|
||||
void MprisController::requestVolumeChange(const double &newVolume)
|
||||
{
|
||||
Q_EMIT volumeChangeRequested(newVolume);
|
||||
}
|
||||
|
||||
qlonglong MprisController::position() const
|
||||
{
|
||||
return d_ptr->position;
|
||||
}
|
||||
|
||||
void MprisController::setPosition(const qlonglong newPosition)
|
||||
{
|
||||
if (d_ptr->position == newPosition) {
|
||||
return;
|
||||
}
|
||||
d_ptr->position = newPosition;
|
||||
}
|
||||
|
||||
double MprisController::minimumRate() const
|
||||
{
|
||||
return d_ptr->minimumRate;
|
||||
}
|
||||
|
||||
void MprisController::setMinimumRate(const double newMinRate)
|
||||
{
|
||||
if (d_ptr->minimumRate == newMinRate) {
|
||||
return;
|
||||
}
|
||||
d_ptr->minimumRate = newMinRate;
|
||||
d_ptr->playerAdaptor.notify(u"MinimumRate"_s, d_ptr->minimumRate);
|
||||
}
|
||||
|
||||
double MprisController::maximumRate() const
|
||||
{
|
||||
return d_ptr->maximumRate;
|
||||
}
|
||||
|
||||
void MprisController::setMaximumRate(const double newMaxRate)
|
||||
{
|
||||
if (d_ptr->maximumRate == newMaxRate) {
|
||||
return;
|
||||
}
|
||||
d_ptr->maximumRate = newMaxRate;
|
||||
d_ptr->playerAdaptor.notify(u"MaximumRate"_s, d_ptr->maximumRate);
|
||||
}
|
||||
|
||||
bool MprisController::canGoNext() const
|
||||
{
|
||||
return d_ptr->canGoNext;
|
||||
}
|
||||
|
||||
void MprisController::setCanGoNext(const bool newCanGoNext)
|
||||
{
|
||||
if (d_ptr->canGoNext == newCanGoNext) {
|
||||
return;
|
||||
}
|
||||
d_ptr->canGoNext = newCanGoNext;
|
||||
d_ptr->playerAdaptor.notify(u"CanGoNext"_s, d_ptr->canGoNext);
|
||||
}
|
||||
|
||||
bool MprisController::canGoPrevious() const
|
||||
{
|
||||
return d_ptr->canGoPrevious;
|
||||
}
|
||||
|
||||
void MprisController::setCanGoPrevious(const bool newCanGoPrevious)
|
||||
{
|
||||
if (d_ptr->canGoPrevious == newCanGoPrevious) {
|
||||
return;
|
||||
}
|
||||
d_ptr->canGoPrevious = newCanGoPrevious;
|
||||
d_ptr->playerAdaptor.notify(u"CanGoPrevious"_s, d_ptr->canGoPrevious);
|
||||
}
|
||||
|
||||
bool MprisController::canPlay() const
|
||||
{
|
||||
return d_ptr->canPlay;
|
||||
}
|
||||
|
||||
void MprisController::setCanPlay(const bool newCanPlay)
|
||||
{
|
||||
if (d_ptr->canPlay == newCanPlay) {
|
||||
return;
|
||||
}
|
||||
d_ptr->canPlay = newCanPlay;
|
||||
d_ptr->playerAdaptor.notify(u"CanPlay"_s, d_ptr->canPlay);
|
||||
}
|
||||
|
||||
bool MprisController::canPause() const
|
||||
{
|
||||
return d_ptr->canPause;
|
||||
}
|
||||
|
||||
void MprisController::setCanPause(const bool newCanPause)
|
||||
{
|
||||
if (d_ptr->canPause == newCanPause) {
|
||||
return;
|
||||
}
|
||||
d_ptr->canPause = newCanPause;
|
||||
d_ptr->playerAdaptor.notify(u"CanPause"_s, d_ptr->canPause);
|
||||
}
|
||||
|
||||
bool MprisController::canSeek() const
|
||||
{
|
||||
return d_ptr->canSeek;
|
||||
}
|
||||
|
||||
void MprisController::setCanSeek(const bool newCanSeek)
|
||||
{
|
||||
if (d_ptr->canSeek == newCanSeek) {
|
||||
return;
|
||||
}
|
||||
d_ptr->canSeek = newCanSeek;
|
||||
d_ptr->playerAdaptor.notify(u"CanSeek"_s, d_ptr->canSeek);
|
||||
}
|
||||
|
||||
bool MprisController::canControl() const
|
||||
{
|
||||
return d_ptr->canControl;
|
||||
}
|
||||
|
||||
void MprisController::setCanControl(const bool newCanControl)
|
||||
{
|
||||
if (d_ptr->canControl == newCanControl) {
|
||||
return;
|
||||
}
|
||||
d_ptr->canControl = newCanControl;
|
||||
d_ptr->playerAdaptor.notify(u"CanControl"_s, d_ptr->canControl);
|
||||
}
|
||||
|
||||
void MprisController::seeked(qlonglong position)
|
||||
{
|
||||
d_ptr->playerAdaptor.seeked(position);
|
||||
}
|
||||
|
||||
#include "moc_mpriscontroller.cpp"
|
||||
@@ -0,0 +1,214 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 George Florea Bănuș <georgefb899@gmail.com>
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
#ifndef MPRISCONTROLLER_H
|
||||
#define MPRISCONTROLLER_H
|
||||
|
||||
#include <QObject>
|
||||
|
||||
class MprisControllerPrivate;
|
||||
|
||||
/*!
|
||||
* \class MprisController
|
||||
* \inmodule MPRIS2
|
||||
* \brief Controls communication between application and MPRIS clients.
|
||||
*
|
||||
* MprisController receives requests from the MPRIS D-Bus adaptors and
|
||||
* synchronizes state between the application and connected MPRIS clients.
|
||||
*/
|
||||
class MprisController : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit MprisController(QObject *parent = nullptr);
|
||||
~MprisController();
|
||||
|
||||
bool canQuit() const;
|
||||
void setCanQuit(bool newCanQuit);
|
||||
|
||||
bool fullscreen() const;
|
||||
void setFullscreen(bool newFullscreen);
|
||||
void requestFullscreenChange(bool newFullscreen);
|
||||
|
||||
bool canSetFullscreen() const;
|
||||
void setCanSetFullscreen(bool newCanSetFullscreen);
|
||||
|
||||
bool canRaise() const;
|
||||
void setCanRaise(bool newCanRaise);
|
||||
|
||||
bool hasTrackList() const;
|
||||
void setHasTrackList(bool newHasTrackList);
|
||||
|
||||
QString identity() const;
|
||||
void setIdentity(const QString &newIdentity);
|
||||
|
||||
QString desktopEntry() const;
|
||||
void setDesktopEntry(const QString &newDesktopEntry);
|
||||
|
||||
QStringList supportedUriSchemes() const;
|
||||
void setSupportedUriSchemes(const QStringList &newSupportedUriSchemes);
|
||||
|
||||
QStringList supportedMimeTypes() const;
|
||||
void setSupportedMimeTypes(const QStringList &newSupportedMimeTypes);
|
||||
|
||||
void raise();
|
||||
void quit();
|
||||
|
||||
void next();
|
||||
void previous();
|
||||
void pause();
|
||||
void playpause();
|
||||
void stop();
|
||||
void play();
|
||||
/*!
|
||||
* \a offset in microseconds to seek by
|
||||
*/
|
||||
void seek(qlonglong offset);
|
||||
/*!
|
||||
* \a position to seek to in microseconds
|
||||
*/
|
||||
void seekTo(qlonglong position);
|
||||
void openUri(QString uri);
|
||||
|
||||
QString playbackStatus() const;
|
||||
void setPlaybackStatus(const QString &newStatus);
|
||||
|
||||
QString loopStatus() const;
|
||||
/*!
|
||||
* Set the internal loop status property to \c newLoopStatus and notify MPRIS clients.
|
||||
*
|
||||
* Should be called when loop status changes in the application.
|
||||
*/
|
||||
void setLoopStatus(const QString &newLoopStatus);
|
||||
/*!
|
||||
* Request the application to change the loop status property to \c newLoopStatus
|
||||
*/
|
||||
void requestLoopStatusChange(const QString &newLoopStatus);
|
||||
|
||||
double rate() const;
|
||||
/*!
|
||||
* Set the internal rate property to \c newRate and notify MPRIS clients.
|
||||
*
|
||||
* Should be called when rate changes in the application.
|
||||
*/
|
||||
void setRate(const double newRate);
|
||||
/*!
|
||||
* Request the application to change the rate property to \c newLoopStatus
|
||||
*/
|
||||
void requestRateChange(double newRate);
|
||||
|
||||
bool shuffle() const;
|
||||
/*!
|
||||
* Set the internal shuffle property to \c newShuffle and notify MPRIS clients.
|
||||
*
|
||||
* Should be called when shuffle changes in the application.
|
||||
*/
|
||||
void setShuffle(const bool newShuffle);
|
||||
/*!
|
||||
* Request the application to change the shuffle property to \c newShuffle
|
||||
*/
|
||||
void requestShuffleChange(bool newShuffle);
|
||||
|
||||
QVariantMap metadata() const;
|
||||
void setMetadata(const QString &field, const QVariant value);
|
||||
|
||||
double volume() const;
|
||||
/*!
|
||||
* Set the internal volume property to \c newVolume and notify MPRIS clients.
|
||||
*
|
||||
* Should be called when volume changes in the application.
|
||||
*/
|
||||
void setVolume(const double newVolume);
|
||||
/*!
|
||||
* Request the application to change the volume property to \c newVolume
|
||||
*/
|
||||
void requestVolumeChange(const double &newVolume);
|
||||
|
||||
/*!
|
||||
* Returns \c position in microseconds
|
||||
*/
|
||||
qlonglong position() const;
|
||||
/*!
|
||||
* Set position to /c newPosition in microseconds
|
||||
*/
|
||||
void setPosition(const qlonglong newPosition);
|
||||
|
||||
double minimumRate() const;
|
||||
void setMinimumRate(const double newMinRate);
|
||||
|
||||
double maximumRate() const;
|
||||
void setMaximumRate(const double newMaxRate);
|
||||
|
||||
bool canGoNext() const;
|
||||
void setCanGoNext(const bool newCanGoNext);
|
||||
bool canGoPrevious() const;
|
||||
void setCanGoPrevious(const bool newCanGoPrevious);
|
||||
bool canPlay() const;
|
||||
void setCanPlay(const bool newCanPlay);
|
||||
bool canPause() const;
|
||||
void setCanPause(const bool newCanPause);
|
||||
bool canSeek() const;
|
||||
void setCanSeek(const bool newCanSeek);
|
||||
bool canControl() const;
|
||||
void setCanControl(const bool newCanControl);
|
||||
|
||||
/*!
|
||||
* Indicates that position has changed on the application side.
|
||||
* Causes the Seeked signal of the M ediaPlayer2.Player interface to be emitted
|
||||
*/
|
||||
void seeked(qlonglong position);
|
||||
|
||||
Q_SIGNALS:
|
||||
void raiseRequested();
|
||||
void quitRequested();
|
||||
void nextRequested();
|
||||
void previousRequested();
|
||||
void pauseRequested();
|
||||
void playpauseRequested();
|
||||
void stopRequested();
|
||||
void playRequested();
|
||||
|
||||
/*!
|
||||
* \a offset in microseconds to seek by
|
||||
*/
|
||||
void seekRequested(qlonglong offset);
|
||||
/*!
|
||||
* \a position to seek to in microseconds
|
||||
*/
|
||||
void seekToRequested(qlonglong position);
|
||||
void openUriRequested(QString uri);
|
||||
|
||||
/*!
|
||||
* Emitted when fullscreen is changed by MPRIS client
|
||||
*/
|
||||
void fullscreenChangeRequested(bool fullscreen);
|
||||
|
||||
/*!
|
||||
* Emitted when volume is changed by MPRIS client
|
||||
*/
|
||||
void volumeChangeRequested(double volume);
|
||||
|
||||
/*!
|
||||
* Emitted when loop status is changed by MPRIS client
|
||||
*/
|
||||
void loopStatusChangeRequested(const QString &loopStatus);
|
||||
|
||||
/*!
|
||||
* Emitted when shuffle is changed by MPRIS client
|
||||
*/
|
||||
void shuffleChangeRequested(bool shuffle);
|
||||
|
||||
/*!
|
||||
* Emitted when rate is changed by MPRIS client
|
||||
*/
|
||||
void rateChangeRequested(double rate);
|
||||
|
||||
private:
|
||||
std::unique_ptr<MprisControllerPrivate> d_ptr;
|
||||
};
|
||||
|
||||
#endif // MPRISCONTROLLER_H
|
||||
@@ -0,0 +1,171 @@
|
||||
// SPDX-FileCopyrightText: 2026 George Florea Bănuș <georgefb899@gmail.com>
|
||||
// SPDX-FileCopyrightText: 2026 Tri Vo Khac
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include "mprisitem.h"
|
||||
|
||||
#include "mpriscontroller.h"
|
||||
#include "mpvitem.h"
|
||||
#include "playlistmodel.h"
|
||||
|
||||
#include <QGuiApplication>
|
||||
#include <QWindow>
|
||||
|
||||
#include <cmath>
|
||||
|
||||
using namespace Qt::Literals::StringLiterals;
|
||||
|
||||
MprisItem::MprisItem(QObject *parent)
|
||||
: QObject(parent)
|
||||
, m_controller(std::make_unique<MprisController>())
|
||||
{
|
||||
m_controller->setCanQuit(true);
|
||||
m_controller->setCanRaise(true);
|
||||
m_controller->setHasTrackList(false);
|
||||
m_controller->setIdentity(QGuiApplication::applicationDisplayName());
|
||||
m_controller->setDesktopEntry(QGuiApplication::desktopFileName());
|
||||
m_controller->setSupportedUriSchemes({u"file"_s, u"http"_s, u"https"_s});
|
||||
m_controller->setSupportedMimeTypes({u"video/*"_s, u"audio/*"_s});
|
||||
m_controller->setMinimumRate(0.1);
|
||||
m_controller->setMaximumRate(10.0);
|
||||
m_controller->setCanControl(true);
|
||||
m_controller->setPlaybackStatus(u"Stopped"_s);
|
||||
|
||||
connect(m_controller.get(), &MprisController::raiseRequested, this, &MprisItem::raiseRequested);
|
||||
connect(m_controller.get(), &MprisController::openUriRequested, this, &MprisItem::openUriRequested);
|
||||
connect(m_controller.get(), &MprisController::quitRequested, QGuiApplication::instance(), &QGuiApplication::quit);
|
||||
connect(m_controller.get(), &MprisController::nextRequested, this, &MprisItem::nextRequested);
|
||||
connect(m_controller.get(), &MprisController::previousRequested, this, &MprisItem::previousRequested);
|
||||
}
|
||||
|
||||
MprisItem::~MprisItem() = default;
|
||||
|
||||
MpvItem *MprisItem::mpvItem() const
|
||||
{
|
||||
return m_mpv;
|
||||
}
|
||||
|
||||
void MprisItem::setMpvItem(MpvItem *mpv)
|
||||
{
|
||||
if (m_mpv || !mpv) {
|
||||
return;
|
||||
}
|
||||
m_mpv = mpv;
|
||||
Q_EMIT mpvItemChanged();
|
||||
|
||||
m_controller->setVolume(mpv->volume() / 100.0);
|
||||
|
||||
connect(m_controller.get(), &MprisController::volumeChangeRequested, mpv, [mpv](double volume) {
|
||||
mpv->setVolume(int(std::round(volume * 100.0)));
|
||||
});
|
||||
connect(m_controller.get(), &MprisController::seekToRequested, mpv, [mpv](qlonglong position) {
|
||||
mpv->seek(position / 1'000'000.0);
|
||||
});
|
||||
connect(m_controller.get(), &MprisController::seekRequested, mpv, [mpv](qlonglong offset) {
|
||||
mpv->seekBy(offset / 1'000'000.0);
|
||||
});
|
||||
connect(m_controller.get(), &MprisController::stopRequested, mpv, &MpvItem::stop);
|
||||
connect(m_controller.get(), &MprisController::playpauseRequested, mpv, &MpvItem::togglePause);
|
||||
connect(m_controller.get(), &MprisController::playRequested, mpv, [mpv] {
|
||||
mpv->setPaused(false);
|
||||
});
|
||||
connect(m_controller.get(), &MprisController::pauseRequested, mpv, [mpv] {
|
||||
mpv->setPaused(true);
|
||||
});
|
||||
connect(m_controller.get(), &MprisController::rateChangeRequested, mpv, &MpvItem::setSpeed);
|
||||
|
||||
connect(mpv, &MpvItem::volumeChanged, this, [this] {
|
||||
m_controller->setVolume(m_mpv->volume() / 100.0);
|
||||
});
|
||||
connect(mpv, &MpvItem::positionChanged, this, [this] {
|
||||
m_controller->setPosition(qlonglong(m_mpv->position() * 1'000'000.0));
|
||||
});
|
||||
connect(mpv, &MpvItem::pausedChanged, this, &MprisItem::updatePlaybackStatus);
|
||||
connect(mpv, &MpvItem::fileLoadedChanged, this, &MprisItem::updatePlaybackStatus);
|
||||
connect(mpv, &MpvItem::speedChanged, this, [this] {
|
||||
m_controller->setRate(m_mpv->speed());
|
||||
});
|
||||
connect(mpv, &MpvItem::mediaTitleChanged, this, [this] {
|
||||
if (!m_mpv->mediaTitle().isEmpty()) {
|
||||
m_controller->setMetadata(u"xesam:title"_s, m_mpv->mediaTitle());
|
||||
}
|
||||
});
|
||||
connect(mpv, &MpvItem::durationChanged, this, [this] {
|
||||
if (m_mpv->duration() > 0) {
|
||||
m_controller->setMetadata(u"mpris:length"_s, qlonglong(m_mpv->duration() * 1'000'000.0));
|
||||
}
|
||||
});
|
||||
connect(mpv, &MpvItem::currentUrlChanged, this, [this] {
|
||||
const bool hasFile = !m_mpv->currentUrl().isEmpty();
|
||||
m_controller->setCanPlay(hasFile);
|
||||
m_controller->setCanPause(hasFile);
|
||||
m_controller->setCanSeek(hasFile);
|
||||
const int index = m_playlist ? m_playlist->playingIndex() : 0;
|
||||
m_controller->setMetadata(u"mpris:trackid"_s, u"/dev/trivk/qtpotato/track/%1"_s.arg(std::max(0, index)));
|
||||
m_controller->setMetadata(u"xesam:url"_s, m_mpv->currentUrl().toString());
|
||||
// No artwork for videos, so the task manager shows the live window preview.
|
||||
m_controller->setMetadata(u"mpris:artUrl"_s, QString());
|
||||
});
|
||||
}
|
||||
|
||||
PlaylistModel *MprisItem::playlist() const
|
||||
{
|
||||
return m_playlist;
|
||||
}
|
||||
|
||||
void MprisItem::setPlaylist(PlaylistModel *playlist)
|
||||
{
|
||||
if (m_playlist || !playlist) {
|
||||
return;
|
||||
}
|
||||
m_playlist = playlist;
|
||||
Q_EMIT playlistChanged();
|
||||
connect(playlist, &PlaylistModel::countChanged, this, &MprisItem::updateNavigation);
|
||||
connect(playlist, &PlaylistModel::playingIndexChanged, this, &MprisItem::updateNavigation);
|
||||
updateNavigation();
|
||||
}
|
||||
|
||||
QWindow *MprisItem::window() const
|
||||
{
|
||||
return m_window;
|
||||
}
|
||||
|
||||
void MprisItem::setWindow(QWindow *window)
|
||||
{
|
||||
if (m_window || !window) {
|
||||
return;
|
||||
}
|
||||
m_window = window;
|
||||
Q_EMIT windowChanged();
|
||||
|
||||
auto updateFullscreen = [this] {
|
||||
const bool fullscreen = m_window->windowStates().testFlag(Qt::WindowFullScreen);
|
||||
m_controller->setFullscreen(fullscreen);
|
||||
m_controller->setCanSetFullscreen(true);
|
||||
};
|
||||
updateFullscreen();
|
||||
connect(window, &QWindow::windowStateChanged, this, updateFullscreen);
|
||||
connect(m_controller.get(), &MprisController::fullscreenChangeRequested, this, [this](bool fullscreen) {
|
||||
if (fullscreen) {
|
||||
m_window->showFullScreen();
|
||||
} else {
|
||||
m_window->showNormal();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void MprisItem::updatePlaybackStatus()
|
||||
{
|
||||
QString status = u"Stopped"_s;
|
||||
if (m_mpv->isFileLoaded()) {
|
||||
status = m_mpv->paused() ? u"Paused"_s : u"Playing"_s;
|
||||
}
|
||||
m_controller->setPlaybackStatus(status);
|
||||
}
|
||||
|
||||
void MprisItem::updateNavigation()
|
||||
{
|
||||
const int count = m_playlist->count();
|
||||
m_controller->setCanGoNext(count > 1);
|
||||
m_controller->setCanGoPrevious(count > 1);
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
// SPDX-FileCopyrightText: 2026 George Florea Bănuș <georgefb899@gmail.com>
|
||||
// SPDX-FileCopyrightText: 2026 Tri Vo Khac
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "mpvitem.h"
|
||||
#include "playlistmodel.h"
|
||||
|
||||
#include <QObject>
|
||||
#include <QPointer>
|
||||
#include <QWindow>
|
||||
#include <QtQml/qqmlregistration.h>
|
||||
|
||||
#include <memory>
|
||||
|
||||
class MprisController;
|
||||
|
||||
// Publishes the player on D-Bus (MPRIS), so Plasma's media controls, the lock screen
|
||||
// and media keys can drive it. Adapted from Haruna's MprisItem.
|
||||
class MprisItem : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
|
||||
Q_PROPERTY(MpvItem *mpvItem READ mpvItem WRITE setMpvItem NOTIFY mpvItemChanged REQUIRED)
|
||||
Q_PROPERTY(PlaylistModel *playlist READ playlist WRITE setPlaylist NOTIFY playlistChanged REQUIRED)
|
||||
Q_PROPERTY(QWindow *window READ window WRITE setWindow NOTIFY windowChanged REQUIRED)
|
||||
|
||||
public:
|
||||
explicit MprisItem(QObject *parent = nullptr);
|
||||
~MprisItem() override;
|
||||
|
||||
MpvItem *mpvItem() const;
|
||||
void setMpvItem(MpvItem *mpv);
|
||||
PlaylistModel *playlist() const;
|
||||
void setPlaylist(PlaylistModel *playlist);
|
||||
QWindow *window() const;
|
||||
void setWindow(QWindow *window);
|
||||
|
||||
Q_SIGNALS:
|
||||
void mpvItemChanged();
|
||||
void playlistChanged();
|
||||
void windowChanged();
|
||||
void raiseRequested();
|
||||
void openUriRequested(const QString &uri);
|
||||
// Play the next/previous entry, the way the playlist actions would.
|
||||
void nextRequested();
|
||||
void previousRequested();
|
||||
|
||||
private:
|
||||
void updatePlaybackStatus();
|
||||
void updateNavigation();
|
||||
|
||||
std::unique_ptr<MprisController> m_controller;
|
||||
QPointer<MpvItem> m_mpv;
|
||||
QPointer<PlaylistModel> m_playlist;
|
||||
QPointer<QWindow> m_window;
|
||||
};
|
||||
@@ -0,0 +1,182 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2021 George Florea Bănuș <georgefb899@gmail.com>
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
#include "playeradaptor.h"
|
||||
|
||||
#include <QDBusConnection>
|
||||
#include <QDBusMessage>
|
||||
#include <QDBusObjectPath>
|
||||
|
||||
#include "mpriscontroller.h"
|
||||
|
||||
using namespace Qt::StringLiterals;
|
||||
|
||||
PlayerAdaptor::PlayerAdaptor(MprisController &parent)
|
||||
: QDBusAbstractAdaptor(&parent)
|
||||
, m_mprisController(parent)
|
||||
{
|
||||
}
|
||||
|
||||
void PlayerAdaptor::notify(const QString &property, const QVariant &value)
|
||||
{
|
||||
const auto path = u"/org/mpris/MediaPlayer2"_s;
|
||||
const auto propertiesInterface = u"org.freedesktop.DBus.Properties"_s;
|
||||
const auto playerInterface = u"org.mpris.MediaPlayer2.Player"_s;
|
||||
const auto properties = QVariantMap{{property, value}};
|
||||
|
||||
QDBusMessage msg = QDBusMessage::createSignal(path, propertiesInterface, u"PropertiesChanged"_s);
|
||||
msg << playerInterface << properties << QStringList{};
|
||||
|
||||
QDBusConnection::sessionBus().send(msg);
|
||||
}
|
||||
|
||||
void PlayerAdaptor::seeked(qlonglong position)
|
||||
{
|
||||
Q_EMIT Seeked(position);
|
||||
}
|
||||
|
||||
QString PlayerAdaptor::playbackStatus()
|
||||
{
|
||||
return m_mprisController.playbackStatus();
|
||||
}
|
||||
|
||||
QString PlayerAdaptor::loopStatus()
|
||||
{
|
||||
return m_mprisController.loopStatus();
|
||||
}
|
||||
|
||||
void PlayerAdaptor::setLoopStatus(const QString &newLoopStatus)
|
||||
{
|
||||
m_mprisController.requestLoopStatusChange(newLoopStatus);
|
||||
}
|
||||
|
||||
double PlayerAdaptor::rate() const
|
||||
{
|
||||
return m_mprisController.rate();
|
||||
}
|
||||
|
||||
void PlayerAdaptor::setRate(double newRate)
|
||||
{
|
||||
m_mprisController.requestRateChange(newRate);
|
||||
}
|
||||
|
||||
bool PlayerAdaptor::shuffle()
|
||||
{
|
||||
return m_mprisController.shuffle();
|
||||
}
|
||||
|
||||
void PlayerAdaptor::setShuffle(bool newShuffle)
|
||||
{
|
||||
m_mprisController.requestShuffleChange(newShuffle);
|
||||
}
|
||||
|
||||
QVariantMap PlayerAdaptor::metadata()
|
||||
{
|
||||
return m_mprisController.metadata();
|
||||
}
|
||||
|
||||
double PlayerAdaptor::volume()
|
||||
{
|
||||
return m_mprisController.volume();
|
||||
}
|
||||
|
||||
void PlayerAdaptor::setVolume(double newVolume)
|
||||
{
|
||||
m_mprisController.requestVolumeChange(newVolume);
|
||||
}
|
||||
|
||||
qlonglong PlayerAdaptor::position()
|
||||
{
|
||||
return m_mprisController.position();
|
||||
}
|
||||
|
||||
double PlayerAdaptor::minimumRate() const
|
||||
{
|
||||
return m_mprisController.minimumRate();
|
||||
}
|
||||
|
||||
double PlayerAdaptor::maximumRate() const
|
||||
{
|
||||
return m_mprisController.maximumRate();
|
||||
}
|
||||
|
||||
bool PlayerAdaptor::canGoNext()
|
||||
{
|
||||
return m_mprisController.canGoNext();
|
||||
}
|
||||
|
||||
bool PlayerAdaptor::canGoPrevious()
|
||||
{
|
||||
return m_mprisController.canGoPrevious();
|
||||
}
|
||||
|
||||
bool PlayerAdaptor::canPlay()
|
||||
{
|
||||
return m_mprisController.canPlay();
|
||||
}
|
||||
|
||||
bool PlayerAdaptor::canPause()
|
||||
{
|
||||
return m_mprisController.canPause();
|
||||
}
|
||||
|
||||
bool PlayerAdaptor::canSeek()
|
||||
{
|
||||
return m_mprisController.canSeek();
|
||||
}
|
||||
|
||||
bool PlayerAdaptor::canControl()
|
||||
{
|
||||
return m_mprisController.canControl();
|
||||
}
|
||||
|
||||
void PlayerAdaptor::Next()
|
||||
{
|
||||
m_mprisController.next();
|
||||
}
|
||||
|
||||
void PlayerAdaptor::Previous()
|
||||
{
|
||||
m_mprisController.previous();
|
||||
}
|
||||
|
||||
void PlayerAdaptor::Pause()
|
||||
{
|
||||
m_mprisController.pause();
|
||||
}
|
||||
|
||||
void PlayerAdaptor::PlayPause()
|
||||
{
|
||||
m_mprisController.playpause();
|
||||
}
|
||||
|
||||
void PlayerAdaptor::Stop()
|
||||
{
|
||||
m_mprisController.stop();
|
||||
}
|
||||
|
||||
void PlayerAdaptor::Play()
|
||||
{
|
||||
m_mprisController.play();
|
||||
}
|
||||
|
||||
void PlayerAdaptor::Seek(qlonglong offset)
|
||||
{
|
||||
m_mprisController.seek(offset);
|
||||
}
|
||||
|
||||
void PlayerAdaptor::SetPosition(const QDBusObjectPath &trackId, qlonglong position)
|
||||
{
|
||||
Q_UNUSED(trackId)
|
||||
m_mprisController.seekTo(position);
|
||||
}
|
||||
|
||||
void PlayerAdaptor::OpenUri(const QString &uri)
|
||||
{
|
||||
m_mprisController.openUri(uri);
|
||||
}
|
||||
|
||||
#include "moc_playeradaptor.cpp"
|
||||
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2021 George Florea Bănuș <georgefb899@gmail.com>
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
#ifndef MEDIAPLAYER2PLAYER_H
|
||||
#define MEDIAPLAYER2PLAYER_H
|
||||
|
||||
#include <QDBusAbstractAdaptor>
|
||||
|
||||
class MprisController;
|
||||
class MpvItem;
|
||||
class QDBusObjectPath;
|
||||
class QImage;
|
||||
|
||||
// https://specifications.freedesktop.org/mpris/latest/Player_Interface.html
|
||||
class PlayerAdaptor : public QDBusAbstractAdaptor
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_CLASSINFO("D-Bus Interface", "org.mpris.MediaPlayer2.Player")
|
||||
|
||||
public:
|
||||
explicit PlayerAdaptor(MprisController &parent);
|
||||
~PlayerAdaptor() = default;
|
||||
|
||||
Q_PROPERTY(QString PlaybackStatus READ playbackStatus CONSTANT)
|
||||
QString playbackStatus();
|
||||
|
||||
Q_PROPERTY(QString LoopStatus READ loopStatus WRITE setLoopStatus)
|
||||
QString loopStatus();
|
||||
void setLoopStatus(const QString &newLoopStatus);
|
||||
|
||||
Q_PROPERTY(double Rate READ rate WRITE setRate)
|
||||
double rate() const;
|
||||
void setRate(double newRate);
|
||||
|
||||
Q_PROPERTY(bool Shuffle READ shuffle WRITE setShuffle)
|
||||
bool shuffle();
|
||||
void setShuffle(bool newShuffle);
|
||||
|
||||
Q_PROPERTY(QVariantMap Metadata READ metadata CONSTANT)
|
||||
QVariantMap metadata();
|
||||
|
||||
Q_PROPERTY(double Volume READ volume WRITE setVolume)
|
||||
double volume();
|
||||
void setVolume(double newVolume);
|
||||
|
||||
Q_PROPERTY(qlonglong Position READ position CONSTANT)
|
||||
qlonglong position();
|
||||
|
||||
Q_PROPERTY(double MinimumRate READ minimumRate CONSTANT)
|
||||
double minimumRate() const;
|
||||
|
||||
Q_PROPERTY(double MaximumRate READ maximumRate CONSTANT)
|
||||
double maximumRate() const;
|
||||
|
||||
Q_PROPERTY(bool CanGoNext READ canGoNext CONSTANT)
|
||||
bool canGoNext();
|
||||
|
||||
Q_PROPERTY(bool CanGoPrevious READ canGoPrevious CONSTANT)
|
||||
bool canGoPrevious();
|
||||
|
||||
Q_PROPERTY(bool CanPlay READ canPlay CONSTANT)
|
||||
bool canPlay();
|
||||
|
||||
Q_PROPERTY(bool CanPause READ canPause CONSTANT)
|
||||
bool canPause();
|
||||
|
||||
Q_PROPERTY(bool CanSeek READ canSeek CONSTANT)
|
||||
bool canSeek();
|
||||
|
||||
Q_PROPERTY(bool CanControl READ canControl CONSTANT)
|
||||
bool canControl();
|
||||
|
||||
void notify(const QString &property, const QVariant &value);
|
||||
void seeked(qlonglong position);
|
||||
|
||||
public Q_SLOTS:
|
||||
void Next();
|
||||
void Previous();
|
||||
void Pause();
|
||||
void PlayPause();
|
||||
void Stop();
|
||||
void Play();
|
||||
void Seek(qlonglong offset);
|
||||
void SetPosition(const QDBusObjectPath &trackId, qlonglong position);
|
||||
void OpenUri(const QString &uri);
|
||||
|
||||
Q_SIGNALS:
|
||||
void Seeked(qlonglong position);
|
||||
|
||||
private:
|
||||
MprisController &m_mprisController;
|
||||
};
|
||||
|
||||
#endif // MEDIAPLAYER2PLAYER_H
|
||||
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2021 George Florea Bănuș <georgefb899@gmail.com>
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
#include "rootadaptor.h"
|
||||
|
||||
#include <QDBusConnection>
|
||||
#include <QDBusMessage>
|
||||
|
||||
#include "mpriscontroller.h"
|
||||
|
||||
using namespace Qt::StringLiterals;
|
||||
|
||||
RootAdaptor::RootAdaptor(MprisController &parent)
|
||||
: QDBusAbstractAdaptor(&parent)
|
||||
, m_mprisController(parent)
|
||||
{
|
||||
}
|
||||
|
||||
void RootAdaptor::notify(const QString &property, const QVariant &value)
|
||||
{
|
||||
const auto path = u"/org/mpris/MediaPlayer2"_s;
|
||||
const auto propertiesInterface = u"org.freedesktop.DBus.Properties"_s;
|
||||
const auto rootInterface = u"org.mpris.MediaPlayer2"_s;
|
||||
const auto properties = QVariantMap{{property, value}};
|
||||
|
||||
QDBusMessage msg = QDBusMessage::createSignal(path, propertiesInterface, u"PropertiesChanged"_s);
|
||||
msg << rootInterface << properties << QStringList{};
|
||||
|
||||
QDBusConnection::sessionBus().send(msg);
|
||||
}
|
||||
|
||||
bool RootAdaptor::canQuit() const
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RootAdaptor::fullscreen() const
|
||||
{
|
||||
return m_mprisController.fullscreen();
|
||||
}
|
||||
|
||||
void RootAdaptor::setFullscreen(bool newFullscreen)
|
||||
{
|
||||
m_mprisController.requestFullscreenChange(newFullscreen);
|
||||
}
|
||||
|
||||
bool RootAdaptor::canSetFullscreen() const
|
||||
{
|
||||
return m_mprisController.canSetFullscreen();
|
||||
}
|
||||
|
||||
bool RootAdaptor::canRaise() const
|
||||
{
|
||||
return m_mprisController.canRaise();
|
||||
}
|
||||
|
||||
bool RootAdaptor::hasTrackList() const
|
||||
{
|
||||
return m_mprisController.hasTrackList();
|
||||
}
|
||||
|
||||
QString RootAdaptor::identity() const
|
||||
{
|
||||
return m_mprisController.identity();
|
||||
}
|
||||
|
||||
QString RootAdaptor::desktopEntry() const
|
||||
{
|
||||
return m_mprisController.desktopEntry();
|
||||
}
|
||||
|
||||
QStringList RootAdaptor::supportedUriSchemes() const
|
||||
{
|
||||
return m_mprisController.supportedUriSchemes();
|
||||
}
|
||||
|
||||
QStringList RootAdaptor::supportedMimeTypes() const
|
||||
{
|
||||
return m_mprisController.supportedMimeTypes();
|
||||
}
|
||||
|
||||
void RootAdaptor::Raise()
|
||||
{
|
||||
m_mprisController.raise();
|
||||
}
|
||||
|
||||
void RootAdaptor::Quit()
|
||||
{
|
||||
m_mprisController.quit();
|
||||
}
|
||||
|
||||
#include "moc_rootadaptor.cpp"
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2021 George Florea Bănuș <georgefb899@gmail.com>
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
#ifndef MEDIAPLAYER2_H
|
||||
#define MEDIAPLAYER2_H
|
||||
|
||||
#include <QDBusAbstractAdaptor>
|
||||
|
||||
class MprisController;
|
||||
class QDBusObjectPath;
|
||||
|
||||
// https://specifications.freedesktop.org/mpris/latest/Media_Player.html
|
||||
class RootAdaptor : public QDBusAbstractAdaptor
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_CLASSINFO("D-Bus Interface", "org.mpris.MediaPlayer2")
|
||||
|
||||
public:
|
||||
explicit RootAdaptor(MprisController &parent);
|
||||
~RootAdaptor() = default;
|
||||
|
||||
Q_PROPERTY(bool CanQuit READ canQuit CONSTANT)
|
||||
bool canQuit() const;
|
||||
|
||||
Q_PROPERTY(bool Fullscreen READ fullscreen WRITE setFullscreen)
|
||||
bool fullscreen() const;
|
||||
void setFullscreen(bool newFullscreen);
|
||||
|
||||
Q_PROPERTY(bool CanSetFullscreen READ canSetFullscreen CONSTANT)
|
||||
bool canSetFullscreen() const;
|
||||
|
||||
Q_PROPERTY(bool CanRaise READ canRaise CONSTANT)
|
||||
bool canRaise() const;
|
||||
|
||||
Q_PROPERTY(bool HasTrackList READ hasTrackList CONSTANT)
|
||||
bool hasTrackList() const;
|
||||
|
||||
Q_PROPERTY(QString Identity READ identity CONSTANT)
|
||||
QString identity() const;
|
||||
|
||||
Q_PROPERTY(QString DesktopEntry READ desktopEntry CONSTANT)
|
||||
QString desktopEntry() const;
|
||||
|
||||
Q_PROPERTY(QStringList SupportedUriSchemes READ supportedUriSchemes CONSTANT)
|
||||
QStringList supportedUriSchemes() const;
|
||||
|
||||
Q_PROPERTY(QStringList SupportedMimeTypes READ supportedMimeTypes CONSTANT)
|
||||
QStringList supportedMimeTypes() const;
|
||||
|
||||
void notify(const QString &property, const QVariant &value);
|
||||
|
||||
public Q_SLOTS:
|
||||
void Raise();
|
||||
void Quit();
|
||||
|
||||
private:
|
||||
MprisController &m_mprisController;
|
||||
};
|
||||
|
||||
#endif // MEDIAPLAYER2_H
|
||||
+162
-2
@@ -9,6 +9,7 @@
|
||||
#include <QSaveFile>
|
||||
#include <QStandardPaths>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <utility>
|
||||
|
||||
@@ -41,6 +42,13 @@ MpvItem::MpvItem(QQuickItem *parent)
|
||||
observeProperty(QStringLiteral("pause"), MPV_FORMAT_FLAG);
|
||||
observeProperty(QStringLiteral("track-list"), MPV_FORMAT_NODE);
|
||||
observeProperty(QStringLiteral("sub-delay"), MPV_FORMAT_DOUBLE);
|
||||
observeProperty(QStringLiteral("volume"), MPV_FORMAT_INT64);
|
||||
observeProperty(QStringLiteral("volume-max"), MPV_FORMAT_INT64);
|
||||
observeProperty(QStringLiteral("mute"), MPV_FORMAT_FLAG);
|
||||
observeProperty(QStringLiteral("speed"), MPV_FORMAT_DOUBLE);
|
||||
observeProperty(QStringLiteral("chapter-list"), MPV_FORMAT_NODE);
|
||||
observeProperty(QStringLiteral("chapter"), MPV_FORMAT_INT64);
|
||||
observeProperty(QStringLiteral("eof-reached"), MPV_FORMAT_FLAG);
|
||||
|
||||
// MpvController lives on a worker thread; queue its signals back to the GUI thread.
|
||||
connect(mpvController(), &MpvController::propertyChanged, this, &MpvItem::onPropertyChanged, Qt::QueuedConnection);
|
||||
@@ -132,6 +140,13 @@ void MpvItem::loadFile(const QUrl &url)
|
||||
setSubtitleDelay(0.0);
|
||||
}
|
||||
|
||||
void MpvItem::stop()
|
||||
{
|
||||
commandAsync(QStringList{QStringLiteral("stop")});
|
||||
m_currentUrl.clear();
|
||||
Q_EMIT currentUrlChanged();
|
||||
}
|
||||
|
||||
void MpvItem::togglePause()
|
||||
{
|
||||
setPaused(!m_paused);
|
||||
@@ -142,6 +157,21 @@ void MpvItem::seek(double seconds)
|
||||
commandAsync(QStringList{QStringLiteral("seek"), QString::number(seconds), QStringLiteral("absolute")});
|
||||
}
|
||||
|
||||
void MpvItem::seekBy(double seconds)
|
||||
{
|
||||
commandAsync(QStringList{QStringLiteral("seek"), QString::number(seconds), QStringLiteral("relative")});
|
||||
}
|
||||
|
||||
void MpvItem::frameStep(bool forward)
|
||||
{
|
||||
commandAsync(QStringList{forward ? QStringLiteral("frame-step") : QStringLiteral("frame-back-step")});
|
||||
}
|
||||
|
||||
void MpvItem::screenshot()
|
||||
{
|
||||
commandAsync(QStringList{QStringLiteral("screenshot")});
|
||||
}
|
||||
|
||||
QVariantList MpvItem::subtitleTracks() const
|
||||
{
|
||||
return m_subtitleTracks;
|
||||
@@ -186,6 +216,85 @@ void MpvItem::setSubtitleDelay(double seconds)
|
||||
setPropertyAsync(QStringLiteral("secondary-sub-delay"), rounded);
|
||||
}
|
||||
|
||||
QVariantList MpvItem::audioTracks() const
|
||||
{
|
||||
return m_audioTracks;
|
||||
}
|
||||
|
||||
int MpvItem::audioId() const
|
||||
{
|
||||
return m_audioId;
|
||||
}
|
||||
|
||||
void MpvItem::setAudioId(int id)
|
||||
{
|
||||
setPropertyAsync(QStringLiteral("aid"), trackIdValue(id));
|
||||
}
|
||||
|
||||
int MpvItem::volume() const
|
||||
{
|
||||
return m_volume;
|
||||
}
|
||||
|
||||
void MpvItem::setVolume(int volume)
|
||||
{
|
||||
volume = std::clamp(volume, 0, m_volumeMax);
|
||||
// Like sub-delay: update right away so repeated steps build on each other. Always
|
||||
// sent, since mpv may not have reported a volume from mpv.conf yet.
|
||||
if (volume != m_volume) {
|
||||
m_volume = volume;
|
||||
Q_EMIT volumeChanged();
|
||||
}
|
||||
setPropertyAsync(QStringLiteral("volume"), volume);
|
||||
}
|
||||
|
||||
int MpvItem::volumeMax() const
|
||||
{
|
||||
return m_volumeMax;
|
||||
}
|
||||
|
||||
bool MpvItem::mute() const
|
||||
{
|
||||
return m_mute;
|
||||
}
|
||||
|
||||
void MpvItem::setMute(bool mute)
|
||||
{
|
||||
setPropertyAsync(QStringLiteral("mute"), mute);
|
||||
}
|
||||
|
||||
double MpvItem::speed() const
|
||||
{
|
||||
return m_speed;
|
||||
}
|
||||
|
||||
void MpvItem::setSpeed(double speed)
|
||||
{
|
||||
const double rounded = std::round(std::clamp(speed, 0.1, 10.0) * 100.0) / 100.0;
|
||||
if (rounded != m_speed) {
|
||||
m_speed = rounded;
|
||||
Q_EMIT speedChanged();
|
||||
}
|
||||
setPropertyAsync(QStringLiteral("speed"), rounded);
|
||||
}
|
||||
|
||||
QVariantList MpvItem::chapters() const
|
||||
{
|
||||
return m_chapters;
|
||||
}
|
||||
|
||||
int MpvItem::chapter() const
|
||||
{
|
||||
return m_chapter;
|
||||
}
|
||||
|
||||
void MpvItem::setChapter(int chapter)
|
||||
{
|
||||
if (chapter >= 0 && chapter < m_chapters.size()) {
|
||||
setPropertyAsync(QStringLiteral("chapter"), chapter);
|
||||
}
|
||||
}
|
||||
|
||||
void MpvItem::addSubtitleFile(const QUrl &url)
|
||||
{
|
||||
if (!url.isValid()) {
|
||||
@@ -301,22 +410,69 @@ void MpvItem::onPropertyChanged(const QString &property, const QVariant &value)
|
||||
} else if (property == u"sub-delay") {
|
||||
m_subtitleDelay = value.toDouble();
|
||||
Q_EMIT subtitleDelayChanged();
|
||||
} else if (property == u"volume") {
|
||||
m_volume = value.toInt();
|
||||
Q_EMIT volumeChanged();
|
||||
} else if (property == u"volume-max") {
|
||||
m_volumeMax = value.toInt();
|
||||
Q_EMIT volumeMaxChanged();
|
||||
} else if (property == u"mute") {
|
||||
m_mute = value.toBool();
|
||||
Q_EMIT muteChanged();
|
||||
} else if (property == u"speed") {
|
||||
m_speed = value.toDouble();
|
||||
Q_EMIT speedChanged();
|
||||
} else if (property == u"chapter-list") {
|
||||
QVariantList chapters;
|
||||
for (const QVariant &entry : value.toList()) {
|
||||
const QVariantMap chapter = entry.toMap();
|
||||
chapters.append(QVariantMap{
|
||||
{QStringLiteral("title"), chapter.value(QStringLiteral("title")).toString()},
|
||||
{QStringLiteral("time"), chapter.value(QStringLiteral("time")).toDouble()},
|
||||
});
|
||||
}
|
||||
m_chapters = chapters;
|
||||
Q_EMIT chaptersChanged();
|
||||
} else if (property == u"chapter") {
|
||||
// Unavailable (no chapters) arrives as an invalid value.
|
||||
m_chapter = value.isValid() ? value.toInt() : -1;
|
||||
Q_EMIT chapterChanged();
|
||||
} else if (property == u"eof-reached") {
|
||||
if (value.toBool()) {
|
||||
Q_EMIT endOfFile();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MpvItem::updateTracks(const QVariantList &trackList)
|
||||
{
|
||||
QVariantList subtitles;
|
||||
QVariantList audio;
|
||||
int primary = 0;
|
||||
int secondary = 0;
|
||||
int audioId = 0;
|
||||
|
||||
for (const QVariant &entry : trackList) {
|
||||
const QVariantMap track = entry.toMap();
|
||||
if (track.value(QStringLiteral("type")).toString() != u"sub") {
|
||||
const QString type = track.value(QStringLiteral("type")).toString();
|
||||
const int id = track.value(QStringLiteral("id")).toInt();
|
||||
|
||||
if (type == u"audio") {
|
||||
if (track.value(QStringLiteral("selected")).toBool()) {
|
||||
audioId = id;
|
||||
}
|
||||
audio.append(QVariantMap{
|
||||
{QStringLiteral("id"), id},
|
||||
{QStringLiteral("title"), track.value(QStringLiteral("title")).toString()},
|
||||
{QStringLiteral("lang"), track.value(QStringLiteral("lang")).toString()},
|
||||
{QStringLiteral("codec"), track.value(QStringLiteral("codec")).toString()},
|
||||
});
|
||||
continue;
|
||||
}
|
||||
if (type != u"sub") {
|
||||
continue;
|
||||
}
|
||||
|
||||
const int id = track.value(QStringLiteral("id")).toInt();
|
||||
if (track.value(QStringLiteral("selected")).toBool()) {
|
||||
// main-selection is 0 for the primary subtitle and 1 for the secondary one.
|
||||
if (track.value(QStringLiteral("main-selection")).toInt() == 0) {
|
||||
@@ -346,6 +502,10 @@ void MpvItem::updateTracks(const QVariantList &trackList)
|
||||
m_subtitleId = primary;
|
||||
m_secondarySubtitleId = secondary;
|
||||
Q_EMIT subtitleTracksChanged();
|
||||
|
||||
m_audioTracks = audio;
|
||||
m_audioId = audioId;
|
||||
Q_EMIT audioTracksChanged();
|
||||
}
|
||||
|
||||
void MpvItem::setFileLoaded(bool loaded)
|
||||
|
||||
@@ -29,6 +29,19 @@ class MpvItem : public MpvAbstractItem
|
||||
// In seconds; applied to both subtitles so a translation stays in sync with its source.
|
||||
Q_PROPERTY(double subtitleDelay READ subtitleDelay WRITE setSubtitleDelay NOTIFY subtitleDelayChanged)
|
||||
|
||||
// Audio tracks of the current file, each a map with id, title, lang and codec.
|
||||
Q_PROPERTY(QVariantList audioTracks READ audioTracks NOTIFY audioTracksChanged)
|
||||
Q_PROPERTY(int audioId READ audioId WRITE setAudioId NOTIFY audioTracksChanged)
|
||||
// 0 to volumeMax (mpv's "volume-max", 130 unless mpv.conf changes it).
|
||||
Q_PROPERTY(int volume READ volume WRITE setVolume NOTIFY volumeChanged)
|
||||
Q_PROPERTY(int volumeMax READ volumeMax NOTIFY volumeMaxChanged)
|
||||
Q_PROPERTY(bool mute READ mute WRITE setMute NOTIFY muteChanged)
|
||||
Q_PROPERTY(double speed READ speed WRITE setSpeed NOTIFY speedChanged)
|
||||
// Chapters of the current file, each a map with title and time (in seconds).
|
||||
Q_PROPERTY(QVariantList chapters READ chapters NOTIFY chaptersChanged)
|
||||
// Index into chapters, -1 before the first chapter or without chapters.
|
||||
Q_PROPERTY(int chapter READ chapter WRITE setChapter NOTIFY chapterChanged)
|
||||
|
||||
// Load the user's mpv.conf when mpv starts. Must be set before the item is shown.
|
||||
Q_PROPERTY(bool loadUserConfig READ loadUserConfig WRITE setLoadUserConfig NOTIFY loadUserConfigChanged)
|
||||
Q_PROPERTY(QString userConfigDir READ userConfigDir CONSTANT)
|
||||
@@ -55,6 +68,19 @@ public:
|
||||
void setSecondarySubtitleId(int id);
|
||||
double subtitleDelay() const;
|
||||
void setSubtitleDelay(double seconds);
|
||||
QVariantList audioTracks() const;
|
||||
int audioId() const;
|
||||
void setAudioId(int id);
|
||||
int volume() const;
|
||||
void setVolume(int volume);
|
||||
int volumeMax() const;
|
||||
bool mute() const;
|
||||
void setMute(bool mute);
|
||||
double speed() const;
|
||||
void setSpeed(double speed);
|
||||
QVariantList chapters() const;
|
||||
int chapter() const;
|
||||
void setChapter(int chapter);
|
||||
bool loadUserConfig() const;
|
||||
void setLoadUserConfig(bool load);
|
||||
QString userConfigDir() const;
|
||||
@@ -62,8 +88,15 @@ public:
|
||||
QStringList skippedUserOptions() const;
|
||||
|
||||
Q_INVOKABLE void loadFile(const QUrl &url);
|
||||
// Unloads the current file.
|
||||
Q_INVOKABLE void stop();
|
||||
Q_INVOKABLE void togglePause();
|
||||
Q_INVOKABLE void seek(double seconds);
|
||||
// Seeks \a seconds forward, or backward when negative.
|
||||
Q_INVOKABLE void seekBy(double seconds);
|
||||
Q_INVOKABLE void frameStep(bool forward);
|
||||
// Saves a screenshot the way mpv does (screenshot-directory, screenshot-format).
|
||||
Q_INVOKABLE void screenshot();
|
||||
// Adds an external subtitle file and makes it the primary subtitle.
|
||||
Q_INVOKABLE void addSubtitleFile(const QUrl &url);
|
||||
// Makes \a url the only subtitle shown, adding it first (named \a title) unless it is
|
||||
@@ -81,6 +114,15 @@ Q_SIGNALS:
|
||||
void fileLoadedChanged();
|
||||
void subtitleTracksChanged();
|
||||
void subtitleDelayChanged();
|
||||
void audioTracksChanged();
|
||||
void volumeChanged();
|
||||
void volumeMaxChanged();
|
||||
void muteChanged();
|
||||
void speedChanged();
|
||||
void chaptersChanged();
|
||||
void chapterChanged();
|
||||
// Playback reached the end of the file. The last frame stays on screen.
|
||||
void endOfFile();
|
||||
void loadUserConfigChanged();
|
||||
// mpv.conf was queued for loading; options set after this take precedence over it.
|
||||
void userConfigApplied();
|
||||
@@ -104,6 +146,14 @@ private:
|
||||
int m_subtitleId = 0;
|
||||
int m_secondarySubtitleId = 0;
|
||||
double m_subtitleDelay = 0.0;
|
||||
QVariantList m_audioTracks;
|
||||
int m_audioId = 0;
|
||||
int m_volume = 100;
|
||||
int m_volumeMax = 130;
|
||||
bool m_mute = false;
|
||||
double m_speed = 1.0;
|
||||
QVariantList m_chapters;
|
||||
int m_chapter = -1;
|
||||
// File added by showTranslatedSubtitleFile() that should be shown once mpv lists it.
|
||||
QString m_pendingTranslatedPath;
|
||||
bool m_loadUserConfig = false;
|
||||
|
||||
@@ -0,0 +1,276 @@
|
||||
// SPDX-FileCopyrightText: 2026 Tri Vo Khac
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include "playlistmodel.h"
|
||||
|
||||
#include <QCollator>
|
||||
#include <QDir>
|
||||
#include <QFileInfo>
|
||||
#include <QMimeDatabase>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
using namespace Qt::Literals::StringLiterals;
|
||||
|
||||
PlaylistModel::PlaylistModel(QObject *parent)
|
||||
: QAbstractListModel(parent)
|
||||
{
|
||||
}
|
||||
|
||||
int PlaylistModel::rowCount(const QModelIndex &parent) const
|
||||
{
|
||||
return parent.isValid() ? 0 : int(m_urls.size());
|
||||
}
|
||||
|
||||
QVariant PlaylistModel::data(const QModelIndex &index, int role) const
|
||||
{
|
||||
if (!checkIndex(index, CheckIndexOption::IndexIsValid)) {
|
||||
return {};
|
||||
}
|
||||
const QUrl &url = m_urls.at(index.row());
|
||||
switch (role) {
|
||||
case UrlRole:
|
||||
return url;
|
||||
case Qt::DisplayRole:
|
||||
case NameRole:
|
||||
return url.isLocalFile() ? url.fileName() : url.toDisplayString();
|
||||
case IsPlayingRole:
|
||||
return index.row() == m_playingIndex;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
QHash<int, QByteArray> PlaylistModel::roleNames() const
|
||||
{
|
||||
return {
|
||||
{UrlRole, "url"},
|
||||
{NameRole, "name"},
|
||||
{IsPlayingRole, "isPlaying"},
|
||||
};
|
||||
}
|
||||
|
||||
int PlaylistModel::count() const
|
||||
{
|
||||
return int(m_urls.size());
|
||||
}
|
||||
|
||||
int PlaylistModel::playingIndex() const
|
||||
{
|
||||
return m_playingIndex;
|
||||
}
|
||||
|
||||
bool PlaylistModel::isMediaFile(const QString &fileName)
|
||||
{
|
||||
static const QMimeDatabase mimeDatabase;
|
||||
const QString mimeType = mimeDatabase.mimeTypeForFile(fileName, QMimeDatabase::MatchExtension).name();
|
||||
return mimeType.startsWith(u"video/") || mimeType.startsWith(u"audio/");
|
||||
}
|
||||
|
||||
QList<QUrl> PlaylistModel::mediaFilesIn(const QString &folder)
|
||||
{
|
||||
QStringList names;
|
||||
const QStringList entries = QDir(folder).entryList(QDir::Files | QDir::Readable);
|
||||
for (const QString &name : entries) {
|
||||
if (isMediaFile(name)) {
|
||||
names.append(name);
|
||||
}
|
||||
}
|
||||
|
||||
QCollator collator;
|
||||
collator.setNumericMode(true);
|
||||
collator.setCaseSensitivity(Qt::CaseInsensitive);
|
||||
std::sort(names.begin(), names.end(), collator);
|
||||
|
||||
QList<QUrl> urls;
|
||||
const QDir dir(folder);
|
||||
for (const QString &name : std::as_const(names)) {
|
||||
urls.append(QUrl::fromLocalFile(dir.absoluteFilePath(name)));
|
||||
}
|
||||
return urls;
|
||||
}
|
||||
|
||||
QList<QUrl> PlaylistModel::expand(const QList<QUrl> &urls) const
|
||||
{
|
||||
QList<QUrl> result;
|
||||
for (const QUrl &url : urls) {
|
||||
if (url.isLocalFile() && QFileInfo(url.toLocalFile()).isDir()) {
|
||||
result.append(mediaFilesIn(url.toLocalFile()));
|
||||
} else {
|
||||
result.append(url);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void PlaylistModel::openFile(const QUrl &url, bool loadSiblings)
|
||||
{
|
||||
if (!url.isValid()) {
|
||||
return;
|
||||
}
|
||||
|
||||
QList<QUrl> urls{url};
|
||||
if (loadSiblings && url.isLocalFile()) {
|
||||
const QFileInfo file(url.toLocalFile());
|
||||
if (file.isDir()) {
|
||||
urls = mediaFilesIn(file.absoluteFilePath());
|
||||
} else if (isMediaFile(file.fileName())) {
|
||||
urls = mediaFilesIn(file.absolutePath());
|
||||
// Files whose type is not recognized (or that vanished) still play.
|
||||
if (!urls.contains(url)) {
|
||||
urls = {url};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
beginResetModel();
|
||||
m_urls = urls;
|
||||
m_playingIndex = -1;
|
||||
endResetModel();
|
||||
Q_EMIT countChanged();
|
||||
play(std::max<int>(0, int(m_urls.indexOf(url))));
|
||||
}
|
||||
|
||||
void PlaylistModel::append(const QList<QUrl> &urls)
|
||||
{
|
||||
const QList<QUrl> added = expand(urls);
|
||||
if (added.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
beginInsertRows({}, int(m_urls.size()), int(m_urls.size() + added.size() - 1));
|
||||
m_urls.append(added);
|
||||
endInsertRows();
|
||||
Q_EMIT countChanged();
|
||||
}
|
||||
|
||||
void PlaylistModel::replace(const QList<QUrl> &urls)
|
||||
{
|
||||
const QList<QUrl> files = expand(urls);
|
||||
if (files.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
beginResetModel();
|
||||
m_urls = files;
|
||||
m_playingIndex = -1;
|
||||
endResetModel();
|
||||
Q_EMIT countChanged();
|
||||
play(0);
|
||||
}
|
||||
|
||||
void PlaylistModel::play(int index)
|
||||
{
|
||||
if (index < 0 || index >= m_urls.size()) {
|
||||
return;
|
||||
}
|
||||
setPlayingIndex(index);
|
||||
Q_EMIT playRequested(m_urls.at(index));
|
||||
}
|
||||
|
||||
bool PlaylistModel::playNext(bool wrap)
|
||||
{
|
||||
if (m_urls.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
int next = m_playingIndex + 1;
|
||||
if (next >= m_urls.size()) {
|
||||
if (!wrap) {
|
||||
return false;
|
||||
}
|
||||
next = 0;
|
||||
}
|
||||
play(next);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PlaylistModel::playPrevious(bool wrap)
|
||||
{
|
||||
if (m_urls.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
int previous = m_playingIndex - 1;
|
||||
if (previous < 0) {
|
||||
if (!wrap) {
|
||||
return false;
|
||||
}
|
||||
previous = int(m_urls.size()) - 1;
|
||||
}
|
||||
play(previous);
|
||||
return true;
|
||||
}
|
||||
|
||||
void PlaylistModel::remove(int index)
|
||||
{
|
||||
if (index < 0 || index >= m_urls.size()) {
|
||||
return;
|
||||
}
|
||||
beginRemoveRows({}, index, index);
|
||||
m_urls.removeAt(index);
|
||||
endRemoveRows();
|
||||
Q_EMIT countChanged();
|
||||
|
||||
// The playing file keeps playing; it just has no entry any more.
|
||||
if (index == m_playingIndex) {
|
||||
m_playingIndex = -1;
|
||||
Q_EMIT playingIndexChanged();
|
||||
} else if (index < m_playingIndex) {
|
||||
--m_playingIndex;
|
||||
Q_EMIT playingIndexChanged();
|
||||
}
|
||||
}
|
||||
|
||||
void PlaylistModel::move(int from, int to)
|
||||
{
|
||||
if (from == to || from < 0 || to < 0 || from >= m_urls.size() || to >= m_urls.size()) {
|
||||
return;
|
||||
}
|
||||
// beginMoveRows wants the row the item lands before, counted before the move.
|
||||
if (!beginMoveRows({}, from, from, {}, to > from ? to + 1 : to)) {
|
||||
return;
|
||||
}
|
||||
m_urls.move(from, to);
|
||||
endMoveRows();
|
||||
|
||||
if (m_playingIndex == from) {
|
||||
m_playingIndex = to;
|
||||
} else if (from < m_playingIndex && to >= m_playingIndex) {
|
||||
--m_playingIndex;
|
||||
} else if (from > m_playingIndex && to <= m_playingIndex) {
|
||||
++m_playingIndex;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
Q_EMIT playingIndexChanged();
|
||||
}
|
||||
|
||||
void PlaylistModel::clear()
|
||||
{
|
||||
if (m_urls.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
beginResetModel();
|
||||
m_urls.clear();
|
||||
m_playingIndex = -1;
|
||||
endResetModel();
|
||||
Q_EMIT countChanged();
|
||||
Q_EMIT playingIndexChanged();
|
||||
}
|
||||
|
||||
QUrl PlaylistModel::urlAt(int index) const
|
||||
{
|
||||
return m_urls.value(index);
|
||||
}
|
||||
|
||||
void PlaylistModel::setPlayingIndex(int index)
|
||||
{
|
||||
if (index == m_playingIndex) {
|
||||
return;
|
||||
}
|
||||
const int previous = m_playingIndex;
|
||||
m_playingIndex = index;
|
||||
if (previous >= 0 && previous < m_urls.size()) {
|
||||
Q_EMIT dataChanged(this->index(previous), this->index(previous), {IsPlayingRole});
|
||||
}
|
||||
if (index >= 0) {
|
||||
Q_EMIT dataChanged(this->index(index), this->index(index), {IsPlayingRole});
|
||||
}
|
||||
Q_EMIT playingIndexChanged();
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
// SPDX-FileCopyrightText: 2026 Tri Vo Khac
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QAbstractListModel>
|
||||
#include <QList>
|
||||
#include <QUrl>
|
||||
#include <QtQml/qqmlregistration.h>
|
||||
|
||||
// The files queued for playback. The model only tracks which entry plays; it asks for
|
||||
// a file to be played through playRequested(), and the player loads it.
|
||||
class PlaylistModel : public QAbstractListModel
|
||||
{
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
|
||||
Q_PROPERTY(int count READ count NOTIFY countChanged)
|
||||
// -1 when nothing from the list is playing.
|
||||
Q_PROPERTY(int playingIndex READ playingIndex NOTIFY playingIndexChanged)
|
||||
|
||||
public:
|
||||
enum Roles {
|
||||
UrlRole = Qt::UserRole + 1,
|
||||
NameRole,
|
||||
IsPlayingRole,
|
||||
};
|
||||
Q_ENUM(Roles)
|
||||
|
||||
explicit PlaylistModel(QObject *parent = nullptr);
|
||||
|
||||
int rowCount(const QModelIndex &parent = {}) const override;
|
||||
QVariant data(const QModelIndex &index, int role) const override;
|
||||
QHash<int, QByteArray> roleNames() const override;
|
||||
|
||||
int count() const;
|
||||
int playingIndex() const;
|
||||
|
||||
// Replaces the list with \a url, plus the other media files in its folder when
|
||||
// \a loadSiblings is set, and plays \a url.
|
||||
Q_INVOKABLE void openFile(const QUrl &url, bool loadSiblings);
|
||||
// Adds files, and the media files inside folders, to the end of the list.
|
||||
Q_INVOKABLE void append(const QList<QUrl> &urls);
|
||||
// Replaces the list with \a urls (folders expanded) and plays the first one.
|
||||
Q_INVOKABLE void replace(const QList<QUrl> &urls);
|
||||
Q_INVOKABLE void play(int index);
|
||||
// Plays the entry after (or before) the playing one. With \a wrap the end of the
|
||||
// list continues at the other end. Returns false when there is nothing to play.
|
||||
Q_INVOKABLE bool playNext(bool wrap = false);
|
||||
Q_INVOKABLE bool playPrevious(bool wrap = false);
|
||||
Q_INVOKABLE void remove(int index);
|
||||
Q_INVOKABLE void move(int from, int to);
|
||||
Q_INVOKABLE void clear();
|
||||
Q_INVOKABLE QUrl urlAt(int index) const;
|
||||
|
||||
// Media files directly inside \a folder, sorted the way a file manager does
|
||||
// ("Episode 2" before "Episode 10").
|
||||
static QList<QUrl> mediaFilesIn(const QString &folder);
|
||||
static bool isMediaFile(const QString &fileName);
|
||||
|
||||
Q_SIGNALS:
|
||||
void countChanged();
|
||||
void playingIndexChanged();
|
||||
void playRequested(const QUrl &url);
|
||||
|
||||
private:
|
||||
QList<QUrl> expand(const QList<QUrl> &urls) const;
|
||||
void setPlayingIndex(int index);
|
||||
|
||||
QList<QUrl> m_urls;
|
||||
int m_playingIndex = -1;
|
||||
};
|
||||
Reference in new issue
Block a user