diff --git a/app/actionmanager.cpp b/app/actionmanager.cpp index 9f455f5..738ad11 100644 --- a/app/actionmanager.cpp +++ b/app/actionmanager.cpp @@ -179,6 +179,11 @@ void ActionManager::updateActionState(PlaylistManager* pm, GraphicsView* gv) const auto* gs = gv->scene(); const auto* settings = Settings::instance(); + // Update copy actions + bool canCopy = pm->currentIndex().has_value(); + actionCopyPixmap->setEnabled(canCopy); + actionCopyFilePath->setEnabled(canCopy); + // Update Prev/Next Picture State const int galleryFileCount = pm->totalCount(); const bool loopGallery = settings->loopGallery(); @@ -186,11 +191,9 @@ void ActionManager::updateActionState(PlaylistManager* pm, GraphicsView* gv) actionNextPicture->setEnabled(galleryFileCount > 1 && (loopGallery || !pm->isLastIndex())); // Update Avoid Reset Transform - actionToggleAvoidResetTransform->setCheckable(gv->avoidResetTransform()); - + actionToggleAvoidResetTransform->setChecked(gv->isAvoidResetTransform()); // Update Checkerboard - actionToggleCheckerboard->setChecked(gv->checkerboard()); - + actionToggleCheckerboard->setChecked(gv->isCheckerboard()); // Update Animation Graphics Action bool canPauseAnimation = gs->canPauseAnimation(); actionTogglePauseAnimation->setEnabled(canPauseAnimation); @@ -202,7 +205,10 @@ void ActionManager::updateActionState(PlaylistManager* pm, GraphicsView* gv) bool canSkipAnimation = gs->canSkipAnimationFrame(); actionAnimationNextFrame->setEnabled(canSkipAnimation); - // TODO: Add more... - + // Update trash, located in explorer and properties. + bool hasLoadedFile = pm->currentIndex().has_value(); + actionTrash->setEnabled(hasLoadedFile); + actionLocateInFileManager->setEnabled(hasLoadedFile); + actionProperties->setEnabled(hasLoadedFile); } diff --git a/app/graphicsscene.cpp b/app/graphicsscene.cpp index 937e646..53b79d5 100644 --- a/app/graphicsscene.cpp +++ b/app/graphicsscene.cpp @@ -129,7 +129,7 @@ private: GraphicsScene::GraphicsScene(QObject *parent) : QGraphicsScene(parent) { - showText(tr("Drag image here")); + showText(tr("Drag image here", "The same meaning of MainWindow's hint.")); connect(qGuiApp->styleHints(), &QStyleHints::colorSchemeChanged, this, &GraphicsScene::updateStyle); } diff --git a/app/graphicsview.cpp b/app/graphicsview.cpp index e6d2ff8..cd9bfac 100644 --- a/app/graphicsview.cpp +++ b/app/graphicsview.cpp @@ -320,7 +320,7 @@ void GraphicsView::setEnableAutoFitToScreen(bool enable) m_enableFitToScreen = enable; } -bool GraphicsView::avoidResetTransform() const +bool GraphicsView::isAvoidResetTransform() const { return m_avoidResetTransform; } @@ -330,7 +330,7 @@ void GraphicsView::setAvoidResetTransform(bool avoidReset) m_avoidResetTransform = avoidReset; } -bool GraphicsView::checkerboard() const { +bool GraphicsView::isCheckerboard() const { return m_checkerboardEnabled; } diff --git a/app/graphicsview.h b/app/graphicsview.h index b1b184e..e601ede 100644 --- a/app/graphicsview.h +++ b/app/graphicsview.h @@ -40,10 +40,10 @@ public: bool isSceneBiggerThanView() const; void setEnableAutoFitToScreen(bool enable = true); - bool avoidResetTransform() const; + bool isAvoidResetTransform() const; void setAvoidResetTransform(bool avoidReset); - bool checkerboard() const; + bool isCheckerboard() const; void toggleCheckerboard(bool invertCheckerboardColor = false); static QTransform resetScale(const QTransform & orig); diff --git a/app/main.cpp b/app/main.cpp index c10947d..9c331a4 100644 --- a/app/main.cpp +++ b/app/main.cpp @@ -4,7 +4,6 @@ #include "mainwindow.h" -#include "playlistmanager.h" #include "settings.h" #ifdef Q_OS_MACOS @@ -68,21 +67,19 @@ int main(int argc, char *argv[]) a.installEventFilter(fileOpenEventHandler); a.connect(fileOpenEventHandler, &FileOpenEventHandler::fileOpen, [&w](const QUrl & url){ if (w.isHidden()) { - w.setWindowOpacity(1); w.showNormal(); } else { w.activateWindow(); } - w.showUrls({url}); + w.showFiles({url}); w.initWindowSize(); }); // Handle dock icon clicks to show hidden window a.connect(&a, &QApplication::applicationStateChanged, [&w](Qt::ApplicationState state) { if (state == Qt::ApplicationActive && w.isHidden()) { - w.showUrls({}); - w.galleryCurrent(true, true); - w.setWindowOpacity(1); + w.showFiles({}); + w.galleryCurrent(false); w.showNormal(); w.raise(); w.activateWindow(); @@ -90,12 +87,8 @@ int main(int argc, char *argv[]) }); #endif // Q_OS_MACOS - QStringList urlStrList = parser.positionalArguments(); - QList && urlList = PlaylistManager::convertToUrlList(urlStrList); - - if (!urlList.isEmpty()) { - w.showUrls(urlList); - } + QStringList fileList = parser.positionalArguments(); + w.showFiles(fileList); w.initWindowSize(); diff --git a/app/mainwindow.cpp b/app/mainwindow.cpp index f55ec34..46cac7d 100644 --- a/app/mainwindow.cpp +++ b/app/mainwindow.cpp @@ -59,7 +59,7 @@ MainWindow::MainWindow(QWidget *parent) this->setWindowIcon(QIcon(u":/icons/app-icon.svg"_s)); this->setAcceptDrops(true); - m_pm->setAutoLoadFilterSuffixes(supportedImageFormats()); + m_pm->setAllowedSuffixes(supportedImageFormats()); GraphicsScene * scene = new GraphicsScene(this); @@ -104,13 +104,13 @@ MainWindow::MainWindow(QWidget *parent) m_am->actionToggleCheckerboard, }, this); - connect(m_pm, &PlaylistManager::totalCountChanged, this, &MainWindow::updateGalleryButtonsVisibility); + connect(m_pm, &PlaylistManager::playlistChanged, this, std::bind(&MainWindow::updateActionState, this)); - connect(m_pm->model(), &PlaylistModel::modelReset, this, std::bind(&MainWindow::galleryCurrent, this, false, false)); - connect(m_pm, &PlaylistManager::currentIndexChanged, this, std::bind(&MainWindow::galleryCurrent, this, true, false)); + connect(m_pm, &PlaylistManager::playlistChanged, this, std::bind(&MainWindow::galleryCurrent, this, false)); + connect(m_pm, &PlaylistManager::currentIndexChanged, this, std::bind(&MainWindow::galleryCurrent, this, false)); connect(m_fileSystemWatcher, &QFileSystemWatcher::fileChanged, this, [this](){ - QTimer::singleShot(500, this, std::bind(&MainWindow::galleryCurrent, this, false, true)); + QTimer::singleShot(500, this, std::bind(&MainWindow::galleryCurrent, this, true)); }); QShortcut * fullscreenShorucut = new QShortcut(QKeySequence(QKeySequence::FullScreen), this); @@ -130,26 +130,15 @@ MainWindow::~MainWindow() } -void MainWindow::showUrls(const QList &urls) +void MainWindow::showFiles(const QStringList &files) { - if (!urls.isEmpty()) { - const QUrl & firstUrl = urls.first(); - if (urls.count() == 1) { - const QString lowerCaseUrlPath(firstUrl.path().toLower()); - if (lowerCaseUrlPath.endsWith(".m3u8") || lowerCaseUrlPath.endsWith(".m3u")) { - m_pm->loadM3U8Playlist(firstUrl); - galleryCurrent(true, true); - return; - } - } - m_gv->showFileFromPath(firstUrl.toLocalFile()); - m_pm->loadPlaylist(urls); - } else { - m_gv->showText(tr("File url list is empty")); - m_pm->setPlaylist(urls); - return; - } + m_pm->loadPlaylist(files); + m_nav->fitInView(m_nav->sceneRect(), Qt::KeepAspectRatio); +} +void MainWindow::showFiles(const QList& urls) +{ + m_pm->loadPlaylist(urls); m_nav->fitInView(m_nav->sceneRect(), Qt::KeepAspectRatio); } @@ -201,15 +190,9 @@ void MainWindow::adjustWindowSizeBySceneRect() } } -// can be empty if it is NOT from a local file. -QUrl MainWindow::currentImageFileUrl() const -{ - return m_pm->urlByIndex(m_pm->curIndex()); -} - void MainWindow::clearGallery() { - m_pm->setPlaylist({}); + m_pm->clearPlaylist(); } void MainWindow::galleryPrev() @@ -217,10 +200,9 @@ void MainWindow::galleryPrev() const bool loopGallery = Settings::instance()->loopGallery(); if (!loopGallery && m_pm->isFirstIndex()) return; - QModelIndex index = m_pm->previousIndex(); - if (index.isValid()) { - m_pm->setCurrentIndex(index); - m_gv->showFileFromPath(m_pm->localFileByIndex(index)); + auto index = m_pm->previousIndex(); + if (index.has_value()) { + m_pm->setCurrentIndex(*index); } } @@ -229,31 +211,29 @@ void MainWindow::galleryNext() const bool loopGallery = Settings::instance()->loopGallery(); if (!loopGallery && m_pm->isLastIndex()) return; - QModelIndex index = m_pm->nextIndex(); - if (index.isValid()) { - m_pm->setCurrentIndex(index); - m_gv->showFileFromPath(m_pm->localFileByIndex(index)); + auto index = m_pm->nextIndex(); + if (index.has_value()) { + m_pm->setCurrentIndex(*index); } } -// Only use this to update minor information. -void MainWindow::galleryCurrent(bool showLoadImageHintWhenEmpty, bool reloadImage) +void MainWindow::galleryCurrent(bool fromFileWatcher) { - QModelIndex index = m_pm->curIndex(); - bool shouldResetfileWatcher = true; - if (index.isValid()) { - const QString & localFilePath(m_pm->localFileByIndex(index)); - if (reloadImage) m_gv->showFileFromPath(localFilePath); - shouldResetfileWatcher = !updateFileWatcher(localFilePath); - setWindowTitle(m_pm->urlByIndex(index).fileName()); - } else if (showLoadImageHintWhenEmpty && m_pm->totalCount() <= 0) { - m_gv->showText(QCoreApplication::translate("GraphicsScene", "Drag image here")); + auto index = m_pm->currentIndex(); + if (index.has_value()) { + QString localFilePath = m_pm->localFileByIndex(*index); + m_gv->showFileFromPath(localFilePath); + if (!fromFileWatcher) { + updateFileWatcher(localFilePath); + setWindowTitle(m_pm->urlByIndex(*index).fileName()); + } + } else { + m_gv->showText(tr("Drag image here")); setWindowTitle(QString()); + updateFileWatcher(); } - updateGalleryButtonsVisibility(); - - if (shouldResetfileWatcher) updateFileWatcher(); + updateActionState(); } QStringList MainWindow::supportedImageFormats() @@ -361,23 +341,10 @@ void MainWindow::resizeEvent(QResizeEvent *event) void MainWindow::contextMenuEvent(QContextMenuEvent *event) { - // TODO: Update this function. QMenu * menu = new QMenu; - // TODO: currentImageFileUrl() may be redundant. - QMenu * copyMenu = new QMenu(tr("&Copy")); - QUrl currentFileUrl = currentImageFileUrl(); - - copyMenu->setIcon(QIcon::fromTheme(u"edit-copy"_s)); - copyMenu->addAction(m_am->actionCopyPixmap); - if (currentFileUrl.isValid()) { - copyMenu->addAction(m_am->actionCopyFilePath); - } - if (copyMenu->actions().count() == 1) { - menu->addActions(copyMenu->actions()); - } else { - menu->addMenu(copyMenu); - } + menu->addAction(m_am->actionCopyPixmap); + menu->addAction(m_am->actionCopyFilePath); menu->addSeparator(); @@ -402,26 +369,16 @@ void MainWindow::contextMenuEvent(QContextMenuEvent *event) menu->addSeparator(); - // TODO: Move checked update to action manager. - QAction * avoidResetTransform = m_am->actionToggleAvoidResetTransform; - avoidResetTransform->setCheckable(true); - avoidResetTransform->setChecked(m_gv->avoidResetTransform()); - menu->addAction(avoidResetTransform); + menu->addAction(m_am->actionToggleAvoidResetTransform); menu->addAction(m_am->actionToggleCheckerboard); - QAction * pauseAnimation = m_am->actionTogglePauseAnimation; - pauseAnimation->setCheckable(true); - pauseAnimation->setChecked(m_gv->scene()->pauseAnimation()); - menu->addAction(pauseAnimation); + menu->addAction(m_am->actionTogglePauseAnimation); menu->addAction(m_am->actionAnimationNextFrame); - if (currentFileUrl.isValid()) { - menu->addSeparator(); - if (currentFileUrl.isLocalFile()) { - menu->addAction(m_am->actionTrash); - menu->addAction(m_am->actionLocateInFileManager); - } - menu->addAction(m_am->actionProperties); - } + menu->addSeparator(); + + menu->addAction(m_am->actionTrash); + menu->addAction(m_am->actionLocateInFileManager); + menu->addAction(m_am->actionProperties); menu->addSeparator(); @@ -430,14 +387,13 @@ void MainWindow::contextMenuEvent(QContextMenuEvent *event) menu->exec(mapToGlobal(event->pos())); menu->deleteLater(); - copyMenu->deleteLater(); return FramelessWindow::contextMenuEvent(event); } void MainWindow::dragEnterEvent(QDragEnterEvent *event) { - if (event->mimeData()->hasUrls() || event->mimeData()->hasImage() || event->mimeData()->hasText()) { + if (event->mimeData()->hasUrls()) { event->acceptProposedAction(); } else { event->ignore(); @@ -456,24 +412,8 @@ void MainWindow::dropEvent(QDropEvent *event) event->acceptProposedAction(); const QMimeData * mimeData = event->mimeData(); - if (mimeData->hasUrls()) { - const QList &urls = mimeData->urls(); - if (urls.isEmpty()) { - m_gv->showText(tr("File url list is empty")); - } else { - showUrls(urls); - } - } else if (mimeData->hasImage()) { - QImage img = qvariant_cast(mimeData->imageData()); - QPixmap pixmap = QPixmap::fromImage(img); - if (pixmap.isNull()) { - m_gv->showText(tr("Image data is invalid")); - } else { - m_gv->showImage(pixmap); - } - } else if (mimeData->hasText()) { - m_gv->showText(mimeData->text()); + showFiles(mimeData->urls()); } else { m_gv->showText(tr("Not supported mimedata: %1").arg(mimeData->formats().first())); } @@ -504,25 +444,7 @@ void MainWindow::updateWidgetsPosition() void MainWindow::toggleAvoidResetTransform() { - m_gv->setAvoidResetTransform(!m_gv->avoidResetTransform()); -} - -bool MainWindow::canPaste() const -{ - const QMimeData * clipboardData = QApplication::clipboard()->mimeData(); - if (clipboardData->hasImage()) { - return true; - } else if (clipboardData->hasText()) { - QString clipboardText(clipboardData->text()); - if (clipboardText.startsWith("PICTURE:")) { - QString maybeFilename(clipboardText.mid(8)); - if (QFile::exists(maybeFilename)) { - return true; - } - } - } - - return false; + m_gv->setAvoidResetTransform(!m_gv->isAvoidResetTransform()); } void MainWindow::quitAppAction(bool force) @@ -563,11 +485,11 @@ void MainWindow::on_actionCopyPixmap_triggered() void MainWindow::on_actionCopyFilePath_triggered() { - QUrl currentFileUrl(currentImageFileUrl()); - if (currentFileUrl.isValid()) { - QClipboard *cb = QApplication::clipboard(); - cb->setText(currentFileUrl.toLocalFile()); - } + auto index = m_pm->currentIndex(); + if (!index.has_value()) return; + + QClipboard *cb = QApplication::clipboard(); + cb->setText(m_pm->localFileByIndex(*index)); } void MainWindow::on_actionPrevPicture_triggered() @@ -673,15 +595,16 @@ void MainWindow::on_actionTrash_triggered() QMessageBox::warning(this, tr("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->removeAt(index); - galleryCurrent(true, true); + m_pm->removeFromPlaylist(*index); } } } void MainWindow::on_actionLocateInFileManager_triggered() { - QUrl currentFileUrl = currentImageFileUrl(); + auto index = m_pm->currentIndex(); + if (!index.has_value()) return; + QUrl currentFileUrl = m_pm->urlByIndex(*index); if (!currentFileUrl.isValid()) return; QFileInfo fileInfo(currentFileUrl.toLocalFile()); @@ -716,7 +639,9 @@ void MainWindow::on_actionLocateInFileManager_triggered() void MainWindow::on_actionProperties_triggered() { - QUrl currentFileUrl = currentImageFileUrl(); + auto index = m_pm->currentIndex(); + if (!index.has_value()) return; + QUrl currentFileUrl = m_pm->urlByIndex(*index); if (!currentFileUrl.isValid()) return; MetadataModel * md = new MetadataModel(); @@ -750,3 +675,7 @@ bool MainWindow::updateFileWatcher(const QString &basePath) if (!basePath.isEmpty()) return m_fileSystemWatcher->addPath(basePath); return false; } + +void MainWindow::updateActionState() { + m_am->updateActionState(m_pm, m_gv); +} diff --git a/app/mainwindow.h b/app/mainwindow.h index 039c3bc..9d83d85 100644 --- a/app/mainwindow.h +++ b/app/mainwindow.h @@ -29,15 +29,15 @@ public: explicit MainWindow(QWidget *parent = nullptr); ~MainWindow() override; - void showUrls(const QList &urls); + void showFiles(const QStringList& files); + void showFiles(const QList& urls); void initWindowSize(); void adjustWindowSizeBySceneRect(); - QUrl currentImageFileUrl() const; void clearGallery(); void galleryPrev(); void galleryNext(); - void galleryCurrent(bool showLoadImageHintWhenEmpty, bool reloadImage); + void galleryCurrent(bool fromFileWatcher); static QStringList supportedImageFormats(); @@ -57,7 +57,6 @@ protected slots: void closeWindow(); void updateWidgetsPosition(); void toggleAvoidResetTransform(); - bool canPaste() const; void quitAppAction(bool force = false); void toggleFullscreen(); void toggleMaximize(); @@ -98,6 +97,7 @@ private slots: private: bool updateFileWatcher(const QString & basePath = QString()); + void updateActionState(); private: ActionManager *m_am; diff --git a/app/playlistmanager.cpp b/app/playlistmanager.cpp index e3dafd7..0fad31d 100644 --- a/app/playlistmanager.cpp +++ b/app/playlistmanager.cpp @@ -9,152 +9,13 @@ #include #include -PlaylistModel::PlaylistModel(QObject *parent) - : QAbstractListModel(parent) -{ - -} - -PlaylistModel::~PlaylistModel() -= default; - -void PlaylistModel::setPlaylist(const QList &urls) -{ - beginResetModel(); - m_playlist = urls; - endResetModel(); -} - -QModelIndex PlaylistModel::loadPlaylist(const QList & urls) -{ - if (urls.isEmpty()) return {}; - if (urls.count() == 1) { - return loadPlaylist(urls.constFirst()); - } else { - setPlaylist(urls); - return index(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 idx = indexOf(url); - return idx == -1 ? appendToPlaylist(url) : index(idx); - } - - 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 idx = -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) { - idx = i; - } - } - if (idx == -1) { - idx = playlist.count(); - playlist.append(url); - } - m_currentDir = dir.path(); - - setPlaylist(playlist); - - return index(idx); -} - -QModelIndex PlaylistModel::appendToPlaylist(const QUrl &url) -{ - const int lastIndex = rowCount(); - beginInsertRows(QModelIndex(), lastIndex, lastIndex); - m_playlist.append(url); - endInsertRows(); - return index(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); -} - -QUrl PlaylistModel::urlByIndex(int index) const -{ - return m_playlist.value(index); -} - -QStringList PlaylistModel::autoLoadFilterSuffixes() const -{ - return m_autoLoadSuffixes; -} - -QHash PlaylistModel::roleNames() const -{ - QHash result = QAbstractListModel::roleNames(); - result.insert(UrlRole, "url"); - return result; -} - -int PlaylistModel::rowCount(const QModelIndex &parent) const -{ - return m_playlist.count(); -} - -QVariant PlaylistModel::data(const QModelIndex &index, int role) const -{ - if (!index.isValid()) return {}; - - switch (role) { - case Qt::DisplayRole: - return m_playlist.at(index.row()).fileName(); - case UrlRole: - return m_playlist.at(index.row()); - } - - return {}; -} - PlaylistManager::PlaylistManager(QObject *parent) : QObject(parent) + , m_currentIndex(std::nullopt) + , m_playlist() + , m_allowedSuffixes() { - 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() @@ -162,35 +23,106 @@ PlaylistManager::~PlaylistManager() } -PlaylistModel *PlaylistManager::model() -{ - return &m_model; +// region: Allowed Suffixes Operations + +const QStringList& PlaylistManager::allowedSuffixes() const { + return m_allowedSuffixes; } -void PlaylistManager::setPlaylist(const QList &urls) -{ - m_model.setPlaylist(urls); +void PlaylistManager::setAllowedSuffixes(const QStringList& nameFilters) { + m_allowedSuffixes = nameFilters; + emit allowedSuffixesChanged(m_allowedSuffixes); } -QModelIndex PlaylistManager::loadPlaylist(const QList &urls) -{ - QModelIndex idx = m_model.loadPlaylist(urls); - setProperty("currentIndex", idx.row()); - return idx; +// endregion + +// region: Playlist Loader and Reset + +bool PlaylistManager::loadPlaylist(const QStringList& files) { + // Convert string list to url list first + QList urls; + for (const QString & file : std::as_const(files)) { + urls.append(urlFromString(file)); + } + + return loadPlaylist(urls); } -QModelIndex PlaylistManager::loadPlaylist(const QUrl &url) -{ - QModelIndex idx = m_model.loadPlaylist(url); - setProperty("currentIndex", idx.row()); - return idx; +bool PlaylistManager::loadPlaylist(const QList& urls) { + // Check its count, and check M3U8 playlist for single file case. + if (urls.isEmpty()) { + clearPlaylist(); + return true; + } else if (urls.count() == 1) { + const QUrl& firstUrl = urls.first(); + const QString lowerCaseUrlPath(firstUrl.path().toLower()); + if (lowerCaseUrlPath.endsWith(".m3u8") || lowerCaseUrlPath.endsWith(".m3u")) { + return loadM3U8Playlist(firstUrl); + } else { + return loadSingleFilePlaylist(firstUrl); + } + } else { + return loadMultipleFilesPlaylist(std::move(urls)); + } } -QModelIndex PlaylistManager::loadM3U8Playlist(const QUrl &url) -{ +bool PlaylistManager::loadSingleFilePlaylist(const QUrl& url) { + // Check URL at first + if (!checkUrl(url)) + return false; + + QFileInfo info(url.toLocalFile()); + QDir dir(info.path()); + QString currentFileName = info.fileName(); + + QStringList entryList = dir.entryList( + m_allowedSuffixes, QDir::Files | QDir::NoSymLinks, QDir::NoSort); + + QCollator collator; + collator.setNumericMode(true); + + std::sort(entryList.begin(), entryList.end(), collator); + + QList playlist; + std::optional idx = std::nullopt; + for (qsizetype i = 0; i < entryList.count(); i++) { + const QString &fileName = entryList.at(i); + const QString &oneEntry = dir.absoluteFilePath(fileName); + const QUrl &url = QUrl::fromLocalFile(oneEntry); + if (!checkUrl(url)) { + return false; + } + playlist.append(url); + if (fileName == currentFileName) { + idx = i; + } + } + + setPlaylist(std::move(playlist), idx); + return true; +} + +bool PlaylistManager::loadMultipleFilesPlaylist(const QList&& urls) { + // Check all of them are valid + for (const auto& url : std::as_const(urls)) { + if (!checkUrl(url)) { + return false; + } + } + + // Set playlist and return + setPlaylist(std::move(urls)); + return true; +} + +bool PlaylistManager::loadM3U8Playlist(const QUrl& url) { + // Check URL at first + if (!checkUrl(url)) + return false; + + QList urls; QFile file(url.toLocalFile()); if (file.open(QIODevice::ReadOnly | QIODevice::Text)) { - QList urls; while (!file.atEnd()) { QString line = file.readLine(); if (line.startsWith('#')) { @@ -198,86 +130,185 @@ QModelIndex PlaylistManager::loadM3U8Playlist(const QUrl &url) } QFileInfo fileInfo(file); QUrl item = QUrl::fromUserInput(line, fileInfo.absolutePath()); + if (!checkUrl(item)) { + return false; + } urls.append(item); } - return loadPlaylist(urls); } else { - return {}; + return false; + } + + setPlaylist(std::move(urls)); + return true; +} + +void PlaylistManager::setPlaylist(const QList&& urls, std::optional indexHint) { + // Setup playlist + m_playlist = std::move(urls); + + // Set index with hint + if (indexHint.has_value()) { + m_currentIndex = *indexHint; + } + sanitizeIndex(); + + // Raise signal + emit playlistChanged(totalCount()); +} + +void PlaylistManager::clearPlaylist() { + m_playlist.clear(); + sanitizeIndex(); + emit playlistChanged(this->totalCount()); +} + +// endregion + +// region: Playlist Operations + +void PlaylistManager::appendToPlaylist(const QString &file) +{ + return appendToPlaylist(urlFromString(file)); +} + +void PlaylistManager::appendToPlaylist(const QUrl &url) +{ + m_playlist.append(url); + sanitizeIndex(); + emit playlistChanged(totalCount()); +} + +bool PlaylistManager::insertIntoPlaylist(qsizetype index, const QString &file) +{ + return insertIntoPlaylist(index, urlFromString(file)); +} + +bool PlaylistManager::insertIntoPlaylist(qsizetype index, const QUrl &url) +{ + if (index < 0 || index > totalCount()) { + return false; + } else { + m_playlist.insert(index, url); + sanitizeIndex(); + emit playlistChanged(totalCount()); + return true; } } -int PlaylistManager::totalCount() const +bool PlaylistManager::removeFromPlaylist(qsizetype index) { - return m_model.rowCount(); + if (index < 0 || index >= totalCount()) { + return false; + } else { + m_playlist.removeAt(index); + sanitizeIndex(); + emit playlistChanged(totalCount()); + return true; + } } -QModelIndex PlaylistManager::previousIndex() const -{ - int count = totalCount(); - if (count == 0) return {}; +// endregion - return m_model.index(isFirstIndex() ? count - 1 : m_currentIndex - 1); +// region: Index Operations + +qsizetype PlaylistManager::totalCount() const +{ + return m_playlist.count(); } -QModelIndex PlaylistManager::nextIndex() const +std::optional PlaylistManager::previousIndex() const { - int count = totalCount(); - if (count == 0) return {}; - - return m_model.index(isLastIndex() ? 0 : m_currentIndex + 1); + if (!m_currentIndex.has_value() || totalCount() <= 0) return std::nullopt; + return (isFirstIndex() ? totalCount() - 1 : *m_currentIndex - 1); } -QModelIndex PlaylistManager::curIndex() const +std::optional PlaylistManager::nextIndex() const { - return m_model.index(m_currentIndex); + if (!m_currentIndex.has_value() || totalCount() <= 0) return std::nullopt; + return (isLastIndex() ? 0 : *m_currentIndex + 1); } bool PlaylistManager::isFirstIndex() const { - return m_currentIndex == 0; + return m_currentIndex.has_value() && *m_currentIndex == 0; } bool PlaylistManager::isLastIndex() const { - return m_currentIndex + 1 == totalCount(); + return m_currentIndex.has_value() && *m_currentIndex + 1 == totalCount(); } -void PlaylistManager::setCurrentIndex(const QModelIndex &index) +std::optional PlaylistManager::currentIndex() const { - if (index.isValid() && index.row() >= 0 && index.row() < totalCount()) { - setProperty("currentIndex", index.row()); + return m_currentIndex; +} + +bool PlaylistManager::setCurrentIndex(qsizetype index) +{ + if (!m_currentIndex.has_value()) { + return false; } + if (index < 0 || index >= totalCount()) { + return false; + } + if (*m_currentIndex == index) { + return true; + } + + m_currentIndex = index; + emit currentIndexChanged(index); + return true; } -QUrl PlaylistManager::urlByIndex(const QModelIndex &index) +QUrl PlaylistManager::urlByIndex(qsizetype index) const { - return m_model.urlByIndex(index.row()); + return m_playlist.value(index); } -QString PlaylistManager::localFileByIndex(const QModelIndex &index) +QString PlaylistManager::localFileByIndex(qsizetype index) const { return urlByIndex(index).toLocalFile(); } -bool PlaylistManager::removeAt(const QModelIndex &index) -{ - return m_model.removeAt(index.row()); -} +// endregion -void PlaylistManager::setAutoLoadFilterSuffixes(const QStringList &nameFilters) -{ - m_model.setProperty("autoLoadFilterSuffixes", nameFilters); -} +// region: Misc -QList PlaylistManager::convertToUrlList(const QStringList &files) +void PlaylistManager::sanitizeIndex() { - QList urlList; - for (const QString & str : std::as_const(files)) { - QUrl url = QUrl::fromLocalFile(str); - if (url.isValid()) { - urlList.append(url); + auto cnt = totalCount(); + if (cnt == 0) { + // If playlist is empty, current index should be invalid. + m_currentIndex = std::nullopt; + } else { + if (m_currentIndex.has_value()) { + // Clamp current index in playlist range. + // If it is already in range, not change was done. + auto currentIndex = *m_currentIndex; + if (currentIndex < 0) { + m_currentIndex = 0; + } else if (currentIndex >= cnt) { + m_currentIndex = cnt - 1; + } else { + ; // No change. + } + } else { + // If there is no set value for current index, + // set it at the beginning of playlist. + m_currentIndex = 0; } } - - return urlList; } + +bool PlaylistManager::checkUrl(const QUrl& url) +{ + return url.isValid() && url.isLocalFile(); +} + +QUrl PlaylistManager::urlFromString(const QString& file) +{ + return QUrl::fromLocalFile(file); +} + +// endregion diff --git a/app/playlistmanager.h b/app/playlistmanager.h index 6d496ef..c0fe7ea 100644 --- a/app/playlistmanager.h +++ b/app/playlistmanager.h @@ -5,84 +5,126 @@ #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() override; - - 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; - QUrl urlByIndex(int index) const; - QStringList autoLoadFilterSuffixes() const; - - QHash roleNames() const override; - 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; -}; +#include +#include +/// The manager of playlist. class PlaylistManager : public QObject { Q_OBJECT public: - Q_PROPERTY(int currentIndex MEMBER m_currentIndex NOTIFY currentIndexChanged) - Q_PROPERTY(QStringList autoLoadFilterSuffixes WRITE setAutoLoadFilterSuffixes) - Q_PROPERTY(PlaylistModel * model READ model CONSTANT) - - explicit PlaylistManager(QObject *parent = nullptr); + /// Constructs a PlaylistManager with an optional parent QObject. + explicit PlaylistManager(QObject* parent = nullptr); + /// Destructor. ~PlaylistManager(); - PlaylistModel * model(); + /// Get the allowed file suffixes of this playlist. + /// @return The current list of allowed suffixes. + const QStringList& allowedSuffixes() const; + /// Set the allowed file suffixes of this playlist. + /// @remark This only works for "load the whole directory by one file" case. + /// @param[in] nameFilters The new list of allowed suffixes. + void setAllowedSuffixes(const QStringList& nameFilters); - void setPlaylist(const QList & url); - Q_INVOKABLE QModelIndex loadPlaylist(const QList & urls); - Q_INVOKABLE QModelIndex loadPlaylist(const QUrl & url); - Q_INVOKABLE QModelIndex loadM3U8Playlist(const QUrl & url); + /// Load user specified files as playlist. + /// @remark The wrapper for another overload. + /// @return True for successful load, otherwise false. + bool loadPlaylist(const QStringList& files); + /// Load user specified URLs as playlist. + /// @remark This function automatically dispatch these files into correct sub-load functions. + /// @return True for successful load, otherwise false. + bool loadPlaylist(const QList& urls); + /// Clear the entire playlist and reset current index. + void clearPlaylist(); - int totalCount() const; - QModelIndex previousIndex() const; - QModelIndex nextIndex() const; - QModelIndex curIndex() const; + /// Append a local file path to the playlist. + void appendToPlaylist(const QString& file); + /// Append a URL to the playlist. + void appendToPlaylist(const QUrl& url); + /// Insert a local file path at the given index. + /// @return True if inserted successfully, false if index is out of range. + bool insertIntoPlaylist(qsizetype index, const QString& file); + /// Insert a URL at the given index. + /// @return True if inserted successfully, false if index is out of range. + bool insertIntoPlaylist(qsizetype index, const QUrl& url); + /// Remove the entry at the given index. + /// @return True if removed successfully, false if index is out of range. + bool removeFromPlaylist(qsizetype index); + + /// Returns the total number of items in the playlist. + /// @return Total count of entries in the playlist. + qsizetype totalCount() const; + /// Try fetching previous index. + /// @details This previous index is gallery-loop-aware. + /// @return std::nullopt if playlist is empty, otherwise the previous index considering gallery loop. + std::optional previousIndex() const; + /// Try fetching next index. + /// @details This next index is gallery-loop-aware. + /// @return std::nullopt if playlist is empty, otherwise the next index considering gallery loop. + std::optional nextIndex() const; + /// Check whether current index is the first index. + /// @return False if current index is not the first index or playlist is empty, otherwise true. bool isFirstIndex() const; + /// Check whether current index is the last index. + /// @return False if current index is not the last index or playlist is empty, otherwise true. bool isLastIndex() const; - void setCurrentIndex(const QModelIndex & index); - QUrl urlByIndex(const QModelIndex & index); - QString localFileByIndex(const QModelIndex & index); - bool removeAt(const QModelIndex & index); - - void setAutoLoadFilterSuffixes(const QStringList &nameFilters); - - static QList convertToUrlList(const QStringList & files); + /// Returns the current index. + /// @return std::nullopt if the playlist is empty, otherwise the current index. + std::optional currentIndex() const; + /// Set the current index. + /// @remark Index only can be set when playlist is not empty. + /// @return True if successful set, including "not changed" set, otherwise false. + /// This return value only indicates whether we can set index and given index is valid for setting. + /// If "no changed" set occurs, this function returns true, but doesn't raise current index changed signal. + bool setCurrentIndex(qsizetype index); + /// Returns the URL at the given index. + /// @return The URL if index is valid. + /// This class guarantee that this QUrl is pointing to a local file. + QUrl urlByIndex(qsizetype index) const; + /// Returns the local file path at the given index. + /// @return The local file path if index is valid. + /// This class guarantee that local file path always is valid. + QString localFileByIndex(qsizetype index) const; signals: - void currentIndexChanged(int index); - void totalCountChanged(int count); + void allowedSuffixesChanged(const QStringList& suffixes); + /// Raised when index was changed bu setter. + /// The argument holds the new current index. + /// Please note that this signal will NOT be raised by the whole playlist change caused index change. + void currentIndexChanged(qsizetype index); + /// The whole playlist was changed, involving loading, editing and deleting some or full entries. + /// The argument holds the total count of new play list. + void playlistChanged(qsizetype count); private: - int m_currentIndex = -1; - PlaylistModel m_model; + /// The fundamental playlist setter. + /// @details All playlist load functions will call this function to set playlist at last. + /// @param[in] urls ALl urls in this list should be checked by checkUrl() function. + /// @param[in] indexHint Try to set current index to this index if possible. + void setPlaylist(const QList&& urls, std::optional indexHint = std::nullopt); + /// Load playlist from a single file. + /// @details This function scans the same directory for siblings matching allowed suffixes, sorted naturally. + /// @return True if the playlist was loaded successfully, false otherwise. + bool loadSingleFilePlaylist(const QUrl& url); + /// Load playlist from multiple URLs directly. + /// @return True if all URLs are valid, false otherwise. + bool loadMultipleFilesPlaylist(const QList&& urls); + /// Load playlist from an M3U/M3U8 playlist file. + /// @return True if loaded successfully, false otherwise. + bool loadM3U8Playlist(const QUrl& url); + + /// Make sure that current index is valid. + /// @details This function usually is used by those functions manipulating playlist. + void sanitizeIndex(); + + /// Check whether given QUrl is proper to be put into playlist. + /// @details Only the url pointing to local file can be put into playlist. + static bool checkUrl(const QUrl& url); + /// Convert a string to local file into a QUrl. + /// @param[in] file The string pointing to local file. + static QUrl urlFromString(const QString& file); + + std::optional m_currentIndex; + QList m_playlist; + QStringList m_allowedSuffixes; };