diff --git a/src/app/qml/Book.qml b/src/app/qml/Book.qml index 5e99184..5a0339d 100644 --- a/src/app/qml/Book.qml +++ b/src/app/qml/Book.qml @@ -1,523 +1,528 @@ /* * 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.Layouts 1.1 import QtQuick.Controls 2.2 as QtControls import QtQuick.Window 2.2 import org.kde.kirigami 2.1 as Kirigami import org.kde.peruse 0.1 as Peruse import "listcomponents" as ListComponents /** * @brief Page that handles reading the book. * * */ Kirigami.Page { id: root; objectName: "bookViewer"; clip: true; // Remove all the padding when we've hidden controls. Content is king! topPadding: applicationWindow().controlsVisible ? (applicationWindow() && applicationWindow().header ? applicationWindow().header.height : 0) : 0; leftPadding: applicationWindow().controlsVisible ? Kirigami.Units.gridUnit : 0; rightPadding: applicationWindow().controlsVisible ? Kirigami.Units.gridUnit : 0; bottomPadding: applicationWindow().controlsVisible ? Kirigami.Units.gridUnit : 0; background: Rectangle { anchors.fill: parent; opacity: applicationWindow().controlsVisible ? 0 : 1; Behavior on opacity { NumberAnimation { duration: applicationWindow().animationDuration; } } color: "black"; } // Perhaps we should store and restore this? property bool showControls: true; property Item pageStackItem: applicationWindow().pageStack.layers.currentItem; onPageStackItemChanged: { if(root.isCurrentPage) { applicationWindow().controlsVisible = root.showControls; } else { root.showControls = applicationWindow().controlsVisible; applicationWindow().controlsVisible = true; } } property bool rtlMode: false; /** * zoomMode: Peruse.Config.ZoomMode */ property int zoomMode: Peruse.Config.ZoomFull; property string file; property int currentPage; property int totalPages; onCurrentPageChanged: { // set off a timer to slightly postpone saving the current page, so it doesn't happen during animations etc updateCurrent.start(); } function nextPage() { if(viewLoader.item.currentPage < viewLoader.item.pageCount - 1) { viewLoader.item.currentPage++; } else { bookInfo.showBookInfo(file); } } function previousPage() { if(viewLoader.item.currentPage > 0) { viewLoader.item.currentPage--; } else { bookInfo.showBookInfo(file); } } function closeBook() { applicationWindow().contextDrawer.close(); // also for storing current page (otherwise postponed a bit after page change, done here as well to ensure it really happens) applicationWindow().controlsVisible = true; applicationWindow().pageStack.layers.pop(); applicationWindow().globalDrawer.open(); } property Item contextualTopItems: ListView { id: thumbnailNavigator; anchors.fill: parent; clip: true; delegate: thumbnailComponent; } Component { id: thumbnailComponent; Item { width: parent.width; height: Kirigami.Units.gridUnit * 6; MouseArea { anchors.fill: parent; onClicked: viewLoader.item.currentPage = model.index; } Rectangle { anchors.fill: parent; color: Kirigami.Theme.highlightColor; opacity: root.currentPage === model.index ? 1 : 0; Behavior on opacity { NumberAnimation { duration: Kirigami.Units.shortDuration; } } } Image { anchors { top: parent.top; left: parent.left; right: parent.right; margins: Kirigami.Units.smallSpacing; } height: parent.height - pageTitle.height - Kirigami.Units.smallSpacing * 2; asynchronous: true; fillMode: Image.PreserveAspectFit; source: model.url; } QtControls.Label { id: pageTitle; anchors { left: parent.left; right: parent.right; bottom: parent.bottom; } height: paintedHeight; text: model.title; elide: Text.ElideMiddle; horizontalAlignment: Text.AlignHCenter; } } } function toggleFullscreen() { applicationWindow().contextDrawer.close(); if(applicationWindow().visibility !== Window.FullScreen) { applicationWindow().visibility = Window.FullScreen; } else { applicationWindow().visibility = Window.AutomaticVisibility; } } property list mobileActions: [ Kirigami.Action { text: applicationWindow().visibility !== Window.FullScreen ? i18nc("Enter full screen mode on a touch-based device", "Go Full Screen") : i18nc("Exit full sceen mode on a touch based device", "Exit Full Screen"); iconName: "view-fullscreen"; onTriggered: toggleFullscreen(); enabled: root.isCurrentPage && applicationWindow().deviceType === applicationWindow().deviceTypePhone; }, Kirigami.Action { text: i18nc("Action used on touch devices to close the currently open book and return to whatever page was most recently shown", "Close Book"); shortcut: bookInfo.sheetOpen ? "" : "Esc"; iconName: "dialog-close"; onTriggered: closeBook(); enabled: root.isCurrentPage && applicationWindow().deviceType === applicationWindow().deviceTypePhone; } ] property list desktopActions: [ Kirigami.Action { text: i18nc("Top level entry leading to a submenu with options for the book display", "View Options"); iconName: "configure"; QtObject { property string text: "Reading Direction" } Kirigami.Action { text: "Left to Right" iconName: "format-text-direction-ltr"; shortcut: rtlMode ? "r" : ""; enabled: root.isCurrentPage && applicationWindow().deviceType === applicationWindow().deviceTypeDesktop && root.rtlMode === true; onTriggered: { root.rtlMode = false; } } Kirigami.Action { text: "Right to Left" iconName: "format-text-direction-rtl"; shortcut: rtlMode ? "" : "r"; enabled: root.isCurrentPage && applicationWindow().deviceType === applicationWindow().deviceTypeDesktop && root.rtlMode === false; onTriggered: { root.rtlMode = true; } } QtObject {} // QtObject { // property string text: "Zoom" // } // Kirigami.Action { // text: "Fit full page" // iconName: "zoom-fit-best"; // enabled: root.isCurrentPage && applicationWindow().deviceType === applicationWindow().deviceTypeDesktop && root.zoomMode !== Peruse.Config.ZoomFull; // onTriggered: { root.zoomMode = Peruse.Config.ZoomFull; } // } // Kirigami.Action { // text: "Fit width" // iconName: "zoom-fit-width"; // enabled: root.isCurrentPage && applicationWindow().deviceType === applicationWindow().deviceTypeDesktop && root.zoomMode !== Peruse.Config.ZoomFitWidth; // onTriggered: { root.zoomMode = Peruse.Config.ZoomFitWidth; } // } // Kirigami.Action { // text: "Fit height" // iconName: "zoom-fit-height"; // enabled: root.isCurrentPage && applicationWindow().deviceType === applicationWindow().deviceTypeDesktop && root.zoomMode !== Peruse.Config.ZoomFitHeight; // onTriggered: { root.zoomMode = Peruse.Config.ZoomFitHeight; } // } // QtObject {} }, Kirigami.Action { text: i18nc("Go to the previous page in the book", "Previous Page"); shortcut: root.isCurrentPage && bookInfo.sheetOpen ? "" : StandardKey.MoveToPreviousChar; iconName: "go-previous"; onTriggered: previousPage(); enabled: root.isCurrentPage && applicationWindow().deviceType === applicationWindow().deviceTypeDesktop; }, Kirigami.Action { text: i18nc("Go to the next page in the book", "Next Page"); shortcut: bookInfo.sheetOpen ? "" : StandardKey.MoveToNextChar; iconName: "go-next"; onTriggered: nextPage(); enabled: root.isCurrentPage && applicationWindow().deviceType === applicationWindow().deviceTypeDesktop; }, Kirigami.Action { text: applicationWindow().visibility !== Window.FullScreen ? i18nc("Enter full screen mode on a non-touch-based device", "Go Full Screen") : i18nc("Exit full sceen mode on a non-touch based device", "Exit Full Screen"); shortcut: (applicationWindow().visibility === Window.FullScreen) ? (bookInfo.sheetOpen ? "" : "Esc") : "f"; iconName: "view-fullscreen"; onTriggered: toggleFullscreen(); enabled: root.isCurrentPage && applicationWindow().deviceType === applicationWindow().deviceTypeDesktop; }, Kirigami.Action { text: i18nc("Action used on non-touch devices to close the currently open book and return to whatever page was most recently shown", "Close Book"); shortcut: (applicationWindow().visibility === Window.FullScreen) ? "" : (bookInfo.sheetOpen ? "" : "Esc"); iconName: "dialog-close"; onTriggered: closeBook(); enabled: root.isCurrentPage && applicationWindow().deviceType === applicationWindow().deviceTypeDesktop; }, // Invisible actions, for use in bookInfo Kirigami.Action { visible: false; shortcut: bookInfo.sheetOpen ? StandardKey.MoveToPreviousChar : ""; onTriggered: bookInfo.previousBook(); enabled: root.isCurrentPage && applicationWindow().deviceType === applicationWindow().deviceTypeDesktop; }, Kirigami.Action { visible: false; shortcut: bookInfo.sheetOpen ? StandardKey.MoveToNextChar : ""; onTriggered: bookInfo.nextBook(); enabled: root.isCurrentPage && applicationWindow().deviceType === applicationWindow().deviceTypeDesktop; }, Kirigami.Action { visible: false; shortcut: bookInfo.sheetOpen ? "Return" : ""; onTriggered: bookInfo.openSelected(); enabled: root.isCurrentPage && applicationWindow().deviceType === applicationWindow().deviceTypeDesktop; } ] actions { contextualActions: applicationWindow().deviceType === applicationWindow().deviceTypePhone ? mobileActions : desktopActions; main: bookInfo.sheetOpen ? bookInfoAction : mainBookAction; } Kirigami.Action { id: mainBookAction; text: applicationWindow().visibility !== Window.FullScreen ? i18nc("Enter full screen mode on any device type", "Go Full Screen") : i18nc("Exit full screen mode on any device type", "Exit Full Screen"); iconName: "view-fullscreen"; onTriggered: toggleFullscreen(); enabled: root.isCurrentPage; } Kirigami.Action { id: bookInfoAction; text: i18nc("Closes the book information drawer", "Close"); shortcut: bookInfo.sheetOpen ? "Esc" : ""; iconName: "dialog-cancel"; onTriggered: bookInfo.close(); enabled: root.isCurrentPage; } /** * This holds an instance of ViewerBase, which can either be the * Okular viewer(the fallback one), or one of the type specific * ones(ImageBrowser based). */ Item { width: root.width - (root.leftPadding + root.rightPadding); height: root.height - (root.topPadding + root.bottomPadding); Timer { id: updateCurrent; interval: applicationWindow().animationDuration; running: false; repeat: false; onTriggered: { if(viewLoader.item && viewLoader.item.pagesModel && viewLoader.item.pagesModel.currentPage !== undefined) { viewLoader.item.pagesModel.currentPage = root.currentPage; } } } NumberAnimation { id: thumbnailMovementAnimation; target: thumbnailNavigator; property: "contentY"; duration: applicationWindow().animationDuration; easing.type: Easing.InOutQuad; } Loader { id: viewLoader; anchors.fill: parent; property bool loadingCompleted: false; onLoaded: { if(status === Loader.Error) { // huh! problem... } else { item.file = root.file; } } Binding { target: viewLoader.item; property: "rtlMode"; value: root.rtlMode; } Binding { target: viewLoader.item; property: "zoomMode"; value: root.zoomMode; } Connections { target: viewLoader.item; onLoadingCompleted: { if(success) { thumbnailNavigator.model = viewLoader.item.pagesModel; if(viewLoader.item.thumbnailComponent) { thumbnailNavigator.delegate = viewLoader.item.thumbnailComponent; } else { thumbnailNavigator.delegate = thumbnailComponent; } peruseConfig.setFilesystemProperty(root.file, "totalPages", viewLoader.item.pageCount); if(root.totalPages !== viewLoader.item.pageCount) { root.totalPages = viewLoader.item.pageCount; } viewLoader.item.currentPage = root.currentPage; viewLoader.loadingCompleted = true; root.title = viewLoader.item.title; applicationWindow().globalDrawer.close(); } } onCurrentPageChanged: { if(root.currentPage !== viewLoader.item.currentPage && viewLoader.loadingCompleted) { root.currentPage = viewLoader.item.currentPage; } thumbnailMovementAnimation.running = false; var currentPos = thumbnailNavigator.contentY; var newPos; thumbnailNavigator.positionViewAtIndex(viewLoader.item.currentPage, ListView.Center); newPos = thumbnailNavigator.contentY; thumbnailMovementAnimation.from = currentPos; thumbnailMovementAnimation.to = newPos; thumbnailMovementAnimation.running = true; } onGoNextPage: root.nextPage(); onGoPreviousPage: root.previousPage(); } } } /** * Overlay with book information and a series selection. */ Kirigami.OverlaySheet { id: bookInfo; function setNewCurrentIndex(newIndex) { seriesListAnimation.running = false; var currentPos = seriesListView.contentX; var newPos; seriesListView.positionViewAtIndex(newIndex, ListView.Center); newPos = seriesListView.contentX; seriesListAnimation.from = currentPos; seriesListAnimation.to = newPos; seriesListAnimation.running = true; seriesListView.currentIndex = newIndex; } function nextBook() { if(seriesListView.currentIndex < seriesListView.model.rowCount()) { setNewCurrentIndex(seriesListView.currentIndex + 1); } } function previousBook() { if(seriesListView.currentIndex > 0) { setNewCurrentIndex(seriesListView.currentIndex - 1); } } function openSelected() { if (detailsTile.filename!==root.file) { closeBook(); applicationWindow().showBook(detailsTile.filename, detailsTile.currentPage); } } function showBookInfo(filename) { if(sheetOpen) { return; } seriesListView.model = contentList.seriesModelForEntry(filename); if (seriesListView.model) { setNewCurrentIndex(seriesListView.model.indexOfFile(filename)); } open(); } onSheetOpenChanged: { if(sheetOpen === false) { applicationWindow().controlsVisible = controlsShown; } else { controlsShown = applicationWindow().controlsVisible; applicationWindow().controlsVisible = true; } } 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; width: root.width - Kirigami.Units.largeSpacing * 2; height: childrenRect.height + Kirigami.Units.largeSpacing * 2; spacing: Kirigami.Units.largeSpacing; ListComponents.BookTile { id: detailsTile; height: neededHeight; width: parent.width; author: bookInfo.currentBook.readProperty("author"); publisher: bookInfo.currentBook.readProperty("publisher"); title: bookInfo.currentBook.readProperty("title"); filename: bookInfo.currentBook.readProperty("filename"); thumbnail: bookInfo.currentBook.readProperty("thumbnail"); categoryEntriesCount: 0; currentPage: bookInfo.currentBook.readProperty("currentPage"); totalPages: bookInfo.currentBook.readProperty("totalPages"); + description: bookInfo.currentBook.readProperty("description"); onBookSelected: { if(root.file !== filename) { openSelected(); } } onBookDeleteRequested: { // Not strictly needed for the listview itself, but it's kind of // nice for making sure the details tile is right var oldIndex = seriesListView.currentIndex; seriesListView.currentIndex = -1; contentList.removeBook(detailsTile.filename, true); seriesListView.currentIndex = oldIndex; } } // tags and ratings, comment by self // store hook for known series with more content ListView { id: seriesListView; width: parent.width; height: Kirigami.Units.gridUnit * 12; 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; filename: model.filename; thumbnail: model.thumbnail; categoryEntriesCount: 0; currentPage: model.currentPage; totalPages: model.totalPages; onBookSelected:{ if (seriesListView.currentIndex !== model.index) { bookInfo.setNewCurrentIndex(model.index); } else { bookInfo.openSelected(); } } selected: seriesListView.currentIndex === model.index; } onCurrentIndexChanged: { bookInfo.currentBook = model.get(currentIndex); } } } } onFileChanged: { // Let's set the page title to something useful var book = contentList.bookFromFile(file); root.title = book.readProperty("title"); // The idea is to have a number of specialised options as relevant to various // types of comic books, and then finally fall back to Okular as a catch-all // but generic viewer component. var attemptFallback = true; var mimetype = contentList.contentModel.getMimetype(file); console.debug("Mimetype is " + mimetype); if(mimetype == "application/x-cbz" || mimetype == "application/x-cbr" || mimetype == "application/vnd.comicbook+zip" || mimetype == "application/vnd.comicbook+rar") { viewLoader.source = "viewers/cbr.qml"; attemptFallback = false; } if(mimetype == "inode/directory" || mimetype == "image/jpeg" || mimetype == "image/png") { viewLoader.source = "viewers/folderofimages.qml"; attemptFallback = false; } if(attemptFallback) { viewLoader.source = "viewers/okular.qml"; } } } diff --git a/src/app/qml/Bookshelf.qml b/src/app/qml/Bookshelf.qml index 90eb316..8d4497c 100644 --- a/src/app/qml/Bookshelf.qml +++ b/src/app/qml/Bookshelf.qml @@ -1,219 +1,221 @@ /* * 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.Layouts 1.1 import QtQuick.Controls 1.4 as QtControls import org.kde.kirigami 2.1 as Kirigami import org.kde.peruse 0.1 as Peruse import "listcomponents" as ListComponents /** * @brief This lays out the books and categories. * * It uses BookTileTall for the books and CategoryTileTall * for showing the categories. Categories can be selected to open * a new bookshelf from the right, showing the entries in that * subcategory. This is particularly in use with the folder category. * * There is also access to the SearchBox, and it is possible to access * a BookTile by press+holding the thumbnail. * This holds information about the book. */ Kirigami.ScrollablePage { id: root; title: headerText; property string categoryName: "bookshelf"; objectName: "bookshelf"; property alias model: shelfList.model; property string sectionRole: "title"; property int sectionCriteria: ViewSection.FirstCharacter; signal bookSelected(string filename, int currentPage); property string headerText; function openBook(index) { applicationWindow().contextDrawer.close(); if(shelfList.model.indexIsBook(index)) { var book = shelfList.model.get(index); root.bookSelected(book.readProperty("filename"), book.readProperty("currentPage")); } else { var catEntry = shelfList.model.getEntry(index); applicationWindow().pageStack.push(bookshelf, { focus: true, headerText: catEntry.readProperty("title"), model: catEntry.readProperty("entriesModel") }); } } function closeShelf() { applicationWindow().contextDrawer.close(); applicationWindow().pageStack.pop(); } property list mobileActions; property list desktopActions: [ Kirigami.Action { text: i18nc("Navigate one page back", "Back"); shortcut: bookDetails.sheetOpen ? "" : "Esc"; iconName: "dialog-close"; onTriggered: closeShelf(); enabled: root.isCurrentPage && applicationWindow().deviceType === applicationWindow().deviceTypeDesktop && applicationWindow().pageStack.currentIndex > 0; }, // Kirigami.Action { // text: i18nc("Select the previous book in the list", "Select Previous Book"); // shortcut: StandardKey.MoveToPreviousChar // iconName: "go-previous"; // onTriggered: shelfList.previousEntry(); // enabled: root.isCurrentPage && applicationWindow().deviceType === applicationWindow().deviceTypeDesktop; // }, // Kirigami.Action { // text: i18nc("Select the next book in the list", "Select Next Book"); // shortcut: StandardKey.MoveToNextChar; // iconName: "go-next"; // onTriggered: shelfList.nextEntry(); // enabled: root.isCurrentPage && applicationWindow().deviceType === applicationWindow().deviceTypeDesktop; // }, 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; } ] actions { contextualActions: PLASMA_PLATFORM.substring(0, 5) === "phone" ? mobileActions : desktopActions; main: bookDetails.sheetOpen ? bookDetailsAction : mainShelfAction; } Kirigami.Action { id: mainShelfAction; text: i18nc("search in the list of books (not inside the books)", "Search Books"); iconName: "system-search"; onTriggered: searchBox.activate(); enabled: root.isCurrentPage; } Kirigami.Action { id: bookDetailsAction; text: i18n("Closes the book details drawer", "Close"); shortcut: bookDetails.sheetOpen ? "Esc" : ""; iconName: "dialog-cancel"; onTriggered: bookDetails.close(); enabled: root.isCurrentPage; } GridView { id: shelfList; SearchBox { id: searchBox; anchors { top: parent.top; left: parent.left; right: parent.right; } maxHeight: parent.height; model: root.model; onBookSelected: root.bookSelected(filename, currentPage); } clip: true; footer: Item { width: parent.width; height: Kirigami.Units.iconSizes.large + Kirigami.Units.largeSpacing; } cellWidth: width / 2; cellHeight: root.height * 3 / 8; currentIndex: -1; function previousEntry() { if(currentIndex > 0) { currentIndex--; } } function nextEntry() { if(currentIndex < model.rowCount() - 1) { currentIndex++; } } delegate: Item { height: model.categoryEntriesCount === 0 ? bookTile.neededHeight : categoryTile.neededHeight; width: root.width / 2; ListComponents.CategoryTileTall { id: categoryTile; height: model.categoryEntriesCount > 0 ? neededHeight : 0; width: parent.width; count: model.categoryEntriesCount; title: model.title; entriesModel: model.categoryEntriesModel ? model.categoryEntriesModel : null; selected: shelfList.currentIndex === index; } ListComponents.BookTileTall { id: bookTile; height: model.categoryEntriesCount < 1 ? neededHeight : 0; width: parent.width; author: model.author ? model.author : i18nc("used for the author data in book lists if author is empty", "(unknown)"); title: model.title; filename: model.filename; thumbnail: model.categoryEntriesCount < 1 ? model.thumbnail : ""; categoryEntriesCount: model.categoryEntriesCount; currentPage: model.currentPage; totalPages: model.totalPages; onBookSelected: root.bookSelected(filename, currentPage); selected: shelfList.currentIndex === index; onPressAndHold: bookDetails.showBookInfo(model.index); pressIndicator: true; } } } Kirigami.OverlaySheet { id: bookDetails; function showBookInfo(index) { currentBook = root.model.getEntry(index); open(); } property QtObject currentBook: fakeBook; property QtObject fakeBook: Peruse.PropertyContainer { property string author: ""; property string title: ""; property string filename: ""; property string publisher: ""; property string thumbnail: ""; property string currentPage: "0"; property string totalPages: "0"; + property string comment: ""; } ListComponents.BookTile { id: detailsTile; height: neededHeight; width: shelfList.width - Kirigami.Units.largeSpacing * 2; author: bookDetails.currentBook.readProperty("author"); publisher: bookDetails.currentBook.readProperty("publisher"); title: bookDetails.currentBook.readProperty("title"); filename: bookDetails.currentBook.readProperty("filename"); thumbnail: bookDetails.currentBook.readProperty("thumbnail"); categoryEntriesCount: 0; currentPage: bookDetails.currentBook.readProperty("currentPage"); totalPages: bookDetails.currentBook.readProperty("totalPages"); + description: bookDetails.currentBook.readProperty("description"); onBookSelected: { bookDetails.close(); applicationWindow().showBook(filename, currentPage); } onBookDeleteRequested: { contentList.removeBook(detailsTile.filename, true); close(); } } } } diff --git a/src/app/qml/listcomponents/BookTile.qml b/src/app/qml/listcomponents/BookTile.qml index 3282019..987cf7a 100644 --- a/src/app/qml/listcomponents/BookTile.qml +++ b/src/app/qml/listcomponents/BookTile.qml @@ -1,258 +1,415 @@ /* * 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.1 import QtQuick.Controls 2.2 as QtControls import org.kde.kirigami 2.1 as Kirigami /** * @brief small window with book information. * * This shows a bit of information about the book and gives a * selector with the other books in the series. * * It pops up after finishing a book in Book, and when pressing long * on a BookTileTall item in BookShelf. * */ Item { 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; opacity: root.selected ? 1 : 0; Behavior on opacity { NumberAnimation { duration: Kirigami.Units.shortDuration; } } } Item { id: bookCover; anchors { top: parent.top; left: parent.left; } width: root.width / 3; height: width * 1.5; Image { id: coverImage; anchors { fill: parent; margins: Kirigami.Units.smallSpacing; } asynchronous: true; fillMode: Image.PreserveAspectFit; } MouseArea { anchors.fill: parent; onClicked: root.bookSelected(root.filename, root.currentPage); } } Kirigami.Heading { id: bookTitle; anchors { top: parent.top; leftMargin: Kirigami.Units.smallSpacing; left: bookCover.right; right: parent.right; } maximumLineCount: 1; elide: Text.ElideMiddle; font.weight: Font.Bold; MouseArea { anchors.fill: parent; onClicked: root.bookSelected(root.filename, root.currentPage); } Rectangle { anchors { left: parent.left; top: parent.baseline; topMargin: 2; } height: 2; width: parent.paintedWidth; color: Kirigami.Theme.linkColor; } } QtControls.Label { id: bookAuthorLabel; anchors { top: bookTitle.bottom; left: bookCover.right; 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; anchors { top: bookTitle.bottom; left: bookAuthorLabel.right; leftMargin: Kirigami.Units.smallSpacing; 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 { id: bookPublisherLabel; anchors { top: bookAuthorLabel.bottom; left: bookCover.right; 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; anchors { top: bookAuthor.bottom; left: bookPublisherLabel.right; leftMargin: Kirigami.Units.smallSpacing; right: parent.right; } elide: Text.ElideRight; text: root.publisher === "" ? "(unknown)" : root.publisher; opacity: (text === "(unknown)" || text === "") ? 0.3 : 1; } QtControls.Label { id: bookFile; anchors { top: bookPublisherLabel.bottom; left: bookCover.right; leftMargin: Kirigami.Units.smallSpacing; right: parent.right; } elide: Text.ElideMiddle; opacity: 0.3; font.pointSize: Kirigami.Theme.defaultFont.pointSize * 0.8; 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 { id: deleteBase; anchors { left: bookCover.right; leftMargin: Kirigami.Units.smallSpacing; right: parent.right; bottom: parent.bottom; } height: deleteButton.height + Kirigami.Units.smallSpacing * 2; Behavior on height { PropertyAnimation { duration: applicationWindow().animationDuration; easing.type: Easing.InOutQuad; } } states: [ State { name: "confirmDelete"; PropertyChanges { target: deleteButton; opacity: 0; } PropertyChanges { target: deleteConfirmBase; opacity: 1; } PropertyChanges { target: deleteBase; height: deleteConfirmBase.height; } } ] QtControls.Button { id: deleteButton; text: i18nc("Spawn inline dialog box to confirm permanent removal of this book", "Delete from Device"); anchors { bottom: parent.bottom; right: parent.right; margins: Kirigami.Units.smallSpacing; } // iconName: "edit-delete"; onClicked: deleteBase.state = "confirmDelete"; Behavior on opacity { PropertyAnimation { duration: applicationWindow().animationDuration; easing.type: Easing.InOutQuad; } } } Item { id: deleteConfirmBase; opacity: 0; width: parent.width; Behavior on opacity { PropertyAnimation { duration: applicationWindow().animationDuration; easing.type: Easing.InOutQuad; } } height: yesDelete.height + confirmDeleteLabel.height + Kirigami.Units.largeSpacing * 2 + Kirigami.Units.smallSpacing; QtControls.Label { id: confirmDeleteLabel; anchors { top: parent.top; topMargin: Kirigami.Units.largeSpacing; left: parent.left; right: parent.right; } height: paintedHeight; wrapMode: Text.WordWrap; horizontalAlignment: Text.AlignHCenter; text: i18nc("Dialog text for delete book dialog", "Are you sure you want to delete this from your device?"); } QtControls.Button { id: yesDelete; anchors { top: confirmDeleteLabel.bottom; topMargin: Kirigami.Units.smallSpacing; right: parent.horizontalCenter; rightMargin: (parent.width - width) / 4; } text: i18nc("Confirmation button for book delete dialog", "Yes, Really Delete"); // iconName: "dialog-ok"; onClicked: root.bookDeleteRequested(); } QtControls.Button { anchors { top: confirmDeleteLabel.bottom; topMargin: Kirigami.Units.smallSpacing; left: parent.horizontalCenter; leftMargin: (parent.width - width) / 4; } text: i18nc("Cancellation button or book delete dialog", "No, Cancel Delete"); // iconName: "dialog-cancel"; onClicked: deleteBase.state = ""; } } } } diff --git a/src/app/qml/listcomponents/BookTileTall.qml b/src/app/qml/listcomponents/BookTileTall.qml index 4c76d1e..2f40d94 100644 --- a/src/app/qml/listcomponents/BookTileTall.qml +++ b/src/app/qml/listcomponents/BookTileTall.qml @@ -1,168 +1,168 @@ /* * Copyright (C) 2016 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.1 import QtQuick.Controls 2.2 as QtControls import org.kde.kirigami 2.1 as Kirigami /** * @brief A button to select a book to read with a nice big thumbnail. */ Item { 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; property string totalPages; property double progress: currentPage / totalPages; property string thumbnail; property bool pressIndicator: false; signal bookSelected(string filename, int currentPage); /// FIXME This signal will also forward the MouseEvent, but the type is not recognised, so we can't /// add it to the signature. Certainly would be nice if that were possible, though, right? /// @see https://bugreports.qt.io/browse/QTBUG-41441 signal pressAndHold(); property int neededHeight: bookCover.height + bookTitle.height + Kirigami.Units.largeSpacing; visible: height > 0; enabled: visible; clip: true; MouseArea { anchors.fill: parent; onClicked: root.bookSelected(root.filename, root.currentPage); onPressAndHold: root.pressAndHold(mouse); onPressed: root.pressIndicator ? pressIndicatorAnimation.start():0; onReleased: {pressIndicatorAnimation.stop(); pressIndicator.width = 0;pressIndicator.height = 0;} // FIXME The duration should ideally be the pressHold interval. ParallelAnimation { id: pressIndicatorAnimation; NumberAnimation { target: pressIndicator; from: coverImage.paintedWidth/3; to: coverOutline.width; property: "width"; duration: 800; } NumberAnimation { target: pressIndicator; from: coverImage.paintedWidth/3; to: coverOutline.height; property: "height"; duration: 800; } NumberAnimation { target: pressIndicator; from: coverImage.paintedWidth/3; to: 0; property: "radius"; duration: 800; } } } Item { id: bookCover; anchors { top: parent.top; horizontalCenter: parent.horizontalCenter; margins: Kirigami.Units.largeSpacing; } width: Math.min(parent.width - Kirigami.Units.largeSpacing * 2, Kirigami.Units.iconSizes.enormous + Kirigami.Units.largeSpacing * 2); height: width; Rectangle { anchors { fill: coverOutline; margins: -Kirigami.Units.smallSpacing; } radius: Kirigami.Units.smallSpacing; color: Kirigami.Theme.highlightColor; opacity: root.selected ? 1 : 0; Behavior on opacity { NumberAnimation { duration: Kirigami.Units.shortDuration; } } } Rectangle { id: coverOutline; anchors.centerIn: coverImage; width: Math.max(coverImage.paintedWidth, Kirigami.Units.iconSizes.large) + Kirigami.Units.smallSpacing * 2; height: Math.max(coverImage.paintedHeight, Kirigami.Units.iconSizes.large) + Kirigami.Units.smallSpacing * 2; color: Kirigami.Theme.viewBackgroundColor; border { width: 2; color: Kirigami.Theme.viewTextColor; } radius: 2; } Image { id: coverImage; anchors { fill: parent; margins: Kirigami.Units.largeSpacing; } source: root.thumbnail; asynchronous: true; fillMode: Image.PreserveAspectFit; } QtControls.BusyIndicator { id: loadingSpinner; anchors.centerIn: parent; visible: running; running: coverImage.status === Image.Loading; } Rectangle{ id: pressIndicator; anchors.centerIn: coverImage; width: 0; height: 0; color: "transparent"; border.color:Kirigami.Theme.highlightColor; border.width:Kirigami.Units.smallSpacing; } } QtControls.Label { id: bookTitle; anchors { top: bookCover.bottom; left: parent.left; right: parent.right; margins: Kirigami.Units.smallSpacing; topMargin: 0; } height: paintedHeight; maximumLineCount: 2; wrapMode: Text.WrapAtWordBoundaryOrAnywhere; elide: Text.ElideMiddle; horizontalAlignment: Text.AlignHCenter; } QtControls.ProgressBar { anchors { top: bookCover.bottom; topMargin: -Kirigami.Units.smallSpacing; left: bookCover.left; right: bookCover.right; bottom: bookTitle.top; } visible: value > 0; value: root.progress > 0 && root.progress <= 1 ? root.progress : 0; } } diff --git a/src/contentlist/ContentListerBase.cpp b/src/contentlist/ContentListerBase.cpp index b137937..556e431 100644 --- a/src/contentlist/ContentListerBase.cpp +++ b/src/contentlist/ContentListerBase.cpp @@ -1,69 +1,76 @@ /* * 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 "ContentListerBase.h" #include #include #include #include ContentListerBase::ContentListerBase(QObject* parent) : QObject(parent) { } ContentListerBase::~ContentListerBase() { } void ContentListerBase::startSearch(const QList& queries) { Q_UNUSED(queries); } QVariantMap ContentListerBase::metaDataForFile(const QString& file) { QVariantMap metadata; //TODO: This should include the same information for both the Baloo and //File searchers. Unfortunately, currently KFileMetaData does not seem able //to provide this. So this needs changes at a lower level. QFileInfo info(file); metadata["lastModified"] = info.lastModified(); metadata["created"] = info.created(); metadata["lastRead"] = info.lastRead(); KFileMetaData::UserMetaData data(file); if (data.hasAttribute("peruse.currentPage")) { int currentPage = data.attribute("peruse.currentPage").toInt(); metadata["currentPage"] = QVariant::fromValue(currentPage); } if (data.hasAttribute("peruse.totalPages")) { 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 index 0e85e33..62b9354 100644 --- a/src/qtquick/ArchiveBookModel.cpp +++ b/src/qtquick/ArchiveBookModel.cpp @@ -1,1254 +1,1254 @@ /* * 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 #include "KRar.h" // "" because it's a custom thing for now 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 { qDebug() << "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().length() > 0) + 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(); qDebug() << "Creating archive in" << archiveFileName; KZip* archive = new KZip(archiveFileName); archive->open(QIODevice::ReadWrite); // We're a zip file... size isn't used qDebug() << "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()); qDebug() << "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(); qDebug() << "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(); qDebug() << "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)) { qDebug() << "Copying all content from" << archiveFileName << "to" << actualFile; while(!originFile.atEnd()) { destinationFile.write(originFile.read(65536)); qApp->processEvents(); } destinationFile.close(); originFile.close(); if(originFile.remove()) { qDebug() << "Success! Now loading the new archive..."; // now load the new thing... setFilename(actualFile); } else { qWarning() << "Failed to delete" << originFile.fileName(); } } else { qWarning() << "Failed to open" << originFile.fileName() << "for reading"; } } else { qWarning() << "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); } // 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")) { filedata.setUserComment(xmlReader.readElementText()); } else if(xmlReader.name() == QStringLiteral("Tags")) { filedata.setTags(xmlReader.readElementText().split(",")); } 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 { qWarning() << 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()) { qWarning() << Q_FUNC_INFO << "Failed to read Comic Info XML document at token" << xmlReader.name() << "(" << xmlReader.lineNumber() << ":" << xmlReader.columnNumber() << ") The reported error was:" << xmlReader.errorString(); } qDebug() << 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()); } } //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 { qWarning() << 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()) { qWarning() << Q_FUNC_INFO << "Failed to read CoMet document at token" << xmlReader.name() << "(" << xmlReader.lineNumber() << ":" << xmlReader.columnNumber() << ") The reported error was:" << xmlReader.errorString(); } qDebug() << 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/BookDatabase.cpp b/src/qtquick/BookDatabase.cpp index 383e3fb..5778f59 100644 --- a/src/qtquick/BookDatabase.cpp +++ b/src/qtquick/BookDatabase.cpp @@ -1,139 +1,147 @@ /* * Copyright (C) 2017 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 "BookDatabase.h" #include "CategoryEntriesModel.h" #include #include #include #include #include #include class BookDatabase::Private { public: Private() { db = QSqlDatabase::addDatabase("QSQLITE"); QDir location{QStandardPaths::writableLocation(QStandardPaths::AppDataLocation)}; if(!location.exists()) location.mkpath("."); dbfile = location.absoluteFilePath("library.sqlite"); db.setDatabaseName(dbfile); } QSqlDatabase db; QString dbfile; bool prepareDb() { if (!db.open()) { qDebug() << "Failed to open the book database file" << dbfile << db.lastError(); return false; } QStringList tables = db.tables(); if (tables.contains("books", Qt::CaseInsensitive)) 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; } return true; } void closeDb() { db.close(); } }; BookDatabase::BookDatabase(QObject* parent) : QObject(parent) , d(new Private) { } BookDatabase::~BookDatabase() { delete d; } QList BookDatabase::loadEntries() { if(!d->prepareDb()) { return QList(); } 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); } d->closeDb(); return entries; } void BookDatabase::addEntry(BookEntry* entry) { if(!d->prepareDb()) { return; } 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 index 68fe570..81e202e 100644 --- a/src/qtquick/BookListModel.cpp +++ b/src/qtquick/BookListModel.cpp @@ -1,346 +1,379 @@ /* * 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 "BookListModel.h" #include "BookDatabase.h" #include "CategoryEntriesModel.h" #include "ArchiveBookModel.h" #include "AcbfAuthor.h" #include "AcbfSequence.h" +#include "AcbfBookinfo.h" #include +#include #include #include #include #include #include #include class BookListModel::Private { public: Private() : contentModel(nullptr) , titleCategoryModel(nullptr) , newlyAddedCategoryModel(nullptr) , authorCategoryModel(nullptr) , seriesCategoryModel(nullptr) , folderCategoryModel(nullptr) , cacheLoaded(false) { db = new BookDatabase(); }; ~Private() { qDeleteAll(entries); db->deleteLater(); } QList entries; QAbstractListModel* contentModel; CategoryEntriesModel* titleCategoryModel; CategoryEntriesModel* newlyAddedCategoryModel; CategoryEntriesModel* authorCategoryModel; CategoryEntriesModel* seriesCategoryModel; CategoryEntriesModel* folderCategoryModel; BookDatabase* db; bool cacheLoaded; void initializeSubModels(BookListModel* q) { if(!titleCategoryModel) { titleCategoryModel = new CategoryEntriesModel(q); connect(q, SIGNAL(entryDataUpdated(BookEntry*)), titleCategoryModel, SIGNAL(entryDataUpdated(BookEntry*))); connect(q, SIGNAL(entryRemoved(BookEntry*)), titleCategoryModel, SIGNAL(entryRemoved(BookEntry*))); emit q->titleCategoryModelChanged(); } if(!newlyAddedCategoryModel) { newlyAddedCategoryModel = new CategoryEntriesModel(q); connect(q, SIGNAL(entryDataUpdated(BookEntry*)), newlyAddedCategoryModel, SIGNAL(entryDataUpdated(BookEntry*))); connect(q, SIGNAL(entryRemoved(BookEntry*)), newlyAddedCategoryModel, SIGNAL(entryRemoved(BookEntry*))); emit q->newlyAddedCategoryModelChanged(); } if(!authorCategoryModel) { authorCategoryModel = new CategoryEntriesModel(q); connect(q, SIGNAL(entryDataUpdated(BookEntry*)), authorCategoryModel, SIGNAL(entryDataUpdated(BookEntry*))); connect(q, SIGNAL(entryRemoved(BookEntry*)), authorCategoryModel, SIGNAL(entryRemoved(BookEntry*))); emit q->authorCategoryModelChanged(); } if(!seriesCategoryModel) { seriesCategoryModel = new CategoryEntriesModel(q); connect(q, SIGNAL(entryDataUpdated(BookEntry*)), seriesCategoryModel, SIGNAL(entryDataUpdated(BookEntry*))); connect(q, SIGNAL(entryRemoved(BookEntry*)), seriesCategoryModel, SIGNAL(entryRemoved(BookEntry*))); emit q->seriesCategoryModelChanged(); } if(!folderCategoryModel) { folderCategoryModel = new CategoryEntriesModel(q); connect(q, SIGNAL(entryDataUpdated(BookEntry*)), folderCategoryModel, SIGNAL(entryDataUpdated(BookEntry*))); connect(q, SIGNAL(entryRemoved(BookEntry*)), folderCategoryModel, SIGNAL(entryRemoved(BookEntry*))); emit q->folderCategoryModel(); } } void addEntry(BookListModel* q, BookEntry* entry) { 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); folderCategoryModel->append(entry); } void loadCache(BookListModel* q) { QList entries = db->loadEntries(); if(entries.count() > 0) { initializeSubModels(q); } int i = 0; foreach(BookEntry* entry, entries) { addEntry(q, entry); if(++i % 100 == 0) { emit q->countChanged(); qApp->processEvents(); } } cacheLoaded = true; emit q->cacheLoadedChanged(); } }; BookListModel::BookListModel(QObject* parent) : CategoryEntriesModel(parent) , d(new Private) { } BookListModel::~BookListModel() { delete d; } void BookListModel::componentComplete() { QTimer::singleShot(0, this, [this](){ d->loadCache(this); }); } bool BookListModel::cacheLoaded() const { return d->cacheLoaded; } void BookListModel::setContentModel(QObject* newModel) { if(d->contentModel) { d->contentModel->disconnect(this); } d->contentModel = qobject_cast(newModel); if(d->contentModel) { connect(d->contentModel, SIGNAL(rowsInserted(QModelIndex,int,int)), this, SLOT(contentModelItemsInserted(QModelIndex,int, int))); } emit contentModelChanged(); } QObject * BookListModel::contentModel() const { return d->contentModel; } void BookListModel::contentModelItemsInserted(QModelIndex index, int first, int last) { d->initializeSubModels(this); int newRow = d->entries.count(); beginInsertRows(QModelIndex(), newRow, newRow + (last - first)); int role = d->contentModel->roleNames().key("filePath"); for(int i = first; i < last + 1; ++i) { QVariant filePath = d->contentModel->data(d->contentModel->index(first, 0, index), role); BookEntry* entry = new BookEntry(); entry->filename = filePath.toUrl().toLocalFile(); QStringList splitName = entry->filename.split("/"); 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); entry->title = fileinfo.completeBaseName(); if(entry->filename.toLower().endsWith("cbr")) { entry->thumbnail = QString("image://comiccover/").append(entry->filename); } #ifdef USE_PERUSE_PDFTHUMBNAILER else if(entry->filename.toLower().endsWith("pdf")) { entry->thumbnail = QString("image://pdfcover/").append(entry->filename); } #endif else { 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")) { entry->publisher = it.value().toString().trimmed(); } else if(it.key() == QLatin1String("created")) { entry->created = it.value().toDateTime(); } else if(it.key() == QLatin1String("currentPage")) { 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; QString mimetype = db.mimeTypeForFile(entry->filename).name(); if(mimetype == "application/x-cbz" || mimetype == "application/x-cbr" || mimetype == "application/vnd.comicbook+zip" || mimetype == "application/vnd.comicbook+rar") { ArchiveBookModel* bookModel = new ArchiveBookModel(this); bookModel->setFilename(entry->filename); 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(); bookModel->deleteLater(); } d->addEntry(this, entry); d->db->addEntry(entry); } endInsertRows(); emit countChanged(); qApp->processEvents(); } QObject * BookListModel::titleCategoryModel() const { return d->titleCategoryModel; } QObject * BookListModel::newlyAddedCategoryModel() const { return d->newlyAddedCategoryModel; } QObject * BookListModel::authorCategoryModel() const { return d->authorCategoryModel; } QObject * BookListModel::seriesCategoryModel() const { return d->seriesCategoryModel; } QObject * BookListModel::seriesModelForEntry(QString fileName) { Q_FOREACH(BookEntry* entry, d->entries) { if(entry->filename == fileName) { return d->seriesCategoryModel->leafModelForEntry(entry); } } return nullptr; } QObject * BookListModel::folderCategoryModel() const { return d->folderCategoryModel; } int BookListModel::count() const { return d->entries.count(); } void BookListModel::setBookData(QString fileName, QString property, QString value) { Q_FOREACH(BookEntry* entry, d->entries) { if(entry->filename == fileName) { if(property == "totalPages") { entry->totalPages = value.toInt(); } else if(property == "currentPage") { 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; } } } void BookListModel::removeBook(QString fileName, bool deleteFile) { if(deleteFile) { KIO::DeleteJob* job = KIO::del(QUrl::fromLocalFile(fileName), KIO::HideProgressInfo); job->start(); } Q_FOREACH(BookEntry* entry, d->entries) { if(entry->filename == fileName) { emit entryRemoved(entry); delete entry; break; } } } QStringList BookListModel::knownBookFiles() const { QStringList files; foreach(BookEntry* entry, d->entries) { files.append(entry->filename); } return files; } diff --git a/src/qtquick/CategoryEntriesModel.cpp b/src/qtquick/CategoryEntriesModel.cpp index e6c6d6f..d892de7 100644 --- a/src/qtquick/CategoryEntriesModel.cpp +++ b/src/qtquick/CategoryEntriesModel.cpp @@ -1,393 +1,420 @@ /* * 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 "CategoryEntriesModel.h" #include "PropertyContainer.h" #include #include #include #include class CategoryEntriesModel::Private { public: Private(CategoryEntriesModel* qq) : q(qq) {}; ~Private() { // No deleting the entries - this is done by the master BookListModel already, so do that at your own risk } CategoryEntriesModel* q; QString name; QList entries; QList categoryModels; QObject* wrapBookEntry(const BookEntry* entry) { PropertyContainer* obj = new PropertyContainer("book", q); obj->setProperty("author", entry->author); obj->setProperty("currentPage", QString::number(entry->currentPage)); obj->setProperty("filename", entry->filename); obj->setProperty("filetitle", entry->filetitle); obj->setProperty("created", entry->created); obj->setProperty("lastOpenedTime", entry->lastOpenedTime); obj->setProperty("publisher", entry->publisher); obj->setProperty("series", entry->series); 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; } }; CategoryEntriesModel::CategoryEntriesModel(QObject* parent) : QAbstractListModel(parent) , d(new Private(this)) { connect(this, SIGNAL(entryDataUpdated(BookEntry*)), this, SLOT(entryDataChanged(BookEntry*))); connect(this, SIGNAL(entryRemoved(BookEntry*)), this, SLOT(entryRemove(BookEntry*))); } CategoryEntriesModel::~CategoryEntriesModel() { delete d; } QHash CategoryEntriesModel::roleNames() const { QHash roles; roles[FilenameRole] = "filename"; roles[FiletitleRole] = "filetitle"; roles[TitleRole] = "title"; roles[SeriesRole] = "series"; roles[AuthorRole] = "author"; roles[PublisherRole] = "publisher"; roles[CreatedRole] = "created"; roles[LastOpenedTimeRole] = "lastOpenedTime"; roles[TotalPagesRole] = "totalPages"; roles[CurrentPageRole] = "currentPage"; roles[CategoryEntriesModelRole] = "categoryEntriesModel"; roles[CategoryEntryCountRole] = "categoryEntriesCount"; roles[ThumbnailRole] = "thumbnail"; + roles[DescriptionRole] = "description"; + roles[CommentRole] = "comment"; + roles[TagsRole] = "tags"; + roles[RatingRole] = "rating"; return roles; } QVariant CategoryEntriesModel::data(const QModelIndex& index, int role) const { QVariant result; if(index.isValid() && index.row() > -1) { if(index.row() < d->categoryModels.count()) { CategoryEntriesModel* model = d->categoryModels[index.row()]; switch(role) { case Qt::DisplayRole: case TitleRole: result.setValue(model->name()); break; case CategoryEntryCountRole: result.setValue(model->bookCount()); break; case CategoryEntriesModelRole: result.setValue(model); break; default: result.setValue(QString("Unknown role")); break; } } else { const BookEntry* entry = d->entries[index.row() - d->categoryModels.count()]; switch(role) { case Qt::DisplayRole: case FilenameRole: result.setValue(entry->filename); break; case FiletitleRole: result.setValue(entry->filetitle); break; case TitleRole: result.setValue(entry->title); break; case SeriesRole: result.setValue(entry->series); break; case AuthorRole: result.setValue(entry->author); break; case PublisherRole: result.setValue(entry->publisher); break; case CreatedRole: result.setValue(entry->created); break; case LastOpenedTimeRole: result.setValue(entry->lastOpenedTime); break; case TotalPagesRole: result.setValue(entry->totalPages); break; case CurrentPageRole: result.setValue(entry->currentPage); break; case CategoryEntriesModelRole: // Nothing, if we're not equipped with one such... break; case CategoryEntryCountRole: result.setValue(0); break; 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; } } } return result; } int CategoryEntriesModel::rowCount(const QModelIndex& parent) const { if(parent.isValid()) return 0; return d->categoryModels.count() + d->entries.count(); } void CategoryEntriesModel::append(BookEntry* entry, Roles compareRole) { int insertionIndex = 0; for(; insertionIndex < d->entries.count(); ++insertionIndex) { if(compareRole == CreatedRole) { if(entry->created <= d->entries.at(insertionIndex)->created) { continue; } break; } else { if(QString::localeAwareCompare(d->entries.at(insertionIndex)->title, entry->title) > 0) { break; } } } beginInsertRows(QModelIndex(), insertionIndex, insertionIndex); d->entries.insert(insertionIndex, entry); endInsertRows(); } QString CategoryEntriesModel::name() const { return d->name; } void CategoryEntriesModel::setName(const QString& newName) { d->name = newName; } QObject * CategoryEntriesModel::leafModelForEntry(BookEntry* entry) { QObject* model(nullptr); if(d->categoryModels.count() == 0) { if(d->entries.contains(entry)) { model = this; } } else { Q_FOREACH(CategoryEntriesModel* testModel, d->categoryModels) { model = testModel->leafModelForEntry(entry); if(model) { break; } } } return model; } void CategoryEntriesModel::addCategoryEntry(const QString& categoryName, BookEntry* entry) { if(categoryName.length() > 0) { QStringList splitName = categoryName.split("/"); // qDebug() << "Parsing" << categoryName; QString nextCategory = splitName.takeFirst(); CategoryEntriesModel* categoryModel = nullptr; Q_FOREACH(CategoryEntriesModel* existingModel, d->categoryModels) { if(existingModel->name() == nextCategory) { categoryModel = existingModel; break; } } if(!categoryModel) { categoryModel = new CategoryEntriesModel(this); connect(this, SIGNAL(entryDataUpdated(BookEntry*)), categoryModel, SIGNAL(entryDataUpdated(BookEntry*))); connect(this, SIGNAL(entryRemoved(BookEntry*)), categoryModel, SIGNAL(entryRemoved(BookEntry*))); categoryModel->setName(nextCategory); int insertionIndex = 0; for(; insertionIndex < d->categoryModels.count(); ++insertionIndex) { if(QString::localeAwareCompare(d->categoryModels.at(insertionIndex)->name(), categoryModel->name()) > 0) { break; } } beginInsertRows(QModelIndex(), insertionIndex, insertionIndex); d->categoryModels.insert(insertionIndex, categoryModel); endInsertRows(); } categoryModel->append(entry); categoryModel->addCategoryEntry(splitName.join("/"), entry); } } QObject* CategoryEntriesModel::get(int index) { BookEntry* entry = new BookEntry(); bool deleteEntry = true; if(index > -1 && index < d->entries.count()) { entry = d->entries.at(index); deleteEntry = false; } QObject* obj = d->wrapBookEntry(entry); if(deleteEntry) { delete entry; } return obj; } int CategoryEntriesModel::indexOfFile(QString filename) { int index = -1, i = 0; if(QFile::exists(filename)) { Q_FOREACH(BookEntry* entry, d->entries) { if(entry->filename == filename) { index = i; break; } ++i; } } return index; } bool CategoryEntriesModel::indexIsBook(int index) { if(index < d->categoryModels.count() || index >= rowCount()) { return false; } return true; } int CategoryEntriesModel::bookCount() const { return d->entries.count(); } QObject* CategoryEntriesModel::getEntry(int index) { PropertyContainer* obj = new PropertyContainer("book", this); if(index < 0 && index > rowCount() -1) { // don't be a silly person, you can't get a nothing... } else if(index > d->categoryModels.count() - 1) { // This is a book - get a book! obj = qobject_cast(get(index - d->categoryModels.count())); } else { CategoryEntriesModel* catEntry = d->categoryModels.at(index); obj->setProperty("title", catEntry->name()); obj->setProperty("categoryEntriesCount", catEntry->bookCount()); obj->setProperty("entriesModel", QVariant::fromValue(catEntry)); } return obj; } QObject* CategoryEntriesModel::bookFromFile(QString filename) { PropertyContainer* obj = qobject_cast(get(indexOfFile(filename))); if(obj->property("filename").toString().isEmpty()) { if(QFileInfo::exists(filename)) { QFileInfo info(filename); obj->setProperty("title", info.completeBaseName()); obj->setProperty("created", info.created()); KFileMetaData::UserMetaData data(filename); if (data.hasAttribute("peruse.currentPage")) { int currentPage = data.attribute("peruse.currentPage").toInt(); obj->setProperty("currentPage", QVariant::fromValue(currentPage)); } if (data.hasAttribute("peruse.totalPages")) { 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; if(filename.toLower().endsWith("cbr")) { thumbnail = QString("image://comiccover/").append(filename); } #ifdef USE_PERUSE_PDFTHUMBNAILER else if(filename.toLower().endsWith("pdf")) { thumbnail = QString("image://pdfcover/").append(filename); } #endif else { thumbnail = QString("image://preview/").append(filename); } obj->setProperty("thumbnail", thumbnail); } } return obj; } void CategoryEntriesModel::entryDataChanged(BookEntry* entry) { int entryIndex = d->entries.indexOf(entry) + d->categoryModels.count(); QModelIndex changed = index(entryIndex); dataChanged(changed, changed); } void CategoryEntriesModel::entryRemove(BookEntry* entry) { int listIndex = d->entries.indexOf(entry); if(listIndex > -1) { int entryIndex = listIndex + d->categoryModels.count(); beginRemoveRows(QModelIndex(), entryIndex, entryIndex); d->entries.removeAll(entry); endRemoveRows(); } } diff --git a/src/qtquick/CategoryEntriesModel.h b/src/qtquick/CategoryEntriesModel.h index cf84e2e..6d7697d 100644 --- a/src/qtquick/CategoryEntriesModel.h +++ b/src/qtquick/CategoryEntriesModel.h @@ -1,199 +1,207 @@ /* * 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 CATEGORYENTRIESMODEL_H #define CATEGORYENTRIESMODEL_H #include #include class CategoryEntriesModel; /** * \brief A struct for an Entry to the Book Database. */ struct BookEntry { BookEntry() : totalPages(0) , currentPage(0) {} 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; }; /** * \brief Model to handle the filter categories. * * This model in specific handles which categories there are * and which books are assigned to a category, if so, which. * * Used to handle sorting by author, title and so forth. * Is extended by BookListModel. * * categories and book entries are both in the same model * because there can be books that are not assigned categories. * Similarly, categories can contain categories, like in the case * of folder category. */ class CategoryEntriesModel : public QAbstractListModel { Q_OBJECT public: explicit CategoryEntriesModel(QObject* parent = nullptr); ~CategoryEntriesModel() override; /** * \brief Extra roles for the book entry access. */ enum Roles { FilenameRole = Qt::UserRole + 1, FiletitleRole, TitleRole, SeriesRole, AuthorRole, PublisherRole, CreatedRole, LastOpenedTimeRole, TotalPagesRole, CurrentPageRole, CategoryEntriesModelRole, CategoryEntryCountRole, - ThumbnailRole + ThumbnailRole, + DescriptionRole, + CommentRole, + TagsRole, + RatingRole }; /** * @returns names for the extra roles defined. */ QHash roleNames() const override; /** * \brief Access the data inside the CategoryEntriesModel. * @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 book entry's data. */ QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; /** * @param parent The QModel index of the parent. This only counts for * tree like page structures, and thus defaults to a freshly constructed * QModelIndex. A wellformed QModelIndex will cause this function to return 0 * @returns the number of total rows(bookentries and categories) there are. */ int rowCount(const QModelIndex& parent = QModelIndex()) const override; /** * \brief Add a book entry to the CategoryEntriesModel. * * @param entry The BookEntry to add. * @param compareRole The role that determines the data to sort the entry into. * Defaults to the Book title. */ void append(BookEntry* entry, Roles compareRole = TitleRole); /** * \brief Add a book entry to a category. * * This also adds it to the model's list of entries. */ void addCategoryEntry(const QString& categoryName, BookEntry* entry); /** * @param index an integer index pointing at the desired book. * @returns a QObject wrapper around a BookEntry struct for the given index. */ Q_INVOKABLE QObject* get(int index); /** * TODO: This is backwards... need to fox this to make get return the actual thing, not just a book, and create a getter for books... * @return an entry object. This can be either a category or a book. * @param index the index of the object. */ Q_INVOKABLE QObject* getEntry(int index); /** * @return an entry object for the given filename. Used to get the recently * read books. * @param filename the filename associated with an entry object. */ Q_INVOKABLE QObject* bookFromFile(QString filename); /** * @return an entry index for the given filename. * @param filename the filename associated with an entry object. */ Q_INVOKABLE int indexOfFile(QString filename); /** * @return whether the entry is a bookentry or a category entry. * @param index the index of the entry. */ Q_INVOKABLE bool indexIsBook(int index); /** * @return an integer with the total books in the model. */ int bookCount() const; /** * \brief Fires when a book entry is updated. * @param entry The updated entry * * Used in the BookListModel::setBookData() */ Q_SIGNAL void entryDataUpdated(BookEntry* entry); /** * \brief set a book entry as changed. * @param entry The changed entry. */ Q_SLOT void entryDataChanged(BookEntry* entry); /** * \brief Fires when a book entry is removed. * @param entry The removed entry */ Q_SIGNAL void entryRemoved(BookEntry* entry); /** * \brief Remove a book entry. * @param entry The entry to remove. */ Q_SLOT void entryRemove(BookEntry* entry); // This will iterate over all sub-models and find the model which contains the entry, or null if not found QObject* leafModelForEntry(BookEntry* entry); protected: /** * @return the name of the model. */ QString name() const; /** * \brief set the name of the model. * @param newName QString with the name. */ void setName(const QString& newName); private: class Private; Private* d; }; #endif//CATEGORYENTRIESMODEL_H diff --git a/src/qtquick/PeruseConfig.cpp b/src/qtquick/PeruseConfig.cpp index 7a722a6..a9e2caa 100644 --- a/src/qtquick/PeruseConfig.cpp +++ b/src/qtquick/PeruseConfig.cpp @@ -1,164 +1,188 @@ /* * Copyright (C) 2016 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 "PeruseConfig.h" #include #include #include #include #include class PeruseConfig::Private { public: Private() : config("peruserc") {}; KConfig config; }; PeruseConfig::PeruseConfig(QObject* parent) : QObject(parent) , d(new Private) { QStringList locations = d->config.group("general").readEntry("book locations", QStringList()); if(locations.count() < 1) { locations = QStandardPaths::standardLocations(QStandardPaths::DocumentsLocation); locations << QStandardPaths::standardLocations(QStandardPaths::DownloadLocation); locations << QString("%1/comics").arg(QStandardPaths::standardLocations(QStandardPaths::GenericDataLocation).first()); d->config.group("general").writeEntry("book locations", locations); d->config.sync(); } } PeruseConfig::~PeruseConfig() { delete d; } void PeruseConfig::bookOpened(QString path) { QStringList recent = recentlyOpened(); int i = recent.indexOf(path); if(i == 0) { // This is already first, don't do work we don't need to, because that's just silly return; } else { recent.removeAll(path); recent.prepend(path); } d->config.group("general").writeEntry("recently opened", recent); d->config.sync(); emit recentlyOpenedChanged(); } QStringList PeruseConfig::recentlyOpened() const { QStringList recent = d->config.group("general").readEntry("recently opened", QStringList()); QStringList actualRecent; while(recent.count() > 0) { QString current = recent.takeFirst(); if(QFile::exists(current)) { actualRecent.append(current); } } return actualRecent; } void PeruseConfig::addBookLocation(const QString& location) { if(location.startsWith("file://")) { #ifdef Q_OS_WIN QString newLocation = location.mid(8); #else QString newLocation = location.mid(7); #endif QStringList locations = bookLocations(); // First, get rid of all the entries which start with the newly added location, because that's silly QStringList newLocations; bool alreadyInThere = false; Q_FOREACH(QString entry, locations) { if(!entry.startsWith(newLocation)) { newLocations.append(entry); } if(newLocation.startsWith(entry)) { alreadyInThere = true; } } if(alreadyInThere) { // Don't be silly, don't add a new location if it's already covered by something more high level... emit showMessage("Attempted to add a new location to the list of search folders which is a sub-folder to something already in the list."); return; } newLocations.append(newLocation); d->config.group("general").writeEntry("book locations", newLocations); d->config.sync(); emit bookLocationsChanged(); } } void PeruseConfig::removeBookLocation(const QString& location) { QStringList locations = bookLocations(); locations.removeAll(location); d->config.group("general").writeEntry("book locations", locations); d->config.sync(); QTimer::singleShot(100, this, SIGNAL(bookLocationsChanged())); } QStringList PeruseConfig::bookLocations() const { QStringList locations = d->config.group("general").readEntry("book locations", QStringList()); return locations; } QString PeruseConfig::newstuffLocation() const { QString knsrc = QStandardPaths::locate(QStandardPaths::GenericConfigLocation, "peruse.knsrc"); if(qEnvironmentVariableIsSet("APPDIR")) { // Because appimage install happens into /app/usr... knsrc = knsrc.prepend("/usr").prepend(qgetenv("APPDIR")); } return knsrc; } QString PeruseConfig::homeDir() const { return QStandardPaths::standardLocations(QStandardPaths::HomeLocation).first(); } 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; } diff --git a/src/qtquick/PeruseConfig.h b/src/qtquick/PeruseConfig.h index 7724272..5c346ba 100644 --- a/src/qtquick/PeruseConfig.h +++ b/src/qtquick/PeruseConfig.h @@ -1,121 +1,128 @@ /* * Copyright (C) 2016 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 PERUSECONFIG_H #define PERUSECONFIG_H #include /** * \brief Class to handle reading and writing the configuration. * * Peruse will store the data in 'peruserc' at the QStandardPath * for configuration. */ class PeruseConfig : public QObject { Q_OBJECT Q_ENUMS(ZoomMode) /** * \brief recently opened represents the recently opened books. */ Q_PROPERTY(QStringList recentlyOpened READ recentlyOpened NOTIFY recentlyOpenedChanged) /** * \brief book locations is where Peruse will look for books, whether they have been removed, added, etc. */ Q_PROPERTY(QStringList bookLocations READ bookLocations NOTIFY bookLocationsChanged) /** * \brief new stuff location is for the location of the KNewStuff config for the GHNS protocol. */ Q_PROPERTY(QString newstuffLocation READ newstuffLocation NOTIFY newstuffLocationChanged) public: /** * \brief Enum holding the preferred zoom mode. */ enum ZoomMode { ZoomFull = 0, ZoomFitWidth = 1, ZoomFitHeight = 2 }; explicit PeruseConfig(QObject* parent = nullptr); ~PeruseConfig() override; /** * \brief Add a book to the recently opened list. * @param path the path/filename of the newly opened book. */ Q_INVOKABLE void bookOpened(QString path); /** * @return a list of recently opened files. */ QStringList recentlyOpened() const; /** * \brief Fires when the list of recently opened files has changed. */ Q_SIGNAL void recentlyOpenedChanged(); /** * \brief Add a folder to the book locations. * @param location path to the folder to add. */ Q_INVOKABLE void addBookLocation(const QString& location); /** * \brief Remove a folder from the book locations. * @param location path of the folder to remove. */ Q_INVOKABLE void removeBookLocation(const QString& location); /** * @return a QStringList with paths to all the folders to check for books. */ QStringList bookLocations() const; /** * \brief Fires when the book locations to check have changed. */ Q_SIGNAL void bookLocationsChanged(); /** * \brief Holds url to the peruse's KNewStuff configuration fle, to * make it easy to retrieve. */ QString newstuffLocation() const; /** * \brief Fires when the location to the KNewStuff config is changed. */ Q_SIGNAL void newstuffLocationChanged(); /** * \brief Fires when there is an config error message to show. * @param message The Error message to show. */ Q_SIGNAL void showMessage(QString message); // This should go somewhere more sensible, really... like a static on Qt. or something :P Q_INVOKABLE QString homeDir() const; /** * 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; }; #endif//PERUSECONFIG_H