refactor(playlist): playlist itself as model

This could make state management easier, and also make it reusable
just in case we need to attach the playlist to a view.
This commit is contained in:
Gary Wang 2024-07-20 23:18:42 +08:00
parent 4a095a0cfd
commit eb2e2e93f9
No known key found for this signature in database
GPG Key ID: 5D30A4F15EA78760
3 changed files with 252 additions and 197 deletions

View File

@ -46,7 +46,7 @@
MainWindow::MainWindow(QWidget *parent) MainWindow::MainWindow(QWidget *parent)
: FramelessWindow(parent) : FramelessWindow(parent)
, m_am(new ActionManager) , m_am(new ActionManager)
, m_pm(new PlaylistManager(PlaylistManager::PL_SAMEFOLDER, this)) , m_pm(new PlaylistManager(this))
{ {
if (Settings::instance()->stayOnTop()) { if (Settings::instance()->stayOnTop()) {
this->setWindowFlag(Qt::WindowStaysOnTopHint); this->setWindowFlag(Qt::WindowStaysOnTopHint);
@ -62,7 +62,7 @@ MainWindow::MainWindow(QWidget *parent)
for (const QByteArray &item : QImageReader::supportedImageFormats()) { for (const QByteArray &item : QImageReader::supportedImageFormats()) {
formatFilters.append(QStringLiteral("*.") % QString::fromLocal8Bit(item)); formatFilters.append(QStringLiteral("*.") % QString::fromLocal8Bit(item));
} }
m_pm->setAutoLoadFilterSuffix(formatFilters); m_pm->setAutoLoadFilterSuffixes(formatFilters);
m_fadeOutAnimation = new QPropertyAnimation(this, "windowOpacity"); m_fadeOutAnimation = new QPropertyAnimation(this, "windowOpacity");
m_fadeOutAnimation->setDuration(300); m_fadeOutAnimation->setDuration(300);
@ -142,19 +142,12 @@ MainWindow::MainWindow(QWidget *parent)
m_gv->setOpacity(0, false); m_gv->setOpacity(0, false);
m_closeButton->setOpacity(0, false); m_closeButton->setOpacity(0, false);
connect(m_pm, &PlaylistManager::loaded, this, [this](int galleryFileCount) { connect(m_pm, &PlaylistManager::totalCountChanged, this, [this](int galleryFileCount) {
m_prevButton->setVisible(galleryFileCount > 1); m_prevButton->setVisible(galleryFileCount > 1);
m_nextButton->setVisible(galleryFileCount > 1); m_nextButton->setVisible(galleryFileCount > 1);
}); });
connect(m_pm, &PlaylistManager::currentIndexChanged, this, [this]() { connect(m_pm, &PlaylistManager::currentIndexChanged, this, &MainWindow::galleryCurrent);
int index;
QUrl url;
std::tie(index, url) = m_pm->currentFileUrl();
if (index != -1) {
this->setWindowTitle(url.fileName());
}
});
QShortcut * fullscreenShorucut = new QShortcut(QKeySequence(QKeySequence::FullScreen), this); QShortcut * fullscreenShorucut = new QShortcut(QKeySequence(QKeySequence::FullScreen), this);
connect(fullscreenShorucut, &QShortcut::activated, connect(fullscreenShorucut, &QShortcut::activated,
@ -187,7 +180,6 @@ void MainWindow::showUrls(const QList<QUrl> &urls)
} else { } else {
m_graphicsView->showFileFromPath(urls.first().toLocalFile(), false); m_graphicsView->showFileFromPath(urls.first().toLocalFile(), false);
m_pm->setPlaylist(urls); m_pm->setPlaylist(urls);
m_pm->setCurrentIndex(0);
} }
} else { } else {
m_graphicsView->showText(tr("File url list is empty")); m_graphicsView->showText(tr("File url list is empty"));
@ -214,7 +206,7 @@ void MainWindow::initWindowSize()
void MainWindow::adjustWindowSizeBySceneRect() void MainWindow::adjustWindowSizeBySceneRect()
{ {
if (m_pm->count() < 1) return; if (m_pm->totalCount() < 1) return;
QSize sceneSize = m_graphicsView->sceneRect().toRect().size(); QSize sceneSize = m_graphicsView->sceneRect().toRect().size();
QSize sceneSizeWithMargins = sceneSize + QSize(130, 125); QSize sceneSizeWithMargins = sceneSize + QSize(130, 125);
@ -245,42 +237,31 @@ void MainWindow::adjustWindowSizeBySceneRect()
// can be empty if it is NOT from a local file. // can be empty if it is NOT from a local file.
QUrl MainWindow::currentImageFileUrl() const QUrl MainWindow::currentImageFileUrl() const
{ {
QUrl url; return m_pm->urlByIndex(m_pm->curIndex());
std::tie(std::ignore, url) = m_pm->currentFileUrl();
return url;
} }
void MainWindow::clearGallery() void MainWindow::clearGallery()
{ {
m_pm->clear(); m_pm->setPlaylist({});
} }
void MainWindow::loadGalleryBySingleLocalFile(const QString &path) void MainWindow::loadGalleryBySingleLocalFile(const QString &path)
{ {
m_pm->setCurrentFile(path); m_pm->loadPlaylist({QUrl::fromLocalFile(path)});
} }
void MainWindow::galleryPrev() void MainWindow::galleryPrev()
{ {
int index; QModelIndex index = m_pm->previousIndex();
QString filePath; if (index.isValid()) {
std::tie(index, filePath) = m_pm->previousFile();
if (index >= 0) {
m_graphicsView->showFileFromPath(filePath, false);
m_pm->setCurrentIndex(index); m_pm->setCurrentIndex(index);
} }
} }
void MainWindow::galleryNext() void MainWindow::galleryNext()
{ {
int index; QModelIndex index = m_pm->nextIndex();
QString filePath; if (index.isValid()) {
std::tie(index, filePath) = m_pm->nextFile();
if (index >= 0) {
m_graphicsView->showFileFromPath(filePath, false);
m_pm->setCurrentIndex(index); m_pm->setCurrentIndex(index);
} }
} }
@ -288,12 +269,10 @@ void MainWindow::galleryNext()
// If playlist (or its index) get changed, use this method to "reload" the current file. // If playlist (or its index) get changed, use this method to "reload" the current file.
void MainWindow::galleryCurrent() void MainWindow::galleryCurrent()
{ {
int index; QModelIndex index = m_pm->curIndex();
QString filePath; if (index.isValid()) {
std::tie(index, filePath) = m_pm->currentFile(); setWindowTitle(m_pm->urlByIndex(index).fileName());
m_graphicsView->showFileFromPath(m_pm->localFileByIndex(index), false);
if (index >= 0) {
m_graphicsView->showFileFromPath(filePath, false);
} else { } else {
m_graphicsView->showText(QCoreApplication::translate("GraphicsScene", "Drag image here")); m_graphicsView->showText(QCoreApplication::translate("GraphicsScene", "Drag image here"));
} }
@ -721,18 +700,16 @@ void MainWindow::on_actionPaste_triggered()
} else if (clipboardFileUrl.isValid()) { } else if (clipboardFileUrl.isValid()) {
QString localFile(clipboardFileUrl.toLocalFile()); QString localFile(clipboardFileUrl.toLocalFile());
m_graphicsView->showFileFromPath(localFile, true); m_graphicsView->showFileFromPath(localFile, true);
m_pm->setCurrentFile(localFile); m_pm->loadPlaylist({clipboardFileUrl});
} }
} }
void MainWindow::on_actionTrash_triggered() void MainWindow::on_actionTrash_triggered()
{ {
int currentFileIndex; QModelIndex index = m_pm->curIndex();
QUrl currentFileUrl; if (!m_pm->urlByIndex(index).isLocalFile()) return;
std::tie(currentFileIndex, currentFileUrl) = m_pm->currentFileUrl();
if (!currentFileUrl.isLocalFile()) return;
QFile file(currentFileUrl.toLocalFile()); QFile file(m_pm->localFileByIndex(index));
QFileInfo fileInfo(file.fileName()); QFileInfo fileInfo(file.fileName());
QMessageBox::StandardButton result = QMessageBox::question(this, tr("Move to Trash"), QMessageBox::StandardButton result = QMessageBox::question(this, tr("Move to Trash"),
@ -743,7 +720,7 @@ void MainWindow::on_actionTrash_triggered()
QMessageBox::warning(this, "Failed to move file to trash", QMessageBox::warning(this, "Failed to move file to trash",
tr("Move to trash failed, it might caused by file permission issue, file system limitation, or platform limitation.")); tr("Move to trash failed, it might caused by file permission issue, file system limitation, or platform limitation."));
} else { } else {
m_pm->removeFileAt(currentFileIndex); m_pm->removeAt(index);
galleryCurrent(); galleryCurrent();
} }
} }

View File

@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: 2022 Gary Wang <wzc782970009@gmail.com> // SPDX-FileCopyrightText: 2024 Gary Wang <git@blumia.net>
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
@ -9,166 +9,222 @@
#include <QFileInfo> #include <QFileInfo>
#include <QUrl> #include <QUrl>
PlaylistManager::PlaylistManager(PlaylistType type, QObject *parent) PlaylistModel::PlaylistModel(QObject *parent)
: QObject(parent) : QAbstractListModel(parent)
, m_type(type)
{ {
} }
PlaylistModel::~PlaylistModel()
{
}
void PlaylistModel::setPlaylist(const QList<QUrl> &urls)
{
beginResetModel();
m_playlist = urls;
endResetModel();
}
QModelIndex PlaylistModel::loadPlaylist(const QList<QUrl> & urls)
{
if (urls.isEmpty()) return QModelIndex();
if (urls.count() == 1) {
return loadPlaylist(urls.constFirst());
} else {
setPlaylist(urls);
return createIndex(0);
}
}
QModelIndex PlaylistModel::loadPlaylist(const QUrl &url)
{
QFileInfo info(url.toLocalFile());
QDir dir(info.path());
QString && currentFileName = info.fileName();
if (dir.path() == m_currentDir) {
int index = indexOf(url);
return index == -1 ? appendToPlaylist(url) : createIndex(index);
}
QStringList entryList = dir.entryList(
m_autoLoadSuffixes,
QDir::Files | QDir::NoSymLinks, QDir::NoSort);
QCollator collator;
collator.setNumericMode(true);
std::sort(entryList.begin(), entryList.end(), collator);
QList<QUrl> playlist;
int index = -1;
for (int i = 0; i < entryList.count(); i++) {
const QString & fileName = entryList.at(i);
const QString & oneEntry = dir.absoluteFilePath(fileName);
const QUrl & url = QUrl::fromLocalFile(oneEntry);
playlist.append(url);
if (fileName == currentFileName) {
index = i;
}
}
if (index == -1) {
index = playlist.count();
playlist.append(url);
}
m_currentDir = dir.path();
setPlaylist(playlist);
return createIndex(index);
}
QModelIndex PlaylistModel::appendToPlaylist(const QUrl &url)
{
const int lastIndex = rowCount();
beginInsertRows(QModelIndex(), lastIndex, lastIndex);
m_playlist.append(url);
endInsertRows();
return createIndex(lastIndex);
}
bool PlaylistModel::removeAt(int index)
{
if (index < 0 || index >= rowCount()) return false;
beginRemoveRows(QModelIndex(), index, index);
m_playlist.removeAt(index);
endRemoveRows();
return true;
}
int PlaylistModel::indexOf(const QUrl &url) const
{
return m_playlist.indexOf(url);
}
QStringList PlaylistModel::autoLoadFilterSuffixes() const
{
return m_autoLoadSuffixes;
}
QModelIndex PlaylistModel::createIndex(int row) const
{
return QAbstractItemModel::createIndex(row, 0, nullptr);
}
int PlaylistModel::rowCount(const QModelIndex &parent) const
{
return m_playlist.count();
}
QVariant PlaylistModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid()) return QVariant();
switch (role) {
case Qt::DisplayRole:
return m_playlist.at(index.row()).fileName();
case UrlRole:
return m_playlist.at(index.row());
}
return QVariant();
}
PlaylistManager::PlaylistManager(QObject *parent)
: QObject(parent)
{
connect(&m_model, &PlaylistModel::rowsRemoved, this,
[this](const QModelIndex &, int, int) {
if (m_model.rowCount() <= m_currentIndex) {
setProperty("currentIndex", m_currentIndex - 1);
}
});
auto onRowCountChanged = [this](){
emit totalCountChanged(m_model.rowCount());
};
connect(&m_model, &PlaylistModel::rowsInserted, this, onRowCountChanged);
connect(&m_model, &PlaylistModel::rowsRemoved, this, onRowCountChanged);
connect(&m_model, &PlaylistModel::modelReset, this, onRowCountChanged);
}
PlaylistManager::~PlaylistManager() PlaylistManager::~PlaylistManager()
{ {
} }
void PlaylistManager::setPlaylistType(PlaylistManager::PlaylistType type) const PlaylistModel *PlaylistManager::model() const
{ {
m_type = type; return &m_model;
}
PlaylistManager::PlaylistType PlaylistManager::playlistType() const
{
return m_type;
}
QStringList PlaylistManager::autoLoadFilterSuffix() const
{
return m_autoLoadSuffix;
}
void PlaylistManager::setAutoLoadFilterSuffix(const QStringList & nameFilters)
{
m_autoLoadSuffix = nameFilters;
}
void PlaylistManager::clear()
{
m_currentIndex = -1;
m_playlist.clear();
} }
void PlaylistManager::setPlaylist(const QList<QUrl> &urls) void PlaylistManager::setPlaylist(const QList<QUrl> &urls)
{ {
m_playlist = urls; m_model.setPlaylist(urls);
} }
void PlaylistManager::setCurrentFile(const QString & filePath) QModelIndex PlaylistManager::loadPlaylist(const QList<QUrl> &urls)
{ {
QFileInfo info(filePath); QModelIndex idx = m_model.loadPlaylist(urls);
QDir dir(info.path()); setProperty("currentIndex", idx.row());
QString && currentFileName = info.fileName(); return idx;
switch (playlistType()) {
case PL_SAMEFOLDER: {
if (dir.path() == m_currentDir) {
int index = indexOf(filePath);
m_currentIndex = index == -1 ? appendFile(filePath) : index;
} else {
QStringList entryList = dir.entryList(
m_autoLoadSuffix,
QDir::Files | QDir::NoSymLinks, QDir::NoSort);
QCollator collator;
collator.setNumericMode(true);
std::sort(entryList.begin(), entryList.end(), collator);
clear();
int index = -1;
for (int i = 0; i < entryList.count(); i++) {
const QString & fileName = entryList.at(i);
const QString & oneEntry = dir.absoluteFilePath(fileName);
const QUrl & url = QUrl::fromLocalFile(oneEntry);
m_playlist.append(url);
if (fileName == currentFileName) {
index = i;
}
}
m_currentIndex = index == -1 ? appendFile(filePath) : index;
m_currentDir = dir.path();
}
break;
}
case PL_USERPLAYLIST:{
int index = indexOf(filePath);
m_currentIndex = index == -1 ? appendFile(filePath) : index;
break;
}
default:
break;
}
emit currentIndexChanged(m_currentIndex);
emit loaded(m_playlist.count());
} }
void PlaylistManager::setCurrentIndex(int index) int PlaylistManager::totalCount() const
{ {
if (index < 0 || index >= m_playlist.count()) return; return m_model.rowCount();
m_currentIndex = index;
emit currentIndexChanged(m_currentIndex);
} }
int PlaylistManager::appendFile(const QString &filePath) QModelIndex PlaylistManager::previousIndex() const
{ {
int index = m_playlist.length(); int count = totalCount();
m_playlist.append(QUrl::fromLocalFile(filePath)); if (count == 0) return QModelIndex();
return index; return m_model.createIndex(m_currentIndex - 1 < 0 ? count - 1 : m_currentIndex - 1);
} }
// Note: this will only remove file out of the list, this will NOT delete the file QModelIndex PlaylistManager::nextIndex() const
void PlaylistManager::removeFileAt(int index)
{ {
m_playlist.removeAt(index); int count = totalCount();
if (count == 0) return QModelIndex();
if (m_playlist.count() <= m_currentIndex) { return m_model.createIndex(m_currentIndex + 1 == count ? 0 : m_currentIndex + 1);
m_currentIndex--; }
QModelIndex PlaylistManager::curIndex() const
{
return m_model.createIndex(m_currentIndex);
}
void PlaylistManager::setCurrentIndex(const QModelIndex &index)
{
if (index.isValid() && index.row() >= 0 && index.row() < totalCount()) {
setProperty("currentIndex", index.row());
} }
} }
int PlaylistManager::indexOf(const QString &filePath) QUrl PlaylistManager::urlByIndex(const QModelIndex &index)
{ {
const QUrl & url = QUrl::fromLocalFile(filePath); return m_model.data(index, PlaylistModel::UrlRole).toUrl();
return m_playlist.indexOf(url);
} }
int PlaylistManager::count() const QString PlaylistManager::localFileByIndex(const QModelIndex &index)
{ {
return m_playlist.count(); return urlByIndex(index).toLocalFile();
} }
std::tuple<int, QString> PlaylistManager::previousFile() const bool PlaylistManager::removeAt(const QModelIndex &index)
{ {
int count = m_playlist.count(); return m_model.removeAt(index.row());
if (count == 0) return std::make_tuple(-1, QString());
int index = m_currentIndex - 1 < 0 ? count - 1 : m_currentIndex - 1;
return std::make_tuple(index, m_playlist.at(index).toLocalFile());
} }
std::tuple<int, QString> PlaylistManager::nextFile() const void PlaylistManager::setAutoLoadFilterSuffixes(const QStringList &nameFilters)
{ {
int count = m_playlist.count(); m_model.setProperty("autoLoadFilterSuffixes", nameFilters);
if (count == 0) return std::make_tuple(-1, QString());
int index = m_currentIndex + 1 == count ? 0 : m_currentIndex + 1;
return std::make_tuple(index, m_playlist.at(index).toLocalFile());
}
std::tuple<int, QString> PlaylistManager::currentFile() const
{
if (m_playlist.count() == 0) return std::make_tuple(-1, QString());
return std::make_tuple(m_currentIndex, m_playlist.at(m_currentIndex).toLocalFile());
}
std::tuple<int, QUrl> PlaylistManager::currentFileUrl() const
{
if (m_playlist.count() == 0) return std::make_tuple(-1, QUrl());
return std::make_tuple(m_currentIndex, m_playlist.at(m_currentIndex));
} }
QList<QUrl> PlaylistManager::convertToUrlList(const QStringList &files) QList<QUrl> PlaylistManager::convertToUrlList(const QStringList &files)

View File

@ -1,10 +1,47 @@
// SPDX-FileCopyrightText: 2022 Gary Wang <wzc782970009@gmail.com> // SPDX-FileCopyrightText: 2024 Gary Wang <git@blumia.net>
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
#pragma once #pragma once
#include <QObject> #include <QAbstractListModel>
class PlaylistModel : public QAbstractListModel
{
Q_OBJECT
public:
enum PlaylistRole {
UrlRole = Qt::UserRole
};
Q_ENUM(PlaylistRole)
Q_PROPERTY(QStringList autoLoadFilterSuffixes MEMBER m_autoLoadSuffixes NOTIFY autoLoadFilterSuffixesChanged)
explicit PlaylistModel(QObject *parent = nullptr);
~PlaylistModel();
void setPlaylist(const QList<QUrl> & urls);
QModelIndex loadPlaylist(const QList<QUrl> & urls);
QModelIndex loadPlaylist(const QUrl & url);
QModelIndex appendToPlaylist(const QUrl & url);
bool removeAt(int index);
int indexOf(const QUrl & url) const;
QStringList autoLoadFilterSuffixes() const;
QModelIndex createIndex(int row) const;
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
signals:
void autoLoadFilterSuffixesChanged(QStringList suffixes);
private:
// model data
QList<QUrl> m_playlist;
// properties
QStringList m_autoLoadSuffixes = {};
// internal
QString m_currentDir;
};
class PlaylistManager : public QObject class PlaylistManager : public QObject
{ {
@ -12,47 +49,32 @@ class PlaylistManager : public QObject
public: public:
Q_PROPERTY(int currentIndex MEMBER m_currentIndex NOTIFY currentIndexChanged) Q_PROPERTY(int currentIndex MEMBER m_currentIndex NOTIFY currentIndexChanged)
enum PlaylistType { explicit PlaylistManager(QObject *parent = nullptr);
PL_USERPLAYLIST, // Regular playlist, managed by user.
PL_SAMEFOLDER // PlaylistManager managed playlist, loaded from files from same folder.
};
explicit PlaylistManager(PlaylistType type = PL_USERPLAYLIST, QObject *parent = nullptr);
~PlaylistManager(); ~PlaylistManager();
void setPlaylistType(PlaylistType type); const PlaylistModel * model() const;
PlaylistType playlistType() const;
QStringList autoLoadFilterSuffix() const; void setPlaylist(const QList<QUrl> & url);
void setAutoLoadFilterSuffix(const QStringList &nameFilters); QModelIndex loadPlaylist(const QList<QUrl> & urls);
void clear(); int totalCount() const;
QModelIndex previousIndex() const;
QModelIndex nextIndex() const;
QModelIndex curIndex() const;
void setCurrentIndex(const QModelIndex & index);
QUrl urlByIndex(const QModelIndex & index);
QString localFileByIndex(const QModelIndex & index);
bool removeAt(const QModelIndex & index);
void setPlaylist(const QList<QUrl> & urls); void setAutoLoadFilterSuffixes(const QStringList &nameFilters);
void setCurrentFile(const QString & filePath);
void setCurrentIndex(int index);
int appendFile(const QString & filePath);
void removeFileAt(int index);
int indexOf(const QString & filePath);
int count() const;
std::tuple<int, QString> previousFile() const;
std::tuple<int, QString> nextFile() const;
std::tuple<int, QString> currentFile() const;
std::tuple<int, QUrl> currentFileUrl() const;
static QList<QUrl> convertToUrlList(const QStringList & files); static QList<QUrl> convertToUrlList(const QStringList & files);
signals: signals:
void loaded(int length);
void currentIndexChanged(int index); void currentIndexChanged(int index);
void totalCountChanged(int count);
private: private:
QList<QUrl> m_playlist; int m_currentIndex;
PlaylistType m_type; PlaylistModel m_model;
QString m_currentDir;
int m_currentIndex = -1;
QStringList m_autoLoadSuffix = {};
}; };