From eb2e2e93f9ae0ca217460adb9162288bcc9340f9 Mon Sep 17 00:00:00 2001 From: Gary Wang Date: Sat, 20 Jul 2024 23:18:42 +0800 Subject: [PATCH] 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. --- app/mainwindow.cpp | 65 +++------ app/playlistmanager.cpp | 296 ++++++++++++++++++++++++---------------- app/playlistmanager.h | 88 +++++++----- 3 files changed, 252 insertions(+), 197 deletions(-) diff --git a/app/mainwindow.cpp b/app/mainwindow.cpp index 675eb22..58963a1 100644 --- a/app/mainwindow.cpp +++ b/app/mainwindow.cpp @@ -46,7 +46,7 @@ MainWindow::MainWindow(QWidget *parent) : FramelessWindow(parent) , m_am(new ActionManager) - , m_pm(new PlaylistManager(PlaylistManager::PL_SAMEFOLDER, this)) + , m_pm(new PlaylistManager(this)) { if (Settings::instance()->stayOnTop()) { this->setWindowFlag(Qt::WindowStaysOnTopHint); @@ -62,7 +62,7 @@ MainWindow::MainWindow(QWidget *parent) for (const QByteArray &item : QImageReader::supportedImageFormats()) { formatFilters.append(QStringLiteral("*.") % QString::fromLocal8Bit(item)); } - m_pm->setAutoLoadFilterSuffix(formatFilters); + m_pm->setAutoLoadFilterSuffixes(formatFilters); m_fadeOutAnimation = new QPropertyAnimation(this, "windowOpacity"); m_fadeOutAnimation->setDuration(300); @@ -142,19 +142,12 @@ MainWindow::MainWindow(QWidget *parent) m_gv->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_nextButton->setVisible(galleryFileCount > 1); }); - connect(m_pm, &PlaylistManager::currentIndexChanged, this, [this]() { - int index; - QUrl url; - std::tie(index, url) = m_pm->currentFileUrl(); - if (index != -1) { - this->setWindowTitle(url.fileName()); - } - }); + connect(m_pm, &PlaylistManager::currentIndexChanged, this, &MainWindow::galleryCurrent); QShortcut * fullscreenShorucut = new QShortcut(QKeySequence(QKeySequence::FullScreen), this); connect(fullscreenShorucut, &QShortcut::activated, @@ -187,7 +180,6 @@ void MainWindow::showUrls(const QList &urls) } else { m_graphicsView->showFileFromPath(urls.first().toLocalFile(), false); m_pm->setPlaylist(urls); - m_pm->setCurrentIndex(0); } } else { m_graphicsView->showText(tr("File url list is empty")); @@ -214,7 +206,7 @@ void MainWindow::initWindowSize() void MainWindow::adjustWindowSizeBySceneRect() { - if (m_pm->count() < 1) return; + if (m_pm->totalCount() < 1) return; QSize sceneSize = m_graphicsView->sceneRect().toRect().size(); QSize sceneSizeWithMargins = sceneSize + QSize(130, 125); @@ -245,42 +237,31 @@ void MainWindow::adjustWindowSizeBySceneRect() // can be empty if it is NOT from a local file. QUrl MainWindow::currentImageFileUrl() const { - QUrl url; - std::tie(std::ignore, url) = m_pm->currentFileUrl(); - - return url; + return m_pm->urlByIndex(m_pm->curIndex()); } void MainWindow::clearGallery() { - m_pm->clear(); + m_pm->setPlaylist({}); } void MainWindow::loadGalleryBySingleLocalFile(const QString &path) { - m_pm->setCurrentFile(path); + m_pm->loadPlaylist({QUrl::fromLocalFile(path)}); } void MainWindow::galleryPrev() { - int index; - QString filePath; - std::tie(index, filePath) = m_pm->previousFile(); - - if (index >= 0) { - m_graphicsView->showFileFromPath(filePath, false); + QModelIndex index = m_pm->previousIndex(); + if (index.isValid()) { m_pm->setCurrentIndex(index); } } void MainWindow::galleryNext() { - int index; - QString filePath; - std::tie(index, filePath) = m_pm->nextFile(); - - if (index >= 0) { - m_graphicsView->showFileFromPath(filePath, false); + QModelIndex index = m_pm->nextIndex(); + if (index.isValid()) { 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. void MainWindow::galleryCurrent() { - int index; - QString filePath; - std::tie(index, filePath) = m_pm->currentFile(); - - if (index >= 0) { - m_graphicsView->showFileFromPath(filePath, false); + QModelIndex index = m_pm->curIndex(); + if (index.isValid()) { + setWindowTitle(m_pm->urlByIndex(index).fileName()); + m_graphicsView->showFileFromPath(m_pm->localFileByIndex(index), false); } else { m_graphicsView->showText(QCoreApplication::translate("GraphicsScene", "Drag image here")); } @@ -721,18 +700,16 @@ void MainWindow::on_actionPaste_triggered() } else if (clipboardFileUrl.isValid()) { QString localFile(clipboardFileUrl.toLocalFile()); m_graphicsView->showFileFromPath(localFile, true); - m_pm->setCurrentFile(localFile); + m_pm->loadPlaylist({clipboardFileUrl}); } } void MainWindow::on_actionTrash_triggered() { - int currentFileIndex; - QUrl currentFileUrl; - std::tie(currentFileIndex, currentFileUrl) = m_pm->currentFileUrl(); - if (!currentFileUrl.isLocalFile()) return; + QModelIndex index = m_pm->curIndex(); + if (!m_pm->urlByIndex(index).isLocalFile()) return; - QFile file(currentFileUrl.toLocalFile()); + QFile file(m_pm->localFileByIndex(index)); QFileInfo fileInfo(file.fileName()); 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", tr("Move to trash failed, it might caused by file permission issue, file system limitation, or platform limitation.")); } else { - m_pm->removeFileAt(currentFileIndex); + m_pm->removeAt(index); galleryCurrent(); } } diff --git a/app/playlistmanager.cpp b/app/playlistmanager.cpp index 5a33024..ac1b9ae 100644 --- a/app/playlistmanager.cpp +++ b/app/playlistmanager.cpp @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2022 Gary Wang +// SPDX-FileCopyrightText: 2024 Gary Wang // // SPDX-License-Identifier: MIT @@ -9,166 +9,222 @@ #include #include -PlaylistManager::PlaylistManager(PlaylistType type, QObject *parent) - : QObject(parent) - , m_type(type) +PlaylistModel::PlaylistModel(QObject *parent) + : QAbstractListModel(parent) { } +PlaylistModel::~PlaylistModel() +{ + +} + +void PlaylistModel::setPlaylist(const QList &urls) +{ + beginResetModel(); + m_playlist = urls; + endResetModel(); +} + +QModelIndex PlaylistModel::loadPlaylist(const QList & 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 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() { } -void PlaylistManager::setPlaylistType(PlaylistManager::PlaylistType type) +const PlaylistModel *PlaylistManager::model() const { - m_type = type; -} - -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(); + return &m_model; } void PlaylistManager::setPlaylist(const QList &urls) { - m_playlist = urls; + m_model.setPlaylist(urls); } -void PlaylistManager::setCurrentFile(const QString & filePath) +QModelIndex PlaylistManager::loadPlaylist(const QList &urls) { - QFileInfo info(filePath); - QDir dir(info.path()); - QString && currentFileName = info.fileName(); - - 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()); + QModelIndex idx = m_model.loadPlaylist(urls); + setProperty("currentIndex", idx.row()); + return idx; } -void PlaylistManager::setCurrentIndex(int index) +int PlaylistManager::totalCount() const { - if (index < 0 || index >= m_playlist.count()) return; - m_currentIndex = index; - emit currentIndexChanged(m_currentIndex); + return m_model.rowCount(); } -int PlaylistManager::appendFile(const QString &filePath) +QModelIndex PlaylistManager::previousIndex() const { - int index = m_playlist.length(); - m_playlist.append(QUrl::fromLocalFile(filePath)); + int count = totalCount(); + 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 -void PlaylistManager::removeFileAt(int index) +QModelIndex PlaylistManager::nextIndex() const { - m_playlist.removeAt(index); + int count = totalCount(); + if (count == 0) return QModelIndex(); - if (m_playlist.count() <= m_currentIndex) { - m_currentIndex--; + return m_model.createIndex(m_currentIndex + 1 == count ? 0 : m_currentIndex + 1); +} + +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_playlist.indexOf(url); + return m_model.data(index, PlaylistModel::UrlRole).toUrl(); } -int PlaylistManager::count() const +QString PlaylistManager::localFileByIndex(const QModelIndex &index) { - return m_playlist.count(); + return urlByIndex(index).toLocalFile(); } -std::tuple PlaylistManager::previousFile() const +bool PlaylistManager::removeAt(const QModelIndex &index) { - int count = m_playlist.count(); - 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()); + return m_model.removeAt(index.row()); } -std::tuple PlaylistManager::nextFile() const +void PlaylistManager::setAutoLoadFilterSuffixes(const QStringList &nameFilters) { - int count = m_playlist.count(); - 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 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 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)); + m_model.setProperty("autoLoadFilterSuffixes", nameFilters); } QList PlaylistManager::convertToUrlList(const QStringList &files) diff --git a/app/playlistmanager.h b/app/playlistmanager.h index 07f17a2..80ea5f3 100644 --- a/app/playlistmanager.h +++ b/app/playlistmanager.h @@ -1,10 +1,47 @@ -// SPDX-FileCopyrightText: 2022 Gary Wang +// SPDX-FileCopyrightText: 2024 Gary Wang // // SPDX-License-Identifier: MIT #pragma once -#include +#include + +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 & urls); + QModelIndex loadPlaylist(const QList & 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 m_playlist; + // properties + QStringList m_autoLoadSuffixes = {}; + // internal + QString m_currentDir; +}; class PlaylistManager : public QObject { @@ -12,47 +49,32 @@ class PlaylistManager : public QObject public: Q_PROPERTY(int currentIndex MEMBER m_currentIndex NOTIFY currentIndexChanged) - enum PlaylistType { - 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); + explicit PlaylistManager(QObject *parent = nullptr); ~PlaylistManager(); - void setPlaylistType(PlaylistType type); - PlaylistType playlistType() const; + const PlaylistModel * model() const; - QStringList autoLoadFilterSuffix() const; - void setAutoLoadFilterSuffix(const QStringList &nameFilters); + void setPlaylist(const QList & url); + QModelIndex loadPlaylist(const QList & 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 & urls); - 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 previousFile() const; - std::tuple nextFile() const; - std::tuple currentFile() const; - std::tuple currentFileUrl() const; + void setAutoLoadFilterSuffixes(const QStringList &nameFilters); static QList convertToUrlList(const QStringList & files); signals: - void loaded(int length); void currentIndexChanged(int index); + void totalCountChanged(int count); private: - QList m_playlist; - PlaylistType m_type; - QString m_currentDir; - int m_currentIndex = -1; - QStringList m_autoLoadSuffix = {}; + int m_currentIndex; + PlaylistModel m_model; }; -