diff --git a/src/app/qml/Book.qml b/src/app/qml/Book.qml --- a/src/app/qml/Book.qml +++ b/src/app/qml/Book.qml @@ -422,13 +422,17 @@ property bool controlsShown; property QtObject currentBook: fakeBook; property QtObject fakeBook: Peruse.PropertyContainer { - property string author: ""; + property var author: [""]; property string title: ""; property string filename: ""; property string publisher: ""; property string thumbnail: ""; property string currentPage: "0"; property string totalPages: "0"; + property string comment: ""; + property var tags: [""]; + property var description: [""]; + property string rating: "0"; } Column { clip: true; @@ -447,6 +451,7 @@ categoryEntriesCount: 0; currentPage: bookInfo.currentBook.readProperty("currentPage"); totalPages: bookInfo.currentBook.readProperty("totalPages"); + description: bookInfo.currentBook.readProperty("description"); onBookSelected: { if(root.file !== filename) { openSelected(); @@ -470,7 +475,7 @@ orientation: ListView.Horizontal; NumberAnimation { id: seriesListAnimation; target: seriesListView; property: "contentX"; duration: applicationWindow().animationDuration; easing.type: Easing.InOutQuad; } delegate: ListComponents.BookTileTall { - height: model.filename != "" ? neededHeight : 1; + height: model.filename !== "" ? neededHeight : 1; width: seriesListView.width / 3; author: model.author; title: model.title; diff --git a/src/app/qml/Bookshelf.qml b/src/app/qml/Bookshelf.qml --- a/src/app/qml/Bookshelf.qml +++ b/src/app/qml/Bookshelf.qml @@ -92,7 +92,7 @@ // }, Kirigami.Action { text: i18nc("Open the book which is currently selected in the list", "Open Selected Book"); - shortcut: "Return"; + shortcut: bookDetails.sheetOpen? "" : "Return"; iconName: "document-open"; onTriggered: openBook(shelfList.currentIndex); enabled: root.isCurrentPage && applicationWindow().deviceType === applicationWindow().deviceTypeDesktop; @@ -193,6 +193,7 @@ property string thumbnail: ""; property string currentPage: "0"; property string totalPages: "0"; + property string comment: ""; } ListComponents.BookTile { id: detailsTile; @@ -206,6 +207,7 @@ categoryEntriesCount: 0; currentPage: bookDetails.currentBook.readProperty("currentPage"); totalPages: bookDetails.currentBook.readProperty("totalPages"); + description: bookDetails.currentBook.readProperty("description"); onBookSelected: { bookDetails.close(); applicationWindow().showBook(filename, currentPage); diff --git a/src/app/qml/listcomponents/BookTile.qml b/src/app/qml/listcomponents/BookTile.qml --- a/src/app/qml/listcomponents/BookTile.qml +++ b/src/app/qml/listcomponents/BookTile.qml @@ -38,20 +38,41 @@ id: root; property bool selected: false; property alias title: bookTitle.text; - property string author; + property var author: []; property string publisher; property alias filename: bookFile.text; property alias thumbnail: coverImage.source; property int categoryEntriesCount; property string currentPage; property string totalPages; + property var description: []; + property string comment: peruseConfig.getFilesystemProperty(root.filename, "comment"); + property var tags: peruseConfig.getFilesystemProperty(root.filename, "tags").split(","); + property int rating: peruseConfig.getFilesystemProperty(root.filename, "rating"); signal bookSelected(string filename, int currentPage); signal bookDeleteRequested(); property int neededHeight: bookCover.height;// + bookAuthorLabel.height + bookFile.height + Kirigami.Units.smallSpacing * 4; + property bool showCommentTags: neededHeight > bookTitle.height + bookAuthorLabel.height + + bookPublisherLabel.height + ratingContainer.height + + tagsContainer.height + commentContainer.height + deleteButton.height + Kirigami.Units.smallSpacing * 7; visible: height > 1; enabled: visible; clip: true; + + onRatingChanged: { + contentList.setBookData(root.filename, "rating", rating); + peruseConfig.setFilesystemProperty(root.filename, "rating", rating); + } + onTagsChanged: { + contentList.setBookData(root.filename, "tags", tags.join(",")); + peruseConfig.setFilesystemProperty(root.filename, "tags", tags.join(",")); + } + onCommentChanged: { + contentList.setBookData(root.filename, "comment", comment); + peruseConfig.setFilesystemProperty(root.filename, "comment", comment); + } + Rectangle { anchors.fill: parent; color: Kirigami.Theme.highlightColor; @@ -114,8 +135,8 @@ leftMargin: Kirigami.Units.smallSpacing; } width: paintedWidth; - text: "Author"; - font.bold: true; + text: i18nc("Label for authors", "Author(s)"); + font.weight: Font.Bold; } QtControls.Label { id: bookAuthor; @@ -126,7 +147,7 @@ right: parent.right; } elide: Text.ElideRight; - text: root.author === "" ? "(unknown)" : root.author; + text: root.author.length === 0 ? "(unknown)" : root.author.join(", "); opacity: (text === "(unknown)" || text === "") ? 0.3 : 1; } QtControls.Label { @@ -137,8 +158,8 @@ leftMargin: Kirigami.Units.smallSpacing; } width: paintedWidth; - text: "Publisher"; - font.bold: true; + text: i18nc("Label for publisher", "Publisher"); + font.weight: Font.Bold; } QtControls.Label { id: bookPublisher; @@ -166,19 +187,155 @@ maximumLineCount: 1; } Item { - id: descriptionContainer; + id: ratingContainer; anchors { top: bookFile.bottom; left: bookCover.right; right: parent.right; + margins: Kirigami.Units.smallSpacing; + } + Row { + id: ratingRow; + QtControls.Label { + width: paintedWidth; + text: i18nc("label for rating widget","Rating"); + height: Kirigami.Units.iconSizes.medium; + font.weight: Font.Bold; + anchors.rightMargin: Kirigami.Units.smallSpacing; + } + property int potentialRating: root.rating; + Repeater{ + model: 5; + Item { + + height: Kirigami.Units.iconSizes.medium; + width: Kirigami.Units.iconSizes.medium; + + Kirigami.Icon { + source: "rating"; + opacity: (ratingRow.potentialRating-2)/2 >= index? 1.0: 0.3; + anchors.fill:parent; + + MouseArea { + anchors.fill: parent; + hoverEnabled: true; + onEntered: { + if (ratingRow.potentialRating === (index+1)*2) { + ratingRow.potentialRating = ratingRow.potentialRating-1; + } else { + ratingRow.potentialRating = (index+1)*2; + } + } + onExited: { + ratingRow.potentialRating = root.rating; + } + onClicked: root.rating === ratingRow.potentialRating? + root.rating = ratingRow.potentialRating-1 : + root.rating = ratingRow.potentialRating; + } + + } + Kirigami.Icon { + source: "rating"; + height: parent.height/2; + clip: true; + anchors.centerIn: parent; + width: height; + visible: ratingRow.potentialRating === (index*2)+1; + } + } + } + } + + height: childrenRect.height; + } + Item { + id: tagsContainer; + height: root.showCommentTags? childrenRect.height: 0; + visible: root.showCommentTags; + anchors { + top: ratingContainer.bottom; + left: bookCover.right; + right: parent.right; + margins: Kirigami.Units.smallSpacing; + } + QtControls.Label { + text: i18nc("label for tags field","Tags"); + height: tagField.height; + font.weight: Font.Bold; + id: tagsLabel; + } + QtControls.TextField { + id: tagField; + anchors{ + leftMargin: Kirigami.Units.smallSpacing; + left: tagsLabel.right; + top: parent.top; + right: parent.right; + } + width: {parent.width - tagsLabel.width - Kirigami.Units.smallSpacing;} + + text: root.tags.length !== 0? root.tags.join(", "): ""; + placeholderText: i18nc("Placeholder tag field", "(No tags)"); + onEditingFinished: { + var tags = text.split(","); + for (var i in tags) { + tags[i] = tags[i].trim(); + } + root.tags = tags; + } + } + } + Item { + id: commentContainer; + anchors { + top: tagsContainer.bottom; + left: bookCover.right; + right: parent.right; + margins: Kirigami.Units.smallSpacing; + } + QtControls.Label { + text: i18nc("label for comment field","Comment"); + height: tagField.height; + font.weight: Font.Bold; + id: commentLabel; + } + QtControls.TextField { + id: commentField; + anchors{ + leftMargin: Kirigami.Units.smallSpacing; + left: commentLabel.right; + top: parent.top; + right: parent.right; + } + width: parent.width - commentLabel.width - Kirigami.Units.smallSpacing; + + text: root.comment !== ""? root.comment: ""; + placeholderText: i18nc("Placeholder comment field", "(No comment)"); + onEditingFinished: { + root.comment = text; + } + } + height: root.showCommentTags? childrenRect.height: 0; + visible: root.showCommentTags; + } + Item { + id: descriptionContainer; + anchors { + top: commentContainer.bottom; + left: bookCover.right; + right: parent.right; bottom: deleteBase.top; margins: Kirigami.Units.smallSpacing; } QtControls.Label { anchors.fill: parent; verticalAlignment: Text.AlignTop; - text: i18nc("Placeholder text for the book description field when no description is set", "(no description available for this book)"); - opacity: 0.3; + text: root.description.length !== 0? + root.description.join("\n\n"): + i18nc("Placeholder text for the book description field when no description is set", "(no description available for this book)"); + wrapMode: Text.WrapAtWordBoundaryOrAnywhere + opacity: root.description.length !== 0? 1.0: 0.3; } } Item { diff --git a/src/app/qml/listcomponents/BookTileTall.qml b/src/app/qml/listcomponents/BookTileTall.qml --- a/src/app/qml/listcomponents/BookTileTall.qml +++ b/src/app/qml/listcomponents/BookTileTall.qml @@ -31,7 +31,7 @@ id: root; property bool selected: false; property alias title: bookTitle.text; - property string author; + property var author: []; property string filename; property int categoryEntriesCount; property string currentPage; diff --git a/src/contentlist/ContentListerBase.cpp b/src/contentlist/ContentListerBase.cpp --- a/src/contentlist/ContentListerBase.cpp +++ b/src/contentlist/ContentListerBase.cpp @@ -64,6 +64,13 @@ int totalPages = data.attribute("peruse.totalPages").toInt(); metadata["totalPages"] = QVariant::fromValue(totalPages); } + if (!data.tags().isEmpty()) { + metadata["tags"] = QVariant::fromValue(data.tags()); + } + if (!data.userComment().isEmpty()) { + metadata["comment"] = QVariant::fromValue(data.userComment()); + } + metadata["rating"] = QVariant::fromValue(data.rating()); return metadata; } diff --git a/src/qtquick/ArchiveBookModel.cpp b/src/qtquick/ArchiveBookModel.cpp --- a/src/qtquick/ArchiveBookModel.cpp +++ b/src/qtquick/ArchiveBookModel.cpp @@ -354,7 +354,7 @@ AdvancedComicBookFormat::Document* acbfDocument = qobject_cast(acbfData()); if(acbfDocument) { - if(acbfDocument->metaData()->publishInfo()->publisher().length() > 0) + if(!acbfDocument->metaData()->publishInfo()->publisher().isEmpty()) { return acbfDocument->metaData()->publishInfo()->publisher(); } diff --git a/src/qtquick/BookDatabase.cpp b/src/qtquick/BookDatabase.cpp --- a/src/qtquick/BookDatabase.cpp +++ b/src/qtquick/BookDatabase.cpp @@ -58,7 +58,7 @@ return true; QSqlQuery q; - if (!q.exec(QLatin1String("create table books(filename varchar primary key, filetitle varchar, title varchar, series varchar, author varchar, publisher varchar, created datetime, lastOpenedTime datetime, totalPages integer, currentPage integer, thumbnail varchar)"))) { + if (!q.exec(QLatin1String("create table books(filename varchar primary key, filetitle varchar, title varchar, series varchar, author varchar, publisher varchar, created datetime, lastOpenedTime datetime, totalPages integer, currentPage integer, thumbnail varchar, description varchar, comment varchar, tags varchar, rating integer)"))) { qDebug() << "Database could not create the table books"; return false; } @@ -89,21 +89,25 @@ } QList entries; - QSqlQuery allEntries("SELECT filename, filetitle, title, series, author, publisher, created, lastOpenedTime, totalPages, currentPage, thumbnail FROM books"); + QSqlQuery allEntries("SELECT filename, filetitle, title, series, author, publisher, created, lastOpenedTime, totalPages, currentPage, thumbnail, description, comment, tags, rating FROM books"); while(allEntries.next()) { BookEntry* entry = new BookEntry(); entry->filename = allEntries.value(0).toString(); entry->filetitle = allEntries.value(1).toString(); entry->title = allEntries.value(2).toString(); - entry->series = allEntries.value(3).toString(); - entry->author = allEntries.value(4).toString(); + entry->series = allEntries.value(3).toString().split(","); + entry->author = allEntries.value(4).toString().split(","); entry->publisher = allEntries.value(5).toString(); entry->created = allEntries.value(6).toDateTime(); entry->lastOpenedTime = allEntries.value(7).toDateTime(); entry->totalPages = allEntries.value(8).toInt(); entry->currentPage = allEntries.value(9).toInt(); entry->thumbnail = allEntries.value(10).toString(); + entry->description = allEntries.value(11).toString().split(","); + entry->comment = allEntries.value(12).toString(); + entry->tags = allEntries.value(13).toString().split(","); + entry->rating = allEntries.value(14).toInt(); entries.append(entry); } @@ -119,20 +123,24 @@ qDebug() << "Adding newly discovered book to the database" << entry->filename; QSqlQuery newEntry; - newEntry.prepare("INSERT INTO books (filename, filetitle, title, series, author, publisher, created, lastOpenedTime, totalPages, currentPage, thumbnail) " - "VALUES (:filename, :filetitle, :title, :series, :author, :publisher, :created, :lastOpenedTime, :totalPages, :currentPage, :thumbnail)"); + newEntry.prepare("INSERT INTO books (filename, filetitle, title, series, author, publisher, created, lastOpenedTime, totalPages, currentPage, thumbnail, description, comment, tags, rating) " + "VALUES (:filename, :filetitle, :title, :series, :author, :publisher, :created, :lastOpenedTime, :totalPages, :currentPage, :thumbnail, :description, :comment, :tags, :rating)"); newEntry.bindValue(":filename", entry->filename); newEntry.bindValue(":filetitle", entry->filetitle); newEntry.bindValue(":title", entry->title); - newEntry.bindValue(":series", entry->series); - newEntry.bindValue(":author", entry->author); + newEntry.bindValue(":series", entry->series.join(",")); + newEntry.bindValue(":author", entry->author.join(",")); newEntry.bindValue(":publisher", entry->publisher); newEntry.bindValue(":publisher", entry->publisher); newEntry.bindValue(":created", entry->created); newEntry.bindValue(":lastOpenedTime", entry->lastOpenedTime); newEntry.bindValue(":totalPages", entry->totalPages); newEntry.bindValue(":currentPage", entry->currentPage); newEntry.bindValue(":thumbnail", entry->thumbnail); + newEntry.bindValue(":description", entry->description.join(",")); + newEntry.bindValue(":comment", entry->comment); + newEntry.bindValue(":tags", entry->tags.join(",")); + newEntry.bindValue(":rating", entry->rating); newEntry.exec(); d->closeDb(); diff --git a/src/qtquick/BookListModel.cpp b/src/qtquick/BookListModel.cpp --- a/src/qtquick/BookListModel.cpp +++ b/src/qtquick/BookListModel.cpp @@ -27,8 +27,10 @@ #include "AcbfAuthor.h" #include "AcbfSequence.h" +#include "AcbfBookinfo.h" #include +#include #include #include @@ -109,8 +111,12 @@ entries.append(entry); q->append(entry); titleCategoryModel->addCategoryEntry(entry->title.left(1).toUpper(), entry); - authorCategoryModel->addCategoryEntry(entry->author, entry); - seriesCategoryModel->addCategoryEntry(entry->series, entry); + for (int i=0; iauthor.size(); i++) { + authorCategoryModel->addCategoryEntry(entry->author.at(i), entry); + } + for (int i=0; iseries.size(); i++) { + seriesCategoryModel->addCategoryEntry(entry->series.at(i), entry); + } newlyAddedCategoryModel->append(entry, CreatedRole); QUrl url(entry->filename.left(entry->filename.lastIndexOf("/"))); folderCategoryModel->addCategoryEntry(url.path().mid(1), entry); @@ -192,7 +198,7 @@ if (!splitName.isEmpty()) entry->filetitle = splitName.takeLast(); if(!splitName.isEmpty()) - entry->series = splitName.takeLast(); // hahahaheuristics (dumb assumptions about filesystems, go!) + entry->series = QStringList(splitName.takeLast()); // hahahaheuristics (dumb assumptions about filesystems, go!) // just in case we end up without a title... using complete basename here, // as we would rather have "book one. part two" and the odd "book one - part two.tar" QFileInfo fileinfo(entry->filename); @@ -210,11 +216,16 @@ entry->thumbnail = QString("image://preview/").append(entry->filename); } + KFileMetaData::UserMetaData data(entry->filename); + entry->rating = data.rating(); + entry->comment = data.userComment(); + entry->tags = data.tags(); + QVariantHash metadata = d->contentModel->data(d->contentModel->index(first, 0, index), Qt::UserRole + 2).toHash(); QVariantHash::const_iterator it = metadata.constBegin(); for (; it != metadata.constEnd(); it++) { if(it.key() == QLatin1String("author")) - { entry->author = it.value().toString().trimmed(); } + { entry->author = it.value().toStringList(); } else if(it.key() == QLatin1String("title")) { entry->title = it.value().toString().trimmed(); } else if(it.key() == QLatin1String("publisher")) @@ -225,6 +236,12 @@ { entry->currentPage = it.value().toInt(); } else if(it.key() == QLatin1String("totalPages")) { entry->totalPages = it.value().toInt(); } + else if(it.key() == QLatin1String("comments")) + { entry->comment = it.value().toString();} + else if(it.key() == QLatin1Literal("tags")) + { entry->tags = it.value().toStringList();} + else if(it.key() == QLatin1String("rating")) + { entry->rating = it.value().toInt();} } // ACBF information is always preferred for CBRs, so let's just use that if it's there QMimeDatabase db; @@ -236,12 +253,17 @@ AdvancedComicBookFormat::Document* acbfDocument = qobject_cast(bookModel->acbfData()); if(acbfDocument) { for(AdvancedComicBookFormat::Sequence* sequence : acbfDocument->metaData()->bookInfo()->sequence()) { - entry->series = sequence->title(); - break; + entry->series.append(sequence->title()); } + for(AdvancedComicBookFormat::Author* author : acbfDocument->metaData()->bookInfo()->author()) { + entry->author.append(author->displayName()); + } + entry->description = acbfDocument->metaData()->bookInfo()->annotation(""); + } + + if (entry->author.isEmpty()) { + entry->author.append(bookModel->author()); } - // TODO extend the model to support multiple authors per book, ditto series/sequences - entry->author = bookModel->author(); entry->title = bookModel->title(); entry->publisher = bookModel->publisher(); entry->totalPages = bookModel->pageCount(); @@ -312,6 +334,17 @@ { entry->currentPage = value.toInt(); } + else if(property == "rating") + { + entry->rating = value.toInt(); + } + else if(property == "tags") + { + entry->tags = value.split(","); + } + else if(property == "comment") { + entry->comment = value; + } emit entryDataUpdated(entry); break; } diff --git a/src/qtquick/CategoryEntriesModel.h b/src/qtquick/CategoryEntriesModel.h --- a/src/qtquick/CategoryEntriesModel.h +++ b/src/qtquick/CategoryEntriesModel.h @@ -37,14 +37,18 @@ QString filename; QString filetitle; QString title; - QString series; - QString author; + QStringList series; + QStringList author; QString publisher; QDateTime created; QDateTime lastOpenedTime; int totalPages; int currentPage; QString thumbnail; + QStringList description; + QString comment; + QStringList tags; + int rating; }; /** @@ -84,7 +88,11 @@ CurrentPageRole, CategoryEntriesModelRole, CategoryEntryCountRole, - ThumbnailRole + ThumbnailRole, + DescriptionRole, + CommentRole, + TagsRole, + RatingRole }; /** diff --git a/src/qtquick/CategoryEntriesModel.cpp b/src/qtquick/CategoryEntriesModel.cpp --- a/src/qtquick/CategoryEntriesModel.cpp +++ b/src/qtquick/CategoryEntriesModel.cpp @@ -53,6 +53,10 @@ obj->setProperty("title", entry->title); obj->setProperty("totalPages", entry->totalPages); obj->setProperty("thumbnail", entry->thumbnail); + obj->setProperty("description", entry->description); + obj->setProperty("comment", entry->comment); + obj->setProperty("tags", entry->tags); + obj->setProperty("rating", QString::number(entry->rating)); return obj; } }; @@ -86,6 +90,10 @@ roles[CategoryEntriesModelRole] = "categoryEntriesModel"; roles[CategoryEntryCountRole] = "categoryEntriesCount"; roles[ThumbnailRole] = "thumbnail"; + roles[DescriptionRole] = "description"; + roles[CommentRole] = "comment"; + roles[TagsRole] = "tags"; + roles[RatingRole] = "rating"; return roles; } @@ -159,6 +167,18 @@ case ThumbnailRole: result.setValue(entry->thumbnail); break; + case DescriptionRole: + result.setValue(entry->description); + break; + case CommentRole: + result.setValue(entry->comment); + break; + case TagsRole: + result.setValue(entry->tags); + break; + case RatingRole: + result.setValue(entry->rating); + break; default: result.setValue(QString("Unknown role")); break; @@ -354,6 +374,13 @@ int totalPages = data.attribute("peruse.totalPages").toInt(); obj->setProperty("totalPages", QVariant::fromValue(totalPages)); } + obj->setProperty("rating", QVariant::fromValue(data.rating())); + if (!data.tags().isEmpty()) { + obj->setProperty("tags", QVariant::fromValue(data.tags())); + } + if (!data.userComment().isEmpty()) { + obj->setProperty("comment", QVariant::fromValue(data.userComment())); + } obj->setProperty("filename", filename); QString thumbnail; diff --git a/src/qtquick/PeruseConfig.h b/src/qtquick/PeruseConfig.h --- a/src/qtquick/PeruseConfig.h +++ b/src/qtquick/PeruseConfig.h @@ -113,6 +113,13 @@ * Creates a KFileMetaData::UserMetaData for this file, propery and value so the information is not lost when files are moved around outside of Peruse */ Q_INVOKABLE void setFilesystemProperty(QString fileName, QString propertyName, QString value); + /** + * @brief getFilesystemProperty + * @param fileName file name of the file to get data from. + * @param propertyName value of the proper to get data from. + * @return the value of the property. + */ + Q_INVOKABLE QString getFilesystemProperty(QString fileName, QString propertyName); private: class Private; Private* d; diff --git a/src/qtquick/PeruseConfig.cpp b/src/qtquick/PeruseConfig.cpp --- a/src/qtquick/PeruseConfig.cpp +++ b/src/qtquick/PeruseConfig.cpp @@ -160,5 +160,29 @@ void PeruseConfig::setFilesystemProperty(QString fileName, QString propertyName, QString value) { KFileMetaData::UserMetaData data(fileName); - data.setAttribute(QString("peruse.").append(propertyName), value); + if (propertyName == "rating") { + data.setRating(value.toInt()); + } else if (propertyName == "tags") { + data.setTags(value.split(",")); + } else if (propertyName == "comment") { + data.setUserComment(value); + } else { + data.setAttribute(QString("peruse.").append(propertyName), value); + } +} + +QString PeruseConfig::getFilesystemProperty(QString fileName, QString propertyName) +{ + QString value; + KFileMetaData::UserMetaData data(fileName); + if (propertyName == "rating") { + value = QString::number(data.rating()); + } else if (propertyName == "tags") { + value = data.tags().join(","); + } else if (propertyName == "comment") { + value = data.userComment(); + } else { + value = data.attribute(QString("peruse.").append(propertyName)); + } + return value; }