diff --git a/src/creator/qml/Book.qml b/src/creator/qml/Book.qml index 0d10384..6413d9b 100644 --- a/src/creator/qml/Book.qml +++ b/src/creator/qml/Book.qml @@ -1,201 +1,201 @@ /* * Copyright (C) 2015 Dan Leinir Turthra Jensen * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) version 3, or any * later version accepted by the membership of KDE e.V. (or its * successor approved by the membership of KDE e.V.), which shall * act as a proxy defined in Section 6 of version 3 of the license. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library. If not, see . * */ import QtQuick 2.2 import QtQuick.Controls 2.2 as QtControls import org.kde.kirigami 2.1 as Kirigami import org.kde.peruse 0.1 as Peruse /** * @brief the page that deals with editing the book. * * This is primarily a list of pages that can be moved around. These are inside * Kirigami ListSwipeItems. * * This also has a button to add pages, which calls up AddPageSheet. * And a button to edit the book metadata, which calls up BookMetainfoPage. */ Kirigami.ScrollablePage { id: root; property string categoryName: "book"; title: i18nc("title of the main book editor page", "Editing %1", bookModel.title == "" ? root.filename : bookModel.title); property string filename; actions { left: addPageSheet.opened ? null : saveBookAction; main: addPageSheet.opened ? closeAddPageSheetAction : defaultMainAction; right: addPageSheet.opened ? null : addPageAction; } Kirigami.Action { id: saveBookAction; text: i18nc("Saves the book to a file on disk", "Save Book"); iconName: "document-save"; onTriggered: bookModel.saveBook(); enabled: bookModel.hasUnsavedChanges; } Kirigami.Action { id: addPageAction; text: i18nc("adds a new page at the end of the book", "Add Page"); iconName: "list-add"; onTriggered: addPage(bookModel.pageCount); } Kirigami.Action { id: defaultMainAction; text: i18nc("causes a dialog to show in which the user can edit the meta information for the entire book", "Edit Metainfo"); iconName: "document-edit"; onTriggered: pageStack.push(editMetaInfo); } Kirigami.Action { id: closeAddPageSheetAction; text: i18nc("closes the add page sheet", "Do not Add a Page"); iconName: "dialog-cancel"; onTriggered: addPageSheet.close(); } function addPage(afterWhatIndex) { addPageSheet.addPageAfter = afterWhatIndex; addPageSheet.open(); } ListView { id: bookList; model: Peruse.ArchiveBookModel { id: bookModel; qmlEngine: globalQmlEngine; readWrite: true; filename: root.filename; } Component { id: editMetaInfo; BookMetainfoPage { model: bookModel; } } Component { id: editBookPage; BookPage { model: bookModel; onSave: { bookList.updateTitle(index, currentPage.title("")); } } } function updateTitle(index, title) { //Need to add feature to update data here. } delegate: Kirigami.SwipeListItem { id: listItem; height: Kirigami.Units.iconSizes.huge + Kirigami.Units.smallSpacing * 2; supportsMouseEvents: true; onClicked: ; actions: [ Kirigami.Action { text: i18nc("swap the position of this page with the previous one", "Move Up"); iconName: "go-up" onTriggered: { bookModel.swapPages(model.index, model.index - 1); } enabled: model.index > 0; visible: enabled; }, Kirigami.Action { text: i18nc("swap the position of this page with the next one", "Move Down"); iconName: "go-down" onTriggered: { bookModel.swapPages(model.index, model.index + 1); } enabled: model.index < bookModel.pageCount - 1; visible: enabled; }, Kirigami.Action { text: i18nc("remove the page from the book", "Delete Page"); iconName: "list-remove" - onTriggered: {} + onTriggered: bookModel.removePage(model.index); }, Kirigami.Action { text: i18nc("add a page to the book after this one", "Add Page After This"); iconName: "list-add" onTriggered: root.addPage(model.index); }, Kirigami.Action { text: i18nc("Edit page data such as title, frames, etc.", "Edit Page"); iconName: "document-edit"; onTriggered: { pageStack.push(editBookPage, { index: model.index, pageUrl: model.url }) } } ] Item { anchors.fill: parent; Item { id: bookCover; anchors { top: parent.top; left: parent.left; bottom: parent.bottom; } width: height; Image { id: coverImage; anchors { fill: parent; margins: Kirigami.Units.smallSpacing; } asynchronous: true; fillMode: Image.PreserveAspectFit; source: model.url; } } QtControls.Label { anchors { verticalCenter: parent.verticalCenter; left: bookCover.right; leftMargin: Kirigami.Units.largeSpacing; } text: model.title; } } } Rectangle { id: processingBackground; anchors.fill: parent; opacity: bookModel.processing ? 0.5 : 0; Behavior on opacity { NumberAnimation { duration: mainWindow.animationDuration; } } MouseArea { anchors.fill: parent; enabled: parent.opacity > 0; onClicked: { } } } QtControls.BusyIndicator { anchors { horizontalCenter: processingBackground.horizontalCenter; top: parent.top topMargin: x; } running: processingBackground.opacity > 0; visible: running; } } AddPageSheet { id: addPageSheet; model: bookModel; } } diff --git a/src/qtquick/ArchiveBookModel.cpp b/src/qtquick/ArchiveBookModel.cpp index 661d73d..a26f0fc 100644 --- a/src/qtquick/ArchiveBookModel.cpp +++ b/src/qtquick/ArchiveBookModel.cpp @@ -1,1268 +1,1295 @@ /* * Copyright (C) 2015 Dan Leinir Turthra Jensen * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) version 3, or any * later version accepted by the membership of KDE e.V. (or its * successor approved by the membership of KDE e.V.), which shall * act as a proxy defined in Section 6 of version 3 of the license. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library. If not, see . * */ #include "ArchiveBookModel.h" #include "ArchiveImageProvider.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "KRar.h" // "" because it's a custom thing for now #include class ArchiveBookModel::Private { public: Private(ArchiveBookModel* qq) : q(qq) , engine(nullptr) , archive(nullptr) , readWrite(false) , imageProvider(nullptr) , isDirty(false) , isLoading(false) {} ArchiveBookModel* q; QQmlEngine* engine; KArchive* archive; bool readWrite; ArchiveImageProvider* imageProvider; bool isDirty; bool isLoading; static int counter() { static int count = 0; return count++; } void setDirty() { isDirty = true; emit q->hasUnsavedChangesChanged(); } AdvancedComicBookFormat::Document* createNewAcbfDocumentFromLegacyInformation() { AdvancedComicBookFormat::Document* acbfDocument = new AdvancedComicBookFormat::Document(q); acbfDocument->metaData()->bookInfo()->setTitle(q->title()); AdvancedComicBookFormat::Author* author = new AdvancedComicBookFormat::Author(acbfDocument->metaData()); author->setNickName(q->author()); acbfDocument->metaData()->bookInfo()->addAuthor(author); acbfDocument->metaData()->publishInfo()->setPublisher(q->publisher()); int prefixLength = QString("image://%1/").arg(imageProvider->prefix()).length(); if(q->pageCount() > 0) { // First, let's see if we have something called "*cover*"... because that would be handy and useful int cover = -1; for(int i = q->pageCount(); i > -1; --i) { QString url = q->data(q->index(i, 0, QModelIndex()), BookModel::UrlRole).toString().mid(prefixLength); // Yup, this is a bit sort of naughty and stuff... but, assume index 0 is the cover if nothing else has shown up... // FIXME this will also fail when there's more than one cover... say, back and front covers... if(url.split('/').last().contains("cover", Qt::CaseInsensitive) || i == 0) { acbfDocument->metaData()->bookInfo()->coverpage()->setImageHref(url); acbfDocument->metaData()->bookInfo()->coverpage()->setTitle(q->data(q->index(0, 0, QModelIndex()), BookModel::TitleRole).toString()); cover = i; break; } } for(int i = 0; i < q->pageCount(); ++i) { if(i == cover) { continue; } AdvancedComicBookFormat::Page* page = new AdvancedComicBookFormat::Page(acbfDocument); page->setImageHref(q->data(q->index(i, 0, QModelIndex()), BookModel::UrlRole).toString().mid(prefixLength)); page->setTitle(q->data(q->index(i, 0, QModelIndex()), BookModel::TitleRole).toString()); acbfDocument->body()->addPage(page); } } q->setAcbfData(acbfDocument); setDirty(); return acbfDocument; } }; ArchiveBookModel::ArchiveBookModel(QObject* parent) : BookModel(parent) , d(new Private(this)) { } ArchiveBookModel::~ArchiveBookModel() { delete d; } QStringList recursiveEntries(const KArchiveDirectory* dir) { QStringList entries = dir->entries(); QStringList allEntries = entries; Q_FOREACH(const QString& entryName, entries) { const KArchiveEntry* entry = dir->entry(entryName); if(entry->isDirectory()) { const KArchiveDirectory* subDir = static_cast(entry); QStringList subEntries = recursiveEntries(subDir); Q_FOREACH(const QString& subEntry, subEntries) { entries.append(entryName + "/" + subEntry); } } } return entries; } void ArchiveBookModel::setFilename(QString newFilename) { setProcessing(true); d->isLoading = true; if(d->archive) { clearPages(); delete d->archive; } d->archive = nullptr; if(d->imageProvider && d->engine) { d->engine->removeImageProvider(d->imageProvider->prefix()); } d->imageProvider = nullptr; QMimeDatabase db; QMimeType mime = db.mimeTypeForFile(newFilename); if(mime.inherits("application/zip")) { d->archive = new KZip(newFilename); } else if (mime.inherits("application/x-rar")) { d->archive = new KRar(newFilename); } bool success = false; if(d->archive) { QString prefix = QString("archivebookpage%1").arg(QString::number(Private::counter())); if(d->archive->open(QIODevice::ReadOnly)) { d->imageProvider = new ArchiveImageProvider(); d->imageProvider->setArchiveBookModel(this); d->imageProvider->setPrefix(prefix); if(d->engine) { d->engine->addImageProvider(prefix, d->imageProvider); } QStringList entries = recursiveEntries(d->archive->directory()); // First check and see if we've got an ACBF document in there... QString acbfEntry; QString comicInfoEntry; QStringList xmlFiles; QLatin1String acbfSuffix(".acbf"); QLatin1String ComicInfoXML("comicinfo.xml"); QLatin1String xmlSuffix(".xml"); QStringList images; Q_FOREACH(const QString& entry, entries) { if(entry.toLower().endsWith(acbfSuffix)) { acbfEntry = entry; break; } if(entry.toLower().endsWith(xmlSuffix)) { if(entry.toLower().endsWith(ComicInfoXML)) { comicInfoEntry = entry; } else { xmlFiles.append(entry); } } if (entry.toLower().endsWith(".jpg") || entry.toLower().endsWith(".jpeg") || entry.toLower().endsWith(".gif") || entry.toLower().endsWith(".png")) { images.append(entry); } } images.sort(); if(!acbfEntry.isEmpty()) { AdvancedComicBookFormat::Document* acbfDocument = new AdvancedComicBookFormat::Document(this); const KArchiveFile* archFile = d->archive->directory()->file(acbfEntry); if(acbfDocument->fromXml(QString(archFile->data()))) { setAcbfData(acbfDocument); addPage(QString("image://%1/%2").arg(prefix).arg(acbfDocument->metaData()->bookInfo()->coverpage()->imageHref()), acbfDocument->metaData()->bookInfo()->coverpage()->title()); Q_FOREACH(AdvancedComicBookFormat::Page* page, acbfDocument->body()->pages()) { addPage(QString("image://%1/%2").arg(prefix).arg(page->imageHref()), page->title()); } } else { // just in case this is, for whatever reason, being reused... setAcbfData(nullptr); } } else if (!comicInfoEntry.isEmpty() || !xmlFiles.isEmpty()) { AdvancedComicBookFormat::Document* acbfDocument = new AdvancedComicBookFormat::Document(this); const KArchiveFile* archFile = d->archive->directory()->file(comicInfoEntry); bool loadData = false; if (!comicInfoEntry.isEmpty()) { loadData = loadComicInfoXML(archFile->data(), acbfDocument, images, newFilename); } else { loadData = loadCoMet(xmlFiles, acbfDocument, images, newFilename); } if (loadData) { setAcbfData(acbfDocument); QString undesired = QString("%1").arg("/").append("Thumbs.db"); addPage(QString("image://%1/%2").arg(prefix).arg(acbfDocument->metaData()->bookInfo()->coverpage()->imageHref()), acbfDocument->metaData()->bookInfo()->coverpage()->title()); Q_FOREACH(AdvancedComicBookFormat::Page* page, acbfDocument->body()->pages()) { addPage(QString("image://%1/%2").arg(prefix).arg(page->imageHref()), page->title()); } } } if(!acbfData()) { // fall back to just handling the files directly if there's no ACBF document... entries.sort(); QString undesired = QString("%1").arg("/").append("Thumbs.db"); Q_FOREACH(const QString& entry, entries) { const KArchiveEntry* archEntry = d->archive->directory()->entry(entry); if(archEntry->isFile() && !entry.endsWith(undesired)) { addPage(QString("image://%1/%2").arg(prefix).arg(entry), entry.split("/").last()); } } } success = true; } else { qCDebug(QTQUICK_LOG) << "Failed to open archive"; } } // QDir dir(newFilename); // if(dir.exists()) // { // QFileInfoList entries = dir.entryInfoList(QDir::Files, QDir::Name); // Q_FOREACH(const QFileInfo& entry, entries) // { // addPage(QString("file://").append(entry.canonicalFilePath()), entry.fileName()); // } // } BookModel::setFilename(newFilename); KFileMetaData::UserMetaData data(newFilename); if(data.hasAttribute("peruse.currentPage")) BookModel::setCurrentPage(data.attribute("peruse.currentPage").toInt(), false); if(!acbfData() && d->readWrite && d->imageProvider) { d->createNewAcbfDocumentFromLegacyInformation(); } d->isLoading = false; emit loadingCompleted(success); setProcessing(false); } QString ArchiveBookModel::author() const { AdvancedComicBookFormat::Document* acbfDocument = qobject_cast(acbfData()); if(acbfDocument) { if(acbfDocument->metaData()->bookInfo()->author().count() > 0) { return acbfDocument->metaData()->bookInfo()->author().at(0)->displayName(); } } return BookModel::author(); } void ArchiveBookModel::setAuthor(QString newAuthor) { if(!d->isLoading) { AdvancedComicBookFormat::Document* acbfDocument = qobject_cast(acbfData()); if(!acbfDocument) { acbfDocument = d->createNewAcbfDocumentFromLegacyInformation(); } if(acbfDocument->metaData()->bookInfo()->author().count() == 0) { AdvancedComicBookFormat::Author* author = new AdvancedComicBookFormat::Author(acbfDocument->metaData()); author->setNickName(newAuthor); acbfDocument->metaData()->bookInfo()->addAuthor(author); } else { acbfDocument->metaData()->bookInfo()->author().at(0)->setNickName(newAuthor); } } BookModel::setAuthor(newAuthor); } QString ArchiveBookModel::publisher() const { AdvancedComicBookFormat::Document* acbfDocument = qobject_cast(acbfData()); if(acbfDocument) { if(!acbfDocument->metaData()->publishInfo()->publisher().isEmpty()) { return acbfDocument->metaData()->publishInfo()->publisher(); } } return BookModel::publisher(); } void ArchiveBookModel::setPublisher(QString newPublisher) { if(!d->isLoading) { AdvancedComicBookFormat::Document* acbfDocument = qobject_cast(acbfData()); if(!acbfDocument) { acbfDocument = d->createNewAcbfDocumentFromLegacyInformation(); } acbfDocument->metaData()->publishInfo()->setPublisher(newPublisher); } BookModel::setAuthor(newPublisher); } QString ArchiveBookModel::title() const { AdvancedComicBookFormat::Document* acbfDocument = qobject_cast(acbfData()); if(acbfDocument) { if(acbfDocument->metaData()->bookInfo()->title().length() > 0) { return acbfDocument->metaData()->bookInfo()->title(); } } return BookModel::title(); } void ArchiveBookModel::setTitle(QString newTitle) { if(!d->isLoading) { AdvancedComicBookFormat::Document* acbfDocument = qobject_cast(acbfData()); if(!acbfDocument) { acbfDocument = d->createNewAcbfDocumentFromLegacyInformation(); } acbfDocument->metaData()->bookInfo()->setTitle(newTitle); } BookModel::setTitle(newTitle); } QObject * ArchiveBookModel::qmlEngine() const { return d->engine; } void ArchiveBookModel::setQmlEngine(QObject* newEngine) { d->engine = qobject_cast(newEngine); emit qmlEngineChanged(); } bool ArchiveBookModel::readWrite() const { return d->readWrite; } void ArchiveBookModel::setReadWrite(bool newReadWrite) { d->readWrite = newReadWrite; emit readWriteChanged(); } bool ArchiveBookModel::hasUnsavedChanges() const { return d->isDirty; } void ArchiveBookModel::setDirty(bool isDirty) { d->isDirty = isDirty; emit hasUnsavedChangesChanged(); } bool ArchiveBookModel::saveBook() { bool success = true; if(d->isDirty) { // TODO get new filenames out of acbf setProcessing(true); qApp->processEvents(); QTemporaryFile tmpFile(this); tmpFile.open(); QString archiveFileName = tmpFile.fileName().append(".cbz"); QFileInfo fileInfo(tmpFile); tmpFile.close(); qCDebug(QTQUICK_LOG) << "Creating archive in" << archiveFileName; KZip* archive = new KZip(archiveFileName); archive->open(QIODevice::ReadWrite); // We're a zip file... size isn't used qCDebug(QTQUICK_LOG) << "Writing in ACBF data"; archive->prepareWriting("metadata.acbf", fileInfo.owner(), fileInfo.group(), 0); AdvancedComicBookFormat::Document* acbfDocument = qobject_cast(acbfData()); if(!acbfDocument) { acbfDocument = d->createNewAcbfDocumentFromLegacyInformation(); } QString acbfString = acbfDocument->toXml(); archive->writeData(acbfString.toUtf8(), acbfString.size()); archive->finishWriting(acbfString.size()); qCDebug(QTQUICK_LOG) << "Copying across cover page"; const KArchiveFile* archFile = archiveFile(acbfDocument->metaData()->bookInfo()->coverpage()->imageHref()); if(archFile) { archive->prepareWriting(acbfDocument->metaData()->bookInfo()->coverpage()->imageHref(), fileInfo.owner(), fileInfo.group(), 0); archive->writeData(archFile->data(), archFile->size()); archive->finishWriting(archFile->size()); } Q_FOREACH(AdvancedComicBookFormat::Page* page, acbfDocument->body()->pages()) { qApp->processEvents(); qCDebug(QTQUICK_LOG) << "Copying over" << page->title(); archFile = archiveFile(page->imageHref()); if(archFile) { archive->prepareWriting(page->imageHref(), archFile->user(), archFile->group(), 0); archive->writeData(archFile->data(), archFile->size()); archive->finishWriting(archFile->size()); } } archive->close(); qCDebug(QTQUICK_LOG) << "Archive created and closed..."; // swap out the two files, tell model we're about to swap things out... beginResetModel(); QString actualFile = d->archive->fileName(); d->archive->close(); // This seems roundabout... but it retains ctime and xattrs, which would be gone // if we just did a delete+rename QFile destinationFile(actualFile); if(destinationFile.open(QIODevice::WriteOnly)) { QFile originFile(archiveFileName); if(originFile.open(QIODevice::ReadOnly)) { qCDebug(QTQUICK_LOG) << "Copying all content from" << archiveFileName << "to" << actualFile; while(!originFile.atEnd()) { destinationFile.write(originFile.read(65536)); qApp->processEvents(); } destinationFile.close(); originFile.close(); if(originFile.remove()) { qCDebug(QTQUICK_LOG) << "Success! Now loading the new archive..."; // now load the new thing... setFilename(actualFile); } else { qCWarning(QTQUICK_LOG) << "Failed to delete" << originFile.fileName(); } } else { qCWarning(QTQUICK_LOG) << "Failed to open" << originFile.fileName() << "for reading"; } } else { qCWarning(QTQUICK_LOG) << "Failed to open" << destinationFile.fileName() << "for writing"; } } endResetModel(); setProcessing(false); setDirty(false); return success; } void ArchiveBookModel::addPage(QString url, QString title) { // don't do this unless we're done loading... don't want to dirty things up until then! if(!d->isLoading) { AdvancedComicBookFormat::Document* acbfDocument = qobject_cast(acbfData()); if(!acbfDocument) { acbfDocument = d->createNewAcbfDocumentFromLegacyInformation(); } QUrl imageUrl(url); if(pageCount() == 0) { acbfDocument->metaData()->bookInfo()->coverpage()->setTitle(title); acbfDocument->metaData()->bookInfo()->coverpage()->setImageHref(QString("%1/%2").arg(imageUrl.path().mid(1)).arg(imageUrl.fileName())); } else { AdvancedComicBookFormat::Page* page = new AdvancedComicBookFormat::Page(acbfDocument); page->setTitle(title); page->setImageHref(QString("%1/%2").arg(imageUrl.path().mid(1)).arg(imageUrl.fileName())); acbfDocument->body()->addPage(page); } } BookModel::addPage(url, title); } +void ArchiveBookModel::removePage(int pageNumber) +{ + if(!d->isLoading) + { + AdvancedComicBookFormat::Document* acbfDocument = qobject_cast(acbfData()); + if(!acbfDocument) + { + acbfDocument = d->createNewAcbfDocumentFromLegacyInformation(); + } + else + { + if(pageNumber == 0) + { + //Page no 0 is the cover page, when removed we'll take the next page. + AdvancedComicBookFormat::Page* page = acbfDocument->body()->page(0); + acbfDocument->metaData()->bookInfo()->setCoverpage(page); + acbfDocument->body()->removePage(page); + } + else { + AdvancedComicBookFormat::Page* page = acbfDocument->body()->page(pageNumber-1); + acbfDocument->body()->removePage(page); + } + } + } + BookModel::removePage(pageNumber); +} + // FIXME any metadata change sets dirty (as we need to replace the whole file in archive when saving) void ArchiveBookModel::addPageFromFile(QString fileUrl, int insertAfter) { if(d->archive && d->readWrite && !d->isDirty) { int insertionIndex = insertAfter; if(insertAfter < 0 || pageCount() - 1 < insertAfter) { insertionIndex = pageCount(); } // This is a permanent thing, renaming in zip files is VERY expensive (literally not possible without // rewriting the entire archive...) QString archiveFileName = QString("page-%1.%2").arg(QString::number(insertionIndex), QFileInfo(fileUrl).completeSuffix()); d->archive->close(); d->archive->open(QIODevice::ReadWrite); d->archive->addLocalFile(fileUrl, archiveFileName); d->archive->close(); d->archive->open(QIODevice::ReadOnly); addPage(QString("image://%1/%2").arg(d->imageProvider->prefix()).arg(archiveFileName), archiveFileName.split("/").last()); saveBook(); } } void ArchiveBookModel::swapPages(int swapThisIndex, int withThisIndex) { d->setDirty(); AdvancedComicBookFormat::Document* acbfDocument = qobject_cast(acbfData()); // Cover pages are special, and in acbf they are very special... (otherwise they're page zero) if(swapThisIndex == 0) { AdvancedComicBookFormat::Page* oldCoverPage = acbfDocument->metaData()->bookInfo()->coverpage(); AdvancedComicBookFormat::Page* otherPage = acbfDocument->body()->page(withThisIndex - 1); acbfDocument->body()->removePage(otherPage); acbfDocument->metaData()->bookInfo()->setCoverpage(otherPage); acbfDocument->body()->addPage(oldCoverPage, withThisIndex - 1); } else if(withThisIndex == 0) { AdvancedComicBookFormat::Page* oldCoverPage = acbfDocument->metaData()->bookInfo()->coverpage(); AdvancedComicBookFormat::Page* otherPage = acbfDocument->body()->page(swapThisIndex - 1); acbfDocument->body()->removePage(otherPage); acbfDocument->metaData()->bookInfo()->setCoverpage(otherPage); acbfDocument->body()->addPage(oldCoverPage, swapThisIndex - 1); } else { AdvancedComicBookFormat::Page* firstPage = acbfDocument->body()->page(swapThisIndex - 1); AdvancedComicBookFormat::Page* otherPage = acbfDocument->body()->page(withThisIndex - 1); acbfDocument->body()->swapPages(firstPage, otherPage); } // FIXME only treat things which have sequential numbers in as pages, split out chapters automatically? BookModel::swapPages(swapThisIndex, withThisIndex); } QString ArchiveBookModel::createBook(QString folder, QString title, QString coverUrl) { bool success = true; QString fileTitle = title.replace( QRegExp("\\W"),QString("")).simplified(); QString filename = QString("%1/%2.cbz").arg(folder).arg(fileTitle); int i = 1; while(QFile(filename).exists()) { filename = QString("%1/%2 (%3).cbz").arg(folder).arg(fileTitle).arg(QString::number(i++)); } ArchiveBookModel* model = new ArchiveBookModel(nullptr); model->setQmlEngine(qmlEngine()); model->setReadWrite(true); QString prefix = QString("archivebookpage%1").arg(QString::number(Private::counter())); model->d->imageProvider = new ArchiveImageProvider(); model->d->imageProvider->setArchiveBookModel(model); model->d->imageProvider->setPrefix(prefix); model->d->archive = new KZip(filename); model->BookModel::setFilename(filename); model->setTitle(title); AdvancedComicBookFormat::Document* acbfDocument = qobject_cast(model->acbfData()); QString coverArchiveName = QString("cover.%1").arg(QFileInfo(coverUrl).completeSuffix()); acbfDocument->metaData()->bookInfo()->coverpage()->setImageHref(coverArchiveName); success = model->saveBook(); model->d->archive->close(); model->d->archive->open(QIODevice::ReadWrite); model->d->archive->addLocalFile(coverUrl, coverArchiveName); model->d->archive->close(); model->deleteLater(); if(!success) return QLatin1String(""); return filename; } const KArchiveFile * ArchiveBookModel::archiveFile(const QString& filePath) { if(d->archive) { return d->archive->directory()->file(filePath); } return nullptr; } bool ArchiveBookModel::loadComicInfoXML(QString xmlDocument, QObject *acbfData, QStringList entries, QString filename) { KFileMetaData::UserMetaData filedata(filename); AdvancedComicBookFormat::Document* acbfDocument = qobject_cast(acbfData); QXmlStreamReader xmlReader(xmlDocument); if(xmlReader.readNextStartElement()) { if(xmlReader.name() == QStringLiteral("ComicInfo")) { // We'll need to collect several items to generate a series. Thankfully, comicinfo only has two types of series. QString series; int number = -1; int volume = 0; QString seriesAlt; int numberAlt = -1; int volumeAlt = 0; // Also publishing date. int year = 0; int month = 0; int day = 0; QStringList publisher; QStringList keywords; QStringList empty; while(xmlReader.readNextStartElement()) { if(xmlReader.name() == QStringLiteral("Title")) { acbfDocument->metaData()->bookInfo()->setTitle(xmlReader.readElementText(),""); } //Summary/annotation. else if(xmlReader.name() == QStringLiteral("Summary")) { acbfDocument->metaData()->bookInfo()->setAnnotation(xmlReader.readElementText().split("\n\n"), ""); } /* * This ought to go into the kfile metadata. */ else if(xmlReader.name() == QStringLiteral("Notes")) { if (filedata.userComment().isEmpty()) { filedata.setUserComment(xmlReader.readElementText()); } else { xmlReader.skipCurrentElement(); } } else if(xmlReader.name() == QStringLiteral("Tags")) { QStringList tags = filedata.tags(); QStringList newTags = xmlReader.readElementText().split(","); for (int i=0; i < newTags.size(); i++) { if (!tags.contains(newTags.at(i))) { tags.append(newTags.at(i)); } } filedata.setTags(tags); } else if(xmlReader.name() == QStringLiteral("PageCount")) { filedata.setAttribute("Peruse.totalPages", xmlReader.readElementText()); } else if(xmlReader.name() == QStringLiteral("ScanInformation")) { QString userComment = filedata.userComment(); userComment.append("\n"+xmlReader.readElementText()); filedata.setUserComment(userComment); } //Series else if(xmlReader.name() == QStringLiteral("Series")) { series = xmlReader.readElementText(); } else if(xmlReader.name() == QStringLiteral("Number")) { number = xmlReader.readElementText().toInt(); } else if(xmlReader.name() == QStringLiteral("Volume")) { volume = xmlReader.readElementText().toInt(); } // Series alt else if(xmlReader.name() == QStringLiteral("AlternateSeries")) { seriesAlt = xmlReader.readElementText(); } else if(xmlReader.name() == QStringLiteral("AlternateNumber")) { numberAlt = xmlReader.readElementText().toInt(); } else if(xmlReader.name() == QStringLiteral("AlternateVolume")) { volumeAlt = xmlReader.readElementText().toInt(); } // Publishing date. else if(xmlReader.name() == QStringLiteral("Year")) { year = xmlReader.readElementText().toInt(); } else if(xmlReader.name() == QStringLiteral("Month")) { month = xmlReader.readElementText().toInt(); } else if(xmlReader.name() == QStringLiteral("Day")) { day = xmlReader.readElementText().toInt(); } //Publisher else if(xmlReader.name() == QStringLiteral("Publisher")) { publisher.append(xmlReader.readElementText()); } else if(xmlReader.name() == QStringLiteral("Imprint")) { publisher.append(xmlReader.readElementText()); } //Genre else if(xmlReader.name() == QStringLiteral("Genre")) { QString key = xmlReader.readElementText(); QString genreKey = key.toLower().replace(" ", "_"); if (acbfDocument->metaData()->bookInfo()->availableGenres().contains(genreKey)) { acbfDocument->metaData()->bookInfo()->setGenre(genreKey); } else { //There must always be a genre in a proper acbf file... acbfDocument->metaData()->bookInfo()->setGenre("other"); keywords.append(key); } } //Language else if(xmlReader.name() == QStringLiteral("LanguageISO")) { acbfDocument->metaData()->bookInfo()->addLanguage(xmlReader.readElementText()); } //Sources/Weblink else if(xmlReader.name() == QStringLiteral("Web")) { acbfDocument->metaData()->documentInfo()->setSource(QStringList(xmlReader.readElementText())); } //One short, trade, etc. else if(xmlReader.name() == QStringLiteral("Format")) { keywords.append(xmlReader.readElementText()); } //Is this a manga? else if(xmlReader.name() == QStringLiteral("Manga")) { if (xmlReader.readElementText() == "Yes") { acbfDocument->metaData()->bookInfo()->setGenre("manga"); acbfDocument->metaData()->bookInfo()->setRightToLeft(true); } } //Content rating... else if(xmlReader.name() == QStringLiteral("AgeRating")) { acbfDocument->metaData()->bookInfo()->addContentRating(xmlReader.readElementText()); } //Authors... else if(xmlReader.name() == QStringLiteral("Writer")) { QStringList people = xmlReader.readElementText().split(",", QString::SkipEmptyParts); for (int i=0; i < people.size(); i++) { acbfDocument->metaData()->bookInfo()->addAuthor("Writer", "", "", "", "", people.at(i).trimmed(), empty, empty); } } else if(xmlReader.name() == QStringLiteral("Plotter")) { QStringList people = xmlReader.readElementText().split(",", QString::SkipEmptyParts); for (int i=0; i < people.size(); i++) { acbfDocument->metaData()->bookInfo()->addAuthor("Writer", "", "", "", "", people.at(i).trimmed(), empty, empty); } } else if(xmlReader.name() == QStringLiteral("Scripter")) { QStringList people = xmlReader.readElementText().split(",", QString::SkipEmptyParts); for (int i=0; i < people.size(); i++) { acbfDocument->metaData()->bookInfo()->addAuthor("Writer", "", "", "", "", people.at(i).trimmed(), empty, empty); } } else if(xmlReader.name() == QStringLiteral("Penciller")) { QStringList people = xmlReader.readElementText().split(",", QString::SkipEmptyParts); for (int i=0; i < people.size(); i++) { acbfDocument->metaData()->bookInfo()->addAuthor("Penciller", "", "", "", "", people.at(i).trimmed(), empty, empty); } } else if(xmlReader.name() == QStringLiteral("Inker")) { QStringList people = xmlReader.readElementText().split(",", QString::SkipEmptyParts); for (int i=0; i < people.size(); i++) { acbfDocument->metaData()->bookInfo()->addAuthor("Inker", "", "", "", "", people.at(i).trimmed(), empty, empty); } } else if(xmlReader.name() == QStringLiteral("Colorist")) { QStringList people = xmlReader.readElementText().split(",", QString::SkipEmptyParts); for (int i=0; i < people.size(); i++) { acbfDocument->metaData()->bookInfo()->addAuthor("Colorist", "", "", "", "", people.at(i).trimmed(), empty, empty); } } else if(xmlReader.name() == QStringLiteral("CoverArtist")) { QStringList people = xmlReader.readElementText().split(",", QString::SkipEmptyParts); for (int i=0; i < people.size(); i++) { acbfDocument->metaData()->bookInfo()->addAuthor("CoverArtist", "", "", "", "", people.at(i).trimmed(), empty, empty); } } else if(xmlReader.name() == QStringLiteral("Letterer")) { QStringList people = xmlReader.readElementText().split(",", QString::SkipEmptyParts); for (int i=0; i < people.size(); i++) { acbfDocument->metaData()->bookInfo()->addAuthor("Letterer", "", "", "", "", people.at(i).trimmed(), empty, empty); } } else if(xmlReader.name() == QStringLiteral("Editor")) { QStringList people = xmlReader.readElementText().split(",", QString::SkipEmptyParts); for (int i=0; i < people.size(); i++) { acbfDocument->metaData()->bookInfo()->addAuthor("Editor", "", "", "", "", people.at(i).trimmed(), empty, empty); } } else if(xmlReader.name() == QStringLiteral("Other")) { QStringList people = xmlReader.readElementText().split(",", QString::SkipEmptyParts); for (int i=0; i < people.size(); i++) { acbfDocument->metaData()->bookInfo()->addAuthor("Other", "", "", "", "", people.at(i).trimmed(), empty, empty); } } //Characters... else if(xmlReader.name() == QStringLiteral("Characters")) { QStringList people = xmlReader.readElementText().split(",", QString::SkipEmptyParts); for (int i=0; i < people.size(); i++) { acbfDocument->metaData()->bookInfo()->addCharacter(people.at(i).trimmed()); } } //Throw the rest into the keywords. else if(xmlReader.name() == QStringLiteral("Teams")) { QStringList teams = xmlReader.readElementText().split(",", QString::SkipEmptyParts); for (int i=0; i < teams.size(); i++) { keywords.append(teams.at(i).trimmed()); } } else if(xmlReader.name() == QStringLiteral("Locations")) { QStringList locations = xmlReader.readElementText().split(",", QString::SkipEmptyParts); for (int i=0; i < locations.size(); i++) { keywords.append(locations.at(i).trimmed()); } } else if(xmlReader.name() == QStringLiteral("StoryArc")) { QStringList arc = xmlReader.readElementText().split(",", QString::SkipEmptyParts); for (int i=0; i < arc.size(); i++) { keywords.append(arc.at(i).trimmed()); } } else if(xmlReader.name() == QStringLiteral("SeriesGroup")) { QStringList group = xmlReader.readElementText().split(",", QString::SkipEmptyParts); for (int i=0; i < group.size(); i++) { keywords.append(group.at(i).trimmed()); } } //Pages... else if(xmlReader.name() == QStringLiteral("Pages")) { while(xmlReader.readNextStartElement()) { if(xmlReader.name() == QStringLiteral("Page")) { int index = xmlReader.attributes().value(QStringLiteral("Image")).toInt(); QString type = xmlReader.attributes().value(QStringLiteral("Type")).toString(); QString bookmark = xmlReader.attributes().value(QStringLiteral("Bookmark")).toString(); AdvancedComicBookFormat::Page* page = new AdvancedComicBookFormat::Page(acbfDocument); page->setImageHref(entries.at(index)); if (type == QStringLiteral("FrontCover")) { acbfDocument->metaData()->bookInfo()->setCoverpage(page); } else { if (bookmark.isEmpty()) { page->setTitle(type.append(QString::number(index))); } else { page->setTitle(bookmark); } acbfDocument->body()->addPage(page, index-1); } xmlReader.readNext(); } } } else { qCWarning(QTQUICK_LOG) << Q_FUNC_INFO << "currently unsupported subsection:" << xmlReader.name(); xmlReader.skipCurrentElement(); } } if (!series.isEmpty() && number>-1) { acbfDocument->metaData()->bookInfo()->addSequence(number, series, volume); } if (!seriesAlt.isEmpty() && numberAlt>-1) { acbfDocument->metaData()->bookInfo()->addSequence(numberAlt, seriesAlt, volumeAlt); } if (year > 0 || month > 0 || day > 0) { //acbfDocument->metaData()->publishInfo()->setPublishDateFromInts(year, month, day); } if (publisher.size()>0) { acbfDocument->metaData()->publishInfo()->setPublisher(publisher.join(", ")); } if (keywords.size()>0) { acbfDocument->metaData()->bookInfo()->setKeywords(keywords, ""); } if (acbfDocument->metaData()->bookInfo()->languages().size()>0) { QString lang = acbfDocument->metaData()->bookInfo()->languageEntryList().at(0); acbfDocument->metaData()->bookInfo()->setTitle(acbfDocument->metaData()->bookInfo()->title(""), lang); acbfDocument->metaData()->bookInfo()->setAnnotation(acbfDocument->metaData()->bookInfo()->annotation(""), lang); acbfDocument->metaData()->bookInfo()->setKeywords(acbfDocument->metaData()->bookInfo()->keywords(""), lang); } } } if (xmlReader.hasError()) { qCWarning(QTQUICK_LOG) << Q_FUNC_INFO << "Failed to read Comic Info XML document at token" << xmlReader.name() << "(" << xmlReader.lineNumber() << ":" << xmlReader.columnNumber() << ") The reported error was:" << xmlReader.errorString(); } qCDebug(QTQUICK_LOG) << Q_FUNC_INFO << "Completed ACBF document creation from ComicInfo.xml for" << acbfDocument->metaData()->bookInfo()->title(); acbfData = acbfDocument; return !xmlReader.hasError(); } bool ArchiveBookModel::loadCoMet(QStringList xmlDocuments, QObject *acbfData, QStringList entries, QString filename) { KFileMetaData::UserMetaData filedata(filename); AdvancedComicBookFormat::Document* acbfDocument = qobject_cast(acbfData); Q_FOREACH(const QString xmlDocument, xmlDocuments) { const KArchiveFile* archFile = d->archive->directory()->file(xmlDocument); QXmlStreamReader xmlReader(archFile->data()); if(xmlReader.readNextStartElement()) { if(xmlReader.name() == QStringLiteral("comet")) { // We'll need to collect several items to generate a series. Thankfully, comicinfo only has two types of series. QString series; int number = -1; int volume = 0; QStringList keywords; QStringList empty; while(xmlReader.readNextStartElement()) { if(xmlReader.name() == QStringLiteral("title")) { acbfDocument->metaData()->bookInfo()->setTitle(xmlReader.readElementText(),""); } //Summary/annotation. else if(xmlReader.name() == QStringLiteral("description")) { acbfDocument->metaData()->bookInfo()->setAnnotation(xmlReader.readElementText().split("\n\n"), ""); } /* * This ought to go into the kfile metadata. */ //pages else if(xmlReader.name() == QStringLiteral("pages")) { filedata.setAttribute("Peruse.totalPages", xmlReader.readElementText()); } //curentpage -- only read this when there's no such entry. else if(xmlReader.name() == QStringLiteral("lastMark")) { if (!filedata.hasAttribute("Peruse.currentPage")) { filedata.setAttribute("Peruse.currentPage", xmlReader.readElementText()); } else { xmlReader.skipCurrentElement(); } } //Series else if(xmlReader.name() == QStringLiteral("series")) { series = xmlReader.readElementText(); } else if(xmlReader.name() == QStringLiteral("issue")) { number = xmlReader.readElementText().toInt(); } else if(xmlReader.name() == QStringLiteral("volume")) { volume = xmlReader.readElementText().toInt(); } // Publishing date. else if(xmlReader.name() == QStringLiteral("date")) { acbfDocument->metaData()->publishInfo()->setPublishDate(QDate::fromString(xmlReader.readElementText(), Qt::ISODate)); } //Publisher else if(xmlReader.name() == QStringLiteral("publisher")) { acbfDocument->metaData()->publishInfo()->setPublisher(xmlReader.readElementText()); } else if(xmlReader.name() == QStringLiteral("rights")) { acbfDocument->metaData()->publishInfo()->setLicense(xmlReader.readElementText()); } else if(xmlReader.name() == QStringLiteral("identifier")) { acbfDocument->metaData()->publishInfo()->setIsbn(xmlReader.readElementText()); } //Genre else if(xmlReader.name() == QStringLiteral("genre")) { QString key = xmlReader.readElementText(); QString genreKey = key.toLower().replace(" ", "_"); if (acbfDocument->metaData()->bookInfo()->availableGenres().contains(genreKey)) { acbfDocument->metaData()->bookInfo()->setGenre(genreKey); } else { keywords.append(key); } } //Language else if(xmlReader.name() == QStringLiteral("language")) { acbfDocument->metaData()->bookInfo()->addLanguage(xmlReader.readElementText()); } //Sources/Weblink else if(xmlReader.name() == QStringLiteral("isVersionOf")) { acbfDocument->metaData()->documentInfo()->setSource(QStringList(xmlReader.readElementText())); } //One short, trade, etc. else if(xmlReader.name() == QStringLiteral("format")) { keywords.append(xmlReader.readElementText()); } //Is this a manga? else if(xmlReader.name() == QStringLiteral("readingDirection")) { if (xmlReader.readElementText() == "rtl") { acbfDocument->metaData()->bookInfo()->setRightToLeft(true); } } //Content rating... else if(xmlReader.name() == QStringLiteral("rating")) { acbfDocument->metaData()->bookInfo()->addContentRating(xmlReader.readElementText()); } //Authors... else if(xmlReader.name() == QStringLiteral("writer")) { QString person = xmlReader.readElementText(); acbfDocument->metaData()->bookInfo()->addAuthor("Writer", "", "", "", "", person, empty, empty); } else if(xmlReader.name() == QStringLiteral("creator")) { QString person = xmlReader.readElementText(); acbfDocument->metaData()->bookInfo()->addAuthor("Writer", "", "", "", "", person, empty, empty); } else if(xmlReader.name() == QStringLiteral("penciller")) { QString person = xmlReader.readElementText(); acbfDocument->metaData()->bookInfo()->addAuthor("Penciller", "", "", "", "", person, empty, empty); } else if(xmlReader.name() == QStringLiteral("editor")) { QString person = xmlReader.readElementText(); acbfDocument->metaData()->bookInfo()->addAuthor("Editor", "", "", "", "", person, empty, empty); } else if(xmlReader.name() == QStringLiteral("coverDesigner")) { QString person = xmlReader.readElementText(); acbfDocument->metaData()->bookInfo()->addAuthor("CoverArtist", "", "", "", "", person, empty, empty); } else if(xmlReader.name() == QStringLiteral("letterer")) { QString person = xmlReader.readElementText(); acbfDocument->metaData()->bookInfo()->addAuthor("Letterer", "", "", "", "", person, empty, empty); } else if(xmlReader.name() == QStringLiteral("inker")) { QString person = xmlReader.readElementText(); acbfDocument->metaData()->bookInfo()->addAuthor("Inker", "", "", "", "", person, empty, empty); } else if(xmlReader.name() == QStringLiteral("colorist")) { QString person = xmlReader.readElementText(); acbfDocument->metaData()->bookInfo()->addAuthor("Colorist", "", "", "", "", person, empty, empty); } //Characters else if(xmlReader.name() == QStringLiteral("character")) { QString person = xmlReader.readElementText(); acbfDocument->metaData()->bookInfo()->addCharacter(person); } //Get the cover image, set it, remove it from entries, then remove all other entries. else if(xmlReader.name() == QStringLiteral("coverImage")) { QString url = xmlReader.readElementText(); AdvancedComicBookFormat::Page* cover = new AdvancedComicBookFormat::Page(acbfDocument); cover->setImageHref(url); acbfDocument->metaData()->bookInfo()->setCoverpage(cover); entries.removeAll(url); Q_FOREACH(QString entry, entries) { AdvancedComicBookFormat::Page* page = new AdvancedComicBookFormat::Page(acbfDocument); page->setImageHref(entry); acbfDocument->body()->addPage(page); } xmlReader.readNext(); } else { qCWarning(QTQUICK_LOG) << Q_FUNC_INFO << "currently unsupported subsection:" << xmlReader.name(); xmlReader.skipCurrentElement(); } } if (!series.isEmpty() && number>-1) { acbfDocument->metaData()->bookInfo()->addSequence(number, series, volume); } if (acbfDocument->metaData()->bookInfo()->genres().size()==0) { //There must always be a genre in a proper acbf file... acbfDocument->metaData()->bookInfo()->setGenre("other"); } if (keywords.size()>0) { acbfDocument->metaData()->bookInfo()->setKeywords(keywords, ""); } if (acbfDocument->metaData()->bookInfo()->languages().size()>0) { QString lang = acbfDocument->metaData()->bookInfo()->languageEntryList().at(0); acbfDocument->metaData()->bookInfo()->setTitle(acbfDocument->metaData()->bookInfo()->title(""), lang); acbfDocument->metaData()->bookInfo()->setAnnotation(acbfDocument->metaData()->bookInfo()->annotation(""), lang); acbfDocument->metaData()->bookInfo()->setKeywords(acbfDocument->metaData()->bookInfo()->keywords(""), lang); } } if (xmlReader.hasError()) { qCWarning(QTQUICK_LOG) << Q_FUNC_INFO << "Failed to read CoMet document at token" << xmlReader.name() << "(" << xmlReader.lineNumber() << ":" << xmlReader.columnNumber() << ") The reported error was:" << xmlReader.errorString(); } qCDebug(QTQUICK_LOG) << Q_FUNC_INFO << "Completed ACBF document creation from CoMet for" << acbfDocument->metaData()->bookInfo()->title(); acbfData = acbfDocument; return !xmlReader.hasError(); } else { xmlReader.skipCurrentElement(); } } return false; } diff --git a/src/qtquick/ArchiveBookModel.h b/src/qtquick/ArchiveBookModel.h index 849864d..34087a8 100644 --- a/src/qtquick/ArchiveBookModel.h +++ b/src/qtquick/ArchiveBookModel.h @@ -1,204 +1,211 @@ /* * Copyright (C) 2015 Dan Leinir Turthra Jensen * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) version 3, or any * later version accepted by the membership of KDE e.V. (or its * successor approved by the membership of KDE e.V.), which shall * act as a proxy defined in Section 6 of version 3 of the license. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library. If not, see . * */ #ifndef ARCHIVEBOOKMODEL_H #define ARCHIVEBOOKMODEL_H #include "BookModel.h" /** * \brief Class to hold pages and metadata for archive based books. * * In particular, ArchiveBookModel handles CBZ and CBR files, reads * potential metadata and holds that into the acbfdata object. * * ArchiveBookModel extends BookModel, which handles the functions for * setting the current page, and returning basic metadata. */ class KArchiveFile; class ArchiveBookModel : public BookModel { Q_OBJECT Q_PROPERTY(QObject* qmlEngine READ qmlEngine WRITE setQmlEngine NOTIFY qmlEngineChanged) Q_PROPERTY(bool readWrite READ readWrite WRITE setReadWrite NOTIFY readWriteChanged) Q_PROPERTY(bool hasUnsavedChanges READ hasUnsavedChanges NOTIFY hasUnsavedChangesChanged) public: explicit ArchiveBookModel(QObject* parent = nullptr); ~ArchiveBookModel() override; /** * \brief Set the filename that points to the archive that describes this book. */ void setFilename(QString newFilename) override; /** * The author name will be either the default bookmodel author name, or * if ACBF data is available, the first authorname in the list of ACBF authors. * * @return the author name as a QString. */ QString author() const override; /** * \brief Set the main author's nickname. * * If there is no ACBF data, this will set the author to BookModel's author. * If there is ACBF data, this will set the nickname entry on the name of the * first possible author. * * Preferably authors should be added by editing the author list in the bookinfo * of the ACBF metadata this book holds. * * @param newAuthor The main author's nickname. */ void setAuthor(QString newAuthor) override; /** * @return the name of the publisher as a QString. */ QString publisher() const override; /** * \brief Set the name of the publisher. * @param newPublisher QString with the name of the publisher. */ void setPublisher(QString newPublisher) override; /** * @return The proper title of this book as a QString. */ QString title() const override; /** * \brief Set the default title of this book. * @param newTitle The default title of this book as a QString. */ void setTitle(QString newTitle) override; /** * @return a QQmlEngine associated with this book. * TODO: What is the QML engine and what is its purpose? * Used in the cbr.qml */ QObject* qmlEngine() const; /** * \brief Set the QML engine on this book. * @param newEngine A QQmlEngine object. */ void setQmlEngine(QObject* newEngine); /** * \brief Fires when a new QQmlEngine is set on this book. */ Q_SIGNAL void qmlEngineChanged(); /** * TODO: What is this? Only used in book.qml once? */ bool readWrite() const; void setReadWrite(bool newReadWrite); Q_SIGNAL void readWriteChanged(); /** * @return whether the book has been modified and has unsaved changes. * * Used in PeruseCreator to determine whether to enable the save dialog. */ bool hasUnsavedChanges() const; /** * \brief Set that the book has been modified. * @param isDirty whether the book has been modified. */ Q_INVOKABLE void setDirty(bool isDirty = true); /** * \brief Fires when there are unsaved changes. */ Q_SIGNAL void hasUnsavedChangesChanged(); /** * \brief Saves the archive back to disk * @return True if the save was successful */ Q_INVOKABLE bool saveBook(); /** * \brief add a page to this book. * * This adds it to the ACBF metadata too. * * @param url The resource location of the page as an url. * @param title The title of the page. This is shown in a table of contents. */ void addPage(QString url, QString title) override; + + /** + * @brief removePage + * remove the given page from the book by number. + * @param pageNumber the number of the page to remove. + */ + Q_INVOKABLE void removePage(int pageNumber) override; /** * Adds a new page to the book archive on disk, by copying in the file * passed to the function. Optionally this can be done at a specific * position in the book. * * @param fileUrl The URL of the file to copy into the archive * @param insertAfter The index to insert the new page after. If invalid, insertion will be at the end */ Q_INVOKABLE void addPageFromFile(QString fileUrl, int insertAfter = -1); /** * @brief Swap the two pages at the specified indices * * This will change the order in the archive file as well (that is, renaming the files inside the archive) * * @param swapThisIndex The index of the first page to be swapped * @param withThisIndex The index of the page you want the first to be swapped with */ Q_INVOKABLE void swapPages(int swapThisIndex, int withThisIndex) override; /** * Creates a new book in the folder, with the given title and cover. * A filename will be constructed to fit the title, and which does not already exist in the * directory. * * @param folder the path to the folder to create this book in. * @param title The title of the book. * @param coverUrl A resource location pointing at the image that will be the coverpage. */ Q_INVOKABLE QString createBook(QString folder, QString title, QString coverUrl); friend class ArchiveImageProvider; protected: const KArchiveFile* archiveFile(const QString& filePath); private: class Private; /** * @brief loadComicInfoXML * Loads ComicInfo.xml, this is an old file metadata type used by comicrack, and since then * written by other editors, amongst which a callibre plugin. * @param xmlDocument string with the archive value. * @param acbfData a pointer pointing to a acbfDocument. * @param entries a list of image entries, sorted. * @param filename the file name of the doument, necessary for writing data to kfilemetadata. * @return whether the reading was succesful. */ bool loadComicInfoXML(QString xmlDocument, QObject* acbfData, QStringList entries, QString filename); /** * @brief loads CoMet xmls, https://www.denvog.com/comet/comet-specification/ * @param xmlDocument string with the archive value. * @param acbfData a pointer pointing to a acbfDocument. * @param entries a list of image entries, sorted. * @param filename the file name of the doument, necessary for writing data to kfilemetadata. * @return whether the reading was succesful. */ bool loadCoMet(QStringList xmlDocuments, QObject* acbfData, QStringList entries, QString filename); Private* d; }; #endif//ARCHIVEBOOKMODEL_H diff --git a/src/qtquick/BookModel.cpp b/src/qtquick/BookModel.cpp index dbb957e..aaf6dbe 100644 --- a/src/qtquick/BookModel.cpp +++ b/src/qtquick/BookModel.cpp @@ -1,218 +1,227 @@ /* * Copyright (C) 2015 Dan Leinir Turthra Jensen * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) version 3, or any * later version accepted by the membership of KDE e.V. (or its * successor approved by the membership of KDE e.V.), which shall * act as a proxy defined in Section 6 of version 3 of the license. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library. If not, see . * */ #include "BookModel.h" #include #include struct BookPage { BookPage() {} QString url; QString title; }; class BookModel::Private { public: Private() : currentPage(0) , acbfData(nullptr) , processing(false) {} QString filename; QString author; QString publisher; QString title; QList entries; int currentPage; AdvancedComicBookFormat::Document* acbfData; bool processing; }; BookModel::BookModel(QObject* parent) : QAbstractListModel(parent) , d(new Private) { } BookModel::~BookModel() { delete d; } QHash BookModel::roleNames() const { QHash roles; roles[UrlRole] = "url"; roles[TitleRole] = "title"; return roles; } QVariant BookModel::data(const QModelIndex& index, int role) const { QVariant result; if(index.isValid() && index.row() > -1 && index.row() < d->entries.count()) { const BookPage* entry = d->entries[index.row()]; switch(role) { case UrlRole: result.setValue(entry->url); break; case TitleRole: result.setValue(entry->title); break; default: result.setValue(QString("Unknown role")); break; } } return result; } int BookModel::rowCount(const QModelIndex& parent) const { if(parent.isValid()) return 0; return d->entries.count(); } void BookModel::addPage(QString url, QString title) { BookPage* page = new BookPage(); page->url = url; page->title = title; beginInsertRows(QModelIndex(), d->entries.count(), d->entries.count()); d->entries.append(page); emit pageCountChanged(); endInsertRows(); } +void BookModel::removePage(int pageNumber) +{ + QModelIndex index = createIndex(pageNumber, 0); + beginRemoveRows(QModelIndex(), index.row(), index.row()); + d->entries.removeAt(pageNumber); + emit pageCountChanged(); + endRemoveRows(); +} + void BookModel::clearPages() { beginResetModel(); qDeleteAll(d->entries); d->entries.clear(); emit pageCountChanged(); endResetModel(); } QString BookModel::filename() const { return d->filename; } void BookModel::setFilename(QString newFilename) { d->filename = newFilename; d->title = newFilename.split('/').last().left(newFilename.lastIndexOf('.')); emit filenameChanged(); emit titleChanged(); } QString BookModel::author() const { return d->author; } void BookModel::setAuthor(QString newAuthor) { d->author = newAuthor; emit authorChanged(); } QString BookModel::publisher() const { return d->publisher; } void BookModel::setPublisher(QString newPublisher) { d->publisher = newPublisher; emit publisherChanged(); } QString BookModel::title() const { return d->title; } void BookModel::setTitle(QString newTitle) { d->title = newTitle; emit titleChanged(); } int BookModel::pageCount() const { return d->entries.count(); } int BookModel::currentPage() const { return d->currentPage; } void BookModel::setCurrentPage(int newCurrentPage, bool updateFilesystem) { // qCDebug(QTQUICK_LOG) << Q_FUNC_INFO << d->filename << newCurrentPage << updateFilesystem; if(updateFilesystem) { KFileMetaData::UserMetaData data(d->filename); data.setAttribute("peruse.currentPage", QString::number(newCurrentPage)); } d->currentPage = newCurrentPage; emit currentPageChanged(); } QObject * BookModel::acbfData() const { return d->acbfData; } void BookModel::setAcbfData(QObject* obj) { d->acbfData = qobject_cast(obj); emit acbfDataChanged(); } bool BookModel::processing() const { return d->processing; } void BookModel::setProcessing(bool processing) { d->processing = processing; emit processingChanged(); } void BookModel::swapPages(int swapThisIndex, int withThisIndex) { if(swapThisIndex > -1 && withThisIndex > -1 && swapThisIndex < d->entries.count() && withThisIndex < d->entries.count()) { QModelIndex firstIndex = createIndex(swapThisIndex, 0); QModelIndex secondIndex = createIndex(withThisIndex, 0); d->entries.swap(swapThisIndex, withThisIndex); dataChanged(firstIndex, secondIndex); } } diff --git a/src/qtquick/BookModel.h b/src/qtquick/BookModel.h index 9460b15..9bd0af4 100644 --- a/src/qtquick/BookModel.h +++ b/src/qtquick/BookModel.h @@ -1,250 +1,258 @@ /* * Copyright (C) 2015 Dan Leinir Turthra Jensen * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) version 3, or any * later version accepted by the membership of KDE e.V. (or its * successor approved by the membership of KDE e.V.), which shall * act as a proxy defined in Section 6 of version 3 of the license. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library. If not, see . * */ #ifndef BOOKMODEL_H #define BOOKMODEL_H #include /** * \brief Base Class to handle books, their pages and their metadata * * BookModel is an QAbstractListModel, holding the pages as a list of objects. * * It also holds metadata for the following entries as Q Properties: * * - filename. * - author * - publisher * - title * - page count. * - current page. * - acbf data * - processing * * The book model in turn is extended by ArchiveBookModel and FolderBookModel * to provide specialised functionality for archives(zip, rar, cbz, cbr) with * a book and Folders with a book and a description file. */ class BookModel : public QAbstractListModel { Q_OBJECT /** * \brief The filename of the archive that describes this book. */ Q_PROPERTY(QString filename READ filename WRITE setFilename NOTIFY filenameChanged) /** * \brief The main author of this book. */ Q_PROPERTY(QString author READ author WRITE setAuthor NOTIFY authorChanged) /** * \brief the name of the publisher of this book. */ Q_PROPERTY(QString publisher READ publisher WRITE setPublisher NOTIFY publisherChanged) /** * \brief The title of the book. */ Q_PROPERTY(QString title READ title WRITE setTitle NOTIFY titleChanged) /** * \brief The page count of the book. */ Q_PROPERTY(int pageCount READ pageCount NOTIFY pageCountChanged) /** * \brief The page currently being read of the book. */ Q_PROPERTY(int currentPage READ currentPage WRITE setCurrentPage NOTIFY currentPageChanged) /** * The Advanced Comic Book Format data management instance associated with this book * This may be null */ Q_PROPERTY(QObject* acbfData READ acbfData NOTIFY acbfDataChanged) /** * \brief Whether or not the book is still being processed. */ Q_PROPERTY(bool processing READ processing WRITE setProcessing NOTIFY processingChanged) public: explicit BookModel(QObject* parent = nullptr); ~BookModel() override; /** * Extra roles for the page data access. */ enum Roles { UrlRole = Qt::UserRole + 1, // This allows access to the resource location of the page. TitleRole, // This allows access to the title of the page, if it has one. }; /** * \brief This gives names for the Roles enum. */ QHash roleNames() const override; /** * \brief Access the data inside the BookModel. * @param index The QModelIndex at which you wish to access the data. * @param role An enumerator of the type of data you want to access. * Is extended by the Roles enum. * * @return a QVariant with the page data. */ QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; /** * @param parent The QModel index of the parent, not used here. * @returns the number of total pages there are in the Book. */ int rowCount(const QModelIndex& parent = QModelIndex()) const override; /** * \brief add a page to this book. * @param url The resource location of the page as an url. * @param title The title of the page. This is shown in a table of contents. */ virtual void addPage(QString url, QString title); + + /** + * @brief removePage + * Remove this page from the book. + * @param index the index of the page to be removed. + */ + virtual void removePage(int pageNumber); + /** * \brief remove all pages from the book. */ virtual void clearPages(); /** * @return the filename of the file that describes this book. */ QString filename() const; /** * \brief set the filename of the file that describes this book. */ virtual void setFilename(QString newFilename); /** * \brief Fires when the filename is changed via setfilename. */ Q_SIGNAL void filenameChanged(); /** * @returns the main author of the book as a QString. */ virtual QString author() const; /** * \brief set the main author of the book as a single string. * @param newAuthor The new name associated with the author * as a single string. */ virtual void setAuthor(QString newAuthor); /** * \brief Fires when the author has changed via setAuthor. */ Q_SIGNAL void authorChanged(); /** * @return the name of the publisher as a QString. */ virtual QString publisher() const; /** * \brief Set the name of the publisher. * @param newPublisher String that describes the publisher's name. */ virtual void setPublisher(QString newPublisher); /** * \brief Fires when publisher's name has changed with setPublisher. */ Q_SIGNAL void publisherChanged(); /** * @return The proper title of the book as a Qstring. */ virtual QString title() const; /** * \brief Set the title of the book. * @param newTitle A QString describing the new title. */ virtual void setTitle(QString newTitle); /** * \brief Fires when the book's title has changed via SetTitle */ Q_SIGNAL void titleChanged(); /** * @return the total pages in the book as an int. */ virtual int pageCount() const; /** * \brief Fires when the page count has changed, via for example pages * being added or removed. */ Q_SIGNAL void pageCountChanged(); /** * @return the number of the current page being viewed as an int. */ int currentPage() const; /** * \brief Set the current page. * @param newCurrentPage Int with the index of the page to switch to. * @param updateFilesystem If this is set to false, the attributes do not get written back to the filesystem. Useful for when the information is first filled out */ virtual void setCurrentPage(int newCurrentPage, bool updateFilesystem = true); /** * \brief Fires when the current page has changed. */ Q_SIGNAL void currentPageChanged(); /** * @return an object with the acbf data, might be null. */ QObject* acbfData() const; /** * This is used by subclasses who want to create one such. Until this is called * with a valid object, acbfData is null. This function causes BookModel to take * ownership of the object. It will further delete any previous objects set as * acbfData. */ void setAcbfData(QObject* obj); /** * \brief Fires when the ACBF data has changed. */ Q_SIGNAL void acbfDataChanged(); /** * @return Whether or not the any processing is currently going on */ bool processing() const; /** * \brief Set whether it is processing or done. * @param processing Whether this model is being processed. */ void setProcessing(bool processing); /** * \brief Fires when the state of processing has changed. */ Q_SIGNAL void processingChanged(); /** * \brief Fires when the book is done loading, and informs whether it was * succesful. * @param success Wether the book's loading was succesful * TODO: This isn't triggered by anything right now? */ Q_SIGNAL void loadingCompleted(bool success); /** * @brief Swap the two pages at the specified indices * * @param swapThisIndex The index of the first page to be swapped * @param withThisIndex The index of the page you want the first to be swapped with */ Q_INVOKABLE virtual void swapPages(int swapThisIndex, int withThisIndex); private: class Private; Private* d; }; #endif//BOOKMODEL_H