From 6217dbcc86806da1b48d0f360e442246504c5fac Mon Sep 17 00:00:00 2001 From: Bart De Vries Date: Thu, 8 Dec 2022 20:47:16 +0100 Subject: [PATCH] Add and use Chapter class --- src/CMakeLists.txt | 1 + src/chapter.cpp | 117 ++++++++++++++++++++++++++++ src/chapter.h | 55 +++++++++++++ src/fetcher.cpp | 4 - src/models/chaptermodel.cpp | 116 ++++++++++++++------------- src/models/chaptermodel.h | 28 +++---- src/qml/ChapterListDelegate.qml | 7 +- src/qml/HeaderBar.qml | 2 +- src/qml/MinimizedPlayerControls.qml | 2 +- src/qml/PlayerControls.qml | 2 +- 10 files changed, 254 insertions(+), 80 deletions(-) create mode 100644 src/chapter.cpp create mode 100644 src/chapter.h diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index bb62f95e..801f1492 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -11,6 +11,7 @@ set(SRCS_base feed.cpp author.cpp enclosure.cpp + chapter.cpp datamanager.cpp audiomanager.cpp error.cpp diff --git a/src/chapter.cpp b/src/chapter.cpp new file mode 100644 index 00000000..b40f4545 --- /dev/null +++ b/src/chapter.cpp @@ -0,0 +1,117 @@ +/** + * SPDX-FileCopyrightText: 2022 Bart De Vries + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#include "chapter.h" + +#include "fetcher.h" + +Chapter::Chapter(Entry *entry, const QString &title, const QString &link, const QString &image, const int &start, QObject *parent) + : QObject(parent) + , m_entry(entry) + , m_title(title) + , m_link(link) + , m_image(image) + , m_start(start) +{ + connect(&Fetcher::instance(), &Fetcher::downloadFinished, this, [this](QString url) { + if (url == m_image) { + Q_EMIT imageChanged(url); + Q_EMIT cachedImageChanged(cachedImage()); + } + }); +} + +Chapter::~Chapter() +{ +} + +Entry *Chapter::entry() const +{ + return m_entry; +} + +QString Chapter::title() const +{ + return m_title; +} + +QString Chapter::link() const +{ + return m_link; +} + +QString Chapter::image() const +{ + if (!m_image.isEmpty()) { + return m_image; + } else if (m_entry) { + // fall back to entry image + return m_entry->image(); + } else { + return QStringLiteral("no-image"); + } +} + +QString Chapter::cachedImage() const +{ + // First check for the feed image, fall back if needed + QString image = m_image; + if (image.isEmpty()) { + if (m_entry) { + return m_entry->cachedImage(); + } else { + return QStringLiteral("no-image"); + } + } + + return Fetcher::instance().image(image); +} + +int Chapter::start() const +{ + return m_start; +} + +void Chapter::setTitle(const QString &title, bool emitSignal) +{ + if (m_title != title) { + m_title = title; + if (emitSignal) { + Q_EMIT titleChanged(m_title); + } + } +} + +void Chapter::setLink(const QString &link, bool emitSignal) +{ + if (m_link != link) { + m_link = link; + if (emitSignal) { + Q_EMIT linkChanged(m_link); + } + } +} + +void Chapter::setImage(const QString &image, bool emitSignal) +{ + if (m_image != image) { + m_image = image; + if (emitSignal) { + Q_EMIT imageChanged(m_image); + Q_EMIT cachedImageChanged(cachedImage()); + } + } +} + +void Chapter::setStart(const int &start, bool emitSignal) +{ + if (m_start != start) { + m_start = start; + if (emitSignal) { + Q_EMIT startChanged(m_start); + } + } +} diff --git a/src/chapter.h b/src/chapter.h new file mode 100644 index 00000000..19e8c859 --- /dev/null +++ b/src/chapter.h @@ -0,0 +1,55 @@ +/** + * SPDX-FileCopyrightText: 2022 Bart De Vries + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#pragma once + +#include +#include +#include + +#include "entry.h" + +class Chapter : public QObject +{ + Q_OBJECT + + Q_PROPERTY(Entry *entry READ entry CONSTANT) + Q_PROPERTY(QString title READ title NOTIFY titleChanged) + Q_PROPERTY(QString link READ link NOTIFY linkChanged) + Q_PROPERTY(QString image READ image NOTIFY imageChanged) + Q_PROPERTY(QString cachedImage READ cachedImage NOTIFY cachedImageChanged) + Q_PROPERTY(int start READ start NOTIFY startChanged) + +public: + Chapter(Entry *entry, const QString &title, const QString &link, const QString &image, const int &start, QObject *parent = nullptr); + ~Chapter(); + + Entry *entry() const; + QString title() const; + QString link() const; + QString image() const; + QString cachedImage() const; + int start() const; + + void setTitle(const QString &title, bool emitSignal = true); + void setLink(const QString &link, bool emitSignal = true); + void setImage(const QString &image, bool emitSignal = true); + void setStart(const int &start, bool emitSignal = true); + +Q_SIGNALS: + void titleChanged(const QString &title); + void linkChanged(const QString &link); + void imageChanged(const QString &url); + void cachedImageChanged(const QString &imagePath); + void startChanged(const int &start); + +private: + Entry *m_entry = nullptr; + QString m_title; + QString m_link; + QString m_image; + int m_start; +}; diff --git a/src/fetcher.cpp b/src/fetcher.cpp index ce0782a7..4bb2b497 100644 --- a/src/fetcher.cpp +++ b/src/fetcher.cpp @@ -115,10 +115,6 @@ QString Fetcher::image(const QString &url) return QLatin1String("no-image"); } - if (url.startsWith(QStringLiteral("/"))) { - return url; - } - // if image is already cached, then return the path QString path = StorageManager::instance().imagePath(url); if (QFileInfo::exists(path)) { diff --git a/src/models/chaptermodel.cpp b/src/models/chaptermodel.cpp index 46047a05..6c0b50ab 100644 --- a/src/models/chaptermodel.cpp +++ b/src/models/chaptermodel.cpp @@ -29,11 +29,13 @@ ChapterModel::ChapterModel(QObject *parent) if (!m_entry || m_entry != AudioManager::instance().entry() || m_chapters.isEmpty()) { return; } - if (m_chapters[m_currentChapter].start > AudioManager::instance().position() / 1000 - || (m_currentChapter < m_chapters.size() - 1 && m_chapters[m_currentChapter + 1].start < AudioManager::instance().position() / 1000)) { + if (m_chapters[m_currentChapter] + && (m_chapters[m_currentChapter]->start() > AudioManager::instance().position() / 1000 + || (m_currentChapter < m_chapters.size() - 1 && m_chapters[m_currentChapter + 1] + && m_chapters[m_currentChapter + 1]->start() < AudioManager::instance().position() / 1000))) { for (int i = 0; i < m_chapters.size(); i++) { - if (m_chapters[i].start < AudioManager::instance().position() / 1000 - && (i == m_chapters.size() - 1 || m_chapters[i + 1].start > AudioManager::instance().position() / 1000)) { + if (m_chapters[i]->start() < AudioManager::instance().position() / 1000 + && (i == m_chapters.size() - 1 || m_chapters[i + 1]->start() > AudioManager::instance().position() / 1000)) { m_currentChapter = i; Q_EMIT currentChapterChanged(); } @@ -42,6 +44,11 @@ ChapterModel::ChapterModel(QObject *parent) }); } +ChapterModel::~ChapterModel() +{ + qDeleteAll(m_chapters.begin(), m_chapters.end()); +} + QVariant ChapterModel::data(const QModelIndex &index, int role) const { if (!index.isValid()) { @@ -49,19 +56,24 @@ QVariant ChapterModel::data(const QModelIndex &index, int role) const } int row = index.row(); - - switch (role) { - case Title: - return QVariant::fromValue(m_chapters.at(row).title); - case Link: - return QVariant::fromValue(m_chapters.at(row).link); - case Image: - return QVariant::fromValue(m_chapters.at(row).image); - case StartTime: - return QVariant::fromValue(m_chapters.at(row).start); - case FormattedStartTime: - return QVariant::fromValue(m_kformat.formatDuration(m_chapters.at(row).start * 1000)); - default: + if (m_chapters.at(row)) { + switch (role) { + case TitleRole: + return QVariant::fromValue(m_chapters.at(row)->title()); + case LinkRole: + return QVariant::fromValue(m_chapters.at(row)->link()); + case ImageRole: + return QVariant::fromValue(m_chapters.at(row)->image()); + case StartTimeRole: + return QVariant::fromValue(m_chapters.at(row)->start()); + case FormattedStartTimeRole: + return QVariant::fromValue(m_kformat.formatDuration(m_chapters.at(row)->start() * 1000)); + case ChapterRole: + return QVariant::fromValue(m_chapters.at(row)); + default: + return QVariant(); + } + } else { return QVariant(); } } @@ -78,11 +90,12 @@ int ChapterModel::rowCount(const QModelIndex &parent) const QHash ChapterModel::roleNames() const { return { - {Title, "title"}, - {Link, "link"}, - {Image, "image"}, - {StartTime, "start"}, - {FormattedStartTime, "formattedStart"}, + {TitleRole, "title"}, + {LinkRole, "link"}, + {ImageRole, "image"}, + {StartTimeRole, "start"}, + {FormattedStartTimeRole, "formattedStart"}, + {ChapterRole, "chapter"}, }; } @@ -96,6 +109,8 @@ void ChapterModel::setEntry(Entry *entry) if (entry) { m_entry = entry; } else { + qDeleteAll(m_chapters.begin(), m_chapters.end()); + m_chapters.clear(); m_entry = nullptr; } load(); @@ -105,11 +120,14 @@ void ChapterModel::setEntry(Entry *entry) void ChapterModel::load() { beginResetModel(); + qDeleteAll(m_chapters.begin(), m_chapters.end()); m_chapters = {}; m_currentChapter = 0; if (m_entry) { - loadFromDatabase(); loadChaptersFromFile(); + if (m_chapters.isEmpty()) { + loadFromDatabase(); + } } endResetModel(); Q_EMIT currentChapterChanged(); @@ -123,11 +141,12 @@ void ChapterModel::loadFromDatabase() query.bindValue(QStringLiteral(":id"), m_entry->id()); Database::instance().execute(query); while (query.next()) { - ChapterEntry chapter{}; - chapter.title = query.value(QStringLiteral("title")).toString(); - chapter.link = query.value(QStringLiteral("link")).toString(); - chapter.image = query.value(QStringLiteral("image")).toString(); - chapter.start = query.value(QStringLiteral("start")).toInt(); + Chapter *chapter = new Chapter(m_entry, + query.value(QStringLiteral("title")).toString(), + query.value(QStringLiteral("link")).toString(), + query.value(QStringLiteral("image")).toString(), + query.value(QStringLiteral("start")).toInt(), + this); m_chapters << chapter; } } @@ -141,10 +160,10 @@ void ChapterModel::loadMPEGChapters(TagLib::MPEG::File &f) for (const auto &frame : f.ID3v2Tag()->frameListMap()["CHAP"]) { auto chapterFrame = dynamic_cast(frame); - ChapterEntry chapter{}; const auto &apicList = chapterFrame->embeddedFrameListMap()["APIC"]; - auto hash = QString::fromLatin1( - QCryptographicHash::hash(QStringLiteral("%1,%2").arg(m_entry->id(), chapterFrame->startTime()).toLatin1(), QCryptographicHash::Md5).toHex()); + QString image = QStringLiteral("%1,%2").arg(m_entry->id()).arg(chapterFrame->startTime()); + // TODO: get hashed filename from a method in Fetcher + auto hash = QString::fromLatin1(QCryptographicHash::hash(image.toLatin1(), QCryptographicHash::Md5).toHex()); auto path = QStringLiteral("%1/images/%2").arg(StorageManager::instance().storagePath(), hash); if (!apicList.isEmpty()) { if (!QFileInfo(path).exists()) { @@ -154,24 +173,23 @@ void ChapterModel::loadMPEGChapters(TagLib::MPEG::File &f) file.write(QByteArray(apic.data(), apic.size())); file.close(); } - chapter.image = path; } else { - chapter.image = QString(); + image = QString(); } - chapter.title = QString::fromStdString(chapterFrame->embeddedFrameListMap()["TIT2"].front()->toString().to8Bit(true)); - chapter.link = QString(); - chapter.start = chapterFrame->startTime() / 1000; + QString title = QString::fromStdString(chapterFrame->embeddedFrameListMap()["TIT2"].front()->toString().to8Bit(true)); + int start = chapterFrame->startTime() / 1000; + Chapter *chapter = new Chapter(m_entry, title, QString(), image, start, this); auto originalChapter = std::find_if(m_chapters.begin(), m_chapters.end(), [chapter](auto it) { - return chapter.start == it.start; + return chapter->start() == it->start(); }); if (originalChapter != m_chapters.end()) { - originalChapter->image = chapter.image; + (*originalChapter)->image() = chapter->image(); } else { m_chapters << chapter; } } - std::sort(m_chapters.begin(), m_chapters.end(), [](const ChapterEntry &a, const ChapterEntry &b) { - return a.start < b.start; + std::sort(m_chapters.begin(), m_chapters.end(), [](const Chapter *a, const Chapter *b) { + return a->start() < b->start(); }); } @@ -187,21 +205,13 @@ void ChapterModel::loadChaptersFromFile() } // TODO else... } -int ChapterModel::currentChapter() const +Chapter *ChapterModel::currentChapter() const { for (int i = 0; i < m_chapters.size(); i++) { - if (m_chapters[i].start < AudioManager::instance().position() / 1000 - && (i == m_chapters.size() - 1 || m_chapters[i + 1].start > AudioManager::instance().position() / 1000)) { - return i; + if (m_chapters[i] && m_chapters[i]->start() < AudioManager::instance().position() / 1000 + && (i == m_chapters.size() - 1 || m_chapters[i + 1]->start() > AudioManager::instance().position() / 1000)) { + return m_chapters[i]; } } - return 0; -} - -QString ChapterModel::currentChapterImage() const -{ - if (m_chapters.size() <= m_currentChapter) { - return {}; - } - return m_chapters[m_currentChapter].image; + return nullptr; } diff --git a/src/models/chaptermodel.h b/src/models/chaptermodel.h index b20e31bc..0ab48195 100644 --- a/src/models/chaptermodel.h +++ b/src/models/chaptermodel.h @@ -11,34 +11,29 @@ #include +#include "chapter.h" #include "entry.h" -struct ChapterEntry { - QString title; - QString link; - QString image; - int start; -}; - class ChapterModel : public QAbstractListModel { Q_OBJECT Q_PROPERTY(Entry *entry READ entry WRITE setEntry NOTIFY entryChanged) - Q_PROPERTY(int currentChapter READ currentChapter NOTIFY currentChapterChanged) - Q_PROPERTY(QString currentChapterImage READ currentChapterImage NOTIFY currentChapterChanged) + Q_PROPERTY(Chapter *currentChapter READ currentChapter NOTIFY currentChapterChanged) public: enum RoleNames { - Title = Qt::DisplayRole, - Link = Qt::UserRole + 1, - Image, - StartTime, - FormattedStartTime, + TitleRole = Qt::DisplayRole, + LinkRole = Qt::UserRole + 1, + ImageRole, + StartTimeRole, + FormattedStartTimeRole, + ChapterRole, }; Q_ENUM(RoleNames); explicit ChapterModel(QObject *parent = nullptr); + ~ChapterModel(); QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; QHash roleNames() const override; @@ -47,8 +42,7 @@ public: void setEntry(Entry *entry); Entry *entry() const; - int currentChapter() const; - QString currentChapterImage() const; + Chapter *currentChapter() const; Q_SIGNALS: void entryChanged(); @@ -61,7 +55,7 @@ private: void loadMPEGChapters(TagLib::MPEG::File &f); Entry *m_entry = nullptr; - QVector m_chapters; + QVector m_chapters; KFormat m_kformat; int m_currentChapter = 0; }; diff --git a/src/qml/ChapterListDelegate.qml b/src/qml/ChapterListDelegate.qml index d72b6bae..cf21831d 100644 --- a/src/qml/ChapterListDelegate.qml +++ b/src/qml/ChapterListDelegate.qml @@ -26,10 +26,11 @@ Kirigami.BasicListItem { text: model.title subtitle: model.formattedStart - leading: Kirigami.Icon { - width: height + leading: ImageWithFallback { + imageSource: model.chapter.cachedImage height: parent.height - source: Fetcher.image(model.image.length > 0 ? model.image : root.entry.image) + width: height + fractionalRadius: 1.0 / 8.0 } trailing: Controls.ToolButton { diff --git a/src/qml/HeaderBar.qml b/src/qml/HeaderBar.qml index 5ac90409..b10d2e91 100644 --- a/src/qml/HeaderBar.qml +++ b/src/qml/HeaderBar.qml @@ -50,7 +50,7 @@ Rectangle { spacing: Kirigami.Units.largeSpacing ImageWithFallback { id: mainImage - imageSource: AudioManager.entry ? ((chapterModel.currentChapterImage && chapterModel.currentChapterImage !== "") ? "file://" + Fetcher.image(chapterModel.currentChapterImage) : AudioManager.entry.cachedImage) : "no-image" + imageSource: AudioManager.entry ? ((chapterModel.currentChapter && chapterModel.currentChapter !== undefined) ? chapterModel.currentChapter.cachedImage : AudioManager.entry.cachedImage) : "no-image" height: controlsLayout.height width: height absoluteRadius: 5 diff --git a/src/qml/MinimizedPlayerControls.qml b/src/qml/MinimizedPlayerControls.qml index 6974edc3..7b0aeef6 100644 --- a/src/qml/MinimizedPlayerControls.qml +++ b/src/qml/MinimizedPlayerControls.qml @@ -56,7 +56,7 @@ Item { anchors.fill: parent ImageWithFallback { - imageSource: AudioManager.entry ? ((chapterModel.currentChapterImage && chapterModel.currentChapterImage !== "") ? "file://" + Fetcher.image(chapterModel.currentChapterImage) : AudioManager.entry.cachedImage) : "no-image" + imageSource: AudioManager.entry ? ((chapterModel.currentChapter && chapterModel.currentChapter !== undefined) ? chapterModel.currentChapter.cachedImage : AudioManager.entry.cachedImage) : "no-image" Layout.fillHeight: true Layout.preferredWidth: height } diff --git a/src/qml/PlayerControls.qml b/src/qml/PlayerControls.qml index 6327c59e..89c19995 100644 --- a/src/qml/PlayerControls.qml +++ b/src/qml/PlayerControls.qml @@ -117,7 +117,7 @@ Kirigami.Page { width: root.isWidescreen ? Math.min(parent.height, parent.width / 2) : Math.min(parent.width, height) ImageWithFallback { - imageSource: AudioManager.entry ? ((chapterModel.currentChapterImage && chapterModel.currentChapterImage !== "") ? "file://" + Fetcher.image(chapterModel.currentChapterImage) : AudioManager.entry.cachedImage) : "no-image" + imageSource: AudioManager.entry ? ((chapterModel.currentChapter && chapterModel.currentChapter !== undefined) ? chapterModel.currentChapter.cachedImage : AudioManager.entry.cachedImage) : "no-image" imageFillMode: Image.PreserveAspectCrop anchors.centerIn: parent anchors.margins: 0